///////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2015-18 The 8-Bit Bunch. Licensed under the Apache License, Version 1.1
// (the "License"); you may not use this file except in compliance with the License.
// You may obtain a copy of the License at <http://www.apache.org/licenses/LICENSE-1.1>.
// Unless required by applicable law or agreed to in writing, software distributed under
// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
// ANY KIND, either express or implied. See the License for the specific language
// governing permissions and limitations under the License.
///////////////////////////////////////////////////////////////////////////////////////////////////

// Functions we import from the main game loop. If you find there are some over there that aren't
// yet exported, modify this header then add the mechanics at the top of gameloop.pla.
include "gamelib.plh"

// Data structure definitions
include "playtype.plh"

// Global definitions
include "globalDefs.plh"
include "gen_modules.plh"
include "gen_items.plh"
include "godmode.plh"
include "itemutil.plh"

// Definition of constants for functions exported by this module
include "party.plh"

// Tab positions
const INVLBL_X = 10
const INV_X = 25
const INV_RT = 170

const STAT_X = 186
const STATLBL_X = 194

const SKILL_JUST_OFF = 34
const SKILL_LABEL_OFF = 40

const INV_ROWS = (BIGWIN_HEIGHT / 9) - 4

const MAX_SKILLS = 26 // should be plenty
byte nSkills, canBumpSkills
word skillName[MAX_SKILLS]
word skillPtr[MAX_SKILLS]
byte skillMin[MAX_SKILLS]
word skillX[MAX_SKILLS]
byte skillY[MAX_SKILLS]

byte mode  // 'S' or 'I' for skills or inventory

word pItemutilModule

// Exported functions go here. First a predef for each one, then a table with function pointers
// in the same order as the constants are defined in the header.
predef _showPlayerSheet(player_num)#1
predef _benchPlayer()#1
predef _unbenchPlayer()#1
word[] funcTbl = @_showPlayerSheet, @_benchPlayer, @_unbenchPlayer

// Other global variables here

///////////////////////////////////////////////////////////////////////////////////////////////////
def isEquipped(pItem)#1
  if pItem->t_type == TYPE_ARMOR or pItem->t_type == TYPE_WEAPON
    return pItem->b_flags & ITEM_FLAG_EQUIP
  fin
  return FALSE
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Display title of a column at top but below character name. Optionally append a second string,
// then leave a little vertical space below before beginning the content of the column.
def showColumnTitle(x, title, page, nPages)#0
  rawDisplayf2("^V000\n^J^J^L^J^T%D%s", x, title)
  if nPages > 1
    rawDisplayf2(" p.%d/%d", page, nPages)
  fin
  rawDisplayStr("^N\n^J^J")
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Display inventory list page. Return number of items on page.
def showInventory(player, page, select)#1
  word item
  byte s_item, n_item, n_page, totalPages, firstTime

  s_item = 0
  n_page = page * INV_ROWS
  totalPages = (countList(player=>p_items)+INV_ROWS-1) / INV_ROWS
  showColumnTitle(INV_X-10, "INVENTORY", page+1, totalPages)
  item = index(player=>p_items, n_page)
  n_item = n_page
  firstTime = TRUE
  while item and n_item < (n_page + INV_ROWS)
    if !firstTime; displayChar('\n'); fin
    firstTime = FALSE

    if isEquipped(item)
      rawDisplayStr("*")
    fin

    rawDisplayf2("^T%D%c.", INVLBL_X, 'A' + s_item)
    s_item++

    rawDisplayf1("^T%D", INV_X)
    pItemutilModule()=>itemutil_displayItemName(item)
    n_item++
    item = item=>p_nextObj
  loop
  return s_item
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def displayDice(dice)#0
  byte n, d, p
  n = (dice >> 12) & $0F
  d = (dice >> 8) & $0F
  p = dice & $FF
  rightJustifyStr(sprintf2("%d-%d", n+p, (n*d)+p), STAT_X)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def vspace()#0
  rawDisplayStr("^J^J^J^J")
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Derived stats like armor, damage, etc.
def showDerived(player)#0
  word weapon, dmg, fmt

  //rawDisplayStr("^V023")
  showColumnTitle(STAT_X-5, "STATS", 0, 0)

  fmt = "^T%D%s\n"
  rawDisplayf2("^T%DLevel %d\n", STAT_X-2, player->b_level)
  vspace()
  if player->b_skillPoints
    rawDisplayf2(fmt, STAT_X-14, "^I LEVEL UP ^N")
    rawDisplayf2(fmt, STAT_X-6, "in S)kills")
    vspace()
  fin
  rightJustifyNum(player=>w_curXP, STAT_X)
  rawDisplayf2(fmt, STATLBL_X, "Current XP")
  if player=>w_nextXP < 32767  // check for level cap
    rightJustifyNum(player=>w_nextXP, STAT_X)
  else
    rightJustifyStr("-----", STAT_X)
  fin
  rawDisplayf2(fmt, STATLBL_X, "Next lvl XP")

  vspace()
  rightJustifyNum(player=>w_health, STAT_X)
  rawDisplayf2(fmt, STATLBL_X, @S_HEALTH)
  rightJustifyNum(player=>w_maxHealth, STAT_X)
  rawDisplayf2(fmt, STATLBL_X, @S_MAX_HEALTH)
  vspace()

  rightJustifyNum(player->b_armor, STAT_X)
  rawDisplayf2(fmt, STATLBL_X, "Armor")

  // Get weapon
  weapon = first(player=>p_items, &(w) w->t_type == TYPE_WEAPON and isEquipped(w))
  if weapon
    if weapon=>r_projectileDmg
      displayDice(weapon=>r_projectileDmg)
      rawDisplayf2(fmt, STATLBL_X, "Projectile")
    fin
    dmg = weapon=>r_meleeDmg
  else
    dmg = $01400
  fin
  displayDice(dmg)
  rawDisplayf2(fmt, STATLBL_X, "Melee")

  vspace()
  rightJustifyStr(sprintf2("%d/%d", countList(player=>p_items), player->b_packSize), STAT_X)
  rawDisplayf2(fmt, STATLBL_X, "Pack size")

  vspace()
  rightJustifyNum(global=>w_gold, STAT_X)
  rawDisplayf2(fmt, STATLBL_X, "Party gold")

end

///////////////////////////////////////////////////////////////////////////////////////////////////
def clearLittleArea(x, y)#0
  setWindow(BIGWIN_TOP+y, BIGWIN_TOP+9+y, BIGWIN_LEFT+x+14, BIGWIN_LEFT+x+40)
  clearWindow()
  setBigWindow()
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Display skill value
def displaySkill(x, str, pVal, allowChg)#0
  word val, cursX, cursY
  cursX, cursY = getCursor()
  val = ^pVal
  skillX[nSkills] = x
  skillY[nSkills] = cursY
  skillName[nSkills] = str
  skillPtr[nSkills] = pVal
  skillMin[nSkills] = val
  if allowChg and canBumpSkills
    rawDisplayf2("^T%D%c.", x, 'A' + nSkills)
    nSkills++
  fin
  rightJustifyStr(sprintf1(" %d ", val), x+SKILL_JUST_OFF)
  rawDisplayf2("^T%D%s^N\n", x+SKILL_LABEL_OFF, str)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Show aquired skills in lower right panel
def showSkills(player)#0
  word skill
  byte x1, x2, cursX, cursY

  nSkills = 0

  // First column: skills
  rawDisplayStr("^V024") // starting Y
  nSkills = 0
  if canBumpSkills
    x1 = 10
    x2 = STAT_X - 35
  else
    x1 = 0
    x2 = STAT_X - 45
  fin
  showColumnTitle(25, "SKILLS", 0, 0)
  if player->b_aiming; displaySkill(x1, @S_AIMING, @player->b_aiming, TRUE); fin
  if player->b_handToHand; displaySkill(x1, @S_HAND_TO_HAND, @player->b_handToHand, TRUE); fin
  if player->b_dodging; displaySkill(x1, @S_DODGING, @player->b_dodging, TRUE); fin
  ctx = x1
  forSome(player=>p_skills, &(p) p=>w_modValue, &(p) displaySkill(ctx, p=>s_name, p+w_modValue, TRUE))
  cursX, cursY = getCursor()

  // Second column: attributes
  showColumnTitle(STAT_X-20, "ATTRIBUTES", 0, 0)
  rawDisplayStr("^V024") // starting Y
  displaySkill(x2, @S_INTELLIGENCE, player+b_intelligence, TRUE)
  displaySkill(x2, @S_STRENGTH, player+b_strength, TRUE)
  displaySkill(x2, @S_AGILITY, player+b_agility, TRUE)
  displaySkill(x2, @S_STAMINA, player+b_stamina, TRUE)
  displaySkill(x2, @S_CHARISMA, player+b_charisma, TRUE)
  displaySkill(x2, @S_SPIRIT, player+b_spirit, TRUE)
  displaySkill(x2, @S_LUCK, player+b_luck, TRUE)

  // Special: skill points (bumped differently from skills)
  vspace()
  displaySkill(x2, "Skill points", player+b_skillPoints, FALSE)

  if player->c_gender > ' '
    vspace()
    rawDisplayf2("^T%D%s", x2+SKILL_LABEL_OFF, "Gender")
    rightJustifyStr(sprintf1(" %c ", player->c_gender), x2+SKILL_JUST_OFF)
  fin

  if player->b_level == 1 and player->b_skillPoints and cursY < BIGWIN_HEIGHT - 51
    setCursor(0, BIGWIN_HEIGHT - 51)
    centerStr(sprintf1("Use keys A-%c to distribute your skill points.", nSkills - 1 + 'A'), BIGWIN_WIDTH)
    rawDisplayStr("\n")
    centerStr("To undo a choice use Ctrl-A, Ctrl-B, etc.", BIGWIN_WIDTH)
    rawDisplayStr("\n")
    centerStr("When done, press X to see starting items.", BIGWIN_WIDTH)
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Returns 1 to redisplay everything, 0 if only minor change
def adjustSkill(player, skillNum, dir)#0
  word p
  word stamDiff
  stamDiff = player->b_stamina
  p = skillPtr[skillNum]
  if dir < 0 and skillMin[skillNum] == ^p
    beep; return
  elsif dir > 0 and !player->b_skillPoints
    beep; return
  elsif dir > 0 and player->b_level == 1 and ^p == 7
    beep; return
  elsif dir > 0 and ^p == 10
    beep; return
  fin
  ^p = ^p + dir
  clearLittleArea(skillX[skillNum], skillY[skillNum])
  rawDisplayf1("^V%D", skillY[skillNum])
  if ^p > skillMin[skillNum]; rawDisplayStr("^I"); fin
  rightJustifyStr(sprintf1(" %d ", ^p), skillX[skillNum]+SKILL_JUST_OFF)
  rawDisplayStr("^N")

  player->b_skillPoints = player->b_skillPoints - dir
  clearLittleArea(skillX[nSkills], skillY[nSkills])
  rawDisplayf1("^V%D", skillY[nSkills])
  if player->b_skillPoints < skillMin[nSkills]; rawDisplayStr("^I"); fin
  rightJustifyStr(sprintf1(" %d ", player->b_skillPoints), skillX[nSkills]+SKILL_JUST_OFF)
  rawDisplayStr("^N")

  // Adjusting stamina also immediately affects health
  stamDiff = player->b_stamina - stamDiff
  player=>w_maxHealth = max(0, player=>w_maxHealth + stamDiff)
  player=>w_health    = max(0, player=>w_health    + stamDiff)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def clearInvRect()#0
  setWindow(BIGWIN_TOP+9, BIGWIN_BOTTOM-10, BIGWIN_LEFT, INV_RT)
  clearWindow()
  setBigWindow()
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def clearMainRect()#0
  setWindow(BIGWIN_TOP+9, BIGWIN_BOTTOM-10, BIGWIN_LEFT, BIGWIN_RIGHT)
  clearWindow()
  setBigWindow()
  rawDisplayStr("^V000\n^J^J^J")
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def clearMenuRect()#0
  byte h
  h = (mode == 'I') ?? 10 :: 19
  setWindow(BIGWIN_BOTTOM-h, BIGWIN_BOTTOM, BIGWIN_LEFT, BIGWIN_RIGHT)
  clearWindow()
  setBigWindow()
  rawDisplayf1("^V%D", BIGWIN_HEIGHT-h)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def doUseSkill(player)#1
  byte sel

  // If the skill key combos aren't showing, display them
  clearMenuRect
  if !player->b_skillPoints
    clearMainRect()
    canBumpSkills = TRUE // temporarily
    showSkills(player)
  fin

  while TRUE
    clearMenuRect
    rawDisplayf1("\nUse which skill/attr? [A-%c] or [Esc]", nSkills-1+'A')
    sel = getUpperKey()
    if sel >= 'A' and (sel-'A') < nSkills
      playerUsing = player
      return skillName[sel-'A']
    elsif sel == $1B // esc
      break
    else
      beep
    fin
  loop

  // Refresh if we temporarily had to show key combos
  clearMenuRect
  if !player->b_skillPoints
    clearMainRect()
    canBumpSkills = FALSE
    showSkills(player)
  fin
  return NULL // nothing valid
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Display menu for selecting inventory items
def showInvMenu(player, totalItems, itemPage, itemsOnPage)#0
  byte playerCount
  playerCount = countList(global=>p_players)

  clearMenuRect()
  if totalItems > 0
    rawDisplayf2("Item [%s%c], ", itemsOnPage>1 ?? "A-" :: "", itemsOnPage-1+'A')
    if totalItems > INV_ROWS
      rawDisplayStr("Pg [")
      if totalItems > (itemPage + 1) * INV_ROWS; rawDisplayStr(">"); fin
      if itemPage; rawDisplayStr("<"); fin
      rawDisplayStr("], ")
    fin
  fin

  if playerCount > 1; rawDisplayf1("Player [1-%d], ", playerCount); fin

  rawDisplayStr("S)kills, [Esc]")
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Display menu for selecting inventory items
def showSkillsMenu(player)#0
  byte playerCount
  playerCount = countList(global=>p_players)
  clearMenuRect()
  if player=>w_health > 0
    if player->b_skillPoints > 0
      rawDisplayf2("Assign point [A-%c], Undo pt [Ctrl-A - Ctrl-%c], ", nSkills-1+'A', nSkills-1+'A')
    fin
    rawDisplayStr("\nU)se skill/attr, ")
  fin
  rawDisplayStr("X:Inventory, ")
  if playerCount > 1; rawDisplayf1("Player [1-%d], ", playerCount); fin
  rawDisplayStr("[Esc]")
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def isSplittable(item)#1
  // Disallow splitting items with modifiers, because too edge-casey
  return item->t_type == TYPE_FANCY_ITEM and item=>w_count > 1 and !item=>p_modifiers
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def isJoinable(item)#1
  // Disallow joining items with modifiers, because too edge-casey
  return item->t_type == TYPE_FANCY_ITEM and item=>w_count > 0 and !item=>p_modifiers
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Display menu for selecting inventory items
def showItemMenu(player, item)#0
  byte type
  clearMenuRect()
  type = item->t_type
  if player=>w_health > 0
    if type == TYPE_ARMOR or type == TYPE_WEAPON
      if isEquipped(item); rawDisplayStr("un-"); fin
      rawDisplayStr("E)quip, ")
    fin
    if type == TYPE_WEAPON
      if item->b_clipCurrent < item->b_clipSize
        rawDisplayStr("R)eload, ")
      fin
    fin
    rawDisplayStr("U)se, ")
  fin
  if isSplittable(item); rawDisplayStr("S)plit, "); fin
  if isJoinable(item); rawDisplayStr("J)oin, "); fin
  if !isEquipped(item) and item=>w_price > 0
    if global=>p_players=>p_nextObj; rawDisplayStr("T)rade, "); fin
    if item=>w_price > 0; rawDisplayStr("D)rop "); fin
  fin
  rawDisplayStr("or [Esc]")
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def armsMatch(i1, i2)
  if i1 == i2 or i1->t_type <> i2->t_type or !isEquipped(i2); return FALSE; fin
  if i1->t_type == TYPE_WEAPON; return TRUE; fin  // can only equip 1 weapon at a time
  return streqi(i1=>s_itemKind, i2=>s_itemKind) // type must be armor, since isEquipped is true
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def findMatchingEquip(player, item)
  ctx = item; return first(player=>p_items, &(p) armsMatch(ctx, p)))
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Equip/unequip a weapon or armor piece
def doEquip(player, item)#0
  word match
  if item->b_flags & ITEM_FLAG_EQUIP
    // Unequipping a piece
    item->b_flags = item->b_flags & ~ITEM_FLAG_EQUIP
  else
    // Equipping a piece: be sure to unequip other matching piece (if any)
    match = findMatchingEquip(player, item)
    if match; match->b_flags = item->b_flags & ~ITEM_FLAG_EQUIP; fin
    equipItem(item)
  fin
  calcPlayerArmor(player)
end

def choosePlayer(disp, chooseNum, avoid)
  word player
  byte num
  player = global=>p_players
  num = 0
  while player
    if player <> avoid
      if num; rawDisplayStr(", "); fin
      if disp
        rawDisplayf2("%c) %s", num+'A', player=>s_name)
      elsif num == chooseNum
        return player
      fin
      num++
    fin
    player = player=>p_nextObj
  loop
  return num
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def displayResult(s_result, doBeep)#1
  clearMenuRect()
  clearMainRect()
  rawDisplayStr(s_result)
  if doBeep
    beep
    pause(400)
  else
    pause(800)
  fin
  return NULL
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def displayDone()#1
  return displayResult("Done.", FALSE)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Trade an item to another player/npc
def doTrade(player, item)#1
  word destPlayer, destItem
  byte firstTime, sel

  clearMenuRect()
  rawDisplayStr("To: ")
  destPlayer = global=>p_players
  sel = 0
  firstTime = TRUE
  while destPlayer
    if player <> destPlayer
      if !firstTime; rawDisplayStr(", "); fin
      firstTime = FALSE
      rawDisplayf2("%d. %s", sel+1, destPlayer=>s_name)
    fin
    sel++
    destPlayer = destPlayer=>p_nextObj
  loop
  rawDisplayStr(" or [Esc]")

  while TRUE
    sel = getUpperKey()
    if sel == $1B; return 0; fin
    destPlayer = index(global=>p_players, sel-'1')
    if destPlayer and destPlayer <> player
      destItem = scanForNamedObj(destPlayer=>p_items, item=>s_name)
      if destItem
        if destItem->t_type == TYPE_FANCY_ITEM and destItem=>w_count > 0
          removeFromList(@player=>p_items, item)
          destItem=>w_count = min(30000, destItem=>w_count + item=>w_count)
          return displayDone
        fin
      fin
      if roomInPack(destPlayer)
        removeFromList(@player=>p_items, item)
        addToList(@destPlayer=>p_items, item)
        return displayDone
      else
        displayResult("No room.", TRUE)
        beep
        return 0
      fin
    fin
    beep
  loop
  return 0
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Split a stack of stackables
def doSplit(player, item)#1
  word nToSplit, newItem
  if !roomInPack(player)
    return displayResult("No room.", TRUE)
  elsif item=>w_count == 2
    nToSplit = 1
  else
    clearMenuRect
    rawDisplayf1("Split off 1-%d: ", item=>w_count - 1)
    nToSplit = parseDec(getStringResponse())
  fin
  if nToSplit >= 1 and nToSplit <= item=>w_count-1
    item=>w_count = item=>w_count - nToSplit
    newItem = mmgr(HEAP_ALLOC, TYPE_FANCY_ITEM)
    memcpy(item, newItem, TFancyItem, 0)
    newItem=>w_count = nToSplit
    newItem=>p_nextObj = item=>p_nextObj
    item=>p_nextObj = newItem
    return 1
  fin
  return 0
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Join a stack of stackables
def doJoin(player, item)#1
  word match, pPrev
  byte anyJoined
  pPrev = @player=>p_items
  match = player=>p_items
  anyJoined = FALSE
  while match
    if match <> item and match->t_type == TYPE_FANCY_ITEM
      if streqi(match=>s_name, item=>s_name)
        item=>w_count = min(30000, item=>w_count + match=>w_count)
        *pPrev = match=>p_nextObj
        match = match=>p_nextObj
        anyJoined = TRUE
        continue
      fin
    fin
    pPrev = @match=>p_nextObj
    match = *pPrev
  loop
  if anyJoined; return 1; fin
  return displayResult("No joinable stack found.", TRUE)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Select an item and use it. Returns item if it needs to be processed by outer loop, else NULL
def doUseItem(player, item)#1
  word pMod, oldVal, newVal, name
  if item->t_type == TYPE_FANCY_ITEM and item=>p_modifiers
    clearMenuRect()
    clearMainRect()
    pMod = item=>p_modifiers // we only support one per item at present
    oldVal = getStat(player, pMod=>s_name)
    setStat(player, pMod=>s_name, oldVal + pMod=>w_modValue)
    newVal = getStat(player, pMod=>s_name)
    rawDisplayStr(pMod=>s_name)
    if newVal <> oldVal
      removeNamed(item=>s_name, @player=>p_items)  // also handles reducing count of stackables
      if newVal > oldVal
        rawDisplayStr(" increased")
      else
        rawDisplayStr(" decreased")
      fin
      rawDisplayf2(" from %d to %d.", oldVal, newVal)
    else
      rawDisplayStr(" already at the limit.")
    fin
    pause(800)
    return NULL
  fin

  // General 'use' handled by outer engine, because it might involve graphics.
  // Depluralize the name on the way out, because scripts will match on that.
  isPlural = FALSE
  playerUsing = player
  return mmgr(HEAP_INTERN, sprintf1("%s", item=>s_name))
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Select an item and drop it. Returns TRUE if anything changed
def doDestroy(player, item)#1
  clearMenuRect()
  rawDisplayStr("Drop ")
  pItemutilModule()=>itemutil_displayItemName(item)
  rawDisplayStr(" (Y/N)?")
  if getYN()
    removeFromList(@player=>p_items, item)
    calcPlayerArmor(player)
    return TRUE
  fin
  return FALSE
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def displayItems(pItem1, pItem2)#0
  clearMainRect()
  rawDisplayf1("^T%DInventory", STATS_COL_1)
  if pItem2
    rawDisplayf1("^T%DEquipped", STATS_COL_2)
  fin
  rawDisplayStr("^N^J^J")
  pItemutilModule()=>itemutil_displayItemStats(pItem1, pItem2)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def interactWithItem(player, item)#1
  word comp, quantity
  byte sel, ok
  ok = TRUE
  displayItems(item, findMatchingEquip(player, item))
  while TRUE
    showItemMenu(player, item)
    if !ok; beep; fin
    ok = FALSE
    sel = getUpperKey()
    when sel
      // Equip/unequip player with weapon/armor
      is 'E'
        if (item->t_type == TYPE_ARMOR or item->t_type == TYPE_WEAPON) and player=>w_health > 0
          doEquip(player, item)
          return displayDone()
        fin
        break
      // Use an item
      is 'U'
        if player=>w_health > 0
          return doUseItem(player, item) // general 'use' handled by outer engine, because it might involve graphics
        fin
        break
      // Trade an item
      is 'T'
        if global=>p_players=>p_nextObj and !isEquipped(item) and item=>w_price > 0
          return doTrade(player, item)
        fin
        break
      is 'R'
        if item->t_type == TYPE_WEAPON and player=>w_health > 0
          if item->b_clipCurrent < item->b_clipSize
            pItemutilModule()=>itemutil_reloadWeapon(player, item, FALSE)
            displayDone
            displayItems(item, NULL)
            ok = TRUE
          fin
        fin
        break
      // Split a stack
      is 'S'
        if isSplittable(item)
          ok = doSplit(player, item)
          displayItems(item, NULL)
        fin
        break
      // Join stacks
      is 'J'
        if isJoinable(item)
          ok = doJoin(player, item)
          displayItems(item, NULL)
        fin
        break
      // Destroy an item
      is 'D'
        if !isEquipped(item) and item=>w_price > 0
          if doDestroy(player, item)
            displayDone()
            return NULL
          fin
          ok = TRUE
        fin
        break
      is $1B // Esc
        return NULL
    wend
  loop
  return NULL
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Do a cheat command, loading and unloading the godmode module
def callGodModule(funcNum)#0
  word pModule, func
  clearMainRect()
  mmgr(START_LOAD, 1) // code is in partition 1
  pModule = mmgr(QUEUE_LOAD, MOD_GODMODE<<8 | RES_TYPE_MODULE)
  mmgr(FINISH_LOAD, 0)
  func = *(pModule() + funcNum)
  func()
  mmgr(FREE_MEMORY, pModule)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Show player sheet and accept command. If using an item (not just for stats gain)
// the item is returned; else NULL is returned.
def doPlayerSheet(player_num)#1
  word player, item
  byte i_page, totalItems, itemsOnPage, redisplay, sel, noRepeatMenu

  mmgr(START_LOAD, 1) // code is in partition 1
  pItemutilModule = mmgr(QUEUE_LOAD, MOD_ITEMUTIL<<8 | RES_TYPE_MODULE)
  mmgr(FINISH_LOAD, 0)

  setBigWindow()

  i_page = 0
  redisplay = 2
  noRepeatMenu = FALSE
  player = index(global=>p_players, player_num)
  // go straight to level up if applicable, else default to inventory mode
  mode = player->b_skillPoints ?? 'S' :: 'I'
  repeat
    player = index(global=>p_players, player_num)
    if !player; beep; return NULL; fin
    if redisplay >= 2
      clearWindow()
      rawDisplayStr("^I")
      centerStr(sprintf1(" %s ", player=>s_name), BIGWIN_WIDTH)
      rawDisplayStr("^N")
      redisplay = 1
      totalItems = countList(player=>p_items)
      if mode == 'I'
        showDerived(player)
      else // 'S'
        canBumpSkills = player->b_skillPoints > 0
        showSkills(player)
      fin
    elsif redisplay > 0
      clearInvRect()
    fin
    if redisplay > 0 and mode == 'I'
      itemsOnPage = showInventory(player, i_page, 0)
    fin
    redisplay = 0

    if !noRepeatMenu
      if mode == 'I'
        showInvMenu(player, totalItems, i_page, itemsOnPage)
      else // 'S'
        showSkillsMenu(player)
      fin
    fin
    noRepeatMenu = FALSE

    // Get a key, do something
    sel = getUpperKey()
    when sel
      is '1'; is '2'; is '3'
        sel = sel - '1'
        if countList(global=>p_players) > sel
          player_num = sel
          i_page = 0
          redisplay = 2
        else
          beep
        fin
        break
      // Next inventory page
      is '>'; is '.'
        if mode=='I' and totalItems > (i_page + 1) * INV_ROWS
          i_page++
          redisplay = 1
        else
          beep
          noRepeatMenu = TRUE
        fin
        break
      // Previous inventory page
      is '<'; is ','
      //is 8  // left-arrow // NO! overlaps with Ctrl-H on skills screen
        if mode=='I' and i_page
          i_page--
          redisplay = 1
        else
          beep
          noRepeatMenu = TRUE
        fin
        break
      // Other operations...
      is '&' // add health cheat
        if global->b_godmode
          player=>w_health = player=>w_maxHealth
          redisplay = 2
        fin
        break
      is '$' // add gold cheat
        if global->b_godmode
          addGold(500)
          redisplay = 2
        fin
        break
      is '%' // add item cheat
        if global->b_godmode
          callGodModule(godmode_addItem)
          redisplay = 2
        fin
        break
      is '9' // add player cheat
        if global->b_godmode
          callGodModule(godmode_addPlayer)
          redisplay = 2
        fin
        break
      is '+' // level up cheat
        if global->b_godmode
          setStat(player, @S_XP, player=>w_nextXP)
          redisplay = 2
        fin
        break
      is $1B // Esc
        heapCollect()
        return NULL
      otherwise
        if sel == 'X' and mode <> 'I' // switch from stats to inv
          mode = 'I'
          redisplay = 2
        elsif sel == 'S' and mode <> 'S' // switch from inv to stats
          mode = 'S'
          redisplay = 2
        elsif mode == 'I'
          sel = sel - 'A'
          if sel >= 0 and sel < itemsOnPage
            item = interactWithItem(player, index(player=>p_items, i_page * INV_ROWS + sel))
            if item; return item; fin // Use an item
            if countList(player=>p_items) <= i_page * INV_ROWS // destroyed last item on pg 2
              i_page--
            fin
            redisplay = 2
          else
            beep
          fin
        else // mode == 'S'
          if sel == 'U' and player=>w_health > 0
            item = doUseSkill(player)
            if item; return item; fin // Use a skill
          else
            noRepeatMenu = TRUE
            if sel >= 'A' and (sel-'A' < nSkills) and player=>w_health > 0
              adjustSkill(player, sel - 'A', 1)
            elsif sel >= 1 and (sel-1 < nSkills) and player=>w_health > 0
              adjustSkill(player, sel - 1, -1)
            else
              beep
              noRepeatMenu = FALSE
            fin
          fin
        fin
    wend
  until 0
  return NULL // never reached
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Show player sheet and accept command. If using an item (not just for stats gain)
// the item is returned; else NULL is returned.
def _showPlayerSheet(player_num)#1  // funcTbl functions always have to return a value
  word ret

  mmgr(START_LOAD, 1) // code is in partition 1
  pItemutilModule = mmgr(QUEUE_LOAD, MOD_ITEMUTIL<<8 | RES_TYPE_MODULE)
  mmgr(FINISH_LOAD, 0)

  ret = doPlayerSheet(player_num)

  mmgr(FREE_MEMORY, pItemutilModule)
  return ret
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Display a list of NPCs and allow user to select one.
def selectPlayer(players)#1
  byte n_item
  word player

  // First, display the list
  player = players
  n_item = 0
  while player
    if player->b_playerFlags & PLAYER_FLAG_NPC
      displayf1("\n%c)", 'A' + n_item)
      rawDisplayStr("^T018")
      displayStr(player=>s_name)
      n_item++
    fin
    player = player=>p_nextObj
  loop

  if !n_item
    displayStr("No applicable characters.")
    return NULL
  fin

  // Then get a selection
  rawDisplayStr("\nWhich character?")
  n_item = getUpperKey() - 'A'
  return select(players, &(p) p->b_playerFlags & PLAYER_FLAG_NPC, n_item)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Allow user to select an active player, and put them on the bench
def _benchPlayer()#1
  word player
  player = selectPlayer(global=>p_players)
  if player
    removeFromList(@global=>p_players, player)
    addToList(@global=>p_benched, player)
    displayStr("\nDone.")
  fin
  return 0
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Allow user to select a benched player, and put them on the bench
def _unbenchPlayer()#1
  word player
  if countList(global=>p_players) == MAX_PARTY
    displayStr("Party too large.")
    return 0
  fin
  player = selectPlayer(global=>p_benched)
  if player
    removeFromList(@global=>p_benched, player)
    addToList(@global=>p_players, player)
    displayStr("\nDone.")
  fin
  return 0
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Boilerplate module initialization code
return @funcTbl
done
