'OHRRPGCE GAME - Various builtin menus
'(C) Copyright 1997-2020 James Paige, Ralph Versteegen, and the OHRRPGCE Developers
'Dual licensed under the GNU GPL v2+ and MIT Licenses. Read LICENSE.txt for terms and disclaimer of liability.

#include "config.bi"
#include "allmodex.bi"
#include "common.bi" 
#include "loading.bi"
#include "gglobals.bi"
#include "const.bi"
#include "uiconst.bi"
#include "game_udts.bi"
#include "savegame.bi"
#include "sliceedit.bi"
#include "bcommon.bi"
#include "game.bi"
#include "scriptcommands.bi"
#include "yetmore2.bi"
#include "walkabouts.bi"
#include "moresubs.bi"
#include "menustuf.bi"
#include "bmodsubs.bi"
#include "bmod.bi"
#include "plankmenu.bi"

'--SUBs and FUNCTIONS only used locally
DECLARE SUB buystuff_make_slices(buyst as ShopBuyState)
DECLARE SUB buystuff_refresh_menu (buyst as ShopBuyState, menu as MenuDef, st as MenuState)
DECLARE SUB buystuff_refresh_selected (buyst as ShopBuyState, byval thing as NodePtr)
DECLARE SUB buystuff_refresh_empty (buyst as ShopBuyState)
DECLARE SUB buystuff_init_info_menu(buyst as ShopBuyState)
DECLARE SUB buystuff_do_purchase(buyst as ShopBuyState, byval thing as NodePtr, byval thing_slot as integer)
DECLARE FUNCTION buystuff_can_show(byval slot as NodePtr, byval shop_id as integer) as bool
DECLARE FUNCTION buystuff_can_buy(byval thing as NodePtr, buyst as ShopBuyState, byval sound_and_alert as bool=NO) as bool
DECLARE SUB buystuff_set_alert(buyst as ShopBuyState, alert as string, byval ticks as integer = 18)
DECLARE FUNCTION find_already_equipped_by_item (item_id as integer, hero_slot as integer) as integer
DECLARE FUNCTION buystuff_change_preview_hero_equipment (buyst as ShopBuyState, byval thing as NodePtr) as bool


DECLARE SUB equip_menu_setup (byref st as EquipMenuState)
DECLARE SUB equip_menu_do_equip(byval item as integer, byref st as EquipMenuState)
DECLARE SUB equip_menu_back_to_menu(byref st as EquipMenuState)
DECLARE SUB equip_menu_stat_bonus(byref st as EquipMenuState)
DECLARE FUNCTION equip_menu_available_item_id(st as EquipMenuState) as integer
DECLARE FUNCTION equip_menu_available_item_caption(st as EquipMenuState, byval menuslot as integer) as string
DECLARE FUNCTION equip_menu_equipped_item_info(st as EquipMenuState) as string
DECLARE FUNCTION equip_menu_available_item_info(st as EquipMenuState) as string

DECLARE FUNCTION menu_attack_targ_picker(byval attack_id as integer, byval learn_id as integer, byval attacker as integer, use_caption as string, byval x_offset as integer=0, byval really_use_attack as bool=YES) as bool
DECLARE FUNCTION menu_attack_targ_picker_make_slices(x_offset as integer) as Slice Ptr
DECLARE SUB menu_targpick_plank_state_callback (byval sl as Slice Ptr, byval state as PlankItemState)
DECLARE FUNCTION getOOBtarg (byval search_direction as integer, byval attacker as integer, byref target as integer, byval atk as integer) as bool

DECLARE SUB spells_menu_refresh_list(sp as SpellsMenuState)
DECLARE SUB spells_menu_refresh_hero(sp as SpellsMenuState)
DECLARE SUB spells_menu_control(sp as SpellsMenuState)
DECLARE SUB spells_menu_paint (byref sp as SpellsMenuState)

DECLARE FUNCTION picksaveload (byval loading as bool, byval newgame_opt as bool = YES, byval beep_if_no_saves as bool = NO) as integer
DECLARE SUB picksave_draw(menu() as string, byval loading as bool, byval newgame_opt as bool, disable_exit as bool=NO, sprites() as GraphicPair, pv() as SaveSlotPreview, mapname() as string, lev() as string, byref st as MenuState, byval page as integer)
DECLARE FUNCTION picksave_confirm(menu() as string, byval loading as bool, byval newgame_opt as bool, exit_load_disabled as bool=NO, sprites() as GraphicPair, pv() as SaveSlotPreview, mapname() as string, lev() as string, byref st as MenuState, byval holdscreen as integer, byval page as integer) as bool
DECLARE SUB picksave_update_mouse(st as MenuState, byval newgame_opt as bool, byref drag_start_top as integer)
DECLARE SUB sellstuff_refresh(byval last_stuff as integer, byval recordsize as integer, permask() as integer, price() as integer, stuffdata() as integer)
DECLARE SUB sellstuff_infostr(byval ic as integer, info as string, b() as integer, permask() as integer, price() as integer, byval stuff_count as integer, byval recordsize as integer)

DECLARE SUB get_virtual_keyboard_buttons (byval sl as Slice Ptr, byref arr as any ptr vector)

ENUM ShopMenuItemType
 shopmtypeItem = mtypeLAST + 1  'Don't overlap with MenuItemType values to avoid draw_menu glitches
 shopmtypeHero
END ENUM


SUB buystuff (byval shop_id as integer, byval shoptype as integer, storebuf() as integer)
 'shoptype is 0 for items and 1 for heroes

 DIM buyst as ShopBuyState
 WITH buyst
  .shop_id = shop_id
  .shoptype = shoptype
  .preview_hero_slot = -1
 END WITH

 DIM stuff_doc as DocPtr
 stuff_doc = CreateDocument()

 buyst.stuff_list = CreateNode(stuff_doc, "stuff_list")
 SetRootNode(stuff_doc, buyst.stuff_list)
 
 load_shop_stuff shop_id, buyst.stuff_list

 DIM holdscreen as integer = duplicatepage(vpage)

 '--walking frame numbers used for animating heroes
 DIM walks(15) as integer
 FOR i as integer = 0 TO 10 STEP 2
  walks(i) = frameSTEP
  walks(i+1) = frameSTAND
 NEXT i
 walks(11) = frameATTACKA
 walks(12) = frameATTACKA
 walks(13) = frameATTACKB
 walks(14) = frameATTACKB

 buystuff_make_slices buyst
 DIM panel as Slice Ptr = LookupSlice(SL_SHOP_BUY_INFO_PANEL, buyst.root_sl)
 IF panel = 0 THEN debug "Shop Buy Slice Collection is missing its Info Panel"

 '--Create the menu
 DIM menu as MenuDef
 
 DIM st as MenuState
 st.active = YES
 st.select_by_mouse_release = YES

 buystuff_refresh_menu buyst, menu, st

 IF menu.numitems = 0 THEN
  'The menu will immediately quit
  menusound gen(genCancelSFX)
 ELSE
  show_virtual_gamepad()
  menusound gen(genAcceptSFX)
 END IF

 setkeys
 DO
  setwait speedcontrol
  setkeys
  buyst.tog = buyst.tog XOR 1
  IF buyst.tog THEN loopvar buyst.walk, 0, 15
  DIM do_quit as bool = NO
  DIM do_pick as bool = NO

  playtimer

  IF get_gen_bool("/mouse/mouse_menus") THEN
   IF panel ANDALSO SliceCollidePoint(panel, readmouse.pos) THEN
    'Ignore most clicks on the info panel
     IF (readmouse.release AND mouseRight) ANDALSO readmouse.drag_dist < 10 THEN do_quit = YES
   ELSEIF rect_collide_point(st.rect, readmouse.pos) THEN
    'In the list of stuff
    IF st.hover >= 0 THEN
     IF (readmouse.release AND mouseLeft) ANDALSO readmouse.drag_dist < 10 ANDALSO st.pt = st.hover THEN do_pick = YES
    END IF
    mouse_drag_menu st
   ELSE
    'Anywhere else
     IF (readmouse.release AND mouseLeft) ANDALSO readmouse.drag_dist < 10 THEN do_quit = YES
     IF (readmouse.release AND mouseRight) ANDALSO readmouse.drag_dist < 10 THEN do_quit = YES
   END IF
  END IF

  usemenusounds
  IF usemenu(st) THEN
   IF menu.numitems > 0 THEN
    buystuff_refresh_selected buyst, menu.items[st.pt]->dataptr
   END IF
  END IF
  IF game_check_cancel_key() THEN do_quit = YES
  IF game_check_use_key() THEN do_pick = YES

  IF menu.numitems > 0 THEN
   IF buystuff_change_preview_hero_equipment(buyst, menu.items[st.pt]->dataptr) THEN
    menusound gen(genCursorSFX)
    buystuff_refresh_selected buyst, menu.items[st.pt]->dataptr
   END IF
  END IF

  buyst.alert_ticks = large(0, buyst.alert_ticks - 1)
  IF buyst.alert_ticks <= 0 THEN
   buyst.alert_box->Visible = NO
  END IF

  IF menu.numitems > 0 THEN
   buyst.selected = menu.items[st.pt]->dataptr
   buyst.curslot = menu.items[st.pt]->sub_t
  END IF

  IF keyval(scF8) > 1 THEN
   slice_editor buyst.root_sl
   st.need_update = YES
  END IF
  
  IF do_quit THEN
   menusound gen(genCancelSFX)
   EXIT DO
  END IF

  IF do_pick ANDALSO menu.numitems > 0 THEN
   IF buystuff_can_buy(buyst.selected, buyst, YES) THEN
    buystuff_do_purchase(buyst, buyst.selected, buyst.curslot)
    st.need_update = YES
   END IF
  END IF

  IF st.need_update THEN
   buystuff_refresh_menu buyst, menu, st
   st.need_update = NO
  END IF

  'Auto-quit when no items left. Don't play CancelSFX, as we might have just bought something
  IF menu.numitems = 0 ANDALSO buyst.alert_ticks = 0 THEN EXIT DO

  'Animate hero walking to indicate equipability
  DIM hero_sl as Slice Ptr
  FOR i as integer = 0 TO 3
   IF buyst.party_sl(i)->Extra(0) THEN
    ChangeSpriteSlice buyst.party_sl(i), , , , walks(buyst.walk)
   ELSE
    ChangeSpriteSlice buyst.party_sl(i), , , , 0
   END IF
  NEXT i
  ChangeSpriteSlice buyst.hire_sl, , , , walks(buyst.walk)


  'Draw the screen
  copypage holdscreen, vpage
  'Draw menu first so it goes below pricebox in case of overlap
  draw_menu menu, st, vpage
  DrawSlice buyst.root_sl, vpage
  draw_menu buyst.info, buyst.info_st, vpage
  setvispage vpage

  dowait
 LOOP

 '--Restore the saved screen
 freepage holdscreen

 DeleteSlice @(buyst.root_sl)
 FreeDocument stuff_doc
END SUB

SUB buystuff_do_purchase(buyst as ShopBuyState, byval thing as NodePtr, byval thing_slot as integer)

 IF thing = 0 THEN debug "buystuff_do_purchase null thing ptr" : EXIT SUB

 DIM thing_kind as string
 thing_kind = NodeName(thing)
 DIM buy as NodePtr
 
 IF thing_kind = "hero" THEN
  buy = thing."hire".ptr
  menusound gen(genHireSFX)
  DIM hero_id as integer = GetInteger(thing)
  DIM party_slot as integer = first_free_slot_in_party()
  DIM her as HeroDef
  loadherodata her, hero_id
  DIM hire_level as integer
  IF thing."level".exists THEN
   hire_level = thing."level".integer
  ELSE
   hire_level = her.def_level
   IF hire_level < 0 THEN hire_level = averagelev()
  END IF
  addhero hero_id, party_slot, hire_level
  buystuff_set_alert buyst, gam.hero(party_slot).name & " " & readglobalstring(95, "Joined!", 20)
 ELSEIF thing_kind = "item" THEN
  buy = thing."buy".ptr
  menusound gen(genBuySFX)
  DIM item_id as integer = GetInteger(thing)
  getitem item_id, 1
  buystuff_set_alert buyst, readglobalstring(93, "Purchased", 20) & " " & readitemname(item_id)
 ELSE
  debug "buystuff_do_purchase: invalid thing """ & thing_kind & """"
  menusound gen(genCantBuySFX)
  EXIT SUB
 END IF

 'deduct stock from gam.stock() (-1 is infinite, 0 is unloaded, 1+ is actual stock +1)
 DIM stockidx as integer = thing."stockidx".default(thing_slot)
 IF gam.stock(buyst.shop_id, stockidx) > 1 THEN
  gam.stock(buyst.shop_id, stockidx) -= 1
 END IF
 
 DIM set_tag as integer = buy."set_tag".default(0).integer
 IF ABS(set_tag) >= 2 THEN
  IF set_tag > 0 THEN settag set_tag, YES
  IF set_tag < 0 THEN settag ABS(set_tag), NO
 END IF
 
 gold -= buy."price".default(0).integer
 
 READNODE buy, ignoreall
  WITHNODE buy."trade" as trade
   delitem GetInteger(trade), trade."count".integer
  END WITHNODE
 END READNODE

 'the last thing to do is re-eval the item and hero tags in case
 'something changed: we do this immediately because the tags affect
 'purchaseability of other items
 evalherotags
 evalitemtags
 tag_updates
END SUB

FUNCTION buystuff_change_preview_hero_equipment (buyst as ShopBuyState, byval thing as NodePtr) as bool
 'Check if the current thing is an item
 'Check if it is equippable
 'If so, check left and right keypresses to update which hero to preview
 'Returns YES on any change, NO on no change

 DIM thing_kind as string = NodeName(thing)
 IF thing_kind <> "item" THEN RETURN NO
 DIM item_id as integer = GetInteger(thing)
 IF item_id = -1 THEN RETURN NO
 DIM item as ItemDef
 loaditemdata item, item_id

 'Check which heroes can equip
 DIM eq_any as bool = NO
 DIM can_equip(3) as bool
 DIM hero_id as integer
 FOR i as integer = 0 to 3
  hero_id = gam.hero(i).id
  IF hero_id >= 0 THEN
   IF item_read_equipbit(item, hero_id) THEN
    eq_any = YES
    can_equip(i) = YES
   END IF
  END IF
 NEXT i
 
 DIM slot as integer = buyst.preview_hero_slot

 IF eq_any = NO THEN
  slot = -1
 ELSE
  'Check keys
  DIM direction as integer = 0
  IF keyval(ccLeft) > 1 THEN direction = -1
  IF keyval(ccRight) > 1 THEN direction = 1
  IF direction <> 0 THEN  
   DO
    loopvar slot, -1, 3, direction
   LOOP UNTIL slot = -1 ORELSE can_equip(slot)
  END IF
 END IF

 IF get_gen_bool("/mouse/mouse_menus") THEN
  DIM panel as Slice Ptr = LookupSlice(SL_SHOP_BUY_INFO_PANEL, buyst.root_sl)
  IF panel ANDALSO SliceCollidePoint(panel, readmouse.pos) THEN
   'A click on the info panel
   IF (readmouse.release AND mouseLeft) ANDALSO readmouse.drag_dist < 10 THEN
    slot = -1
    FOR i as integer = 0 TO 3
     IF buyst.party_box(i) ANDALSO can_equip(i) ANDALSO SliceCollidePoint(buyst.party_box(i), readmouse.pos) THEN
      slot = i
     END IF
    NEXT i
   END IF
  END IF
 END IF
 
 DIM result as integer = slot <> buyst.preview_hero_slot
 buyst.preview_hero_slot = slot
 RETURN result
END FUNCTION

'thing is either "hero" or "item" node of a shop thing, in the new RELOAD respresentation of STF
SUB buystuff_refresh_selected (buyst as ShopBuyState, byval thing as NodePtr)
 ChangeTextSlice buyst.money_sl, readglobalstring(145, "You have", 20) & " " & price_string(gold)

 DIM thing_kind as string = NodeName(thing)
 DIM thing_id as integer = GetInteger(thing)
 DIM buy as NodePtr
 DIM info as string
 DIM item as ItemDef
 DIM trade_str as string
 
 buystuff_init_info_menu buyst
 
 IF thing_kind = "hero" THEN
  buy = thing."hire".ptr
  trade_str = readglobalstring(87, "Joins for", 20)
  DIM her as HeroDef
  loadherodata her, thing_id
  append_menu_item buyst.info, her.name
  buyst.info.last->disabled = YES
  DIM hire_level as integer
  IF thing."level".exists THEN
   hire_level = thing."level".integer
  ELSE
   hire_level = her.def_level
   IF hire_level < 0 THEN hire_level = averagelev()
  END IF
  loaditemdata item, her.def_weapon
  append_menu_item buyst.info, (atlevel(hire_level, her.lev0.hp, her.levMax.hp) + item.stat_bonuses.hp) & " " & statnames(statHP)
  buyst.info.last->disabled = YES
  buyst.hero_box->Visible = NO
  buyst.hire_box->Visible = YES
  ChangeSpriteSlice buyst.hire_sl, , her.sprite, her.sprite_pal
  IF her.portrait >= 0 THEN
   buyst.portrait_box->Visible = YES
   ChangeSpriteSlice buyst.portrait_sl, , her.portrait, her.portrait_pal
  ELSE
   buyst.portrait_box->Visible = NO
  END IF
 ELSEIF thing_kind = "item" THEN
  buy = thing."buy".ptr
  
  trade_str = readglobalstring(85, "Trade for", 20)
  append_menu_item buyst.info, readitemname(thing_id)

  DIM desc_str as string = wordwrap(readitemdescription(thing_id), buyst.info_sl->Width / 8)
  REDIM spl() as string
  split desc_str, spl()
  FOR i as integer = 0 TO UBOUND(spl)
   append_menu_item buyst.info, spl(i)
   buyst.info.last->col = uilook(uiBuyItemDescription)
  NEXT i
  
  buyst.hire_box->Visible = NO
  buyst.portrait_box->Visible = NO
  loaditemdata item, thing_id
  IF item_is_equippable(item) THEN
   'This item is equippable
   buyst.hero_box->Visible = YES
   DIM eqprefix as string = readglobalstring(99, "Equip:", 10)
   IF item.eqslots(0) THEN append_menu_item buyst.info, eqprefix & " " & readglobalstring(38, "Weapon", 10)
   FOR i as integer = 1 to 4
    IF item.eqslots(i) THEN append_menu_item buyst.info, eqprefix & " " & readglobalstring(24 + i, "Armor" & i)
   NEXT i 
   'Preview which heroes can equip it
   DIM hero_id as integer
   DIM can_equip(3) as bool
   DIM equip_any as bool = NO
   FOR i as integer = 0 TO 3
    buyst.party_sl(i)->Extra(0) = NO
    ChangeRectangleSlice buyst.party_box(i), 0, , , , transOpaque
    hero_id = gam.hero(i).id
    IF hero_id >= 0 THEN
     IF item_read_equipbit(item, hero_id) THEN
      can_equip(i) = YES
      equip_any = YES
      buyst.party_sl(i)->Extra(0) = YES
      ChangeRectangleSlice buyst.party_box(i), 3, , , , transOpaque
     END IF
    END IF
    ChangeRectangleSlice buyst.party_box(i), , , , borderLine
   NEXT i
   'Validate which hero party slot we are previewing equipment changes for
   IF buyst.preview_hero_slot >= 0 ANDALSO NOT can_equip(buyst.preview_hero_slot) THEN
    'Selected hero can't equip, reselect the first one that can
    buyst.preview_hero_slot = -1
    FOR i as integer = 0 TO 3
     IF can_equip(i) THEN
      buyst.preview_hero_slot = i
      EXIT FOR
     END IF
    NEXT i
   END IF
   'Show equipment bonuses
   DIM bonus as integer
   DIM bonus_info as string
   DIM eq_id as integer = -1
   DIM already_item as ItemDef
   IF buyst.preview_hero_slot >= 0 THEN
    eq_id = find_already_equipped_by_item(thing_id, buyst.preview_hero_slot)
    IF eq_id >= 0 THEN loaditemdata already_item, eq_id
   END IF
   FOR i as integer = 0 to 11
    bonus = item.stat_bonuses.sta(i)
    IF eq_id >= 0 THEN
     bonus = item.stat_bonuses.sta(i) - already_item.stat_bonuses.sta(i)
    END IF
    bonus_info = ""
    IF bonus <> 0 THEN
     IF bonus > 0 THEN
      bonus_info = "+" & bonus & " " & statnames(i)
      append_menu_item buyst.info, bonus_info
      buyst.info.last->col = uilook(uiBuyBonusUp)
     END IF
     IF bonus < 0 THEN
      append_menu_item buyst.info, bonus & " " & statnames(i)
      buyst.info.last->col = uilook(uiBuyBonusDown)
     END IF
    END IF
   NEXT i
   buyst.preview_cursor->Visible = NO
   DIM preview_str as string
   IF equip_any = NO THEN
    preview_str = readglobalstring(337, "No heroes can equip", 30)
    append_menu_item buyst.info, preview_str
   ELSEIF buyst.preview_hero_slot = -1 THEN
    preview_str = readglobalstring(334, "< preview hero >", 30)
    append_menu_item buyst.info, preview_str
   ELSEIF eq_id >= 0 THEN
    DIM vs_eq as string = readglobalstring(340, "Vs equipped $I", 30)
    replacestr vs_eq, "$I", already_item.name
    append_menu_item buyst.info, vs_eq
    buyst.preview_cursor->Visible = YES
    buyst.preview_cursor->X = buyst.party_box(buyst.preview_hero_slot)->X + buyst.party_box(buyst.preview_hero_slot)->Width / 2
   END IF
  ELSE
   'Not equippable
   buyst.hero_box->Visible = NO
  END IF
 END IF

 '--show stock
 ' There's no real need to provide a default for stockidx; it should always exist
 DIM stockamount as integer = gam.stock(buyst.shop_id, thing."stockidx")
 IF stockamount > 1 THEN
  append_menu_item buyst.info, stockamount - 1 & " " & readglobalstring(97, "in stock", 20)
 END IF

 DIM already_count as integer = countitem(thing_id)
 IF thing_kind = "item" ANDALSO already_count > 0 THEN
  append_menu_item buyst.info, readglobalstring(324, "You own", 20) & " " & already_count
 END IF

 DIM equipped_count as integer = count_equipped_item(thing_id)
 IF equipped_count > 0 THEN
  append_menu_item buyst.info, readglobalstring(326, "Equipped", 20) & " " & equipped_count
 END IF
 
 IF buy THEN
  DIM price as integer = buy."price".default(0).integer
  DIM price_str as string = ""
  IF price THEN
   price_str = price_string(price)
  END IF
  READNODE buy, ignoreall
   WITHNODE buy."trade" as trade
    IF LEN(price_str) THEN
     price_str &= ", "
    ELSE
     price_str &= trade_str & " "
    END IF
    price_str &= readitemname(GetInteger(trade))
    DIM count as integer = trade."count".default(0).integer
    IF count > 1 THEN price_str &= CHR(1) & count
    DIM own as string = readglobalstring(328, " (have $N)", 20)
    replacestr own, "$N", STR(countitem(GetInteger(trade)))
    price_str &= own
   END WITHNODE
  END READNODE
  'Set price text but don't wrap yet
  ChangeTextSlice buyst.price_sl, price_str, , , NO
  buyst.price_sl->Fill = NO
  IF LEN(price_str) THEN
   buyst.price_box->Visible = YES
   buyst.price_box->Width = small(buyst.price_sl->Width + 16, buyst.root_sl->Width)
   buyst.price_box->Height = 16
   IF buyst.price_box->Width >= buyst.root_sl->Width THEN
    'Fix the box at max width and wrap
    buyst.price_sl->Fill = YES
    buyst.price_sl->FillMode = sliceFillHoriz
    'Update width of price_sl
    RefreshSliceScreenPos buyst.price_sl
    'Enable wrapping, which calculates the price_sl height
    ChangeTextSlice buyst.price_sl, , , , YES
    buyst.price_box->Height = 6 + buyst.price_sl->Height
   END IF
  ELSE
   buyst.price_box->Visible = NO
  END IF
 END IF

 init_menu_state buyst.info_st, buyst.info
 buyst.info_st.active = NO
 
END SUB

FUNCTION find_already_equipped_by_item (item_id as integer, hero_slot as integer) as integer
 'Given an item and a hero slot,
 'check which slot the item is equippable in
 '(use the first if there are multiple, we might add a "preferred" slot in the future)
 'then check what item the hero in that slot has equipped currently, and return its id
 'or -1 for nothing
 IF gam.hero(hero_slot).id = -1 THEN RETURN -1
 IF item_id = -1 THEN RETURN -1
 DIM item as ItemDef
 loaditemdata item, item_id
 DIM eq_slot as integer = -1
 'For now we just pick the first slot, but eventually we might want a preferred slot
 'or per-hero slot exceptions or someting.
 FOR i as integer = 0 to UBOUND(item.eqslots)
  IF item.eqslots(i) THEN
   eq_slot = i
   EXIT FOR
  END IF
 NEXT i
 IF eq_slot = -1 THEN RETURN -1
 RETURN gam.hero(hero_slot).equip(eq_slot).id
END FUNCTION

SUB buystuff_refresh_empty (buyst as ShopBuyState)
 ChangeTextSlice buyst.money_sl, readglobalstring(145, "You have", 20) & " " & price_string(gold)

 buystuff_init_info_menu buyst

 buyst.hero_box->Visible = NO
 buyst.hire_box->Visible = NO

 ChangeTextSlice buyst.price_sl, readglobalstring(309, "The shop is empty", 30)
 buyst.price_box->Width = buyst.price_sl->Width + 16
 'Move the box up so it's not hidden behind the 'Bought...' alert box: once the
 'alert disappears, the menu will close.
 WITH *buyst.price_box
  .AnchorVert = alignCenter
  .AlignVert = alignCenter
 END WITH

 init_menu_state buyst.info_st, buyst.info
 buyst.info_st.active = NO
 
END SUB

SUB buystuff_init_info_menu(buyst as ShopBuyState)
 ClearMenuData buyst.info
 IF buyst.info_sl = 0 THEN
  debug "buyst.info_sl null ptr"
 ELSE
  WITH buyst.info
   .offset.x = buyst.info_sl->ScreenX - (vpages(vpage)->w / 2) + buyst.info_sl->Width / 2
   .offset.y = buyst.info_sl->ScreenY - (vpages(vpage)->h / 2 + 2)
   .anchorvert = alignTop
   .bordersize = -8
   .maxrows = buyst.info_sl->Height / 8
   .itemspacing = -2
   .min_chars = buyst.info_sl->Width / 8
   .max_chars = buyst.info_sl->Width / 8
   .no_box = YES 'comment this out if you want to change/debug the box positioning
  END WITH
 END IF
END SUB

'Initialises or resets an element of gam.stock() as needed, and returns its value.
'Pass in data from the STF record:
'stockidx:      This is the non-offset index in gam.stock(): subtract 1 from STF 'stockidx' value
'thingtype:     0 for items, 1 for heroes
'initial_stock: 'In Stock' field, which differs from gam.stock(): -1 for infinite, >= 0 for finite.
FUNCTION initialize_stock OVERLOAD (shop_id as integer, stockidx as integer, thingtype as integer, thingid as integer, initial_stock as integer) as integer

 DIM byref stock as integer = gam.stock(shop_id, stockidx)
 DIM byref origstock as OriginalStock = gam.original_stock(shop_id, stockidx)

 'If this thing record has been edited since 'stock' was initialised, reset it
 '(When loading old saved games, gam.stock() will be initialised but gam.original_stock() won't be)
 WITH origstock
  IF .thingtype >= 0 THEN  'Is initialised
   IF .thingtype <> thingtype ORELSE .thingid <> thingid ORELSE _
      (.stock < 0) <> (initial_stock < 0) THEN
    'The 'thing' changed, or the In Stock level was changed to/from Infinite
    stock = 0  'Reset gam.stock()
    .thingtype = -1  'Reset gam.original_stock()
   ELSEIF .stock <> initial_stock THEN
    'If the initial stock was changed to some different finite value, adjust by the difference
    DIM diff as integer = initial_stock - .stock
    stock = large(1, stock + diff)  'Minimum of 0 in stock
    .thingtype = -1  'Reset gam.original_stock()
   END IF
  END IF

  IF .thingtype < 0 THEN  'Needs (re-)initialising
   .thingtype = thingtype
   .thingid = thingid
   .stock = initial_stock
  END IF
 END WITH

 'Load stock data if uninitialised or reset
 IF stock = 0 THEN stock = IIF(initial_stock < 0, initial_stock, initial_stock + 1)

 RETURN stock
END FUNCTION

'Initialises or resets an element of gam.stock() as needed, and returns its value.
FUNCTION initialize_stock(shop_id as integer, slot as NodePtr) as integer
 DIM thing as NodePtr
 IF slot."hero".exists THEN
  thing = slot."hero".ptr
 ELSE
  thing = slot."item".ptr
 END IF

 'There's no real need to provide a default for stockidx, it should always exist
 DIM stockidx as integer = thing."stockidx".default(GetInteger(slot))
 DIM thingtype as integer = IIF(slot."item".exists, 0, 1)
 DIM thingid as integer = GetInteger(thing)
 DIM initial_stock as integer
 IF thing."stock".exists THEN
  initial_stock = thing."stock"
 ELSEIF thing."infinite".exists THEN
  initial_stock = -1
 END IF

 RETURN initialize_stock(shop_id, stockidx, thingtype, thingid, initial_stock)
END FUNCTION

FUNCTION buystuff_can_show(byval slot as NodePtr, byval shop_id as integer) as bool
 'Return YES if this thing should appear in the menu

 DIM thing as NodePtr
 DIM buy as NodePtr
 IF slot."hero".exists THEN
  thing = slot."hero".ptr
  buy = thing."hire".ptr
 ELSEIF slot."item".exists THEN
  thing = slot."item".ptr
  buy = thing."buy".ptr
 ELSE
  debug "buystuff_can_show: no hero or item node"
  RETURN NO
 END IF

 'Initialise and retrieve gam.stock() value
 DIM stock as integer = initialize_stock(shop_id, slot)

 IF stock = 1 THEN RETURN NO  'This item is out of stock

 IF buy."require_tag".exists THEN
  IF NOT istag(tag(), buy."require_tag", YES) THEN
   RETURN NO
  END IF
 END IF

 RETURN YES
END FUNCTION

FUNCTION buystuff_can_buy(byval thing as NodePtr, buyst as ShopBuyState, byval sound_and_alert as bool=NO) as bool
 IF thing = 0 THEN debug "buystuff_can_buy: null thing ptr" : RETURN NO
 
 DIM thing_kind as string = NodeName(thing)
 DIM thing_id as integer = GetInteger(thing)
 DIM buy as Nodeptr
 DIM alert as string
 
 IF thing_kind = "hero" THEN
  buy = thing."hire".ptr
  'debug "Hero=" & getheroname(thing_id)
  IF active_party_size() = active_party_slots() ORELSE free_slots_in_party() <= 0 THEN
   IF sound_and_alert THEN
    menusound gen(genCantBuySFX)
    buystuff_set_alert buyst, readglobalstring(100, "No room in party", 20)
   END IF
   RETURN NO
  END IF
  alert = readglobalstring(91, "Cannot Hire", 20)
  
 ELSEIF thing_kind = "item" THEN
  buy = thing."buy".ptr
  'debug "item=" & readitemname(thing_id)
  IF NOT room_for_item(thing_id) THEN
   IF sound_and_alert THEN
    menusound gen(genCantBuySFX)
    buystuff_set_alert buyst, readglobalstring(305, "No room in inventory", 30)
   END IF
   RETURN NO
  END IF
  alert = readglobalstring(89, "Cannot Afford", 20)
  
 ELSE
  debug "buystuff_can_buy: unknown stuff kind """ & thing_kind & """"
  menusound gen(genCantBuySFX)
  RETURN NO
 END IF
 
 'debug "  price=" & buy."price".default(0).integer
 IF buy."price".default(0).integer > gold THEN
  IF sound_and_alert THEN
   menusound gen(genCantBuySFX)
   buystuff_set_alert buyst, alert
  END IF
  RETURN NO
 END IF
 
 READNODE buy, ignoreall
  WITHNODE buy."trade" as trade
   'debug "  trade=" & GetInteger(trade) & " " & readitemname(GetInteger(trade)) & " *" & trade."count".integer
   IF countitem(GetInteger(trade)) < trade."count".integer THEN
    IF sound_and_alert THEN
     menusound gen(genCantBuySFX)
     buystuff_set_alert buyst, alert
    END IF
    RETURN NO
   END IF
  END WITHNODE
 END READNODE

 RETURN YES
END FUNCTION

FUNCTION is_shop_empty(byval shop_id as integer, byval shoptype as integer) as bool
 'Load the shop stuff state
 DIM stuff_doc as DocPtr
 stuff_doc = CreateDocument()
 DIM stuff_list as NodePtr
 stuff_list = CreateNode(stuff_doc, "stuff_list")
 SetRootNode(stuff_doc, stuff_list)
 load_shop_stuff shop_id, stuff_list

 DIM available_stuff_count as integer = 0
 READNODE stuff_list
  WITHNODE stuff_list."stuff_slot" as stuff_slot
   IF buystuff_can_show(stuff_slot, shop_id) THEN
    IF shoptype = 0 THEN
     IF stuff_slot."item".exists THEN
      available_stuff_count += 1
     END IF
    ELSEIF shoptype = 1 THEN
     IF stuff_slot."hero".exists THEN
      available_stuff_count += 1
     END IF
    END IF
   END IF
  END WITHNODE
 END READNODE

 RETURN available_stuff_count = 0
END FUNCTION

SUB buystuff_refresh_menu (buyst as ShopBuyState, menu as MenuDef, st as MenuState)

 DIM thing as NodePtr
 DIM id as integer
 ClearMenuData menu
 WITH menu
  .textalign = alignLeft
  .anchorhoriz = alignLeft
  .anchorvert = alignTop
  .offset.x = -155
  .offset.y = -96
  .bordersize = -2
  .min_chars = 16
  .max_chars = 16
  .maxrows = 16
  .highlight_selection = YES
 END WITH
 DIM stuff_list as NodePtr = buyst.stuff_list
 READNODE stuff_list
  WITHNODE stuff_list."stuff_slot" as stuff_slot
   IF buystuff_can_show(stuff_slot, buyst.shop_id) THEN
    IF buyst.shoptype = 0 THEN
     IF stuff_slot."item".exists THEN
      thing = stuff_slot."item".ptr
      id = stuff_slot."item".integer
      append_menu_item menu, thing."name".default(readitemname(id)).string
      menu.last->t = shopmtypeItem  'Not actually used
      menu.last->sub_t = GetInteger(stuff_slot)
      menu.last->dataptr = thing
      menu.last->disabled = NOT buystuff_can_buy(thing, buyst)
     END IF
    ELSEIF buyst.shoptype = 1 THEN
     IF stuff_slot."hero".exists THEN
      thing = stuff_slot."hero".ptr
      id = stuff_slot."hero".integer
      append_menu_item menu, thing."name".default(getheroname(id)).string
      menu.last->t = shopmtypeHero  'Not actually used
      menu.last->sub_t = GetInteger(stuff_slot)
      menu.last->dataptr = thing
      menu.last->disabled = NOT buystuff_can_buy(thing, buyst)
     END IF
    END IF
   END IF
  END WITHNODE
 END READNODE
   
 FOR i as integer = 0 TO 3
  IF gam.hero(i).id >= 0 THEN
   ChangeSpriteSlice buyst.party_sl(i), , gam.hero(i).battle_pic, gam.hero(i).battle_pal
  ELSE
   buyst.party_sl(i)->Visible = NO
  END IF
 NEXT i

 buyst.price_box->Width = buyst.price_sl->Width + 16

 init_menu_state st, menu

 IF menu.numitems = 0 THEN
  buystuff_refresh_empty buyst
 ELSE
  buystuff_refresh_selected buyst, menu.items[st.pt]->dataptr
 END IF

END SUB

SUB buystuff_make_slices(buyst as ShopBuyState)
 '--Create the slices
 
 buyst.root_sl = NewSliceOfType(slSpecial)
 WITH *buyst.root_sl
  .Fill = YES
 END WITH
 RefreshSliceScreenPos buyst.root_sl

 'The container for the right side of the shop
 DIM right_panel as Slice Ptr
 right_panel = NewSliceOfType(slContainer, buyst.root_sl)
 WITH *right_panel
  .AnchorHoriz = alignRight
  .AlignHoriz = alignRight
  .Width = 168
  .Height = 176
  .PaddingTop = 25
  .Lookup = SL_SHOP_BUY_INFO_PANEL
 END WITH
 
 DIM your_money_box as Slice Ptr
 your_money_box = NewSliceOfType(slRectangle, right_panel)
 WITH *your_money_box
  .AnchorHoriz = alignCenter
  .AlignHoriz = alignCenter
  .Width = .parent->Width
  .Height = INT(.parent->PaddingTop * 0.75)
  .Y = -(.parent->PaddingTop) + 4
 END WITH
 ChangeRectangleSlice your_money_box, 3, , , , transOpaque
 
 buyst.money_sl = NewSliceOfType(slText, your_money_box)
 WITH *(buyst.money_sl)
  .AnchorHoriz = alignCenter
  .AnchorVert = alignCenter
  .AlignHoriz = alignCenter
  .AlignVert = alignCenter
 END WITH
 ChangeTextSlice buyst.money_sl, "You have ??? Gold" , uilook(uiGold), YES 
  
 DIM info_box as Slice Ptr
 info_box = NewSliceOfType(slRectangle, right_panel)
 WITH *info_box
  .Fill = YES
  .PaddingTop = 4
  .PaddingBottom = 4
  .PaddingLeft = 4
  .PaddingRight = 4
 END WITH
 ChangeRectangleSlice info_box, 0, , , , transOpaque
 RefreshSliceScreenPos info_box

 buyst.hero_box = NewSliceOfType(slContainer, info_box)
 WITH *(buyst.hero_box)
  .AnchorVert = alignBottom
  .AlignVert = alignBottom
  .Width = .parent->Width - (.parent->PaddingLeft + .parent->PaddingRight)
  .Height = 44
 END WITH
 
 DIM w_tmp as integer = buyst.hero_box->Width / 4
 FOR i as integer = 0 TO 3
  buyst.party_box(i) = NewSliceOfType(slRectangle, buyst.hero_box)
  WITH *(buyst.party_box(i))
   .Width = 38
   .Height = 44
   .X = w_tmp * i + (w_tmp - 32) / 2
  END WITH
  ChangeRectangleSlice buyst.party_box(i), 0, , , , transOpaque
  ChangeRectangleSlice buyst.party_box(i), , , , borderLine
  buyst.party_sl(i) = NewSliceOfType(slSprite, buyst.party_box(i))
  WITH *(buyst.party_sl(i))
   .AnchorVert = alignCenter
   .AlignVert = alignCenter
   .AnchorHoriz = alignCenter
   .AlignHoriz = alignCenter
  END WITH
  ChangeSpriteSlice buyst.party_sl(i), sprTypeHero
 NEXT i
 
 buyst.preview_cursor =  NewSliceOfType(slText, buyst.hero_box)
 ChangeTextSlice buyst.preview_cursor, CHR(24), uiSelectedItem2 * -1 - 1, YES
 WITH *(buyst.preview_cursor)
  .AnchorVert = alignBottom
  .AnchorHoriz = alignCenter
 END WITH
 buyst.preview_cursor->Visible = NO
 
 'info_sl is just used later to set the position and size of the info menu
 buyst.info_sl = NewSliceOfType(slContainer, info_box)
 WITH *(buyst.info_sl)
  .Width = .parent->Width - (.parent->PaddingLeft + .parent->PaddingRight)
  .Height = .parent->Height - (.parent->PaddingTop + .parent->PaddingBottom) - buyst.hero_box->Height
 END WITH
 RefreshSliceScreenPos buyst.info_sl

 buyst.hire_box = NewSliceOfType(slRectangle, right_panel)
 WITH *(buyst.hire_box)
  .AnchorVert = alignBottom
  .AlignVert = alignBottom
  .X = 30
  .Y = -20
  .Width = 38
  .Height = 44
  .Visible = NO
 END WITH
 ChangeRectangleSlice buyst.hire_box, 3
 
 buyst.hire_sl = NewSliceOfType(slSprite, buyst.hire_box)
 WITH *(buyst.hire_sl)
  .AnchorVert = alignCenter
  .AlignVert = alignCenter
  .AnchorHoriz = alignCenter
  .AlignHoriz = alignCenter
 END WITH
 ChangeSpriteSlice buyst.hire_sl, sprTypeHero

 buyst.portrait_box = NewSliceOfType(slRectangle, right_panel)
 WITH *(buyst.portrait_box)
  .AnchorVert = alignBottom
  .AlignVert = alignBottom
  .AnchorHoriz = alignRight
  .AlignHoriz = alignRight
  .X = -20
  .Y = -20
  .Width = 54
  .Height = 54
  .Visible = NO
 END WITH
 ChangeRectangleSlice buyst.portrait_box, 3
 
 buyst.portrait_sl = NewSliceOfType(slSprite, buyst.portrait_box)
 WITH *(buyst.portrait_sl)
  .AnchorVert = alignCenter
  .AlignVert = alignCenter
  .AnchorHoriz = alignCenter
  .AlignHoriz = alignCenter
 END WITH
 ChangeSpriteSlice buyst.portrait_sl, sprTypePortrait

 buyst.price_box = NewSliceOfType(slRectangle, buyst.root_sl)
 WITH *(buyst.price_box)
  .AnchorHoriz = alignCenter
  .AlignHoriz = alignCenter
  .AnchorVert = alignBottom
  .AlignVert = alignBottom
  .Y = -3
  .Height = 16
  .PaddingLeft = 8
  .PaddingRight = 8
 END WITH
 ChangeRectangleSlice buyst.price_box, 0
 
 buyst.price_sl = NewSliceOfType(slText, buyst.price_box)
 WITH *(buyst.price_sl)
  .AnchorVert = alignCenter
  .AlignVert = alignCenter
  .AnchorHoriz = alignCenter
  .AlignHoriz = alignCenter
  .Fill = NO
  .FillMode = sliceFillHoriz
 END WITH
 ChangeTextSlice buyst.price_sl, "Price goes here", uilook(uiText), YES

 buyst.alert_box = NewSliceOfType(slRectangle, buyst.root_sl)
 WITH *(buyst.alert_box)
  .AnchorHoriz = alignCenter
  .AlignHoriz = alignCenter
  .AnchorVert = alignBottom
  .AlignVert = alignBottom
  .Y = -3
  .Height = 16
  .Visible = NO
 END WITH
 ChangeRectangleSlice buyst.alert_box, 2
 
 buyst.alert_sl = NewSliceOfType(slText, buyst.alert_box)
 WITH *(buyst.alert_sl)
  .AnchorVert = alignCenter
  .AlignVert = alignCenter
  .AnchorHoriz = alignCenter
  .AlignHoriz = alignCenter
 END WITH
 ChangeTextSlice buyst.alert_sl, "", uilook(uiText), YES

END SUB

SUB buystuff_set_alert(buyst as ShopBuyState, alert as string, byval ticks as integer = 18)
 ChangeTextSlice buyst.alert_sl, alert
 WITH *(buyst.alert_box)
  .Visible = YES
  .Width = buyst.alert_sl->Width + 16
 END WITH
 buyst.alert_ticks = ticks
END SUB

FUNCTION chkOOBtarg (byval attacker as integer, byval target as integer, byval atk as integer) as bool
 'Out-of-battle attack target class evaluation: Whether 'target' is a valid target.
 'attacker is -1 when no specific attacker, e.g. using an item or not passed to "map cure".
 'atk id can be -1 for when no attack is relevant (when we are learning an attack, not using it)

 IF target < 0 OR target > UBOUND(gam.hero) THEN RETURN NO
 IF gam.hero(target).id = -1 THEN RETURN NO
 IF atk < -1 OR atk > gen(genMaxAttack) THEN RETURN NO

 DIM hp as integer
 hp = gam.hero(target).stat.cur.hp

 IF atk >= 0 THEN
  DIM attack as AttackData
  loadattackdata attack, atk

  SELECT CASE attack.targ_class
   CASE 2 'Self
    IF attacker > -1 ANDALSO attacker <> target THEN RETURN NO
   CASE 5 'Ally-not-self
    IF attacker > -1 ANDALSO attacker = target THEN RETURN NO
   CASE 10, 15  'Dead Ally, Dead Foe
    RETURN (hp <= 0)
   CASE 16  'Foe-including-dead
    RETURN YES  'attack_can_hit_dead() would return NO because heroes can't hit dead enemies
  END SELECT
  'Other target classes are treated equivalent to "Ally (not dead)", or
  'to Ally-including-dead if they can hit dead.

  IF hp <= 0 THEN RETURN attack_can_hit_dead(attack)
 ELSE
  IF hp <= 0 THEN RETURN NO
 END IF

 RETURN YES
END FUNCTION

'After a change in equipment or stat caps, update the current and max values
'according to slightly odd rule.
SUB update_hero_max_and_cur_stats (byval who as integer)
 recompute_hero_max_stats who

 '--update current stats
 WITH gam.hero(who).stat
  FOR statnum as integer = 0 TO statLast
   'max stats are already capped, so current stats will be too
   IF statnum <> statHP AND statnum <> statMP THEN
    .cur.sta(statnum) = .max.sta(statnum)
   ELSE
    .cur.sta(statnum) = small(.cur.sta(statnum), .max.sta(statnum))
   END IF
  NEXT statnum
 END WITH
END SUB

'Equip an item. Automatically unequips anything previously equipped.
'toequip: item ID (-1 is invalid)
'who: hero slot
'where: equip slot
'Return value YES for success, NO for failure
FUNCTION doequip (toequip as integer, who as integer, where as integer) as bool

 'First remove the item from inventory (unless it is the default weapon)
 IF toequip = gam.hero(who).def_wep - 1 AND where = 0 THEN
 ELSE
  'delete the item from inventory
  delitem toequip
 END IF

 'Now remove the old equipment (Removes the default weapon too)
 IF gam.hero(who).equip(where).id >= 0 THEN
  IF unequip(who, where, NO, NO) = NO THEN
   'Unequipping failed. This is because there was no room to return the item back to inventory
   'Instead, return the original item to inventory (which we can do because we just removed it,
   'and its stack is guaranteed to have room even if the rest of the inventory is full)
   '
   '(Also there is no danger of unequip recursively calling doequip again because that
   ' only happens if the resetdefwep arg is YES and it is NO here)
   getitem toequip
   debug "Failed attempt to unequip " & readitemname(gam.hero(who).equip(where).id) & " and equip " & readitemname(toequip) & " (No room in inventory)"
   RETURN NO
  END IF
 END IF

 'set new equipment
 gam.hero(who).equip(where).id = toequip

 'Update this hero's stat bonuses
 update_hero_max_and_cur_stats who

 evalitemtags
 evalherotags  'You could kill someone, right? (Yes, with a negative HP bonus)
 tag_updates
 RETURN YES
END FUNCTION

LOCAL FUNCTION getOOBtarg (byval search_direction as integer, byval attacker as integer, byref target as integer, byval atk as integer) as bool
 'Move the hero selection cursor for the target picker for an OOB attack/learning attack.
 '--return true on success, false on failure
 '--atk id can be -1 for when no attack is relevant (when we are learning an attack, not using it)
 DIM safety as integer = 0
 DO
  loopvar target, 0, 3, search_direction
  IF chkOOBtarg(attacker, target, atk) THEN RETURN YES
  safety += 1
  IF safety >= 4 THEN EXIT DO
 LOOP
 'Failure
 target = -1
 RETURN NO
END FUNCTION

'Returns the string such as "x 9" that appears in inventory slots
FUNCTION inventory_slot_number_str(slot as InventSlot) as string
 IF slot.num = 1 THEN
  'Optionally, don't display the x1 suffix
  IF gen(genInventSlotx1Display) = 1 ORELSE _  'Never display x1
     (gen(genInventSlotx1Display) = 2 ANDALSO get_item_stack_size(slot.id) <= 1) THEN 'Only if stacksize>1
   RETURN "   "
  END IF
 END IF
 RETURN CHR(1) & lpad(STR(slot.num), , 2)   'CHR(1) is 'X'
END FUNCTION

SUB update_inventory_caption (byval i as integer)
 'WARNING: this is no longer used by the Items menu, but is still used by Equip and Battle items menus,
 'which need re-writing. (I think the goal is to get rid of InventSlot.text?)
 WITH inventory(i)
  IF .used THEN
   .text = rpad(readitemname(.id), " ", 8) & inventory_slot_number_str(inventory(i))
  ELSE
   .text = SPACE(11)
  END IF
 END WITH
END SUB

FUNCTION oobcure (byval attacker as integer, byval target as integer, byval atk as integer, byval target_count as integer, byref was_full as bool=NO) as bool
 '--outside-of-battle cure
 ' attacker and target are hero slots; attacker may be -1
 ' Returns YES if the attack happened.
 ' Returns NO if the attack failed
 ' Sets was_full=YES if the target stat was already full, and general preference bits plus attack bits dictate that it should fail

 WITH gam.hero(target).stat
  DIM as BattleSprite attacker_obj, target_obj

  '--populate attacker object
  IF attacker = -1 THEN
   '--average stats for item-triggered spells
   DIM partysize as integer = 0
   FOR o as integer = 0 TO 3
    IF gam.hero(o).id >= 0 THEN
     partysize += 1
     FOR i as integer = 0 TO statLast
      attacker_obj.stat.cur.sta(i) += gam.hero(o).stat.cur.sta(i)
      attacker_obj.stat.max.sta(i) += gam.hero(o).stat.max.sta(i)
     NEXT i
    END IF
   NEXT o
   FOR i as integer = 0 TO statLast
    attacker_obj.stat.cur.sta(i) /= partysize
    attacker_obj.stat.max.sta(i) /= partysize
   NEXT i
  ELSE
   FOR i as integer = 0 TO statLast
    attacker_obj.stat.cur.sta(i) = gam.hero(attacker).stat.cur.sta(i)
    attacker_obj.stat.max.sta(i) = gam.hero(attacker).stat.max.sta(i)
   NEXT i
  END IF

  '--populate the target object
  FOR i as integer = 0 to statLast
   target_obj.stat.cur.sta(i) = .cur.sta(i)
   target_obj.stat.max.sta(i) = .max.sta(i)
  NEXT i
  calc_hero_elementals target_obj.elementaldmg(), target

  DIM attack as AttackData
  loadattackdata attack, atk

  '--out of battle attacks that target stats other than HP and MP
  '--always affect the max stat, so force exceed_maximum on
  IF attack.targ_stat > 1 THEN
   attack.allow_cure_to_exceed_maximum = YES
  END IF

  '-- For cure attacks that target HP and MP, we have special handling to prevent out-of-battle spells/items from being wasted on full targets
  IF attack.targ_stat <= 1 ANDALSO attack.allow_cure_to_exceed_maximum = NO ANDALSO attack.cure_instead_of_harm = YES THEN
   IF target_obj.stat.cur.sta(attack.targ_stat) >= target_obj.stat.max.sta(attack.targ_stat) THEN
    IF prefbit(62) THEN ' Don't allow wasting out-of-battle cures
     MenuSound gen(genCancelSFX)
     was_full = YES
     RETURN NO
    END IF
   END IF
  END IF

  '--out of battle attacks aren't allowed to miss.
  attack.aim_math = 3

  'Currently (bug 980) stat caps aren't enforced in-battle, which makes this easier.
  'Once it's fixed, I think we'll want an argument (or attack bitset!) to disable capping.
  'Also, we assume that inflict only modifies current values of stats.
  inflict( , , 0, 1, attacker_obj, target_obj, attack, target_count)

  '--copy back stats that need copying back
  DIM last_plain_stat as integer

  IF prefbit(31) = NO THEN  '"Don't reset max stats after OOB attack"
   '--Then update just the max for the other stats
   '--this kinda sucks but it is consistent with the way outside of battle cure has always worked.
   '--It's needed so that items which permanently change stats out of battle can work, but it's
   '--also a bad thing if a script expects the max values to not change.
   '--Note that an item can't permanently change HP or MP!
   '--Note also that this has bad results if cur != max to begin with: the cur becomes
   '--the new max even if the attack does nothing! (We should probably change that)
   last_plain_stat = statMP
   FOR statnum as integer = statMP + 1 to statLast
    'Indirectly update base
    DIM diff as integer = target_obj.stat.cur.sta(statnum) - .max.sta(statnum)
    .base.sta(statnum) += diff
    'Previously we effectively did .max.sta(statnum) += diff
   NEXT statnum

   recompute_hero_max_stats target
   FOR statnum as integer = statMP + 1 to statLast
    .cur.sta(statnum) = .max.sta(statnum)
   NEXT

  ELSE
   last_plain_stat = statLast
  END IF

  '--Stats which are copied directly: usually just HP and MP
  FOR statnum as integer = 0 to last_plain_stat
   .cur.sta(statnum) = target_obj.stat.cur.sta(statnum)
   DIM cap as integer = gen(genStatCap + statnum)
   IF cap > 0 THEN
    .cur.sta(statnum) = small(.cur.sta(statnum), cap)
   END IF
  NEXT statnum

  MenuSound attack.sound_effect

  '--TODO: Must add the attack-tag conditional stuff.
 END WITH
 
 RETURN YES
END FUNCTION

SUB patcharray (array() as integer, n as string)

DIM num(2) as string
DIM hexk(15) as integer

hexk(0) = 11
FOR i as integer = 1 TO 9
 hexk(i) = i + 1
NEXT i
hexk(10) = 30
hexk(11) = 48
hexk(12) = 46
hexk(13) = 32
hexk(14) = 18
hexk(15) = 33
DIM pt as integer = 0
DIM tog as integer
DIM csr as integer

ensure_normal_palette
setkeys
DO
 setwait speedcontrol
 setkeys
 tog = tog XOR 1
 IF keyval(ccCancel) > 1 THEN EXIT DO
 IF keyval(ccUp) > 1 THEN csr = large(0, csr - 1)
 IF keyval(ccDown) > 1 THEN csr = small(2, csr + 1)
 IF csr = 0 THEN intgrabber pt, 0, UBOUND(array)
 IF csr = 1 THEN intgrabber array(pt), -32768, 32767
 IF csr = 2 THEN
  FOR i as integer = 0 TO 15
   IF keyval(hexk(i)) > 1 THEN setbit array(), pt, i, readbit(array(), pt, i) XOR 1
  NEXT i
 END IF
 num(0) = n & "(" & ABS(pt) & ")"
 num(1) = "value = " & array(pt)
 num(2) = ""
 FOR i as integer = 0 TO 15
  IF readbit(array(), pt, i) THEN
   num(2) = num(2) + "1"
  ELSE
   num(2) = num(2) + "0"
  END IF
 NEXT i
 clearpage dpage
 edgeprint "DEBUG MODE", pCentered, rCenter - 50, uilook(uiText), dpage
 centerbox  , , 140, 60, 1, dpage
 DIM c as integer
 FOR i as integer = 0 TO 2
  IF i = csr THEN
   c = uilook(uiSelectedItem + tog)
  ELSE
   c = uilook(uiMenuItem)
  END IF
  edgeprint num(i), pCentered, rCenter - 20 + i * 10, c, dpage
 NEXT i
 edgeprint "0123456789ABCDEF", pCentered, rCenter + 10, uilook(uiSelectedDisabled), dpage
 SWAP vpage, dpage
 setvispage vpage
 dowait
LOOP
restore_previous_palette

END SUB

FUNCTION pickload (newgame_opt as bool = YES, beep_if_no_saves as bool = NO) as integer
 ' Load game menu. The menu will be skipped if there are no saves (optionally plays Cancel sound)
 ' newgame_opt: whether the "New Game" button is shown.
 '
 ' Return value:
 '  0 or higher: save slot
 '  -1   New Game/no saves available (even if newgame_opt=NO)
 '  -2   cancelled/Exit
 RETURN picksaveload(YES, newgame_opt, beep_if_no_saves)
END FUNCTION

FUNCTION picksave () as integer
 ' Return value:
 '  0 or higher: save slot
 '  -2: cancelled/Exit
 RETURN picksaveload(NO)
END FUNCTION

PRIVATE FUNCTION picksaveload (byval loading as bool, byval newgame_opt as bool = YES, byval beep_if_no_saves as bool = NO) as integer
' Save or load menu.
' loading: false is the save menu, true is the load menu.
' newgame_opt is ignored for the save menu. For the load menu it determines whether
'   the "New Game" button is shown.
' beep_if_no_saves is for providing audible feedback if the load menu is skipped if
'   there are no saves.
' Return value:
'  0 or higher: save slot
'  -1   (loading only) no saves available/New Game
'  -2   cancelled/Exit

show_virtual_gamepad()

DIM ret_slot as integer
DIM max_slot as integer = maxSaveSlotCount - 1
DIM last_slot as integer
last_slot = gen(genSaveSlotCount) - 1
IF last_slot < 0 THEN last_slot = 3 'Use the default!
IF last_slot > max_slot THEN
 debug "Can't allow last save slot to be " & last_slot & ", " & max_slot & " is the max."
 last_slot = max_slot
END IF

IF prefbit(44) THEN '"Hide empty save slots at the bottom of the save/load menus"
 FOR i as integer = last_slot TO 4 STEP -1
  IF save_slot_used(i) THEN
   'Found a used save slot
   EXIT FOR
  ELSE
   'Save slot is empty
   IF loading THEN
    last_slot = i - 1
   ELSE
    last_slot = i
   END IF
  END IF
 NEXT i
END IF

DIM mapname(max_slot) as string
DIM lev(max_slot) as string
DIM menu(1) as string
DIM sprites(max_slot, 3) as GraphicPair
DIM st as MenuState

'newgame_opt controls whether there are one (left only) or two buttons (left and right) at the top
'menu() holds the names of the options at the top of the screen (only one appears when saving)
'st.pt = -1 and menu(0) is the left button, st.pt = -2 and menu(1) is the right button

'If "New game" is an option it is left (st.pt = -1) and cancel is the right one (st.pt = -2),
'Otherwise "Cancel" is the only button (st.pt = -1).
'But if exiting the game is disallowed there will instead be only one button on the left
'(exit can't be disabled when newgame is disabled).
'Note st.pt is no longer equal to the return value.

st.first = -1  'Note that st.pt can actually go down to -2
st.last = last_slot
st.size = 3
DIM drag_start_top as integer = -10

DIM allow as bool

IF loading THEN
 st.pt = 0
ELSE
 newgame_opt = NO
 st.pt = lastsaveslot - 1
END IF

IF newgame_opt THEN
 menu(0) = readglobalstring(52, "NEW GAME", 10)
 menu(1) = readglobalstring(53, "EXIT", 10)
ELSE
 menu(0) = readglobalstring(59, "CANCEL", 10)
END IF

DIM holdscreen as integer
DIM page as integer
page = compatpage
holdscreen = allocatepage
'--preserve background for display beneath the save/load picker
copypage page, holdscreen

DIM pv(last_slot) as SaveSlotPreview
FOR i as integer = 0 TO last_slot
 get_save_slot_preview i, pv(i)
 IF pv(i).valid THEN
  mapname(i) = embed_text_codes(getmapname(pv(i).cur_map), i)
  '--leader level
  lev(i) = readglobalstring(43, "Level", 10) & " " & pv(i).leader_lev
  FOR o as integer = 0 TO 3
   '--hero pic and palette
   IF pv(i).hero_id(o) > 0 THEN
    load_sprite_and_pal sprites(i, o), sprTypeHero, pv(i).hero(o).battle_pic, pv(i).hero(o).battle_pal
   END IF
  NEXT o
 END IF
NEXT i

DIM exit_load_disabled as bool = NO

IF loading THEN
 'Check for no saves
 DIM nosaves as bool = YES
 FOR i as integer = 0 TO last_slot
  IF pv(i).valid THEN nosaves = NO
 NEXT i
 IF nosaves THEN
  ret_slot = -1
  IF beep_if_no_saves THEN MenuSound gen(genCancelSFX)
  GOTO freesprites
 END IF
END IF

IF loading ANDALSO newgame_opt ANDALSO NOT exit_from_game_is_allowed() THEN exit_load_disabled = YES

'FIXME: using need_fade_in to trigger the sound effect seems like a bad idea
IF gam.need_fade_in = NO THEN MenuSound gen(genAcceptSFX)
setkeys
DO
 setwait speedcontrol
 setkeys
 IF loading = NO THEN playtimer
 IF game_check_cancel_key() THEN
  MenuSound gen(genCancelSFX)
  IF exit_load_disabled THEN
   'No nothing if exiting is not allowed from the initital loadgame screen
  ELSE
   'Cancel and exit out of the load menu
   ret_slot = -2
   EXIT DO
  END IF
 END IF

 'Make menu position -2 appear as -1 to usemenu
 DIM temppt as integer = iif(st.pt = -2, -1, st.pt)
 usemenusounds
 IF usemenu(temppt, st.top, st.first, st.last, st.size) THEN
  st.pt = temppt
 END IF
 IF get_gen_bool("/mouse/mouse_menus") THEN
  picksave_update_mouse st, newgame_opt, drag_start_top
 END IF
 IF st.pt < 0 AND newgame_opt THEN
  IF exit_load_disabled THEN
   'If exit is disabled, then new game is the only option (exit can't be disabled when newgame is disabled)
   st.pt = -1
  ELSE
   IF carray(ccLeft) > 1 THEN st.pt = -1: MenuSound gen(genCursorSFX)
   IF carray(ccRight) > 1 THEN st.pt = -2: MenuSound gen(genCursorSFX)
  END IF
 END IF

 IF game_check_use_key() ORELSE (get_gen_bool("/mouse/mouse_menus") ANDALSO (readmouse.release AND mouseLeft) ANDALSO readmouse.drag_dist < 10 ANDALSO (readmouse.buttons AND mouseRight) = 0 ) THEN
  IF newgame_opt AND st.pt = -1 THEN
   'Selected "New Game"
   MenuSound gen(genAcceptSFX)
   ret_slot = -1
   EXIT DO
  ELSEIF st.pt < 0 THEN
   'Selected "Cancel" or "Exit"
   MenuSound gen(genCancelSFX)
   IF exit_load_disabled THEN
    'Do nothing if exiting is not allowed from the initial loadgame screen
   ELSE
    'Cancel and exit out of the load menu
    ret_slot = -2
    EXIT DO
   END IF
  ELSE
   allow = YES
   IF loading THEN
    '--don't allow load of an empty save
    IF pv(st.pt).valid = 0 THEN allow = NO
   ELSE
    '--normal save in a slot
    IF pv(st.pt).valid THEN allow = picksave_confirm(menu(), loading, newgame_opt, exit_load_disabled, sprites(), pv(), mapname(), lev(), st, holdscreen, page)
   END IF
   IF allow = YES THEN
    MenuSound gen(genAcceptSFX)
    ret_slot = st.pt
    EXIT DO
   ELSE
    MenuSound gen(genCancelSFX)
   END IF
  END IF
 END IF
 copypage holdscreen, page
 picksave_draw menu(), loading, newgame_opt, exit_load_disabled, sprites(), pv(), mapname(), lev(), st, page
 setvispage vpage
 check_for_queued_fade_in
 dowait
LOOP

freesprites:
freepage page
freepage holdscreen
clearkeys
FOR i as integer = 0 TO last_slot
 FOR o as integer = 0 TO 3
  unload_sprite_and_pal sprites(i, o)
 NEXT
NEXT

RETURN ret_slot

END FUNCTION

'Returns YES if overwrite is allowed, NO if canceled
FUNCTION picksave_confirm(menu() as string, byval loading as bool, byval newgame_opt as bool, exit_load_disabled as bool=NO, sprites() as GraphicPair, pv() as SaveSlotPreview, mapname() as string, lev() as string, byref st as MenuState, byval holdscreen as integer, byval page as integer) as bool

 DIM confirmboxY as integer = 14 + (44 * (st.pt - st.top))
 DIM tog as integer
 DIM confirm(1) as string
 confirm(0) = readglobalstring(44, "Yes", 10)
 confirm(1) = readglobalstring(45, "No", 10)
 DIM menuwidth as integer
 menuwidth = 8 * large(LEN(confirm(0)), LEN(confirm(1)))
 DIM replacedat as string
 replacedat = readglobalstring(102, "Replace Old Data?", 20)

 DIM state as MenuState
 state.last = 1
 state.size = 2
 state.pt = 1 ' default to "No"

 MenuSound gen(genAcceptSFX)
 setkeys
 DO
  setwait speedcontrol
  setkeys
  tog = tog XOR 1
  playtimer
  IF game_check_cancel_key() THEN
   MenuSound gen(genCancelSFX)
   RETURN NO
  END IF
  IF game_check_use_key() THEN RETURN state.pt = 0

  usemenu state
  usemenusounds
  IF get_gen_bool("/mouse/mouse_menus") THEN
   state.hover = -1
   DIM overbutton as bool = NO
   FOR i as integer = 0 to 1
    'The button rect is slightly wider than the default "YES/NO" text
    'but slightly smaller than the maximum possible length of the captions
    '(the part of the text that might overflow outside the decorative box is not in the button rect)
    'FIXME: this will all be better when it is sliceified ;)
    DIM r as RectType = (208, confirmboxY - 9 + (i * 9), 40, 9)
    IF rect_collide_point(r, readmouse.pos) THEN
     state.hover = i
     overbutton = YES
    END IF
   NEXT i
   IF state.hover >= 0 ANDALSO (readmouse.buttons AND mouseLeft) THEN
    state.pt = state.hover
   END IF
   IF overbutton ANDALSO (readmouse.release AND mouseLeft) THEN RETURN state.pt = 0
  END IF
 
  copypage holdscreen, page
  picksave_draw menu(), loading, newgame_opt, exit_load_disabled, sprites(), pv(), mapname(), lev(), st, page
  centerbox 160, confirmboxY, 40 + (LEN(replacedat) * 8) + menuwidth, 24, 3, page
  edgeprint replacedat, 200 - (LEN(replacedat) * 8), confirmboxY - 5, uilook(uiText), page
  FOR i as integer = 0 TO 1
   DIM col as integer = uilook(uiMenuItem)
   IF state.hover = i THEN col = uilook(uiMouseHoverItem)
   IF state.pt = i THEN col = uilook(uiSelectedItem + tog)
   edgeprint confirm(i), 216, confirmboxY - 9 + (i * 9), col, page
  NEXT i
  setvispage vpage
  dowait
 LOOP
END FUNCTION

SUB picksave_draw(menu() as string, byval loading as bool, byval newgame_opt as bool, exit_load_disabled as bool=NO, sprites() as GraphicPair, pv() as SaveSlotPreview, mapname() as string, lev() as string, byref st as MenuState, byval page as integer)
 'newgame_opt controls whether there are one (left only) or two buttons (left and right) at the top
 'st.pt = -1 and menu(0) is the left button, st.pt = -2 and menu(1) is the right button
 DIM col as integer
 DIM boxstyle as integer
 'load and save menus enjoy different colour schemes
 IF loading THEN boxstyle = 1 ELSE boxstyle = 0

 STATIC tog as integer
 STATIC wtog as integer
 tog = tog XOR 1
 loopvar wtog, 0, 2 * wtog_ticks() - 1  '' These are hero battle sprites. max_wtog() is for walkabouts

 DIM last_slot as integer = UBOUND(pv)
 
 centerbox 50, 11, 80, 14, 1 + 14, page
 IF newgame_opt ANDALSO NOT exit_load_disabled THEN centerbox 270, 11, 80, 14, 1 + 14, page
 DIM top as integer = large(st.top, 0)
 DIM yoff as integer
 FOR i as integer = top TO small(top + 3, last_slot)
  yoff = (i - top) * 44
  centerbox 160, 44 + yoff, 310, 42, 1 + 14, page
 NEXT i
 SELECT CASE st.pt
  CASE -2
   centerbox 270, 11, 82, 16, 1 + boxstyle, page
  CASE -1
   centerbox 50, 11, 82, 16, 1 + boxstyle, page
  CASE ELSE
   centerbox 160, 44 + (st.pt - top) * 44, 312, 44, 1 + boxstyle, page
 END SELECT
 FOR i as integer = top TO small(top + 3, last_slot)
  col = uilook(uiMenuItem)
  IF st.hover = i THEN col = uilook(uiMouseHoverItem)
  IF st.pt = i THEN col = uilook(uiSelectedItem + tog)
  yoff = (i - top) * 44
  IF pv(i).valid THEN
   'If the sprites are much larger than the default 32x40 then they will overlap, and get clipped
   setclip 5, yoff + 22, 315, yoff + 63, vpages(page)
   FOR o as integer = 3 TO 0 STEP -1
    IF sprites(i, o).sprite THEN
     DIM wframe as integer = 0
     IF st.pt = i THEN wframe = wtog_to_frame(wtog)
     frame_draw sprites(i, o).sprite + wframe, sprites(i, o).pal, 140 + (o * 42), 24 + yoff, , page
    END IF
   NEXT o
   setclip , , , , vpages(page)
   edgeprint pv(i).leader_name, 14, 25 + yoff, col, page
   edgeprint lev(i), 14, 34 + yoff, col, page
   edgeprint pv(i).playtime, 14, 43 + yoff, col, page
   edgeprint mapname(i), 14, 52 + yoff, col, page
  END IF
  DIM slot_label as string = "" & (i + 1)
  edgeprint slot_label, 312 - len(slot_label) * 8, 25 + yoff, col, page
 NEXT i
 IF st.hover = -1 THEN col = uilook(uiMouseHoverItem)
 col = uilook(IIF(st.pt = -1, uiSelectedItem + tog, uiMenuItem))
 edgeprint menu(0), ancCenter + 50, 6, col, page
 IF newgame_opt ANDALSO NOT exit_load_disabled THEN
  IF st.hover = -2 THEN col = uilook(uiMouseHoverItem)
  col = uilook(IIF(st.pt = -2, uiSelectedItem + tog, uiMenuItem))
  edgeprint menu(1), ancCenter + 270, 6, col, page
 END IF
 DIM scroll_st as MenuState
 WITH scroll_st
  .pt = large(0, st.pt)
  .top = large(0, st.top)
  .last = st.last
  .size = st.size
 END WITH
 draw_fullscreen_scrollbar scroll_st, , page
END SUB

SUB picksave_update_mouse(st as MenuState, byval newgame_opt as bool, byref drag_start_top as integer)
 st.hover = -10
 DIM top as integer = large(st.top, 0)
 DIM yoff as integer
 DIM r as RectType
 FOR i as integer = top TO small(top + 3, st.last)
  yoff = (i - top) * 44
  r = Type(5, 23 + yoff, 310, 42)
  IF rect_collide_point(r, readmouse.pos) THEN st.hover = i
 NEXT i
 
 ' Check for load "Exit" button
 IF newgame_opt THEN
  r = Type(229, 3, 82, 16)
  IF rect_collide_point(r, readmouse.pos) THEN st.hover = -2
 END IF
 ' Check for save "Exit" or load "New Game" 
 r = Type(9, 3, 82, 16)
 IF rect_collide_point(r, readmouse.pos) THEN st.hover = -1
 'Check for click to focus
 IF (readmouse.buttons AND mouseLeft) ANDALSO st.hover > -10 ANDALSO (readmouse.buttons AND mouseRight) = 0 THEN
  st.pt = st.hover
 END IF
 IF (readmouse.clicks AND mouseRight) ANDALSO st.hover > -10 THEN
  st.pt = st.hover
 END IF

 'Now handle scrolling
 IF st.pt >= 0 THEN
  'Check for left-drag scrolling
  IF (readmouse.dragging AND mouseLeft) THEN
   IF readmouse.drag_dist > 10 ANDALSO drag_start_top = -10 THEN
    drag_start_top = st.top
   END IF
   IF drag_start_top > -10 THEN
    st.top = drag_start_top + (readmouse.clickstart.y - readmouse.pos.y) \ 44
   END IF
  ELSE
   drag_start_top = -10
  END IF
  'Check for wheel scrolling
  st.top = st.top + (4 * readmouse.wheel_delta) \ 120
  ' Bound top
  DIM lasttop as integer = large(st.first, st.last - st.size)
  st.top = bound(st.top, st.first, lasttop) 
  ' Make sure .pt is visible
  st.pt = bound(st.pt, st.top, st.top + st.size)
 END IF
END SUB

SUB sellstuff (byval id as integer, storebuf() as integer)
 DIM b((getbinsize(binSTF) \ 2) * 50 - 1) as integer
 DIM permask((inventoryMax + 1) \ 16 ) as integer
 DIM price(inventoryMax + 1) as integer
 DIM recordsize as integer = curbinsize(binSTF) \ 2 ' get size in INTs

'--preserve background for display under sell menu
DIM page as integer
DIM holdscreen as integer
page = compatpage
holdscreen = allocatepage
copypage page, holdscreen

DIM sold as string = readglobalstring(84, "Sold", 10)

loadshopstuf b(), id

' Initialise state of the shop stock (if we haven't visited the shop before, or if
' new items have been added to it since last time, or if the stock needs resetting
FOR slot as integer = 0 TO storebuf(16)
 DIM stockidx as integer = b(slot * recordsize + 37) - 1
 DIM thingtype as integer = b(slot * recordsize + 17)
 DIM thingid as integer = b(slot * recordsize + 18)
 DIM initial_stock as integer = b(slot * recordsize + 19)
 initialize_stock id, stockidx, thingtype, thingid, initial_stock
NEXT slot

DIM ic as integer = 0
DIM top as integer = 0
DIM tog as integer
DIM alert as integer = 0
DIM alert_str as string = ""
DIM info as string = ""
DIM drag_top as integer = -1
CONST rowheight as integer = 8  'Pixel height of each inventory row
DIM pagesize as integer  'Size of the menu: number of inventory slots visible at once
pagesize = small(63, last_inv_slot() + 1)
CONST rowsize as integer = 3  'Number of inventory slots on each row

DIM box as RectType = (8, 5, 304, 10 + (pagesize \ rowsize) * rowheight)
menusound gen(genAcceptSFX)
sellstuff_refresh storebuf(16), recordsize, permask(), price(), b()

DIM scroll_state as MenuState 'Only used for the scrollbar
WITH scroll_state
 .first = 0
 .last = last_inv_slot() \ rowsize
 .size = pagesize \ rowsize - 1
END WITH

sellstuff_infostr ic, info, b(), permask(), price(), storebuf(16), recordsize

show_virtual_gamepad()

setkeys
DO
 setwait speedcontrol
 setkeys
 tog = tog XOR 1
 playtimer
 
 DIM do_quit as bool = NO
 DIM do_pick as bool = NO

 DIM old_ic as integer = ic
 'Keyboard controls
 IF carray(ccUp) > 1 ANDALSO ic >= rowsize THEN
  ic -= rowsize
  IF ic < top THEN top -= rowsize
 END IF
 IF carray(ccDown) > 1 ANDALSO ic <= last_inv_slot() - rowsize THEN
  ic += rowsize
  IF ic >= top + pagesize THEN top += rowsize
 END IF
 IF carray(ccLeft) > 1 THEN
  IF ic MOD rowsize = 0 THEN ic += rowsize
  ic -= 1
 END IF
 IF carray(ccRight) > 1 THEN
  ic += 1
  IF ic MOD rowsize = 0 THEN ic -= rowsize
 END IF
 IF keyval(scPageUp) > 1 THEN
  ic -= pagesize - rowsize
  top -= pagesize - rowsize
 END IF
 IF keyval(scPageDown) > 1 THEN
  ic += pagesize - rowsize
  top += pagesize - rowsize
 END IF
 IF keyval(scHome) > 1 THEN
  ic = 0
  top = 0
 END IF
 IF keyval(scEnd) > 1 THEN
  ic = last_inv_slot()
  top = ic - (pagesize - 1)
 END IF
 IF game_check_cancel_key() THEN do_quit = YES
 IF game_check_use_key()  THEN do_pick = YES

 IF get_gen_bool("/mouse/mouse_menus") THEN
  IF rect_collide_point(box, readmouse.pos) THEN
   'Inside the inventory box
   DIM hover as integer = -1
   FOR i as integer = top TO top + pagesize - 1
    IF rect_collide_point(XYWH(20 + 96 * (i MOD rowsize), 12 + rowheight * ((i - top) \ rowsize), 96, rowheight), readmouse.pos) THEN hover = i
   NEXT i
   IF hover >= 0 THEN
    IF (readmouse.release AND mouseLeft) ANDALSO readmouse.drag_dist < 10 THEN
     IF ic = hover THEN
      do_pick = YES
     ELSE
      ic = hover
     END IF
    END IF
   END IF
   IF (readmouse.dragging AND mouseLeft) THEN
    IF drag_top = -1 THEN drag_top = top
    IF readmouse.drag_dist > 10 THEN
     DIM dist as integer = (readmouse.clickstart.y - readmouse.pos.y) / rowheight
     top = drag_top + dist * rowsize
    END IF
   END IF
   top += readmouse.wheel_clicks * rowsize * 3
  ELSE
   'Outside the box
   IF (readmouse.release AND mouseLeft) ANDALSO readmouse.drag_dist < 10 THEN do_quit = YES
  END IF
  IF (readmouse.release AND mouseRight) ANDALSO readmouse.drag_dist < 10 THEN do_quit = YES
  IF (readmouse.dragging AND mouseLeft) = 0 THEN drag_top = -1
 END IF

 'Clamp the top of the scrollable area into the safe range
 top = bound(top, 0, large(0, last_inv_slot() - (pagesize - 1)))
 'Make sure the cursor stays onscreen
 DO WHILE ic < top : ic += rowsize: LOOP
 DO WHILE ic > top + pagesize - 1 : ic -= rowsize: LOOP
 'Update scrollbar
 scroll_state.top = top \ rowsize

 IF old_ic <> ic THEN
  menusound gen(genCursorSFX)
  sellstuff_infostr ic, info, b(), permask(), price(), storebuf(16), recordsize
 END IF

 IF do_pick ANDALSO inventory(ic).used THEN
  IF readbit(permask(), 0, ic) = 0 THEN
   menusound gen(genSellSFX)
   alert = 10
   alert_str = sold & " " & readitemname(inventory(ic).id)
   'INCREMENT GOLD-----------
   gold = gold + price(ic)
   IF gold > 2000000000 THEN gold = 2000000000
   IF gold < 0 THEN gold = 0
   'CHECK FOR SPECIAL CASES---------
   FOR i as integer = 0 TO storebuf(16)
    IF b(i * recordsize + 17) = 0 AND b(i * recordsize + 18) = inventory(ic).id THEN
     'SET SELL BIT---
     settag b(i * recordsize + 23)
     'ADD TRADED ITEM-----------
     IF b(i * recordsize + 28) > 0 THEN getitem b(i * recordsize + 28) - 1, b(i * recordsize + 29) + 1
     'INCREMENT STOCK-------
     IF b(i * recordsize + 26) > 0 THEN
      DIM stockidx as integer = b(i * recordsize + 37) - 1
      IF b(i * recordsize + 26) = 1 THEN gam.stock(id, stockidx) = -1  'Acquire infinite stock
      IF b(i * recordsize + 26) = 2 AND gam.stock(id, stockidx) > 0 THEN gam.stock(id, stockidx) += 1  'Increment stock
     END IF
    END IF
   NEXT i
   'DECREMENT ITEM-----------
   consumeitem ic
   'UPDATE ITEM POSESSION TAGS--------
   evalitemtags
   tag_updates
   'REFRESH DISPLAY--------
   sellstuff_refresh storebuf(16), recordsize, permask(), price(), b()
   sellstuff_infostr ic, info, b(), permask(), price(), storebuf(16), recordsize
  ELSE
   menusound gen(genCantSellSFX)
  END IF
 END IF

 IF do_quit THEN EXIT DO
 
 '--Draw the sell screen
 copypage holdscreen, page
 edgeboxstyle box, 0, page
 draw_scrollbar scroll_state, box, , page
 FOR i as integer = top TO top + pagesize - 1
  textcolor uilook(uiMenuItem), 0
  IF readbit(permask(), 0, i) THEN textcolor uilook(uiDisabledItem), 0
  IF ic = i THEN
   textcolor uilook(uiSelectedItem + tog), uilook(uiHighlight2)
   IF readbit(permask(), 0, i) THEN textcolor uilook(uiGold), uilook(uiHighlight2)
  END IF
  DIM item_name as string
  IF i > last_inv_slot() THEN
   item_name = "OUTOFRANGE!"
  ELSE
   item_name = inventory(i).text
  END IF
  printstr item_name, 20 + 96 * (i MOD rowsize), 12 + rowheight * ((i - top) \ rowsize), page
 NEXT i
 centerbox 160, 192, 312, 14, 4, page
 edgeprint info, pCenteredLeft, 187, uilook(uiText), page
 edgeprint price_string(gold), pRight - 10, 1, uilook(uiGold), page
 IF alert THEN
  alert -= 1
  centerbox 160, 178, textwidth(alert_str) + 8, 14, 4, page
  edgeprint alert_str, pCentered, 173, uilook(uiSelectedItem + tog), page
 END IF
 setvispage vpage
 dowait
LOOP
freepage page
freepage holdscreen
menusound gen(genCancelSFX)

evalitemtags
tag_updates

END SUB

SUB sellstuff_infostr(byval ic as integer, info as string, b() as integer, permask() as integer, price() as integer, byval stuff_count as integer, byval recordsize as integer)

 DIM cannotsell as string = readglobalstring(75, "CANNOT SELL", 20)
 DIM worth as string = readglobalstring(77, "Worth", 20)
 DIM tradefor as string = readglobalstring(79, "Trade for", 20)
 DIM worthnothing as string = readglobalstring(82, "Worth Nothing", 20)

 info = ""
 IF inventory(ic).used = 0 THEN RETURN
 IF readbit(permask(), 0, ic) = 1 THEN info = cannotsell: RETURN
 IF price(ic) > 0 THEN info = worth & " " & price_string(price(ic))
 FOR i as integer = 0 TO stuff_count
  IF b(i * recordsize + 17) = 0 AND b(i * recordsize + 18) = inventory(ic).id THEN
   IF b(i * recordsize + 28) > 0 THEN
    IF info = "" THEN
     info = tradefor & " "
    ELSE
     IF b(i * recordsize + 29) > 0 THEN
      info &= " " & readglobalstring(153, "and", 10) & " "
     ELSE
      info &= " " & readglobalstring(81, "and a", 10) & " "
     END IF
    END IF
    IF b(i * recordsize + 29) > 0 THEN info &= (b(i * recordsize + 29) + 1) & " "
    info &= readitemname(b(i * recordsize + 28) - 1)
   END IF
  END IF
 NEXT i
 IF info = "" THEN info = worthnothing
END SUB

SUB sellstuff_refresh(byval last_stuff as integer, byval recordsize as integer, permask() as integer, price() as integer, stuffdata() as integer)
 DIM item as ItemDef
 DIM thing_type as integer
 DIM num as integer
 DIM sell_req_tag as integer
 DIM sell_price as integer
 DIM sell_mode as integer
 FOR i as integer = 0 TO last_inv_slot()
  IF inventory(i).used THEN
   loaditemdata item, inventory(i).id
   IF item.cannot_be_sold_or_dropped THEN setbit permask(), 0, i, 1
   price(i) = INT(item.buy_price * .5)
   FOR o as integer = 0 TO last_stuff
    thing_type   = stuffdata(o * recordsize + 17)
    num          = stuffdata(o * recordsize + 18)
    sell_req_tag = stuffdata(o * recordsize + 21)
    sell_mode    = stuffdata(o * recordsize + 26)
    sell_price   = stuffdata(o * recordsize + 27)
    IF num = inventory(i).id THEN
     IF ABS(sell_req_tag) > 0 THEN
      IF NOT istag(sell_req_tag, 0) THEN setbit permask(), 0, i, 1
     END IF
     IF thing_type = 0 THEN
      price(i) = sell_price
      IF sell_mode = 3 THEN setbit permask(), 0, i, 1
     END IF
    END IF
   NEXT o
  END IF
 NEXT i
END SUB

'Format one of the strings on the second Status menu screen
FUNCTION hero_elemental_resist_msg (element as string, damage as single) as string
 DIM raw as string
 IF ABS(damage) < 0.000005 THEN
  raw = readglobalstring(168, "Immune to $E", 30)
 ELSEIF damage < 0.0 THEN
  raw = readglobalstring(171, "Absorb $E", 30)
 ELSEIF damage < 1.0 THEN
  raw = readglobalstring(165, "Strong to $E", 30)
 ELSEIF damage > 1.0 THEN
  raw = readglobalstring(162, "Weak to $E", 30)
 END IF
 'No message for 100% damage
 replacestr raw, "$E", element
 replacestr raw, "$D", format_percent(damage, 3)
 replacestr raw, "$X", format_percent(damage - 1.0, 3)
 replacestr raw, "$R", format_percent(1.0 - damage, 3)
 replacestr raw, "$A", format_percent(-damage, 3)
 RETURN raw
END FUNCTION

FUNCTION get_elemental_info_text(byval hero_slot as integer) as string
 '--get the list of elemental names
 DIM elementnames() as string
 getelementnames elementnames()
 '--get this hero's elemental resists, with worn equipment
 DIM elementaldmg(maxElements - 1) as single
 calc_hero_elementals elementaldmg(), hero_slot
 '--build elemental strings
 DIM eleminfo as string
 DIM msg as string
 DIM found_elem as bool = NO
 msg = readglobalstring(302, "Elemental Effects:", 30)
 IF LEN(msg) THEN eleminfo &= msg & CHR(10)
 FOR i as integer = 0 TO gen(genNumElements) - 1
  msg = hero_elemental_resist_msg(elementnames(i), elementaldmg(i))
  IF LEN(msg) THEN
   eleminfo &= msg & CHR(10)
   found_elem = YES
  END IF
 NEXT i
 IF NOT found_elem THEN
  eleminfo = readglobalstring(130, "No Elemental Effects", 30)
 END IF
 RETURN eleminfo
END FUNCTION

FUNCTION hero_has_mp (byval hero_slot as integer) as bool
 RETURN gam.hero(hero_slot).stat.max.mp > 0
END FUNCTION

FUNCTION hero_uses_lmp (byval hero_slot as integer) as bool
 DIM hero_id as integer = gam.hero(hero_slot).id
 IF hero_id < 0 THEN
  debug "hero_uses_lmp: empty hero slot " & hero_slot
  RETURN NO
 END IF
 DIM her as HeroDef
 loadherodata her, hero_id
 DIM hero_node as NodePtr = her.reld
 READNODE hero_node."battle_menus" as batmenu
  WITHNODE batmenu."menu" as m
   DIM k as NodePtr
   k = m."kind".ptr
   IF k."spells".exists THEN
    DIM listnum as integer = k."spells".integer
    IF LEN(her.list_name(listnum)) > 0 THEN
     IF her.list_type(listnum) = 1 THEN
      'This hero has a LMP spell list, and it is in their battle menu
      'And it has a non-blank name
      RETURN YES
     END IF
    END IF
   END IF
  END WITHNODE
 END READNODE
 RETURN NO
END FUNCTION

FUNCTION StatCodeName (byval statnum as integer) as string
 'Don't confuse this with statnames()
 'These are internal codes that never change, and statnames()
 'is user-defined names for display
 SELECT CASE statnum
  CASE statHP: RETURN "HP"
  CASE statMP: RETURN "MP"
  CASE statAtk: RETURN "ATK"
  CASE statAim: RETURN "AIM"
  CASE statDef: RETURN "DEF"
  CASE statDodge: RETURN "DODGE"
  CASE statMagic: RETURN "MAGIC"
  CASE statWill: RETURN "WILL"
  CASE statSpeed: RETURN "SPEED"
  CASE statCtr: RETURN "CTR"
  CASE statFocus: RETURN "FOCUS"
  CASE statHitX: RETURN "HITX"
 END SELECT
 debug "StatCodeName: invalid stat number " & statnum
 RETURN "???"
END FUNCTION

FUNCTION StatByCodeName (statcode as string) as integer
 SELECT CASE statcode
  CASE "HP": RETURN statHP
  CASE "MP": RETURN statMP
  CASE "ATK": RETURN statAtk
  CASE "AIM": RETURN statAim
  CASE "DEF": RETURN statDef
  CASE "DODGE": RETURN statDodge
  CASE "MAGIC": RETURN statMagic
  CASE "WILL": RETURN statWill
  CASE "SPEED": RETURN statSpeed
  CASE "CTR": RETURN statCtr
  CASE "FOCUS": RETURN statFocus
  CASE "HITX": RETURN statHitX
 END SELECT
 debug "StatByCodeName: invalid code " & statcode
 RETURN -1
END FUNCTION

SUB ExpandTextHeroSlot (code as string, result as string, byval arg0 as ANY ptr=0, byval arg1 as ANY ptr=0, byval arg2 as ANY ptr=0)
 
 DIM hero_slot as integer = *CAST(integer ptr, arg0)
 
 DIM hero_id as integer = gam.hero(hero_slot).id

 IF hero_id < 0 THEN
  'Doesn't work on empty slots
  EXIT SUB
 END IF

 WITH gam.hero(hero_slot)

  'Simple stuff
  SELECT CASE UCASE(code)
   CASE "HERONAME": result = gam.hero(hero_slot).name
   CASE "LEVLABEL": result = readglobalstring(43, "Level", 10)
   CASE "LEV": result = STR(.lev)
   CASE "EXPLABEL": result = readglobalstring(33, "Experience", 10)
   CASE "EXPCUR": result = STR(.exp_cur)
   CASE "EXPNEXT": result = STR(.exp_next)
   CASE "EXPNEED": result = STR(.exp_next - .exp_cur)
   CASE "FORNEXT": result = readglobalstring(47, "for next", 10)
   CASE "LEVMPLABEL": result = readglobalstring(160, "Level MP", 20)
   CASE "ELEMENTS": result = get_elemental_info_text(hero_slot)
   CASE "MONEY": result = price_string(gold)
  END SELECT
  
  DIM her as HeroDef
  loadherodata her, hero_id
  DIM codename as string
  FOR statnum as integer = 0 TO statLast
   IF NOT should_hide_hero_stat(her, statnum) THEN
    codename = StatCodeName(statnum)
    SELECT CASE UCASE(code)
     CASE codename & "LABEL": result = statnames(statnum)
     CASE codename & "CUR": result = STR(.stat.cur.sta(statnum))
     CASE codename & "MAX": result = STR(.stat.max.sta(statnum))
     CASE codename & "BASE": result = STR(.stat.base.sta(statnum))
    END SELECT
   END IF
  NEXT statnum
  
  IF LEFT(code, 3) = "LMP" THEN
   DIM lmplev as integer = str2int(MID(code, 4)) - 1
   IF lmplev >= 0 ANDALSO lmplev <= 8 THEN
    result = STR(gam.hero(hero_slot).levelmp(lmplev))
   END IF
  END IF

 END WITH
END SUB

SUB ExpandTextHeroSlotStat (code as string, result as string, byval arg0 as ANY ptr=0, byval arg1 as ANY ptr=0, byval arg2 as ANY ptr=0)

 DIM hero_slot as integer = *CAST(integer ptr, arg0)
 DIM statnum as integer = *CAST(integer ptr, arg1)

 DIM hero_id as integer = gam.hero(hero_slot).id

 IF hero_id < 0 THEN
  'Doesn't work on empty slots
  EXIT SUB
 END IF

 WITH gam.hero(hero_slot)

  SELECT CASE UCASE(code)
   CASE "LABEL": result = statnames(statnum)
   CASE "CUR": result = STR(.stat.cur.sta(statnum))
   CASE "MAX": result = STR(.stat.max.sta(statnum))
   CASE "BASE": result = STR(.stat.base.sta(statnum))
  END SELECT

 END WITH 
END SUB

SUB ExpandTextItemScreen (code as string, result as string, byval arg0 as ANY ptr=0, byval arg1 as ANY ptr=0, byval arg2 as ANY ptr=0)

 DIM cur as Slice Ptr = arg0
 DIM swapcur as Slice Ptr = arg1

 DIM slot as integer = -1
 IF cur THEN slot = cur->Extra(0)

 SELECT CASE UCASE(code)
  CASE "EXIT": result = readglobalstring(35, "DONE", 10)
  CASE "SORT": result = readglobalstring(36, "AUTOSORT", 10)
  CASE "TRASH": result = readglobalstring(37, "TRASH", 10)
  CASE "ITEM":
   result = ""
   IF slot >= 0 ANDALSO inventory(slot).used THEN
    result = readitemname(inventory(slot).id)
   END IF
  CASE "DESC":
   result = ""
   IF slot >= 0 ANDALSO inventory(slot).used THEN
    result = readitemdescription(inventory(slot).id)
   ELSEIF cur ANDALSO cur->Lookup = SL_ITEM_TRASHBUTTON THEN
    IF swapcur THEN
     DIM swapslot as integer = swapcur->Extra(0)
     IF swapslot >= 0 ANDALSO inventory(swapslot).used THEN
      DIM id as integer = inventory(swapslot).id
      'info is the same as inventory(swapslot).text but with less whitespace
      DIM info as string = RTRIM(readitemname(id) & inventory_slot_number_str(inventory(swapslot)))
      IF item_can_be_discarded(id) THEN
       result = readglobalstring(41, "Discard", 10) & " " & info
      ELSE
       result = readglobalstring(42, "Cannot", 10) & " " & readglobalstring(41, "Discard", 10) & " " & info & "!"
      END IF
     END IF
    END IF
   END IF
 END SELECT
END SUB

SUB ExpandTextItemSlot (code as string, result as string, byval arg0 as ANY ptr=0, byval arg1 as ANY ptr=0, byval arg2 as ANY ptr=0)

 DIM slot as integer = *CAST(integer ptr, arg0)

 '.used .id .num .text
 IF inventory(slot).used THEN
  SELECT CASE UCASE(code)
   CASE "ITEM": result = readitemname(inventory(slot).id)
   CASE "NUM":  result = inventory_slot_number_str(inventory(slot))
  END SELECT
 ELSE
  'An empty slot
  SELECT CASE UCASE(code)
   CASE "ITEM": result = ""
   CASE "NUM": result = ""
  END SELECT
 END IF
END SUB

SUB ExpandTextVirtualKeyboard (code as string, result as string, byval arg0 as ANY ptr=0, byval arg1 as ANY ptr=0, byval arg2 as ANY ptr=0)

 DIM st as VirtualKeyboardScreenState Ptr = CAST(VirtualKeyboardScreenState ptr, arg0)

 SELECT CASE UCASE(code)
  CASE "PROMPT": result = st->prompt
 END SELECT

END SUB

'-----------------------------------------------------------------------

TYPE StatusScreenState
 sl as slice ptr
 slot as integer
 need_update as bool
END TYPE

SUB status_screen_refresh(byref st as StatusScreenState)
 plank_menu_clear st.sl, SL_STATUS_STATLIST

 expand_slice_text_insert_codes st.sl, @ExpandTextHeroSlot, @(st.slot)

 DIM hero_id as integer = gam.hero(st.slot).id
 IF hero_id >= 0 THEN
  DIM her as HeroDef
  loadherodata her, hero_id

  hide_slices_by_lookup_code st.sl, SL_STATUS_HIDE_IF_NO_HP, should_hide_hero_stat(her, statHP)
  hide_slices_by_lookup_code st.sl, SL_STATUS_HIDE_IF_NO_MP, (NOT hero_has_mp(st.slot) ORELSE should_hide_hero_stat(her, statMP))
  hide_slices_by_lookup_code st.sl, SL_STATUS_HIDE_IF_NO_LMP, NOT hero_uses_lmp(st.slot)
  hide_slices_by_lookup_code st.sl, SL_STATUS_HIDE_IF_MAX_LEV, gam.hero(st.slot).lev >= current_max_level
  hide_slices_by_lookup_code st.sl, SL_STATUS_HIDE_IF_NO_PORTRAIT, gam.hero(st.slot).portrait_pic < 0
  set_sprites_by_lookup_code st.sl, SL_STATUS_PORTRAIT, sprTypePortrait, gam.hero(st.slot).portrait_pic, gam.hero(st.slot).portrait_pal
  set_sprites_by_lookup_code st.sl, SL_STATUS_WALKABOUT, sprTypeWalkabout, gam.hero(st.slot).pic, gam.hero(st.slot).pal
  set_sprites_by_lookup_code st.sl, SL_STATUS_BATTLESPRITE, sprTypeHero, gam.hero(st.slot).battle_pic, gam.hero(st.slot).battle_pal

  FOR statnum as integer = 2 TO statLast
   IF NOT should_hide_hero_stat(her, statnum) THEN
    plank_menu_append st.sl, SL_STATUS_STATLIST, , @ExpandTextHeroSlotStat, @(st.slot), @statnum
   END IF
  NEXT statnum
 END IF
 
END SUB

SUB status_screen (byval slot as integer)

 DIM st as StatusScreenState
 st.slot = slot

 DIM holdscreen as integer = duplicatepage(vpage)

 st.sl = LoadSliceCollection(SL_COLLECT_STATUSSCREEN)
 ERROR_IF(st.sl = NULL, "Couldn't load slices!")

 status_screen_refresh st

 show_virtual_gamepad()

 menusound gen(genAcceptSFX)
 setkeys
 DO
  setwait speedcontrol
  setkeys
  playtimer
  IF game_check_cancel_key() THEN EXIT DO
  DIM do_next as bool = NO
  IF game_check_use_key() THEN do_next = YES
  IF get_gen_bool("/mouse/mouse_menus") THEN
   IF (readmouse.release AND mouseLeft) THEN
    do_next = YES
   ELSEIF (readmouse.release AND mouseRight) THEN
    EXIT DO
   END IF
  END IF
  IF do_next THEN
   DIM try_rename as bool = NO
   DIM sel as Slice Ptr = LookupSlice(SL_STATUS_PAGE_SELECT, st.sl)
   IF sel THEN
    SelectSliceNext sel
    menusound gen(genCursorSFX)
    DIM seldat as SelectSliceData ptr
    seldat = sel->SliceData
    if seldat->index = 0 then try_rename = YES
   ELSE
    try_rename = YES
   END IF
   IF try_rename ANDALSO gam.hero(st.slot).rename_on_status THEN
    '--status-screen rename is allowed
    renamehero st.slot, YES
    st.need_update = YES
    IF game_check_cancel_key() THEN EXIT DO
   END IF
  END IF
  IF is_active_party_slot(slot) THEN
   IF carray(ccLeft) > 1 THEN
    st.slot = loop_active_party_slot(st.slot, -1)
    st.need_update = YES
    menusound gen(genCursorSFX)
   END IF
   IF carray(ccRight) > 1 THEN
    st.slot = loop_active_party_slot(st.slot, 1)
    st.need_update = YES
    menusound gen(genCursorSFX)
   END IF
  END IF
  IF keyval(scF8) > 1 THEN
   slice_editor st.sl
   st.need_update = YES
  END IF

  IF UpdateScreenSlice() THEN st.need_update = YES
 
  IF st.need_update THEN
   status_screen_refresh st
   st.need_update = NO
  END IF
 
  'Draw the screen
  copypage holdscreen, vpage
  DrawSlice st.sl, vpage
  setvispage vpage
  dowait
 LOOP
 menusound gen(genCancelSFX)

 freepage holdscreen
 DeleteSlice @(st.sl)
 setkeys
 
END SUB

'-----------------------------------------------------------------------

SUB set_item_plank_state (byval sl as Slice Ptr, byval state as PlankItemState)
 SELECT CASE sl->SliceType
  CASE slText:
   SELECT CASE state
    CASE plankNORMAL:          ChangeTextSlice sl, , uiItemScreenItem * -1 - 1
    CASE plankSEL:             ChangeTextSlice sl, , uiItemScreenSelected2 * -1 - 1
    CASE plankDISABLE:         ChangeTextSlice sl, , uiItemScreenDisabled * -1 - 1
    CASE plankSELDISABLE:      ChangeTextSlice sl, , uiItemScreenSelectedDisabled2 * -1 - 1
    CASE plankSPECIAL:         ChangeTextSlice sl, , uiItemScreenSpecial * -1 - 1
    CASE plankSELSPECIAL:      ChangeTextSlice sl, , uiItemScreenSelectedSpecial2 * -1 - 1
    CASE plankITEMSWAP:        ChangeTextSlice sl, , uiItemScreenSwap * -1 - 1
    CASE plankITEMSWAPDISABLE: ChangeTextSlice sl, , uiItemScreenSwapDisabled * -1 - 1
    CASE plankITEMSWAPSPECIAL: ChangeTextSlice sl, , uiItemScreenSwapSpecial * -1 - 1
   END SELECT
  CASE slRectangle:
   sl->Visible = YES
   SELECT CASE state
    CASE plankNORMAL:          sl->Visible = NO
    CASE plankSEL:             ChangeRectangleSlice sl, , uiItemScreenHighlight2 * -1 - 1
    CASE plankDISABLE:         sl->Visible = NO
    CASE plankSELDISABLE:      ChangeRectangleSlice sl, , uiItemScreenHighlight2 * -1 - 1
    CASE plankSPECIAL:         sl->Visible = NO
    CASE plankSELSPECIAL:      ChangeRectangleSlice sl, , uiItemScreenHighlight2 * -1 - 1
    CASE plankITEMSWAP:        ChangeRectangleSlice sl, , uiItemScreenSwapHighlight2 * -1 - 1
    CASE plankITEMSWAPDISABLE: ChangeRectangleSlice sl, , uiItemScreenSwapHighlight2 * -1 - 1
    CASE plankITEMSWAPSPECIAL: ChangeRectangleSlice sl, , uiItemScreenSwapHighlight2 * -1 - 1
   END SELECT
 END SELECT
END SUB

SUB update_item_plank (byval pl as Slice Ptr, byref st as ItemScreenState)
 IF pl = 0 THEN debug "update_item_plank: null ptr" : EXIT SUB

 DIM slot as integer = pl->Extra(0)

 DIM sel as bool = (st.ps.cur = pl)
 DIM special as bool = NO
 DIM usable as bool = NO
 DIM swapsel as bool = (st.swapcur = pl)

 IF slot < 0 THEN
  usable = YES
 ELSEIF slot <= UBOUND(inventory) THEN
  IF inventory(slot).used THEN
   DIM item as ItemDef
   loaditemdata item, inventory(slot).id
   IF item.cannot_be_sold_or_dropped THEN special = YES 'Cannot be sold/dropped
   'TODO: call item_can_be_used_bits instead
   IF item.teach_spell >= 0 THEN usable = YES 'Teaches a spell
   IF item.oob_attack >= 0 THEN usable = YES 'Triggers an attack (cure)
   IF item.text_box >= 0 THEN usable = YES 'Triggers a text box
  END IF
 ELSE
  debug "update_item_plank: out of bounds slot " & slot
 END IF

 DIM state as PlankItemState = plankNORMAL

 IF sel = NO  ANDALSO usable = YES ANDALSO special = NO  THEN state = plankNORMAL
 IF sel = YES ANDALSO usable = YES ANDALSO special = NO  THEN state = plankSEL
 IF sel = NO  ANDALSO usable = NO  ANDALSO special = NO  THEN state = plankDISABLE
 IF sel = YES ANDALSO usable = NO  ANDALSO special = NO  THEN state = plankSELDISABLE
 IF sel = NO  ANDALSO                      special = YES THEN state = plankSPECIAL
 IF sel = YES ANDALSO                      special = YES THEN state = plankSELSPECIAL
 IF swapsel = YES THEN
  state = plankITEMSWAP
  IF usable = NO THEN state = plankITEMSWAPDISABLE
  IF special = YES THEN state = plankITEMSWAPSPECIAL
 END IF
 
 set_plank_state st.ps, pl, state
END SUB

SUB item_screen_refresh(byref st as ItemScreenState)

 IF st.refocus THEN save_plank_selection st.ps
 st.ps.cur = 0
 st.ps.hover = 0

 plank_menu_clear st.ps.m, SL_ITEM_ITEMLIST

 DIM pl as Slice Ptr
 FOR i as integer = 0 TO last_inv_slot()
  pl = plank_menu_append(st.ps.m, SL_ITEM_ITEMLIST, , @ExpandTextItemSlot, @i)
  IF pl THEN
   pl->Extra(0) = i
   update_item_plank pl, st
  ELSE
   debug "Failed to load item plank for slot " & i
  END IF
 NEXT i

 pl = LookupSlice(SL_ITEM_EXITBUTTON, st.ps.m)
 IF pl THEN pl->Extra(0) = -1 : update_item_plank pl, st
 pl = LookupSlice(SL_ITEM_SORTBUTTON, st.ps.m)
 IF pl THEN pl->Extra(0) = -1 : update_item_plank pl, st
 pl = LookupSlice(SL_ITEM_TRASHBUTTON, st.ps.m)
 IF pl THEN pl->Extra(0) = -1 :update_item_plank pl, st

 'Force update of slice positions
 RefreshSliceTree st.ps.m
 
 IF st.refocus THEN
  restore_plank_selection st.ps
  st.refocus = NO
 ELSE
  st.ps.cur = top_left_plank(st.ps)
 END IF
 update_item_plank st.ps.cur, st

 st.swapcur = 0
 expand_slice_text_insert_codes st.ps.m, @ExpandTextItemScreen, st.ps.cur, st.swapcur
 RefreshSliceTree st.ps.m
 update_plank_scrolling st.ps
 
END SUB

FUNCTION is_item_plank(byval sl as Slice Ptr) as bool
 IF sl = 0 THEN debug "is_item_plank: null slice ptr" : RETURN NO
 IF sl->Lookup = SL_PLANK_HOLDER THEN RETURN YES
 IF sl->Lookup = SL_ITEM_EXITBUTTON THEN RETURN YES
 IF sl->Lookup = SL_ITEM_SORTBUTTON THEN RETURN YES
 IF sl->Lookup = SL_ITEM_TRASHBUTTON THEN RETURN YES
 RETURN NO 
END FUNCTION

FUNCTION item_can_be_discarded (byval item_id as integer) as bool
 DIM item as ItemDef
 loaditemdata item, item_id
 IF item.cannot_be_sold_or_dropped THEN RETURN NO 'Cannot be sold/dropped
 RETURN YES
END FUNCTION

'Returns a length two bitvector (values 0-3), a combination of:
'1: can be used in-battle
'2: can be used out of battle
FUNCTION item_can_be_used_bits (byval item_id as integer) as integer
 DIM item as ItemDef
 loaditemdata item, item_id
 DIM ret as integer
 IF item.teach_spell >= 0 THEN ret OR= 2 'Teaches a spell
 IF item.oob_attack >= 0 THEN ret OR= 2 'Triggers an attack (cure) out-of-battle
 IF item.text_box >= 0 THEN ret OR= 2 'Triggers a text box out-of-battle
 IF item.battle_items_menu_attack >= 0 THEN ret OR= 1 'Triggers an attack in battle
 RETURN ret
END FUNCTION

SUB item_screen_cancel_action (byref st as ItemScreenState)
 menusound gen(genCancelSFX)
 IF st.swapcur THEN
  '--De-select the currently selected item
  DIM last as slice ptr = st.swapcur
  st.swapcur = 0
  update_item_plank last, st
 ELSE
  st.quit = YES
 END IF
END SUB

'Updates autoset tags
SUB item_screen_usable_item_action (byref st as ItemScreenState)
 IF st.ps.cur = 0 THEN debug "item_screen_usable_item_action: no cursor ptr" : EXIT SUB
 DIM slot as integer = st.ps.cur->Extra(0)
 
 IF inventory(slot).used ANDALSO inventory(slot).num > 0 THEN
  DIM item_id as integer = inventory(slot).id
  IF item_can_be_used_bits(item_id) AND 2 THEN
   DIM consumed as bool = NO ' FIXME: can probably get rid of this later
   IF use_item_in_slot(slot, st.textbox, consumed) THEN
    st.re_use = YES
    evalitemtags
    evalherotags
    tag_updates
   END IF
  END IF
  st.need_update = YES
  st.refocus = YES
  st.swapcur = 0
 END IF
END SUB

'Updates autoset tags
SUB item_screen_use_action (byref st as ItemScreenState)
 IF st.ps.cur = 0 THEN debug "item_screen_use_action: no cursor ptr" : EXIT SUB
 DIM slot as integer = st.ps.cur->Extra(0)
 DIM swapslot as integer = -1
 IF st.swapcur THEN swapslot = st.swapcur->Extra(0)
 
 DIM lastcur as slice ptr = st.ps.cur
 DIM lastswap as slice ptr = st.swapcur
 
 SELECT CASE st.ps.cur->Lookup
  CASE SL_ITEM_EXITBUTTON:
   st.quit = YES
   EXIT SUB
  CASE SL_ITEM_SORTBUTTON:
   IF st.swapcur THEN
    st.swapcur = 0
    update_item_plank lastswap, st
   ELSE
    inventory_autosort()
    st.need_update = YES
    st.refocus = YES
   END IF
   EXIT SUB
  CASE SL_ITEM_TRASHBUTTON:
   IF st.swapcur THEN
    IF inventory(swapslot).used ANDALSO item_can_be_discarded(inventory(swapslot).id) THEN
     MenuSound gen(genAcceptSFX)
     WITH inventory(swapslot)
      .used = 0
      .id = 0
      .num = 0
     END WITH
     evalitemtags
     tag_updates
    END IF
    update_inventory_caption swapslot
    st.need_update = YES
    st.refocus = YES
   END IF
   EXIT SUB
 END SELECT
 
 IF st.re_use THEN
  st.re_use = NO
  item_screen_usable_item_action st
  EXIT SUB
 END IF

 IF st.swapcur THEN
  IF st.swapcur = st.ps.cur THEN
   item_screen_usable_item_action st
   EXIT SUB
  ELSE
   SWAP inventory(slot), inventory(swapslot)
   st.need_update = YES
   st.refocus = YES
  END IF
  st.swapcur = 0
  update_item_plank lastswap, st
 ELSE
  st.swapcur = st.ps.cur
  update_item_plank st.swapcur, st
 END IF
 
END SUB

SUB item_screen_mouse_handler(byref st as ItemScreenState)
 DIM oldhover as Slice Ptr = st.ps.hover
 IF plank_menu_update_hover(st.ps) THEN
  IF oldhover THEN update_item_plank oldhover, st
  IF st.ps.hover THEN update_item_plank st.ps.hover, st
 END IF
 
 DIM oldswap as Slice Ptr = st.swapcur
 IF st.swapcur ANDALSO (readmouse.buttons AND mouseRight) THEN
  'Cancel swap cursor selection if you right-click
  st.swapcur = 0
  IF oldswap THEN update_item_plank oldswap, st
 END IF
  
 DIM oldcur as Slice Ptr = st.ps.cur
 IF st.ps.hover THEN
  IF ((readmouse.release AND mouseLeft) ORELSE (readmouse.clicks AND mouseRight)) ANDALSO readmouse.drag_dist < 10 THEN
   'Selecting the hovered plank
   'and deselect the old selection
   st.ps.cur = st.ps.hover
   IF oldcur THEN update_item_plank oldcur, st
   IF st.ps.cur THEN update_item_plank st.ps.cur, st
  END IF
 END IF
 IF st.ps.cur THEN
  IF st.ps.cur <> oldcur THEN
   'Cursor just changed
   menusound gen(genCursorSFX)
   update_plank_scrolling st.ps
   expand_slice_text_insert_codes st.ps.m, @ExpandTextItemScreen, st.ps.cur, st.swapcur
   IF st.ps.cur ANDALSO st.swapcur ANDALSO st.ps.cur <> st.swapcur THEN
    'Confirming a swap, so it is okay to to activate now
    IF (readmouse.release AND mouseLeft) THEN
     item_screen_use_action st
    END IF
   END IF
   IF st.ps.cur ANDALSO (st.ps.cur->Lookup = SL_ITEM_EXITBUTTON ORELSE st.ps.cur->Lookup = SL_ITEM_SORTBUTTON) THEN
    'the exit button and the sort buttons don't need to be pre-focused, we can activate them now
    IF (readmouse.release AND mouseLeft) THEN
     item_screen_use_action st
    END IF
   END IF
  ELSEIF st.ps.hover = st.ps.cur THEN
   'Cursor did not change, so a second click can activate it
   IF (readmouse.release AND mouseLeft) THEN
    menusound gen(genCursorSFX)
    item_screen_use_action st
   END IF
  END IF
 END IF
 
 plank_menu_drag_scroll st.ps
 plank_menu_mouse_wheel st.ps
END SUB

FUNCTION item_screen () as integer
'Returns 0, or ID of textbox to open (box 0 can't be opened)

 DIM st as ItemScreenState
 st.ps.is_plank_callback = @is_item_plank
 st.ps.state_callback = @set_item_plank_state

 DIM holdscreen as integer = duplicatepage(vpage)

 st.ps.m = LoadSliceCollection(SL_COLLECT_ITEMSCREEN)
 ERROR_IF(st.ps.m = NULL, "Couldn't load slices!", 0)
 
 'This only needs to be called once after the collection loads because gen(genInvDescFixedHeight) won't change mid-inventory
 refresh_inventory_description_box_sizing st.ps.m

 item_screen_refresh st

 show_virtual_gamepad()
 
 menusound gen(genAcceptSFX)
 setkeys
 DO
  setwait speedcontrol
  setkeys
  playtimer
  
  IF game_check_cancel_key() THEN
   item_screen_cancel_action st
  END IF

  IF game_check_use_key() ORELSE st.re_use THEN
   item_screen_use_action st
  END IF

  IF get_gen_bool("/mouse/mouse_menus") ANDALSO NOT st.re_use THEN
   item_screen_mouse_handler(st) 
  END IF

  st.lastcur = st.ps.cur
  IF plank_menu_arrows(st.ps) THEN
   update_item_plank st.lastcur, st
   update_item_plank st.ps.cur, st
   menusound gen(genCursorSFX)
   expand_slice_text_insert_codes st.ps.m, @ExpandTextItemScreen, st.ps.cur, st.swapcur
  END IF
  
  IF keyval(scF8) > 1 THEN
   slice_editor st.ps.m
   st.need_update = YES
  END IF
  
  IF st.quit THEN
   menusound gen(genCancelSFX)
   EXIT DO
  END IF
  
  IF st.textbox > 0 THEN
   EXIT DO
  END IF
  
  IF UpdateScreenSlice() THEN st.need_update = YES
 
  IF st.need_update THEN
   item_screen_refresh st
   st.need_update = NO
  END IF

  'Always refresh the scrolling (because the description box size can change dynamically)
  update_plank_scrolling st.ps
 
  'Draw the screen
  copypage holdscreen, vpage
  DrawSlice st.ps.m, vpage
  setvispage vpage
  dowait
 LOOP

 freepage holdscreen
 DeleteSlice @(st.ps.m)
 setkeys

 RETURN st.textbox
END FUNCTION

SUB inventory_autosort()
 DIM autosort_changed as bool = NO
 'First sort all items to the top
 DIM numitems as integer = 0
 FOR i as integer = 0 TO last_inv_slot() - 1
  IF inventory(i).used THEN numitems += 1 : CONTINUE FOR
  FOR o as integer = i + 1 TO last_inv_slot()
   IF inventory(o).used THEN
    SWAP inventory(i), inventory(o)
    numitems += 1
    autosort_changed = YES
    EXIT FOR
   END IF
  NEXT o
 NEXT i

 IF gen(genAutosortScheme) = 0 THEN
  'Sorting by type: cache the sort order of the items, or the cost of repeatedly loading
  'item data might go out of control
  DIM item as ItemDef
  FOR slot as integer = 0 TO numitems - 1
   loaditemdata item, inventory(slot).id

   IF item.battle_items_menu_attack >= 0 THEN  'Usable in-battle
    inventory(slot).sortorder = 0
   ELSEIF item.oob_attack >= 0 THEN  'Usable out-of-battle (attack)
    inventory(slot).sortorder = 10
   ELSEIF item.text_box >= 0 THEN  'Usable out-of-battle (textbox)
    inventory(slot).sortorder = 20
   ELSEIF item.teach_spell >= 0 THEN  'Teach spell
    inventory(slot).sortorder = 30
   ELSEIF item_is_equippable(item) THEN  'Equippable
    FOR i as integer = 0 to 4
     IF item.eqslots(i) THEN
      inventory(slot).sortorder = 40 + i
      EXIT FOR
     END IF
    NEXT i
   ELSE
    inventory(slot).sortorder = 50
   END IF
   IF item.cannot_be_sold_or_dropped THEN
    inventory(slot).sortorder += 0  'Can not be sold/dropped
   ELSEIF item.consumed_by_use THEN
    inventory(slot).sortorder += 1  'Consumed by use
   ELSE
    inventory(slot).sortorder += 2  'Unlimited use
   END IF
  NEXT
 ELSEIF gen(genAutosortScheme) = 1 THEN
  'Sorting by usable (into four groups, depending on whether usable in battle and out of battle):
  'Usable in-and-out of battle, usable out of battle, usable in battle, not usable
  FOR slot as integer = 0 TO numitems - 1
   inventory(slot).sortorder = 3 - item_can_be_used_bits(inventory(slot).id)
  NEXT
 END IF

 'Then sort by the autosort criterion (use bubble sort because it's stable)
 FOR i as integer = 0 TO numitems - 1
  FOR slot as integer = numitems - 2 TO i STEP -1
   DIM disordered as bool = NO
   SELECT CASE gen(genAutosortScheme)
    CASE 0 'type
     IF inventory(slot).sortorder > inventory(slot + 1).sortorder THEN disordered = YES
    CASE 1 'use
     IF inventory(slot).sortorder > inventory(slot + 1).sortorder THEN disordered = YES
    CASE 2 'alphabetical
     IF string_compare(inventory(slot).text, inventory(slot + 1).text) > 0 THEN disordered = YES
    CASE 3 'id
     IF inventory(slot).id > inventory(slot + 1).id THEN disordered = YES
    CASE 4 'nothing
   END SELECT
   IF disordered THEN
    SWAP inventory(slot), inventory(slot + 1)
    autosort_changed = YES
   END IF
  NEXT slot
 NEXT i
 IF autosort_changed THEN
  menusound gen(genAcceptSFX)
 ELSE
  menusound gen(genCancelSFX)
 END IF
END SUB

'-----------------------------------------------------------------------

TYPE SpellScreenState
 ps as PlankState
 slot as integer
 need_update as bool
 in_list as bool
END TYPE

SUB spell_screen_refresh(byref st as SpellScreenState)

 plank_menu_clear st.ps.m, SL_SPELL_LISTLIST
 plank_menu_clear st.ps.m, SL_SPELL_SPELLLIST

 'expand_slice_text_insert_codes st.ps.m, @ExpandTextSpellScreen, st.ps.cur

' DIM pl as Slice Ptr
' FOR i as integer = 0 TO last_inv_slot()
'  pl = plank_menu_append(st.ps.m, SL_ITEM_ITEMLIST, , @ExpandTextItemSlot, @i)
'  IF pl THEN
'   pl->Extra(0) = i
'   update_item_plank pl, st
'  ELSE
'   debug "Failed to load item plank for slot " & i
'  END IF
' NEXT i
'
' pl = LookupSlice(SL_ITEM_EXITBUTTON, st.ps.m)
' IF pl THEN pl->Extra(0) = -1 : update_item_plank pl, st
' pl = LookupSlice(SL_ITEM_SORTBUTTON, st.ps.m)
' IF pl THEN pl->Extra(0) = -1 : update_item_plank pl, st
' pl = LookupSlice(SL_ITEM_TRASHBUTTON, st.ps.m)
' IF pl THEN pl->Extra(0) = -1 :update_item_plank pl, st
'
' 'Early draw to force update of slice positions 
' DrawSlice st.ps.m, vpage
' 
' IF st.refocus THEN
'  restore_plank_selection st.ps
'  st.refocus = NO
' ELSE
'  st.ps.cur = top_left_plank(st.ps)
' END IF
' update_item_plank st.ps.cur, st
'
' st.swapcur = 0
' update_plank_scrolling st.ps
 
END SUB


SUB spell_screen (byval slot as integer)

 DIM st as SpellScreenState

 DIM holdscreen as integer = duplicatepage(vpage)

 st.ps.m = LoadSliceCollection(SL_COLLECT_SPELLSCREEN)
 ERROR_IF(st.ps.m = NULL, "Couldn't load slices!")

 spell_screen_refresh st
 
 show_virtual_gamepad()
 
 menusound gen(genAcceptSFX)
 setkeys
 DO
  setwait speedcontrol
  setkeys
  playtimer
  IF game_check_cancel_key() THEN EXIT DO
  IF game_check_use_key() THEN
  END IF
  IF is_active_party_slot(slot) THEN
   IF NOT st.in_list THEN
    IF carray(ccLeft) > 1 THEN
     st.slot = loop_active_party_slot(st.slot, -1)
     st.need_update = YES
     menusound gen(genCursorSFX)
    END IF
    IF carray(ccRight) > 1 THEN
     st.slot = loop_active_party_slot(st.slot, 1)
     st.need_update = YES
     menusound gen(genCursorSFX)
    END IF
   END IF
  END IF
  IF keyval(scF8) > 1 THEN
   slice_editor st.ps.m
   st.need_update = YES
  END IF

  IF UpdateScreenSlice() THEN st.need_update = YES
 
  IF st.need_update THEN
   spell_screen_refresh st
   st.need_update = NO
  END IF
 
  'Draw the screen
  copypage holdscreen, vpage
  DrawSlice st.ps.m, vpage
  setvispage vpage
  dowait
 LOOP
 menusound gen(genCancelSFX)

 freepage holdscreen
 DeleteSlice @(st.ps.m)

END SUB

'-----------------------------------------------------------------------

FUNCTION trylearn (byval who as integer, byval atk as integer) as bool
'Try to add a spell to a hero's spelllists whereever it is set to be learnable from an item.
'who is hero position in the party, atk is attack ID + 1

'--returns whether the spell was learned at least once

IF gam.hero(who).id = -1 THEN
 debug "trylearn fail on empty party slot " & who
 RETURN NO
END IF

DIM result as bool = NO

dim her as HeroDef
'--load the hero's data.
loadherodata her, gam.hero(who).id

'--for each spell list
FOR j as integer = 0 TO 3
 '--for each spell slot
 FOR o as integer = 0 TO 23
  '--if this slot is empty and accepts this spell
  '--and is learnable by an item
  IF gam.hero(who).spells(j, o) = 0 AND her.spell_lists(j,o).attack = atk AND her.spell_lists(j,o).learned = 0 THEN
   gam.hero(who).spells(j, o) = atk
   result = YES
  END IF
 NEXT o
NEXT j

RETURN result

END FUNCTION

'Remove an equipped item. Returns false if couldn't empty the slot, otherwise true.
'who: hero slot
'where: equip slot
'resetdefwep: if unequipping weapon, reequip the default weapon
'force: unequip even if there's no room in the inventory (item will be destroyed!)
FUNCTION unequip (who as integer, where as integer, resetdefwep as bool = YES, force as bool = YES) as bool
 WITH gam.hero(who)
  '--exit if nothing is equiped
  IF .equip(where).id < 0 THEN RETURN YES

  '--return item to inventory (if not the default weapon)
  IF where = 0 AND .equip(where).id = .def_wep - 1 THEN
  ELSE
   IF getitem(.equip(where).id, , YES) = NO THEN  'ok_to_fail=YES
    debug "Returning " & readitemname(.equip(where).id) & " to inventory failed (force=" & force & ")"
    IF force = NO THEN RETURN NO
   END IF
  END IF

  '--blank out equipment
  .equip(where).id = -1

  update_hero_max_and_cur_stats who

  IF where = 0 AND resetdefwep THEN
   '--restore default weapon
   IF doequip(.def_wep - 1, who, where) = NO THEN
    'This would be a really weird edge case for this to fail. I don't think it could happen
    debug "Failed attempt to restore default weapon """ & readitemname(.def_wep - 1) & """ for hero """ & .name & """ after an unequip (That's weird)"
   END IF
  END IF
 END WITH

 evalitemtags
 evalherotags  'You could kill someone, right?
 tag_updates
 RETURN YES
END FUNCTION

SUB loadshopstuf (array() as integer, byval id as integer)
 DIM ol as integer = getbinsize(binSTF) \ 2 'old size on disk
 DIM nw as integer = curbinsize(binSTF) \ 2 'new size in memory
 flusharray array(), nw * 50 - 1, 0
 'load shop data from STF lump
 loadrecord buffer(), game + ".stf", ol * 50, id
 'in case shop data has been resized, scale records to new size
 FOR thingidx as integer = 0 to 49  'shop item
  FOR i as integer = 0 TO ol - 1  'field in shop item record
   array(thingidx * nw + i) = buffer(thingidx * ol + i)
  NEXT i
  ' Format upgrade: initialise stockidx
  IF array(thingidx * nw + 37) = 0 THEN array(thingidx * nw + 37) = 1 + thingidx
 NEXT thingidx
END SUB

FUNCTION count_available_spells(byval who as integer, byval list as integer) as integer
 DIM i as integer
 DIM n as integer = 0
 FOR i = 0 to 23
  IF gam.hero(who).spells(list, i) > 0 THEN n + = 1
 NEXT i
 RETURN n
END FUNCTION

FUNCTION outside_battle_cure (byval atk as integer, byval target as integer, byval attacker as integer, byval spread as bool, byref was_full as bool=NO) as bool
 DIM i as integer
 DIM didcure as bool = NO

 IF spread = NO THEN
  IF chkOOBtarg(attacker, target, atk) THEN
   oobcure attacker, target, atk, 1, was_full
   didcure = YES
  END IF
 ELSE
  DIM target_count as integer = 0
  FOR i = 0 TO 3
   IF chkOOBtarg(attacker, i, atk) THEN target_count += 1
  NEXT i

  FOR i = 0 TO 3
   IF chkOOBtarg(attacker, i, atk) THEN
     oobcure attacker, i, atk, target_count, was_full
     didcure = YES
   END IF 
  NEXT i
 END IF
 checkfatal = YES
 evalherotags
 evalitemtags
 tag_updates
 RETURN didcure
END FUNCTION

'===Equip menu===

SUB equip_menu_controls_slot_picker (st as EquipMenuState)
 '--primary menu
 DIM do_pick as bool = NO
 IF game_check_cancel_key() THEN
  st.quit = YES
  EXIT SUB
 END IF
 IF st.allow_switch THEN
  IF carray(ccLeft) > 1 THEN 'Left: previous hero
   st.who = loop_active_party_slot(st.who, -1)
   equip_menu_setup st
   MenuSound gen(genCursorSFX)
   st.item_info = equip_menu_equipped_item_info(st)
  END IF
  IF carray(ccRight) > 1 THEN 'Right: next hero
   st.who = loop_active_party_slot(st.who, 1)
   equip_menu_setup st
   MenuSound gen(genCursorSFX)
   st.item_info = equip_menu_equipped_item_info(st)
  END IF
 END IF
 usemenusounds
 IF usemenu(st.slot, 0, 0, 6, 6) THEN
  st.item_info = equip_menu_equipped_item_info(st)
 END IF
 IF game_check_use_key() THEN
  do_pick = YES
 END IF
 
 IF get_gen_bool("/mouse/mouse_menus") THEN
  DIM hover as integer = -1
  DIM old_slot as integer = st.slot
  IF rect_collide_point(Type(14, 10, 140, 16), readmouse.pos) THEN
   'hero name box
   IF st.allow_switch ANDALSO (readmouse.release AND mouseLeft) THEN
    st.who = loop_active_party_slot(st.who, 1)
    equip_menu_setup st
    MenuSound gen(genCursorSFX)
    st.item_info = equip_menu_equipped_item_info(st)
   END IF
  ELSEIF rect_collide_point(Type(196, 36, 80, 78), readmouse.pos) THEN
   'equipment box
   FOR i as integer = 0 TO 6
    IF rect_collide_point(Type(204, 45 + i * 9, 8 * 8, 9), readmouse.pos) THEN hover = i
   NEXT i
   IF hover >= 0 THEN
    IF (readmouse.release AND mouseLeft) ANDALSO (hover = st.slot ORELSE hover = 6) THEN do_pick = YES
    IF (readmouse.release AND mouseLeft) THEN st.slot = hover
   END IF
  ELSE
   'Anywhere else
   IF (readmouse.release AND mouseLeft) THEN
    st.quit = YES
    EXIT SUB
   END IF
  END IF
  IF (readmouse.release AND mouseRight) ANDALSO readmouse.drag_dist < 10 THEN
   IF hover >= 0 THEN
    st.slot = hover
   ELSE
    st.quit = YES
    EXIT SUB
   END IF
  END IF
  IF st.slot <> old_slot THEN
   MenuSound gen(genCursorSFX)
   st.item_info = equip_menu_equipped_item_info(st)
  END IF
 END IF

 IF do_pick THEN
  IF st.slot < 5 THEN
   '--change equipment
   IF st.eq(st.slot).count > 0 OR gam.hero(st.who).equip(st.slot).id >= 0 THEN
    '--switch to change equipment mode
    st.mode = 1
    st.eq_cursor.pt = 0
    st.eq_cursor.top = 0
    'Number of options = num equippable things + nothing/unequip option
    st.eq_cursor.last = (st.eq(st.slot).count + 1) - 1
    equip_menu_stat_bonus st
    MenuSound gen(genAcceptSFX)
    st.item_info = equip_menu_available_item_info(st)
   END IF
   'UPDATE ITEM POSSESSION BITSETS
   evalitemtags
  END IF
  IF st.slot = 5 THEN
   MenuSound gen(genCancelSFX)
   '--unequip all
   FOR i as integer = 0 TO 4
    IF unequip(st.who, i, , NO) = NO THEN  'force=NO
     st.item_info = readglobalstring(305, "No room in inventory", 30)
    END IF
   NEXT i
   equip_menu_setup st
   'UPDATE ITEM POSSESSION BITSETS
   evalitemtags
  END IF
  IF st.slot = 6 THEN st.quit = YES
 END IF
END SUB

SUB equip_menu_controls_equip_change (st as EquipMenuState)
 '--change equip menu
 DIM do_pick as bool = NO
 DIM do_cancel as bool = NO
 IF game_check_cancel_key() THEN do_cancel = YES
 usemenusounds
 IF usemenu(st.eq_cursor) THEN
  equip_menu_stat_bonus st
  st.item_info = equip_menu_available_item_info(st)
 END IF
 IF game_check_use_key() THEN do_pick = YES

 IF get_gen_bool("/mouse/mouse_menus") THEN
  DIM hover as integer = -1
  DIM old_cursor as integer = st.eq_cursor.pt
  IF rect_collide_point(Type(188, 13, 96, 152), readmouse.pos) THEN
   'equipment box
   DIM list_bottom as integer = st.eq_cursor.top + st.eq_cursor.size
   DIM list_last as integer = st.eq(st.slot).count
   FOR i as integer = st.eq_cursor.top TO small(list_bottom, list_last)
    IF rect_collide_point(Type(192, 19 + (i - st.eq_cursor.top) * 8, 11 * 8, 8), readmouse.pos) THEN hover = i
   NEXT i
   IF hover >= 0 ANDALSO readmouse.drag_dist < 10 THEN
    IF (readmouse.release AND mouseLeft) ANDALSO hover = st.eq_cursor.pt THEN do_pick = YES
    IF (readmouse.release AND mouseLeft) THEN st.eq_cursor.pt = hover
   END IF
  ELSE
   'Anywhere else
   IF (readmouse.release AND mouseLeft) ANDALSO readmouse.drag_dist < 10 THEN
    do_cancel = YES
   END IF
  END IF
  IF (readmouse.release AND mouseRight) ANDALSO readmouse.drag_dist < 10 THEN
   IF hover >= 0 THEN
    st.eq_cursor.pt = hover
   ELSE
    do_cancel = YES
   END IF
  END IF
  'Scrolling support
  st.eq_cursor.hover = hover
  st.eq_cursor.spacing = 8
  mouse_scroll_menu(st.eq_cursor)
  mouse_drag_menu(st.eq_cursor)
  
  IF st.eq_cursor.pt <> old_cursor THEN
   MenuSound gen(genCursorSFX)
   equip_menu_stat_bonus st
   st.item_info = equip_menu_available_item_info(st)
  END IF
 END IF

 IF do_cancel THEN
  st.mode = 0
  MenuSound gen(genCancelSFX)
  st.item_info = equip_menu_equipped_item_info(st)
  EXIT SUB
 END IF

 IF do_pick THEN
  IF st.eq_cursor.pt = st.eq(st.slot).count THEN
   '--unequip
   IF unequip(st.who, st.slot, , NO) = NO THEN  'force=NO
    st.item_info = readglobalstring(305, "No room in inventory", 30)
   END IF
   equip_menu_back_to_menu st
   MenuSound gen(genCancelSFX)
  ELSE
   '--normal equip
   DIM item_id as integer = inventory(st.eq(st.slot).offset(st.eq_cursor.pt)).id
   equip_menu_do_equip item_id, st
   MenuSound gen(genAcceptSFX)
  END IF
 END IF
END SUB

SUB equip_menu (who as integer, allow_switch as bool = YES)
'who is party slot.
'allow_switch is whether to allow switching left/right to other heroes

DIM m(4) as string
DIM page as integer = compatpage
DIM holdscreen as integer = allocatepage
DIM st as EquipMenuState
st.allow_switch = allow_switch

DIM i as integer
DIM tog as integer = 0

'--get names
m(0) = readglobalstring(38, "Weapon", 10)
FOR i = 0 TO 3
 m(i + 1) = readglobalstring(25 + i, "Armor" & i+1)
NEXT i
st.menu(5) = rpad(readglobalstring(39, "-REMOVE-", 8), " ", 8)
st.menu(6) = rpad(readglobalstring(40, "-EXIT-", 8), " ", 8)

'--initialize
WITH st
 .mode = 0
 .who = who
 .eq_cursor.size = 17
 .default_weapon_name = ""
 .unequip_caption = rpad(readglobalstring(110, "Nothing", 10), " ", 11)
END WITH
equip_menu_setup st

st.item_info = equip_menu_equipped_item_info(st)

'--prepare the backdrop
'preserve the background behind the equip menu
copypage page, holdscreen

show_virtual_gamepad()

'--main loop
MenuSound gen(genAcceptSFX)
setkeys
DO
 setwait speedcontrol
 setkeys
 tog = tog XOR 1
 playtimer
 IF st.mode = 0 THEN
  equip_menu_controls_slot_picker st
 ELSE
  equip_menu_controls_equip_change st
 END IF
 IF st.quit THEN EXIT DO

 '--display
 copypage holdscreen, page
 centerfuz 160, 100, 304, 184, 1, page 'backdrop box
 centerbox 84, 18, 140, 16, 4, page    'hero name
 centerbox 84, 102, 140, 130, 4, page  'stats
 centerbox 236, 75, 80, 78, 4, page    'equipment
 edgeprint gam.hero(st.who).name, 84 + ancCenter, 13, uilook(uiText), page
 DIM stat_v_offset as integer = 0
 FOR i = 0 TO statLast
  'Draw stats and bonuses
  IF should_hide_hero_stat(st.hero, i) THEN CONTINUE FOR
  DIM as string stat_caption, stat_caption2
  DIM statcol2 as integer
  DIM resultant_stat as integer
  statcol2 = uilook(uiMenuItem)
  IF st.mode = 0 THEN
   '--Selecting an equip slot
   resultant_stat = gam.hero(st.who).stat.max.sta(i)
  ELSE
   '--Selecting an item
   IF st.stat_bonus(i) < 0 THEN
    stat_caption = STR(st.stat_bonus(i))
    statcol2 = uilook(uiDisabledItem)
   ELSEIF st.stat_bonus(i) > 0 THEN
    stat_caption = "+" & st.stat_bonus(i)
    statcol2 = uilook(uiSelectedItem + tog)
   END IF
   'Calculate new max stat
   resultant_stat = gam.hero(st.who).stat.base.sta(i) + st.stat_total_bonus(i)
   IF prefbit(43) THEN 'Cap minimum stats to zero
    resultant_stat = large(resultant_stat, 0)
   END IF
   DIM cap as integer = gen(genStatCap + i)
   IF cap > 0 AND resultant_stat > cap THEN
    resultant_stat = cap
    'Don't flash anything unless the change in equipment actually modifies the stat
    IF st.stat_bonus(i) <> 0 THEN
     stat_caption = fgcol_text(stat_caption, uilook(uiSelectedDisabled))
     statcol2 = uilook(uiSelectedDisabled + tog)
    END IF
   END IF
  END IF
  stat_caption2 = STR(resultant_stat)
  edgeprint statnames(i) & stat_caption, 20, 42 + stat_v_offset * 10, uilook(uiMenuItem), page, YES
  edgeprint stat_caption2, 148 - LEN(stat_caption2) * 8, 42 + stat_v_offset * 10, statcol2, page
  stat_v_offset += 1
 NEXT i
 IF st.mode = 0 THEN
  '--main menu display
  FOR i = 0 TO 6
   textcolor uilook(uiMenuItem), uilook(uiEquipSlotBg)
   IF i < 5 THEN
    IF gam.hero(st.who).equip(i).id = -1 AND st.eq(i).count = 0 THEN textcolor uilook(uiMenuItem), uilook(uiEquipDisabledSlotBg)
   END IF
   IF st.slot = i THEN
    textcolor uilook(uiSelectedItem + tog), uilook(uiEquipCursorBg)
    IF i < 5 THEN
      IF st.eq(i).count = 0 THEN textcolor uilook(uiSelectedItem), uilook(uiEquipCursorBg)
    END IF
   END IF
   printstr st.menu(i), 204, 45 + i * 9, page
  NEXT i
  IF st.slot < 5 THEN
   centerbox 236, 22, (LEN(m(st.slot)) + 2) * 8, 16, 4, page
   edgeprint m(st.slot), 236 - (LEN(m(st.slot)) * 4), 17, uilook(uiText), page
  END IF
 END IF
 IF st.mode = 1 THEN
  '--change equipment menu
  centerbox 236, 91, 96, 152, 4, page
  FOR i = st.eq_cursor.top TO st.eq_cursor.top + st.eq_cursor.size
   textcolor uilook(uiMenuItem), 0
   IF i = st.eq_cursor.pt THEN textcolor uilook(uiSelectedItem + tog), uilook(uiEquipCursorBg)
   IF i > st.eq(st.slot).count THEN
    '--all done!
    EXIT FOR
   END IF
   printstr equip_menu_available_item_caption(st, i), 192, 19 + (i - st.eq_cursor.top) * 8, page
  NEXT i
 END IF

 DIM desc_str as string = wordwrap(st.item_info, 304 / 8)
 REDIM spl() as string
 split desc_str, spl()
 edgeboxstyle pCentered, pBottom - 4, 312, 6 + 10 * (UBOUND(spl) + 1), 4, page
 FOR i as integer = 0 TO UBOUND(spl)
  edgeprint spl(i), pCenteredLeft, 182 - (UBOUND(spl) - i) * 10, uilook(uiText), page
 NEXT i

 setvispage vpage
 dowait
LOOP
setkeys
freepage page
freepage holdscreen
MenuSound gen(genCancelSFX)

'tags handled in unequip, doequip
END SUB

'Returns the item ID corresponding to the selected menu item of the equippable item menu,
'or -1 if Unequip is selected and it's not the default weapon
FUNCTION equip_menu_available_item_id(st as EquipMenuState) as integer
 IF st.eq_cursor.pt = st.eq(st.slot).count THEN
  '--last option: unequip
  IF st.slot = 0 THEN
   '--special handling for weapon
   RETURN gam.hero(st.who).def_wep - 1
  ELSE
   RETURN -1
  END IF
 ELSE
  '--equippable item
  RETURN inventory(st.eq(st.slot).offset(st.eq_cursor.pt)).id
 END IF
END FUNCTION

'Returns the caption for a menu item in the equippable item menu
FUNCTION equip_menu_available_item_caption(st as EquipMenuState, byval menuslot as integer) as string
 IF menuslot = st.eq(st.slot).count THEN
  '--last option: unequip
  IF st.slot = 0 THEN
   RETURN st.default_weapon_name
  ELSE
   RETURN st.unequip_caption
  END IF
 ELSE
  '--equippable item
  RETURN inventory(st.eq(st.slot).offset(menuslot)).text
 END IF
END FUNCTION

FUNCTION equip_menu_equipped_item_info(st as EquipMenuState) as string
 WITH gam.hero(st.who)
  IF st.slot <= UBOUND(.equip) ANDALSO .equip(st.slot).id >= 0 THEN
   RETURN readitemdescription(.equip(st.slot).id)
  END IF
 END WITH
 RETURN ""
END FUNCTION

FUNCTION equip_menu_available_item_info(st as EquipMenuState) as string
 DIM item as integer = equip_menu_available_item_id(st)
 IF item >= 0 THEN
  RETURN readitemdescription(item)
 END IF
 RETURN ""
END FUNCTION

SUB equip_menu_setup (byref st as EquipMenuState)
 DIM byref herost as HeroState = gam.hero(st.who)
 loadherodata st.hero, herost.id
 st.default_weapon_name = rpad(readitemname(herost.def_wep - 1), " ", 11)
 IF LEN(TRIM(st.default_weapon_name)) = 0 THEN
  st.default_weapon_name = st.unequip_caption
 END IF

 FOR i as integer = 0 TO 4
  st.menu(i) = "        "
  IF herost.equip(i).id >= 0 THEN
   st.menu(i) = rpad(readitemname(herost.equip(i).id), " ", 8)
  END IF
 NEXT i
 
 'erase the tables of equippables
 FOR i as integer = 0 TO 4
  FOR j as integer = 0 TO last_inv_slot()
   st.eq(i).offset(j) = -1
  NEXT j
  st.eq(i).count = 0
 NEXT i
 
 DIM item as ItemDef
 FOR i as integer = 0 TO last_inv_slot()
  IF inventory(i).used THEN
   '--load item data
   loaditemdata item, inventory(i).id
   FOR eq_slot as integer = 0 to 4
    IF item.eqslots(eq_slot) THEN
     '--if this item is equipable in this slot
     IF item_read_equipbit(item, herost.id) THEN
      '--if this item is equipable by this hero
      WITH st.eq(eq_slot)
       .offset(.count) = i
       .count += 1
      END WITH
     END IF
    END IF
   NEXT eq_slot
  END IF
 NEXT i

END SUB

SUB equip_menu_do_equip(byval item as integer, byref st as EquipMenuState)
 IF doequip(item, st.who, st.slot) = NO THEN
  st.item_info = readglobalstring(305, "No room in inventory", 30)
  MenuSound gen(genCancelSFX)
 END IF
 equip_menu_back_to_menu st
END SUB

SUB equip_menu_back_to_menu(byref st as EquipMenuState)
 st.mode = 0
 equip_menu_setup st
END SUB

SUB equip_menu_stat_bonus(byref st as EquipMenuState)
 'Load change in stat bonuses of currently hovered item for display
 '(that is, bonuses of hovered item minus bonuses of equipped item)
 'as well as the resultant total stat bonuses

 DIM item_id as integer = equip_menu_available_item_id(st)

 'Add new item bonuses
 DIM item as ItemDef
 IF item_id = -1 THEN
  '--nothing to load!
  flusharray st.stat_bonus()
 ELSE
  loaditemdata item, item_id
  FOR i as integer = 0 TO statLast
   st.stat_bonus(i) = item.stat_bonuses.sta(i)
  NEXT i
 END IF

 'Subtract old item bonuses
 DIM equipped as integer = gam.hero(st.who).equip(st.slot).id
 IF equipped >= 0 THEN
  loaditemdata item, equipped
  FOR i as integer = 0 TO statLast
   st.stat_bonus(i) -= item.stat_bonuses.sta(i)
  NEXT i
 END IF

 'Calculate new total bonuses
 hero_total_equipment_bonuses st.who, st.stat_total_bonus()
 FOR i as integer = 0 TO statLast
  st.stat_total_bonus(i) += st.stat_bonus(i)
 NEXT i
END SUB

FUNCTION use_item_in_slot(byval slot as integer, byref trigger_box as integer, byref consumed as bool = NO) as bool
 '--slot is the index in your inventory
 
 '--trigger_box is used to communicate when an item has triggered a text box (note, box 0 can't be triggered)

 '--consumed optionally communicates whether the item was actually consumed
 
 '--return value is YES when an item is used, and NO when it is not used.

 '--Does not update autoset tags!

 consumed = NO
 IF inventory(slot).used = NO THEN RETURN NO

 DIM item as ItemDef
 loaditemdata item, inventory(slot).id
 DIM attack_name as string = item.name
 DIM should_consume as integer = item.consumed_by_use
 DIM attack_id as integer = item.oob_attack
 DIM is_attack_item as integer = ( item.oob_attack >= 0 ANDALSO NOT item.teach_spell >= 0 )

 IF use_item_by_id(inventory(slot).id, trigger_box, inventory(slot).text) THEN
  IF should_consume THEN
   IF consumeitem(slot) THEN
    IF is_attack_item ANDALSO inventory(slot).used = NO THEN
     '--used the last attack item (potion) in a consumable stack
     menu_attack_targ_picker attack_id, -1, -1, rpad(attack_name, " ", 8) & CHR(1) & " 0", , NO
     menusound gen(genCancelSFX)
    END IF
    consumed = YES
   END IF
  END IF
  RETURN YES
 END IF 
 RETURN NO
END FUNCTION

FUNCTION use_item_by_id(byval item_id as integer, byref trigger_box as integer, name_override as string="") as bool
 '--item_id is the actual ID number, not offset.

 '--trigger_box communicates the box id if this item triggerd a text box.
 '  this will remain unchanged (zero) if there is no box. The caller is
 '  responsible for loading the box. (FIXME: if it makes sense to
 '  actually load the box from here later, that would be awesome!)
 
 '--name_override is used so the inventory slot text can be used to replace
 '   the item name (which we would want because inventory slot text shows
 '   how many items are in the slot you are currently using)
 '   if name_override is left blank, you will just see the item name

 '--return value is YES if the item use was confirmed by the user
 '  or NO if it was cancelled or otherwise failed.

 '--This sub does not care if you actually own the item in question,
 '   nor will it consume items from your inventory even if the item is a consuming item.

 '--Does not update autoset tags!

 DIM item as ItemDef
 loaditemdata item, item_id

 DIM caption as string
 IF name_override <> "" THEN
  caption = name_override
 ELSE
  caption = item.name
 END IF

 IF item.teach_spell >= 0 THEN '--learn a spell
  MenuSound gen(genAcceptSFX)
  IF menu_attack_targ_picker(-1, item.teach_spell, -1, caption) THEN
   '--successfully learned
   RETURN YES
  END IF
  RETURN NO
 END IF
 
 IF item.oob_attack >= 0 THEN '--attack/oobcure
  MenuSound gen(genAcceptSFX)
  IF menu_attack_targ_picker(item.oob_attack, -1, -1, caption) THEN
   RETURN YES
  END IF
  RETURN NO
 END IF
 
 IF item.text_box >= 0 THEN '--trigger a text box
  trigger_box = item.text_box
  RETURN YES
 END IF
 
 RETURN NO
END FUNCTION

FUNCTION menu_attack_targ_picker(byval attack_id as integer, byval learn_id as integer, byval attacker as integer, use_caption as string, byval x_offset as integer=0, byval really_use_attack as bool=YES) as bool
 'Lets the player pick a target, and then performs an attack or teaches a spell
 'attack_id: attack to be used, -1 otherwise
 'learn_id: attack to be learnt, -1 otherwise
 '(FIXME: should move to separate function! Why not just use pickhero for learning spells??)
 'attacker == -1 when not using an attack from a spell list. In that case, use party's avg stats
 'Returns true if the attack/spell was actually used/learned
 'Doesn't update tags, caller must.
 
 'FIXME: x_offset should probably go away in favor of a slice template at some point in the future
 
 menu_attack_targ_picker = NO

 STATIC targ as integer  '0-3 or not used if spread=YES. Can be -1 but that doesn't indicate spread.
 STATIC spread as bool

 '--Preserve background for display beneath the targ picker menu
 DIM page as integer
 page = compatpage
 DIM holdscreen as integer
 holdscreen = allocatepage
 copypage page, holdscreen

 DIM wtog(3) as integer
 DIM tog as integer
 DIM col as integer
 DIM atk as AttackData
 DIM learn_attack as AttackData
 DIM caption as string
 DIM allow_spread as bool = NO
 DIM must_spread as bool = NO
 
 '--Build the menu from slices
 DIM ps as PlankState 'This menu is not fully plankified, but I did want to be able to call set_plank_state
 ps.m = menu_attack_targ_picker_make_slices(x_offset)
 ps.state_callback = @menu_targpick_plank_state_callback
 DIM holder as Slice Ptr = LookupSlice(SL_PLANK_HOLDER, ps.m)
 
 IF attack_id >= 0 THEN
  loadattackdata atk, attack_id
  allow_spread = (atk.targ_set = 2)
  must_spread = (atk.targ_set = 1)
 END IF
 
 IF learn_id >= 0 THEN
  loadattackdata learn_attack, learn_id
 END IF

 '--make sure the default target is okay
 IF chkOOBtarg(attacker, targ, attack_id) = NO THEN
  targ = -1
  FOR i as integer = 0 TO 3
   IF chkOOBtarg(attacker, i, attack_id) THEN
    targ = i
    EXIT FOR
   END IF
  NEXT i
 END IF

 IF allow_spread = NO THEN spread = NO
 IF must_spread THEN spread = YES
 DIM do_effect as bool = NO

 setkeys
 DO
  setwait speedcontrol
  setkeys
  tog = tog XOR 1

  playtimer
  do_effect = NO
  
  '--handle keys
  IF game_check_cancel_key() THEN
   menusound gen(genCancelSFX)
   EXIT DO
  END IF
  IF spread = NO THEN
   IF carray(ccUp) > 1 THEN
    getOOBtarg -1, attacker, targ, attack_id
    MenuSound gen(genCursorSFX)
   END IF
   IF carray(ccDown) > 1 THEN
    getOOBtarg 1, attacker, targ, attack_id
    MenuSound gen(genCursorSFX)
   END IF
   'FIXME: should use usemenu() with unselectable menu items, so pgup/pgdn/home/end work... and mouse too
  END IF
  
  IF allow_spread THEN
   IF carray(ccLeft) > 1 OR carray(ccRight) > 1 THEN
    MenuSound gen(genCursorSFX)
    spread = NOT spread
   END IF
  END IF

  IF game_check_use_key() THEN
   do_effect = YES
  END IF
  
  IF keyval(scF8) > 1 THEN
   slice_editor ps.m
  END IF
  
  '--handle mouse
  DIM old_targ as integer = targ
  DIM old_spread as bool = spread
  IF get_gen_bool("/mouse/mouse_menus") THEN
   DIM hover as integer = -1
   FOR i as integer = 0 to 3
    DIM hover_sl as Slice Ptr = SliceChildByIndex(holder, i)
    IF SliceCollidePoint(hover_sl, readmouse.pos) THEN hover = i
   NEXT i

   IF hover = -1 ANDALSO ((readmouse.release AND mouseLeft) ORELSE (readmouse.release AND mouseRight)) THEN
    'Clicked outside the box, close the menu
    menusound gen(genCancelSFX)
    EXIT DO
   END IF

   IF spread THEN
    'Spread targetting
    IF (hover >= 0) ANDALSO (readmouse.release AND mouseLeft) THEN
     'Trigger action
     do_effect = YES
    END IF
    IF NOT must_spread ANDALSO (readmouse.release AND mouseRight) THEN
     'Right-click on the box can cancel spread
     spread = NO
    END IF
   ELSE
    'Focused targetting
    IF hover >= 0 THEN
     IF allow_spread THEN
      IF (readmouse.release AND mouseRight) THEN
       'Toggle spread with a right-click in the box
       spread = YES
      END IF
      IF (readmouse.release AND mouseLeft) ANDALSO readmouse.drag_dist > 20 THEN
       'Turn spread on with a left-drag in the box
       spread = YES
      END IF
     END IF
     IF NOT spread THEN
      'Only proceed if we have not just switched to spread mode
      IF hover <> targ ANDALSO (readmouse.release AND mouseLeft) THEN
       'Change focus
       IF chkOOBtarg(attacker, hover, attack_id) THEN
        targ = hover
        spread = NO
       ELSE
        'Play the cancel sound if you tried to select an invalid hero
        menusound gen(genCancelSFX)
       END IF
      ELSEIF hover = targ ANDALSO (readmouse.release AND mouseLeft) THEN
       'Trigger action
       do_effect = YES
      END IF
     END IF
    END IF
   END IF
  END IF
  IF targ <> old_targ ORELSE spread <> old_spread THEN
   MenuSound gen(genCursorSFX)
  END IF
  
  IF do_effect THEN
   'DO ACTUAL EFFECT
   IF targ = -1 THEN
    menusound gen(genCancelSFX)
    EXIT DO
   END IF
   'if can teach a spell
   IF learn_id >= 0 THEN '--teach spell
    IF trylearn(targ, learn_id + 1) THEN
     '--announce learn
     menusound gen(genItemLearnSFX)
     caption = gam.hero(targ).name & " " & readglobalstring(124, "learned", 10) & " " & learn_attack.name
     centerbox 160, 100, small(LEN(caption) * 8 + 16, 320), 24, 1, page
     edgeprint caption, pCenteredLeft, 95, uilook(uiText), page
     IF learn_attack.learn_sound_effect > 0 THEN playsfx learn_attack.learn_sound_effect - 1
     setvispage vpage
     waitforanykey
     menu_attack_targ_picker = YES
    ELSE
     menusound gen(genCantLearnSFX)
    END IF
   END IF
   
   '--do attack outside of battle (cure)
   DIM should_exit as bool = YES
   IF attack_id >= 0 ANDALSO really_use_attack THEN
    DIM was_full as bool = NO
    IF outside_battle_cure(attack_id, targ, attacker, spread, was_full) THEN
     IF was_full THEN should_exit = NO
     'Re-check validity of target
     IF chkOOBtarg(attacker, targ, attack_id) = NO THEN
      getOOBtarg 1, attacker, targ, attack_id
     END IF
     'We will return success, but don't leave menu yet; you might be able to use the attack again
     menu_attack_targ_picker = NOT was_full
    END IF
   END IF
  
   IF should_exit THEN EXIT DO
  END IF '--done using attack

  '--draw the targ picker menu
  copypage holdscreen, page
  FOR i as integer = 0 TO 3
   DIM holder as Slice ptr = LookupSlice(SL_PLANK_HOLDER, ps.m)
   DIM plank as Slice Ptr = SliceChildByIndex(holder, i)
   IF plank <> NULL THEN
    DIM targetted as bool = (targ = i ORELSE (spread ANDALSO chkOOBtarg(attacker, i, attack_id)))
    IF targetted THEN ps.cur = holder  'Dubious
    set_plank_state ps, plank, iif(targetted, plankSEL, plankNORMAL)

    IF gam.hero(i).id >= 0 THEN
     IF targetted THEN
      loopvar wtog(i), 0, max_wtog(plank, dirDown)
     ELSE
      wtog(i) = 0
     END IF
     set_walkabout_frame plank, dirDown, wtog_to_frame(wtog(i))
    END IF

    caption = ""
    IF attack_id >= 0 THEN
     IF atk.targ_stat = statHP or atk.targ_stat = statMP THEN
      caption = gam.hero(i).stat.cur.sta(atk.targ_stat) & "/" & gam.hero(i).stat.max.sta(atk.targ_stat) & " " & statnames(atk.targ_stat)
     ELSEIF atk.targ_stat <= statLast THEN
      caption = gam.hero(i).stat.cur.sta(atk.targ_stat) & " " & statnames(atk.targ_stat)
     ELSE
      'Targets a register, which does nothing, but maybe the attack has some side effect
      caption = gam.hero(i).name
     END IF
    ELSEIF learn_id >= 0 THEN
     caption = gam.hero(i).name
    END IF
    DIM txt as Slice Ptr = LookupSlice(SL_PLANK_MENU_SELECTABLE, plank, slText)
    'It is normal for this to be null of the part isn't full
    IF txt THEN ChangeTextSlice txt, caption
   END IF
  NEXT i
  DIM txt as Slice Ptr = LookupSlice(SL_SINGLE_CAPTION_TEXT, ps.m, slText)
  ChangeTextSlice txt, use_caption
   
  DrawSlice ps.m, page
 
  setvispage vpage
  dowait
 LOOP

 setkeys
 freepage page
 freepage holdscreen
 DeleteSlice @(ps.m)
END FUNCTION

FUNCTION menu_attack_targ_picker_make_slices(x_offset as integer) as Slice Ptr

 DIM tallness as integer = 20
 FOR i as integer = 0 to 3
  IF gam.hero(i).id >= 0 THEN
   DIM spr as Slice Ptr = LookupSlice(SL_WALKABOUT_SPRITE_COMPONENT, gam.hero(i).sl)
   tallness = large(tallness, spr->Height)
  END IF
 NEXT i

 DIM root_sl as Slice Ptr = NewSliceOfType(slSpecial)
 root_sl->Fill = YES
 RefreshSliceScreenPos root_sl

 DIM box as Slice Ptr = NewSliceOfType(slRectangle, root_sl)
 WITH *box
  .x = x_offset 
  .y = 3
  .Width = 160
  .Height = tallness * 4 + 2
  .AnchorHoriz = alignCenter
  .AnchorVert = alignTop
  .AlignHoriz = alignCenter
  .AlignVert = alignTop
  .PaddingLeft = 4
  .PaddingRight = 4
  .PaddingTop = 5
  .PaddingBottom = 0
 END WITH
 ChangeRectangleSlice box, 1
 
 DIM grid as Slice Ptr = NewSliceOfType(slGrid, box, SL_PLANK_HOLDER)
 WITH *grid
  .Fill = YES
 END WITH
 ChangeGridSlice grid, 4, 1
 
 FOR i as integer = 0 TO 3
  '--Plank
  DIM plank as Slice Ptr = NewSliceOfType(slContainer, grid)
  plank->Fill = YES
  '--Rectangle that highlights the selected one
  DIM target_rect as Slice Ptr = NewSliceOfType(slRectangle, plank, SL_PLANK_MENU_SELECTABLE)
  target_rect->Fill = YES
  ChangeRectangleSlice target_rect, , uiHighlight2 * -1 - 1, , borderNone
  IF gam.hero(i).id >= 0 THEN
   '--Copy of the hero's walkabout slices
   DIM walk as Slice Ptr = CloneSliceTree(gam.hero(i).sl)
   SetSliceParent walk, plank
   walk->X = 5
   walk->Y = 0
   walk->Visible = YES
   RealignSlice walk, alignLeft, alignBottom, alignLeft, alignBottom
   DIM spr as Slice Ptr = LookupSlice(SL_WALKABOUT_SPRITE_COMPONENT, walk)
   'Cancel any foot offset copied from the current map
   spr->Y = 0
   '--Text caption
   DIM txt as Slice Ptr = NewSliceOfType(slText, plank, SL_PLANK_MENU_SELECTABLE)
   ChangeTextSlice txt, "placeholder", uiMenuItem * -1 - 1, YES
   RealignSlice txt, alignLeft, alignCenter, alignLeft, alignCenter
   txt->X = 35
   txt->Y = 3
  END IF
 NEXT i

 DIM caption_rect as Slice ptr = NewSliceOfType(slRectangle, box)
 RealignSlice caption_rect, alignCenter, alignBottom, alignCenter, alignTop
 WITH *caption_rect
  .Y = 1
  .Width = LEN("placeholder") * 8 + 32
  .Height = 16
 END WITH
 ChangeRectangleSlice caption_rect, 3, , , , transFuzzy
 DIM caption as Slice ptr = NewSliceOfType(slText, caption_rect, SL_SINGLE_CAPTION_TEXT)
 CenterSlice caption
 ChangeTextSlice caption, "placeholder", uilook(uiText), YES
 
 RETURN root_sl
END FUNCTION

SUB menu_targpick_plank_state_callback (byval sl as Slice Ptr, byval state as PlankItemState)
 'First call the default callback
 set_plank_state_default_callback sl, state
 'Now custom stuff for the menu attack target picker
 SELECT CASE sl->SliceType
  CASE slRectangle:
   'This menu uses a different bgcol for the highlight
   SELECT CASE state
    CASE plankSEL: ChangeRectangleSlice sl, , uiHighlight2 * -1 - 1
   END SELECT
 END SELECT
END SUB

SUB spells_menu_refresh_list(sp as SpellsMenuState)
 IF sp.lists(sp.listnum).magic_type < 0 THEN EXIT SUB

 DIM atk as AttackData

 FOR i as integer = 0 TO 23
  WITH sp.spell(i)
   .name = ""
   .desc = ""
   .cost = ""
   .id = -1
   .can_use = NO
   .targt = 0
   .tstat = 0
   IF gam.hero(sp.hero).spells(sp.lists(sp.listnum).menu_index, i) > 0 THEN
    .id = gam.hero(sp.hero).spells(sp.lists(sp.listnum).menu_index, i) - 1
    loadattackdata atk, .id
    IF atk.useable_outside_battle THEN
     .can_use = atkallowed(atk, sp.hero, sp.lists(sp.listnum).magic_type, i \ spellsPerLMP)
     .targt = atk.targ_set
     .tstat = atk.targ_stat
    END IF

    .name = atk.name
    .desc = atk.description
    .cost = hero_attack_cost_info(atk, sp.hero, sp.lists(sp.listnum).magic_type, i \ spellsPerLMP)
   END IF
   .name = rpad(.name, " ", 10)
  END WITH
 NEXT i
END SUB

SUB spells_menu_refresh_hero(sp as SpellsMenuState)
 DIM her as HeroDef
 loadherodata her, gam.hero(sp.hero).id
 
 '--first blank out lists
 FOR i as integer = 0 TO UBOUND(sp.lists)
  WITH sp.lists(i)
   .magic_type = -1
   .menu_index = -1
   .name = ""
  END WITH
 NEXT i

 DIM lists_added as integer  'bitvector
 DIM slot as integer = 0
 DIM listnum as integer
 DIM hero_node as NodePtr = her.reld
 READNODE hero_node."battle_menus" as batmenus
  WITHNODE batmenus."menu" as m
   DIM k as NodePtr
   k = m."kind".ptr
   IF k."spells".exists THEN
    listnum = k."spells".integer
    IF listnum < 0 ORELSE listnum > UBOUND(her.list_type) THEN
     showerror "Invalid spell list " & listnum
     FreeNode m
     EXIT READNODE
    END IF
    'Only add each spell list once
    DIM list_bit as integer = 1 SHL listnum
    IF lists_added AND list_bit THEN CONTINUE READNODE

    WITH sp.lists(slot)
     .menu_index = listnum
     .magic_type = her.list_type(.menu_index)
     IF .magic_type = 0 OR .magic_type = 1 THEN
      'Only display MP-based and LMP-based spell lists. Ignore Random lists.
      IF readbit(her.bits(), 0, 26) = NO ORELSE count_available_spells(sp.hero, .menu_index) > 0 THEN
       .name = her.list_name(.menu_index)
       IF .name <> "" THEN
        'Only show lists with non-blank names
        .name = rpad(.name, " ", 10)
        slot += 1
        lists_added OR= list_bit
       END IF
      END IF
     END IF
    END WITH 
   END IF
  END WITHNODE
 END READNODE

 sp.last = slot
 
 WITH sp.lists(sp.last)
  .name = rpad(readglobalstring(46, "EXIT", 10), " ", 10)
  .menu_index = -1
  .magic_type = -1
 END WITH
 IF sp.listnum > sp.last THEN sp.listnum = sp.last

 spells_menu_refresh_list sp
END SUB

FUNCTION spells_menu_control_hover() as integer
 'Returns which spell slot the mouse is over or 24 for "cancel" or -1 for none
 FOR i as integer = 0 to 23
  DIM x as integer = (i MOD 3)
  DIM y as integer = (i \ 3)
  IF rect_collide_point(XYWH(12 + x * 104, 90 + y * 8, 96, 8), readmouse.pos) THEN RETURN i
 NEXT i
 IF rect_collide_point(XYWH(9, 171, 96, 8), readmouse.pos) THEN RETURN 24 'The cancel button
 RETURN -1
END FUNCTION

SUB spells_menu_control_which_list(sp as SpellsMenuState)
 IF game_check_cancel_key() THEN sp.quit = YES : EXIT SUB
 IF carray(ccLeft) > 1 THEN
  DO
   loopvar sp.hero, 0, 3, -1
  LOOP UNTIL gam.hero(sp.hero).id >= 0
  menusound gen(genCursorSFX)
  spells_menu_refresh_hero sp
 END IF
 IF carray(ccRight) > 1 THEN
  DO
   loopvar sp.hero, 0, 3
  LOOP UNTIL gam.hero(sp.hero).id >= 0
  menusound gen(genCursorSFX)
  spells_menu_refresh_hero sp
 END IF
 usemenusounds
 IF usemenu (sp.listnum, 0, 0, sp.last, 5) THEN
  spells_menu_refresh_list sp
 END IF
 DIM do_pick as bool = NO
 IF game_check_use_key() THEN do_pick = YES

 IF get_gen_bool("/mouse/mouse_menus") THEN
  DIM hover as integer = -1
  DIM old_listnum as integer = sp.listnum
  IF rect_collide_point(XYWH(6, 86, 308, 96), readmouse.pos) THEN
   'Mouse click over the spell area switches to that mode
   IF (readmouse.release AND mouseLeft) ORELSE (readmouse.release AND mouseRight) THEN
    sp.mset = 1
    sp.cursor = spells_menu_control_hover()
    IF sp.cursor = -1 THEN sp.cursor = 0
    EXIT SUB
   END IF
  ELSEIF rect_collide_point(XYWH(14, 20, 84, 60), readmouse.pos) THEN
   'Mouse over list of spell lists
   FOR i as integer = 0 TO sp.last
    IF rect_collide_point(XYWH(16, 25 + i * 10, 16 * 8, 10), readmouse.pos) THEN hover = i
   NEXT i
   IF (readmouse.release AND mouseLeft) ANDALSO hover >= 0 THEN
    IF sp.listnum = hover ORELSE hover = sp.last THEN do_pick = YES
   END IF
   IF (readmouse.buttons AND mouseLeft) ANDALSO hover >= 0 THEN
    sp.listnum = hover
   END IF
  ELSEIF rect_collide_point(XYWH(106, 28, 200, 17), readmouse.pos) THEN
   IF (readmouse.release AND mouseLeft) THEN
    'Clicked on hero name box
    DO
     loopvar sp.hero, 0, 3
    LOOP UNTIL gam.hero(sp.hero).id >= 0
    menusound gen(genCursorSFX)
    spells_menu_refresh_hero sp
   END IF
  ELSE
   'Mouse anywhere else
   IF (readmouse.release AND mouseLeft) THEN
    sp.quit = YES
    EXIT SUB
   END IF
  END IF
  IF (readmouse.release AND mouseRight) ANDALSO readmouse.drag_dist < 10 THEN
   IF hover >= 0 THEN
    'right-click select on the list of spell lists
    sp.listnum = hover
   ELSE
    'Right-click anywhere else exits
    sp.quit = YES
    EXIT SUB
   END IF
  END IF
  IF old_listnum <> sp.listnum THEN menusound gen(genCursorSFX)
 END IF

 IF do_pick THEN
  IF sp.lists(sp.listnum).menu_index = -1 THEN sp.quit = YES : EXIT SUB
  menusound gen(genAcceptSFX)
  sp.mset = 1
  sp.cursor = 0
 END IF
END SUB

SUB spells_menu_control_pick_spell(sp as SpellsMenuState)
 DIM do_pick as bool = NO

 'Keyboard controls
 IF sp.re_use = NO THEN
  IF game_check_cancel_key() THEN
   sp.mset = 0
   menusound gen(genCancelSFX)
  END IF
  IF carray(ccUp) > 1 THEN
   sp.cursor = sp.cursor - 3
   menusound gen(genCursorSFX)
   IF sp.cursor < 0 THEN sp.cursor = 24
  END IF
  IF carray(ccDown) > 1 THEN
   IF sp.cursor < 24 THEN
    sp.cursor = small(sp.cursor + 3, 24)
   ELSE
    sp.cursor = 0
   END IF
   menusound gen(genCursorSFX)
  END IF
  IF sp.cursor < 24 THEN   'EXIT not selected
   IF carray(ccLeft) > 1 THEN
    IF sp.cursor MOD 3 THEN
     sp.cursor = sp.cursor - 1
    ELSE
     sp.cursor = sp.cursor + 2
    END IF
    menusound gen(genCursorSFX)
   END IF
   IF carray(ccRight) > 1 THEN
    IF sp.cursor MOD 3 = 2 THEN sp.cursor = sp.cursor - 2 ELSE sp.cursor += 1
    menusound gen(genCursorSFX)
   END IF
   IF keyval(scPageUp) > 1 THEN
    sp.cursor = sp.cursor MOD 3
    menusound gen(genCursorSFX)
   END IF
   IF keyval(scPageDown) > 1 THEN
    sp.cursor = (sp.cursor MOD 3) + 21
    menusound gen(genCursorSFX)
   END IF
  END IF
 END IF
 IF game_check_use_key() OR sp.re_use THEN
  do_pick = YES
 END IF

 IF get_gen_bool("/mouse/mouse_menus") ANDALSO NOT sp.re_use THEN
  DIM hover as integer = spells_menu_control_hover()
  DIM old_cursor as integer = sp.cursor
  IF rect_collide_point(XYWH(6, 86, 308, 96), readmouse.pos) THEN
   ' Mouse is over the spell area
   IF hover >= 0 THEN
    IF (readmouse.release AND mouseLeft) ANDALSO (sp.cursor = hover ORELSE hover = 24) THEN do_pick = YES
    IF (readmouse.release AND mouseLeft) THEN sp.cursor = hover
   END IF
  ELSEIF rect_collide_point(XYWH(14, 20, 84, 60), readmouse.pos) THEN
   'Mouse over list of spell lists
   IF (readmouse.release AND mouseLeft) THEN
    'Click back into the spell list picker
    sp.mset = 0
    EXIT SUB
   END IF
  ELSEIF rect_collide_point(XYWH(106, 28, 200, 17), readmouse.pos) THEN
   IF (readmouse.release AND mouseLeft) THEN
    'Clicked on hero name box
    sp.mset = 0
    DO
     loopvar sp.hero, 0, 3
    LOOP UNTIL gam.hero(sp.hero).id >= 0
    menusound gen(genCursorSFX)
    spells_menu_refresh_hero sp
   END IF
  ELSE
   'Mouse anywhere else
   IF (readmouse.release AND mouseLeft) THEN
    sp.quit = YES
    EXIT SUB
   END IF
  END IF
  IF (readmouse.release AND mouseRight) ANDALSO readmouse.drag_dist < 10 THEN
   IF hover >= 0 THEN
    'Right click select
    sp.cursor = hover
   ELSE
    'Right anywhere else goes back
    sp.mset = 0
   END IF
  END IF
  IF old_cursor <> sp.cursor THEN menusound gen(genCursorSFX)
 END IF
 
 IF do_pick THEN
  sp.re_use = NO
  IF sp.cursor = 24 THEN sp.mset = 0
  IF sp.spell(sp.cursor).can_use THEN
   '--spell that can be used oob
   DIM lmplev as integer = sp.cursor \ spellsPerLMP
   
   DIM atk as AttackData
   loadattackdata atk, sp.spell(sp.cursor).id
   '--NOTE: atkallowed isn't needed here because the check is done elswehere...
   '--still, it would be nice if we could use it anyway...
   'IF atkallowed(atk, sp.hero, sp.lists(sp.listnum).magic_type, lmplev)
   
   '--repaint the screen so it will show up under the menu attack targ picker
   spells_menu_paint sp
   IF menu_attack_targ_picker(sp.spell(sp.cursor).id, -1, sp.hero, TRIM(sp.spell(sp.cursor).name), 36) THEN
    '--attack was actually used
    subtract_attack_costs atk, sp.hero, sp.lists(sp.listnum).magic_type, lmplev
    evalitemtags
    evalherotags
    tag_updates

    spells_menu_refresh_hero sp
    sp.re_use = YES
   ELSE
    menusound gen(genCancelSFX)
   END IF
  ELSE
   menusound gen(genCancelSFX)
  END IF
 END IF
END SUB

SUB spells_menu_control(sp as SpellsMenuState)
 IF sp.mset = 0 THEN '--picking which spell list
  spells_menu_control_which_list sp
 ELSE '--picking a specific spell from a list
  spells_menu_control_pick_spell sp
 END IF
END SUB

SUB old_spells_menu (byval who as integer)

 DIM sp as SpellsMenuState
 sp.hero = who
 sp.listnum = 0

 sp.cancel_menu_caption = readglobalstring(51, "(CANCEL)", 10)
 sp.has_none_caption = readglobalstring(133, "has no spells", 20)

 spells_menu_refresh_hero sp
 '--Preserve background for display beneath the spells menu
 sp.page = compatpage
 DIM holdscreen as integer = allocatepage
 copypage sp.page, holdscreen

 menusound gen(genAcceptSFX)
 setkeys
 DO
  setwait speedcontrol
  setkeys
  sp.tog = sp.tog XOR 1
  playtimer
  spells_menu_control sp
  IF sp.quit THEN EXIT DO
  copypage holdscreen, sp.page
  spells_menu_paint sp
  setvispage vpage
  IF sp.re_use = NO THEN
   dowait
  END IF
 LOOP

 menusound gen(genCancelSFX)
 setkeys
 freepage sp.page
 freepage holdscreen

 checkfatal = YES
END SUB

SUB spells_menu_paint (byref sp as SpellsMenuState)
 centerfuz 160, 100, 312, 184, 1, sp.page 'outer box
 centerbox 206, 36, 200, 17, 2, sp.page   'name box
 centerbox 56, 50, 84, 60, 2, sp.page     'spell lists menu box
 centerbox 160, 134, 308, 96, 2, sp.page  'spell list
 rectangle 6, 168, 308, 1, boxlook(1).edgecol, sp.page 'divider 2
 'top menu (spell lists)
 FOR i as integer = 0 TO sp.last
  textcolor uilook(uiMenuItem), 0
  IF sp.listnum = i THEN textcolor uilook(uiSelectedItem + sp.tog), uilook(uiHighlight2): IF sp.mset = 1 THEN textcolor uilook(uiMenuItem), uilook(uiHighlight2)
  printstr sp.lists(i).name, 16, 25 + i * 10, sp.page 'spell menu
 NEXT i

 'bottom menu (spells in spell list)
 IF sp.lists(sp.listnum).menu_index >= 0 THEN
  FOR o as integer = 0 TO 23
   IF sp.cursor = o AND sp.mset = 1 THEN
    IF sp.spell(o).can_use THEN 
     textcolor uilook(uiSelectedItem + sp.tog), uilook(uiHighlight) 
    ELSE 
     textcolor uilook(uiMenuItem), uilook(uiHighlight)
    END IF
   ELSE
    IF sp.spell(o).can_use THEN
     textcolor uilook(uiMenuItem), 0
    ELSE
     textcolor uilook(uiDisabledItem), 0
    END IF
   END IF
   printstr sp.spell(o).name, 12 + (o MOD 3) * 104, 90 + (o \ 3) * 8, sp.page 'spells
  NEXT o
  IF sp.mset = 1 THEN
   textcolor uilook(uiMenuItem), 0
   IF sp.cursor = 24 AND sp.mset = 1 THEN textcolor uilook(uiSelectedItem + sp.tog), uilook(uiHighlight)
   printstr sp.cancel_menu_caption, 9, 171, sp.page 'cancel
   IF sp.spell(sp.cursor).desc <> "" THEN
    rectangle 6, 155, 308, 1, boxlook(2).edgecol, sp.page  'description divider
   END IF
   textcolor uilook(uiDescription), 0
   DIM cost_caption as string = RIGHT(sp.spell(sp.cursor).cost, 30)
   printstr cost_caption, 311 - LEN(cost_caption) * 8, 171, sp.page 'cost
   printstr sp.spell(sp.cursor).desc, 9, 158, sp.page 'description
  END IF
 END IF

 IF sp.last = 0 THEN edgeprint gam.hero(sp.hero).name & " " & sp.has_none_caption, pCentered, 120, uilook(uiText), sp.page
 edgeprint gam.hero(sp.hero).name, ancCenter + 206, 31, uilook(uiText), sp.page
END SUB

'-----------------------------------------------------------------------

SUB get_virtual_keyboard_buttons (byval sl as Slice Ptr, byref arr as any ptr vector)
 IF sl = 0 THEN debug "get_virtual_keyboard_buttons null ptr": EXIT SUB
 DIM ch as Slice Ptr = sl->FirstChild
 DO WHILE ch
  IF ch->Lookup = SL_VIRTUAL_KEYBOARD_BUTTON THEN
   v_append arr, ch
  ELSEIF ch->Lookup = SL_VIRTUAL_KEYBOARD_ENTER THEN
   v_append arr, ch
  ELSEIF ch->Lookup = SL_VIRTUAL_KEYBOARD_DEL THEN
   v_append arr, ch
  ELSEIF ch->Lookup = SL_VIRTUAL_KEYBOARD_SHIFT THEN
   v_append arr, ch
  ELSEIF ch->Lookup = SL_VIRTUAL_KEYBOARD_SYMBOLS THEN
   v_append arr, ch
  ELSEIF ch->Lookup = SL_VIRTUAL_KEYBOARD_SELECT THEN
   'If this is the select, only explore the active child
   get_virtual_keyboard_buttons SliceChildByIndex(ch, ch->SelectData->index), arr
  ELSE
   IF ch->NumChildren > 0 THEN
    get_virtual_keyboard_buttons ch, arr
   END IF
  END IF
  ch = ch->NextSibling
 LOOP
END SUB

SUB virtual_keyboard_refresh_array (byref st as VirtualKeyboardScreenState)
 v_new st.arr
 get_virtual_keyboard_buttons st.sl, st.arr
END SUB

SUB virtual_keyboard_button_defocus (byval button as Slice Ptr)
 IF button THEN
  IF button->Lookup = SL_VIRTUAL_KEYBOARD_ENTER THEN
   ChangeRectangleSlice button, , uiLook(uiHighlight2)
  ELSEIF button->Lookup = SL_VIRTUAL_KEYBOARD_DEL THEN
   ChangeRectangleSlice button, , uiLook(uiSelectedDisabled)
  ELSEIF button->Lookup = SL_VIRTUAL_KEYBOARD_SHIFT THEN
   ChangeRectangleSlice button, , uiLook(uiSelectedDisabled)
  ELSEIF button->Lookup = SL_VIRTUAL_KEYBOARD_SYMBOLS THEN
   ChangeRectangleSlice button, , uiLook(uiSelectedDisabled)
  ELSE
   ChangeRectangleSlice button, , uiLook(uiMenuItem)
  END IF
 END IF
END SUB

SUB virtual_keyboard_button_focus (byval button as Slice Ptr)
 IF button THEN
  IF button->Lookup = SL_VIRTUAL_KEYBOARD_ENTER THEN
   ChangeRectangleSlice button, , uiLook(uiHighlight)
  ELSEIF button->Lookup = SL_VIRTUAL_KEYBOARD_DEL THEN
   ChangeRectangleSlice button, , uiLook(uiDisabledItem)
  ELSEIF button->Lookup = SL_VIRTUAL_KEYBOARD_SHIFT THEN
   ChangeRectangleSlice button, , uiLook(uiDisabledItem)
  ELSEIF button->Lookup = SL_VIRTUAL_KEYBOARD_SYMBOLS THEN
   ChangeRectangleSlice button, , uiLook(uiDisabledItem)
  ELSE
   ChangeRectangleSlice button, , uiLook(uiDisabledItem)
  END IF
 END IF
END SUB

FUNCTION virtual_keyboard_button_at_pos (byref st as VirtualKeyboardScreenState, byval pos as XYPair) as Slice Ptr
 DIM button as Slice Ptr
 FOR i as integer = 0 TO v_len(st.arr) - 1
  button = st.arr[i]
  IF SliceCollidePoint(button, pos) THEN
   RETURN button
  END IF
 NEXT i
 RETURN 0
END FUNCTION

SUB virtual_keyboard_push_button (byref st as VirtualKeyboardScreenState, byval button as Slice Ptr)
 IF button = 0 THEN EXIT SUB
 menusound gen(genCursorSFX)
 SELECT CASE button->Lookup
  CASE SL_VIRTUAL_KEYBOARD_BUTTON:
   IF st.max_length = -1 ORELSE LEN(st.result) < st.max_length THEN
    DIM txt as Slice Ptr
    txt = LookupSlice(SL_VIRTUAL_KEYBOARD_BUTTONTEXT, button)
    IF txt THEN
     DIM dat as TextSliceData Ptr = txt->SliceData
     st.result &= dat->text
    END IF
   END IF
  CASE SL_VIRTUAL_KEYBOARD_DEL:
   IF LEN(st.result) > 0 THEN st.result = LEFT(st.result, LEN(st.result) - 1)
  CASE SL_VIRTUAL_KEYBOARD_ENTER:
   st.done = YES
  CASE SL_VIRTUAL_KEYBOARD_SHIFT:
   IF st.select_sl THEN
    st.shift = NOT st.shift
    IF st.shift THEN st.symbols = NO
    IF st.shift THEN
     ChangeSelectSlice st.select_sl, 1
    ELSE
     ChangeSelectSlice st.select_sl, 0
    END IF
    virtual_keyboard_refresh_array st
   END IF
  CASE SL_VIRTUAL_KEYBOARD_SYMBOLS:
   IF st.select_sl THEN
    st.symbols = NOT st.symbols
    IF st.symbols THEN st.shift = NO
    IF st.symbols THEN
     ChangeSelectSlice st.select_sl, 2
    ELSE
     ChangeSelectSlice st.select_sl, 0
    END IF
    virtual_keyboard_refresh_array st
   END IF
 END SELECT
END SUB

SUB virtual_keyboard_display_refresh (byref st as VirtualKeyboardScreenState)
 IF st.entry_sl THEN ChangeTextSlice st.entry_sl, st.result

 DIM button as Slice Ptr
 FOR i as integer = 0 TO v_len(st.arr) - 1
  button = st.arr[i]
  IF st.pushed_sl <> 0 ANDALSO st.pushed_sl = button THEN
   virtual_keyboard_button_focus button
  ELSE
   virtual_keyboard_button_defocus button
  END IF
 NEXT i

END SUB

FUNCTION touch_virtual_keyboard (default_str as string, max_length as integer=-1, prompt as string="") as string

 DIM st as VirtualKeyboardScreenState
 st.result = default_str
 st.max_length = max_length
 st.prompt = prompt

 st.sl = LoadSliceCollection(SL_COLLECT_VIRTUALKEYBOARDSCREEN)
 ERROR_IF(st.sl = NULL, "Couldn't load slices!", st.result)

 expand_slice_text_insert_codes st.sl, @ExpandTextVirtualKeyboard, @st
 virtual_keyboard_refresh_array st

 st.entry_sl = LookupSlice(SL_VIRTUAL_KEYBOARD_ENTRYTEXT, st.sl)
 IF st.entry_sl = 0 THEN
  visible_debug "touch_virtual_keyboard: no text slice has SL_VIRTUAL_KEYBOARD_ENTRYTEXT lookup code"
 END IF
 st.select_sl = LookupSlice(SL_VIRTUAL_KEYBOARD_SELECT, st.sl)

 menusound gen(genAcceptSFX)
 setkeys
 DO
  setwait speedcontrol
  setkeys

  IF readmouse.buttons AND mouseLeft THEN
   IF st.pushed_sl = 0 THEN
    IF NOT st.drag_off THEN
     st.pushed_sl = virtual_keyboard_button_at_pos(st, readmouse.pos)
    END IF
   ELSE
    IF st.pushed_sl <> 0 ANDALSO st.pushed_sl <> virtual_keyboard_button_at_pos(st, readmouse.pos) THEN
     st.drag_off = YES
     st.pushed_sl = 0
    END IF
   END IF
  ELSE
   st.drag_off = NO
   IF st.pushed_sl <> 0 THEN
    virtual_keyboard_push_button st, st.pushed_sl
    IF st.done THEN EXIT DO
   END IF
   st.pushed_sl = 0
  END IF

  'IF game_check_cancel_key() THEN EXIT DO
  'IF game_check_use_key() THEN
  'END IF
  IF keyval(scF8) > 1 THEN
   slice_editor st.sl
  END IF

  virtual_keyboard_display_refresh st
 
  'Draw the screen
  clearpage vpage
  DrawSlice st.sl, vpage
  setvispage vpage
  dowait
 LOOP
 menusound gen(genAcceptSFX)

 v_free st.arr
 DeleteSlice @(st.sl)
 
 RETURN st.result
END FUNCTION

FUNCTION game_check_use_key() as bool
 IF prefbit(58) THEN
  'no key repeats
  RETURN (carray(ccUse) AND 4) > 0
 ELSE
  'allow key repeats
  RETURN carray(ccUse) > 1
 END IF
END FUNCTION

FUNCTION game_battle_check_use_key() as bool
 IF prefbit(58) ANDALSO NOT prefbit(59) THEN
  'no key repeats
  RETURN (carray(ccUse) AND 4) > 0
 ELSE
  'allow key repeats
  RETURN carray(ccUse) > 1
 END IF
END FUNCTION

FUNCTION game_check_cancel_key() as bool
 IF prefbit(58) THEN
  'no key repeats
  RETURN (carray(ccCancel) AND 4) > 0
 ELSE
  'allow key repeats
  RETURN carray(ccCancel) > 1
 END IF
END FUNCTION

FUNCTION game_check_menu_key() as bool
 IF prefbit(58) THEN
  'no key repeats
  RETURN (carray(ccMenu) AND 4) > 0
 ELSE
  'allow key repeats
  RETURN carray(ccMenu) > 1
 END IF
END FUNCTION

