Custom RPG Shop System

This bundle is marked as pending. It has not been reviewed by a staff member yet.

Overview

Source Code

API

Example

Changelog



RpgMerchantShop

A configurable RPG shop interface for Warcraft III maps.



Description

RpgMerchantShop is a shop system for RPG maps that need a more flexible merchant interface than the default Warcraft III shop.

The system supports custom categories, custom rarities, searchable item catalogs, item sorting, quantity-based buying and selling, owned-count tracking, automatic gold display updates, and per-shop visual configuration.

The catalog is data-driven. The system does not hardcode shop items, categories, or rarities.



Features

• Custom categories
• Custom rarities
• Real Warcraft III item entries
• Virtual goods for materials, tokens, currencies, keys, and quest resources
• Search by item name, category name, or rarity name
• Sort by rarity, price, name, or type
• Ascending and descending sort order
• Quantity controls
• Max quantity button
• Buy and sell buttons
• Owned-count display
• Automatic gold display refresh
• In-window system messages
• Configurable font and textures per shop
• Configurable visible item slot count
• Paged category list
• Paged item list
• No buyer hero requirement



Notes

• Real item entries create hidden stored item handles internally.
• Virtual goods are count-only entries and do not create item handles.
• Requires RpgMerchantShop.toc imported with no filepath changes.

vJASS

WurstScript

Lua


JASS:
library RpgMerchantShopSystem

/*------------------------------------------------------------------------------------------------------------------------------------------------------------
*
*    -------------------
*    | RpgMerchantShop |
*    -------------------
*
*    RpgMerchantShop class
*    --------------------
*        A configurable popup shop that displays a searchable, sortable catalog of goods.
*        The public API is intentionally limited to the methods listed below. Internal frame handles,
*        player state, catalog storage, sorting internals, and transaction bookkeeping are private.
*        The shop supports custom categories, custom rarities, real item rawcodes, virtual goods,
*        per-shop visuals, adjustable item-card counts, buy/sell transactions, and owned-count display.
*
*    RpgMerchantShop.create(string titleText) -> RpgMerchantShop
*        - Creates a new shop window with the specified title.
*        - The window starts hidden. Use shop.openFor(player) to display it.
*
*    shop.addCategory(string label) -> integer
*        - Registers a category and returns the category id.
*        - Store the returned id and pass it to addItem or addVirtualItem.
*        - Category order controls the Type sort order.
*
*    shop.addRarity(string label, string colorPrefix) -> integer
*        - Registers a rarity and returns the rarity id.
*        - colorPrefix should be a Warcraft color prefix such as "|cff66aaff".
*        - Rarity order controls the Rarity sort order. Add low rarities first and high rarities later when you want descending rarity to show the best items first.
*
*    shop.addItem(string name, string icon, integer categoryId, integer rarityId, integer price, integer attack, integer defense, integer rawId, string desc) -> integer
*        - Adds a real Warcraft item-backed shop entry and returns its shop item id.
*        - rawId should use a single-quoted rawcode literal such as 'phea'. Do not use FourCC.
*        - Bought real items are created as hidden storage items near a map corner and tracked by count.
*        - Returns -1 if categoryId or rarityId was not registered on this shop.
*
*    shop.addVirtualItem(string name, string icon, integer categoryId, integer rarityId, integer price, integer attack, integer defense, string desc) -> integer
*        - Adds a non-rawcode good and returns its shop item id.
*        - Use this for tokens, crafting materials, keys, currencies, reputation goods, quest counters, or other abstract goods.
*        - Virtual goods are tracked by count only.
*        - Returns -1 if categoryId or rarityId was not registered on this shop.
*
*    shop.clearCatalog()
*        - Removes all shop items and owned counts.
*        - Categories and rarities remain registered.
*
*    shop.clearTaxonomyAndCatalog()
*        - Removes all shop items, owned counts, categories, and rarities.
*        - Use this when fully rebuilding a shop from scratch.
*
*    shop.openFor(player p)
*        - Opens the shop for player p and refreshes its current state.
*
*    shop.closeFor(player p)
*        - Closes the shop for player p.
*
*    shop.toggleFor(player p)
*        - Opens the shop if it is hidden for player p, otherwise closes it.
*
*    shop.setSortByRarity(player p)
*        - Sets player p's current sort mode to rarity order.
*
*    shop.setSortByPrice(player p)
*        - Sets player p's current sort mode to price order.
*
*    shop.setSortByName(player p)
*        - Sets player p's current sort mode to name order.
*
*    shop.setSortByType(player p)
*        - Sets player p's current sort mode to category/type order.
*
*    shop.setSortAscending(player p)
*        - Sets player p's current sort direction to ascending.
*
*    shop.setSortDescending(player p)
*        - Sets player p's current sort direction to descending.
*
*    shop.setVisibleItemSlotCount(integer slotCount)
*        - Sets how many item cards are shown per page.
*        - Values below 1 are clamped to 1.
*        - Item cards reflow inside the content area so they do not cover the system text or page controls.
*
*    shop.getVisibleItemSlotCount() -> integer
*        - Returns the current item-card count for this shop.
*
*    shop.setShopFont(string fontPath)
*        - Changes the font used by this shop.
*
*    shop.setBlockerTexture(string texturePath)
*        - Changes the dark screen-blocker texture used behind this shop.
*
*    shop.setPanelTexture(string texturePath)
*        - Changes the main popup panel texture used by this shop.
*
*    shop.setTooltipTexture(string texturePath)
*        - Changes the item-card, details-panel, and smaller UI-panel texture used by this shop.
*
*    shop.setVisualStyle(string fontPath, string blockerPath, string panelPath, string tooltipPath)
*        - Changes the font, blocker texture, main panel texture, and tooltip/panel texture at once.
*
*    Built-in behavior
*    -----------------
*        - Search matches item names, category names, and rarity names.
*        - Category changes, sort changes, and search changes preserve the selected item unless the item no longer exists.
*        - Buy/sell feedback is shown inside the shop window.
*        - Gold text refreshes automatically while the shop is open.
*        - The quantity buttons, sort buttons, and transaction buttons use click guards to avoid duplicate click processing.
*        - The MAX quantity button chooses the highest useful amount based on owned count and/or affordable count, capped at 99.
*
*    Example
*    -------
*        local RpgMerchantShop shop = RpgMerchantShop.create("MERCHANT'S STORE")
*        local integer weapons = shop.addCategory("Weapons")
*        local integer potions = shop.addCategory("Potions")
*        local integer materials = shop.addCategory("Materials")
*        local integer common = shop.addRarity("COMMON", "|cffffffff")
*        local integer uncommon = shop.addRarity("UNCOMMON", "|cff66ff66")
*        local integer rare = shop.addRarity("RARE", "|cff66aaff")
*
*        call shop.setVisibleItemSlotCount(8)
*        call shop.addItem("Ironwood Bow", "ReplaceableTextures\\CommandButtons\\BTNImprovedBows.blp", weapons, uncommon, 2150, 64, 0, 'rat9', "A sturdy bow made from reinforced ironwood.")
*        call shop.addItem("Elixir of Superior Healing", "ReplaceableTextures\\CommandButtons\\BTNPotionRed.blp", potions, rare, 1250, 0, 0, 'phea', "Restores 750 Health.")
*        call shop.addVirtualItem("Soulstone Shard", "ReplaceableTextures\\CommandButtons\\BTNGem.blp", materials, common, 750, 0, 0, "Crafting material used to imbue powerful gear.")
*        call shop.setSortByRarity(Player(0))
*        call shop.setSortDescending(Player(0))
*        call shop.openFor(Player(0))
*
*------------------------------------------------------------------------------------------------------------------------------------------------------------*/

globals
    private constant integer ALL_CATEGORY_ID = -1
    private constant integer SORT_RARITY = 0
    private constant integer SORT_PRICE = 1
    private constant integer SORT_NAME = 2
    private constant integer SORT_TYPE = 3
    private constant integer SORT_ASCENDING = 0
    private constant integer SORT_DESCENDING = 1
    private timer RpgShopClock = CreateTimer()
    private integer RpgShopNextInstanceId = 0
    private real RpgShopBoundMinX = 0.0
    private real RpgShopBoundMinY = 0.0
    private real RpgShopBoundMaxX = 0.0
    private real RpgShopBoundMaxY = 0.0
endglobals

private function RpgShopGold takes player p returns integer
    return GetPlayerState(p, PLAYER_STATE_RESOURCE_GOLD)
endfunction

private function RpgShopAddGold takes player p, integer amount returns nothing
    call SetPlayerState(p, PLAYER_STATE_RESOURCE_GOLD, RpgShopGold(p) + amount)
endfunction

private function RpgShopSubGold takes player p, integer amount returns nothing
    call SetPlayerState(p, PLAYER_STATE_RESOURCE_GOLD, RpgShopGold(p) - amount)
endfunction

private function RpgShopFrameShowFor takes player p, framehandle f, boolean flag returns nothing
    if GetLocalPlayer() == p then
        call BlzFrameSetVisible(f, flag)
    endif
endfunction

private function RpgShopFrameEnableFor takes player p, framehandle f, boolean flag returns nothing
    if GetLocalPlayer() == p then
        call BlzFrameSetEnable(f, flag)
    endif
endfunction

private function RpgShopFrameShowEnableFor takes player p, framehandle f, boolean flag returns nothing
    if GetLocalPlayer() == p then
        call BlzFrameSetVisible(f, flag)
        call BlzFrameSetEnable(f, flag)
    endif
endfunction

private function RpgShopFrameTextFor takes player p, framehandle f, string text returns nothing
    if GetLocalPlayer() == p then
        call BlzFrameSetText(f, text)
    endif
endfunction

private function RpgShopFrameTextureFor takes player p, framehandle f, string texturePath returns nothing
    if GetLocalPlayer() == p then
        call BlzFrameSetTexture(f, texturePath, 0, true)
    endif
endfunction

private struct RpgShopCategoryData
    integer id
    string label
    thistype next
endstruct

private struct RpgShopRarityData
    integer id
    string label
    string colorPrefix
    thistype next
endstruct

private struct RpgShopItemData
    integer id
    string name
    string icon
    string desc
    integer categoryId
    integer rarityId
    integer price
    integer attack
    integer defense
    integer rawId
    thistype next
endstruct

private struct RpgShopItemNode
    RpgShopItemData data
    thistype next
endstruct

private struct RpgShopOwnedEntry
    integer itemId
    integer count
    item hiddenItem
    thistype next
endstruct

private struct RpgShopPlayerState
    player owner
    boolean isOpen
    integer pid
    integer selectedCategory
    integer categoryPage
    integer selectedItem
    integer sortMode
    integer sortOrder
    integer quantity
    integer currentPage
    string searchText
    string systemMessage
    real lastQuantityClick
    real lastTradeClick
    real lastSortClick
    real lastSortOrderClick
    RpgShopOwnedEntry ownedHead
    RpgShopOwnedEntry ownedTail
    thistype next
endstruct

private struct RpgShopCategoryButtonSlot
    integer index
    framehandle buttonFrame
    thistype next
endstruct

private struct RpgShopItemFrameSlot
    integer index
    framehandle backdrop
    framehandle icon
    framehandle text
    framehandle buttonFrame
    thistype next
endstruct

struct RpgMerchantShop
    private static thistype first = 0
    private static thistype last = 0

    private thistype nextShop
    private integer instanceId
    private integer nextCategoryId
    private integer nextRarityId
    private integer nextItemId
    private string framePrefix
    private string windowTitle
    private string shopFontPath
    private string blockerTexturePath
    private string panelTexturePath
    private string tooltipTexturePath
    private integer visibleItemSlotCount

    private RpgShopPlayerState playerHead
    private RpgShopPlayerState playerTail
    private RpgShopCategoryData categoryHead
    private RpgShopCategoryData categoryTail
    private RpgShopRarityData rarityHead
    private RpgShopRarityData rarityTail
    private RpgShopItemData itemHead
    private RpgShopItemData itemTail
    private RpgShopItemNode visibleHead
    private RpgShopItemNode visibleTail
    private RpgShopItemNode sortedHead
    private RpgShopItemNode sortedTail
    private integer visibleCount
    private integer sortedCount
    private RpgShopCategoryButtonSlot categoryButtonHead
    private RpgShopCategoryButtonSlot categoryButtonTail
    private integer categoryButtonSlotCount
    private RpgShopItemFrameSlot itemFrameHead
    private RpgShopItemFrameSlot itemFrameTail
    private integer itemFrameSlotCount

    private framehandle blocker
    private framehandle root
    private framehandle windowBackdrop
    private framehandle title
    private framehandle closeButton
    private framehandle searchBox
    private framehandle sortButton
    private framehandle sortOrderButton
    private framehandle goldText
    private framehandle systemText
    private framehandle detailsPanel
    private framehandle detailsIcon
    private framehandle detailsTitle
    private framehandle detailsBody
    private framehandle costText
    private framehandle ownedText
    private framehandle minusButton
    private framehandle plusButton
    private framehandle maxButton
    private framehandle quantityText
    private framehandle buyButton
    private framehandle sellButton
    private framehandle pageText
    private framehandle prevButton
    private framehandle nextButton
    private framehandle categoryPrevButton
    private framehandle categoryNextButton

    private static method timerNow takes nothing returns real
        return TimerGetElapsed(RpgShopClock)
    endmethod

    private static method registerClick takes framehandle f returns nothing
        local trigger t = CreateTrigger()
        call BlzTriggerRegisterFrameEvent(t, f, FRAMEEVENT_CONTROL_CLICK)
        call TriggerAddAction(t, function thistype.onFrameClick)
        set t = null
    endmethod

    private static method registerEditbox takes framehandle f returns nothing
        local trigger t = CreateTrigger()
        call BlzTriggerRegisterFrameEvent(t, f, FRAMEEVENT_EDITBOX_TEXT_CHANGED)
        call TriggerAddAction(t, function thistype.onEditboxChanged)
        set t = null
    endmethod

    private static method registerMouseDown takes framehandle f returns nothing
        local trigger t = CreateTrigger()
        call BlzTriggerRegisterFrameEvent(t, f, FRAMEEVENT_MOUSE_DOWN)
        call TriggerAddAction(t, function thistype.onMouseDown)
        set t = null
    endmethod

    static method create takes string titleText returns thistype
        local thistype this = thistype.allocate()
        set this.nextShop = 0
        set this.instanceId = RpgShopNextInstanceId
        set RpgShopNextInstanceId = RpgShopNextInstanceId + 1
        set this.nextCategoryId = 0
        set this.nextRarityId = 0
        set this.nextItemId = 0
        set this.framePrefix = "RpgShop" + I2S(this.instanceId) + "_"
        set this.windowTitle = titleText
        set this.shopFontPath = "Fonts\\FRIZQT__.TTF"
        set this.blockerTexturePath = "UI\\Widgets\\EscMenu\\Human\\blank-background.blp"
        set this.panelTexturePath = "UI\\Widgets\\EscMenu\\Human\\human-options-menu-background.blp"
        set this.tooltipTexturePath = "UI\\Widgets\\ToolTips\\Human\\human-tooltip-background.blp"
        set this.visibleItemSlotCount = 8
        set this.playerHead = 0
        set this.playerTail = 0
        set this.categoryHead = 0
        set this.categoryTail = 0
        set this.rarityHead = 0
        set this.rarityTail = 0
        set this.itemHead = 0
        set this.itemTail = 0
        set this.visibleHead = 0
        set this.visibleTail = 0
        set this.sortedHead = 0
        set this.sortedTail = 0
        set this.visibleCount = 0
        set this.sortedCount = 0
        set this.categoryButtonHead = 0
        set this.categoryButtonTail = 0
        set this.categoryButtonSlotCount = 0
        set this.itemFrameHead = 0
        set this.itemFrameTail = 0
        set this.itemFrameSlotCount = 0
        if thistype.first == 0 then
            set thistype.first = this
        else
            set thistype.last.nextShop = this
        endif
        set thistype.last = this
        call this.createFrames()
        call this.bindEvents()
        call BlzFrameSetVisible(this.root, false)
        call BlzFrameSetEnable(this.root, false)
        call BlzFrameSetVisible(this.blocker, false)
        call BlzFrameSetEnable(this.blocker, false)
        return this
    endmethod

    method openFor takes player p returns nothing
        local RpgShopPlayerState state = this.getState(p)
        set state.currentPage = 0
        set state.quantity = 1
        set state.systemMessage = "|cffaaaaaaSelect an item, then press BUY or SELL.|r"
        set state.isOpen = true
        call this.refreshFor(p)
        call RpgShopFrameShowEnableFor(p, this.root, true)
        call RpgShopFrameShowEnableFor(p, this.blocker, true)
        call RpgShopFrameShowEnableFor(p, this.searchBox, true)
    endmethod

    method closeFor takes player p returns nothing
        local RpgShopPlayerState state = this.getState(p)
        set state.isOpen = false
        call RpgShopFrameShowEnableFor(p, this.searchBox, false)
        call RpgShopFrameShowEnableFor(p, this.root, false)
        call RpgShopFrameShowEnableFor(p, this.blocker, false)
    endmethod

    method toggleFor takes player p returns nothing
        local RpgShopPlayerState state = this.getState(p)
        if state.isOpen then
            call this.closeFor(p)
        else
            call this.openFor(p)
        endif
    endmethod

    method setShopFont takes string fontPath returns nothing
        set this.shopFontPath = fontPath
        call this.applyShopFont()
    endmethod

    method setBlockerTexture takes string texturePath returns nothing
        set this.blockerTexturePath = texturePath
        call BlzFrameSetTexture(this.blocker, this.blockerTexturePath, 0, true)
    endmethod

    method setPanelTexture takes string texturePath returns nothing
        set this.panelTexturePath = texturePath
        call BlzFrameSetTexture(this.root, this.panelTexturePath, 0, true)
        call BlzFrameSetTexture(this.windowBackdrop, this.panelTexturePath, 0, true)
    endmethod

    method setTooltipTexture takes string texturePath returns nothing
        set this.tooltipTexturePath = texturePath
        call this.applyTooltipTexture()
    endmethod

    method setVisualStyle takes string fontPath, string blockerPath, string panelPath, string tooltipPath returns nothing
        set this.shopFontPath = fontPath
        set this.blockerTexturePath = blockerPath
        set this.panelTexturePath = panelPath
        set this.tooltipTexturePath = tooltipPath
        call BlzFrameSetTexture(this.blocker, this.blockerTexturePath, 0, true)
        call BlzFrameSetTexture(this.root, this.panelTexturePath, 0, true)
        call BlzFrameSetTexture(this.windowBackdrop, this.panelTexturePath, 0, true)
        call this.applyTooltipTexture()
        call this.applyShopFont()
    endmethod

    method setVisibleItemSlotCount takes integer slotCount returns nothing
        local integer newSlotCount = slotCount
        if newSlotCount < 1 then
            set newSlotCount = 1
        endif
        set this.visibleItemSlotCount = newSlotCount
        call this.ensureItemCardFrames()
        call this.refreshOpenPlayers()
    endmethod

    method getVisibleItemSlotCount takes nothing returns integer
        return this.visibleItemSlotCount
    endmethod

    private method applySortOrder takes player p, integer order returns nothing
        local RpgShopPlayerState state = this.getState(p)
        if order == SORT_ASCENDING then
            set state.sortOrder = SORT_ASCENDING
        else
            set state.sortOrder = SORT_DESCENDING
        endif
        call this.refreshFor(p)
    endmethod

    private method applySortMode takes player p, integer mode returns nothing
        if mode < SORT_RARITY or mode > SORT_TYPE then
            return
        endif
        set this.getState(p).sortMode = mode
        call this.refreshFor(p)
    endmethod

    method setSortByRarity takes player p returns nothing
        call this.applySortMode(p, SORT_RARITY)
    endmethod

    method setSortByPrice takes player p returns nothing
        call this.applySortMode(p, SORT_PRICE)
    endmethod

    method setSortByName takes player p returns nothing
        call this.applySortMode(p, SORT_NAME)
    endmethod

    method setSortByType takes player p returns nothing
        call this.applySortMode(p, SORT_TYPE)
    endmethod

    method setSortAscending takes player p returns nothing
        call this.applySortOrder(p, SORT_ASCENDING)
    endmethod

    method setSortDescending takes player p returns nothing
        call this.applySortOrder(p, SORT_DESCENDING)
    endmethod

    method addCategory takes string label returns integer
        local RpgShopCategoryData newCategory = RpgShopCategoryData.create()
        set newCategory.id = this.nextCategoryId
        set newCategory.label = label
        set newCategory.next = 0
        set this.nextCategoryId = this.nextCategoryId + 1
        if this.categoryHead == 0 then
            set this.categoryHead = newCategory
        else
            set this.categoryTail.next = newCategory
        endif
        set this.categoryTail = newCategory
        call this.refreshOpenPlayers()
        return newCategory.id
    endmethod

    method addRarity takes string label, string colorPrefix returns integer
        local RpgShopRarityData newRarity = RpgShopRarityData.create()
        set newRarity.id = this.nextRarityId
        set newRarity.label = label
        set newRarity.colorPrefix = colorPrefix
        set newRarity.next = 0
        set this.nextRarityId = this.nextRarityId + 1
        if this.rarityHead == 0 then
            set this.rarityHead = newRarity
        else
            set this.rarityTail.next = newRarity
        endif
        set this.rarityTail = newRarity
        call this.refreshOpenPlayers()
        return newRarity.id
    endmethod

    method addItem takes string name, string icon, integer categoryId, integer rarityId, integer price, integer attack, integer defense, integer rawId, string desc returns integer
        local RpgShopItemData newItem
        if (not this.validCategory(categoryId)) or (not this.validRarity(rarityId)) then
            return -1
        endif
        set newItem = RpgShopItemData.create()
        set newItem.id = this.nextItemId
        set newItem.name = name
        set newItem.icon = icon
        set newItem.categoryId = categoryId
        set newItem.rarityId = rarityId
        set newItem.price = price
        set newItem.attack = attack
        set newItem.defense = defense
        set newItem.rawId = rawId
        set newItem.desc = desc
        set newItem.next = 0
        set this.nextItemId = this.nextItemId + 1
        if this.itemHead == 0 then
            set this.itemHead = newItem
        else
            set this.itemTail.next = newItem
        endif
        set this.itemTail = newItem
        call this.refreshOpenPlayers()
        return newItem.id
    endmethod

    method addVirtualItem takes string name, string icon, integer categoryId, integer rarityId, integer price, integer attack, integer defense, string desc returns integer
        return this.addItem(name, icon, categoryId, rarityId, price, attack, defense, 0, desc)
    endmethod

    method clearCatalog takes nothing returns nothing
        local RpgShopPlayerState state = this.playerHead
        local RpgShopOwnedEntry entry
        local RpgShopOwnedEntry nextEntry
        local RpgShopItemData selected = this.itemHead
        local RpgShopItemData nextSelected
        loop
            exitwhen state == 0
            set entry = state.ownedHead
            loop
                exitwhen entry == 0
                set nextEntry = entry.next
                if entry.hiddenItem != null then
                    call RemoveItem(entry.hiddenItem)
                    set entry.hiddenItem = null
                endif
                call entry.destroy()
                set entry = nextEntry
            endloop
            set state.ownedHead = 0
            set state.ownedTail = 0
            set state.selectedItem = -1
            set state.currentPage = 0
            set state.quantity = 1
            set state = state.next
        endloop
        loop
            exitwhen selected == 0
            set nextSelected = selected.next
            call selected.destroy()
            set selected = nextSelected
        endloop
        set this.itemHead = 0
        set this.itemTail = 0
        call this.clearVisibleLists()
        set this.nextItemId = 0
        call this.refreshOpenPlayers()
    endmethod

    method clearTaxonomyAndCatalog takes nothing returns nothing
        local RpgShopCategoryData category
        local RpgShopCategoryData nextCategory
        local RpgShopRarityData rarity
        local RpgShopRarityData nextRarity
        local RpgShopPlayerState state
        call this.clearCatalog()
        set category = this.categoryHead
        loop
            exitwhen category == 0
            set nextCategory = category.next
            call category.destroy()
            set category = nextCategory
        endloop
        set rarity = this.rarityHead
        loop
            exitwhen rarity == 0
            set nextRarity = rarity.next
            call rarity.destroy()
            set rarity = nextRarity
        endloop
        set this.categoryHead = 0
        set this.categoryTail = 0
        set this.rarityHead = 0
        set this.rarityTail = 0
        set this.nextCategoryId = 0
        set this.nextRarityId = 0
        set state = this.playerHead
        loop
            exitwhen state == 0
            set state.selectedCategory = ALL_CATEGORY_ID
            set state.categoryPage = 0
            set state = state.next
        endloop
        call this.refreshOpenPlayers()
    endmethod

    private method getState takes player p returns RpgShopPlayerState
        local integer pid = GetPlayerId(p)
        local RpgShopPlayerState state = this.playerHead
        local RpgShopPlayerState newState
        loop
            exitwhen state == 0
            if state.pid == pid then
                return state
            endif
            set state = state.next
        endloop
        set newState = RpgShopPlayerState.create()
        set newState.owner = p
        set newState.isOpen = false
        set newState.pid = pid
        set newState.selectedCategory = ALL_CATEGORY_ID
        set newState.categoryPage = 0
        set newState.selectedItem = -1
        set newState.sortMode = SORT_RARITY
        set newState.sortOrder = SORT_DESCENDING
        set newState.quantity = 1
        set newState.currentPage = 0
        set newState.searchText = ""
        set newState.systemMessage = "|cffaaaaaaSelect an item, then press BUY or SELL.|r"
        set newState.lastQuantityClick = -1000.0
        set newState.lastTradeClick = -1000.0
        set newState.lastSortClick = -1000.0
        set newState.lastSortOrderClick = -1000.0
        set newState.ownedHead = 0
        set newState.ownedTail = 0
        set newState.next = 0
        if this.playerHead == 0 then
            set this.playerHead = newState
        else
            set this.playerTail.next = newState
        endif
        set this.playerTail = newState
        return newState
    endmethod

    private method createFrames takes nothing returns nothing
        local framehandle gameUi = BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0)
        set this.blocker = BlzCreateFrameByType("BACKDROP", this.framePrefix + "Blocker", gameUi, "", 0)
        call BlzFrameSetSize(this.blocker, 0.80, 0.60)
        call BlzFrameSetAbsPoint(this.blocker, FRAMEPOINT_CENTER, 0.40, 0.30)
        call BlzFrameSetTexture(this.blocker, this.blockerTexturePath, 0, true)
        call BlzFrameSetAlpha(this.blocker, 120)
        call BlzFrameSetLevel(this.blocker, 1)

        set this.root = BlzCreateFrameByType("BACKDROP", this.framePrefix + "Root", gameUi, "", 0)
        call BlzFrameSetSize(this.root, 0.72, 0.50)
        call BlzFrameSetAbsPoint(this.root, FRAMEPOINT_CENTER, 0.40, 0.30)
        call BlzFrameSetTexture(this.root, this.panelTexturePath, 0, true)
        call BlzFrameSetAlpha(this.root, 245)
        call BlzFrameSetLevel(this.root, 5)

        set this.windowBackdrop = BlzCreateFrameByType("BACKDROP", this.framePrefix + "WindowBackdrop", this.root, "", 0)
        call BlzFrameSetSize(this.windowBackdrop, 0.690, 0.465)
        call BlzFrameSetPoint(this.windowBackdrop, FRAMEPOINT_CENTER, this.root, FRAMEPOINT_CENTER, 0.0, -0.006)
        call BlzFrameSetTexture(this.windowBackdrop, this.panelTexturePath, 0, true)
        call BlzFrameSetAlpha(this.windowBackdrop, 135)
        call BlzFrameSetLevel(this.windowBackdrop, 0)
        call BlzFrameSetEnable(this.windowBackdrop, false)

        set this.title = BlzCreateFrameByType("TEXT", this.framePrefix + "Title", this.root, "", 0)
        call BlzFrameSetSize(this.title, 0.360, 0.030)
        call BlzFrameSetPoint(this.title, FRAMEPOINT_CENTER, this.root, FRAMEPOINT_TOP, 0.0, -0.027)
        call BlzFrameSetFont(this.title, this.shopFontPath, 0.022, 0)
        call BlzFrameSetText(this.title, "|cffffcc66" + this.windowTitle + "|r")
        call BlzFrameSetEnable(this.title, false)
        call BlzFrameSetTextAlignment(this.title, TEXT_JUSTIFY_MIDDLE, TEXT_JUSTIFY_CENTER)

        set this.closeButton = BlzCreateFrame("ScriptDialogButton", this.root, 0, 0)
        call BlzFrameSetSize(this.closeButton, 0.045, 0.035)
        call BlzFrameSetPoint(this.closeButton, FRAMEPOINT_TOPRIGHT, this.root, FRAMEPOINT_TOPRIGHT, -0.014, -0.014)
        call BlzFrameSetText(this.closeButton, "X")

        set this.goldText = BlzCreateFrameByType("TEXT", this.framePrefix + "GoldText", this.root, "", 0)
        call BlzFrameSetSize(this.goldText, 0.145, 0.024)
        call BlzFrameSetPoint(this.goldText, FRAMEPOINT_CENTER, this.title, FRAMEPOINT_CENTER, 0.240, 0.0)
        call BlzFrameSetFont(this.goldText, this.shopFontPath, 0.014, 0)
        call BlzFrameSetText(this.goldText, "Gold: 0")
        call BlzFrameSetEnable(this.goldText, false)
        call BlzFrameSetTextAlignment(this.goldText, TEXT_JUSTIFY_MIDDLE, TEXT_JUSTIFY_CENTER)

        set this.searchBox = BlzCreateFrame("EscMenuEditBoxTemplate", this.root, 0, 0)
        call BlzFrameSetSize(this.searchBox, 0.150, 0.030)
        call BlzFrameSetPoint(this.searchBox, FRAMEPOINT_TOPLEFT, this.windowBackdrop, FRAMEPOINT_TOPLEFT, 0.155, -0.046)
        call BlzFrameSetText(this.searchBox, "")
        call BlzFrameSetTextSizeLimit(this.searchBox, 32)
        call BlzFrameSetLevel(this.searchBox, 70)

        set this.sortButton = BlzCreateFrame("ScriptDialogButton", this.root, 0, 0)
        call BlzFrameSetSize(this.sortButton, 0.125, 0.034)
        call BlzFrameSetPoint(this.sortButton, FRAMEPOINT_TOPLEFT, this.windowBackdrop, FRAMEPOINT_TOPLEFT, 0.312, -0.045)
        call BlzFrameSetText(this.sortButton, "Sort: Rarity")
        call BlzFrameSetLevel(this.sortButton, 40)

        set this.sortOrderButton = BlzCreateFrame("ScriptDialogButton", this.root, 0, 0)
        call BlzFrameSetSize(this.sortOrderButton, 0.065, 0.034)
        call BlzFrameSetPoint(this.sortOrderButton, FRAMEPOINT_TOPLEFT, this.windowBackdrop, FRAMEPOINT_TOPLEFT, 0.443, -0.045)
        call BlzFrameSetText(this.sortOrderButton, "Desc")
        call BlzFrameSetLevel(this.sortOrderButton, 40)

        call this.createCategoryButtons()
        call this.createItemCards()
        call this.createDetailsPanel()

        set this.systemText = BlzCreateFrameByType("TEXT", this.framePrefix + "SystemText", this.root, "", 0)
        call BlzFrameSetSize(this.systemText, 0.345, 0.024)
        call BlzFrameSetPoint(this.systemText, FRAMEPOINT_BOTTOM, this.root, FRAMEPOINT_BOTTOM, -0.016, 0.063)
        call BlzFrameSetFont(this.systemText, this.shopFontPath, 0.011, 0)
        call BlzFrameSetText(this.systemText, "|cffaaaaaaSelect an item, then press BUY or SELL.|r")
        call BlzFrameSetEnable(this.systemText, false)
        call BlzFrameSetTextAlignment(this.systemText, TEXT_JUSTIFY_MIDDLE, TEXT_JUSTIFY_CENTER)

        set this.prevButton = BlzCreateFrame("ScriptDialogButton", this.root, 0, 0)
        call BlzFrameSetSize(this.prevButton, 0.040, 0.030)
        call BlzFrameSetPoint(this.prevButton, FRAMEPOINT_CENTER, this.root, FRAMEPOINT_BOTTOM, -0.065, 0.035)
        call BlzFrameSetText(this.prevButton, "<")

        set this.pageText = BlzCreateFrameByType("TEXT", this.framePrefix + "PageText", this.root, "", 0)
        call BlzFrameSetSize(this.pageText, 0.060, 0.025)
        call BlzFrameSetPoint(this.pageText, FRAMEPOINT_CENTER, this.root, FRAMEPOINT_BOTTOM, 0.0, 0.035)
        call BlzFrameSetFont(this.pageText, this.shopFontPath, 0.013, 0)
        call BlzFrameSetText(this.pageText, "1 / 1")
        call BlzFrameSetEnable(this.pageText, false)
        call BlzFrameSetTextAlignment(this.pageText, TEXT_JUSTIFY_MIDDLE, TEXT_JUSTIFY_CENTER)

        set this.nextButton = BlzCreateFrame("ScriptDialogButton", this.root, 0, 0)
        call BlzFrameSetSize(this.nextButton, 0.040, 0.030)
        call BlzFrameSetPoint(this.nextButton, FRAMEPOINT_CENTER, this.root, FRAMEPOINT_BOTTOM, 0.065, 0.035)
        call BlzFrameSetText(this.nextButton, ">")
        set gameUi = null
    endmethod

    private method createCategoryButtons takes nothing returns nothing
        local real buttonWidth = 0.132
        local real buttonHeight = 0.030
        local real buttonGap = 0.003
        local real leftInset = 0.012
        local real topInset = 0.048
        local real pagerReserve = 0.040
        local real mainBackdropHeight = 0.465
        local real usableHeight = mainBackdropHeight - topInset - pagerReserve
        local integer visibleButtonCount = R2I(usableHeight / (buttonHeight + buttonGap))
        local integer buttonIndex = 0
        local real y = -topInset
        local framehandle categoryButtonFrame
        local RpgShopCategoryButtonSlot slot
        if visibleButtonCount < 1 then
            set visibleButtonCount = 1
        endif
        loop
            exitwhen buttonIndex >= visibleButtonCount
            set categoryButtonFrame = BlzCreateFrame("ScriptDialogButton", this.root, 0, buttonIndex)
            call BlzFrameSetSize(categoryButtonFrame, buttonWidth, buttonHeight)
            call BlzFrameSetPoint(categoryButtonFrame, FRAMEPOINT_TOPLEFT, this.windowBackdrop, FRAMEPOINT_TOPLEFT, leftInset, y)
            call BlzFrameSetText(categoryButtonFrame, "")
            set slot = RpgShopCategoryButtonSlot.create()
            set slot.index = buttonIndex
            set slot.buttonFrame = categoryButtonFrame
            set slot.next = 0
            if this.categoryButtonHead == 0 then
                set this.categoryButtonHead = slot
            else
                set this.categoryButtonTail.next = slot
            endif
            set this.categoryButtonTail = slot
            set this.categoryButtonSlotCount = this.categoryButtonSlotCount + 1
            set buttonIndex = buttonIndex + 1
            set y = y - buttonHeight - buttonGap
        endloop

        set this.categoryPrevButton = BlzCreateFrame("ScriptDialogButton", this.root, 0, 0)
        call BlzFrameSetSize(this.categoryPrevButton, 0.062, 0.026)
        call BlzFrameSetPoint(this.categoryPrevButton, FRAMEPOINT_BOTTOMLEFT, this.windowBackdrop, FRAMEPOINT_BOTTOMLEFT, leftInset, 0.012)
        call BlzFrameSetText(this.categoryPrevButton, "<")

        set this.categoryNextButton = BlzCreateFrame("ScriptDialogButton", this.root, 0, 0)
        call BlzFrameSetSize(this.categoryNextButton, 0.062, 0.026)
        call BlzFrameSetPoint(this.categoryNextButton, FRAMEPOINT_BOTTOMLEFT, this.windowBackdrop, FRAMEPOINT_BOTTOMLEFT, leftInset + 0.070, 0.012)
        call BlzFrameSetText(this.categoryNextButton, ">")
        set categoryButtonFrame = null
    endmethod

    private method createItemCards takes nothing returns nothing
        call this.ensureItemCardFrames()
    endmethod

    private method ensureItemCardFrames takes nothing returns nothing
        loop
            exitwhen this.itemFrameSlotCount >= this.visibleItemSlotCount
            call this.createItemCardFrame(this.itemFrameSlotCount)
        endloop
        call this.layoutItemCardFrames()
    endmethod

    private method itemGridRows takes nothing returns integer
        local integer rows = (this.visibleItemSlotCount + 1) / 2
        if rows < 1 then
            return 1
        endif
        return rows
    endmethod

    private method itemCardHeight takes nothing returns real
        local real gridTopInset = 0.088
        local real gridBottomReserve = 0.105
        local real rowGap = 0.007
        local integer rows = this.itemGridRows()
        local real availableHeight = 0.465 - gridTopInset - gridBottomReserve
        local real height = (availableHeight - I2R(rows - 1) * rowGap) / I2R(rows)
        if height > 0.070 then
            set height = 0.070
        endif
        if height < 0.026 then
            set height = 0.026
        endif
        return height
    endmethod

    private method itemCardY takes integer row returns real
        local real gridTop = -0.088
        local real rowGap = 0.007
        local real height = this.itemCardHeight()
        local real y = gridTop
        local integer rowStep = 0
        loop
            exitwhen rowStep >= row
            set y = y - height - rowGap
            set rowStep = rowStep + 1
        endloop
        return y
    endmethod

    private method layoutItemCardFrames takes nothing returns nothing
        local RpgShopItemFrameSlot slot = this.itemFrameHead
        loop
            exitwhen slot == 0
            call this.layoutItemCardFrame(slot)
            set slot = slot.next
        endloop
    endmethod

    private method layoutItemCardFrame takes RpgShopItemFrameSlot slot returns nothing
        local real cardWidth = 0.170
        local real colGap = 0.007
        local integer col = ModuloInteger(slot.index, 2)
        local integer row = slot.index / 2
        local real x = 0.155
        local real y
        local real height
        local real iconSize
        local real textFontSize = 0.010
        if col == 1 then
            set x = x + cardWidth + colGap
        endif
        set y = this.itemCardY(row)
        set height = this.itemCardHeight()
        set iconSize = height - 0.016
        if iconSize > 0.052 then
            set iconSize = 0.052
        endif
        if iconSize < 0.018 then
            set iconSize = 0.018
        endif
        if height < 0.050 then
            set textFontSize = 0.008
        endif
        if height < 0.038 then
            set textFontSize = 0.007
        endif
        call BlzFrameClearAllPoints(slot.backdrop)
        call BlzFrameSetSize(slot.backdrop, cardWidth, height)
        call BlzFrameSetPoint(slot.backdrop, FRAMEPOINT_TOPLEFT, this.windowBackdrop, FRAMEPOINT_TOPLEFT, x, y)
        call BlzFrameClearAllPoints(slot.icon)
        call BlzFrameSetSize(slot.icon, iconSize, iconSize)
        call BlzFrameSetPoint(slot.icon, FRAMEPOINT_TOPLEFT, slot.backdrop, FRAMEPOINT_TOPLEFT, 0.008, -0.008)
        call BlzFrameClearAllPoints(slot.text)
        call BlzFrameSetSize(slot.text, cardWidth - iconSize - 0.026, height - 0.010)
        call BlzFrameSetPoint(slot.text, FRAMEPOINT_TOPLEFT, slot.backdrop, FRAMEPOINT_TOPLEFT, iconSize + 0.014, -0.006)
        call BlzFrameSetFont(slot.text, this.shopFontPath, textFontSize, 0)
        call BlzFrameClearAllPoints(slot.buttonFrame)
        call BlzFrameSetSize(slot.buttonFrame, cardWidth, height)
        call BlzFrameSetPoint(slot.buttonFrame, FRAMEPOINT_TOPLEFT, this.windowBackdrop, FRAMEPOINT_TOPLEFT, x, y)
    endmethod

    private method createItemCardFrame takes integer slotIndex returns nothing
        local framehandle cardBackdrop
        local framehandle cardIcon
        local framehandle cardText
        local framehandle cardButtonFrame
        local RpgShopItemFrameSlot slot
        set cardBackdrop = BlzCreateFrameByType("BACKDROP", this.framePrefix + "ItemBackdrop" + I2S(slotIndex), this.root, "", slotIndex)
        call BlzFrameSetSize(cardBackdrop, 0.170, 0.070)
        call BlzFrameSetPoint(cardBackdrop, FRAMEPOINT_TOPLEFT, this.windowBackdrop, FRAMEPOINT_TOPLEFT, 0.155, -0.088)
        call BlzFrameSetTexture(cardBackdrop, this.tooltipTexturePath, 0, true)
        call BlzFrameSetAlpha(cardBackdrop, 235)
        call BlzFrameSetEnable(cardBackdrop, false)

        set cardIcon = BlzCreateFrameByType("BACKDROP", this.framePrefix + "ItemIcon" + I2S(slotIndex), this.root, "", slotIndex)
        call BlzFrameSetSize(cardIcon, 0.052, 0.052)
        call BlzFrameSetPoint(cardIcon, FRAMEPOINT_TOPLEFT, cardBackdrop, FRAMEPOINT_TOPLEFT, 0.008, -0.008)
        call BlzFrameSetTexture(cardIcon, "ReplaceableTextures\\CommandButtons\\BTNChestOfGold.blp", 0, true)
        call BlzFrameSetEnable(cardIcon, false)

        set cardText = BlzCreateFrameByType("TEXT", this.framePrefix + "ItemText" + I2S(slotIndex), this.root, "", slotIndex)
        call BlzFrameSetSize(cardText, 0.100, 0.060)
        call BlzFrameSetPoint(cardText, FRAMEPOINT_TOPLEFT, cardBackdrop, FRAMEPOINT_TOPLEFT, 0.066, -0.006)
        call BlzFrameSetFont(cardText, this.shopFontPath, 0.010, 0)
        call BlzFrameSetText(cardText, "")
        call BlzFrameSetEnable(cardText, false)

        set cardButtonFrame = BlzCreateFrame("ScriptDialogButton", this.root, 0, slotIndex)
        call BlzFrameSetSize(cardButtonFrame, 0.170, 0.070)
        call BlzFrameSetPoint(cardButtonFrame, FRAMEPOINT_TOPLEFT, this.windowBackdrop, FRAMEPOINT_TOPLEFT, 0.155, -0.088)
        call BlzFrameSetText(cardButtonFrame, "")
        call BlzFrameSetAlpha(cardButtonFrame, 0)

        set slot = RpgShopItemFrameSlot.create()
        set slot.index = slotIndex
        set slot.backdrop = cardBackdrop
        set slot.icon = cardIcon
        set slot.text = cardText
        set slot.buttonFrame = cardButtonFrame
        set slot.next = 0
        if this.itemFrameHead == 0 then
            set this.itemFrameHead = slot
        else
            set this.itemFrameTail.next = slot
        endif
        set this.itemFrameTail = slot
        set this.itemFrameSlotCount = this.itemFrameSlotCount + 1
        call thistype.registerClick(cardButtonFrame)
        set cardBackdrop = null
        set cardIcon = null
        set cardText = null
        set cardButtonFrame = null
    endmethod

    private method createDetailsPanel takes nothing returns nothing
        set this.detailsPanel = BlzCreateFrameByType("BACKDROP", this.framePrefix + "DetailsPanel", this.root, "", 0)
        call BlzFrameSetSize(this.detailsPanel, 0.182, 0.405)
        call BlzFrameSetPoint(this.detailsPanel, FRAMEPOINT_TOPRIGHT, this.root, FRAMEPOINT_TOPRIGHT, -0.018, -0.070)
        call BlzFrameSetTexture(this.detailsPanel, this.tooltipTexturePath, 0, true)
        call BlzFrameSetAlpha(this.detailsPanel, 240)
        call BlzFrameSetEnable(this.detailsPanel, false)

        set this.detailsIcon = BlzCreateFrameByType("BACKDROP", this.framePrefix + "DetailIcon", this.root, "", 0)
        call BlzFrameSetSize(this.detailsIcon, 0.086, 0.086)
        call BlzFrameSetPoint(this.detailsIcon, FRAMEPOINT_TOP, this.detailsPanel, FRAMEPOINT_TOP, 0.0, -0.026)
        call BlzFrameSetTexture(this.detailsIcon, "ReplaceableTextures\\CommandButtons\\BTNArcaniteMelee.blp", 0, true)
        call BlzFrameSetEnable(this.detailsIcon, false)

        set this.detailsTitle = BlzCreateFrameByType("TEXT", this.framePrefix + "DetailTitle", this.root, "", 0)
        call BlzFrameSetSize(this.detailsTitle, 0.160, 0.040)
        call BlzFrameSetPoint(this.detailsTitle, FRAMEPOINT_TOPLEFT, this.detailsPanel, FRAMEPOINT_TOPLEFT, 0.012, -0.124)
        call BlzFrameSetFont(this.detailsTitle, this.shopFontPath, 0.013, 0)
        call BlzFrameSetText(this.detailsTitle, "")
        call BlzFrameSetEnable(this.detailsTitle, false)

        set this.detailsBody = BlzCreateFrameByType("TEXT", this.framePrefix + "DetailBody", this.root, "", 0)
        call BlzFrameSetSize(this.detailsBody, 0.158, 0.066)
        call BlzFrameSetPoint(this.detailsBody, FRAMEPOINT_TOPLEFT, this.detailsPanel, FRAMEPOINT_TOPLEFT, 0.012, -0.174)
        call BlzFrameSetFont(this.detailsBody, this.shopFontPath, 0.010, 0)
        call BlzFrameSetText(this.detailsBody, "")
        call BlzFrameSetEnable(this.detailsBody, false)

        set this.ownedText = BlzCreateFrameByType("TEXT", this.framePrefix + "OwnedText", this.root, "", 0)
        call BlzFrameSetSize(this.ownedText, 0.160, 0.020)
        call BlzFrameSetPoint(this.ownedText, FRAMEPOINT_TOPLEFT, this.detailsPanel, FRAMEPOINT_TOPLEFT, 0.012, -0.246)
        call BlzFrameSetFont(this.ownedText, this.shopFontPath, 0.011, 0)
        call BlzFrameSetText(this.ownedText, "|cffaaaaaaOwned: 0|r")
        call BlzFrameSetEnable(this.ownedText, false)

        set this.costText = BlzCreateFrameByType("TEXT", this.framePrefix + "CostText", this.root, "", 0)
        call BlzFrameSetSize(this.costText, 0.160, 0.020)
        call BlzFrameSetPoint(this.costText, FRAMEPOINT_TOPLEFT, this.detailsPanel, FRAMEPOINT_TOPLEFT, 0.012, -0.270)
        call BlzFrameSetFont(this.costText, this.shopFontPath, 0.012, 0)
        call BlzFrameSetText(this.costText, "")
        call BlzFrameSetEnable(this.costText, false)

        set this.minusButton = BlzCreateFrame("ScriptDialogButton", this.root, 0, 0)
        call BlzFrameSetSize(this.minusButton, 0.030, 0.028)
        call BlzFrameSetPoint(this.minusButton, FRAMEPOINT_TOP, this.detailsPanel, FRAMEPOINT_TOP, -0.069, -0.296)
        call BlzFrameSetText(this.minusButton, "-")

        set this.quantityText = BlzCreateFrameByType("TEXT", this.framePrefix + "QuantityText", this.root, "", 0)
        call BlzFrameSetSize(this.quantityText, 0.045, 0.022)
        call BlzFrameSetPoint(this.quantityText, FRAMEPOINT_CENTER, this.detailsPanel, FRAMEPOINT_TOP, -0.025, -0.310)
        call BlzFrameSetFont(this.quantityText, this.shopFontPath, 0.012, 0)
        call BlzFrameSetText(this.quantityText, "1")
        call BlzFrameSetEnable(this.quantityText, false)
        call BlzFrameSetTextAlignment(this.quantityText, TEXT_JUSTIFY_MIDDLE, TEXT_JUSTIFY_CENTER)

        set this.plusButton = BlzCreateFrame("ScriptDialogButton", this.root, 0, 0)
        call BlzFrameSetSize(this.plusButton, 0.030, 0.028)
        call BlzFrameSetPoint(this.plusButton, FRAMEPOINT_TOP, this.detailsPanel, FRAMEPOINT_TOP, 0.019, -0.296)
        call BlzFrameSetText(this.plusButton, "+")

        set this.maxButton = BlzCreateFrame("ScriptDialogButton", this.root, 0, 0)
        call BlzFrameSetSize(this.maxButton, 0.043, 0.028)
        call BlzFrameSetPoint(this.maxButton, FRAMEPOINT_TOP, this.detailsPanel, FRAMEPOINT_TOP, 0.063, -0.296)
        call BlzFrameSetText(this.maxButton, "MAX")

        set this.buyButton = BlzCreateFrame("ScriptDialogButton", this.root, 0, 0)
        call BlzFrameSetSize(this.buyButton, 0.158, 0.030)
        call BlzFrameSetPoint(this.buyButton, FRAMEPOINT_TOP, this.detailsPanel, FRAMEPOINT_TOP, 0.0, -0.333)
        call BlzFrameSetText(this.buyButton, "BUY")

        set this.sellButton = BlzCreateFrame("ScriptDialogButton", this.root, 0, 0)
        call BlzFrameSetSize(this.sellButton, 0.158, 0.030)
        call BlzFrameSetPoint(this.sellButton, FRAMEPOINT_TOP, this.detailsPanel, FRAMEPOINT_TOP, 0.0, -0.368)
        call BlzFrameSetText(this.sellButton, "SELL")
    endmethod

    private method applyShopFont takes nothing returns nothing
        local RpgShopItemFrameSlot slot = this.itemFrameHead
        call BlzFrameSetFont(this.title, this.shopFontPath, 0.022, 0)
        call BlzFrameSetFont(this.goldText, this.shopFontPath, 0.014, 0)
        call BlzFrameSetFont(this.systemText, this.shopFontPath, 0.011, 0)
        call BlzFrameSetFont(this.pageText, this.shopFontPath, 0.013, 0)
        call BlzFrameSetFont(this.detailsTitle, this.shopFontPath, 0.013, 0)
        call BlzFrameSetFont(this.detailsBody, this.shopFontPath, 0.010, 0)
        call BlzFrameSetFont(this.ownedText, this.shopFontPath, 0.011, 0)
        call BlzFrameSetFont(this.costText, this.shopFontPath, 0.012, 0)
        call BlzFrameSetFont(this.quantityText, this.shopFontPath, 0.012, 0)
        loop
            exitwhen slot == 0
            call BlzFrameSetFont(slot.text, this.shopFontPath, 0.010, 0)
            set slot = slot.next
        endloop
    endmethod

    private method applyTooltipTexture takes nothing returns nothing
        local RpgShopItemFrameSlot slot = this.itemFrameHead
        call BlzFrameSetTexture(this.detailsPanel, this.tooltipTexturePath, 0, true)
        loop
            exitwhen slot == 0
            call BlzFrameSetTexture(slot.backdrop, this.tooltipTexturePath, 0, true)
            set slot = slot.next
        endloop
    endmethod

    private method bindEvents takes nothing returns nothing
        local RpgShopCategoryButtonSlot categorySlot
        call thistype.registerClick(this.closeButton)
        call thistype.registerEditbox(this.searchBox)
        call thistype.registerMouseDown(this.searchBox)
        call thistype.registerClick(this.sortButton)
        call thistype.registerClick(this.sortOrderButton)
        call thistype.registerClick(this.buyButton)
        call thistype.registerClick(this.sellButton)
        call thistype.registerClick(this.plusButton)
        call thistype.registerClick(this.minusButton)
        call thistype.registerClick(this.maxButton)
        call thistype.registerClick(this.prevButton)
        call thistype.registerClick(this.nextButton)
        call thistype.registerClick(this.categoryPrevButton)
        call thistype.registerClick(this.categoryNextButton)
        set categorySlot = this.categoryButtonHead
        loop
            exitwhen categorySlot == 0
            call thistype.registerClick(categorySlot.buttonFrame)
            set categorySlot = categorySlot.next
        endloop
    endmethod

    private method dispatchClick takes player p, framehandle clicked returns boolean
        local RpgShopItemFrameSlot itemSlot
        local RpgShopCategoryButtonSlot categorySlot
        if clicked == this.closeButton then
            call this.closeFor(p)
            return true
        elseif clicked == this.sortButton then
            call this.onSort(p)
            return true
        elseif clicked == this.sortOrderButton then
            call this.onSortOrder(p)
            return true
        elseif clicked == this.buyButton then
            call this.onBuy(p)
            return true
        elseif clicked == this.sellButton then
            call this.onSell(p)
            return true
        elseif clicked == this.plusButton then
            call this.onPlus(p)
            return true
        elseif clicked == this.minusButton then
            call this.onMinus(p)
            return true
        elseif clicked == this.maxButton then
            call this.onMax(p)
            return true
        elseif clicked == this.prevButton then
            call this.onPrevPage(p)
            return true
        elseif clicked == this.nextButton then
            call this.onNextPage(p)
            return true
        elseif clicked == this.categoryPrevButton then
            call this.onPrevCategoryPage(p)
            return true
        elseif clicked == this.categoryNextButton then
            call this.onNextCategoryPage(p)
            return true
        endif
        set itemSlot = this.itemFrameHead
        loop
            exitwhen itemSlot == 0
            if clicked == itemSlot.buttonFrame then
                call this.onItemClicked(p, itemSlot.index)
                return true
            endif
            set itemSlot = itemSlot.next
        endloop
        set categorySlot = this.categoryButtonHead
        loop
            exitwhen categorySlot == 0
            if clicked == categorySlot.buttonFrame then
                call this.onCategory(p, categorySlot.index)
                return true
            endif
            set categorySlot = categorySlot.next
        endloop
        return false
    endmethod

    private static method onFrameClick takes nothing returns nothing
        local framehandle clicked = BlzGetTriggerFrame()
        local player p = GetTriggerPlayer()
        local thistype shop = thistype.first
        loop
            exitwhen shop == 0
            if shop.dispatchClick(p, clicked) then
                call BlzFrameSetFocus(clicked, false)
                set clicked = null
                set p = null
                return
            endif
            set shop = shop.nextShop
        endloop
        set clicked = null
        set p = null
    endmethod

    private static method onEditboxChanged takes nothing returns nothing
        local framehandle changed = BlzGetTriggerFrame()
        local player p = GetTriggerPlayer()
        local thistype shop = thistype.first
        loop
            exitwhen shop == 0
            if changed == shop.searchBox then
                call shop.onSearchChanged(p)
                set changed = null
                set p = null
                return
            endif
            set shop = shop.nextShop
        endloop
        set changed = null
        set p = null
    endmethod

    private static method onMouseDown takes nothing returns nothing
        local framehandle clicked = BlzGetTriggerFrame()
        if clicked != null then
            call BlzFrameSetFocus(clicked, true)
        endif
        set clicked = null
    endmethod

    private method onSearchChanged takes player p returns nothing
        local RpgShopPlayerState state = this.getState(p)
        set state.searchText = BlzGetTriggerFrameText()
        set state.currentPage = 0
        set state.quantity = 1
        call this.refreshFor(p)
        call BlzFrameSetFocus(this.searchBox, true)
    endmethod

    private method onSort takes player p returns nothing
        local RpgShopPlayerState state = this.getState(p)
        local integer keptSelection = state.selectedItem
        local integer keptPage = state.currentPage
        if not this.takeSortClick(state) then
            return
        endif
        set state.sortMode = state.sortMode + 1
        if state.sortMode > SORT_TYPE then
            set state.sortMode = SORT_RARITY
        endif
        set state.currentPage = keptPage
        call this.buildVisibleList(state)
        if keptSelection >= 0 and this.visibleContains(keptSelection) then
            set state.selectedItem = keptSelection
        endif
        call this.notify(p, "Sorted by " + this.sortLabel(state.sortMode) + " " + this.sortOrderLabel(state.sortOrder) + ".")
        call this.refreshFor(p)
    endmethod

    private method onSortOrder takes player p returns nothing
        local RpgShopPlayerState state = this.getState(p)
        local integer keptSelection = state.selectedItem
        local integer keptPage = state.currentPage
        if not this.takeSortOrderClick(state) then
            return
        endif
        if state.sortOrder == SORT_DESCENDING then
            set state.sortOrder = SORT_ASCENDING
        else
            set state.sortOrder = SORT_DESCENDING
        endif
        set state.currentPage = keptPage
        call this.buildVisibleList(state)
        if keptSelection >= 0 and this.visibleContains(keptSelection) then
            set state.selectedItem = keptSelection
        endif
        call this.notify(p, "Sorted by " + this.sortLabel(state.sortMode) + " " + this.sortOrderLabel(state.sortOrder) + ".")
        call this.refreshFor(p)
    endmethod

    private method onCategory takes player p, integer slotIndex returns nothing
        local RpgShopPlayerState state = this.getState(p)
        local integer categoryId = this.categoryIdForButton(state, slotIndex)
        if categoryId == ALL_CATEGORY_ID or this.validCategory(categoryId) then
            set state.selectedCategory = categoryId
            set state.currentPage = 0
            call this.refreshFor(p)
        endif
    endmethod

    private method onPrevCategoryPage takes player p returns nothing
        local RpgShopPlayerState state = this.getState(p)
        if state.categoryPage > 0 then
            set state.categoryPage = state.categoryPage - 1
        endif
        call this.refreshFor(p)
    endmethod

    private method onNextCategoryPage takes player p returns nothing
        local RpgShopPlayerState state = this.getState(p)
        if state.categoryPage < this.categoryPageCount() - 1 then
            set state.categoryPage = state.categoryPage + 1
        endif
        call this.refreshFor(p)
    endmethod

    private method onItemClicked takes player p, integer slotIndex returns nothing
        local RpgShopPlayerState state = this.getState(p)
        local integer visibleIndex = state.currentPage * this.visibleItemSlotCount + slotIndex
        local RpgShopItemData clickedItem = this.visibleItemAt(visibleIndex)
        if clickedItem != 0 then
            set state.selectedItem = clickedItem.id
            set state.quantity = 1
        endif
        call this.refreshFor(p)
    endmethod

    private method onPlus takes player p returns nothing
        local RpgShopPlayerState state = this.getState(p)
        if not this.takeQuantityClick(state) then
            return
        endif
        set state.quantity = state.quantity + 1
        if state.quantity > 99 then
            set state.quantity = 99
        endif
        call this.refreshFor(p)
    endmethod

    private method onMinus takes player p returns nothing
        local RpgShopPlayerState state = this.getState(p)
        if not this.takeQuantityClick(state) then
            return
        endif
        set state.quantity = state.quantity - 1
        if state.quantity < 1 then
            set state.quantity = 1
        endif
        call this.refreshFor(p)
    endmethod

    private method maxQuantityFor takes player p, RpgShopPlayerState state, RpgShopItemData selected returns integer
        local integer maxAmount = this.getOwnedCount(state, selected.id)
        local integer affordable = 0
        if selected.price <= 0 then
            if maxAmount < 99 then
                set maxAmount = 99
            endif
        else
            set affordable = RpgShopGold(p) / selected.price
            if affordable > maxAmount then
                set maxAmount = affordable
            endif
        endif
        if maxAmount < 1 then
            set maxAmount = 1
        endif
        if maxAmount > 99 then
            set maxAmount = 99
        endif
        return maxAmount
    endmethod

    private method onMax takes player p returns nothing
        local RpgShopPlayerState state = this.getState(p)
        local RpgShopItemData selected
        if not this.takeQuantityClick(state) then
            return
        endif
        set selected = this.getItemById(state.selectedItem)
        if selected == 0 then
            call this.notify(p, "No item selected.")
            call this.refreshFor(p)
            return
        endif
        set state.quantity = this.maxQuantityFor(p, state, selected)
        call this.notify(p, "Quantity set to " + I2S(state.quantity) + ".")
        call this.refreshFor(p)
    endmethod

    private method onPrevPage takes player p returns nothing
        local RpgShopPlayerState state = this.getState(p)
        if state.currentPage > 0 then
            set state.currentPage = state.currentPage - 1
        endif
        call this.refreshFor(p)
    endmethod

    private method onNextPage takes player p returns nothing
        local RpgShopPlayerState state = this.getState(p)
        local integer maxPage
        call this.buildVisibleList(state)
        set maxPage = this.pageCount() - 1
        if state.currentPage < maxPage then
            set state.currentPage = state.currentPage + 1
        endif
        call this.refreshFor(p)
    endmethod

    private method onBuy takes player p returns nothing
        local RpgShopPlayerState state = this.getState(p)
        local RpgShopItemData selected
        local integer amount
        local integer total
        if not this.takeTradeClick(state) then
            return
        endif
        set selected = this.getItemById(state.selectedItem)
        if selected == 0 then
            call this.notify(p, "No item selected.")
            return
        endif
        set amount = state.quantity
        set total = selected.price * amount
        if RpgShopGold(p) < total then
            call this.notify(p, "Not enough gold.")
            return
        endif
        if not this.addBoughtItems(p, selected, amount) then
            call this.notify(p, "Could not create this item's hidden storage handle. Check its rawcode.")
            call this.refreshGoldOnly(p)
            return
        endif
        call RpgShopSubGold(p, total)
        call this.notify(p, "Purchased " + I2S(amount) + "x " + selected.name + ".")
        call this.refreshFor(p)
    endmethod

    private method onSell takes player p returns nothing
        local RpgShopPlayerState state = this.getState(p)
        local RpgShopItemData selected
        local integer owned
        local integer sold
        local integer gain
        if not this.takeTradeClick(state) then
            return
        endif
        set selected = this.getItemById(state.selectedItem)
        if selected == 0 then
            call this.notify(p, "No item selected.")
            return
        endif
        set owned = this.getOwnedCount(state, selected.id)
        if owned <= 0 then
            call this.notify(p, "You have not bought that item.")
            return
        endif
        set sold = state.quantity
        if sold > owned then
            set sold = owned
        endif
        call this.removeBoughtItems(state, selected.id, sold)
        set gain = (selected.price / 2) * sold
        call RpgShopAddGold(p, gain)
        call this.notify(p, "Sold " + I2S(sold) + "x " + selected.name + " for " + I2S(gain) + " gold.")
        call this.refreshFor(p)
    endmethod

    private method refreshFor takes player p returns nothing
        local RpgShopPlayerState state = this.getState(p)
        local RpgShopItemData firstVisible
        call this.buildVisibleList(state)
        if state.selectedItem >= 0 and this.getItemById(state.selectedItem) == 0 then
            set state.selectedItem = -1
        endif
        if state.selectedItem < 0 and this.sortedCount > 0 then
            set firstVisible = this.visibleItemAt(state.currentPage * this.visibleItemSlotCount)
            if firstVisible != 0 then
                set state.selectedItem = firstVisible.id
            endif
        endif
        call this.renderStaticState(p, state)
        call this.renderCards(p, state)
        call this.renderDetails(p, state)
    endmethod

    private method refreshOpenPlayers takes nothing returns nothing
        local RpgShopPlayerState state = this.playerHead
        loop
            exitwhen state == 0
            if state.isOpen then
                call this.refreshFor(state.owner)
            endif
            set state = state.next
        endloop
    endmethod

    private static method onGoldPoll takes nothing returns nothing
        local thistype shop = thistype.first
        loop
            exitwhen shop == 0
            call shop.refreshGoldForOpenPlayers()
            set shop = shop.nextShop
        endloop
    endmethod

    private method refreshGoldForOpenPlayers takes nothing returns nothing
        local RpgShopPlayerState state = this.playerHead
        loop
            exitwhen state == 0
            if state.isOpen then
                call this.refreshGoldOnly(state.owner)
            endif
            set state = state.next
        endloop
    endmethod

    private method refreshGoldOnly takes player p returns nothing
        call RpgShopFrameTextFor(p, this.goldText, "|cffffcc66Gold:|r " + I2S(RpgShopGold(p)))
    endmethod

    private method takeQuantityClick takes RpgShopPlayerState state returns boolean
        local real now = thistype.timerNow()
        if now - state.lastQuantityClick < 0.22 then
            return false
        endif
        set state.lastQuantityClick = now
        return true
    endmethod

    private method takeTradeClick takes RpgShopPlayerState state returns boolean
        local real now = thistype.timerNow()
        if now - state.lastTradeClick < 0.35 then
            return false
        endif
        set state.lastTradeClick = now
        return true
    endmethod

    private method takeSortClick takes RpgShopPlayerState state returns boolean
        local real now = thistype.timerNow()
        if now - state.lastSortClick < 0.18 then
            return false
        endif
        set state.lastSortClick = now
        return true
    endmethod

    private method takeSortOrderClick takes RpgShopPlayerState state returns boolean
        local real now = thistype.timerNow()
        if now - state.lastSortOrderClick < 0.18 then
            return false
        endif
        set state.lastSortOrderClick = now
        return true
    endmethod

    private method findOwnedEntry takes RpgShopPlayerState state, integer itemId returns RpgShopOwnedEntry
        local RpgShopOwnedEntry entry = state.ownedHead
        loop
            exitwhen entry == 0
            if entry.itemId == itemId then
                return entry
            endif
            set entry = entry.next
        endloop
        return 0
    endmethod

    private method getOwnedEntry takes RpgShopPlayerState state, integer itemId, boolean createMissing returns RpgShopOwnedEntry
        local RpgShopOwnedEntry existingEntry = this.findOwnedEntry(state, itemId)
        local RpgShopOwnedEntry newEntry
        if existingEntry != 0 or not createMissing then
            return existingEntry
        endif
        set newEntry = RpgShopOwnedEntry.create()
        set newEntry.itemId = itemId
        set newEntry.count = 0
        set newEntry.hiddenItem = null
        set newEntry.next = 0
        if state.ownedHead == 0 then
            set state.ownedHead = newEntry
        else
            set state.ownedTail.next = newEntry
        endif
        set state.ownedTail = newEntry
        return newEntry
    endmethod

    private method getOwnedCount takes RpgShopPlayerState state, integer itemId returns integer
        local RpgShopOwnedEntry entry = this.findOwnedEntry(state, itemId)
        if entry == 0 then
            return 0
        endif
        return entry.count
    endmethod

    private method hiddenStorageX takes integer pid returns real
        if pid < 6 or (pid >= 12 and pid < 18) then
            return RpgShopBoundMinX + 64.0
        endif
        return RpgShopBoundMaxX - 64.0
    endmethod

    private method hiddenStorageY takes integer pid returns real
        if pid < 12 then
            return RpgShopBoundMinY + 64.0
        endif
        return RpgShopBoundMaxY - 64.0
    endmethod

    private method ensureBoughtItemHandle takes player p, RpgShopPlayerState state, RpgShopItemData selected returns boolean
        local RpgShopOwnedEntry entry
        local item hiddenItem
        local real x
        local real y
        if selected.rawId == 0 then
            return true
        endif
        set entry = this.getOwnedEntry(state, selected.id, true)
        set x = this.hiddenStorageX(state.pid)
        set y = this.hiddenStorageY(state.pid)
        if entry.hiddenItem == null then
            set hiddenItem = CreateItem(selected.rawId, x, y)
            if hiddenItem == null then
                return false
            endif
            call SetItemPlayer(hiddenItem, p, false)
            call SetItemDropOnDeath(hiddenItem, false)
            call SetItemDroppable(hiddenItem, false)
            call SetItemPawnable(hiddenItem, false)
            call SetItemInvulnerable(hiddenItem, true)
            call SetItemVisible(hiddenItem, false)
            set entry.hiddenItem = hiddenItem
        else
            call SetItemPosition(entry.hiddenItem, x, y)
            call SetItemVisible(entry.hiddenItem, false)
        endif
        set hiddenItem = null
        return true
    endmethod

    private method updateBoughtItemHandle takes RpgShopPlayerState state, integer itemId returns nothing
        local RpgShopOwnedEntry entry = this.findOwnedEntry(state, itemId)
        local item hiddenItem
        if entry == 0 then
            return
        endif
        set hiddenItem = entry.hiddenItem
        if hiddenItem == null then
            return
        endif
        if entry.count <= 0 then
            call RemoveItem(hiddenItem)
            set entry.hiddenItem = null
        else
            call SetItemCharges(hiddenItem, entry.count)
            call SetItemPosition(hiddenItem, this.hiddenStorageX(state.pid), this.hiddenStorageY(state.pid))
            call SetItemVisible(hiddenItem, false)
        endif
        set hiddenItem = null
    endmethod

    private method addBoughtItems takes player p, RpgShopItemData selected, integer amount returns boolean
        local RpgShopPlayerState state = this.getState(p)
        local RpgShopOwnedEntry entry
        if not this.ensureBoughtItemHandle(p, state, selected) then
            return false
        endif
        set entry = this.getOwnedEntry(state, selected.id, true)
        set entry.count = entry.count + amount
        call this.updateBoughtItemHandle(state, selected.id)
        return true
    endmethod

    private method removeBoughtItems takes RpgShopPlayerState state, integer itemId, integer amount returns nothing
        local RpgShopOwnedEntry entry = this.findOwnedEntry(state, itemId)
        if entry == 0 then
            return
        endif
        set entry.count = entry.count - amount
        if entry.count < 0 then
            set entry.count = 0
        endif
        call this.updateBoughtItemHandle(state, itemId)
    endmethod

    private method renderStaticState takes player p, RpgShopPlayerState state returns nothing
        local RpgShopCategoryButtonSlot slot
        local integer categoryId
        call RpgShopFrameShowEnableFor(p, this.searchBox, true)
        call this.refreshGoldOnly(p)
        call RpgShopFrameTextFor(p, this.sortButton, "Sort: " + this.sortLabel(state.sortMode))
        call RpgShopFrameTextFor(p, this.sortOrderButton, this.sortOrderShortLabel(state.sortOrder))
        call RpgShopFrameTextFor(p, this.pageText, I2S(state.currentPage + 1) + " / " + I2S(this.pageCount()))
        call RpgShopFrameTextFor(p, this.quantityText, I2S(state.quantity))
        call RpgShopFrameTextFor(p, this.systemText, state.systemMessage)
        call this.clampCategoryPage(state)
        set slot = this.categoryButtonHead
        loop
            exitwhen slot == 0
            if this.categoryButtonIsVisible(state, slot.index) then
                call RpgShopFrameShowEnableFor(p, slot.buttonFrame, true)
                set categoryId = this.categoryIdForButton(state, slot.index)
                if state.selectedCategory == categoryId then
                    call RpgShopFrameTextFor(p, slot.buttonFrame, "|cffffcc66> " + this.categoryButtonLabel(state, slot.index) + "|r")
                else
                    call RpgShopFrameTextFor(p, slot.buttonFrame, this.categoryButtonLabel(state, slot.index))
                endif
            else
                call RpgShopFrameShowEnableFor(p, slot.buttonFrame, false)
            endif
            set slot = slot.next
        endloop
        if this.categoryCount() > this.categoryPageSize() then
            call RpgShopFrameShowEnableFor(p, this.categoryPrevButton, true)
            call RpgShopFrameShowEnableFor(p, this.categoryNextButton, true)
        else
            call RpgShopFrameShowEnableFor(p, this.categoryPrevButton, false)
            call RpgShopFrameShowEnableFor(p, this.categoryNextButton, false)
        endif
    endmethod

    private method renderCards takes player p, RpgShopPlayerState state returns nothing
        local RpgShopItemFrameSlot slot = this.itemFrameHead
        local integer visibleIndex
        local RpgShopItemData selected
        loop
            exitwhen slot == 0
            if slot.index < this.visibleItemSlotCount then
                set visibleIndex = state.currentPage * this.visibleItemSlotCount + slot.index
                set selected = this.visibleItemAt(visibleIndex)
                if selected != 0 then
                    call RpgShopFrameShowFor(p, slot.backdrop, true)
                    call RpgShopFrameShowEnableFor(p, slot.buttonFrame, true)
                    call RpgShopFrameShowFor(p, slot.icon, true)
                    call RpgShopFrameShowFor(p, slot.text, true)
                    call RpgShopFrameTextureFor(p, slot.icon, selected.icon)
                    call RpgShopFrameTextFor(p, slot.text, this.rarityColor(selected.rarityId) + selected.name + "|r|n" + this.rarityLabel(selected.rarityId) + "|n" + this.statLine(selected) + "|n|cffffcc66" + I2S(selected.price) + "g|r")
                else
                    call RpgShopFrameShowFor(p, slot.backdrop, false)
                    call RpgShopFrameShowEnableFor(p, slot.buttonFrame, false)
                    call RpgShopFrameShowFor(p, slot.icon, false)
                    call RpgShopFrameShowFor(p, slot.text, false)
                endif
            else
                call RpgShopFrameShowFor(p, slot.backdrop, false)
                call RpgShopFrameShowEnableFor(p, slot.buttonFrame, false)
                call RpgShopFrameShowFor(p, slot.icon, false)
                call RpgShopFrameShowFor(p, slot.text, false)
            endif
            set slot = slot.next
        endloop
    endmethod

    private method renderDetails takes player p, RpgShopPlayerState state returns nothing
        local RpgShopItemData selected = this.getItemById(state.selectedItem)
        if selected == 0 then
            call RpgShopFrameShowFor(p, this.detailsIcon, false)
            call RpgShopFrameTextFor(p, this.detailsTitle, "No items found")
            call RpgShopFrameTextFor(p, this.detailsBody, "Try another search or category.")
            call RpgShopFrameTextFor(p, this.ownedText, "")
            call RpgShopFrameTextFor(p, this.costText, "")
            call RpgShopFrameTextFor(p, this.quantityText, "1")
            return
        endif
        call RpgShopFrameShowFor(p, this.detailsIcon, true)
        call RpgShopFrameTextureFor(p, this.detailsIcon, selected.icon)
        call RpgShopFrameTextFor(p, this.detailsTitle, this.rarityColor(selected.rarityId) + selected.name + "|r|n" + this.rarityLabel(selected.rarityId))
        call RpgShopFrameTextFor(p, this.detailsBody, this.statLine(selected) + "|n|n" + selected.desc)
        call RpgShopFrameTextFor(p, this.ownedText, "|cffaaaaaaOwned:|r " + I2S(this.getOwnedCount(state, selected.id)))
        call RpgShopFrameTextFor(p, this.costText, "|cffffcc66Cost:|r " + I2S(selected.price * state.quantity) + " gold")
        call RpgShopFrameTextFor(p, this.quantityText, I2S(state.quantity))
    endmethod

    private method clearVisibleLists takes nothing returns nothing
        local RpgShopItemNode node = this.visibleHead
        local RpgShopItemNode nextNode
        loop
            exitwhen node == 0
            set nextNode = node.next
            call node.destroy()
            set node = nextNode
        endloop
        set node = this.sortedHead
        loop
            exitwhen node == 0
            set nextNode = node.next
            call node.destroy()
            set node = nextNode
        endloop
        set this.visibleHead = 0
        set this.visibleTail = 0
        set this.sortedHead = 0
        set this.sortedTail = 0
        set this.visibleCount = 0
        set this.sortedCount = 0
    endmethod

    private method addVisibleNode takes RpgShopItemData data returns nothing
        local RpgShopItemNode node = RpgShopItemNode.create()
        set node.data = data
        set node.next = 0
        if this.visibleHead == 0 then
            set this.visibleHead = node
        else
            set this.visibleTail.next = node
        endif
        set this.visibleTail = node
        set this.visibleCount = this.visibleCount + 1
    endmethod

    private method addSortedNode takes RpgShopItemData data returns nothing
        local RpgShopItemNode node = RpgShopItemNode.create()
        set node.data = data
        set node.next = 0
        if this.sortedHead == 0 then
            set this.sortedHead = node
        else
            set this.sortedTail.next = node
        endif
        set this.sortedTail = node
        set this.sortedCount = this.sortedCount + 1
    endmethod

    private method buildVisibleList takes RpgShopPlayerState state returns nothing
        local RpgShopItemData selected
        local RpgShopItemData nextItem
        local integer maxPage
        call this.clearVisibleLists()
        set selected = this.itemHead
        loop
            exitwhen selected == 0
            if this.itemMatches(state, selected) then
                call this.addVisibleNode(selected)
            endif
            set selected = selected.next
        endloop
        loop
            exitwhen this.sortedCount >= this.visibleCount
            set nextItem = this.bestRemainingVisibleItem(state)
            exitwhen nextItem == 0
            call this.addSortedNode(nextItem)
        endloop
        set maxPage = this.pageCount() - 1
        if state.currentPage > maxPage then
            set state.currentPage = maxPage
        endif
        if state.currentPage < 0 then
            set state.currentPage = 0
        endif
    endmethod

    private method bestRemainingVisibleItem takes RpgShopPlayerState state returns RpgShopItemData
        local RpgShopItemNode node = this.visibleHead
        local RpgShopItemData best = 0
        loop
            exitwhen node == 0
            if not this.sortedContains(node.data.id) then
                if best == 0 or this.itemGreater(state, node.data, best) then
                    set best = node.data
                endif
            endif
            set node = node.next
        endloop
        return best
    endmethod

    private method itemMatches takes RpgShopPlayerState state, RpgShopItemData selected returns boolean
        if state.selectedCategory != ALL_CATEGORY_ID and selected.categoryId != state.selectedCategory then
            return false
        endif
        if state.searchText != "" then
            if this.containsIgnoreCase(selected.name, state.searchText) then
                return true
            endif
            if this.containsIgnoreCase(this.categoryLabel(selected.categoryId), state.searchText) then
                return true
            endif
            if this.containsIgnoreCase(this.rarityLabel(selected.rarityId), state.searchText) then
                return true
            endif
            return false
        endif
        return true
    endmethod

    private method sortedContains takes integer itemId returns boolean
        local RpgShopItemNode node = this.sortedHead
        loop
            exitwhen node == 0
            if node.data.id == itemId then
                return true
            endif
            set node = node.next
        endloop
        return false
    endmethod

    private method visibleContains takes integer itemId returns boolean
        local RpgShopItemNode node = this.sortedHead
        loop
            exitwhen node == 0
            if node.data.id == itemId then
                return true
            endif
            set node = node.next
        endloop
        return false
    endmethod

    private method visibleItemAt takes integer index returns RpgShopItemData
        local integer currentIndex = 0
        local RpgShopItemNode node = this.sortedHead
        if index < 0 then
            return 0
        endif
        loop
            exitwhen node == 0
            if currentIndex == index then
                return node.data
            endif
            set currentIndex = currentIndex + 1
            set node = node.next
        endloop
        return 0
    endmethod

    private method itemGreater takes RpgShopPlayerState state, RpgShopItemData a, RpgShopItemData b returns boolean
        local boolean ascending = state.sortOrder == SORT_ASCENDING
        local boolean aBefore
        local boolean bBefore
        if state.sortMode == SORT_PRICE then
            if a.price != b.price then
                if ascending then
                    return a.price < b.price
                endif
                return a.price > b.price
            endif
            return this.nameComesBefore(a.name, b.name)
        endif
        if state.sortMode == SORT_NAME then
            set aBefore = this.nameComesBefore(a.name, b.name)
            set bBefore = this.nameComesBefore(b.name, a.name)
            if aBefore or bBefore then
                if ascending then
                    return aBefore
                endif
                return bBefore
            endif
            if a.price != b.price then
                return a.price > b.price
            endif
            return a.rarityId > b.rarityId
        endif
        if state.sortMode == SORT_TYPE then
            if a.categoryId != b.categoryId then
                if ascending then
                    return a.categoryId < b.categoryId
                endif
                return a.categoryId > b.categoryId
            endif
            if a.rarityId != b.rarityId then
                return a.rarityId > b.rarityId
            endif
            if a.price != b.price then
                return a.price > b.price
            endif
            return this.nameComesBefore(a.name, b.name)
        endif
        if a.rarityId != b.rarityId then
            if ascending then
                return a.rarityId < b.rarityId
            endif
            return a.rarityId > b.rarityId
        endif
        if a.price != b.price then
            return a.price > b.price
        endif
        return this.nameComesBefore(a.name, b.name)
    endmethod

    private method pageCount takes nothing returns integer
        if this.sortedCount <= 0 then
            return 1
        endif
        return ((this.sortedCount - 1) / this.visibleItemSlotCount) + 1
    endmethod

    private method categoryCount takes nothing returns integer
        local integer count = 0
        local RpgShopCategoryData category = this.categoryHead
        loop
            exitwhen category == 0
            set count = count + 1
            set category = category.next
        endloop
        return count
    endmethod

    private method categoryButtonCount takes nothing returns integer
        return this.categoryButtonSlotCount
    endmethod

    private method categoryPageSize takes nothing returns integer
        local integer size = this.categoryButtonCount() - 1
        if size < 1 then
            return 1
        endif
        return size
    endmethod

    private method categoryPageCount takes nothing returns integer
        if this.categoryCount() <= 0 then
            return 1
        endif
        return ((this.categoryCount() - 1) / this.categoryPageSize()) + 1
    endmethod

    private method clampCategoryPage takes RpgShopPlayerState state returns nothing
        local integer maxPage = this.categoryPageCount() - 1
        if state.categoryPage > maxPage then
            set state.categoryPage = maxPage
        endif
        if state.categoryPage < 0 then
            set state.categoryPage = 0
        endif
    endmethod

    private method categoryIdForButton takes RpgShopPlayerState state, integer slotIndex returns integer
        if slotIndex == 0 then
            return ALL_CATEGORY_ID
        endif
        return state.categoryPage * this.categoryPageSize() + slotIndex - 1
    endmethod

    private method categoryButtonIsVisible takes RpgShopPlayerState state, integer slotIndex returns boolean
        if slotIndex == 0 then
            return true
        endif
        return this.getCategoryById(this.categoryIdForButton(state, slotIndex)) != 0
    endmethod

    private method categoryButtonLabel takes RpgShopPlayerState state, integer slotIndex returns string
        if slotIndex == 0 then
            return "All Items"
        endif
        return this.categoryLabel(this.categoryIdForButton(state, slotIndex))
    endmethod

    private method getItemById takes integer itemId returns RpgShopItemData
        local RpgShopItemData selected = this.itemHead
        loop
            exitwhen selected == 0
            if selected.id == itemId then
                return selected
            endif
            set selected = selected.next
        endloop
        return 0
    endmethod

    private method getCategoryById takes integer categoryId returns RpgShopCategoryData
        local RpgShopCategoryData category = this.categoryHead
        loop
            exitwhen category == 0
            if category.id == categoryId then
                return category
            endif
            set category = category.next
        endloop
        return 0
    endmethod

    private method getRarityById takes integer rarityId returns RpgShopRarityData
        local RpgShopRarityData rarity = this.rarityHead
        loop
            exitwhen rarity == 0
            if rarity.id == rarityId then
                return rarity
            endif
            set rarity = rarity.next
        endloop
        return 0
    endmethod

    private method validCategory takes integer categoryId returns boolean
        return this.getCategoryById(categoryId) != 0
    endmethod

    private method validRarity takes integer rarityId returns boolean
        return this.getRarityById(rarityId) != 0
    endmethod

    private method categoryLabel takes integer categoryId returns string
        local RpgShopCategoryData category = this.getCategoryById(categoryId)
        if category != 0 then
            return category.label
        endif
        return "Unknown"
    endmethod

    private method rarityLabel takes integer rarityId returns string
        local RpgShopRarityData rarity = this.getRarityById(rarityId)
        if rarity != 0 then
            return rarity.label
        endif
        return "UNKNOWN"
    endmethod

    private method rarityColor takes integer rarityId returns string
        local RpgShopRarityData rarity = this.getRarityById(rarityId)
        if rarity != 0 and rarity.colorPrefix != "" then
            return rarity.colorPrefix
        endif
        return "|cffffffff"
    endmethod

    private method sortLabel takes integer mode returns string
        if mode == SORT_PRICE then
            return "Price"
        endif
        if mode == SORT_NAME then
            return "Name"
        endif
        if mode == SORT_TYPE then
            return "Type"
        endif
        return "Rarity"
    endmethod

    private method sortOrderLabel takes integer order returns string
        if order == SORT_ASCENDING then
            return "Ascending"
        endif
        return "Descending"
    endmethod

    private method sortOrderShortLabel takes integer order returns string
        if order == SORT_ASCENDING then
            return "Asc"
        endif
        return "Desc"
    endmethod

    private method statLine takes RpgShopItemData selected returns string
        if selected.attack > 0 then
            return I2S(selected.attack) + " Attack"
        endif
        if selected.defense > 0 then
            return I2S(selected.defense) + " Defense"
        endif
        return "Utility Item"
    endmethod

    private method containsIgnoreCase takes string text, string needle returns boolean
        local string hay = StringCase(text, false)
        local string ndl = StringCase(needle, false)
        local integer hayLen = StringLength(hay)
        local integer ndlLen = StringLength(ndl)
        local integer pos = 0
        if ndlLen <= 0 then
            return true
        endif
        if ndlLen > hayLen then
            return false
        endif
        loop
            exitwhen pos > hayLen - ndlLen
            if SubString(hay, pos, pos + ndlLen) == ndl then
                return true
            endif
            set pos = pos + 1
        endloop
        return false
    endmethod

    private method notify takes player p, string message returns nothing
        local RpgShopPlayerState state = this.getState(p)
        set state.systemMessage = "|cffffcc66" + message + "|r"
        call RpgShopFrameTextFor(p, this.systemText, state.systemMessage)
    endmethod

    private method charRank takes string c returns integer
        local string lower = StringCase(c, false)
        if lower == "0" then
            return 1
        elseif lower == "1" then
            return 2
        elseif lower == "2" then
            return 3
        elseif lower == "3" then
            return 4
        elseif lower == "4" then
            return 5
        elseif lower == "5" then
            return 6
        elseif lower == "6" then
            return 7
        elseif lower == "7" then
            return 8
        elseif lower == "8" then
            return 9
        elseif lower == "9" then
            return 10
        elseif lower == "a" then
            return 11
        elseif lower == "b" then
            return 12
        elseif lower == "c" then
            return 13
        elseif lower == "d" then
            return 14
        elseif lower == "e" then
            return 15
        elseif lower == "f" then
            return 16
        elseif lower == "g" then
            return 17
        elseif lower == "h" then
            return 18
        elseif lower == "i" then
            return 19
        elseif lower == "j" then
            return 20
        elseif lower == "k" then
            return 21
        elseif lower == "l" then
            return 22
        elseif lower == "m" then
            return 23
        elseif lower == "n" then
            return 24
        elseif lower == "o" then
            return 25
        elseif lower == "p" then
            return 26
        elseif lower == "q" then
            return 27
        elseif lower == "r" then
            return 28
        elseif lower == "s" then
            return 29
        elseif lower == "t" then
            return 30
        elseif lower == "u" then
            return 31
        elseif lower == "v" then
            return 32
        elseif lower == "w" then
            return 33
        elseif lower == "x" then
            return 34
        elseif lower == "y" then
            return 35
        elseif lower == "z" then
            return 36
        endif
        return 0
    endmethod

    private method nameComesBefore takes string a, string b returns boolean
        local string lowerA = StringCase(a, false)
        local string lowerB = StringCase(b, false)
        local integer lenA = StringLength(lowerA)
        local integer lenB = StringLength(lowerB)
        local integer pos = 0
        local integer rankA
        local integer rankB
        loop
            exitwhen pos >= lenA or pos >= lenB
            set rankA = this.charRank(SubString(lowerA, pos, pos + 1))
            set rankB = this.charRank(SubString(lowerB, pos, pos + 1))
            if rankA < rankB then
                return true
            endif
            if rankA > rankB then
                return false
            endif
            set pos = pos + 1
        endloop
        return lenA < lenB
    endmethod
    private static method onInit takes nothing returns nothing
        local rect bounds = GetWorldBounds()
        set RpgShopBoundMinX = GetRectMinX(bounds)
        set RpgShopBoundMinY = GetRectMinY(bounds)
        set RpgShopBoundMaxX = GetRectMaxX(bounds)
        set RpgShopBoundMaxY = GetRectMaxY(bounds)
        set bounds = null
        call TimerStart(RpgShopClock, 1000000.0, false, null)
        call TimerStart(CreateTimer(), 0.25, true, function thistype.onGoldPoll)
    endmethod
endstruct

endlibrary

Wurst:
package RpgMerchantShop
/*------------------------------------------------------------------------------------------------------------------------------------------------------------
*
*    -------------------
*    | RpgMerchantShop |
*    -------------------
*
*    RpgMerchantShop class
*    --------------------
*        A configurable popup shop that displays a searchable, sortable catalog of goods.
*        The public API is intentionally limited to the methods listed below. Internal frame handles,
*        player state, catalog storage, sorting internals, and transaction bookkeeping are private.
*        The shop supports custom categories, custom rarities, real item rawcodes, virtual goods,
*        per-shop visuals, adjustable item-card counts, buy/sell transactions, and owned-count display.
*
*    new RpgMerchantShop(string titleText) --> RpgMerchantShop
*        - Creates a new shop window with the specified title.
*        - The window starts hidden. Use <shop>.openFor(player) to display it.
*
*    <RpgMerchantShop>.addCategory(string label) --> int
*        - Registers a category and returns the category id.
*        - Store the returned id and pass it to addItem or addVirtualItem.
*        - Category order controls the Type sort order.
*
*    <RpgMerchantShop>.addRarity(string label, string colorPrefix) --> int
*        - Registers a rarity and returns the rarity id.
*        - colorPrefix should be a Warcraft color prefix such as "|cff66aaff".
*        - Rarity order controls the Rarity sort order. Add low rarities first and high rarities later when you want descending rarity to show the best items first.
*
*    <RpgMerchantShop>.addItem(string name, string icon, int categoryId, int rarityId, int price, int attack, int defense, int rawId, string desc) --> int
*        - Adds a real Warcraft item-backed shop entry and returns its shop item id.
*        - rawId should use a single-quoted rawcode literal such as 'phea'. Do not use FourCC.
*        - Bought real items are created as hidden storage items near a map corner and tracked by count.
*        - Returns -1 if categoryId or rarityId was not registered on this shop.
*
*    <RpgMerchantShop>.addVirtualItem(string name, string icon, int categoryId, int rarityId, int price, int attack, int defense, string desc) --> int
*        - Adds a non-rawcode good and returns its shop item id.
*        - Use this for tokens, crafting materials, keys, currencies, reputation goods, quest counters, or other abstract goods.
*        - Virtual goods are tracked by count only.
*        - Returns -1 if categoryId or rarityId was not registered on this shop.
*
*    <RpgMerchantShop>.clearCatalog()
*        - Removes all shop items and owned counts.
*        - Categories and rarities remain registered.
*
*    <RpgMerchantShop>.clearTaxonomyAndCatalog()
*        - Removes all shop items, owned counts, categories, and rarities.
*        - Use this when fully rebuilding a shop from scratch.
*
*    <RpgMerchantShop>.openFor(player p)
*        - Opens the shop for player p and refreshes its current state.
*
*    <RpgMerchantShop>.closeFor(player p)
*        - Closes the shop for player p.
*
*    <RpgMerchantShop>.toggleFor(player p)
*        - Opens the shop if it is hidden for player p, otherwise closes it.
*
*    <RpgMerchantShop>.setSortByRarity(player p)
*        - Sets player p's current sort mode to rarity order.
*
*    <RpgMerchantShop>.setSortByPrice(player p)
*        - Sets player p's current sort mode to price order.
*
*    <RpgMerchantShop>.setSortByName(player p)
*        - Sets player p's current sort mode to name order.
*
*    <RpgMerchantShop>.setSortByType(player p)
*        - Sets player p's current sort mode to category/type order.
*
*    <RpgMerchantShop>.setSortAscending(player p)
*        - Sets player p's current sort direction to ascending.
*
*    <RpgMerchantShop>.setSortDescending(player p)
*        - Sets player p's current sort direction to descending.
*
*    <RpgMerchantShop>.setVisibleItemSlotCount(int slotCount)
*        - Sets how many item cards are shown per page.
*        - Values below 1 are clamped to 1.
*        - Item cards reflow inside the content area so they do not cover the system text or page controls.
*
*    <RpgMerchantShop>.getVisibleItemSlotCount() --> int
*        - Returns the current item-card count for this shop.
*
*    <RpgMerchantShop>.setShopFont(string fontPath)
*        - Changes the font used by this shop.
*
*    <RpgMerchantShop>.setBlockerTexture(string texturePath)
*        - Changes the dark screen-blocker texture used behind this shop.
*
*    <RpgMerchantShop>.setPanelTexture(string texturePath)
*        - Changes the main popup panel texture used by this shop.
*
*    <RpgMerchantShop>.setTooltipTexture(string texturePath)
*        - Changes the item-card, details-panel, and smaller UI-panel texture used by this shop.
*
*    <RpgMerchantShop>.setVisualStyle(string fontPath, string blockerPath, string panelPath, string tooltipPath)
*        - Changes the font, blocker texture, main panel texture, and tooltip/panel texture at once.
*
*    Built-in behavior
*    -----------------
*        - Search matches item names, category names, and rarity names.
*        - Category changes, sort changes, and search changes preserve the selected item unless the item no longer exists.
*        - Buy/sell feedback is shown inside the shop window.
*        - Gold text refreshes automatically while the shop is open.
*        - The quantity buttons, sort buttons, and transaction buttons use click guards to avoid duplicate click processing.
*        - The MAX quantity button chooses the highest useful amount based on owned count and/or affordable count, capped at 99.
*
*    Example
*    -------
*        let shop = new RpgMerchantShop("MERCHANT'S STORE")
*
*        shop.setVisualStyle(
*            "Fonts\\FRIZQT__.TTF",
*            "UI\\Widgets\\EscMenu\\Human\\blank-background.blp",
*            "UI\\Widgets\\EscMenu\\Human\\human-options-menu-background.blp",
*            "UI\\Widgets\\ToolTips\\Human\\human-tooltip-background.blp"
*        )
*        shop.setVisibleItemSlotCount(8)
*
*        let weapons = shop.addCategory("Weapons")
*        let potions = shop.addCategory("Potions")
*        let materials = shop.addCategory("Materials")
*
*        let common = shop.addRarity("COMMON", "|cffffffff")
*        let uncommon = shop.addRarity("UNCOMMON", "|cff66ff66")
*        let rare = shop.addRarity("RARE", "|cff66aaff")
*        let epic = shop.addRarity("EPIC", "|cffcc66ff")
*
*        shop.addItem("Ironwood Bow", Icons.bTNImprovedBows, weapons, uncommon, 2150, 64, 0, 'rat9', "A sturdy bow made from reinforced ironwood.")
*        shop.addItem("Elixir of Superior Healing", Icons.bTNPotionRed, potions, rare, 1250, 0, 0, 'phea', "Restores 750 Health.")
*        shop.addVirtualItem("Soulstone Shard", Icons.bTNGem, materials, common, 750, 0, 0, "Crafting material used to imbue powerful gear.")
*
*        shop.setSortByRarity(Player(0))
*        shop.setSortDescending(Player(0))
*        shop.openFor(Player(0))
*
*------------------------------------------------------------------------------------------------------------------------------------------------------------*/
import ClosureFrames
import ClosureTimers
import Icons
import MapBounds
import LinkedList
constant int ALL_CATEGORY_ID = -1
constant int SORT_RARITY = 0
constant int SORT_PRICE  = 1
constant int SORT_NAME   = 2
constant int SORT_TYPE   = 3
constant int SORT_ASCENDING  = 0
constant int SORT_DESCENDING = 1

int nextRpgMerchantShopInstanceId = 0
class RpgShopCategoryData
    int id
    string label
    construct(int id, string label)
        this.id = id
        this.label = label
class RpgShopRarityData
    int id
    string label
    string colorPrefix
    construct(int id, string label, string colorPrefix)
        this.id = id
        this.label = label
        this.colorPrefix = colorPrefix
class RpgShopItemData
    int id
    string name
    string icon
    string desc
    int categoryId
    int rarityId
    int price
    int attack
    int defense
    int rawId
    construct(int id, string name, string icon, int categoryId, int rarityId, int price, int attack, int defense, int rawId, string desc)
        this.id = id
        this.name = name
        this.icon = icon
        this.categoryId = categoryId
        this.rarityId = rarityId
        this.price = price
        this.attack = attack
        this.defense = defense
        this.rawId = rawId
        this.desc = desc
class RpgShopOwnedEntry
    int itemId
    int count
    item hiddenItem
    construct(int itemId)
        this.itemId = itemId
        this.count = 0
        this.hiddenItem = null
class RpgShopPlayerState
    player owner
    int pid
    int selectedCategory = ALL_CATEGORY_ID
    int categoryPage = 0
    int selectedItem = -1
    int sortMode = SORT_RARITY
    int sortOrder = SORT_DESCENDING
    int quantity = 1
    int currentPage = 0
    string searchText = ""
    string systemMessage = "|cffaaaaaaSelect an item, then press BUY or SELL.|r"
    boolean quantityClickLocked = false
    boolean tradeClickLocked = false
    boolean sortClickLocked = false
    boolean sortOrderClickLocked = false
    LinkedList<RpgShopOwnedEntry> ownedEntries = new LinkedList<RpgShopOwnedEntry>
    construct(player owner)
        this.owner = owner
        this.pid = owner.getId()
class RpgShopCategoryButtonSlot
    int index
    framehandle buttonFrame
    construct(int index, framehandle buttonFrame)
        this.index = index
        this.buttonFrame = buttonFrame
class RpgShopItemFrameSlot
    int index
    framehandle backdrop
    framehandle icon
    framehandle text
    framehandle buttonFrame
    construct(int index, framehandle backdrop, framehandle icon, framehandle text, framehandle buttonFrame)
        this.index = index
        this.backdrop = backdrop
        this.icon = icon
        this.text = text
        this.buttonFrame = buttonFrame
public class RpgMerchantShop
    private int instanceId
    private int nextCategoryId = 0
    private int nextRarityId = 0
    private int nextItemId = 0
    private string framePrefix
    private string windowTitle
    private string shopFontPath = "Fonts\\FRIZQT__.TTF"
    private string blockerTexturePath = "UI\\Widgets\\EscMenu\\Human\\blank-background.blp"
    private string panelTexturePath = "UI\\Widgets\\EscMenu\\Human\\human-options-menu-background.blp"
    private string tooltipTexturePath = "UI\\Widgets\\ToolTips\\Human\\human-tooltip-background.blp"
    private int visibleItemSlotCount = 8
    private LinkedList<RpgShopPlayerState> playerStates = new LinkedList<RpgShopPlayerState>
    private LinkedList<RpgShopCategoryData> categories = new LinkedList<RpgShopCategoryData>
    private LinkedList<RpgShopRarityData> rarities = new LinkedList<RpgShopRarityData>
    private LinkedList<RpgShopItemData> items = new LinkedList<RpgShopItemData>
    private LinkedList<RpgShopItemData> visibleItems = new LinkedList<RpgShopItemData>
    private LinkedList<RpgShopItemData> sortedVisibleItems = new LinkedList<RpgShopItemData>
    private LinkedList<RpgShopCategoryButtonSlot> categoryButtonSlots = new LinkedList<RpgShopCategoryButtonSlot>
    private LinkedList<RpgShopItemFrameSlot> itemFrameSlots = new LinkedList<RpgShopItemFrameSlot>
    private framehandle blocker
    private framehandle root
    private framehandle windowBackdrop
    private framehandle title
    private framehandle closeButton
    private framehandle searchBox
    private framehandle sortButton
    private framehandle sortOrderButton
    private framehandle goldText
    private framehandle systemText
    private framehandle detailsPanel
    private framehandle detailsIcon
    private framehandle detailsTitle
    private framehandle detailsBody
    private framehandle costText
    private framehandle ownedText
    private framehandle minusButton
    private framehandle plusButton
    private framehandle maxButton
    private framehandle quantityText
    private framehandle buyButton
    private framehandle sellButton
    private framehandle pageText
    private framehandle prevButton
    private framehandle nextButton
    private framehandle categoryPrevButton
    private framehandle categoryNextButton
    construct(string titleText)
        instanceId = nextRpgMerchantShopInstanceId
        nextRpgMerchantShopInstanceId += 1
        framePrefix = "RpgShop" + instanceId.toString() + "_"
        windowTitle = titleText
        createFrames()
        bindEvents()
        startGoldPolling()
        root.hideAndDisable()
        blocker.hideAndDisable()
    function openFor(player p)
        let state = getState(p)
        state.currentPage = 0
        state.quantity = 1
        state.systemMessage = "|cffaaaaaaSelect an item, then press BUY or SELL.|r"
        refreshFor(p)
        root.showAndEnable(p)
        blocker.showAndEnable(p)
        searchBox.showAndEnable(p)
    function closeFor(player p)
        searchBox.hideAndDisable(p)
        root.hideAndDisable(p)
        blocker.hideAndDisable(p)
    function toggleFor(player p)
        if root.isVisible(p)
            closeFor(p)
        else
            openFor(p)
    function setShopFont(string fontPath)
        shopFontPath = fontPath
        applyShopFont()
    function setBlockerTexture(string texturePath)
        blockerTexturePath = texturePath
        blocker.setTexture(blockerTexturePath, 0, true)
    function setPanelTexture(string texturePath)
        panelTexturePath = texturePath
        root.setTexture(panelTexturePath, 0, true)
        windowBackdrop.setTexture(panelTexturePath, 0, true)
    function setTooltipTexture(string texturePath)
        tooltipTexturePath = texturePath
        applyTooltipTexture()
    function setVisualStyle(string fontPath, string blockerPath, string panelPath, string tooltipPath)
        shopFontPath = fontPath
        blockerTexturePath = blockerPath
        panelTexturePath = panelPath
        tooltipTexturePath = tooltipPath
        blocker.setTexture(blockerTexturePath, 0, true)
        root.setTexture(panelTexturePath, 0, true)
        windowBackdrop.setTexture(panelTexturePath, 0, true)
        applyTooltipTexture()
        applyShopFont()
    function setVisibleItemSlotCount(int slotCount)
        var newSlotCount = slotCount
        if newSlotCount < 1
            newSlotCount = 1
        visibleItemSlotCount = newSlotCount
        ensureItemCardFrames()
        refreshOpenPlayers()
    function getVisibleItemSlotCount() returns int
        return visibleItemSlotCount
    private function applySortOrder(player p, int order)
        let state = getState(p)
        if order == SORT_ASCENDING
            state.sortOrder = SORT_ASCENDING
        else
            state.sortOrder = SORT_DESCENDING
        refreshFor(p)
    private function applySortMode(player p, int mode)
        if mode < SORT_RARITY or mode > SORT_TYPE
            return
        getState(p).sortMode = mode
        refreshFor(p)
    function setSortByRarity(player p)
        applySortMode(p, SORT_RARITY)
    function setSortByPrice(player p)
        applySortMode(p, SORT_PRICE)
    function setSortByName(player p)
        applySortMode(p, SORT_NAME)
    function setSortByType(player p)
        applySortMode(p, SORT_TYPE)
    function setSortAscending(player p)
        applySortOrder(p, SORT_ASCENDING)
    function setSortDescending(player p)
        applySortOrder(p, SORT_DESCENDING)
    function addCategory(string label) returns int
        let newCategory = new RpgShopCategoryData(nextCategoryId, label)
        nextCategoryId += 1
        categories.add(newCategory)
        refreshOpenPlayers()
        return newCategory.id
    function addRarity(string label, string colorPrefix) returns int
        let newRarity = new RpgShopRarityData(nextRarityId, label, colorPrefix)
        nextRarityId += 1
        rarities.add(newRarity)
        refreshOpenPlayers()
        return newRarity.id
    function addItem(string name, string icon, int categoryId, int rarityId, int price, int attack, int defense, int rawId, string desc) returns int
        if not validCategory(categoryId) or not validRarity(rarityId)
            return -1
        let newItem = new RpgShopItemData(nextItemId, name, icon, categoryId, rarityId, price, attack, defense, rawId, desc)
        nextItemId += 1
        items.add(newItem)
        refreshOpenPlayers()
        return newItem.id
    function addVirtualItem(string name, string icon, int categoryId, int rarityId, int price, int attack, int defense, string desc) returns int
        return addItem(name, icon, categoryId, rarityId, price, attack, defense, 0, desc)
    function clearCatalog()
        for state in playerStates
            for entry in state.ownedEntries
                if entry.hiddenItem != null
                    entry.hiddenItem.remove()
                    entry.hiddenItem = null
            state.ownedEntries = new LinkedList<RpgShopOwnedEntry>
            state.selectedItem = -1
            state.currentPage = 0
            state.quantity = 1
        items = new LinkedList<RpgShopItemData>
        visibleItems = new LinkedList<RpgShopItemData>
        sortedVisibleItems = new LinkedList<RpgShopItemData>
        nextItemId = 0
        refreshOpenPlayers()
    function clearTaxonomyAndCatalog()
        clearCatalog()
        categories = new LinkedList<RpgShopCategoryData>
        rarities = new LinkedList<RpgShopRarityData>
        nextCategoryId = 0
        nextRarityId = 0
        for state in playerStates
            state.selectedCategory = ALL_CATEGORY_ID
            state.categoryPage = 0
        refreshOpenPlayers()
    private function getState(player p) returns RpgShopPlayerState
        let pid = p.getId()
        for state in playerStates
            if state.pid == pid
                return state
        let newState = new RpgShopPlayerState(p)
        playerStates.add(newState)
        return newState
    private function createFrames()
        blocker = createBackdrop(framePrefix + "Blocker", GAME_UI, 0)
            ..setSize(0.80, 0.60)
            ..setAbsPoint(FRAMEPOINT_CENTER, 0.40, 0.30)
            ..setTexture(blockerTexturePath, 0, true)
            ..setAlpha(120)
            ..setLevel(1)
        root = createBackdrop(framePrefix + "Root", GAME_UI, 0)
            ..setSize(0.72, 0.50)
            ..setAbsPoint(FRAMEPOINT_CENTER, 0.40, 0.30)
            ..setTexture(panelTexturePath, 0, true)
            ..setAlpha(245)
            ..setLevel(5)
        windowBackdrop = createBackdrop(framePrefix + "WindowBackdrop", root, 0)
            ..setSize(0.690, 0.465)
            ..setPoint(FRAMEPOINT_CENTER, root, FRAMEPOINT_CENTER, 0.0, -0.006)
            ..setTexture(panelTexturePath, 0, true)
            ..setAlpha(135)
            ..setLevel(0)
            ..disable()
        title = createTextFrame(framePrefix + "Title", root, 0)
            ..setSize(0.360, 0.030)
            ..setPoint(FRAMEPOINT_CENTER, root, FRAMEPOINT_TOP, 0.0, -0.027)
            ..setFont(shopFontPath, 0.022, 0)
            ..setText("|cffffcc66" + windowTitle + "|r")
            ..disable()
        BlzFrameSetTextAlignment(title, TEXT_JUSTIFY_MIDDLE, TEXT_JUSTIFY_CENTER)
        closeButton = createGlueTextButton(framePrefix + "Close", root, "ScriptDialogButton", 0)
            ..setSize(0.045, 0.035)
            ..setPoint(FRAMEPOINT_TOPRIGHT, root, FRAMEPOINT_TOPRIGHT, -0.014, -0.014)
            ..setText("X")
        goldText = createTextFrame(framePrefix + "GoldText", root, 0)
            ..setSize(0.145, 0.024)
            ..setPoint(FRAMEPOINT_CENTER, title, FRAMEPOINT_CENTER, 0.240, 0.0)
            ..setFont(shopFontPath, 0.014, 0)
            ..setText("Gold: 0")
            ..disable()
        BlzFrameSetTextAlignment(goldText, TEXT_JUSTIFY_MIDDLE, TEXT_JUSTIFY_CENTER)
        searchBox = createFrame("EscMenuEditBoxTemplate", root, 0, 0)
            ..setSize(0.150, 0.030)
            ..setPoint(FRAMEPOINT_TOPLEFT, windowBackdrop, FRAMEPOINT_TOPLEFT, 0.155, -0.046)
            ..setText("")
            ..setTextSizeLimit(32)
            ..setLevel(70)
        sortButton = createGlueTextButton(framePrefix + "Sort", root, "ScriptDialogButton", 0)
            ..setSize(0.125, 0.034)
            ..setPoint(FRAMEPOINT_TOPLEFT, windowBackdrop, FRAMEPOINT_TOPLEFT, 0.312, -0.045)
            ..setText("Sort: Rarity")
            ..setLevel(40)
        sortOrderButton = createGlueTextButton(framePrefix + "SortOrder", root, "ScriptDialogButton", 0)
            ..setSize(0.065, 0.034)
            ..setPoint(FRAMEPOINT_TOPLEFT, windowBackdrop, FRAMEPOINT_TOPLEFT, 0.443, -0.045)
            ..setText("Desc")
            ..setLevel(40)
        createCategoryButtons()
        createItemCards()
        createDetailsPanel()
        systemText = createTextFrame(framePrefix + "SystemText", root, 0)
            ..setSize(0.345, 0.024)
            ..setPoint(FRAMEPOINT_BOTTOM, root, FRAMEPOINT_BOTTOM, -0.016, 0.063)
            ..setFont(shopFontPath, 0.011, 0)
            ..setText("|cffaaaaaaSelect an item, then press BUY or SELL.|r")
            ..disable()
        BlzFrameSetTextAlignment(systemText, TEXT_JUSTIFY_MIDDLE, TEXT_JUSTIFY_CENTER)
        prevButton = createGlueTextButton(framePrefix + "PrevPage", root, "ScriptDialogButton", 0)
            ..setSize(0.040, 0.030)
            ..setPoint(FRAMEPOINT_CENTER, root, FRAMEPOINT_BOTTOM, -0.065, 0.035)
            ..setText("<")
        pageText = createTextFrame(framePrefix + "PageText", root, 0)
            ..setSize(0.060, 0.025)
            ..setPoint(FRAMEPOINT_CENTER, root, FRAMEPOINT_BOTTOM, 0.0, 0.035)
            ..setFont(shopFontPath, 0.013, 0)
            ..setText("1 / 1")
            ..disable()
        BlzFrameSetTextAlignment(pageText, TEXT_JUSTIFY_MIDDLE, TEXT_JUSTIFY_CENTER)
        nextButton = createGlueTextButton(framePrefix + "NextPage", root, "ScriptDialogButton", 0)
            ..setSize(0.040, 0.030)
            ..setPoint(FRAMEPOINT_CENTER, root, FRAMEPOINT_BOTTOM, 0.065, 0.035)
            ..setText(">")
    private function createCategoryButtons()
        let buttonWidth = 0.132
        let buttonHeight = 0.030
        let buttonGap = 0.003
        let leftInset = 0.012
        let topInset = 0.048
        let pagerReserve = 0.040
        let mainBackdropHeight = 0.465
        let usableHeight = mainBackdropHeight - topInset - pagerReserve
        int visibleButtonCount = (usableHeight / (buttonHeight + buttonGap)).toInt()
        if visibleButtonCount < 1
            visibleButtonCount = 1
        int buttonIndex = 0
        real y = -topInset
        while buttonIndex < visibleButtonCount
            let categoryButton = createGlueTextButton(framePrefix + "Category" + buttonIndex.toString(), root, "ScriptDialogButton", buttonIndex)
                ..setSize(buttonWidth, buttonHeight)
                ..setPoint(FRAMEPOINT_TOPLEFT, windowBackdrop, FRAMEPOINT_TOPLEFT, leftInset, y)
                ..setText("")
            categoryButtonSlots.add(new RpgShopCategoryButtonSlot(buttonIndex, categoryButton))
            buttonIndex += 1
            y -= buttonHeight + buttonGap
        categoryPrevButton = createGlueTextButton(framePrefix + "CategoryPrev", root, "ScriptDialogButton", 0)
            ..setSize(0.062, 0.026)
            ..setPoint(FRAMEPOINT_BOTTOMLEFT, windowBackdrop, FRAMEPOINT_BOTTOMLEFT, leftInset, 0.012)
            ..setText("<")
        categoryNextButton = createGlueTextButton(framePrefix + "CategoryNext", root, "ScriptDialogButton", 0)
            ..setSize(0.062, 0.026)
            ..setPoint(FRAMEPOINT_BOTTOMLEFT, windowBackdrop, FRAMEPOINT_BOTTOMLEFT, leftInset + 0.070, 0.012)
            ..setText(">")
    private function createItemCards()
        ensureItemCardFrames()
    private function ensureItemCardFrames()
        while itemFrameSlots.size() < visibleItemSlotCount
            createItemCardFrame(itemFrameSlots.size())
        layoutItemCardFrames()
    private function itemGridRows() returns int
        let rows = (visibleItemSlotCount + 1) div 2
        if rows < 1
            return 1
        return rows
    private function itemCardHeight() returns real
        let gridTopInset = 0.088
        let gridBottomReserve = 0.105
        let rowGap = 0.007
        let rows = itemGridRows()
        let availableHeight = 0.465 - gridTopInset - gridBottomReserve
        var height = (availableHeight - (rows - 1) * rowGap) / (rows * 1.0)
        if height > 0.070
            height = 0.070
        if height < 0.026
            height = 0.026
        return height
    private function itemCardY(int row) returns real
        let gridTop = -0.088
        let rowGap = 0.007
        let height = itemCardHeight()
        var y = gridTop
        int rowStep = 0
        while rowStep < row
            y -= height + rowGap
            rowStep += 1
        return y
    private function layoutItemCardFrames()
        for slot in itemFrameSlots
            layoutItemCardFrame(slot)
    private function layoutItemCardFrame(RpgShopItemFrameSlot slot)
        let cardWidth = 0.170
        let colGap = 0.007
        let col = slot.index mod 2
        let row = slot.index div 2
        var x = 0.155
        if col == 1
            x += cardWidth + colGap
        let y = itemCardY(row)
        let height = itemCardHeight()
        var iconSize = height - 0.016
        if iconSize > 0.052
            iconSize = 0.052
        if iconSize < 0.018
            iconSize = 0.018
        var textFontSize = 0.010
        if height < 0.050
            textFontSize = 0.008
        if height < 0.038
            textFontSize = 0.007
        BlzFrameClearAllPoints(slot.backdrop)
        slot.backdrop
            ..setSize(cardWidth, height)
            ..setPoint(FRAMEPOINT_TOPLEFT, windowBackdrop, FRAMEPOINT_TOPLEFT, x, y)
        BlzFrameClearAllPoints(slot.icon)
        slot.icon
            ..setSize(iconSize, iconSize)
            ..setPoint(FRAMEPOINT_TOPLEFT, slot.backdrop, FRAMEPOINT_TOPLEFT, 0.008, -0.008)
        BlzFrameClearAllPoints(slot.text)
        slot.text
            ..setSize(cardWidth - iconSize - 0.026, height - 0.010)
            ..setPoint(FRAMEPOINT_TOPLEFT, slot.backdrop, FRAMEPOINT_TOPLEFT, iconSize + 0.014, -0.006)
            ..setFont(shopFontPath, textFontSize, 0)
        BlzFrameClearAllPoints(slot.buttonFrame)
        slot.buttonFrame
            ..setSize(cardWidth, height)
            ..setPoint(FRAMEPOINT_TOPLEFT, windowBackdrop, FRAMEPOINT_TOPLEFT, x, y)
    private function createItemCardFrame(int slotIndex)
        let cardBackdrop = createBackdrop(framePrefix + "ItemBackdrop" + slotIndex.toString(), root, slotIndex)
            ..setSize(0.170, 0.070)
            ..setPoint(FRAMEPOINT_TOPLEFT, windowBackdrop, FRAMEPOINT_TOPLEFT, 0.155, -0.088)
            ..setTexture(tooltipTexturePath, 0, true)
            ..setAlpha(235)
            ..disable()
        let cardIcon = createBackdrop(framePrefix + "ItemIcon" + slotIndex.toString(), root, slotIndex)
            ..setSize(0.052, 0.052)
            ..setPoint(FRAMEPOINT_TOPLEFT, cardBackdrop, FRAMEPOINT_TOPLEFT, 0.008, -0.008)
            ..setTexture(Icons.bTNChestOfGold, 0, true)
            ..disable()
        let cardText = createTextFrame(framePrefix + "ItemText" + slotIndex.toString(), root, slotIndex)
            ..setSize(0.100, 0.060)
            ..setPoint(FRAMEPOINT_TOPLEFT, cardBackdrop, FRAMEPOINT_TOPLEFT, 0.066, -0.006)
            ..setFont(shopFontPath, 0.010, 0)
            ..setText("")
            ..disable()
        let cardButtonFrame = createGlueTextButton(framePrefix + "ItemButton" + slotIndex.toString(), root, "ScriptDialogButton", slotIndex)
            ..setSize(0.170, 0.070)
            ..setPoint(FRAMEPOINT_TOPLEFT, windowBackdrop, FRAMEPOINT_TOPLEFT, 0.155, -0.088)
            ..setText("")
            ..setAlpha(0)
        cardButtonFrame.onClick() ->
            onItemClicked()
        cardButtonFrame.onClickReleaseFocus()
        itemFrameSlots.add(new RpgShopItemFrameSlot(slotIndex, cardBackdrop, cardIcon, cardText, cardButtonFrame))
    private function createDetailsPanel()
        detailsPanel = createBackdrop(framePrefix + "DetailsPanel", root, 0)
            ..setSize(0.182, 0.405)
            ..setPoint(FRAMEPOINT_TOPRIGHT, root, FRAMEPOINT_TOPRIGHT, -0.018, -0.070)
            ..setTexture(tooltipTexturePath, 0, true)
            ..setAlpha(240)
            ..disable()
        detailsIcon = createBackdrop(framePrefix + "DetailIcon", root, 0)
            ..setSize(0.086, 0.086)
            ..setPoint(FRAMEPOINT_TOP, detailsPanel, FRAMEPOINT_TOP, 0.0, -0.026)
            ..setTexture(Icons.bTNArcaniteMelee, 0, true)
            ..disable()
        detailsTitle = createTextFrame(framePrefix + "DetailTitle", root, 0)
            ..setSize(0.160, 0.040)
            ..setPoint(FRAMEPOINT_TOPLEFT, detailsPanel, FRAMEPOINT_TOPLEFT, 0.012, -0.124)
            ..setFont(shopFontPath, 0.013, 0)
            ..setText("")
            ..disable()
        detailsBody = createTextFrame(framePrefix + "DetailBody", root, 0)
            ..setSize(0.158, 0.066)
            ..setPoint(FRAMEPOINT_TOPLEFT, detailsPanel, FRAMEPOINT_TOPLEFT, 0.012, -0.174)
            ..setFont(shopFontPath, 0.010, 0)
            ..setText("")
            ..disable()
        ownedText = createTextFrame(framePrefix + "OwnedText", root, 0)
            ..setSize(0.160, 0.020)
            ..setPoint(FRAMEPOINT_TOPLEFT, detailsPanel, FRAMEPOINT_TOPLEFT, 0.012, -0.246)
            ..setFont(shopFontPath, 0.011, 0)
            ..setText("|cffaaaaaaOwned: 0|r")
            ..disable()
        costText = createTextFrame(framePrefix + "CostText", root, 0)
            ..setSize(0.160, 0.020)
            ..setPoint(FRAMEPOINT_TOPLEFT, detailsPanel, FRAMEPOINT_TOPLEFT, 0.012, -0.270)
            ..setFont(shopFontPath, 0.012, 0)
            ..setText("")
            ..disable()
        minusButton = createGlueTextButton(framePrefix + "QtyMinus", root, "ScriptDialogButton", 0)
            ..setSize(0.030, 0.028)
            ..setPoint(FRAMEPOINT_TOP, detailsPanel, FRAMEPOINT_TOP, -0.069, -0.296)
            ..setText("-")
        quantityText = createTextFrame(framePrefix + "QuantityText", root, 0)
            ..setSize(0.045, 0.022)
            ..setPoint(FRAMEPOINT_CENTER, detailsPanel, FRAMEPOINT_TOP, -0.025, -0.310)
            ..setFont(shopFontPath, 0.012, 0)
            ..setText("1")
            ..disable()
        BlzFrameSetTextAlignment(quantityText, TEXT_JUSTIFY_MIDDLE, TEXT_JUSTIFY_CENTER)
        plusButton = createGlueTextButton(framePrefix + "QtyPlus", root, "ScriptDialogButton", 0)
            ..setSize(0.030, 0.028)
            ..setPoint(FRAMEPOINT_TOP, detailsPanel, FRAMEPOINT_TOP, 0.019, -0.296)
            ..setText("+")
        maxButton = createGlueTextButton(framePrefix + "QtyMax", root, "ScriptDialogButton", 0)
            ..setSize(0.043, 0.028)
            ..setPoint(FRAMEPOINT_TOP, detailsPanel, FRAMEPOINT_TOP, 0.063, -0.296)
            ..setText("MAX")
        buyButton = createGlueTextButton(framePrefix + "Buy", root, "ScriptDialogButton", 0)
            ..setSize(0.158, 0.030)
            ..setPoint(FRAMEPOINT_TOP, detailsPanel, FRAMEPOINT_TOP, 0.0, -0.333)
            ..setText("BUY")
        sellButton = createGlueTextButton(framePrefix + "Sell", root, "ScriptDialogButton", 0)
            ..setSize(0.158, 0.030)
            ..setPoint(FRAMEPOINT_TOP, detailsPanel, FRAMEPOINT_TOP, 0.0, -0.368)
            ..setText("SELL")
    private function applyShopFont()
        title.setFont(shopFontPath, 0.022, 0)
        goldText.setFont(shopFontPath, 0.014, 0)
        systemText.setFont(shopFontPath, 0.011, 0)
        pageText.setFont(shopFontPath, 0.013, 0)
        detailsTitle.setFont(shopFontPath, 0.013, 0)
        detailsBody.setFont(shopFontPath, 0.010, 0)
        ownedText.setFont(shopFontPath, 0.011, 0)
        costText.setFont(shopFontPath, 0.012, 0)
        quantityText.setFont(shopFontPath, 0.012, 0)
        for slot in itemFrameSlots
            slot.text.setFont(shopFontPath, 0.010, 0)
    private function applyTooltipTexture()
        detailsPanel.setTexture(tooltipTexturePath, 0, true)
        for slot in itemFrameSlots
            slot.backdrop.setTexture(tooltipTexturePath, 0, true)
    private function bindEvents()
        closeButton.onClick() ->
            closeFor(GetTriggerPlayer())
        closeButton.onClickReleaseFocus()
        searchBox.onEditboxChange() ->
            onSearchChanged()
        searchBox.onMouseDown() ->
            BlzFrameSetFocus(searchBox, true)
        sortButton.onClick() ->
            onSort()
        sortButton.onClickReleaseFocus()
        sortOrderButton.onClick() ->
            onSortOrder()
        sortOrderButton.onClickReleaseFocus()
        buyButton.onClick() ->
            onBuy()
        buyButton.onClickReleaseFocus()
        sellButton.onClick() ->
            onSell()
        sellButton.onClickReleaseFocus()
        plusButton.onClick() ->
            onPlus()
        plusButton.onClickReleaseFocus()
        minusButton.onClick() ->
            onMinus()
        minusButton.onClickReleaseFocus()
        maxButton.onClick() ->
            onMax()
        maxButton.onClickReleaseFocus()
        prevButton.onClick() ->
            onPrevPage()
        prevButton.onClickReleaseFocus()
        nextButton.onClick() ->
            onNextPage()
        nextButton.onClickReleaseFocus()
        categoryPrevButton.onClick() ->
            onPrevCategoryPage()
        categoryPrevButton.onClickReleaseFocus()
        categoryNextButton.onClick() ->
            onNextCategoryPage()
        categoryNextButton.onClickReleaseFocus()
        for slot in categoryButtonSlots
            slot.buttonFrame.onClick() ->
                onCategory()
            slot.buttonFrame.onClickReleaseFocus()

    private function onSearchChanged()
        let p = GetTriggerPlayer()
        let state = getState(p)
        state.searchText = BlzGetTriggerFrameText()
        state.currentPage = 0
        state.quantity = 1
        refreshFor(p)
        BlzFrameSetFocus(searchBox, true)
    private function onSort()
        let p = GetTriggerPlayer()
        let state = getState(p)
        if not takeSortClick(state)
            return
        let keptSelection = state.selectedItem
        let keptPage = state.currentPage
        state.sortMode += 1
        if state.sortMode > SORT_TYPE
            state.sortMode = SORT_RARITY
        state.currentPage = keptPage
        buildVisibleList(state)
        if keptSelection >= 0 and visibleContains(keptSelection)
            state.selectedItem = keptSelection
        notify(p, "Sorted by " + sortLabel(state.sortMode) + " " + sortOrderLabel(state.sortOrder) + ".")
        refreshFor(p)
    private function onSortOrder()
        let p = GetTriggerPlayer()
        let state = getState(p)
        if not takeSortOrderClick(state)
            return
        let keptSelection = state.selectedItem
        let keptPage = state.currentPage
        if state.sortOrder == SORT_DESCENDING
            state.sortOrder = SORT_ASCENDING
        else
            state.sortOrder = SORT_DESCENDING
        state.currentPage = keptPage
        buildVisibleList(state)
        if keptSelection >= 0 and visibleContains(keptSelection)
            state.selectedItem = keptSelection
        notify(p, "Sorted by " + sortLabel(state.sortMode) + " " + sortOrderLabel(state.sortOrder) + ".")
        refreshFor(p)
    private function onCategory()
        let p = GetTriggerPlayer()
        let clicked = BlzGetTriggerFrame()
        let state = getState(p)
        for slot in categoryButtonSlots
            if clicked == slot.buttonFrame
                let categoryId = categoryIdForButton(state, slot.index)
                if categoryId == ALL_CATEGORY_ID or validCategory(categoryId)
                    state.selectedCategory = categoryId
                    state.currentPage = 0
                    refreshFor(p)
                return
    private function onPrevCategoryPage()
        let p = GetTriggerPlayer()
        let state = getState(p)
        if state.categoryPage > 0
            state.categoryPage -= 1
        refreshFor(p)
    private function onNextCategoryPage()
        let p = GetTriggerPlayer()
        let state = getState(p)
        if state.categoryPage < categoryPageCount() - 1
            state.categoryPage += 1
        refreshFor(p)
    private function onItemClicked()
        let p = GetTriggerPlayer()
        let clicked = BlzGetTriggerFrame()
        let state = getState(p)
        for slot in itemFrameSlots
            if clicked == slot.buttonFrame
                let visibleIndex = state.currentPage * visibleItemSlotCount + slot.index
                let clickedItem = visibleItemAt(visibleIndex)
                if clickedItem != null
                    state.selectedItem = clickedItem.id
                    state.quantity = 1
        refreshFor(p)
    private function onPlus()
        let p = GetTriggerPlayer()
        let state = getState(p)
        if not takeQuantityClick(state)
            return
        state.quantity += 1
        if state.quantity > 99
            state.quantity = 99
        refreshFor(p)
    private function onMinus()
        let p = GetTriggerPlayer()
        let state = getState(p)
        if not takeQuantityClick(state)
            return
        state.quantity -= 1
        if state.quantity < 1
            state.quantity = 1
        refreshFor(p)
    private function maxQuantityFor(player p, RpgShopPlayerState state, RpgShopItemData selected) returns int
        var maxAmount = getOwnedCount(state, selected.id)
        if selected.price <= 0
            if maxAmount < 99
                maxAmount = 99
        else
            let affordable = p.getGold() div selected.price
            if affordable > maxAmount
                maxAmount = affordable
        if maxAmount < 1
            maxAmount = 1
        if maxAmount > 99
            maxAmount = 99
        return maxAmount
    private function onMax()
        let p = GetTriggerPlayer()
        let state = getState(p)
        if not takeQuantityClick(state)
            return
        let selected = getItemById(state.selectedItem)
        if selected == null
            notify(p, "No item selected.")
            refreshFor(p)
            return
        state.quantity = maxQuantityFor(p, state, selected)
        notify(p, "Quantity set to " + state.quantity.toString() + ".")
        refreshFor(p)
    private function onPrevPage()
        let p = GetTriggerPlayer()
        let state = getState(p)
        if state.currentPage > 0
            state.currentPage -= 1
        refreshFor(p)
    private function onNextPage()
        let p = GetTriggerPlayer()
        let state = getState(p)
        buildVisibleList(state)
        let maxPage = pageCount() - 1
        if state.currentPage < maxPage
            state.currentPage += 1
        refreshFor(p)
    private function onBuy()
        let p = GetTriggerPlayer()
        let state = getState(p)
        if not takeTradeClick(state)
            return
        let selected = getItemById(state.selectedItem)
        if selected == null
            notify(p, "No item selected.")
            return
        let amount = state.quantity
        let total = selected.price * amount
        if p.getGold() < total
            notify(p, "Not enough gold.")
            return
        if not addBoughtItems(p, selected, amount)
            notify(p, "Could not create this item's hidden storage handle. Check its rawcode.")
            refreshGoldOnly(p)
            return
        p.subGold(total)
        notify(p, "Purchased " + amount.toString() + "x " + selected.name + ".")
        refreshFor(p)
    private function onSell()
        let p = GetTriggerPlayer()
        let state = getState(p)
        if not takeTradeClick(state)
            return
        let selected = getItemById(state.selectedItem)
        if selected == null
            notify(p, "No item selected.")
            return
        let owned = getOwnedCount(state, selected.id)
        if owned <= 0
            notify(p, "You have not bought that item.")
            return
        var sold = state.quantity
        if sold > owned
            sold = owned
        removeBoughtItems(state, selected.id, sold)
        let gain = (selected.price div 2) * sold
        p.addGold(gain)
        notify(p, "Sold " + sold.toString() + "x " + selected.name + " for " + gain.toString() + " gold.")
        refreshFor(p)
    private function refreshFor(player p)
        let state = getState(p)
        buildVisibleList(state)
        if state.selectedItem >= 0 and getItemById(state.selectedItem) == null
            state.selectedItem = -1
        if state.selectedItem < 0 and sortedVisibleItems.size() > 0
            let firstVisible = visibleItemAt(state.currentPage * visibleItemSlotCount)
            if firstVisible != null
                state.selectedItem = firstVisible.id
        renderStaticState(p, state)
        renderCards(p, state)
        renderDetails(p, state)
    private function refreshOpenPlayers()
        for state in playerStates
            if root.isVisible(state.owner)
                refreshFor(state.owner)
    private function startGoldPolling()
        doPeriodically(0.25) goldPoll ->
            refreshGoldForOpenPlayers()
    private function refreshGoldForOpenPlayers()
        for state in playerStates
            if root.isVisible(state.owner)
                refreshGoldOnly(state.owner)
    private function refreshGoldOnly(player p)
        goldText.setText(p, "|cffffcc66Gold:|r " + p.getGold().toString())
    private function takeQuantityClick(RpgShopPlayerState state) returns boolean
        if state.quantityClickLocked
            return false
        state.quantityClickLocked = true
        doAfter(0.22) ->
            state.quantityClickLocked = false
        return true
    private function takeTradeClick(RpgShopPlayerState state) returns boolean
        if state.tradeClickLocked
            return false
        state.tradeClickLocked = true
        doAfter(0.35) ->
            state.tradeClickLocked = false
        return true
    private function takeSortClick(RpgShopPlayerState state) returns boolean
        if state.sortClickLocked
            return false
        state.sortClickLocked = true
        doAfter(0.18) ->
            state.sortClickLocked = false
        return true
    private function takeSortOrderClick(RpgShopPlayerState state) returns boolean
        if state.sortOrderClickLocked
            return false
        state.sortOrderClickLocked = true
        doAfter(0.18) ->
            state.sortOrderClickLocked = false
        return true
    private function findOwnedEntry(RpgShopPlayerState state, int itemId) returns RpgShopOwnedEntry
        for entry in state.ownedEntries
            if entry.itemId == itemId
                return entry
        return null
    private function getOwnedEntry(RpgShopPlayerState state, int itemId, boolean createMissing) returns RpgShopOwnedEntry
        let existingEntry = findOwnedEntry(state, itemId)
        if existingEntry != null or not createMissing
            return existingEntry
        let newEntry = new RpgShopOwnedEntry(itemId)
        state.ownedEntries.add(newEntry)
        return newEntry
    private function getOwnedCount(RpgShopPlayerState state, int itemId) returns int
        let entry = findOwnedEntry(state, itemId)
        if entry == null
            return 0
        return entry.count
    private function hiddenStoragePos(int pid) returns vec2
        if pid < 6
            return vec2(boundMin.x + 64.0, boundMin.y + 64.0)
        if pid < 12
            return vec2(boundMax.x - 64.0, boundMin.y + 64.0)
        if pid < 18
            return vec2(boundMin.x + 64.0, boundMax.y - 64.0)
        return vec2(boundMax.x - 64.0, boundMax.y - 64.0)
    private function ensureBoughtItemHandle(player p, RpgShopPlayerState state, RpgShopItemData selected) returns boolean
        if selected.rawId == 0
            return true
        let entry = getOwnedEntry(state, selected.id, true)
        if entry.hiddenItem == null
            let hiddenItem = createItem(selected.rawId, hiddenStoragePos(state.pid))
            if hiddenItem == null
                return false
            hiddenItem.setPlayer(p, false)
            hiddenItem.setDropOnDeath(false)
            hiddenItem.setDroppable(false)
            hiddenItem.setPawnable(false)
            hiddenItem.setInvulnerable(true)
            hiddenItem.setVisible(false)
            entry.hiddenItem = hiddenItem
        else
            entry.hiddenItem.setPos(hiddenStoragePos(state.pid))
            entry.hiddenItem.setVisible(false)
        return true
    private function updateBoughtItemHandle(RpgShopPlayerState state, int itemId)
        let entry = findOwnedEntry(state, itemId)
        if entry == null
            return
        let hiddenItem = entry.hiddenItem
        if hiddenItem == null
            return
        if entry.count <= 0
            hiddenItem.remove()
            entry.hiddenItem = null
        else
            hiddenItem.setCharges(entry.count)
            hiddenItem.setPos(hiddenStoragePos(state.pid))
            hiddenItem.setVisible(false)
    private function addBoughtItems(player p, RpgShopItemData selected, int amount) returns boolean
        let state = getState(p)
        if not ensureBoughtItemHandle(p, state, selected)
            return false
        let entry = getOwnedEntry(state, selected.id, true)
        entry.count += amount
        updateBoughtItemHandle(state, selected.id)
        return true
    private function removeBoughtItems(RpgShopPlayerState state, int itemId, int amount)
        let entry = findOwnedEntry(state, itemId)
        if entry == null
            return
        entry.count -= amount
        if entry.count < 0
            entry.count = 0
        updateBoughtItemHandle(state, itemId)
    private function renderStaticState(player p, RpgShopPlayerState state)
        searchBox.showAndEnable(p)
        refreshGoldOnly(p)
        sortButton.setText(p, "Sort: " + sortLabel(state.sortMode))
        sortOrderButton.setText(p, sortOrderShortLabel(state.sortOrder))
        pageText.setText(p, (state.currentPage + 1).toString() + " / " + pageCount().toString())
        quantityText.setText(p, state.quantity.toString())
        systemText.setText(p, state.systemMessage)
        clampCategoryPage(state)
        for slot in categoryButtonSlots
            if categoryButtonIsVisible(state, slot.index)
                slot.buttonFrame.showAndEnable(p)
                let categoryId = categoryIdForButton(state, slot.index)
                if state.selectedCategory == categoryId
                    slot.buttonFrame.setText(p, "|cffffcc66> " + categoryButtonLabel(state, slot.index) + "|r")
                else
                    slot.buttonFrame.setText(p, categoryButtonLabel(state, slot.index))
            else
                slot.buttonFrame.hideAndDisable(p)
        if categoryCount() > categoryPageSize()
            categoryPrevButton.showAndEnable(p)
            categoryNextButton.showAndEnable(p)
        else
            categoryPrevButton.hideAndDisable(p)
            categoryNextButton.hideAndDisable(p)
    private function renderCards(player p, RpgShopPlayerState state)
        for slot in itemFrameSlots
            if slot.index < visibleItemSlotCount
                let visibleIndex = state.currentPage * visibleItemSlotCount + slot.index
                let selected = visibleItemAt(visibleIndex)
                if selected != null
                    slot.backdrop.show(p)
                    slot.buttonFrame.showAndEnable(p)
                    slot.icon.show(p)
                    slot.text.show(p)
                    slot.icon.setTexture(p, selected.icon, 0, true)
                    slot.text.setText(p, rarityColor(selected.rarityId) + selected.name + "|r\n" + rarityLabel(selected.rarityId) + "\n" + statLine(selected) + "\n|cffffcc66" + selected.price.toString() + "g|r")
                else
                    slot.backdrop.hide(p)
                    slot.buttonFrame.hideAndDisable(p)
                    slot.icon.hide(p)
                    slot.text.hide(p)
            else
                slot.backdrop.hide(p)
                slot.buttonFrame.hideAndDisable(p)
                slot.icon.hide(p)
                slot.text.hide(p)
    private function renderDetails(player p, RpgShopPlayerState state)
        let selected = getItemById(state.selectedItem)
        if selected == null
            detailsIcon.hide(p)
            detailsTitle.setText(p, "No items found")
            detailsBody.setText(p, "Try another search or category.")
            ownedText.setText(p, "")
            costText.setText(p, "")
            quantityText.setText(p, "1")
            return
        detailsIcon.show(p)
        detailsIcon.setTexture(p, selected.icon, 0, true)
        detailsTitle.setText(p, rarityColor(selected.rarityId) + selected.name + "|r\n" + rarityLabel(selected.rarityId))
        detailsBody.setText(p, statLine(selected) + "\n\n" + selected.desc)
        ownedText.setText(p, "|cffaaaaaaOwned:|r " + getOwnedCount(state, selected.id).toString())
        costText.setText(p, "|cffffcc66Cost:|r " + (selected.price * state.quantity).toString() + " gold")
        quantityText.setText(p, state.quantity.toString())
    private function buildVisibleList(RpgShopPlayerState state)
        visibleItems = new LinkedList<RpgShopItemData>
        sortedVisibleItems = new LinkedList<RpgShopItemData>
        for selected in items
            if itemMatches(state, selected)
                visibleItems.add(selected)
        while sortedVisibleItems.size() < visibleItems.size()
            let nextItem = bestRemainingVisibleItem(state)
            if nextItem == null
                return
            sortedVisibleItems.add(nextItem)
        let maxPage = pageCount() - 1
        if state.currentPage > maxPage
            state.currentPage = maxPage
        if state.currentPage < 0
            state.currentPage = 0
    private function bestRemainingVisibleItem(RpgShopPlayerState state) returns RpgShopItemData
        RpgShopItemData best = null
        for selected in visibleItems
            if not sortedContains(selected.id)
                if best == null or itemGreater(state, selected, best)
                    best = selected
        return best
    private function itemMatches(RpgShopPlayerState state, RpgShopItemData selected) returns boolean
        if state.selectedCategory != ALL_CATEGORY_ID and selected.categoryId != state.selectedCategory
            return false
        if state.searchText != ""
            let query = state.searchText
            if containsIgnoreCase(selected.name, query)
                return true
            if containsIgnoreCase(categoryLabel(selected.categoryId), query)
                return true
            if containsIgnoreCase(rarityLabel(selected.rarityId), query)
                return true
            return false
        return true
    private function sortedContains(int itemId) returns boolean
        for selected in sortedVisibleItems
            if selected.id == itemId
                return true
        return false
    private function visibleContains(int itemId) returns boolean
        for selected in sortedVisibleItems
            if selected.id == itemId
                return true
        return false
    private function visibleItemAt(int index) returns RpgShopItemData
        if index < 0
            return null
        int currentIndex = 0
        for selected in sortedVisibleItems
            if currentIndex == index
                return selected
            currentIndex += 1
        return null
    private function itemGreater(RpgShopPlayerState state, RpgShopItemData a, RpgShopItemData b) returns boolean
        let ascending = state.sortOrder == SORT_ASCENDING
        if state.sortMode == SORT_PRICE
            if a.price != b.price
                if ascending
                    return a.price < b.price
                return a.price > b.price
            return nameComesBefore(a.name, b.name)
        if state.sortMode == SORT_NAME
            let aBefore = nameComesBefore(a.name, b.name)
            let bBefore = nameComesBefore(b.name, a.name)
            if aBefore or bBefore
                if ascending
                    return aBefore
                return bBefore
            if a.price != b.price
                return a.price > b.price
            return a.rarityId > b.rarityId
        if state.sortMode == SORT_TYPE
            if a.categoryId != b.categoryId
                if ascending
                    return a.categoryId < b.categoryId
                return a.categoryId > b.categoryId
            if a.rarityId != b.rarityId
                return a.rarityId > b.rarityId
            if a.price != b.price
                return a.price > b.price
            return nameComesBefore(a.name, b.name)
        if a.rarityId != b.rarityId
            if ascending
                return a.rarityId < b.rarityId
            return a.rarityId > b.rarityId
        if a.price != b.price
            return a.price > b.price
        return nameComesBefore(a.name, b.name)
    private function pageCount() returns int
        if sortedVisibleItems.size() <= 0
            return 1
        return ((sortedVisibleItems.size() - 1) div visibleItemSlotCount) + 1
    private function categoryCount() returns int
        return categories.size()
    private function categoryButtonCount() returns int
        return categoryButtonSlots.size()
    private function categoryPageSize() returns int
        let size = categoryButtonCount() - 1
        if size < 1
            return 1
        return size
    private function categoryPageCount() returns int
        if categoryCount() <= 0
            return 1
        return ((categoryCount() - 1) div categoryPageSize()) + 1
    private function clampCategoryPage(RpgShopPlayerState state)
        let maxPage = categoryPageCount() - 1
        if state.categoryPage > maxPage
            state.categoryPage = maxPage
        if state.categoryPage < 0
            state.categoryPage = 0
    private function categoryIdForButton(RpgShopPlayerState state, int buttonIndex) returns int
        if buttonIndex == 0
            return ALL_CATEGORY_ID
        return state.categoryPage * categoryPageSize() + buttonIndex - 1
    private function categoryButtonIsVisible(RpgShopPlayerState state, int buttonIndex) returns boolean
        if buttonIndex == 0
            return true
        return getCategoryById(categoryIdForButton(state, buttonIndex)) != null
    private function categoryButtonLabel(RpgShopPlayerState state, int buttonIndex) returns string
        if buttonIndex == 0
            return "All Items"
        return categoryLabel(categoryIdForButton(state, buttonIndex))
    private function getItemById(int itemId) returns RpgShopItemData
        for selected in items
            if selected.id == itemId
                return selected
        return null
    private function getCategoryById(int categoryId) returns RpgShopCategoryData
        for category in categories
            if category.id == categoryId
                return category
        return null
    private function getRarityById(int rarityId) returns RpgShopRarityData
        for rarity in rarities
            if rarity.id == rarityId
                return rarity
        return null
    private function validCategory(int categoryId) returns boolean
        return getCategoryById(categoryId) != null
    private function validRarity(int rarityId) returns boolean
        return getRarityById(rarityId) != null
    private function categoryLabel(int categoryId) returns string
        let category = getCategoryById(categoryId)
        if category != null
            return category.label
        return "Unknown"
    private function rarityLabel(int rarityId) returns string
        let rarity = getRarityById(rarityId)
        if rarity != null
            return rarity.label
        return "UNKNOWN"
    private function rarityColor(int rarityId) returns string
        let rarity = getRarityById(rarityId)
        if rarity != null and rarity.colorPrefix != ""
            return rarity.colorPrefix
        return "|cffffffff"
    private function sortLabel(int mode) returns string
        if mode == SORT_PRICE
            return "Price"
        if mode == SORT_NAME
            return "Name"
        if mode == SORT_TYPE
            return "Type"
        return "Rarity"
    private function sortOrderLabel(int order) returns string
        if order == SORT_ASCENDING
            return "Ascending"
        return "Descending"
    private function sortOrderShortLabel(int order) returns string
        if order == SORT_ASCENDING
            return "Asc"
        return "Desc"
    private function statLine(RpgShopItemData selected) returns string
        if selected.attack > 0
            return selected.attack.toString() + " Attack"
        if selected.defense > 0
            return selected.defense.toString() + " Defense"
        return "Utility Item"
    private function containsIgnoreCase(string text, string needle) returns boolean
        let hay = StringCase(text, false)
        let ndl = StringCase(needle, false)
        let hayLen = StringLength(hay)
        let ndlLen = StringLength(ndl)
        if ndlLen <= 0
            return true
        if ndlLen > hayLen
            return false
        int pos = 0
        while pos <= hayLen - ndlLen
            if SubString(hay, pos, pos + ndlLen) == ndl
                return true
            pos += 1
        return false
    private function notify(player p, string message)
        let state = getState(p)
        state.systemMessage = "|cffffcc66" + message + "|r"
        systemText.setText(p, state.systemMessage)
    private function charRank(string c) returns int
        let lower = StringCase(c, false)
        if lower == "0"
            return 1
        if lower == "1"
            return 2
        if lower == "2"
            return 3
        if lower == "3"
            return 4
        if lower == "4"
            return 5
        if lower == "5"
            return 6
        if lower == "6"
            return 7
        if lower == "7"
            return 8
        if lower == "8"
            return 9
        if lower == "9"
            return 10
        if lower == "a"
            return 11
        if lower == "b"
            return 12
        if lower == "c"
            return 13
        if lower == "d"
            return 14
        if lower == "e"
            return 15
        if lower == "f"
            return 16
        if lower == "g"
            return 17
        if lower == "h"
            return 18
        if lower == "i"
            return 19
        if lower == "j"
            return 20
        if lower == "k"
            return 21
        if lower == "l"
            return 22
        if lower == "m"
            return 23
        if lower == "n"
            return 24
        if lower == "o"
            return 25
        if lower == "p"
            return 26
        if lower == "q"
            return 27
        if lower == "r"
            return 28
        if lower == "s"
            return 29
        if lower == "t"
            return 30
        if lower == "u"
            return 31
        if lower == "v"
            return 32
        if lower == "w"
            return 33
        if lower == "x"
            return 34
        if lower == "y"
            return 35
        if lower == "z"
            return 36
        return 0
    private function nameComesBefore(string a, string b) returns boolean
        let lowerA = StringCase(a, false)
        let lowerB = StringCase(b, false)
        let lenA = StringLength(lowerA)
        let lenB = StringLength(lowerB)
        int pos = 0
        while pos < lenA and pos < lenB
            let rankA = charRank(SubString(lowerA, pos, pos + 1))
            let rankB = charRank(SubString(lowerB, pos, pos + 1))
            if rankA < rankB
                return true
            if rankA > rankB
                return false
            pos += 1
        return lenA < lenB

Lua:
Coming Soon!


vJASS

WurstScript

Lua


RpgMerchantShop



Creation

JASS:
static method create takes string titleText returns thistype

Creates a new shop instance with the given title.

JASS:
local RpgMerchantShop shop
set shop = RpgMerchantShop.create("MERCHANT'S STORE")



Window Control

JASS:
method openFor takes player p returns nothing
method closeFor takes player p returns nothing
method toggleFor takes player p returns nothing

openFor opens the shop for a player.
closeFor closes the shop for a player.
toggleFor opens the shop if closed, or closes it if open.



Visual Configuration

JASS:
method setShopFont takes string fontPath returns nothing
method setBlockerTexture takes string texturePath returns nothing
method setPanelTexture takes string texturePath returns nothing
method setTooltipTexture takes string texturePath returns nothing
method setVisualStyle takes string fontPath, string blockerPath, string panelPath, string tooltipPath returns nothing

These methods configure the font and texture paths used by the shop.



Visible Item Slots

JASS:
method setVisibleItemSlotCount takes integer slotCount returns nothing
method getVisibleItemSlotCount takes nothing returns integer

Controls how many item cards are shown per page.



Sorting

JASS:
method setSortByRarity takes player p returns nothing
method setSortByPrice takes player p returns nothing
method setSortByName takes player p returns nothing
method setSortByType takes player p returns nothing
method setSortAscending takes player p returns nothing
method setSortDescending takes player p returns nothing

Rarity uses rarity registration order.
Price uses item price.
Name uses item name.
Type uses category registration order.



Categories

JASS:
method addCategory takes string label returns integer

Adds a category and returns its category id.



Rarities

JASS:
method addRarity takes string label, string colorPrefix returns integer

Adds a rarity and returns its rarity id.



Catalog

JASS:
method addItem takes string name, string icon, integer categoryId, integer rarityId, integer price, integer attack, integer defense, integer rawId, string desc returns integer
method addVirtualItem takes string name, string icon, integer categoryId, integer rarityId, integer price, integer attack, integer defense, string desc returns integer

addItem adds a real Warcraft III item-backed shop entry.
addVirtualItem adds a count-only virtual shop good.



Clearing Data

JASS:
method clearCatalog takes nothing returns nothing
method clearTaxonomyAndCatalog takes nothing returns nothing

clearCatalog removes all registered shop items and owned counts.
clearTaxonomyAndCatalog removes all shop items, owned counts, categories, and rarities.

RpgMerchantShop



Creation

Wurst:
new RpgMerchantShop(string titleText)

Creates a new shop instance with the given title.

Wurst:
let shop = new RpgMerchantShop("MERCHANT'S STORE")



Window Control

Wurst:
function openFor(player p)
function closeFor(player p)
function toggleFor(player p)

openFor opens the shop for a player.
closeFor closes the shop for a player.
toggleFor opens the shop if closed, or closes it if open.



Visual Configuration

Wurst:
function setShopFont(string fontPath)
function setBlockerTexture(string texturePath)
function setPanelTexture(string texturePath)
function setTooltipTexture(string texturePath)
function setVisualStyle(string fontPath, string blockerPath, string panelPath, string tooltipPath)

These functions configure the font and texture paths used by the shop.



Visible Item Slots

Wurst:
function setVisibleItemSlotCount(int slotCount)
function getVisibleItemSlotCount() returns int

Controls how many item cards are shown per page.



Sorting

Wurst:
function setSortByRarity(player p)
function setSortByPrice(player p)
function setSortByName(player p)
function setSortByType(player p)
function setSortAscending(player p)
function setSortDescending(player p)

Rarity uses rarity registration order.
Price uses item price.
Name uses item name.
Type uses category registration order.



Categories

Wurst:
function addCategory(string label) returns int

Adds a category and returns its category id.



Rarities

Wurst:
function addRarity(string label, string colorPrefix) returns int

Adds a rarity and returns its rarity id.



Catalog

Wurst:
function addItem(string name, string icon, int categoryId, int rarityId, int price, int attack, int defense, int rawId, string desc) returns int
function addVirtualItem(string name, string icon, int categoryId, int rarityId, int price, int attack, int defense, string desc) returns int

addItem adds a real Warcraft III item-backed shop entry.
addVirtualItem adds a count-only virtual shop good.



Clearing Data

Wurst:
function clearCatalog()
function clearTaxonomyAndCatalog()

clearCatalog removes all registered shop items and owned counts.
clearTaxonomyAndCatalog removes all shop items, owned counts, categories, and rarities.

RpgMerchantShop



Creation

Lua:
RpgMerchantShop.create(titleText)

Creates a new shop instance with the given title.

Lua:
local shop = RpgMerchantShop.create("MERCHANT'S STORE")



Window Control

Lua:
shop:openFor(player)
shop:closeFor(player)
shop:toggleFor(player)

openFor opens the shop for a player.
closeFor closes the shop for a player.
toggleFor opens the shop if closed, or closes it if open.



Visual Configuration

Lua:
shop:setShopFont(fontPath)
shop:setBlockerTexture(texturePath)
shop:setPanelTexture(texturePath)
shop:setTooltipTexture(texturePath)
shop:setVisualStyle(fontPath, blockerPath, panelPath, tooltipPath)

These methods configure the font and texture paths used by the shop.



Visible Item Slots

Lua:
shop:setVisibleItemSlotCount(slotCount)
shop:getVisibleItemSlotCount()

Controls how many item cards are shown per page.



Sorting

Lua:
shop:setSortByRarity(player)
shop:setSortByPrice(player)
shop:setSortByName(player)
shop:setSortByType(player)
shop:setSortAscending(player)
shop:setSortDescending(player)

Rarity uses rarity registration order.
Price uses item price.
Name uses item name.
Type uses category registration order.



Categories

Lua:
shop:addCategory(label)

Adds a category and returns its category id.



Rarities

Lua:
shop:addRarity(label, colorPrefix)

Adds a rarity and returns its rarity id.



Catalog

Lua:
shop:addItem(name, icon, categoryId, rarityId, price, attack, defense, rawId, desc)
shop:addVirtualItem(name, icon, categoryId, rarityId, price, attack, defense, desc)

addItem adds a real Warcraft III item-backed shop entry.
addVirtualItem adds a count-only virtual shop good.



Clearing Data

Lua:
shop:clearCatalog()
shop:clearTaxonomyAndCatalog()

clearCatalog removes all registered shop items and owned counts.
clearTaxonomyAndCatalog removes all shop items, owned counts, categories, and rarities.


vJASS

WurstScript

Lua


JASS:
scope RpgMerchantShopExample initializer OnInit

globals
    private RpgMerchantShop shop = 0
endglobals

private function AddSword takes integer categoryId, integer rarityId returns nothing
    local string name
    local string icon
    local string desc
    local integer price
    local integer attack
    local integer defense
    local integer rawId

    set name = "Steel Sword"
    set icon = "ReplaceableTextures\\CommandButtons\\BTNArcaniteMelee.blp"
    set desc = "A reliable sword."
    set price = 1200
    set attack = 15
    set defense = 0
    set rawId = 'ratf'

    call shop.addItem(name, icon, categoryId, rarityId, price, attack, defense, rawId, desc)
endfunction

private function AddPotion takes integer categoryId, integer rarityId returns nothing
    local string name
    local string icon
    local string desc
    local integer price
    local integer attack
    local integer defense
    local integer rawId

    set name = "Healing Potion"
    set icon = "ReplaceableTextures\\CommandButtons\\BTNPotionRed.blp"
    set desc = "Restores health."
    set price = 125
    set attack = 0
    set defense = 0
    set rawId = 'phea'

    call shop.addItem(name, icon, categoryId, rarityId, price, attack, defense, rawId, desc)
endfunction

private function AddMaterial takes integer categoryId, integer rarityId returns nothing
    local string name
    local string icon
    local string desc
    local integer price
    local integer attack
    local integer defense

    set name = "Soulstone Shard"
    set icon = "ReplaceableTextures\\CommandButtons\\BTNGem.blp"
    set desc = "Used for crafting."
    set price = 750
    set attack = 0
    set defense = 0

    call shop.addVirtualItem(name, icon, categoryId, rarityId, price, attack, defense, desc)
endfunction

private function OnInit takes nothing returns nothing
    local integer weapons
    local integer potions
    local integer materials
    local integer common
    local integer uncommon
    local integer rare

    set shop = RpgMerchantShop.create("MERCHANT'S STORE")
    call shop.setVisibleItemSlotCount(8)

    set weapons = shop.addCategory("Weapons")
    set potions = shop.addCategory("Potions")
    set materials = shop.addCategory("Materials")

    set common = shop.addRarity("COMMON", "|cffffffff")
    set uncommon = shop.addRarity("UNCOMMON", "|cff66ff66")
    set rare = shop.addRarity("RARE", "|cff66aaff")

    call AddSword(weapons, rare)
    call AddPotion(potions, common)
    call AddMaterial(materials, uncommon)

    call shop.openFor(Player(0))
endfunction

endscope

Wurst:
package RpgMerchantShopExample

import Icons
import RpgMerchantShop

init
    let shop = new RpgMerchantShop("MERCHANT'S STORE")

    shop.setVisibleItemSlotCount(8)

    let weapons = shop.addCategory("Weapons")
    let potions = shop.addCategory("Potions")
    let materials = shop.addCategory("Materials")

    let common = shop.addRarity("COMMON", "|cffffffff")
    let uncommon = shop.addRarity("UNCOMMON", "|cff66ff66")
    let rare = shop.addRarity("RARE", "|cff66aaff")

    let swordName = "Steel Sword"
    let swordIcon = Icons.bTNArcaniteMelee
    let swordDesc = "A reliable sword."

    shop.addItem(
        swordName,
        swordIcon,
        weapons,
        rare,
        1200,
        15,
        0,
        'ratf',
        swordDesc
    )

    let potionName = "Healing Potion"
    let potionIcon = Icons.bTNPotionRed
    let potionDesc = "Restores health."

    shop.addItem(
        potionName,
        potionIcon,
        potions,
        common,
        125,
        0,
        0,
        'phea',
        potionDesc
    )

    let materialName = "Soulstone Shard"
    let materialIcon = Icons.bTNGem
    let materialDesc = "Used for crafting."

    shop.addVirtualItem(
        materialName,
        materialIcon,
        materials,
        uncommon,
        750,
        0,
        0,
        materialDesc
    )

    shop.openFor(Player(0))

Lua:
do
    local shop
    local weapons
    local potions
    local materials
    local common
    local uncommon
    local rare

    shop = RpgMerchantShop.create("MERCHANT'S STORE")

    shop:setVisibleItemSlotCount(8)

    weapons = shop:addCategory("Weapons")
    potions = shop:addCategory("Potions")
    materials = shop:addCategory("Materials")

    common = shop:addRarity("COMMON", "|cffffffff")
    uncommon = shop:addRarity("UNCOMMON", "|cff66ff66")
    rare = shop:addRarity("RARE", "|cff66aaff")

    local swordName = "Steel Sword"
    local swordIcon = "ReplaceableTextures\\CommandButtons\\BTNArcaniteMelee.blp"
    local swordDesc = "A reliable sword."

    shop:addItem(
        swordName,
        swordIcon,
        weapons,
        rare,
        1200,
        15,
        0,
        FourCC("ratf"),
        swordDesc
    )

    local potionName = "Healing Potion"
    local potionIcon = "ReplaceableTextures\\CommandButtons\\BTNPotionRed.blp"
    local potionDesc = "Restores health."

    shop:addItem(
        potionName,
        potionIcon,
        potions,
        common,
        125,
        0,
        0,
        FourCC("phea"),
        potionDesc
    )

    local materialName = "Soulstone Shard"
    local materialIcon = "ReplaceableTextures\\CommandButtons\\BTNGem.blp"
    local materialDesc = "Used for crafting."

    shop:addVirtualItem(
        materialName,
        materialIcon,
        materials,
        uncommon,
        750,
        0,
        0,
        materialDesc
    )

    shop:openFor(Player(0))
end


Changelog

1.0.0

Initial release.
Contents

RPG Merchant Shop (Map)

Wurst Version (Binary)

Back
Top