########################################################################
# Many of the tests in this script might seem silly. That is okay.
#
# See interactivetest.hss for tests which require user input.
#
# Any time you are preparing to fix an engine bug, consider adding a
# test that demonstrates the bug FIRST before you fix the bug.

include, hstests.hss
include, autotest.hsi
include, "autotest-plotstr.hsi"

# Save slots used:
# 1 for save/load tests
# 2 for save/load tests
# 3 for export globals tests
# 4 test expanding strings from save slots
# 5 empty, for testing failure of expanding strings from empty save slots
# 31 save slot: persistent
# 400 used for testing higher number save slots


########################################################################

define constant(1, default wait)
define constant(10, timer:checkpoint)
define constant(30, timer:set args test)

global variable(100, menu item script global)
global variable(101, timer global)
global variable(102, timer global will become)
global variable(103, battle script sequence)
global variable(104, menu close global)
global variable(105, embedding global)
global variable(106, export global 1)
global variable(107, export global 2)
global variable(108, expecting death)
global variable(109, died)
global variable(110, map autorun triggered)
global variable(111, eachstep triggered)
global variable(112, textbox script triggered)
global variable(113, npc script triggered)
global variable(114, testing script triggers)
global variable(115, savegame map)
global variable(116, savegame slice)
global variable(117, savegame npc)
global variable(118, called resetgame)
global variable(119, hook loadgame)
global variable(120, timer checkpoint interval)
global variable(121, saveglobal)
global variable(122, observed hero count)
global variable(123, move hero count)
global variable(124, timer expect arg 1)
global variable(125, timer expect arg 2)
global variable(126, autorun counter)
global variable(127, start tick)   # Often needed by different scripts, initialise to ticknumber
global variable(128, textbox20 instead triggered)
global variable(129, timer script triggered)
global variable(130, move hero script triggered)
global variable(50000, the global with the biggest id)

# 200-500 used in hstests.hss
# 1000-1063 used in zonetests
define constant(1000, array:zones)
# 5000-6000 used in hstests.hss

#string 0 = error messages
#string 1-9 = temporary misc
#string 10 = error message arg
#string 11 = asserts
#string 95-99 = temp misc
define constant(10, err arg string)
define constant(11, assert expression string)

# see notes at the top of this file for what save slots get used
define constant(31, save slot: persistent)
define constant(400, save slot: high)

########################################################################

# Set on starting map 0
plotscript, mapautorun, begin
  # Test that the map autorun script on the starting map happens before
  # the newgame script
  map autorun triggered += 1
end

# TODO: What would be nice is to reset the game between each test suite,
# making use of the ability to pass args to a new/load game script,
# so that we don't have to worry about things interfering with each other
plotscript, new game, arg1, arg2, arg3, begin
  trace($1="newgame script")
  tracevalue(arg1, arg2, arg3)
  suspend player
  seed random(4444)

  # This stuff is for "test reset game"
  import globals(save slot: persistent, @called resetgame, @called resetgame)
  if (called resetgame || arg1) then (
    after resetgame(arg1, arg2, arg3)
    run tests 2   # continue tests
  )
  else (
    run tests 1   # start tests
  )
end

plotscript, load game, slot, arg1, arg2, arg3, begin
  trace($1="loadgame script")
  tracevalue(slot, arg1, arg2, arg3, hook loadgame, start tick, ticknumber)
  suspend player
  seed random(4444)

  if (hook loadgame) then (
    after loadgame(arg1, arg2, arg3)
    run tests 3   # continue tests
  )
  else (
    # This means we weren't expecting this script to be called
    crash
  )
end

# testing starts here
script, run tests 1, begin
  assert(map autorun triggered == 1)
  start tick counter
  interpreter tests
  extra array tests
  global npc tests
  script trigger interference tests
  plotstr tests
  string load tests
  platform tests
  save slot tests
  slice tests
  master palette tests
  maptile tests
  wallchecking tests
  zone tests
  map state tests
  inventory tests
  items tests
  hero tests
  death tests
  harm tile tests
  door tests
  map autorun tests
  npc tests
  menu tests
  textbox tests
  shop tests
  attack tests
  enemy tests
  battle tests
  timer tests
  pathfinding tests
  vehicle tests
  obsolete map data tests
  run game tests
  reset game tests   # Resets the game
  # Shouldn't get here
  crash
end

script, start checkpoint timer, interval, begin
  timer checkpoint interval := interval
  do start checkpoint interval
end

script, do start checkpoint interval , begin
  _checkpoint
  set timer(timer:checkpoint, 0, timer checkpoint interval, @do start checkpoint interval)
end

script, stop checkpoint timer, begin
  stop timer(timer:checkpoint)
end

# follows "reset game tests"
script, run tests 2, begin
  savegame tests  # Saves and loads the game
  # Shouldn't get here
  crash
end

# follows "savegame tests"
script, run tests 3, begin
  # more tests here, potentially
  passed tests
end

script, passed tests, begin
  $0="TESTS SUCCEEDED"
  trace(0)
  trace value(ticknumber)
  gameover
end


########################################################################

script, w, ticks=default wait, begin
  wait(ticks)
  _checkpoint
end

# Returns a string of the script call stack in given string id
script, callstack, string, begin
  variable(depth, id)
  depth := 1
  $ string = ""
  while (true) do (
    id := getcallingscriptid(depth)
    if (id == 0) then (break)
    if (depth == 1) then (
      getscriptname(string, id)
    ) else (
      getscriptname(95, id)
      stringsprintf(string, $96="%s -> %s", 95, string)
    )
    depth += 1
  )
  return(string)
end

script, crash, begin
  if(string length(10) >> 0) then(
    $0+"("
    concatenate strings(0, err arg string)
    $0+")"
  )
  show text box(1)
  $1="crash() called from "
  callstack(2)
  1 $+ 2
  trace(1)
  $1="in testsuite: "
  1 $+ 0
  trace(1)
  w(25)
  # script error(0)
  game over
end

script, assert failure, begin
  $1="ASSERT FAILURE:"
  trace(1)
  trace(assert expression string)
  crash
end

########################################################################

script, start tick counter, tick = 0, begin
  tick counter slice := create container
  set slice x(tick counter slice, tick)
  set slice velocity x(tick counter slice, 1, 100000)
end

script, ticknumber, begin
  return(slice x(tick counter slice))
end

########################################################################

script, blocker script, begin
#  assert(ticker 0 == expected ticker 0)
  ticker 1 += 1
  wait
  ticker 1 += 1
end

script, blocker script 2, begin
  assert(ticker 1 == expected ticker 1)
  ticker 2 += 1
  wait
  ticker 2 += 1
  assert(ticker 1 == expected ticker 1)
end

script, eachtick script, begin
  ticker 0 += 1
  assert(ticknumber == ticker 0 + start tick)
  set timer(3, 0, 1, @eachtick script)
end

# Test plotscripts blocking each other from running
script, plotscript blocking tests, begin
  $0="plotscript blocking tests"

  start tick := ticknumber
  ticker 0 := 0
  ticker 1 := 0  # globals
  ticker 2 := 0

  # Start an each-tick script on a timer that should not interfere with anything else
  # (since it has a higher timer ID)
  set timer(3, 0, 1, @eachtick script)

  wait(2)
  assert(ticknumber == start tick + 2)

  # Trigger another script and check that this one is blocked
  # (This is also a regression test for bug 430, because of eachtick script)
  expected ticker 0 := 0
  set timer(1, 0, 1, @blocker script)
  assert(ticknumber == start tick + 2)
  # This script will continue running until the next tick
  assert(ticker 1 == 0)
  wait(1)
  assert(ticker 1 == 2)
  assert(ticknumber == start tick + 4)

  # Trigger two scripts to run at once; one blocks the other (additional test for bug 430)
  expected ticker 1 := 2
  set timer(1, 0, 1, @blocker script)
  set timer(2, 0, 1, @blocker script 2)  # Runs first
  assert(ticknumber == start tick + 4)
  wait
  assert(ticknumber == start tick + 7)  # one tick wait each plus the wait above
  assert(ticker 1 == 4)
  assert(ticker 2 == 2)

  # Check eachtick ran every tick
  #trace value(ticknumber, start tick, ticker 0, ticker 1, ticker 2)
  assert(ticknumber == start tick + ticker 0)
  stop timer(3)
end

########################################################################

script, bug 430 tester, begin
  variable(tick)
  tick := ticknumber
  wait  # should be skipped
  if (ticknumber == tick) then (
    # was skipped
    ticker 1 += 1
  )
end

# "plotscript blocking tests" is completely broken if bug 430 is enabled, so here is a separate test.
# Returns whether bug 430 is enabled
script, check bug 430 enabled, begin
  $0="check bug 430 enabled"
  start tick := ticknumber
  ticker 1 := 0  # globals
  ticker 2 := 0

  # Trigger two scripts to run at once; one blocks the other (test for bug 430)
  expected ticker 1 := 0
  set timer(1, 0, 1, @bug 430 tester)
  set timer(2, 0, 1, @blocker script 2)  # Runs first
  wait
  assert(ticker 2 == 2)
  if(ticknumber == start tick + 2 && ticker 1 == 1) then (
    # wait in blocker script 2 plus the wait above
    return (true)
  ) else if(ticknumber == start tick + 3 && ticker 1 == 0) then (
    return (false)
  ) else (
    # Other combinations should be impossible
    crash
  )
end

script, bug 430 emulation tests, begin
  assert(check bug 430 enabled == false)
  variable(oldval)
  oldval := read general(178)  # genBits2 + 1
  write general(178, oldval, or, (2 ^ 1)) # Enable bug 430 (prefbit 33)
  assert(check bug 430 enabled == true)
  write general(178, oldval)
end

########################################################################

script, inventory tests, begin
  $0="Inventory tests"
  # This tests only the inventory and commands to manipulate it.
  # Far more item tests including some equipment/inventory stuff is in 'equipment tests'

  # Precondition: empty inventory

  assert(get inventory size == 30)
  variable(slot, lastslot)
  for (slot, 0, get inventory size -- 1) do (
    assert(item in slot(slot) == -1)
    assert(item count in slot(slot) == 0)
  )

  # Simple stuff
  get item(item:Boots, 3)
  assert(inventory(item:Boots) == 3)
  assert(item in slot(0) == item:Boots)
  assert(item count in slot(0) == 3)
  get item(item:Boots, 99)
  assert(inventory(item:Boots) == 102)
  delete item(item:Boots)
  assert(inventory(item:Boots) == 101)
  delete item(item:Boots, 101)
  assert(inventory(item:Boots) == 0)

  # test item slot commands

  assert(item in slot(0) == -1)
  assert(item count in slot(0) == 0)
  assert(item in slot(1) == -1)
  assert(item count in slot(1) == 0)

  set item in slot(1, item:Boots)  # used on empty slot, adds one item
  assert(item in slot(1) == item:Boots)
  assert(item count in slot(1) == 1)
  assert(inventory(item:Boots) == 1)
  lastslot := get inventory size -- 1
  set item in slot(lastslot, item:Boots)
  assert(item in slot(lastslot) == item:Boots)
  set item count in slot(lastslot, 99)
  assert(inventory(item:Boots) == 100)

  # check deleteitem deletes in right order
  delete item(item:Boots, 5)
  assert(inventory(item:Boots) == 95)
  assert(item count in slot(1) == 0)
  assert(item count in slot(lastslot) == 95)

  # transmog a stack of items
  set item in slot(lastslot, item:SteelSho)
  assert(item in slot(lastslot) == item:SteelSho)
  assert(item count in slot(lastslot) == 95)
  assert(inventory(item:SteelSho) == 95)
  assert(inventory(item:Boots) == 0)

  # test the two additional ways of deleting items
  set item in slot(lastslot, -1)
  assert(item count in slot(lastslot) == 0)
  set item in slot(2, item:Boots)
  set item count in slot(2, 0)
  assert(item in slot(2) == -1) 

  # leave nothing
  for (slot, 0, get inventory size -- 1) do (
    assert(item in slot(slot) == -1)
  )

  # test max stack sizes
  assert(get item maximum stack size(item:Boots) == 99)
  assert(get item maximum stack size(item:Bulky) == 3)
  get item(item:Bulky, 6)
  assert(inventory(item:Bulky) == 6)
  assert(item count in slot(0) == 3)
  assert(item count in slot(1) == 3)
  set item in slot(2, item:Boots)
  set item count in slot(2, 40)
  # changing type should cause stacks to overflow if too large
  set item in slot(2, item:Bulky)
  assert(inventory(item:Bulky) == 46)
  assert(item count in slot(2) == 3)
  delete item(item:Bulky, 46)

  # Max out the inventory (was set to 10 rows, 30 slots)
  assert(get item(item:spamknif, 99) == 99) # yes there is room for 99 of these
  assert(get item(item:spamknif, 3000) == 2871) # No, there will not be room for all of these
  assert(inventory(item:spamknif) == 2970)
  variable(who)
  who := find hero(hero:Freki)
  assert(force equip(who, slot:weapon, item:spamknif) == true) # Force Freki to use one of the spamknifes from the inventory
  assert(inventory(item:spamknif) == 2969)
  assert(force equip(who, slot:weapon, item:overflow) == true) # The previously equipped spamknife will go back to the inventory
  assert(inventory(item:spamknif) == 2970)
  assert(force equip(who, slot:weapon, item:overflow) == false) # There is no free slot for the currently equipped overflow sword that has to be removed to make room for the new overflow sword
  # Delete the items we filled the inventory with (some later tests expect an empty inventory)
  delete item(item:spamknif, inventory(item:spamknif))
  assert(inventory(item:spamknif) == 0)
  # Force equip should also fail for an invalid item number, but we can't check that without throwing an error
  #assert(force equip(who, slot:weapon, 99999999) == false)
  # Force equip should also fail for an invalid hero party slot, but we can't check that without throwing an error
  #assert(force equip(-5, slot:weapon, item:overflow) == false)
  # Remove the overflow sword and delete it
  unequip(who, slot:weapon)
  assert(inventory(item:overflow) == 1)
  delete item(item:overflow, 1)
  assert(inventory(item:overflow) == 0)
  
end

########################################################################

script, items tests, begin
  $0="Items tests"
  # These tests concern the definitions of items.
  # For items actually in your inventory, see "inventory tests"

  get item name(1, item:Boots)
  assert(string equal(1, $2="Boots"))
  get item description(1, item:Boots)
  assert(string equal(1, $2="For foots"))
  assert(get item value(item:boots) == 10)
  assert(get item teach spell(item:boots) == none)
  assert(get item teach spell(item:larnspel) == atk:ice attack)
  assert(get item attack outside battle(item:boots) == none)
  assert(get item attack outside battle(item:potion) == atk:potionatk)
  assert(get item attack in battle(item:boots) == none)
  assert(get item attack in battle(item:potion) == atk:potionatk)
  assert(get item attack as weapon(item:boots) == none)
  assert(get item attack as weapon(item:dulsword) == atk:dullsword)
  variable(box, line)
  box := get item text box(item:boots)
  assert(box == -1)
  box := get item text box(item:boxitem)
  text box line(1, box, 0)
  assert(string equal(1, $2="Nobody expects the textbox inquisition"))
  assert(get item equip stat bonus(item:skort, stat:atk) == 0)
  assert(get item equip stat bonus(item:skort, stat:def) == 2)
  assert(get item equip stat bonus(item:skort, stat:dog) == -1)
  assert(equippable in slot(item:skort,slot:weapon) == false)
  assert(equippable in slot(item:skort,slot:legs) == true)
  assert(equippable in slot(item:skort,slot:head) == true)
  assert(equippable in slot(item:skort,slot:body) == false)
  assert(equippable in slot(item:skort,slot:arms) == false)
  assert(get item weapon pic(item:sordpic) == 1)
  assert(get item weapon pal(item:sordpic) == -1)
  assert(get item weapon pic(item:ackspic) == 2)
  assert(get item weapon pal(item:ackspic) == 36)
end

########################################################################

script, hero tests, begin
  hero party management tests
  # The party now contains heroes 0, 1, 2, 3 in that order
  hero appearance tests
  hero caterpillar tests
  suspend walkabouts tests
  hero exp and levels tests
  hero stat growth test
  hero stat tests
  equipment tests
  hero misc tests
  hero names embed tests
  # The party should continue to be 0, 1, 2, 3
end

## hero commands left to test

## spell lists
#can learn spell (hero,attack)
#forget spell (hero,attack)
#knows spell (hero,attack)
#read spell (hero,list,slot)
#teach spell (hero,attack)
#write spell (hero,list,slot,attack)

## Equipment
#set default weapon (hero,item)

## Stats
#get level MP (who, mp level slot, type)
#set level MP (who, mp level slot, new value)

## Misc
#hero base elemental resist as int (who, element)
#hero total elemental resist as int (who, element)
#set hero base elemental resist (who, element, percent)

plotscript, add hero hook, slot, who, begin
  assert(hero by slot(slot) == who)
  observed hero count += 1
end

plotscript, remove hero hook, slot, who, begin
  assert(hero by slot(slot) == -1)
  observed hero count -= 1
end

plotscript, move hero hook, slot, old slot, loading, begin
  # This script only does something on three specific calls, see below,
  if (testing script triggers) then (
    move hero count += 1
    assert(loading == false)
  )
  if (testing script triggers == 1) then (
    # Used inside "concurrent script trigger tests"
    called from move hero hook
  )
  if (testing script triggers == 3) then (
    # Swapping slots 2 and 3, both occupied
    assert(slot == 2 || slot == 3)
    assert(slot + old slot == 5)
  )
  if (testing script triggers == 2) then (
    # Swapping hero into an empty slot
    assert(slot == 20)
    assert(old slot >= 4)
    assert(hero by slot(slot) == hero:Kitt)
    assert(hero by slot(old slot) == -1)
  )
end

script, hero party management tests, begin
  $0="hero party management tests"
  # Preconditions
  assert(room in active party == 3)
  variable(h0x, h0y, h0speed, h0d)
  h0x := hero x(0)
  h0y := hero y(0)
  h0speed := get hero speed(0)
  h0d := hero direction(0)

  assert(observed hero count == 1)
  assert(add hero(hero:Helga) == 1)
  assert(add hero(hero:Olaf) == 2)
  assert(add hero(hero:Frumpy) == 3)
  assert(room in active party == 0)
  assert(observed hero count == 4)
  assert(hero by rank(0) == hero:Freki)
  assert(hero by rank(1) == hero:Helga)
  assert(hero by rank(2) == hero:Olaf)
  assert(hero by rank(3) == hero:Frumpy)
  # with a full party, hero by rank and hero by slot should return the same values
  assert(hero by rank(0) == hero by slot(0))
  assert(hero by rank(1) == hero by slot(1))
  assert(hero by rank(2) == hero by slot(2))
  assert(hero by rank(3) == hero by slot(3))
  # now delete the leader
  delete hero(hero:Freki)
  assert(observed hero count == 3)
  assert(room in active party == 1)
  assert(hero by rank(0) == 1)
  assert(hero by rank(1) == 2)
  assert(hero by rank(2) == 3)
  assert(hero by rank(3) == -1)
  assert(hero by slot(0) == -1)
  assert(hero by slot(1) == 1)
  assert(hero by slot(2) == 2)
  assert(hero by slot(3) == 3)
  # Test some commands which should work by rank, not party slot
  # (TODO: incomplete)
  assert(hero x(0) == h0x)
  assert(hero y(0) == h0y)
  assert(hero z(0) == 0)
  assert(get hero speed(0) == h0speed)
  assert(hero direction(0) == h0d)
  
  # add a different leader
  add hero(hero:Styrge)
  assert(hero by slot(0) == hero:Styrge)
  # test find hero
  assert(find hero(hero:Freki)  == -1)
  assert(find hero(hero:Helga)  == 1)
  assert(find hero(hero:Olaf)   == 2)
  assert(find hero(hero:Frumpy) == 3)
  assert(find hero(hero:Styrge) == 0)
  # test rank in caterpillar
  assert(rank in caterpillar(hero:Freki)  == -1)
  assert(rank in caterpillar(hero:Helga)  == 1)
  assert(rank in caterpillar(hero:Olaf)   == 2)
  assert(rank in caterpillar(hero:Frumpy) == 3)
  assert(rank in caterpillar(hero:Styrge) == 0)
  # Add a hero to the reserve
  add hero(hero:Kitt)
  assert(room in active party == 0)
  variable(slot)
  slot := find hero(hero:Kitt)
  assert(slot >= 4)
  assert(rank in caterpillar(hero:Kitt) == -1)
  assert(hero by slot(slot) == hero:Kitt) # this command needs to work on reserve slots
  assert(hero by rank(4) == -1) # this command should not work on reserve slots
  # names
  $1="Styrge"
  get hero name(2, 0)
  assert(string compare(1, 2))
  $1="Helga"
  get hero name(2, 1)
  assert(string compare(1, 2))
  $1="Olaf"
  get hero name(2, 2)
  assert(string compare(1, 2))
  $1="Frumpy"
  get hero name(2, 3)
  assert(string compare(1, 2))
  $1="Kitt"
  get hero name(2, find hero(hero:Kitt))
  assert(string compare(1, 2))
  $1="Kittzilla"
  set hero name(1, find hero(hero:Kitt))
  get hero name(2, find hero(hero:Kitt))
  assert(string compare(1, 2))
  # Swapping
  assert(swap out hero(hero:Helga) >= 4)
  assert(hero by slot(1) == -1)
  assert(find hero(hero:Helga) >= 4)
  assert(swap in hero(hero:Kitt) == 1)
  assert(hero by slot(1) == hero:Kitt)
  assert(find hero(hero:Kitt) >> 0 && find hero(hero:Kitt) <= 3)
  swap by position(0, 3) # swap Styrge and Frumpy
  assert(hero by slot(0) == hero:Frumpy)
  assert(hero by slot(3) == hero:Styrge)
  swap by position(3, find hero(hero:Helga)) # swap Styrge and Helga
  assert(hero by slot(3) == hero:Helga)
  assert(find hero(hero:Styrge) >= 4)
  swap by name(hero:Kitt, hero:Styrge)
  assert(hero by slot(1) == hero:Styrge)
  assert(find hero(hero:Kitt) >= 4)
  swap out hero(hero by slot(0)) # swap out frumpy
  assert(hero by slot(0) == -1)
  # # Invalid swaps of empty slots (these throw errors)
  # assert(hero by slot(20) == -1)  # Precondition
  # assert(swap out hero by slot(20) == -1)
  # assert(swap in hero by slot(20) == -1)
  # assert(swap in hero by slot(9999) == -1)
  # assert(swap out hero by slot(9999) == -1)
  # assert(swap in hero by slot(-1) == -1)
  # assert(swap out hero by slot(-1) == -1)
  # Locking
  lock hero(hero:Frumpy)
  assert(hero is locked(hero:Frumpy) == true)
  # ...check locking doesn't prevent scripted swapping
  swap in hero(hero:Frumpy)
  assert(find hero(hero:Frumpy) < 4)
  swap out hero(hero:Frumpy)
  assert(find hero(hero:Frumpy) >= 4)
  unlock hero(hero:Frumpy)
  assert(hero is locked(hero:Frumpy) == false)

  # restore numeric order
  add hero(hero:Freki) # Freki
  assert(hero by slot(0) == hero:Freki)
  swap by position(1, find hero(hero:Helga))
  assert(hero by slot(1) == hero:Helga)
  swap by position(2, find hero(hero:Olaf))
  assert(hero by slot(2) == hero:Olaf)
  #swap by position(3, find hero(hero:Frumpy))
  swap by slot(3, find hero(hero:Frumpy))  # Test alias
  assert(hero by slot(3) == hero:Frumpy)

  # add another copy of Freki, should go to first free reserve slot
  add hero(hero:Freki)
  slot := find hero(hero:Freki)
  assert(slot == 0)
  delete hero by slot(slot)
  slot := find hero(hero:Freki)
  #assert(slot >= 4)
  assert(slot == 4)
  # add back leader Freki
  assert(hero by slot(0) == -1)
  add hero(hero:Freki)
  assert(hero by slot(0) == hero:Freki)
  # Check 'by slot' locking and swapping
  lock hero by slot(0)
  assert(hero is locked(hero:Freki) == true)
  variable(newslot)
  newslot := swap out hero by slot(0)
  assert(newslot > 4)
  # The Freki in slot 4 now is the first
  assert(hero is locked(hero:Freki) == false)
  unlock hero by slot(newslot)
  assert(hero is locked(hero:Freki) == false)  # slot 4
  lock hero by slot(newslot)
  assert(swap in hero by slot(newslot) == 0)
  assert(hero is locked(hero:Freki) == true)  # slot 0
  # Delete the reserve Freki
  assert(slot >= 4)
  assert(hero by slot(slot) == hero:Freki)
  assert(delete hero by slot(slot) == 0)
  assert(hero by slot(slot) == -1)
  # original Freki should be untouched, and not in the reserve
  assert(find hero(hero:Freki) >= 0)
  assert(find hero(hero:Freki) <= 3)
  w

  # Test "move hero hook"
  testing script triggers := 3  # Tell the hook script to pay attention
  assert(move hero count == 0)
  # Swapping two occupied slots
  swap by position (2, 3)
  assert(move hero count == 2)
  testing script triggers := 2
  # Swap occupied and empty
  swap by position(find hero(hero:Kitt), 20)
  assert(move hero count == 3)
  testing script triggers := false
  swap by position (2, 3)

end

script, hero appearance tests, begin
  # preconditions
  assert(hero by slot(3) == hero:Frumpy)
  # ensure we're using party slots, not hero ids or caterpillar position
  swap by position(2, 3)
  swap by position(1, 10)

  # 2 = hero:Frumpy
  assert(get hero picture(2, inside battle) == 8)
  assert(get hero picture(2, outside battle) == 9)
  assert(get hero picture(2, hero portrait) == 2)
  assert(get hero palette(2, inside battle) == -1)
  assert(get hero palette(2, outside battle) == -1)
  assert(get hero palette(2, hero portrait) == 2)

  set hero picture(2, 0, inside battle)
  set hero picture(2, 4, outside battle)
  set hero picture(2, 7, hero portrait)
  set hero palette(2, 37, inside battle)
  set hero palette(2, 1, outside battle)
  set hero palette(2, -1, hero portrait)

  show text box(17)  # test portrait by party slot
  w(3)
  advance text box

  assert(get hero picture(2, inside battle) == 0)
  assert(get hero picture(2, outside battle) == 4)
  assert(get hero picture(2, hero portrait) == 7)
  assert(get hero palette(2, inside battle) == 37)
  assert(get hero palette(2, outside battle) == 1)
  assert(get hero palette(2, hero portrait) == -1)

  reset hero picture(2, inside battle)
  reset hero picture(2, outside battle)
  reset hero picture(2, hero portrait)
  reset hero palette(2, inside battle)
  reset hero palette(2, outside battle)
  reset hero palette(2, hero portrait)

  assert(get hero picture(2, inside battle) == 8)
  assert(get hero picture(2, outside battle) == 9)
  assert(get hero picture(2, hero portrait) == 2)
  assert(get hero palette(2, inside battle) == -1)
  assert(get hero palette(2, outside battle) == -1)
  assert(get hero palette(2, hero portrait) == 2)

  # Restore state
  swap by position(2, 3)
  swap by position(1, 10)
end

script, hero names embed tests, begin
  $0="hero names embed tests"

  variable(h0, h1, h2, h3)
  # remember the old party order
  h0 := hero by slot(0)
  h1 := hero by slot(1)
  h2 := hero by slot(2)
  h3 := hero by slot(3)
  
  swap by position(0, find hero(hero:Olaf))
  swap out hero(hero by slot(1))
  swap out hero(hero by slot(2))
  swap out hero(hero by slot(3))

  $1="Olaf,,,"
  $2="Olaf,,,"
  $3="Freki,Helga,Olaf,Frumpy,Styrge"
  hero names embed helper

  swap by position(2, find hero(hero:Olaf))
  $1="Olaf,,,"
  $2=",,Olaf,"
  hero names embed helper

  swap by position(0, find hero(hero:Frumpy))
  $1="Frumpy,Olaf,,"
  $2="Frumpy,,Olaf,"
  hero names embed helper

  swap by position(1, find hero(hero:Olaf))
  swap by position(3, find hero(hero:Frumpy))
  $1="Olaf,Frumpy,,"
  $2=",Olaf,,Frumpy"
  hero names embed helper

  add hero(hero:Olaf)  # slot 1
  $4="Nolaf"
  set hero name(4, 1)
  $1="Olaf,Nolaf,Frumpy,"
  $2="Olaf,Nolaf,,Frumpy"
  hero names embed helper

  swap by position(0, find hero(hero:Frumpy))
  $1="Frumpy,Nolaf,Olaf,"
  $2="Frumpy,Nolaf,,Olaf"
  $3="Freki,Helga,Nolaf,Frumpy,Styrge"
  hero names embed helper

  show no value, w
  advance textbox
  swap by position(0, 1)  # Nolaf to front
  delete hero(hero:Olaf)
  
  # Test embedtext from save slots

  # Control test, no change to hero name
  save in slot(4), w
  expand string($1="${H1}", 4)
  show string(1), w
  assert(string compare(1, $2="Helga"))
  
  # Hero name by hero id
  # Helga should be in the reserve when this happens, but we don't care exactly which reserve slot
  variable(helgaslot)
  helgaslot := find hero(hero:Helga)
  assert(helgaslot > 3)
  # Change Helga's name
  set hero name($3="Hagar", helgaslot)
  save in slot(4), w
  expand string($1="${H1}", 4)
  show string(1), w
  assert(string compare(1, $2="Hagar"))
  # Restore hero name, check again
  set hero name($3="Helga", helgaslot)
  assert(string compare(get hero name(1, helgaslot), $2="Helga"))
  expand string($1="${H1}", 4)
  show string(1), w
  assert(string compare(1, $2="Hagar"))
  
  # Hero name by party slot
  # Put helga in slot 2
  swap by position(find hero(hero:Helga), 2)
  assert(find hero(hero:Helga) == 2)
  # Rename her and resave
  set hero name($3="Humpty", 2)
  save in slot(4), w
  expand string($1="${P2}", 4)
  show string(1), w
  assert(string compare(1, $2="Humpty"))
  # Restore name, check again
  set hero name($3="Helga", 2)
  assert(string compare(get hero name(1, 2), $2="Helga"))
  expand string($1="${P2}", 4)
  show string(1), w
  assert(string compare(1, $2="Humpty"))

  # Hero name by caterpillar slot  
  # Put helga in slot 3
  swap by position(find hero(hero:Helga), 3)
  assert(find hero(hero:Helga) == 3)
  # Make slot 2 empty
  show string(get hero name(1, 2)), w
  swap out hero(hero:Olaf)
  assert(hero by slot(2) == -1)
  # Make sure slot 0 is empty
  assert(hero by slot(0) == -1)
  # Helga should be rank 1 (2nd) in the caterpillar
  assert(rank in caterpillar(hero:Helga) == 1)
  # Rename Helga and save
  set hero name($1="Hissy", 3)
  save in slot(4), w
  expand string($1="${C1}", 4)
  show string(1), w
  assert(string compare(1, $2="Hissy"))
  # Restore name, check again
  set hero name($3="Helga", 3)
  assert(string compare(get hero name(1, 3), $2="Helga"))
  expand string($1="${C1}", 4)
  show string(1), w
  assert(string compare(1, $2="Hissy"))
  show no value
  
  # Check strings being expanded from an empty slot
  # Make sure slot 5 is really empty
  delete save(5)
  assert(string compare($1="${H1}", expand string($2="${H1}", 5)))
  assert(string compare($1="${P0}", expand string($2="${P0}", 5)))
  assert(string compare($1="${C0}", expand string($2="${C0}", 5)))
  assert(string compare($1="${H1},${H2},${H3}", expand string($2="${H1},${H2},${H3}", 5)))
  
  # Although not hero related, lets throw some tests for ${V} and ${S} in here too.
  #saveglobal is global id 121
  saveglobal := 54321
  show string(expand string($1="Var=${V121}")), w
  assert(string compare(1, $2="Var=54321"))
  # Save the global to save slot 4, check it
  save in slot(4), w
  saveglobal := 1
  show string(expand string($1="Var=${V121}", 4)), w
  assert(string compare(1, $2="Var=54321"))
  # Replace it with another value
  saveglobal := 100001
  save in slot(4), w
  saveglobal := 1
  show string(expand string($1="Var=${V121}", 4)), w
  assert(string compare(1, $2="Var=100001"))
  assert(saveglobal == 1)
  # Check empty slot 5
  show string(expand string($1="Var=${V121}", 5)), w
  assert(string compare(1, $2="Var=${V121}"))

  # Test strings
  $255="A very fine mess"
  save in slot(4), w
  $255="Cleaned up"
  show string(expand string($2="${S255}", 4)), w
  assert(string compare(255, $1="Cleaned up"))
  assert(string compare(2, $1="A very fine mess"))
  # A different string
  $2="Is better than sloppy order"
  save in slot(4), w
  $2=""
  show string(expand string($3="${S2}", 4)), w
  assert(string compare(3, $1="Is better than sloppy order"))
  # An empty save slot (5)
  show string(expand string($3="${S2}", 5)), w
  assert(string compare(3, $1="${S2}"))
  
  # Test a string in a high-numbered save slot
  $3="Not Over 9000!!"
  save in slot(save slot: high)
  $3="Cleaned up"
  show string(expand string($2="String=${S3}", save slot: high))
  assert(string compare(2, $1="String=Not Over 9000!!"))

  # restore old party order
  swap by position(0, find hero(h0))  
  swap by position(1, find hero(h1))
  swap by position(2, find hero(h2))
  swap by position(3, find hero(h3))
  show box names
  advance textbox
  
end

script, hero names embed helper, begin
  show string(2)
  $4="${C0},${C1},${C2},${C3}"
  expand string(4)
  $5="${P0},${P1},${P2},${P3}"
  expand string(5)
  $6="${H65},${H1},${H2},${H3},${H4}"
  expand string(6)
  assert(string compare(1, 4))
  assert(string compare(2, 5))
  assert(string compare(3, 6))
  show box names
end

script, show box names, begin
  show text box(9)
  w(10)
  show text box(10)
  w(10)
  show text box(11)
  w(10)
end

script, hero caterpillar tests, begin
  variable(i)
  $0="hero caterpillar tests"

  $err arg string="leader, cater=ON"
  do hero cater tests(me)
  
  suspend caterpillar
  for(i, 0, 3) do(
    $err arg string="hero "
    append number(err arg string, i)
    $err arg string=", cater=OFF"
    do hero cater tests(i)
  )
  resume caterpillar
  $err arg string=""

end

script, do hero cater tests, who, begin
  variable(d)
  walk hero to x (who, 8), w
  wait for hero(who)
  assert(hero x(who) == 8)
  walk hero to y(who, 10), w
  wait for hero(who)
  assert(hero y(who) == 10)
  camera follows hero(who)
  walk hero to x(who, 9), w
  wait for hero(who)
  assert(hero x(who) == 9)
  walk hero to y(who, 8), w
  wait for hero(who)
  assert(hero y(who) == 8)
  camera follows hero(0)
  walk hero to x (who, 6), w
  wait for hero(who)
  assert(hero x(who) == 6)
  set hero position(who, 7, 7), w
  assert(hero x(who) == 7)
  assert(hero y(who) == 7)
  walk hero(who, south, 2)
  wait for hero(who), w
  assert(hero y(who) == 9)
  assert(hero direction(who) == south)
  assert(forward x(who) == 7)
  assert(forward y(who) == 10)
  walk hero(who, south, -1) # negative distance means walk backwards
  wait for hero(who), w
  assert(hero y(who) == 8)
  assert(hero direction(who) == south)
  walk hero(who, south, 1)
  wait for hero(who), w
  assert(hero y(who) == 9)
  assert(hero direction(who) == south)
  walk hero(who, west, 2)
  wait for hero(who), w
  assert(hero x(who) == 5)
  assert(hero direction(who) == west)
  assert(forward x(who) == 4)
  assert(forward y(who) == 9)
  walk hero(who, north, 1)
  wait for hero(who), w
  assert(hero y(who) == 8)
  assert(hero direction(who) == north)
  walk hero(who, east, 1)
  wait for hero(who), w
  assert(hero x(who) == 6)
  assert(hero direction(who) == east)
  # spin
  for(d, north, west) do(
    set hero direction(who, d), w
    assert(hero direction(who) == d)
  )
  # moonwalk in a circle
  walk hero(who, south, 1)
  set hero direction(who, north)
  wait for hero(who), w
  assert(hero direction(who) == north)
  assert(hero x(who) == 6)
  assert(hero y(who) == 9)
  walk hero(who, west, 1)
  set hero direction(who, east)
  wait for hero(who), w
  assert(hero direction(who) == east)
  assert(hero x(who) == 5)
  assert(hero y(who) == 9)
  walk hero(who, north, 1)
  set hero direction(who, south)
  wait for hero(who), w
  assert(hero direction(who) == south)
  assert(hero x(who) == 5)
  assert(hero y(who) == 8)
  walk hero(who, east, 1)
  set hero direction(who, west)
  wait for hero(who), w
  assert(hero direction(who) == west)
  assert(hero x(who) == 6)
  assert(hero y(who) == 8)
  # moonwalk in a circle using negative distance
  walk hero(who, north, -1)
  wait for hero(who), w
  assert(hero direction(who) == north)
  assert(hero x(who) == 6)
  assert(hero y(who) == 9)
  walk hero(who, east, -1)
  wait for hero(who), w
  assert(hero direction(who) == east)
  assert(hero x(who) == 5)
  assert(hero y(who) == 9)
  walk hero(who, south, -1)
  wait for hero(who), w
  assert(hero direction(who) == south)
  assert(hero x(who) == 5)
  assert(hero y(who) == 8)
  walk hero(who, west, -1)
  wait for hero(who), w
  assert(hero direction(who) == west)
  assert(hero x(who) == 6)
  assert(hero y(who) == 8)
  # walk into wall
  walk hero(who, north, 3) # should only make it 2 tiles
  wait for hero(who)
  assert(hero Y(who) == 6)
  # check walls
  expect hero walls(who, true, false, false, false)
  walk hero(who, west, 1)
  wait for hero(who), w
  walk hero(who, north, 1)
  wait for hero(who), w
  expect hero walls(who, false, true, false, false)
  walk hero(who, north, 2)
  wait for hero(who), w
  walk hero(who, east, 1)
  wait for hero(who), w
  expect hero walls(who, false, false, true, false)
  walk hero(who, east, 5)
  wait for hero(who), w
  expect hero walls(who, true, true, false, false)
  # speed test time!
  set hero speed(who, 2)
  assert(get hero speed(who) == 2)
  walk hero(who, south, 1)
  w(9), assert(hero is walking(who))
  w(1), assert(hero is walking(who) == false)
  set hero speed(who, 4)
  assert(get hero speed(who) == 4)
  walk hero(who, south, 1)
  w(4), assert(hero is walking(who))
  w(1), assert(hero is walking(who) == false)
  set hero speed(who, 5)
  assert(get hero speed(who) == 5)
  walk hero(who, south, 1)
  w(3), assert(hero is walking(who))
  w(1), assert(hero is walking(who) == false)
  set hero speed(who, 10)
  assert(get hero speed(who) == 10)
  walk hero(who, south, 1)
  w(1), assert(hero is walking(who))
  w(1), assert(hero is walking(who) == false)
  set hero speed(who, 20)
  assert(get hero speed(who) == 20)
  walk hero(who, south, 1)
  w(1), assert(hero is walking(who) == false)
  walk hero(who, west, 1), w
  walk hero(who, north, 2), w(2)
  assert(hero x(who) == 10)
  assert(hero y(who) == 6)
  set hero speed(who)
  assert(get hero speed(who) == 4)
  walk hero(who, west, 3)
  wait for hero(who)
  #FIXME: should probably test misaligned walking too

  # frame (mirrored for NPCs in "npc slice tests"
  variable(fr,dir,numframes)
  for(dir, 0, 3) do(
    numframes := sprite frame group size(get hero sprite(who), dir)
    if (dir == right && hero by rank(who) == hero:frumpy) then (
      assert(numframes == 4)
    ) else (
      assert(numframes == 2)
    )

    for(fr, 0, numframes -- 1) do(
      set hero direction(who, dir)
      set hero frame(who, fr)
      assert(hero frame(who) == fr)
      w
    )
    set hero frame(who, -1)
    assert(hero frame(who) == 0)
    set hero frame(who, numframes)
    assert(hero frame(who) == numframes -- 1)  # Not too important
  )
  #pixel pos
  variable(px, py)
  px := hero pixel x(who)
  assert(px == 140)
  py := hero pixel y(who)
  assert(py == 120)
  variable(ix, iy)
  for(iy, 0, 5) do(
    for(ix, 0, 5) do(
      put hero(who, 105 + ix, 105 + iy)
      assert(hero pixel x(who) == 105 + ix)
      assert(hero pixel y(who) == 105 + iy)
      w
    )
  )

  # Test one-wall walls
  set hero position(who, 5, 9)
  walk hero(who, south, 1)  # normal walk onto a tile with aone-way wall
  wait for hero(who)
  assert(hero y(who) == 10)
  walk hero(who, east, 1)  # walk off, where there is no wall
  wait for hero(who)
  assert(hero x(who) == 6)
  walk hero(who, west, 2)  # walking between tiles with one-way walls in other directions
  wait for hero(who)
  assert(hero x(who) == 4)
  walk hero(who, west, 1)  # attempt to walk through a normal wall while on a oneway wall
  wait for hero(who)
  assert(hero x(who) == 4)
  walk hero(who, north, 1) # walk through one-way wall (allowed)
  wait for hero(who)
  assert(hero y(who) == 9)
  walk hero(who, south, 1) # try to walk disallowed direction
  wait for hero(who)
  assert(hero y(who) == 9)
  walk hero(who, east, 1)
  wait for hero(who)
  walk hero(who, south, 2) # walk through one-way wall
  wait for hero(who)
  assert(hero y(who) == 11)
  walk hero(who, north, 1) # try to walk disallowed direction
  wait for hero(who)
  assert(hero y(who) == 11)

  put hero(who, px, py), w
end


script, suspend walkabouts tests, begin
  $0="suspend walkabouts tests"

  # There's an awful lot of things that could be tested, including NPC and hero movement.
  # Mostly just test heroes.

  trace(0)
  trace value (herox, heroy)

  # Check initial conditions
  assert(get hero speed(me) == 4)
  assert(hero is walking(me) == false)
  variable (initial x)
  initial x := hero x(me)

  walk hero(me, left, 1)
  wait(1)
  variable (px)
  px := hero pixel x(me)

  suspend walkabouts
  assert(walkabouts are suspended)

  # Check movement paused
  wait(2)
  assert(px == hero pixel x(me))
  assert(hero is walking(me))

  resume walkabouts
  assert(walkabouts are suspended == false)

  # Check moving
  wait(1)
  assert(px <> hero pixel x(me))
  # Cancel move
  walk hero(me, left, 0)
  wait(1)
  assert(hero is walking(me) == false)

  suspend walkabouts
  # Can still move the hero while suspended
  assert(hero x(me) <> initial x)
  set hero position(me, initial x, hero y(me))
  assert(hero x(me) == initial x)
  wait(1)
  # Walk commands don't take effect while suspended
  assert(hero direction(me) == left)
  walk hero(me, right, 1)
  assert(hero direction(me) == right)  # walkhero changes the direction directly
  wait(1)
  assert(hero pixel x(me) == initial x * 20)

  # But do once resumed
  resume walkabouts
  wait(1)
  assert(hero pixel x(me) <> initial x * 20)

  # Cleanup
  set hero position(me, initial x, hero y(me))
  walk hero(me, left, 0)
end


script, expect hero walls, who, wn, we, ws, ww, begin
  assert(check hero wall(who, north) == wn)
  assert(check hero wall(who, east) == we)
  assert(check hero wall(who, south) == ws)
  assert(check hero wall(who, west) == ww)
end

## caterpillar
#suspend hero walls
#resume hero walls
#set hero frame (who, frame)
#set hero speed (who, speed)
#set hero z (who, z)

script, hero exp and levels tests, begin
  variable(i)
  $0="hero exp and levels tests"

  # check default level curve
  assert(experience to level(1) == 30)
  assert(experience to level(2) == 71)
  assert(experience to level(3) == 125)
  assert(experience to level(4) == 195)
  assert(experience to level(5) == 284)
  assert(experience to level(6) == 396)
  assert(experience to level(7) == 535)
  assert(experience to level(8) == 707)
  assert(experience to level(9) == 918)
  assert(experience to level(10) == 1176)
  assert(experience to level(11) == 1491)
  assert(experience to level(12) == 1874)
  assert(experience to level(13) == 2339)
  assert(experience to level(14) == 2902)
  assert(experience to level(15) == 3583)
  assert(experience to level(99) == 50183838)

  # check Freki's level curve (should be same as default)
  variable(freki slot)
  freki slot := find hero(hero:freki)
  assert(freki slot >= 0)
  assert(experience to level(1, freki slot) == 30)
  assert(experience to level(2, freki slot) == 71)
  assert(experience to level(3, freki slot) == 125)
  assert(experience to level(4, freki slot) == 195)
  assert(experience to level(5, freki slot) == 284)
  assert(experience to level(6, freki slot) == 396)
  assert(experience to level(7, freki slot) == 535)
  assert(experience to level(8, freki slot) == 707)
  assert(experience to level(9, freki slot) == 918)
  assert(experience to level(10, freki slot) == 1176)
  assert(experience to level(11, freki slot) == 1491)
  assert(experience to level(12, freki slot) == 1874)
  assert(experience to level(13, freki slot) == 2339)
  assert(experience to level(14, freki slot) == 2902)
  assert(experience to level(15, freki slot) == 3583)
  assert(experience to level(99, freki slot) == 50183838)

  # check Bram's level curve (should faster than default)
  add hero(hero:Bram)
  variable(bram slot)
  bram slot := find hero(hero:Bram)
  assert(bram slot >= 0)
  assert(experience to level(1, bram slot) == 30)
  assert(experience to level(2, bram slot) == 70)
  assert(experience to level(3, bram slot) == 121)
  assert(experience to level(4, bram slot) == 185)
  assert(experience to level(5, bram slot) == 264)
  assert(experience to level(6, bram slot) == 360)
  assert(experience to level(7, bram slot) == 475)
  assert(experience to level(8, bram slot) == 612)
  assert(experience to level(9, bram slot) == 775)
  assert(experience to level(10, bram slot) == 967)
  assert(experience to level(11, bram slot) == 1193)
  assert(experience to level(12, bram slot) == 1458)
  assert(experience to level(13, bram slot) == 1768)
  assert(experience to level(14, bram slot) == 2130)
  assert(experience to level(15, bram slot) == 2551)
  assert(experience to level(99, bram slot) == 36587434)
  delete hero(hero:Bram)
  
  assert(get hero level(0) == 0)
  # level setting
  for(i, 1, 99) do(
    set hero level(0, i)
    assert(get hero level(0) == i)
    assert(total experience(0) == experience to level(i))
  )
  # level-based spell learning/forgetting
  set hero level(0, 9)
  assert(knows spell (0, 2) == false) # Freki should not have L.10 Wolf at level 9
  set hero level(0, 10)
  assert(knows spell (0, 2) == true) # Freki should learn L.10 Wolf at level 10
  set hero level(0, 0, false)
  assert(get hero level(0) == 0)
  assert(hero levelled(0) == -10)
  assert(get hero stat(0, 12, 1) == -10)  # Undocumented use of stat 12
  assert(knows spell (0, 2) == true) # Freki should not forget L.10 Wolf because of how we deleveled
  update level up learning(0, true)
  assert(knows spell (0, 2) == false) # Now Freki should forget L.10 Wolf.
  # experience
  assert(experience to next level(0) == 30)
  give experience(0, 5)
  assert(hero levelled(0) == 0)
  assert(experience to next level(0) == 25)
  give experience(0, 25)
  assert(get hero level(0) == 1)
  assert(hero levelled(0) == 1)
  assert(experience to next level(0) == 41)
  give experience(0, -1) # de-level should not work with this command, but experience will go down
  assert(get hero level(0) == 1)
  assert(hero levelled(0) == 0)
  assert(experience to next level(0) == 42)
  give experience(0, 42)
  assert(get hero level(0) == 2)
  assert(hero levelled(0) == 1)
  set hero level(0, 9)
  give experience(0, experience to next level(0))
  assert(get hero level(0) == 10)
  assert(hero levelled(0) == 1)
  assert(spells learned(0, get count) == 1)
  assert(spells learned(0, 0) == 2)
  assert(spells learnt(0, 0) == 1) #deprecated, but why not test it?
  give experience(0, 1) # should not cause levelling
  assert(hero levelled == 0)
  assert(spells learned(0, get count) == 0)
  # split experience
  set hero level(0, 0, true)
  give experience(party, 30)
  assert(hero levelled(0) == 0)
  assert(total experience(0) == 8)
  assert(total experience(1) == 8)
  assert(total experience(2) == 8)
  assert(total experience(3) == 8)
  give experience(party, 88)
  assert(get hero level(0) == 1)
  assert(get hero level(1) == 1)
  assert(get hero level(2) == 1)
  assert(get hero level(3) == 1)
  assert(total experience(find hero(hero:Kitt)) == 0) # outside of the party
  # experience for dead heroes
  set hero level(0, 0, true)
  set hero level(1, 0, true)
  set hero level(2, 0, true)
  set hero level(3, 0, true)
  set hero stat(2, 0, 0) # kill Olaf
  set dead heroes gain experience (false)
  give experience(party, 120)
  assert(total experience(0) == 40)
  assert(total experience(1) == 40)
  assert(total experience(2) == 0)
  assert(total experience(3) == 40)
  set hero level(0, 0, true)
  set hero level(1, 0, true)
  set hero level(2, 0, true)
  set hero level(3, 0, true)
  set dead heroes gain experience (true)
  give experience(party, 120)
  assert(total experience(0) == 30)
  assert(total experience(1) == 30)
  assert(total experience(2) == 30)
  assert(total experience(3) == 30)
  set hero level(0, 0, true)
  set hero level(1, 0, true)
  set hero level(2, 0, true)
  set hero level(3, 0, true)
  set hero stat(2, 0, get hero stat(2, 0, maximum stat)) # revive Olaf
  # level caps
  set hero level(0, 1)
  assert(get level cap == 99)
  set level cap(2)
  assert(get level cap == 2)
  give experience(0, 41)
  assert(get hero level(0) == 2)
  assert(total experience(0) == 71)
  assert(experience to next level(0) == 54) # being at the level cap does not alter the exp to next level
  give experience(0, 10000000)
  assert(hero levelled(0) == 0)
  assert(get hero level(0) == 2)
  set level cap(99)
  # Hero level range check tags
  assert(hero by slot(0) == hero:Freki)
  set hero level(0, 0, true), w
  assert(check tag(tag: freki level 0) == true)
  assert(check tag(tag: freki level teens) == false)
  set hero level(0, 12, true), w
  assert(check tag(tag: freki level 0) == false)
  assert(check tag(tag: freki level teens) == false)
  set hero level(0, 13, true), w
  assert(check tag(tag: freki level 0) == false)
  assert(check tag(tag: freki level teens) == true)
  set hero level(0, 19, true), w
  assert(check tag(tag: freki level 0) == false)
  assert(check tag(tag: freki level teens) == true)
  set hero level(0, 20, true), w
  assert(check tag(tag: freki level 0) == false)
  assert(check tag(tag: freki level teens) == false)

  # Undocumented stat 12 stuff
  assert(hero levelled(0) == 1)  # Precondition
  assert(get hero level(0) == 20)  # Precondition
  assert(get hero stat(0, 12, 1) == 1)  # == hero levelled
  assert(get hero stat(0, 12, 0) == 20)  # == getherolevel
  variable(temp)
  temp := get hero stat(0, 0, maximum stat)
  set hero stat(0, 12, 42, 0)  # Set hero level = 42 (but don't change stats)
  assert(get hero level(0) == 42)
  assert(get hero stat(0, 12, 0) == 42)
  assert(get hero stat(0, 0, maximum stat) == temp)  # Doesn't change stats
  assert(hero levelled(0) == 1)  # Doesn't change "hero levelled"
  set hero stat(0, 12, 3, 1)  # Set "hero levelled" = 3
  assert(hero levelled(0) == 3)
  set hero stat(0, 12, 20, 0)  # Reset

  # Hero base stat by level
  assert(hero base stat at level(hero:Freki, stat:hp, 0) == 10)
  assert(hero base stat at level(hero:Freki, stat:hp, 10) == 46)
  assert(hero base stat at level(hero:Freki, stat:hp, 50) == 334)
  assert(hero base stat at level(hero:Freki, stat:hp, 99) == 1000)
  
  # Hero base stat resetting
  freki slot := find hero(hero:freki)
  assert(get hero stat(freki slot, stat:hp, base stat) == 96)
  assert(get hero stat(freki slot, stat:hp, maximum stat) == 96)
  assert(get hero level(freki slot) == 20)
  assert(check equipment(freki slot, slot:head) == -1) # Head should start unequipped
  force equip(freki slot, slot:head, item:mighty) # Wear a helmet that gives +100 max hp
  assert(get hero stat(freki slot, stat:hp, base stat) == 96)
  assert(get hero stat(freki slot, stat:hp, maximum stat) == 196)
  # boost base stat +5
  set hero stat(freki slot, stat:hp, get hero stat(freki slot, stat:hp, base stat) + 5, base stat)
  assert(get hero stat(freki slot, stat:hp, base stat) == 101)
  assert(get hero stat(freki slot, stat:hp, maximum stat) == 201)
  # Reset base stat
  reset hero base stat(freki slot, stat:hp)
  assert(get hero stat(freki slot, stat:hp, base stat) == 96)
  assert(get hero stat(freki slot, stat:hp, maximum stat) == 196)
  
  #clean up after base stat reset tests
  unequip(freki slot, slot:head)
  delete item(item:mighty)
  assert(get hero stat(freki slot, stat:hp, base stat) == 96)
  assert(get hero stat(freki slot, stat:hp, maximum stat) == 96)

  # reset
  set hero level(0, 0, true)
  set hero level(1, 0, true)
  set hero level(2, 0, true)
  set hero level(3, 0, true)
end

script, hero stat growth test, begin
  $0="hero stat growth test"
  show string(0), w
  # This tests the crappy default stat growth curve, which we will hopefully replace someday
  set hero level(0 ,0)
  assert(get hero stat(0, 0, maximum stat) == 10)
  set hero level(0 ,1)
  assert(get hero stat(0, 0, maximum stat) == 13)
  set hero level(0 ,2)
  assert(get hero stat(0, 0, maximum stat) == 16)
#  set hero level(0 ,3)
#  assert(get hero stat(0, 0, maximum stat) == 19)
#  set hero level(0 ,4)
#  assert(get hero stat(0, 0, maximum stat) == 23)
#  set hero level(0 ,5)
#  assert(get hero stat(0, 0, maximum stat) == 26)
#  set hero level(0 ,6)
#  assert(get hero stat(0, 0, maximum stat) == 30)
#  set hero level(0 ,7)
#  assert(get hero stat(0, 0, maximum stat) == 34)
#  set hero level(0 ,8)
#  assert(get hero stat(0, 0, maximum stat) == 38)
#  set hero level(0 ,9)
#  assert(get hero stat(0, 0, maximum stat) == 42)
  set hero level(0 ,10)
  assert(get hero stat(0, 0, maximum stat) == 46)

  assert(get level mp(0, 0) == 5)
  assert(get level mp(0, 1) == 3)
  assert(get level mp(0, 2) == 2)
  assert(get level mp(0, 3) == 1)
  assert(get level mp(0, 4) == 0)
  assert(get level mp(0, 5) == 0)
  assert(get level mp(0, 6) == 0)
  assert(get level mp(0, 7) == 0)

#  set hero level(0 ,11)
#  assert(get hero stat(0, 0, maximum stat) == 50)
#  set hero level(0 ,12)
#  assert(get hero stat(0, 0, maximum stat) == 55)
#  set hero level(0 ,13)
#  assert(get hero stat(0, 0, maximum stat) == 60)
#  set hero level(0 ,14)
#  assert(get hero stat(0, 0, maximum stat) == 64)
#  set hero level(0 ,15)
#  assert(get hero stat(0, 0, maximum stat) == 69)
#  set hero level(0 ,16)
#  assert(get hero stat(0, 0, maximum stat) == 75)
#  set hero level(0 ,17)
#  assert(get hero stat(0, 0, maximum stat) == 80)
#  set hero level(0 ,18)
#  assert(get hero stat(0, 0, maximum stat) == 85)
#  set hero level(0 ,19)
#  assert(get hero stat(0, 0, maximum stat) == 91)
  set hero level(0 ,20)
  assert(get hero stat(0, 0, maximum stat) == 96)
#  set hero level(0 ,21)
#  assert(get hero stat(0, 0, maximum stat) == 102)
#  set hero level(0 ,22)
#  assert(get hero stat(0, 0, maximum stat) == 108)
#  set hero level(0 ,23)
#  assert(get hero stat(0, 0, maximum stat) == 114)
#  set hero level(0 ,24)
#  assert(get hero stat(0, 0, maximum stat) == 121)
#  set hero level(0 ,25)
#  assert(get hero stat(0, 0, maximum stat) == 127)
#  set hero level(0 ,26)
#  assert(get hero stat(0, 0, maximum stat) == 134)
#  set hero level(0 ,27)
#  assert(get hero stat(0, 0, maximum stat) == 140)
#  set hero level(0 ,28)
#  assert(get hero stat(0, 0, maximum stat) == 147)
#  set hero level(0 ,29)
#  assert(get hero stat(0, 0, maximum stat) == 154)
  set hero level(0 ,30)
  assert(get hero stat(0, 0, maximum stat) == 161)
#  set hero level(0 ,31)
#  assert(get hero stat(0, 0, maximum stat) == 168)
#  set hero level(0 ,32)
#  assert(get hero stat(0, 0, maximum stat) == 176)
#  set hero level(0 ,33)
#  assert(get hero stat(0, 0, maximum stat) == 183)
#  set hero level(0 ,34)
#  assert(get hero stat(0, 0, maximum stat) == 191)
#  set hero level(0 ,35)
#  assert(get hero stat(0, 0, maximum stat) == 199)
#  set hero level(0 ,36)
#  assert(get hero stat(0, 0, maximum stat) == 207)
#  set hero level(0 ,37)
#  assert(get hero stat(0, 0, maximum stat) == 215)
#  set hero level(0 ,38)
#  assert(get hero stat(0, 0, maximum stat) == 223)
#  set hero level(0 ,39)
#  assert(get hero stat(0, 0, maximum stat) == 232)
  set hero level(0 ,40)
  assert(get hero stat(0, 0, maximum stat) == 240)
#  set hero level(0 ,41)
#  assert(get hero stat(0, 0, maximum stat) == 249)
#  set hero level(0 ,42)
#  assert(get hero stat(0, 0, maximum stat) == 258)
#  set hero level(0 ,43)
#  assert(get hero stat(0, 0, maximum stat) == 267)
#  set hero level(0 ,44)
#  assert(get hero stat(0, 0, maximum stat) == 276)
#  set hero level(0 ,45)
#  assert(get hero stat(0, 0, maximum stat) == 285)
#  set hero level(0 ,46)
#  assert(get hero stat(0, 0, maximum stat) == 295)
#  set hero level(0 ,47)
#  assert(get hero stat(0, 0, maximum stat) == 304)
#  set hero level(0 ,48)
#  assert(get hero stat(0, 0, maximum stat) == 314)
#  set hero level(0 ,49)
#  assert(get hero stat(0, 0, maximum stat) == 324)
  set hero level(0 ,50)
  assert(get hero stat(0, 0, maximum stat) == 334)
#  set hero level(0 ,51)
#  assert(get hero stat(0, 0, maximum stat) == 344)
#  set hero level(0 ,52)
#  assert(get hero stat(0, 0, maximum stat) == 354)
#  set hero level(0 ,53)
#  assert(get hero stat(0, 0, maximum stat) == 365)
#  set hero level(0 ,54)
#  assert(get hero stat(0, 0, maximum stat) == 375)
#  set hero level(0 ,55)
#  assert(get hero stat(0, 0, maximum stat) == 386)
#  set hero level(0 ,56)
#  assert(get hero stat(0, 0, maximum stat) == 397)
#  set hero level(0 ,57)
#  assert(get hero stat(0, 0, maximum stat) == 408)
#  set hero level(0 ,58)
#  assert(get hero stat(0, 0, maximum stat) == 419)
#  set hero level(0 ,59)
#  assert(get hero stat(0, 0, maximum stat) == 430)
  set hero level(0 ,60)
  assert(get hero stat(0, 0, maximum stat) == 442)
#  set hero level(0 ,61)
#  assert(get hero stat(0, 0, maximum stat) == 453)
#  set hero level(0 ,62)
#  assert(get hero stat(0, 0, maximum stat) == 465)
#  set hero level(0 ,63)
#  assert(get hero stat(0, 0, maximum stat) == 477)
#  set hero level(0 ,64)
#  assert(get hero stat(0, 0, maximum stat) == 489)
#  set hero level(0 ,65)
#  assert(get hero stat(0, 0, maximum stat) == 501)
#  set hero level(0 ,66)
#  assert(get hero stat(0, 0, maximum stat) == 513)
#  set hero level(0 ,67)
#  assert(get hero stat(0, 0, maximum stat) == 526)
#  set hero level(0 ,68)
#  assert(get hero stat(0, 0, maximum stat) == 538)
#  set hero level(0 ,69)
#  assert(get hero stat(0, 0, maximum stat) == 551)
  set hero level(0 ,70)
  assert(get hero stat(0, 0, maximum stat) == 564)
#  set hero level(0 ,71)
#  assert(get hero stat(0, 0, maximum stat) == 577)
#  set hero level(0 ,72)
#  assert(get hero stat(0, 0, maximum stat) == 590)
#  set hero level(0 ,73)
#  assert(get hero stat(0, 0, maximum stat) == 604)
#  set hero level(0 ,74)
#  assert(get hero stat(0, 0, maximum stat) == 617)
#  set hero level(0 ,75)
#  assert(get hero stat(0, 0, maximum stat) == 631)
#  set hero level(0 ,76)
#  assert(get hero stat(0, 0, maximum stat) == 644)
#  set hero level(0 ,77)
#  assert(get hero stat(0, 0, maximum stat) == 658)
#  set hero level(0 ,78)
#  assert(get hero stat(0, 0, maximum stat) == 672)
#  set hero level(0 ,79)
#  assert(get hero stat(0, 0, maximum stat) == 686)
  set hero level(0 ,80)
  assert(get hero stat(0, 0, maximum stat) == 701)
#  set hero level(0 ,81)
#  assert(get hero stat(0, 0, maximum stat) == 715)
#  set hero level(0 ,82)
#  assert(get hero stat(0, 0, maximum stat) == 730)
#  set hero level(0 ,83)
#  assert(get hero stat(0, 0, maximum stat) == 745)
#  set hero level(0 ,84)
#  assert(get hero stat(0, 0, maximum stat) == 759)
#  set hero level(0 ,85)
#  assert(get hero stat(0, 0, maximum stat) == 774)
#  set hero level(0 ,86)
#  assert(get hero stat(0, 0, maximum stat) == 790)
#  set hero level(0 ,87)
#  assert(get hero stat(0, 0, maximum stat) == 805)
#  set hero level(0 ,88)
#  assert(get hero stat(0, 0, maximum stat) == 820)
#  set hero level(0 ,89)
#  assert(get hero stat(0, 0, maximum stat) == 836)
  set hero level(0 ,90)
  assert(get hero stat(0, 0, maximum stat) == 852)
#  set hero level(0 ,91)
#  assert(get hero stat(0, 0, maximum stat) == 868)
#  set hero level(0 ,92)
#  assert(get hero stat(0, 0, maximum stat) == 884)
#  set hero level(0 ,93)
#  assert(get hero stat(0, 0, maximum stat) == 900)
#  set hero level(0 ,94)
#  assert(get hero stat(0, 0, maximum stat) == 916)
#  set hero level(0 ,95)
#  assert(get hero stat(0, 0, maximum stat) == 933)
#  set hero level(0 ,96)
#  assert(get hero stat(0, 0, maximum stat) == 949)
#  set hero level(0 ,97)
#  assert(get hero stat(0, 0, maximum stat) == 966)
  set hero level(0 ,98)
  assert(get hero stat(0, 0, maximum stat) == 983)
  set hero level(0 ,99)
  assert(get hero stat(0, 0, maximum stat) == 1000)
  set hero level(0, 0, true)
  show no value, w
end

script, restore hp and mp, begin
  variable(i)
  for (i, 0, 40) do (
    if (hero by slot(i) <> -1) then (
      set hero stat(i, stat:hp, get hero stat(i, stat:hp, maximum stat), current stat)
      set hero stat(i, stat:mp, get hero stat(i, stat:mp, maximum stat), current stat)
    )
  )
end

script, hero stat tests, begin
  $0="hero stat tests"
  # test hero stat commands, stat capping, equip stat bonuses, current vs. max quirks, negative stats, level mp

  # check initial condition
  assert(hero by slot(1) == hero:Helga)
  assert(get hero level(1) == 0)
  assert(get hero stat(1, stat:hp, current stat) == 10)
  assert(get hero stat(1, stat:mp, current stat) == 20)
  assert(get hero stat(1, stat:atk, current stat) == 10)
  assert(get hero stat(1, stat:spd, current stat) == 30)
  assert(get hero stat(1, stat:hp, base stat) == 10)
  assert(get hero stat(1, stat:mp, base stat) == 20)
  assert(get hero stat(1, stat:atk, base stat) == 9)  # default weapon has +1 atk
  assert(get hero stat(1, stat:spd, base stat) == 30)

  # test stat caps
  assert(get hero stat cap(stat:hp) == 0)
  assert(get hero stat cap(stat:mp) == 0)
  assert(get hero stat cap(stat:atk) == 100)  #in-editor
  assert(get hero stat cap(stat:spd) == 0)
  # set capped hero stat
  set capped hero stat(1, stat:atk, 110, current stat)
  assert(get hero stat(1, stat:atk, current stat) == 100)
  assert(get hero stat(1, stat:atk, base stat) == 9)
  set capped hero stat(1, stat:atk, 120, maximum stat)
  assert(get hero stat(1, stat:atk, maximum stat) == 100)
  assert(get hero stat(1, stat:atk, base stat) == 99)
  set capped hero stat(1, stat:hp, 999999, current stat)
  assert(get hero stat(1, stat:hp, current stat) == 999999)
  set hero stat(1, stat:hp, 10, current stat)  #back to original

  # set hero stat should not be affected by caps
  set hero stat(1, stat:atk, 110, current stat)
  assert(get hero stat(1, stat:atk, current stat) == 110)

  # modify caps  
  set hero stat cap(stat:hp, 5)
  set hero stat cap(stat:mp, 12)
  assert(get hero stat cap(stat:hp) == 5)
  assert(get hero stat(1, stat:hp, current stat) == 5)
  assert(get hero stat(1, stat:hp, maximum stat) == 5)
  assert(get hero stat(1, stat:hp, base stat) == 10)
  assert(get hero stat(1, stat:mp, current stat) == 12)
  assert(get hero stat(1, stat:mp, maximum stat) == 12)
  assert(get hero stat(1, stat:mp, base stat) == 20)

  # set base stat
  set hero stat(1, stat:atk, 20, base stat)
  assert(get hero stat(1, stat:atk, maximum stat) == 21)  #+1 from weapon
  set hero stat(1, stat:atk, 200, base stat)
  assert(get hero stat(1, stat:atk, base stat) == 200)
  assert(get hero stat(1, stat:atk, maximum stat) == 100)  #capped
  set hero stat(1, stat:atk, 10, current stat)  #back to original
  set hero stat(1, stat:atk, 10, maximum stat)  #original max stat but not base stat!
  assert(get hero stat(1, stat:atk, base stat) == 110)
  set hero stat(1, stat:atk, 9, base stat)  #back to original max and base
  assert(get hero stat(1, stat:atk, maximum stat) == 10)

  set hero stat cap(stat:atk, 0)
  set hero stat cap(stat:spd, 10)
  assert(get hero stat cap(stat:atk) == 0)
  assert(get hero stat cap(stat:spd) == 10)
  assert(get hero stat(1, stat:atk, current stat) == 10)
  assert(get hero stat(1, stat:atk, maximum stat) == 10)
  assert(get hero stat(1, stat:spd, current stat) == 10)
  assert(get hero stat(1, stat:spd, maximum stat) == 10)
  set hero stat cap(stat:hp, 0)
  set hero stat cap(stat:mp, 0)
  set hero stat cap(stat:spd, 0)

  # gain hero stat
  assert(gain hero stat(1, stat:atk, 3) == 13)  #return new maximum
  assert(get hero stat(1, stat:atk, base stat) == 12)
  assert(get hero stat(1, stat:atk, maximum stat) == 13)
  assert(get hero stat(1, stat:atk, current stat) == 13)
  # Only max and current should respect caps
  set hero stat cap(stat:atk, 11)
  assert(gain hero stat(1, stat:atk, 4) == 11)  #return new maximum
  assert(get hero stat(1, stat:atk, base stat) == 16)
  assert(get hero stat(1, stat:atk, maximum stat) == 11)
  assert(get hero stat(1, stat:atk, current stat) == 11)
  gain hero stat(1, stat:atk, -7)  #back to original
  set hero stat cap(stat:atk, 0)
  # Test proportional increase
  assert(get hero stat(1, stat:hp, current stat) == 5)  # HP: 5/10
  assert(gain hero stat(1, stat:hp, 6) == 16)
  assert(get hero stat(1, stat:hp, current stat) == 8)  # HP: 8/16
  assert(get hero stat(1, stat:hp, maximum stat) == 16)
  assert(gain hero stat(1, stat:hp, -7) == 9)
  # Test reset-to-max
  assert(gain hero stat(1, stat:hp, 1, true) == 10)
  assert(get hero stat(1, stat:hp, current stat) == 10)  # HP: 10/10
  assert(get hero stat(1, stat:hp, maximum stat) == 10)

  # test original stats restored
  assert(get hero stat(1, stat:hp, maximum stat) == 10)
  assert(get hero stat(1, stat:hp, base stat) == 10)
  assert(get hero stat(1, stat:mp, maximum stat) == 20)
  assert(get hero stat(1, stat:mp, base stat) == 20)
  assert(get hero stat(1, stat:atk, current stat) == 10)
  assert(get hero stat(1, stat:atk, maximum stat) == 10)
  assert(get hero stat(1, stat:atk, base stat) == 9)
  assert(get hero stat(1, stat:spd, current stat) == 30)
  assert(get hero stat(1, stat:spd, maximum stat) == 30)
  assert(get hero stat(1, stat:spd, base stat) == 30)
  # but not current values for hp, mp
  assert(get hero stat(1, stat:hp, current stat) == 10)
  assert(get hero stat(1, stat:mp, current stat) == 12)

  # Simple test of hero level mp
  # The hero is level 0. Other levels are tested in "hero stat growth test"
  assert(get level mp(1, 0, current stat) == 1)
  assert(get level mp(1, 0, maximum stat) == 1)
  variable(level)
  for(level, 1, 7) do (
    assert(get level mp(1, level, current stat) == 0)
    assert(get level mp(1, level, maximum stat) == 0)
  )
  set level mp(1, 7, 99)
  assert(get level mp(1, 7) == 99)
  set level mp(1, 7, 0)

  # Check for valid level MP spell lists
  assert(hero uses level mp(find hero(hero:Frumpy)) == true) # has valid Level MP spell list
  assert(hero uses level mp(find hero(hero:Olaf)) == false)  # has Level MP spell list, but it is not in the battle menu
  assert(hero uses level mp(find hero(hero:Freki)) == false) # has other spell list, but not level mp based
  assert(hero uses level mp(find hero(hero:Helga)) == false) # Has no spell lists

  # test heal party
  assert(get hero stat(1, stat:hp, current stat) == 10)  # Precondition
  assert(read preference bit(4) == false)  # Precondition: "Inns don't revive dead heroes" off
  set level mp(1, 0, 0)
  heal party
  assert(get hero stat(1, stat:mp, current stat) == 20)
  assert(get level mp(1, 0, current stat) == 1)
  set hero stat(1, stat:hp, 0, current stat)
  heal party(false)   # Don't revive heroes
  assert(get hero stat(1, stat:hp, current stat) == 0)
  heal party(true)   # Revive heroes
  assert(get hero stat(1, stat:hp, current stat) == 10)
  set hero stat(1, stat:hp, 0, current stat)
  heal party     # Default: Revive heroes, because of the above pref bit
  assert(get hero stat(1, stat:hp, current stat) == 10)
end

script, equipment tests, begin
  $0="equipment tests"
  # This is a continuation of 'hero stat tests', see that for preconditions
  # additional precondition: empty inventory, as well as:
  assert(find hero(hero:Freki) == 0)
  assert(find hero(hero:Helga) == 1)

  # test equipment commands
  get item(item:Boots)
  assert(inventory(item:Boots) == 1)
  assert(equip where(1, item:Boots) == slot:legs)  # equippable by all heroes
  assert(check equipment(1, slot:legs) == -1)
  assert(check equipment(1, slot:weapon) == item:DefltWep)  # default weapon  (not properly tested)
  assert(get default weapon(1) == item:DefltWep)

  force equip(1, slot:legs, item:Boots)
  # stat bonuses: spd+2 hp+3
  assert(check equipment(1, slot:legs) == item:Boots)
  assert(inventory(item:Boots) == 0)
  # test stat bonuses
  assert(get hero stat(1, stat:hp, current stat) == 10)
  assert(get hero stat(1, stat:hp, maximum stat) == 13)  # only affects max values of hp, mp
  assert(get hero stat(1, stat:hp, base stat) == 10)
  assert(get hero stat(1, stat:spd, current stat) == 32)
  assert(get hero stat(1, stat:spd, maximum stat) == 32) # resets current value to max for other stats
  assert(get hero stat(1, stat:spd, base stat) == 30)

  assert(inventory(item:SteelSho) == 0)
  assert(equip where(0, item:SteelSho) == slot:legs)  #equippable only by Freki
  assert(equip where(1, item:SteelSho) == false)

  # Note that force equip can be used to equip an item not in the inventory,
  # in that case you get a free copy.
  # Force equip also allows equipping in wrong slot
  force equip(1, slot:weapon, item:SteelSho)
  # stat bonuses: atk+4  (default weapon has atk+1)
  assert(check equipment(1, slot:weapon) == item:SteelSho)
  assert(check equipment(1, slot:legs) == item:Boots)
  assert(inventory(item:DefltWep) == 0)  # shouldn't gain a default weapon
  assert(get hero stat(1, stat:atk, current stat) == 13)
  assert(get hero stat(1, stat:atk, maximum stat) == 13)

  # test unequipping
  unequip(1, slot:legs)
  assert(inventory(item:Boots) == 1)
  assert(check equipment(1, slot:legs) == -1)

  set hero stat(1, stat:hp, 29, current stat)  # check effects of unequipping... even on stats
  set hero stat(1, stat:spd, 29, current stat) # which the equipment doesn't have bonuses on

  unequip(1, slot:weapon)
  assert(inventory(item:SteelSho) == 1)
  assert(check equipment(1, slot:weapon) == item:DefltWep)

  assert(get hero stat(1, stat:hp, current stat) == 10)  # current values of hp,mp should be capped to max
  assert(get hero stat(1, stat:hp, maximum stat) == 10)
  assert(get hero stat(1, stat:spd, current stat) == 30)
  assert(get hero stat(1, stat:spd, maximum stat) == 30) # resets current value to max for other stats

  # TODO: test equip elemental resists

  # test for Bug 743 - Unequipping items with stat bonuses that exceed stat limits is broken
  assert(get hero stat(1, stat:hp, current stat) == 10)  # preconditions (max same)
  assert(get hero stat(1, stat:spd, current stat) == 30)
  set hero stat cap(stat:hp, 5)
  set hero stat cap(stat:spd, 5)
  force equip(1, slot:legs, item:Boots)
  assert(get hero stat(1, stat:hp, current stat) == 5)  # cap hp, mp to the cap
  assert(get hero stat(1, stat:hp, maximum stat) == 5)
  assert(get hero stat(1, stat:spd, current stat) == 5) # resets current value to max for other stats
  assert(get hero stat(1, stat:spd, maximum stat) == 5)
  unequip(1, slot:legs)
  assert(get hero stat(1, stat:hp, current stat) == 5)
  assert(get hero stat(1, stat:hp, maximum stat) == 5)
  assert(get hero stat(1, stat:spd, current stat) == 5)
  assert(get hero stat(1, stat:spd, maximum stat) == 5)
  set hero stat cap(stat:hp, 0)
  set hero stat cap(stat:spd, 0)
  assert(get hero stat(1, stat:hp, current stat) == 5)  # only affects max values of hp, mp
  assert(get hero stat(1, stat:hp, maximum stat) == 10)
  assert(get hero stat(1, stat:spd, current stat) == 30)  # resets current value to max for other stats
  assert(get hero stat(1, stat:spd, maximum stat) == 30)
  # ...fix that up
  restore hp and mp
  assert(get hero stat(1, stat:hp, current stat) == 10)

  # cleanup
  delete item(item:SteelSho)
  delete item(item:Boots)
end

script, hero misc tests, begin
  $0="hero misc tests"
  show string(0), w
  variable(n)
  assert(hero by slot(0) == hero:Freki)
  assert(get hero hand x(0, hand:attack A) == 22)
  assert(get hero hand y(0, hand:attack A) == 11)
  assert(get hero hand x(0, hand:attack B) == 2)
  assert(get hero hand y(0, hand:attack B) == 17)
  set hero hand x(0, hand:attack A, 50)
  set hero hand y(0, hand:attack A, -25)
  set hero hand x(0, hand:attack B, -10)
  set hero hand y(0, hand:attack B, 1)
  assert(get hero hand x(0, hand:attack A) == 50)
  assert(get hero hand y(0, hand:attack A) == -25)
  assert(get hero hand x(0, hand:attack B) == -10)
  assert(get hero hand y(0, hand:attack B) == 1)
  set hero hand x(0, hand:attack A, get default hero hand x(0, hand:attack A))
  set hero hand y(0, hand:attack A, get default hero hand y(0, hand:attack A))
  set hero hand x(0, hand:attack B, get default hero hand x(0, hand:attack B))
  set hero hand y(0, hand:attack B, get default hero hand y(0, hand:attack B))
  assert(get hero hand x(0, hand:attack A) == 22)
  assert(get hero hand y(0, hand:attack A) == 11)
  assert(get hero hand x(0, hand:attack B) == 2)
  assert(get hero hand y(0, hand:attack B) == 17)
  
end

########################################################################

script, npc tests, begin
  npc reference tests
  npc movement tests(1)
  npc movement tests(NPC reference(1))
  npc id tests
  npc extra tests
  npc tag and onetime tests
  use npc script tests
  alter npc tests
  npc slice tests
end

script, npc movement tests, n, begin
  $0="npc movement tests"
  $err arg string="n=", append number(err arg string, n)
  show string(0)
  
  # assert starting pos
  assert(npc X(n) == 3)
  assert(npc Y(n) == 5)
  # walk in a circle, testing speed
  set npc speed(n, 4)
  walk npc(n, south, 1), w
  wait for npc(n),w
  assert(npc Y(n) == 6)
  set NPC speed(n, 5)
  walk npc(n, east, 1), w
  wait for npc(n),w
  assert(npc X(n) == 4)
  set NPC speed(n, 10)
  walk npc(n, north, 1), w
  wait for npc(n),w
  assert(npc Y(n) == 5)
  set NPC speed(n, 2)
  walk npc(n, west, 1), w
  wait for npc(n),w
  assert(npc X(n) == 3)
  set npc speed(n, 4)
  
  # Walk backwards
  walk npc(n, east, -1)
  wait for npc(n), w
  assert(npc X(n) == 2)
  assert(npc direction(n) == east)
  walk npc(n, south, -1)
  wait for npc(n), w
  assert(npc Y(n) == 4)
  assert(npc direction(n) == south)
  
  set npc position(n, 3, 5)
  
  show no value
  $err arg string=""
end

script, npc reference tests, begin
  variable(n)
  assert(npc copy count(1) == 1)
  assert(npc copy count(2) == 0)
  assert(npc copy count(4) == 3)

  n := npc reference(1)
  assert(n < 0)
  assert(get npc id(n) == 1)
  assert(npc copy number(n) == 0)

  n := NPC reference(4, 1)
  assert(get npc id(n) == 4)
  assert(npc copy number(n) == 1)
  assert(npc x(n) == 2)
  assert(npc y(n) == 13)

  n := NPC reference(4, 3)
  assert(n == 0)  # Doesn't exist

  # Passing an ID should return the ID, copy 0
  assert(get npc id(1) == 1)
  assert(npc copy number(1) == 0)
  assert(get npc id(2) == -1)  # Doesn't exist
  assert(npc copy number(2) == -1)  # Doesn't exist
  assert(get npc id(3) == 3)
  assert(npc copy number(3) == 0)
  # Invalid IDs or references
  assert(get npc id(9000) == -1)
  assert(get npc id(-9000) == -1)
  assert(npc copy number(9000) == -1)
  assert(npc copy number(-9000) == -1)
  assert(npc copy count(9000) == 0)
  assert(npc copy count(-9000) == 0)
end

script, npc id tests, begin
  variable(n)
  n := NPC reference(1)
  set npc direction(n, south), w
  assert(get npc id(n) == 1)
  change npc id(n, 2), w(4)
  assert(get npc id(n) == 2)
  change npc id(n, 1), w
end

script, npc extra tests, begin
  $0="npc extra tests"

  variable(npc)
  npc := npc reference(4)
  assert(extra length(npc) == 3)
  assert(npc extra(npc, 0) == 0)
  assert(npc extra(npc, 2) == 0)
  set npc extra(npc, -1, 999)
  assert(npc extra(npc, 2) == 999)
  assert(get extra(npc, 2) == 999)
  append extra(npc, 1000)
  assert(get extra(npc, 3) == 1000)
end

script, npc tag and onetime tests, begin
  $0="npc onetime tests"
  variable(hx, hy, n)
  # Moving the hero in this script is a little silly,
  # but ensures that the onetime NPCs are on-screen
  # ...which they probably would have been anyway
  hx := hero x(me)
  hy := hero y(me)
  walk hero to y(me, 12)
  wait for hero(me), w
  walk hero to x(me, 4)
  wait for hero(me), w
  
  assert(check onetime(3) == false)
  assert(check onetime(4) == false)
  use npc(3), w
  assert(check onetime(3) == true)
  assert(check onetime(4) == false)
  use npc(4), w
  assert(check onetime(3) == true)
  assert(check onetime(4) == true)
  w
  n := npc reference(4)
  assert(n == false)
  set onetime(4, false), w
  n := npc reference(4)
  assert(n <> false)
  
  assert(check tag(10000) == false)
  w
  n := npc reference(5)
  assert(n <> false)
  set tag(10000, true)
  w
  n := npc reference(5)
  assert(n == false)
  assert(check tag(10000) == true)
  
  walk hero to x(me, hx)
  wait for hero(me), w
  walk hero to y(me, hy)
  wait for hero(me), w
end

plotscript, another npc script, begin
  assert(ticknumber == start tick + 2)
  npc script triggered := 1
end

script, use npc script tests, begin
  $0="use npc script tests"
  # Tests the implicit wait behaviour of usenpc

  start tick := ticknumber

  create npc(8)  # Does nothing
  use npc(8)
  assert(ticknumber == start tick + 1)
  delete npc(8)

  # Both this script and "another npc script" should wait
  create npc(7)  # Calls "another npc script"
  npc script triggered := 0
  use npc(7)
  assert(npc script triggered == 1)
  assert(ticknumber == start tick + 2)
  delete npc(7)
end

script, npc slice tests, begin
  $0="npc slice tests"
  # Test updating of NPC slices: position and dir/frame (including walk in place animation)
  # Note that there's lots of tests for npc slices elsewhere too,
  # in particular sprite slice spritesets are tested in "global npc tests".

  variable(n1, sl1, spr1)
  n1 := create npc(9, 10, 8, west)

  # Check that walkabout slice positions are set immediately
  sl1 := get npc slice(n1)
  assert(slice x(sl1) == 10 * 20 && slice y(sl1) == 8 * 20)
  set NPC position(n1, 3, 4)
  assert(slice x(sl1) == 3 * 20 && slice y(sl1) == 4 * 20)
  put NPC(n1, 31, 41)
  assert(slice x(sl1) == 31 && slice y(sl1) == 41)

  spr1 := get npc sprite(n1)

  # Testing variable sprite frames. Change to a set with frame group sizes: 0, 1, 2, 3, 0, 6, 1

  alter npc(n1, npcstat:picture, 18)
  assert(read npc(n1, npcstat:picture) == 18)
  # Changes immediately, doesn't recreate the slice
  assert(spr1 == get npc sprite(n1))
  assert(get spriteset number(spr1) == 18)

  # Change directions and check setting/getting frame
  set npc direction(n1, down)
  w
  set npc direction(n1, up)
  w
  # Doesn't have any up frames, so down still displayed
  assert(get sprite frameid(spr1) == 200)  # down 0
  variable(dir, fr, numframes)
  for (dir, 0, 3) do (
    set npc direction(n1, dir)
    numframes := sprite frame group size(spr1, dir)
    assert(numframes == dir)

    # Doesn't have any up frames, but still should be able to set the direction and set frame to 0
    if (numframes == 0) then (numframes := 1)
    for(fr, 0, numframes -- 1) do(
      set npc direction(n1, dir)
      assert(npc direction(n1) == dir)
      set npc frame(n1, fr)
      assert(npc frame(n1) == fr)
      # slice frame is not yet changed immediately (TODO), so wait
      w
      if (dir <> up) then (
        assert(get sprite frameid(spr1) == 100 * dir + fr)
      )
    )
    set npc frame(n1, -1)
    assert(npc frame(n1) == 0)
    set npc frame(n1, numframes)
    assert(npc frame(n1) == numframes -- 1)  # Not too important

    w
  )

  # NPC 9 is Walk in Place. Check the animation cycles through the sprite frames, for each of the 4 directions
  variable(firstframe, expected, expectedid, wtog, tick, ticks per frame)
  ticks per frame := 2  # 18fps
  set sprite frameid(spr1, 500)  # initial
  for (dir, 0, 3) do (
    numframes := sprite frame group size(spr1, dir)
    firstframe := 1  # "set npc frame" frame number to start on
    if (numframes == 0) then (firstframe := 0)
    elseif (firstframe >= numframes) then (firstframe := numframes -- 1)

    set npc direction(n1, dir)
    set npc frame(n1, firstframe)
    assert(npc frame(n1) == firstframe)
    # setnpcframe sets wtog = 2*firstframe
    wtog := ticks per frame * firstframe

    for (tick, 1, 2*(numframes + 1)) do (
      # Again, need to wait before the sprite frame updates
      w(1)
      wtog += 1
      # Expected frame and frameid
      if (dir == 0) then (
        expected := 0
        expectedid := 500  # Has no up frames, so should preserve frameid from "initial"
      ) else (
        expected := (wtog / ticks per frame), mod, numframes
        expectedid := 100 * dir + expected
      )
      #tracevalue(dir, expected, expectedid, npc frame(n1), get sprite frameid(spr1))
      assert (npc frame(n1) == expected)
      assert (get sprite frameid(spr1) == expectedid)
    )
  )

  delete npc(n1)
end

script, alter npc tests, begin
  $0="alter npc tests"
  
  assert(current map == 0)
  w
  
  # make a pair of NPCs to use in these tests
  variable(n1, n2)
  n1 := create npc(9, 5, 5, west),w
  n2 := create npc(9, 11, 5, east),w
  # First do picture and palette changes on the first instance
  # and verify the changes on the second instance
  assert(get npc sprite id(n2) == 13)
  assert(get npc sprite palette(n2) == -1)
  alter npc(n1, NPCStat:picture, 12),w
  assert(get npc sprite id(n2) == 12)
  alter npc(n1, NPCStat:palette, 5),w
  assert(get npc sprite palette(n2) == 5)
  alter npc(n1, NPCStat:picture, 13),w
  alter npc(n1, NPCStat:palette, -1),w

  # Test move speed
  variable(ox, oy)
  assert(read npc(n1, NPCStat:move speed) == 0)
  alter npc(n1, NPCStat:move speed, 1), w
  ox := npc pixel x(n2)
  oy := npc pixel y(n2)
  walk npc(n2, east, 1)
  w(10)
  assert(npc pixel x(n2) == ox + 10)
  w(10)
  assert(npc pixel x(n2) == ox + 20)
  alter npc(n1, NPCStat:move speed, 2),w
  walk npc(n2, south, 1)
  w(1)
  assert(npc pixel y(n2) == oy + 2)
  wait for npc(n2)
  assert(npc pixel y(n2) == oy + 20)
  alter npc(n1, NPCStat:move speed, 4),w
  walk npc(n2, west, 1)
  w(1)
  assert(npc pixel x(n2) == ox + 16)
  wait for npc(n2)
  alter npc(n1, NPCStat:move speed, 5),w
  walk npc(n2, north, 1)
  w(1)
  assert(npc pixel y(n2) == oy + 15)
  wait for npc(n2)
  # Has gone in a complete circle
  assert(npc pixel x(n2) == ox)
  assert(npc pixel y(n2) == oy)
  # How about diagonal next?
  alter npc(n1, NPCStat:move speed, 10),w
  walk npc(n2, right, 1)
  walk npc(n2, down, 1)
  w(1)
  assert(npc pixel x(n2) == ox + 10)
  assert(npc pixel y(n2) == oy + 10)
  w(1)
  assert(npc pixel x(n2) == ox + 20)
  assert(npc pixel y(n2) == oy + 20)
  alter npc(n1, NPCStat:move speed, 20)
  walk npc(n2, left, 1)
  walk npc(n2, up, 1)
  w(1)
  assert(npc pixel x(n2) == ox + 0)
  assert(npc pixel y(n2) == oy + 0)
  alter npc(n1, NPCStat:move speed, 0)
  
  # Now test textbox display
  assert(read npc(n1, NPCStat:display text) == 0)
  alter npc(n1, NPCStat:display text, 18),w
  assert(current text box == -1)
  use npc(n2), w
  assert(current text box == 18)
  advance text box, w
  assert(current text box == -1)

  # Test touch and step-on activation
  # We will use a text box for this test
  alter npc(n1, NPCStat:display text, 18),w
  assert(current text box == -1)
  assert(read npc(n1, NPCstat:activation) == NPCactivation:use)
  # Move the hero adjacent to the npc
  walk hero to x(me, npc x(n2)),w
  wait for hero(me),w
  assert(hero x(me) == npc x(n2))
  assert(hero y(me) == npc y(n2) + 1)
  assert(current text box == -1)
  # Try to walking into the npc, nothing should happen
  walk hero(me, north, 1), w
  wait for hero(me),w
  assert(hero y(me) == npc y(n2) + 1)
  assert(current text box == -1)
  # Make the NPC touch-activated
  alter npc(n1, NPCstat:activation, NPCactivation:touch), w
  # Nothing should happen yet
  assert(current text box == -1)
  # Walk the hero into the NPC
  walk hero(me, north, 1), w
  wait for hero(me), w
  assert(current text box == 18)
  advance text box
  assert(current text box == -1)
  # Walk the hero next to the NPC (does nothing because the NPC isn't moving)
  walk hero(me, right, 1), w
  wait for hero(me), w
  assert(current text box == -1)
  walk hero(me, left, 1), w
  wait for hero(me), w
  assert(current text box == -1)
  # Now move the npc. Even when moving away into a tile one space diagonal, activation should happen
  alter npc(n1, NPCStat:move speed, 5)
  walk npc(n2, right, 1), w
  wait for npc(n2), w
  assert(current text box == 18)
  advance text box
  assert(current text box == -1)
  # Move one tile further. This should not activate
  walk npc(n2, right, 1), w
  wait for npc(n2), w
  assert(current text box == -1)
  # Walk back one tile, into the diagonal space. Will activate
  walk npc(n2, left, 1), w
  wait for npc(n2), w
  assert(current text box == 18)
  advance text box, w
  assert(current text box == -1)
  # Walk back one more space, into the adjacent tile, will activate again
  walk npc(n2, left, 1), w
  wait for npc(n2), w
  assert(current text box == 18)
  advance text box, w
  assert(current text box == -1)
  # Now try step-on activation
  alter npc(n1, NPCstat:activation, NPCactivation:stepon),w
  walk hero(me, north, 1), w
  assert(current text box == -1)
  wait for hero(me), w
  assert(current text box == 18)
  advance text box, w
  assert(current text box == -1)
  # Move the heroes around
  walk hero(me, north, 1), w
  wait for hero(me),w
  assert(current text box == -1)
  walk hero(me, right, 1), w
  wait for hero(me),w
  walk hero(me, down, 2), w
  wait for hero(me),w
  walk hero(me, left, 1), w
  wait for hero(me),w
  assert(hero x(me) == npc x(n2))
  assert(hero y(me) == npc y(n2) + 1)
  # Move the step-on npc under the hero (for better or for worse, this does not activate)
  assert(current text box == -1)
  walk npc(n2, down, 1), w
  assert(current text box == -1)
  wait for npc(n2), w
  assert(current text box == -1)
  # Walk the npc back and reset its activation
  walk npc(n2, up, 1), w
  wait for npc(n2), w
  assert(current text box == -1)
  alter npc(n1, NPCstat:activation, NPCactivation:use), w
  assert(read npc(n1, NPCstat:activation) == NPCactivation:use)
  alter npc(n1, NPCstat:display text, 0)

  #_cancel runfast

  #wait for key
  #_runfast
  
  # Test getting items
  assert(read npc(n1, NPCstat:give item) == 0)
  alter npc(n1, NPCstat:give item, item:Whatsit + 1), w
  assert(read npc(n1, NPCstat:give item) == item:Whatsit + 1)
  # There should not be any Whatsits in your inventory yet
  assert(inventory(item:Whatsit) == 0)
  # Now get one
  use npc(n2),w
  assert(inventory(item:Whatsit) == 1)
  # Get a few more
  use npc(n2),w
  use npc(n2),w
  use npc(n2),w
  assert(inventory(item:Whatsit) == 4)
  # Now clear the get item
  alter npc(n1, NPCstat:give item, 0), w
  # This should not give the item
  use npc(n2),w
  assert(inventory(item:Whatsit) == 4)
  
  ### still needs tests:
  # NPCstat:move type
  # NPCstat:when activated
  # NPCstat:pushability
  # NPCstat:script
  # NPCstat:script argument
  # NPCstat:vehicle
  # NPCstat:default movement zone
  # NPCstat:default avoidance zone
  # NPCstat:ignore passmap
  
  while (npc copy count(9) > 0) do(
    delete npc(9),w
  )

end


script, get npc sprite id, ref, begin
  variable(sl, spr)
  sl := get npc slice(ref)
  spr := lookup slice(sl:walkabout sprite component, sl)
  assert(get sprite type(spr) == spritetype:walkabout)
  exit returning(get sprite set number(spr))
end

script, get npc sprite palette, ref, begin
  variable(sl, spr)
  sl := get npc slice(ref)
  spr := lookup slice(sl:walkabout sprite component, sl)
  assert(get sprite type(spr) == spritetype:walkabout)
  exit returning(get sprite palette(spr))
end


########################################################################

script, death tests, begin
  $0="death tests"
  # See also "test harm tile death" below

  assert (get death script == 0)
  set death script(@on death script)
  assert (get death script == @on death script)

  assert(died == 0)
  expecting death := false

  # Setting hero stats to 0 should NOT trigger death
  variable (i)
  for (i, 0, 3) do (
    set hero stat(i, stat:hp, 0)
  )
  wait
  assert(died == 0)

  # Death by oob attack
  set hero stat(0, stat:hp, 10)
  # Only hero 0 alive
  expecting death := true
  map cure(atk:suicide, 0)
  assert(get hero stat(0, stat:hp) == 0)
  # Death script should not be triggered until next tick
  assert(died == 0)
  wait(1)
  assert(died == 1)

  # Re-trigger even if already dead
  map cure(atk:suicide, 0)
  assert(died == 1)
  wait(1)
  assert(died == 2)

  # Death shouldn't be triggered if one hero has 0 max hp
  variable(old maxhp)
  oldmaxhp := get hero stat(1, stat:hp, maximum stat)
  set hero stat(1, stat:hp, 0, maximum stat)
  map cure(atk:suicide, 0)
  wait(1)
  assert(died == 2)
  set hero stat(1, stat:hp, old maxhp, maximum stat)

  # Death shouldn't be triggered if heroes are healed before the next tick
  map cure(atk:suicide, 0)
  set hero stat(0, stat:hp, 1)
  wait(1)
  assert(died == 2)

  #FIXME: test death by battle

  expecting death := false
  died := 0
  restore hp and mp
end

script, on death script, begin
  $9="on death script"
  trace(9)
  if (expecting death) then (
    died += 1
    exit script
  )
  crash
end

script, test harm tile death, start hp, begin
  # Called from "harm tile tests"

  expecting death := true
  set harm tile damage(100)
  walk hero(me, right, 1)
  wait for hero(me)
  # The step finishes, causing death, then it will be processed the same tick, spawning the script
  assert(died == 1)
  assert hurt pattern(7, start hp, start hp, start hp, start hp, start hp)
  expecting death := false
  died := 0
end

script, global npc tests, begin
  $0="global npc tests"
  
  variable(oldmap, oldx, oldy)
  oldmap := current map
  oldx := hero x(me)
  oldy := hero y(me)
  teleport to map(8, 3, 3)
  w(1)
  
  variable(n1, n2)
  n2 := create npc(0, 10, 8, south, pool:global)
  n1 := create npc(0, 9, 8, south, pool:local)
  w(1)
  # Global NPC 0 and local NPC 0 will have different sprites
  assert(get sprite set number(get npc sprite(n1)) == 11) # sheep
  assert(get sprite set number(get npc sprite(n2)) == 15) # chewie
  # By ref works on both
  set npc direction(n1, left)
  set npc direction(n2, left)
  w(1)
  assert(npc direction(n1) == left)
  assert(npc direction(n2) == left)
  # by ID does the local ID
  set npc direction(0, right)
  w(1)
  assert(npc direction(n1) == right)
  assert(npc direction(n2) == left)
  
  teleport to map(9, 3, 2)
  w(1)
  n1 := npc reference(2)
  assert(n1 <> 0)
  assert(npc x(n1) == 4)
  assert(npc y(n1) == 3)
  assert(get sprite set number(get npc sprite(n1)) == 13)
  w(1)
  n1 := global npc reference(2)
  assert(n1 == global npc(2))  # test alias
  assert(n1 <> 0)
  assert(npc x(n1) == 11)
  assert(npc y(n1) == 3)
  assert(get sprite set number(get npc sprite(n1)) == 17)
  w(1)
  n1 := global npc reference(2, 2)
  assert(n1 <> 0)
  assert(npc x(n1) == 11)
  assert(npc y(n1) == 5)
  assert(get sprite set number(get npc sprite(n1)) == 17)
  w(1)
  
  n2 := create npc(2, 11, 6, left, pool:global)
  assert(npc copy count(2) == 3)
  assert(npc copy count(2, pool:global) == 4)
  w(1)
  
  # Change NPC ID and pool
  change npc id(n2, 1)
  # should still be global
  assert(get sprite set number(get npc sprite(n2)) == 16) # triangle
  w(1)
  # become local
  change npc id(n2, 0, pool:local)
  assert(get sprite set number(get npc sprite(n2)) == 13) # pup
  w(1)
  # back to a global again
  change npc id(n2, 0, pool:global)
  assert(get sprite set number(get npc sprite(n2)) == 15) # chewie
  w(1)
  
  # Test change npc id with ID numbers
  # Change the first local 2 into 3
  change npc id(2, 3)
  assert(npc y(2) == 4)
  w(1)
  # change the first global 2 into 1
  change npc id(2, 1, pool:global)
  assert(get sprite set number(get npc sprite(global npc reference(1))) == 16) # chewie
  w(1)
  # many NPC commands will only refer to local NPCs by ID, you need a refernce to worj with globals
  # No NPC should move, because local ID 1 does not exist, even though global ID 1 exists
  walk npc(1, left, 1)
  wait for npc(1)
  # Global NPC id 1 will not have moved
  assert(get npc id(npc at spot(11, 3)) == 1)
  w(1)
  
  # Test read npc
  assert(read npc(2, npcstat:picture, pool:local) == 13) # pup
  n1 := npc reference(2)
  assert(read npc(n1, npcstat:picture) == 13) # pup
  w(1)
  assert(read npc(2, npcstat:picture, pool:global) == 17) # snake
  n1 := global npc reference(2)
  assert(read npc(n1, npcstat:picture) == 17) # snake
  w(1)
  
  n2 := create global npc(0, 7, 5)
  assert(n2 < 0)
  assert(get sprite set number(get npc sprite(n2)) == 15) # chewie
  walk npc(n2, south, 1)
  wait for npc(n2)
  assert(npc y(n2) == 6)
  w(1)
  
  # Test alter NPC
  assert(read npc(0, npcstat:movespeed, pool:global) == 4)
  alter npc(0, npcstat:movespeed, 10, pool:global)
  assert(read npc(0, npcstat:movespeed, pool:global) == 10)
  w(1)
  # Alterations made to global NPC persist between map changes
  teleport to map(8, 3, 3)
  w(1)
  assert(npc copy count(0, pool:global) == 0)
  assert(read npc(0, npcstat:movespeed, pool:global) == 10)
  n1 := create global npc(0, 4, 4, south)
  assert(read npc(0, npcstat:movespeed, pool:global) == 10)
  walk npc(n1, south,1)
  wait for npc(n1)
  w(1)
  # And a local npc with the same ID is unaffected
  n2 := create npc(0, 5, 4, left)
  assert(read npc(0, npcstat:movespeed, pool:local) == 4)
  assert(read npc(n2, npcstat:movespeed) == 4)
  w(1)
  # change it into a global npc
  assert(get npc pool(n2) == pool:local)
  change npc id(n2, 0, pool:global)
  assert(get npc pool(n2) == pool:global)
  assert(read npc(n2, npcstat:movespeed) == 10)
  w(1)

  teleport to map(oldmap, oldx, oldy)
end

########################################################################

script, assert hurt pattern, test no, start hp, h0, h1, h2, h3, begin
  # h# is the amount of damage by which hero # in the walkabout party should be hurt
  # h# is ignored if # > number of heroes in walkabout party
  $0="assert hurt pattern, test no. "
  append number(0, test no)

  subscript, consider rank, rank, harm amount, begin
    if (hero by rank(rank) == -1) then (exit script)
    # This will break if there are duplicate heroes, a well known flaw
    variable(slot)
    slot := find hero(hero by rank(rank))
tracevalue(rank, slot, get hero stat(slot, stat:hp), start hp -- harm amount)
    assert(get hero stat(slot, stat:hp) == start hp -- harm amount)
    set hero stat(slot, stat:hp, start hp)
  end

  consider rank(0, h0)
  consider rank(1, h1)
  consider rank(2, h2)
  consider rank(3, h3)
end

script, harm tile tests, begin
  $0="harm tile tests"

  # Test harmtile damage
  walk hero to x(me, 7)
  walk hero to y(me, 6)
  wait for hero(me)

  variable(start hp)
  start hp := get hero stat(0, stat:hp, current stat)

  write map block(6, 6, 140, 1)
  write pass block(6, 6, harmtile)

  subscript, test damage patterns, begin
    set caterpillar mode(on)
    # Turn off "Harm tiles harm non-caterpillar party" (actually the current setting in the .rpg)
    write general(177, read general(177), and, (-1, xor, 2^12))
    # hero is now at tile 7,6, caterpillar is enabled and not suspended, and all heroes have 'start hp' hp

    # Test damage to individual heroes, caterpillar enabled
    # also testing crossing over harmtile without stopping
    walk hero(me, left, 1)
    wait for hero(me), _checkpoint   # check for flash
    assert hurt pattern(0, start hp, true, false, false, false)
    walk hero(me, left, 2)
    wait for hero(me), _checkpoint
    assert hurt pattern(1, start hp, false, true, true, false)

    # Test damage to individual heroes, caterpillar suspended
    suspend caterpillar
    walk hero(1, right, 1)  # step onto harm tile
    walk hero(2, down, 1)   # step off harm tile
    walk hero(3, left, 1)   # step onto harm tile
    wait for hero(2)
    assert hurt pattern(2, start hp, false, true, false, true)

    # Test damage to whole party, caterpillar disabled
    # (Leader walks back and forth over the tile)
    set caterpillar mode(off)
    walk hero(me, right, 3)  # two tiles onto harm, then one off
    wait for hero(me)
    # "Harm tiles harm non-caterpillar party" is off
    assert hurt pattern(3, start hp, true, false, false, false)
    # Turn on "Harm tiles harm non-caterpillar party"
    write general(177, read general(177), or, 2^12)
    walk hero(me, left, 2)
    wait for hero(me)
    assert hurt pattern(4, start hp, true, true, true, true)
    # Check this makes no difference
    resume caterpillar
    walk hero(me, right, 2)
    wait for hero(me)
    assert hurt pattern(5, start hp, true, true, true, true)
  end

  # Test with four heroes in the party
  trace($99="First harmtile test run")
  test damage patterns

  # Move hero back to 7,6
  set hero position(me, 11, 6)
  walk hero to x(me, 7)
  wait for hero(me)

  # Test party and walkabout slots not matching
  trace($99="Second harmtile test run")
  swap by position(0, 20) # remove leader
  test damage patterns

  # Test damage value
  set harm tile damage(2)
  walk hero(me, left, 2)
  wait for hero(me)
  assert hurt pattern(6, start hp, 2, 2, 2, 2)

  # Test death
  test harm tile death(start hp)

  # Cleanup
  write map block(6, 6, 0, 1)
  write pass block(6, 6, 0)
  swap by position(0, 20) # return leader
  set caterpillar mode(on)
  set harm tile damage(1)  # restore
  # Caterpiller on and resumed, all heroes have 'start hp' hp
end

########################################################################

script, menu tests, begin
  $0="menu tests"
  variable(i, m, mi, main, m1)
  m := create menu, w
  if(get menu id(m) <> -1) then($0="get menu id should have reported -1 for script-generated menu", crash)
  
  mi := add menu item(m), w
  $1="Puppies"
  set menu item caption(mi, 1), w
  mi := add menu item(m), w
  $1="Kittens"
  set menu item caption(mi, 1), w
  mi := add menu item(m), w
  $1="Walruses"
  set menu item caption(mi, 1), w
  mi := add menu item(m), w
  $1="Octopus"
  set menu item caption(mi, 1), w
  mi := add menu item(m), w
  $1="Plip"
  set menu item caption(mi, 1), w

  main := open menu, w
  bring menu forward(bottom menu), w

  for(i, align:left, align:right) do(
    set menu anchor x(m, i), w
    if(get menu anchor x(m) <> i) then($0="menu anchor x", crash)
    set menu anchor y(m, i), w
    if(get menu anchor y(m) <> i) then($0="menu anchor y", crash)
  )

  $1="Puppies"
  mi := find menu item caption(m, 1)
  if(mi == 0) then($0="failed to find first menu item by caption", crash)
  get menu item caption(mi, 2)
  if(not(string compare(1, 2))) then($0="found menu item that was not puppies", crash)

  $1="Walruses"
  mi := find menu item caption(m, 1)
  get menu item caption(mi, 2)
  if(not(string compare(1, 2))) then($0="found menu item that was not walruses", crash)
  
  mi := find menu item caption(m, 1, mi) #search for Walruses starting after Walruses
  if(mi <> 0) then($0="find menu item walrus should have failed", crash)

  mi := add menu item(m)
  $1="Kittens"
  set menu item caption(mi, 1), w

  variable(kitten1, kitten2, walruses)
  #search for Kitten from top
  $1="Kittens"
  kitten1 := find menu item caption(m, 1)
  #search for Kitten starting after Walruses
  $2="Walruses"
  walruses := find menu item caption(m, 2) 
  kitten2 := find menu item caption(m, 1, walruses) 
  if(kitten1 == 0) then($0="failed to find first kitten", crash)
  if(kitten2 == 0) then($0="failed to find second kitten", crash)
  get menu item caption(kitten1, 2)
  if(not(string compare(1, 2))) then($0="found menu item that was not kitten(1)", crash)
  get menu item caption(kitten2, 2)
  if(not(string compare(1, 2))) then($0="found menu item that was not kitten(2)", crash)
  if(kitten1 == kitten2) then($0="found the same kitten twice", crash)

  mi := add menu item(m), w
  delete menu item(mi), w
  $1="Kittens"
  mi := find menu item caption(m, 1)
  delete menu item(mi), w
  mi := first menu item(m)
  delete menu item(mi), w

  if(find menu ID(1) <> 0) then($0="found unopened menu", crash)
  open menu(1),w
  m1 := find menu ID(1)
  if(get menu ID(m1) <> 1) then($0="get menu ID mismatch vs find menu", crash)
  if(m1 == 0) then($0="failed to find opened menu", crash)  
  close menu(m1),w

  for(i, -3, 12) do(
    set menu border(main, i), w
  )
  if(get menu border(main) <> 12) then($0="readback of menu border thickness failed", crash)
  for(i, 0, 10) do(
    set menu boxstyle(main, i)
  )
  if(get menu boxstyle(main) <> 10) then($0="readback of menu boxstyle failed", crash)

  for(i, 0, 15) do(
    set menu max chars(main, i), w
    if(get menu max chars(main) <> i) then($0="readback of menu max chars failed", crash)
  )
  set menu max chars(main, 0), w
  for(i, 10, 30) do(
    set menu min chars(main, i), w
    if(get menu min chars(main) <> i) then($0="readback of menu min chars failed", crash)
  )
  set menu min chars(main, 0), w

  for(i, -50, 50, 10) do(
    set menu offset x(main, i), w
    if(get menu offset x(main) <> i) then($0="readback of menu offset x failed", crash)
    set menu offset y(main, i), w
    if(get menu offset y(main) <> i) then($0="readback of menu offset y failed", crash)
  )
  set menu offset x(main, 0)
  set menu offset y(main, 0)
  for(i, align:left, align:right) do(
    set menu anchor x(main, i), w
    if(get menu anchor x(main) <> i) then($0="readback of menu anchor x failed", crash)
    set menu anchor y(main, i), w
    if(get menu anchor y(main) <> i) then($0="readback of menu anchor y failed", crash)
  )
  set menu anchor x(main, 0), w
  set menu anchor y(main, 0), w

  for(i, align:left, align:right) do(
    set menu text align(main, i), w
    if(get menu text align(main) <> i) then($0="readback of menu text align failed", crash)
  )
  set menu text align(main, 0), w

  set menu bit(main, 0, true), w  #Transparent box
  if(get menu bit(main, 0) <> true) then($0="failed to set menu bit", crash)
  set menu bit(main, 0, false), w #Opaque box
  if(get menu bit(main, 0) <> false) then($0="failed to unset menu bit", crash)
  variable(save rows)
  save rows := get menu max rows(main)
  set menu max rows(main, 3), w
  set menu bit(main, 1, true), w  #never show scrollbar
  set menu bit(main, 1, false), w #show scrollbar
  set menu max rows(main, save rows)
  set menu bit(main, 4, true), w  #no box
  set menu bit(main, 4, false), w #show box
  set menu bit(main, 8, true), w  #Advance text box when menu closes
  show text box(6), w
  if(not(menu is open(main))) then($0="why isn't the main menu open?", crash)
  close menu(top menu), w
  close menu(main), w
  if(current text box <> -1) then($0="menu bit failed to close textbox", crash)

  if(menu is open(main)) then($0="why is the main menu still open?", crash)

  variable(antelope, buffalo, catbus, duiker)
  m := create menu, w
  antelope := add menu item(m), $0="Antelope", set menu item caption(antelope, 0), w
  buffalo  := add menu item(m), $0="Buffalo",  set menu item caption(buffalo, 0), w
  catbus   := add menu item(m), $0="Catbus",   set menu item caption(catbus, 0), w
  duiker   := add menu item(m), $0="Duiker",   set menu item caption(duiker, 0), w

  # Test disabled, hidden, and unselectable items
  set menu item bit(catbus, menuitembit:hide when disabled, true), w
  assert(get menu item bit(catbus, menuitembit:hide when disabled) == true)
  # Disable and hide now
  set menu item type(catbus, menutype:label)
  set menu item subtype(catbus, menulabel:disabled)
  # (menu item disabled/visible/selectable should reflect changes immediately without needing a wait)
  assert(menu item disabled(catbus) == true)
  assert(menu item visible(catbus) == false)
  assert(menu item selectable(catbus) == false)
  w
  # Don't hide
  set menu item bit(catbus, menuitembit:hide when disabled, false)
  assert(menu item disabled(catbus) == true)
  assert(menu item visible(catbus) == true)
  assert(menu item selectable(catbus) == true)
  w
  # Un-selectable
  set menu item subtype(catbus, menulabel:unselectable)
  assert(menu item disabled(catbus) == false)
  assert(menu item visible(catbus) == true)
  assert(menu item selectable(catbus) == false)
  w
  # Re-enable
  set menu item subtype(catbus, menulabel:selectable)
  assert(menu item disabled(catbus) == false)
  assert(menu item visible(catbus) == true)
  assert(menu item selectable(catbus) == true)
  w

  # Test unselectable menu items are unselectable
  # (Note: you can select hidden items! Keeping for backcompat?)
  select menu item(catbus), w
  assert(selected menu item(m) == catbus)
  set menu item subtype(catbus, menulabel:unselectable)
  w  # Requires a wait to take effect
  assert(selected menu item(m) == duiker)  # The next selectable item
  assert(select menu item(antelope) == true)
  assert(select menu item(catbus) == false)  # Unselectable, fails
  assert(selected menu item(m) == antelope)
  set menu item subtype(catbus, menulabel:selectable)
  select menu item(catbus)
  assert(selected menu item(m) == catbus)

  set menu item settag(catbus, 3)
  if(get menu item settag(catbus) <> 3) then($0="menu item settag readback failed", crash)
  if(check tag(3)) then($0="tag 3 shouldn't be on yet (menu)", crash)
  use menu item(catbus), w
  if(not(check tag(3))) then($0="tag 3 shouldn't be on now (menu)", crash)
  set menu item settag(catbus, 0)
  set menu item togtag(catbus, 3)
  use menu item(catbus), w
  if(check tag(3)) then($0="tag 3 should be toggled off (menu)", crash)
  use menu item(catbus), w
  if(check tag(3)==OFF) then($0="tag 3 should be toggled on (menu)", crash)
  use menu item(catbus), w
  if(check tag(3)) then($0="tag 3 should be toggled off again (menu)", crash)
  set menu item togtag(catbus, 0)

  for(i, 1, 2) do(
    set menu item tag(catbus, -3, i), w
    if(get menu item tag(catbus, i) <> -3) then($0="menu item tag readback failed", crash)
    set tag(3, ON), w
    set menu item tag(catbus, 3, i), w
    set tag(3, OFF), w
    set menu item tag(catbus, 0, i), w
  )

  $0="menu item tag req", show string(0)
  for(i, 1, 2) do(
    set tag(3, OFF)
    if(get menu item tag(catbus, i) <> 0) then($0="menu item tag req should be zero:", append number(0, i), crash)
    set menu item tag(catbus, 3, i), w
    if(get menu item tag(catbus, i) <> 3) then($0="menu item tag req readback fail:", append number(0, i), crash)
    set tag(3, ON), w
    set menu item tag(catbus, -3, i), w
    set tag(3, OFF), w
    set menu item tag(catbus, 0, i), w
  )
  show no value

  # menu item extra data
  assert(extra length(buffalo) == 3)
  for(i, 0, 2) do(
    assert(get menu item extra(buffalo, i) == 0)
    set menu item extra(buffalo, i, i*10)
    assert(get menu item extra(buffalo, i) == i*10)
    assert(get extra(buffalo, i) == i*10)
    set extra(buffalo, i, 20)
    assert(get menu item extra(buffalo) == 20)
  )
  resize extra(buffalo, 0)
  assert(extra length(buffalo) == 0)
  append extra(buffalo, 42)
  assert(extra length(buffalo) == 1)
  assert(get menu item extra(buffalo, -1) == 42)  # Check negative indices

  set menu item type(catbus, menutype: special)
  $0="" # use default caption
  set menu item caption(catbus, 0), w
  for (i, 0, 13) do(
    set menu item subtype(catbus, i), w
  )
  $0="Catbus, not Save"
  set menu item caption(catbus, 0), w
  $0=""
  set menu item caption(catbus, 0), w
  set menu item type(catbus, menutype:menu), w
  for(i, 0, 2) do(
    set menu item subtype(catbus, i), w
    if(get menu item subtype(catbus) <> i) then($0="menu item subtype readback failed", crash)
  )
  $0="Catbus"
  set menu item caption(catbus, 0), w
  
  # open another menu
  set menu item type(catbus, menutype:menu)
  set menu item subtype(catbus, 2)
  use menu item(catbus), w
  if(get menu id(top menu) <> 2) then($0="Failed to open a menu from a menu", crash)
  close menu(top menu), w

  # open a text box
  set menu item type(catbus, menutype:textbox)
  set menu item subtype(catbus, 7)
  use menu item(catbus), w
  if(current textbox <> 7) then($0="Failed to open a text box from a menu", crash)
  advance text box, w

  if(get menu item type(catbus) <> menutype:textbox) then($0="menu item type readback failed", crash)
  set menu item type(catbus, menutype:label)

  # run a script from a menu
  set menu item type(catbus, menutype:script)
  set menu item subtype(catbus, @on menu item use)
  use menu item(catbus), w
  if(menu item script global <> 99) then($0="Failed to run a script from a menu", crash)

  variable(oldcol)
  oldcol := get menu textcolor(m)
  for(i, 0, 10) do(
    set menu textcolor(m, i * 13), w
    if(get menu textcolor(m) <> i * 13) then($0="menu textcolor readback failed", crash)
  )
  set menu textcolor(m, oldcol)

  # iterate all menu items
  variable(count)
  count := 0
  mi := first menu item(m)
  while(mi) do(
    count += 1
    mi := next menu item(mi)
  )
  if(count <> 4) then($0="iterated wrong number of menu items (all)")
  # now hide catbus
  set menu item bit(catbus, menu item bit:hide when disabled, true)
  set menu item tag(catbus, 3)
  set tag(3, OFF)
  # iterate all except hidden
  count := 0
  mi := first menu item(m)
  while(mi) do(
    count += 1
    mi := next menu item(mi, true)
  )
  if(count <> 3) then($0="iterated wrong number of menu items (vis only)")
  # iterate all including hidden
  count := 0
  mi := first menu item(m)
  while(mi) do(
    count += 1
    mi := next menu item(mi, false)
  )
  if(count <> 4) then($0="iterated wrong number of menu items (vis only)")

  # close when selected
  set menu item bit(antelope, 1, true)
  use menu item(antelope), w
  if(top menu == m) then($0="Even Toed Ungulate menu failed to close", crash)

  menu close global := 0
  m := create menu, w
  mi := add menu item(m)
  $1="close me"
  set menu item caption(mi, 1), w
  set menu item bit(mi, menu item bit:close menu when selected, true)
  set menu on close script(m, @on close menu test)
  if(get menu on close script(m) <> @on close menu test) then($0="menu close script readback failed", crash)
  use menu item(mi), w
  if(menu close global <> 99) then($0="menu close script didn't run", crash)

  # Test background highlight
  w(2)
  m := create menu, w
  mi := add menu item(m), w
  set menu item caption(mi, $1="Lorem"), w
  mi := add menu item(m), w
  set menu item caption(mi, $1="Ipsum"), w
  mi := add menu item(m), w
  set menu item caption(mi, $1="Dolor"), w
  mi := add menu item(m), w
  set menu item caption(mi, $1="Sit"), w
  mi := add menu item(m), w
  set menu item caption(mi, $1="Amet"), w
  assert(get menu bit(m, menubit:highlight selection background) == false)
  set menu bit(m, menubit:highlight selection background), w
  assert(get menu bit(m, menubit:highlight selection background) == true)
  mi := first menu item(m)
  while(mi) do(
    mi := next menu item(mi, true)
    if(mi) then(select menu item(mi))
    w(2)
  )
  w
  close menu(m),w
  
  # Test closing a textbox by closing the menu
  m := create menu, w
  mi := add menu item(m), w
  set menu item caption(mi, $1="Twas brillig"), w
  mi := add menu item(m), w
  set menu item caption(mi, $1="And the slithy toaves..."), w
  assert(get menu bit(m, menubit:advance text box when menu closes) == false)
  show text box(2),w
  # first, closing the menu should not close the textbox by default
  close menu(m),w
  assert(current text box == 2)
  advance text box, w
  assert(current text box == -1)
  # Now do it again...
  m := create menu, w
  mi := add menu item(m), w
  set menu item caption(mi, $1="Did gyre and gimbal"), w
  mi := add menu item(m), w
  set menu item caption(mi, $1="In the wabe..."), w
  assert(get menu bit(m, menubit:advance text box when menu closes) == false)
  set menu bit(m, menubit:advance text box when menu closes), w
  assert(get menu bit(m, menubit:advance text box when menu closes) == true)
  show text box(2),w
  # This time, closing the menu should close the textbox too
  close menu(m),w
  assert(current text box ==-1)

  # Test edgecases for menus without selectable menu item.
  # Open a menu where the first item is unselectable:
  m := open menu(3)
  assert(menu item slot(selected menu item(m)) == 1)
  w
  close menu(m)
  # Empty menu
  m := create menu()
  assert(selected menu item == 0)
  close menu(m)
  # Open a menu with no selectable items
  m := open menu(4)
  assert(selected menu item == 0)
  w
  close menu(m)

  menu scroll tests
end

script, on menu item use, begin
  menu item script global := 99
end

script, on close menu test, begin
  menu close global := 99
end


script, scroll through menu, menu, numitems, begin
  variable (i, j, mi)

  # test scrolling
  for (i, 1, 3, 2) do (
    set menu max rows(menu, i)
    # forwards
    mi := first menu item(menu)
    while (mi) do (
      select menu item(mi), w
      mi := next menu item(mi)
    )
    # backwards
    mi := menu item by slot(menu, numitems -- 1)
    while (mi) do (
      select menu item(mi), w
      mi := previous menu item(mi)
    )
    # randomly
    for (j, 0, 4) do (
      select menu item(menu item by slot(menu, random(0, numitems -- 1))), w
    )
  )
  set menu max rows(menu, 0)
end

script, menu scroll tests, begin
  $0="menu scroll tests"

  variable (main, mi, next, i, numitems, numvisitems)
  main := open menu, w
  set menu bit(main, 1, false), w #show scrollbar
  allow minimap(true), w #make sure there are no hidden items
  allow save anywhere(true), w

  # test menu item iteration
  mi := first menu item(main)
  while (mi) do (
    get menuitemcaption(mi, err arg string)
    #trace(err arg string)
    appendnumber(err arg string, numitems)
    assert (menu item slot(mi) == numitems)
    assert (menu item by slot(main, numitems, true) == mi)
    assert (menu item by slot(main, numitems, false) == mi)
    assert (menu item true slot(mi) == numitems)
    assert (menu item by true slot(main, numitems) == mi)
    next := next menu item(mi, false)
    if (next) then (
      assert (mi == previous menu item(next))
    )
    set menu item extra(mi, 0, numitems)
    numitems += 1
    mi := next
  )
  assert(numitems == 9)

  $err arg string=""

  # menu item iteration edge cases
  assert (previous menu item(first menu item(main)) == 0)
  assert (next menu item(menu item by slot(main, numitems -- 1)) == 0)
  assert (menu item by slot(main, numitems, true) == 0)
  assert (menu item by slot(main, numitems, false) == 0)
  assert (menu item by slot(main, -1) == 0)
  assert (menu item by true slot(main, -1) == 0)
  assert (menu item by true slot(main, numitems) == 0)

  # test "select menu item"
  mi := first menu item(main)
  while (mi) do (
    select menu item(mi), w
    assert (selected menu item(main) == mi)
    mi := next menu item(mi)
  )

  # test scrolling
  scroll through menu(main, numitems)

  # check hidden items sorted to end:
  # 1. hide three items
  set tag(3, OFF)
  variable(hidden1, hidden2, hidden3)
  hidden3 := menu item by slot(main, 4)
  set menu item bit(hidden3, menu item bit: hide when disabled, true)
  set menu item tag(hidden3, 3, 1)
  hidden1 := menu item by slot(main, 0)
  set menu item bit(hidden1, menu item bit: hide when disabled, true)
  set menu item tag(hidden1, 3, 1)
  hidden2 := menu item by slot(main, 3)
  set menu item bit(hidden2, menu item bit: hide when disabled, true)
  set menu item tag(hidden2, 3, 1)
  w

  # 2. add new item
  mi := add menu item(main)
  set menu item extra(mi, 0, numitems)
  numitems += 1
  $1="a new item!"
  set menu item caption(mi, 1), w

  assert (numitems == menu item count(main))
  numvisitems := numitems -- 3
  assert (numvisitems == visible menu item count(main))

  # 3. check visible items in correct order
  variable (temp, extra, mi2)
  temp := -1
  for (i, 0, num items -- 3 -- 1) do (
    mi := menu item by slot(main, i, true)
    mi2 := menu item by slot(main, i, false)
    assert (mi == mi2)
    extra := get menu item extra(mi2, 0)
    if (extra <= temp) then ($0="menu items not in increasing order", crash)
    temp := extra
  )

  # 4. check hidden items at end in correct order
  if (menu item by slot(main, numitems -- 3, true) <> 0) then ($0="menuitembyslot returned hidden item", crash)
  if (menu item by slot(main, numitems -- 3, false) <> hidden1) then ($0="hidden items out of order", crash)
  if (menu item by slot(main, numitems -- 2, false) <> hidden2) then ($0="hidden items out of order", crash)
  if (menu item by slot(main, numitems -- 1, false) <> hidden3) then ($0="hidden items out of order", crash)
  assert (menu item by slot(main, numitems, false) == 0)

  # 5. check "true order" hasn't changed
  for (i, 0, num items -- 1) do (
    mi := menu item by true slot(main, i)
    assert (menu item true slot(mi) == i)
    assert (get menu item extra(mi, 0) == i)
  )

  # 6. test "last menu item"
  assert (last menu item(main) == menu item by slot(main, numvisitems -- 1))
  assert (last menu item(main, false) == menu item by slot(main, numitems -- 1, false))
  assert (next menu item(last menu item(main)) == 0)  # Don't return first invisible item
  assert (next menu item(last menu item(main), false) <> 0)  # Do return first invisible item

  # test scrolling again
  scroll through menu(main, numitems -- 3)

  # check unhidden menu items sorted back correctly
  set tag(3, ON), w
  for (i, 0, numitems -- 1) do (
    mi := menu item by slot(main, i, false)
    assert (mi == menu item by slot(main, i, true))
    assert (get menu item extra(mi, 0) == i)
  )

  close menu(main)
end

#next menu(menu handle)
#open menu (ID, allow duplicate)
#parent menu(menu item handle)
#previous menu(menu handle)
#swap menu items(handle1, handle2)
#wait for menu (menu handle)

########################################################################

# Run by text box 20
plotscript, textbox20 instead script, begin
  assert(current textbox == -1)
  # Check the set tag condition skipped
  assert(checktag(tag:textbox 20 settag) == false)
  textbox20 instead triggered += 1
end

script, textbox tests, begin
  $0="textbox tests"
  show text box(2), w
  advance text box, w
  show text box(3), w
  advance text box, w
  if(current textbox <> 4) then(crash)
  advance text box, w
  show text box(3), w
  advance text box, w
  if(current textbox <> 5) then(crash)
  advance text box, w
  if(current textbox <> -1) then(crash)
  w
  show text box(12), w, advance text box, w
  show text box(13), w, advance text box, w

  ###### "showtextbox happens immediately" is ON
  variable (oldval)
  oldval := read general(178)  # genBits2 + 1
  assert((oldval, and, 2^2) == 2^2)

  # check showtextbox isn't delayed
  start tick := ticknumber
  show text box(14)
  assert(ticknumber == start tick)
  assert(checktag(tag:textbox 14 happened) == true)
  # no wait required before advance
  advance textbox
  wait
  assert(current textbox == -1)
  assert(ticknumber == start tick + 1)
  settag(tag:textbox 14 happened, false)

  # check both showtextboxes work
  show text box(14)
  show text box(15)
  assert(checktag(tag:textbox 14 happened) == true)
  assert(checktag(tag:textbox 15 happened) == true)
  advance textbox
  settag(tag:textbox 14 happened, false)
  settag(tag:textbox 15 happened, false)
  assert(current textbox == -1)
  assert(ticknumber == start tick + 1)  # No ticks happened

  # check Instead scripts happen immediately, and Instead skips Set Tag conditions
  show text box(20)
  assert(ticknumber == start tick + 1)
  assert(current textbox == -1)
  assert(checktag(tag:textbox 20 settag) == false)
  assert(textbox20 instead triggered == 1)

  ###### "showtextbox happens immediately" OFF
  write general(178, oldval -- 2^2)

  # Check currenttextbox still updates immediately
  show text box(2)
  assert(current textbox == 2)
  # wait required before advance
  wait
  advance text box
  assert(current textbox == -1)

  # check showtextbox is delayed
  show text box(14)
  assert(checktag(tag:textbox 14 happened) == false)
  wait
  assert(checktag(tag:textbox 14 happened) == true)
  advance textbox
  settag(tag:textbox 14 happened, false)

  # check Instead scripts happen, and Instead script skips Set Tag conditions
  textbox20 instead triggered := 0
  start tick := ticknumber
  show text box(20)
  wait
  assert(current textbox == -1)
  assert(checktag(tag:textbox 20 settag) == false)
  assert(textbox20 instead triggered == 1)
  assert(ticknumber == start tick + 1)

  # check only last showtextbox works
  show text box(14)
  show text box(15)
  wait
  assert(checktag(tag:textbox 14 happened) == false)
  assert(checktag(tag:textbox 15 happened) == true)
  advance textbox
  assert(current textbox == -1)

  # check only last showtextbox works, using Instead script triggers
  show text box(20)
  show text box(20)
  wait
  assert(textbox20 instead triggered == 2)

  ###### Back to "showtextbox happens immediately" ON
  write general(178, oldval)
end

########################################################################

script, shop tests, begin
  # Ain't much here yet...
  $0="shop tests"
  assert(is shop buy menu empty(shop:Boring Shop) == false)
  assert(is shop hire menu empty(shop:Boring Shop) == false)
  assert(is shop buy menu empty(shop:Empty Shop) == true)
  assert(is shop hire menu empty(shop:Empty Shop) == true)
  w
end

########################################################################

script, battle tests, begin
  # there are some other battle tests in with the timer tests
  $0="battle tests"

  set battle countdown (182)
  assert(get battle countdown == 182)

  show value(random(1, 999999999)), w
  force random battle(1)
  show value(random(1, 999999999)), w # this should remain deterministic after battle
  show no value, w
  # attack forces run
  set tag(tag:Victory, true)
  assert(fight formation(1) == false), w  # sets victory tag
  assert(check tag(tag:Victory) == false)
  # attack forces victory
  assert(fight formation(2) == true), w
  assert(check tag(tag:Victory) == true)
  # attack forces exit
  assert(fight formation(3) == false), w
  # test tag check: battle should not trigger
  force random battle(2)
  assert(last formation <> 4)
  w(5)
  fight formation(5)
  w(5)
  # Test attacks setting tags.
  set tag(4, false)
  set tag(5, false)
  fight formation(6)
  w(5)
  # The first time should NOT set tag 5 because tag 4 was not set
  assert(not(check tag(5))), w
  set tag(4, true)
  fight formation(6)
  w(5)
  # The second time SHOULD set tag 5 because tag 4 WAS set
  assert(check tag(5)), w
  set tag(6, false)
  fight formation(7)
  w(5)
  # The third one should always turn the tag on because the tag check is 0.
  assert(check tag(6)), w

  test battle formation modification
  test hero counterattacks
  test battle rewards
end

# Formation set should be 1-2
script, force random battle, formation set = 1, begin
  variable(s, x, y, d)
  if (formation set == 0) then (formation set := 1)  # If run with runscriptbyid
  # save hero state
  s := get hero speed(me)
  x := hero x(me)
  y := hero y(me)
  d := hero direction(me)
  # get the hero ready for forcing the battle
  set hero speed(0, 20)
  # Formations 1-3 are at x = 14-16
  set hero position(0, 13 + formation set, 9), w
  # force the battle
  walk hero(0, north, 1), w
  # restore hero state
  set hero speed(me, s)
  set hero position(me, x, y)
  set hero direction(me, d)
  w
  # Countdown should be in range 40-160 (test this here so we see multiple battles
  assert(get battle countdown >= 40 && get battle countdown <= 160)
end

script, force non random battle, begin
  w
  fight formation(0)
  w(2)
end

plotscript, after battle, begin
  $0="After battle script:"
  battle script sequence += 1
  append number(0, battle script sequence)
  trace(0)
  show string at(0, 0, 0)
  w
  hide string(0)
end

plotscript, instead of battle, form, begin
  $0="Instead of battle script("
  append number(0, form)
  $0+")"
  trace(0)
  w
  fight formation(form)
  $0="  finish instead of battle"
  trace(0)
end

script, test battle formation modification, begin
  # Test formation editing commands
  # use reward gold to keep track of how many enemies are present in each formation
  variable(orig gold, gold)
  orig gold := party money
  gold := party money
  fight formation(8), w
  assert(party money == gold + 1)

  # Add an enemy to the formation
  add enemy to formation (8, 8, 100, 90, 1)
  assert(formation slot enemy(8, 1) == 8)
  assert(formation slot x(8, 1) == 100)
  assert(formation slot y(8, 1) == 90)
  gold := party money
  fight formation(8), w
  assert(party money == gold + 2)

  # Fill the empty slots in the formation  
  variable(i)
  for(i, 2, 7) do(
    add enemy to formation (8, 8, 100, 90 + i * 20, 1)
  )
  gold := party money
  fight formation(8), w
  assert(party money == gold + 8)
  
  # remove several enemies, including the original enemy in slot 0
  delete enemy from formation (8, 0)
  delete enemy from formation (8, 1)
  delete enemy from formation (8, 2)
  delete enemy from formation (8, 3)
  w
  assert(formation slot enemy(8, 0) == -1)
  assert(formation slot enemy(8, 1) == -1)
  assert(formation slot enemy(8, 2) == -1)
  assert(formation slot enemy(8, 3) == -1)
  gold := party money
  fight formation(8), w
  assert(party money == gold + 4)
  
  # Test finding an enemy
  w
  # an enemy that will not be found
  assert(find enemy in formation (8, 1, 0) == -1)
  assert(find enemy in formation (8, 1, get count) == 0)
  # an enemy that is actually present
  assert(find enemy in formation (8, 8, 0) == 4)
  assert(find enemy in formation (8, 8, 3) == 7)
  assert(find enemy in formation (8, 8, get count) == 4)

 
  # Test background changing
  assert(get formation background(8) == 1)
  set formation background(8, 0)
  assert(get formation background(8) == 0)
  # Test writing song id number
  show value(get formation song(8))
  assert(get formation song(8) == song:silence)
  set formation song(8, song:same as map)
  assert(get formation song(8) == song:same as map)
  set formation song(8, song:tin glazed and noncommutative)
  assert(get formation song(8) == song:tin glazed and noncommutative)
  # actually run the battle. We can't test the visuals, but -autosnap will see it
  fight formation(8), w
  
  # Test resetting the formation to default
  reset formation(8), w
  assert(get formation background(8) == 1)
  assert(get formation song(8) == song:silence)
  assert(find enemy in formation (8, 8, 0) == 0)
  assert(find enemy in formation (8, 8, get count) == 1)
  assert(formation slot enemy(8, 0) == 8)
  assert(formation slot x(8, 0) == 131)
  assert(formation slot y(8, 0) == 169)
  show string(string sprintf(2, $1="%dx%d", formation slot x(8, 0), formation slot y(8, 0))), w
  fight formation(8), w

  # Test modifying and resetting individual formation slots
  # Assert original values
  assert(formation slot enemy(9, 0) == 0)
  assert(formation slot x(9, 0) == 18)
  assert(formation slot y(9, 0) == 201)
  assert(formation slot enemy(9, 1) == -1)
  assert(formation slot x(9, 1) == 0)
  assert(formation slot y(9, 1) == 0)
  # Make changes
  delete enemy from formation(9, 0)
  add enemy to formation(9, 8, 50, 190, 1)
  assert(formation slot enemy(9, 0) == -1)
  # deleted enemies do not have their x,y deleted, but width/height are no longer included 
  assert(formation slot x(9, 0) == 1)
  assert(formation slot y(9, 0) == 167)
  assert(formation slot enemy(9, 1) == 8)
  assert(formation slot x(9, 1) == 50)
  assert(formation slot y(9, 1) == 190)
  fight formation(9)
  # Reset single slots
  reset formation slot(9, 0)
  assert(formation slot enemy(9, 0) == 0)
  assert(formation slot x(9, 0) == 18)
  assert(formation slot y(9, 0) == 201)
  assert(formation slot enemy(9, 1) == 8)
  assert(formation slot x(9, 1) == 50)
  assert(formation slot y(9, 1) == 190)
  reset formation slot(9, 1)
  assert(formation slot enemy(9, 0) == 0)
  assert(formation slot x(9, 0) == 18)
  assert(formation slot y(9, 0) == 201)
  assert(formation slot enemy(9, 1) == -1)
  assert(formation slot x(9, 1) == 0)
  assert(formation slot y(9, 1) == 0)
  
  # restore original money
  set money(orig gold)
  show no value
end

script, test hero counterattacks, begin
  assert(hero by slot(0) == hero:Freki)
  assert(get hero stat(0, stat:HP, current stat) == 10), w
  fight formation(10), w
  # Freki's HP should be 4 points lower, -1 for the ice attack, and -3 for her self-counterattack
  assert(get hero stat(0, stat:HP, current stat) == 6), w
  # Heal up
  set hero stat(0, stat:HP, 10, current stat), w
end

script, test battle rewards, begin
  fight formation(11), w
  assert(money reward == 4)
  assert(experience reward == 2)
  assert(items reward (get count) == 1)
  variable (i, item, qty)
  for (i, 0, items reward (get count) -- 1) do (
    item := items reward(i)
    qty := items quantity reward(i)
    assert(item == item:tooth)
    assert(qty == 2)
    get item name(3, item)
    string sprintf(1, $2="Got %s x %d", 3, qty)
    show string(1)
    w
  )
  show no value
  clear string(1)
  clear string(2)
  clear string(3)
end

########################################################################

script, attack tests, begin
  $0="attack tests"

  # Reading attack name
  $1="SelfStab"
  read attack name(2, atk:SelfStab)
  assert(string compare(1,2))
  $2=""
  read attack name(2, 0 + 1)
  assert(string compare(1,2))
  $2=""
  get attack name(2, 0 -- 1)
  assert(string compare(1,2))
  
  # Reading attack caption
  $1="Attack self"
  get attack caption(2, 0 + 1)
  assert(string compare(1,2))

  assert(get attack extra(atk:SelfStab, 0) == 42)
  assert(get attack extra(atk:SelfStab, 2) == 99)
  assert(get attack extra(atk:ForceRun) == 0)
  assert(get attack extra(10 + 1, 2) == 1)  # Currently the last attack
end

########################################################################

script, enemy tests, begin
  $0="enemy tests"

  assert(enemy elemental resist as int(enemy:Data Read Test, 2) == 0)
  assert(enemy elemental resist as int(enemy:Data Read Test, 15) == -5)

  # Name
  get enemy name(enemy:Data Read Test, 1)
  $2 = "Data read test"
  assert(string compare(1, 2))
  $2 = "Data read test2"
  set enemy name(enemy:Data Read Test, 2)
  get enemy name(enemy:Data Read Test, 1)
  assert(string compare(1, 2))
  reset enemy name(enemy:Data Read Test)
  get enemy name(enemy:Data Read Test, 1)
  $2 = "Data read test"
  assert(string compare(1, 2))

  # Length limit is currently 16, so check at least first 16 chars saved
  $2 = "SeenGoldDuckUponHigh"
  set enemy name(enemy:Data Read Test, 2)
  get enemy name(enemy:Data Read Test, 1)
  trim string(1, 1, 16)
  trim string(2, 1, 16)
  assert(string compare(1, 2))

  # Stats
  assert(get enemy stat(enemy:Data Read Test, stat:HP) == 13)
  set enemy stat(enemy:Data Read Test, stat:Hits, 6)
  assert(get enemy stat(enemy:Data Read Test, stat:Hits) == 6)
  reset enemy stat(enemy:Data Read Test, stat:Hits)
  assert(get enemy stat(enemy:Data Read Test, stat:HP) == 13)

  # Other
  set enemy appearance(enemy:Data Read Test, Enemy:PictureSize, EnemySize:large)
  assert(get enemy appearance(enemy:Data Read Test, Enemy:PictureSize) == EnemySize:large)
  assert(get enemy appearance(enemy:Data Read Test, Enemy:Palette) == -1)
  assert(read enemy data(enemy:Data Read Test, Enemy:Item) == 1)
  write enemy data(enemy:Data Read Test, Enemy:RareItem, 2)
  assert(read enemy data(enemy:Data Read Test, Enemy:RareItem) == 2)
  assert(read enemy data(enemy:Data Read Test, Enemy:ItemPercent) == 50)
  write enemy data(enemy:Data Read Test, Enemy:ItemPercent, 24)
  assert(read enemy data(enemy:Data Read Test, Enemy:ItemPercent) == 24)
  reset enemy data(enemy:Data Read Test, Enemy:ItemPercent)
  assert(read enemy data(enemy:Data Read Test, Enemy:ItemPercent) == 50)
end

########################################################################

script, timer tests, begin
  variable(i)
  
  allocate timers(32)
  set timer(31, 10, 1, @timer test 1, 1)
  show string at(1, 10, 10)
  w, w, w, w
  if(read timer(31) <> 6) then($0="read timer failed", crash)
  w(6), w

  timer global := 100
  set timer(31, 0, 1, @looping timer test, 1)
  w(11)

  # Test "set timer args"
  timer global := 0
  set timer(timer:set args test, 0, 1, @timer args test)
  set timer args(timer:set args test, -999, 100)
  timer expect arg 1 := -999
  timer expect arg 2 := 100
  wait
  assert(timer global == 1)
  # Args should be preserved when run again
  set timer(timer:set args test, 0)
  wait
  assert(timer global == 2)
  # Changing the trigger to different script wipes the args
  set timer(timer:set args test, 0, 1, @timer args test 2)
  timer expect arg 1 := timer:set args test  # Default args
  timer expect arg 2 := 0
  wait
  assert(timer global == 12)
  # Passing no args
  set timer args(timer:set args test)
  set timer(timer:set args test, 0)
  timer expect arg 1 := 0
  timer expect arg 2 := 0
  wait
  assert(timer global == 22)
  # Throw an error if no script triggered
  # set timer(timer:set args test, 0, 1, 0)
  # set timer args(timer:set args test, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)

  $err arg string="random"
  test timer and battle interactions(@force random battle)
  $err arg string="fight formation"
  test timer and battle interactions(@force non random battle)
  $err arg string=""

  
  $0="timer interactions with after battle", trace(0)
  teleport to map(1, 10, 8), w # after battle
  battle script sequence := 0
  test timer and battle interactions(@force random battle)
  if(battle script sequence <> 3) then($0="(ab) afterbattle should have happened 3 times", crash)

  $0="timer interactions with instead of battle", trace(0)
  teleport to map(2, 10, 8), w # instead of battle
  battle script sequence := 0
  test timer and battle interactions(@force random battle)
  if(battle script sequence <> 0) then($0="(ib) afterbattle should have happened 0 times", crash)

  $0="timer interactions with after battle + instead of battle", trace(0)
  teleport to map(3, 10, 8), w # after battle + instead of battle
  battle script sequence := 0
  test timer and battle interactions(@force random battle)
  if(battle script sequence <> 3) then($0="(ab+ib) afterbattle should have happened 3 times", crash)

  teleport to map(0, 10, 8), w

  hide string(1)
end

script, test timer and battle interactions, trigger battle, begin
  #regular timer
  timer global := 0
  timer global will become := 1000
  set timer(31, 10, 1, @battle timer test, -1, 0)
  run script by id(trigger battle)
  if(read timer(31) <= 0) then($0="timer should still be running", crash)
  if(timer global <> 0) then($0="timer global should still be zero", crash)
  w(10)
  if(read timer(31) >> 0) then($0="timer should be done by now", crash)
  if(timer global <> timer global will become) then($0="timer global didn't change", crash)
  
  #timerflag:battle
  timer global := 0
  timer global will become := 2000
  set timer(31, 10, 1, @battle timer test, 1, timerflag:battle)
  run script by id(trigger battle)
  if(read timer(31) >> 0) then($0="(bat) timer should be done by now", crash)
  if(timer global <> timer global will become) then($0="(bat) timer global didn't change", crash)

  #timerflag:battle ,or, timerflag:critical
  timer global := 0
  timer global will become := 3000
  set timer(31, 10, 1, @battle timer test, 1, (timerflag:battle,or,timerflag:critical))
  set timer(30, 20, 1, @battle timer test, 0, (timerflag:battle))
  run script by id(trigger battle)
  if(read timer(31) >> 0) then($0="(bat+crit) timer should be done by now", crash)
  if(timer global <> timer global will become) then($0="(bat+crit) timer global didn't change", crash)
  timer global will become := 4000
  if(read timer(30) <= 0) then($0="(bat+crit) second timer should still be running", crash)
  w(12)
  if(read timer(30) >> 0) then($0="(bat+crit) second timer should be done by now", crash)
  if(timer global <> timer global will become) then($0="(bat+crit) timer global didn't change on second timer", crash)
  timer global := 0
  w
end

script, timer test 1, begin
  show value(12345)
  w
end

script, looping timer test, begin
  if(timer global <= 0) then(timer global := 0, exit script)
  set timer(31, 0, 1, @looping timer test, 1)
  timer global -= 10
  show value(timer global)
  _checkpoint
end

script, timer args test, arg1, arg2, arg3 = 42, begin
  assert(arg1 == timer expect arg 1)
  assert(arg2 == timer expect arg 2)
  #assert(arg3 == 42)  # arg3 defaults to 0 instead of 42 due to an engine bug
  assert(arg3 == 0)
  timer global += 1
end

script, timer args test 2, arg1, arg2, begin
  assert(arg1 == timer expect arg 1)
  assert(arg2 == timer expect arg 2)
  timer global += 10
end

script, battle timer test, begin
  timer global := timer global will become
  $0="battle timer test:"
  append number(0, timer global)
  trace(0)
  show string(0), w
end

########################################################################

script, pathfinding tests, begin
  $0="Pathfinding tests"
  
  variable(oldx, oldy, oldmap)
  oldx := herox(me)
  oldy := heroy(me)
  oldmap := current map
  
  teleport to map(map:Pathland, 8, 7)
  start checkpoint timer(4)
  
  variable(ref)
  ref := create NPC(0, 17, 3)
  pathfind npc to(ref, hero x(me), hero y(me), 1)
  wait for npc(ref)
  assert(dist from leader(ref) == 1)
 
  pathfind npc to(ref, 8, 3)
  wait for npc(ref)
  assert(npc x(ref) == 8)
  assert(npc y(ref) == 3)
  
  # With pacing NPCs in the way
  create npc(1, 8, 10, right)
  create npc(1, 8, 11, right)
  create npc(1, 8, 12, right)
  create npc(1, 8, 13, right)
  camera follows npc(ref)
  pathfind npc to(ref, 10, 16)
  wait for npc(ref)
  assert(npc x(ref) == 10)
  assert(npc y(ref) == 16)
  
  # To an unreachable zone destination
  pathfind npc to(ref, 15, 5, 2)
  wait for npc(ref)
  assert(dist from pos(ref, 15, 5) == 2)
  
  # Cancelling a walk
  pathfind npc to(ref, 14, 9)
  wait for npc(ref)
  assert(is at pos(ref, 14, 9))
  pathfind npc to(ref, 17, 11)
  wait(10)
  cancel npc walk(ref)
  assert(is at pos(ref, 15, 8))
  # also test cancelling a regular walk npc
  walk npc(ref, left, 4)
  wait(10)
  cancel npc walk(ref)
  assert(is at pos(ref, 13, 8))
  # cancel a walk mid-step
  walk npc(ref, right, 3)
  wait(1)
  cancel npc walk(ref)
  assert(npc is walking(ref) == true) # this doesn't become false until the current step finishes
  wait for npc(ref)
  assert(npc is walking(ref) == false)
  assert(is at pos(ref, 14, 8))
  # Test obstructed wait with no timeout
  create npc(2, 9, 8)
  pathfind npc to(ref, 9, 7)
  wait(7 * 5) # wait the time it takes to walk 7 steps (will get stuck after 6 steps)
  assert(is at pos(ref, 10, 8))
  walk npc(2, south, 1)
  wait for npc(ref)
  assert(is at pos(ref, 9, 7))
  # Test npc following npc
  walk npc(ref, south, 1)
  wait for npc(1)
  assert(is at pos(ref, 9, 7))
  pathfind npc to(ref, 16, 18)
  wait(10)
  npc chases npc(2, ref, false, 10)
  wait for npc(ref)
  assert(is at pos(ref, 16, 18))
  wait for npc(2)
  assert(dist from pos(2, 16, 18) == 1)

  # Test that npc is walking supports pathfinding
  pathfind npc to(2, 13, 16)
  variable(i)
  # The whole path (4 tiles at 4 ticks each) should return true
  for(i, 1, 4 * 4) do(
    w
    assert(npc is walking(2))
  )
  # And then when the path finishes, return false
  w
  assert(npc is walking(2) == false)
  
  # Test NPC giving up an unreachable path after stillticks
  pathfind npc to(2, 14, 14, 12)
  # the first two steps should go fine
  w(4)
  w(4)
  assert(is at pos(2, 14, 15))
  assert(npc is walking(2))
  # wait 12 more ticks, should not have moved
  w(12)
  assert(is at pos(2, 14, 15))
  assert(npc is walking(2))
  # wait 1 more tick, and the npc should have given up walking
  w(1)
  assert(npc is walking(2)==false)
  
  # now add some barrier npcs
  variable(n0, n1, n2, n3)
  n0 := create npc(3, 15, 15, left)
  n1 := create npc(3, 14, 16, up)
  n2 := create npc(3, 13, 15, right)
  set npc speed(3, 10)
  
  # make the npc walk, make one of the barrier npcs move at the last moment
  pathfind npc to(2, 17, 15, 7) # try for 7 ticks
  # should fail for 6 ticks
  for(i, 1, 6) do(
    w(1)
    assert(npc is walking(2))
    assert(is at pos(2, 14, 15))
    assert(npc pixel x(2) == 14 * 20)
  )
  # abruptly remove a barrier npc
  destroy npc(n0)
  w(1)
  # NPC 2 should start walking
  assert(npc is walking(2)==true)
  assert(npc pixel x(2) == 14 * 20 + 5)
  wait for npc(2)
  assert(is at pos(2, 17, 15))
  # clean up the other barrier npcs
  destroy npc(n1)
  destroy npc(n2)
  
  
  # Now test some hero pathfinding
  camera follows hero

  subscript, hero tile, rank, x, y, begin
    exit returning(hero x(rank) == x && hero y(rank) == y)
  end
  
  #show string(string sprintf(1, $2="x=%d y=%d", hero x(me), hero y(me)))
  assert(hero tile(me, 8, 7))
  pathfind hero to(me, 13, 5)
  wait(5)
  assert(hero tile(me, 9, 7))
  wait for hero(me)
  # should reach dest
  assert(hero tile(me, 13, 5))
  # caterpillar should follow
  assert(hero tile(1, 12, 5))
  assert(hero tile(2, 12, 6))
  assert(hero tile(3, 11, 6))
  # now path each hero individually
  suspend caterpillar
  pathfind hero to(0, 8, 7)
  pathfind hero to(1, 9, 7)
  pathfind hero to(2, 8, 8)
  pathfind hero to(3, 9, 8)
  wait for hero(0)
  wait for hero(1)
  wait for hero(2)
  wait for hero(3)
  assert(hero tile(0, 8, 7))
  assert(hero tile(1, 9, 7))
  assert(hero tile(2, 8, 8))
  assert(hero tile(3, 9, 8))
 
  # Test hero-on-hero collision (or lack thereof)
  pathfind hero to(0, 12, 7)
  pathfind hero to(1, 11, 7)
  wait for hero(0)
  wait for hero(1)
  # Hero 1 should walk right through hero 0
  pathfind hero to(1, 13, 7)
  wait(5)
  assert(hero tile(1, hero x(0), hero y(0)))
  wait(5)
  # should still be walking for one more tick
  assert(hero is walking(1))
  wait(1)
  # okay, should be done now
  assert(hero is walking(1)==false)
  
  # now lets test hero-on-npc collisions
  # get hero 1 out of the way
  walk hero(1, right, 1)
  # create some NPC barriers
  n0 := create npc(3, 12, 6, down)
  n1 := create npc(3, 13, 7, left)
  n2 := create npc(3, 12, 8, up)
  n3 := create npc(3, 11, 7, right)
  w(1)
  # Hero pathing should fail
  pathfind hero to(0, 12, 10)
  wait(1)
  # the hero is still trying to walk
  assert(hero is walking(0))
  # the hero has not moved
  assert(hero tile(0, 12, 7))
  cancel hero walk(0)
  assert(hero is walking(0) == false)
  # now move one NPC out of the way
  set npc speed(3, 10)
  pathfind npc to(n0, 14, 6)
  wait(2)
  # Now the hero can move
  pathfind hero to(0, 12, 10)
  wait(5)
  assert(hero is walking(0))
  assert(hero tile(0, 12, 6))
  wait for hero(0)
  
  # move the npcs over to block hero 1
  pathfind npc to(n2, 15, 7)
  pathfind npc to(n3, 14, 8)

  # npc 1 is blocked, but non-leader heroes
  # are currently allowed to pass through npcs  
  pathfind hero to(1, 16, 7)
  wait(1)
  assert(hero is walking(1) == true)
  wait for hero(1)
  assert(hero tile(1, 16, 7))
  
  # send an NPC running, and have the hero chase it
  pathfind npc to(n1, 5, 5)
  hero chases npc(0, n1, true)
  wait for npc(n1)
  # hero should still be catching up
  assert(hero is walking(0))
  wait for hero(0)
  assert(hero is walking(0) == false)
  
  # now path the hero somewhere it can't reach, it should get close
  pathfind hero to(0, 8, 5)
  wait(5)
  assert(hero is walking(0))
  wait(5)
  assert(hero tile(0, 7, 4))
  w

  # send the hero on a long path, but cancel after a step and a half
  pathfind hero to(0, 8, 12)
  w(7)
  cancel hero walk(0)
  # hero should still finish the current step
  assert(hero is walking(0)), w
  assert(hero is walking(0)), w
  wait for hero(0)
  assert(hero is walking(0) == false)
  assert(hero tile(0, 6, 5))
  
  # now to test stop-after-stillticks
  # put hero hero back in the NPC cage
  set hero speed(me, 10)
  pathfind hero to(0, 14, 7)
  wait(2)
  # move the npc back to block
  pathfind npc to(n1, 13, 7)
  wait for hero(0)
  wait for npc(n1)
  
  # The hero should try to walk for 10 ticks and give up
  pathfind hero to(0, 13, 8, 10)
  for(i, 1, 10) do(
    show value(i)
    w(1)
    assert(hero is walking(0))
    assert(hero tile(0, 14, 7))
  )
  w(1)
  # the hero should have given up by now
  assert(hero is walking(0) == false)
  assert(hero tile(0, 14, 7))
  
  # Now more tests of hero chasing npc
  hero chases npc(0, n1)
  w(2)
  # should not have moved yet, but trying to move
  assert(hero tile(0, 14, 7))
  assert(hero is walking(0))
  pathfind npc to(n1, 12, 7)
  wait for npc(n1)
  #the hero is slower, so they should still be walking
  assert(hero tile(0, 13, 7))
  assert(hero is walking(0))
  # the hero does not give up
  w(10)
  assert(hero tile(0, 13, 7))
  assert(hero is walking(0))
  # make the NPC move again
  walk npc(n1, south, 1)
  wait(5)
  assert(hero tile(0, 12, 7))
  assert(hero is walking(0))
  # cancel chase
  cancel hero walk(0)
  assert(hero is walking(0)==false)
  # move the npc again
  pathfind npc to(n1, 10, 8)
  wait for npc(n1)
  assert(is at pos(n1, 10, 8))
  # make the hero chase the npc, but stop when reached
  hero chases npc(0, n1, true)
  wait for hero(0)
  assert(hero tile(0, 11, 8))
  assert(hero is walking(0)==false)
  # make the npc move again, hero will not follow
  walk npc(n1, south, 1)
  wait for npc(n1)
  assert(hero tile(0, 11, 8))
  assert(hero is walking(0)==false)
  # get rid of the barrier npcs
  w
  destroy npc(n0)
  destroy npc(n1)
  destroy npc(n2)
  destroy npc(n3)
  w
  
  # Now test doors and teleporting when pathfinding
  resume caterpillar
  set hero speed(0, 4)
  pathfind hero to(0, 15, 5)
  wait for hero(0)
  assert(hero tile(0, 15, 5))
  
  # path the hero into the door
  pathfind hero to(0, 17, 5)
  for(i, 1, 9) do(
    w
    assert(current map == 5)
  )
  # In one more tick, we should be through the door
  w
  assert(current map == 7)

  assert(hero is walking(0)==false)
  assert(hero tile(0, 3, 4))

  # walk through a door that leads to the current map
  pathfind hero to(0, 8, 2)
  wait for hero(0)
  assert(hero tile(0, 8, 2))
  # ready start to go through the door
  pathfind hero to(0, 14, 2)
  assert(hero pixel x(0) == 8 * 20)
  assert(hero pixel y(0) == 2 * 20)
  w(4)
  # almost through the door
  assert(hero pixel x(0) == 8 * 20 + 16)
  assert(hero pixel y(0) == 2 * 20)
  w(1)
  # through the door, should keep walking since the dest is still on this map
  assert(hero pixel x(0) == 9 * 20)
  assert(hero pixel y(0) == 6 * 20)
  wait for hero(0)
  assert(hero tile(0, 14, 2))
  assert(hero tile(1, 13, 2))
  assert(hero tile(2, 13, 3))
  assert(hero tile(3, 12, 3))

  # now test teleporting while pathing
  pathfind hero to(0, 14, 8)
  #wait for one step 
  w(5)
  assert(hero tile(0, 14, 3))
  teleport to map(5, 4, 4)
  assert(hero is walking(0)==false)
  assert(hero tile(0, 4, 4))
  # Test again, but partway through a step
  # go back to the other map
  teleport to map(7, 14, 2)
  w
  pathfind hero to(0, 14, 8)
  #wait for a fraction of a step
  w(2)
  assert(hero pixel y(0) == 2 * 20 + 8)
  # now teleport mid-step
  teleport to map(5, 4, 4)
  wait for hero(0)
  assert(hero pixel x(0) == 4 * 20)
  # Uh oh! the hero is misaligned with the grid
  # (which sucks, but is sort of expected, and is consistent with teleporting while doing a "walk hero")
  assert(hero pixel y(0) == 4 * 20 + 3 * 4)
  w
  # fix alignment
  put hero(0, 4 * 20, 5 * 20)
  w
  # Do it again with "walk hero" instead of "pathfind hero to"
  # go back to the other map
  teleport to map(7, 14, 2)
  w
  walk hero(0, south, 1)
  #wait for a fraction of a step
  w(2)
  assert(hero pixel y(0) == 2 * 20 + 8)
  # now teleport mid-step
  teleport to map(5, 4, 4)
  wait for hero(0)
  assert(hero pixel x(0) == 4 * 20)
  # Yep, hero is misaligned again, just like with "pathfind hero to"
  assert(hero pixel y(0) == 4 * 20 + 3 * 4)
  w
  # fix alignment again
  put hero(0, 4 * 20, 5 * 20)
  w
  
  # test that "wait for all" respects pathfinding
  suspend caterpillar
  pathfind hero to(2, 4, 3)
  pathfind hero to(3, 4, 2)
  n0 := create npc(3, 5, 2)
  n1 := create npc(3, 5, 4)
  set npc speed(3, 10)
  w
  pathfind hero to(0, 2, 6)
  pathfind hero to(1, 1, 6)
  pathfind hero to(2, 2, 7)
  pathfind hero to(3, 1, 7)
  pathfind npc to(n0, 7, 8)
  pathfind npc to(n1, 8, 8)
  wait for all
  assert(hero tile(0, 2, 6))
  assert(hero tile(1, 1, 6))
  assert(hero tile(2, 2, 7))
  assert(hero tile(3, 1, 7))
  assert(is at pos(n0, 7, 8))
  assert(is at pos(n1, 8, 8))
  # now a path the hero should finish first
  pathfind hero to(0, 2, 5)
  pathfind npc to(n1, 4, 4)
  wait for all
  assert(hero tile(0, 2, 5))
  assert(is at pos(n1, 4, 4))
  # now a path the npc should finish first
  pathfind hero to(0, 8, 8)
  pathfind npc to(n1, 5, 4)
  wait for all
  assert(hero tile(0, 8, 8))
  assert(is at pos(n1, 5, 4))
  
  resume caterpillar

  # clean up
  stop checkpoint timer
  show no value
  
  # Pathfinding to extra data
  set hero position(me, 12, 6) # Hero pos doesn't matter, just moving the camera
  variable(sl, result)
  sl := create map overlay container
  result := pathfind into extra as hero(sl, 13, 6, 14, 3)
  assert(result==true)
  visualize path from extra(sl)
  assert(get extra(sl, 0) == 13)
  assert(get extra(sl, 1) == 6)
  assert(get extra(sl, extra length(sl)--2) == 14)
  assert(get extra(sl, extra length(sl)--1) == 3)
  assert(extra length(sl) == 5 * 2)
  w
  # With maxsearch, give up at a range of 2
  result := pathfind into extra as hero(sl, 13, 6, 14, 3, 2)
  assert(result==false)
  visualize path from extra(sl)
  assert(extra length(sl) == 2 * 2)
  w
  # To an unreachable spot
  result := pathfind into extra as hero(sl, 13, 6, 19, 0)
  assert(result==false)
  visualize path from extra(sl)
  w
  # Ignore the first step
  result := pathfind into extra as hero(sl, 13, 6, 14, 3, 0, false, true)
  assert(result==true)
  visualize path from extra(sl)
  assert(get extra(sl, extra length(sl)--2) == 14)
  assert(get extra(sl, extra length(sl)--1) == 3)
  assert(extra length(sl) == 4 * 2)
  w
  # Append a second path to the first
  result := pathfind into extra as hero(sl, 13, 6, 14, 3, 0, false, false)
  assert(result==true)
  result := pathfind into extra as hero(sl, 14, 3, 15, 2, 0, true, true)
  assert(result==true)
  visualize path from extra(sl)
  assert(extra length(sl) == 7 * 2)
  w
  # Append a first path while preserving default 3 extra values
  free slice(sl)
  sl := create map overlay container
  result := pathfind into extra as hero(sl, 13, 6, 14, 3, 0, true, false)
  assert(extra length(sl) == 3 + 5*2)
  assert(get extra(sl, 0) == 0)
  assert(get extra(sl, 1) == 0)
  assert(get extra(sl, 2) == 0)
  assert(get extra(sl, 3) == 13)
  assert(get extra(sl, 4) == 6)
  visualize path from extra(sl, 3)
  w
  # Test NPC pathing (current NPC positions don't matter really)
  n0 := create npc(0, 10, 6) # Cannot walk across grass
  n1 := create npc(1, 10, 8) # Can walk across grass
  # Across the grass
  result := pathfind into extra as npc(sl, n1, 12, 5, 14, 3)
  assert(result == true)
  assert(extra length(sl) == 5 * 2)
  assert(get extra(sl, 0) == 12)
  assert(get extra(sl, 1) == 5)
  assert(get extra(sl, 8) == 14)
  assert(get extra(sl, 9) == 3)
  visualize path from extra(sl)
  w
  # Around the grass
  result := pathfind into extra as npc(sl, n0, 12, 5, 14, 3)
  assert(result == true)
  assert(extra length(sl) == 9 * 2)
  assert(get extra(sl, 0) == 12)
  assert(get extra(sl, 1) == 5)
  assert(get extra(sl, extra length(sl)--2) == 14)
  assert(get extra(sl, extra length(sl)--1) == 3)
  visualize path from extra(sl)
  w
  # around the grass and the hero
  set hero position(me, 11, 3)
  result := pathfind into extra as npc(sl, n0, 12, 5, 14, 3)
  assert(extra length(sl) == 11 * 2)
  visualize path from extra(sl)
  w
  # Can't reach the middle of the grass
  result := pathfind into extra as npc(sl, n0, 12, 5, 13, 4)
  assert(result == false)
  assert(extra length(sl) == 1 * 2)
  visualize path from extra(sl)
  w
  # Zero-length path (start excluded)
  result := pathfind into extra as npc(sl, n0, 12, 5, 13, 4, 0, false, true)
  assert(result == false)
  assert(extra length(sl) == 0 * 2)
  visualize path from extra(sl)
  w
  # Append 3 waypoints (full circle)
  pathfind into extra as npc(sl, n0, 12, 5, 14, 3, 0, false, true)
  pathfind into extra as npc(sl, n0, 14, 3, 15, 7, 0, true, true)
  pathfind into extra as npc(sl, n0, 15, 7, 12, 5, 0, true, true)
  assert(extra length(sl) == 24 * 2)
  visualize path from extra(sl)
  w
  # Cleanup
  free slice(sl)
  
  teleport to map(oldmap, oldx, oldy)
end

script, create map overlay container, begin
  variable(sl)
  sl := create container(0, 0)
  set parent(sl, lookup slice(sl:map overlay))
  fill parent(sl, true)
  exit(sl)
end

script, visualize path from extra, sl, offset=0, begin
 free slice children(sl)
 variable(i, x, y, box)
 for(i, 0, ((extra length(sl) -- offset) / 2) -- 1) do(
   x := get extra(sl, offset + i * 2)
   y := get extra(sl, offset + i * 2 + 1)
   box := create rect(18, 18)
   set parent(box, sl)
   set rect bgcol(box, 1)
   set rect opacity(box, 25)
   set slice x(box, x * 20 + 1)
   set slice y(box, y * 20 + 1)
 )
end

script, is at pos, ref, x, y, begin
  exit returning(npc x (ref) == x && npc y (ref) == y)
end

script, dist from leader, ref, begin
  exit returning(dist from pos(ref, hero x(me), hero y(me)))
end

script, dist from pos, ref, x, y, begin
  variable(dx, dy)
  dx := abs(npc x(ref) -- x)
  dy := abs(npc y(ref) -- y)
  exit returning(dx + dy)
end

########################################################################

script, vehicle tests, begin
  $0="Vehicle tests"
  
  variable(oldx, oldy, oldmap)
  oldx := herox(me)
  oldy := heroy(me)
  oldmap := current map
  
  teleport to map(map:Vehicle Test Map, 4, 4)
  w
  
  assert(current vehicle id == -1)
  assert(current vehicle npc == false)
  
  # First test mount with usenpc. Does not need to be adjacent, but does check mount passability rules
  assert(check tag(tag:boat tag) == false)
  assert(hero x(me) == 4)
  assert(hero y(me) == 4)
  w
  # This should fail to mount because we are not on a "B" tile
  use npc(1)
  assert(current vehicle npc == false)
  w(20)
  assert(check tag(tag:boat tag) == false)
  assert(hero x(me) == 4)
  assert(hero y(me) == 4)
  walk hero(me, left, 2)
  wait for hero(me), w
  assert(hero x(me) == 2)
  assert(hero y(me) == 4)
  # This should work because although we are not adjacent, we are standing on a B tile
  use npc(1)
  assert(current vehicle npc == NPC reference(1))
  assert(current vehicle id == 0)
  w(20)
  assert(check tag(tag:boat tag) == true)
  assert(hero x(me) == 2)
  assert(hero y(me) == 2)
  # Now move the hero, and the vehicle npc should move also
  walk hero(me, left, 2)
  wait for hero(me), w
  walk hero(me, up, 2)
  wait for hero(me), w
  assert(hero x(me) == 0)
  assert(hero y(me) == 0)
  assert(npc x(1) == 0)
  assert(npc y(1) == 0)
  # Moving the NPC while it is being used as a vehicle should not move it
  # (although the sprite will be shifted slightly as it tries to move)
  walk npc(1, right, 5)
  wait for npc(1), w
  assert(npc x(1) == 0)
  assert(npc y(1) == 0)
  # Move back to the shore
  walk hero(me, right, 3)
  wait for hero(me), w
  walk hero(me, down, 2)
  wait for hero(me), w
  show string(string sprintf(1, $2="x=%d y=%d", hero x(me), hero y(me)))
  assert(hero x(me) == 3)
  assert(hero y(me) == 1)
  set hero direction(me, right), w
  # Dismount into the ocean. should not step forward
  assert(current vehicle npc)
  dismount vehicle
  assert(not(current vehicle npc))
  w(20)
  assert(hero x(me) == 3)
  assert(hero y(me) == 1)
  assert(check tag(tag:boat tag) == false)
  # Mounting from here should fail
  use npc(1)
  assert(not(current vehicle npc))
  w(20)
  assert(hero x(me) == 3)
  assert(hero y(me) == 1)
  assert(check tag(tag:boat tag) == false)
  # Force mounting, which ignores checking the tile we are standing on
  force mount vehicle(1)
  assert(current vehicle npc)
  w(20)
  assert(hero x(me) == 3)
  assert(hero y(me) == 1)
  assert(check tag(tag:boat tag) == true)
  walk hero(me, right, 1)
  wait for hero(me), w
  assert(hero x(me) == 4)
  assert(hero y(me) == 1)
  assert(npc x(1) == 4)
  assert(npc y(1) == 1)
  # Dismount, does not step ahead (which sucks, and was a mistake, but is rather too old to fix cleanly)
  dismount vehicle, w(20)
  assert(not(current vehicle npc))
  assert(check tag(tag:boat tag) == false)
  assert(hero x(me) == 4)
  assert(hero y(me) == 1)
  assert(npc x(1) == 4)
  assert(npc y(1) == 1)
  w
  
  set hero position(me, 4, 2)
  variable(n)
  n := create global npc(3, 2, 5)
  hero chases npc(me, n, true, 10)
  wait for hero(me)
  set hero direction(me, left)
  use npc(n)
  w(20)
  walk hero(me, right, 5)
  w
  wait for hero(me)
  gracefully dismount vehicle
  w(20)
  walk hero(me, right, 1)
  w
  wait for hero(me)
  walk hero(me, south, 4)

  w
  teleport to map(oldmap, oldx, oldy)
end

########################################################################

script, door tests, begin
  $0="Door tests"

  # preconditions
  assert(herox(me) == 6)
  assert(heroy(me) == 6)

  # querying doors
  assert(get door x(0) == 8)
  assert(get door y(0) == 5)
  assert(door at spot(8, 5) == 0)
  assert(door at spot(0, 0) == -1)  # lots of doors at 0,0 with exists=NO
  assert(door at spot(1, 1) == 1)   # door 1 has no enabled doorlinks
  assert(get door destination id(0) == 1)
  assert(get door destination map(0) == 4)
  assert(door exists(0))
  assert(get door destination id(1) == -1)
  assert(get door destination map(1) == -1)
  assert(door exists(1))
  assert(door exists(10) == false)

  # using doors
  walk hero to x(0, 8)
  wait for hero
  walk hero to y(0, 5)
  wait for hero
  assert(current map == map:door test map)
  assert(hero x(0) == 4)
  assert(hero y(0) == 4)
  walk hero(0, south, 1)
  wait for hero
  use door(1)  # shouldn't work, disabled by tag
  assert(hero x(0) == 4)  # not triggered
  walk hero(0, north, 1)
  wait for hero
  assert(hero x(0) == 4)  # not triggered
  set tag(tag:door req tag, on)
  wait
  assert(hero x(0) == 4)  # not triggered by enabling a door underneath you
  suspend doors
  walk hero(0, south, 1)
  wait for hero
  walk hero(0, north, 1)
  wait for hero
  assert(hero x(0) == 4)  # not triggered
  resume doors
  wait
  assert(hero x(0) == 4)  # door underneath hero not triggered
  walk hero(0, south, 1)
  wait for hero
  walk hero(0, north, 1)
  wait for hero
  assert(hero x(0) == 11)  # triggered
  assert(hero y(0) == 4)
  show string($99="screen should be faded out")
  use door(0)  # door underneath hero
  _checkpoint
  assert(current map == map:test town)
  show string($99="screen should not be faded out")
  use door(4, false)
  _checkpoint
  show no value
  assert(hero x(0) == 2)
  assert(hero y(0) == 6)
  walk hero to x(0, 6)
  wait for hero

  # querying doors on other maps
  assert(current map == map:test town)
  assert(door exists(0, map:Door test map))
  assert(not(door exists(2, map:Door test map)))
  assert(get door x(0, map:Door test map) == 11)
  assert(get door y(0, map:Door test map) == 4)
  assert(check tag(7) == on)
  assert(get door destination id(0, map:Door test map) == 0)
  assert(get door destination map(0, map:Door test map) == map:Test Town)
  assert(get door destination id(1, map:Door test map) == 0)
  assert(get door destination map(1, map:Door test map) == map:Door test map)
  set tag(7, off)
  assert(get door destination id(1, map:Door test map) == -1)
  assert(get door destination map(1, map:Door test map) == -1)
  set tag(7, on)
  assert(get door destination id(2, map:Door test map) == -1)
  assert(get door destination map(2, map:Door test map) == -1)
  

end

########################################################################

# Not defined as a plotscript on purpose
script, each-step script, x, y, dir, begin
  $0="each-step script", trace
  assert(x == hero x)
  assert(y == hero y)
  assert(dir == hero direction)

  # First to run
  assert(npc script triggered == 0)
  assert(map autorun triggered == 0)
  eachstep triggered += 1
end

plotscript, npc triggered script, arg, npc, begin
  $0="npc triggered script", trace
  assert(arg == -1)
  assert(npc == npc reference(6))

  # Second to run
  assert(eachstep triggered == 1)
  assert(map autorun triggered == 0)
  assert(move hero script triggered == 0)
  npc script triggered += 1

  # Should happen immediately (also tested in textbox tests)
  # and trigger two scripts
  advance textbox

  # Sixth to run
  assert(current textbox == -1)
  assert(textbox script triggered == 1)
  assert(map autorun triggered == 1)
  assert(move hero script triggered >= 1)
  assert(timer script triggered == 0)
  npc script triggered += 1
end

plotscript, textbox advance script, begin
  $0="textbox advance script", trace
  assert(current map == 4)
  assert(hero direction == left)

  # These should be the correct position on the new map
  assert(hero x == 11)
  assert(hero y == 4)

  # Third to run
  assert(current textbox == -1)
  assert(npc script triggered == 1)
  assert(eachstep triggered == 1)
  assert(map autorun triggered == 0)
  assert(move hero script triggered == 0)
  textbox script triggered += 1
end

# Set on map 4
plotscript, map 4 autorun, arg, begin
  if (testing script triggers == false) then (exit script)
  $0="map 4 autorun", trace

  assert(arg == 42)
  assert(current map == 4)

  # Fourth to run
  assert(textbox script triggered == 1)
  assert(move hero script triggered == 0)
  map autorun triggered += 1   # also set in "map autorun"
end

script, called from move hero hook, begin
  $0="from move hero hook", trace
  # Fifth to run
  assert(current textbox == -1)
  assert(npc script triggered == 1)
  assert(eachstep triggered == 1)
  assert(textbox script triggered == 1)
  assert(map autorun triggered == 1)
  move hero script triggered += 1
  # Note: since Freki is the only hero, swapping them out swaps them
  # back in, and this hook is called twice. I think that's a bug.
end

# Seventh to run
plotscript, timer triggered script, begin
  $0="timer triggered script", trace
  assert(move hero script triggered >= 1)
  assert(npc script triggered == 2)
  timer script triggered += 1
end


script, concurrent script trigger tests, begin
  $0="concurrent script trigger tests"

  # Test multiple triggers happening the same tick, and that they all happen in the right order.
  # This tests both scripts triggered inside and outside the interpreter

  # Take a step, triggering each-step script and stepping on an NPC, which runs a script,
  # and opens a textbox, which is advanced, which swaps out a hero (calling move hero hook),
  # runs a script and activates a door (which goes to a map with an autorun script)

  # Preconditions
  assert(current map == 0)
  assert(hero by slot(0) == hero:Freki)
  set hero position(0, 6, 6)
  npc script triggered := 0
  move hero script triggered := 0
  map autorun triggered := 0
  timer script triggered := 0

  set eachstep script (@each-step script)
  testing script triggers := true

  start tick := ticknumber
  assert(get hero speed(me) == 4)

  set timer(1, 0, 5, @timer triggered script)  # Triggered on the same tick as second step

  create npc(6, hero x -- 1, hero y)
  walk hero(me, left, 1)
  wait for hero

  # Events happen and scripts trigger in this order, and run in reverse order of triggering:
  # (timers are updated)
  #  timer triggered script
  # (hero move logic, step finished)
  #  (npc 6 triggered, opens textbox 16)
  #    npc triggered script
  #   each-step script
  # (builtin logic finishes, the frame is drawn, start of next main loop tick)
  # (interpreter entered)
  #  (npc script calls advancetextbox:)
  #   (hero is swapped out)
  #    move hero hook
  #   (door is forced)
  #    (preparemap called)
  #     map 4 autorun
  #   textbox advance script (After text box trigger)
  # (this script resumes, waitfornpc finishing)

  # So scripts run in order (all in the same tick):
  # | each step
  # | npc
  # | | textbox
  # | | autorun
  # | | move hero hook
  # | (npc finishes)
  # | timer
  # this script

  # FIXME: add/remove/move hero hooks have a higher priority than other triggers (affects
  # fibres triggered on the same tick only), but currently that has no effect if triggered
  # while inside interpreter. It should, so the move hero hook should run earlier.

  # Eighth to run
  assert(npc script triggered == 2)
  assert(textbox script triggered == 1)
  assert(map autorun triggered == 1)
  assert(move hero script triggered >= 1)
  assert(timer script triggered == 1)
  assert(ticknumber == start tick + 5)

  # reset
  testing script triggers := false
  teleport to map (0, 6, 6)
  assert(current map == 0)
  # Freki is only hero in the party, so they wouldn't have actually been swapped out
  assert(hero by slot(0) == hero:Freki)
  move hero count -= move hero script triggered
  # Incremented by the other map autorun script
  assert(map autorun triggered == 2)
end

########################################################################

script, another eachstep script, begin
  eachstep triggered += 1
  if (hero is walking) then (
    # Note: always waits at least one tick!
    wait for hero
  )
end

script, yet another eachstep script, begin
  eachstep triggered += 1
  subscript, waiting, begin
    wait for hero
  end
  waiting
end

# Test "permit double triggering of scripts"
script, permit double triggering tests, begin
  $0="permit double triggering tests"

  #### "permit double-triggering of scripts" is off
  variable (oldval)
  oldval := read general(101)  # genBits
  assert((oldval, and, 2^10) == 0)

  test double triggering (off)

  #### Turn on "permit double-triggering of scripts"
  write general(101, oldval + 2^10)

  test double triggering (on)

  #### Turn off "permit double-triggering of scripts"
  write general(101, oldval)
end


script, waitless script, begin
  ticker 1 += 1
end

script, blocker script 3, begin
  wait
  ticker 1 += 1
end

# Run twice, with permitted = true/false
script, test double triggering, permitted, begin
  $0="test double triggering"
  tracevalue(permitted)

  # precondition
  assert(hero x == 6 && hero y == 6)
  assert(get hero speed == 4)
  #set hero position(me, 6, 6)

  # 1. Basic case: eachstep script that's already running
  set eachstep script(@another eachstep script)
  eachstep triggered := 0
  walk hero(me, left, 3)
  wait for hero
  if (permitted) then (
    assert(eachstep triggered == 3)
  ) else (
    # eachstep should only trigger once
    assert(eachstep triggered == 1)
  )

  # 2. A plotscript that was triggered and started running, hasn't called any other script, but has
  # been buried another plotscript defeats the double trigger check.
  # 2.1 ...buried under a script that doesn't wait.

  # Repeat with varying timing. Only when delay=10 is the timer script triggered on the same
  # tick as the hero's second step and possibly defeats the double-trigger check.

  subscript, walk and trigger, begin
    # Setup
    set hero position(0, 3, 6)
    eachstep triggered := 0
    ticker 1 := 0
    start tick := ticknumber

    walk hero(me, right, 2)
    wait for hero(me)
    assert(hero x == 5)
    wait(1)  # needed for delay=11
    assert(ticker 1 == 1)
  end

  variable(delay)
  for (delay, 9, 11) do (
    set timer(1, 0, delay, @waitless script)
    walk and trigger

    if (permitted || delay == 10) then (
      assert(eachstep triggered == 2)
    ) else (
      assert(eachstep triggered == 1)  # Didn't double trigger
    )
    assert(ticknumber == start tick + 11)
  )

  # 2.2 ...buried under a script that's waiting
  for (delay, 8, 11) do (
    # When delay=9, triggered on the tick before the second step, and has a wait(1) so still there the next tick
    # When delay=10, triggered on same tick. (Not waiting, so not what we're trying to test)

    set timer(1, 0, delay, @blocker script 3)
    walk and trigger

    if (permitted || delay == 9 || delay == 10) then (
      assert(eachstep triggered == 2)
    ) else (
      assert(eachstep triggered == 1)  # Didn't double trigger
    )
    if (delay >= 10) then (
      assert(ticknumber == start tick + 12)
    ) else (
      assert(ticknumber == start tick + 11)
    )
  )

  # 3. eachstep script that's the current active script fibre, but has called another script
  # also defeats the double trigger check
  set hero position(0, 3, 6)
  set eachstep script(@yet another eachstep script)
  eachstep triggered := 0
  walk hero(me, right, 3)
  wait for hero
  # Regardless of permitted, eachstep should trigger three times because the "waiting"
  # subscript is topmost
  assert(eachstep triggered == 3)

  set eachstep script(0)
  assert(hero x == 6 && hero y == 6)

  # 4. Not all script triggers check for double triggering. Check timers don't.
  # Trigger same script twice at once.
  ticker 1 := 0
  start tick := ticknumber
  # Each script is "wait, ticker 1 += 1"
  set timer(1, 0, 1, @blocker script 3)
  set timer(2, 0, 1, @blocker script 3)  # Runs first
  wait
  assert(ticker 1 == 2)
  assert(ticknumber == start tick + 3)
end

########################################################################

# Test the interaction of triggered script fibres, including ordering,
# blocking, double-triggering, and two back-compat bits.
# This makes use of timers, but the real timer tests are elsewhere.
# (Timer tests happen after these tests!)
# Triggering of each specific script trigger type is mostly in
# test suites for npcs, menus, battles, etc.
script, script trigger interference tests, begin
  plotscript blocking tests

  # The following three all check scripts triggered on the same tick:
  # -whether the scripts are triggered in the right order
  concurrent script trigger tests
  # -whether the scripts are triggered at all
  permit double triggering tests
  # -whether that causes waits to be skipped
  bug 430 emulation tests
end

########################################################################

script, slice tests, begin
  $0="Slice tests"

  # Highly unfinished!

  slice extra tests
  slice collection tests
  slice tree tests
  slice search tests
  slice collision tests
  slice correspondence tests
  slice blending tests
  slice sort tests
  slice clone tests
  slice handle tests
  slice filling and covering tests
  slice refresh tests

  sprite slice tests
  rect slice tests
  text slice tests
  panel slice tests
  ellipse slice tests
end

########################################################################

script, slice filling and covering tests, begin
  $0="Slice filling and covering tests"
  
  variable(par, sl, ch, ch2, grandch)
  par := create rect(150, 150, 0)
  center slice(par)
  set padding(par, 4)
  
  sl := create rect(0, 0, 1)
  fill parent(sl, true)
  w
  assert(slice width(sl) == 320)
  assert(slice height(sl) == 200)
  
  set parent(sl, par)
  w
  assert(slice width(sl) == 142)
  assert(slice height(sl) == 142)
  
  set slice width(par, 160)
  w
  assert(slice width(sl) == 152)
  assert(slice height(sl) == 142)

  ch := load hero sprite(1)  # 32x40
  ch2 := load hero sprite(2)  # 32x40
  set parent(ch, sl)
  set parent(ch2, sl)
  set fill parent(sl, fill:none)
  set cover children(sl, cover:all)
  w

  assert(slice width(sl) == 32)
  assert(slice height(sl) == 40)
  set padding(sl, 2)
  w
  assert(slice width(sl) == 36)
  assert(slice height(sl) == 44)
  w
  # Children start anchored top left.
  set slice x(ch, 20)
  set slice y(ch, 10)
  w
  assert(slice width(sl) == 56)
  assert(slice height(sl) == 54)
  
  # A left anchored child only matters for its right edge, so doesn't get covered if its right edge is negative
  set slice x(ch, -20)
  w
  assert(slice width(sl) == 36)
  
  # Right aligned child only matters for its left edge, so doesn't get covered if its left edge is positive
  realign slice(ch, edge:right, edge:top, edge:right, edge:top)
  set slice x(ch, 50)
  w
  assert(slice width(sl) == 36) # Not covering the right-aligned slice, is by design here
  set slice x(ch, -50)
  w
  assert(slice width(sl) == 86)

  # For a center aligned slice, both edges matter  
  realign slice(ch, edge:center, edge:top, edge:center, edge:top)
  set slice x(ch, 0)
  w
  assert(slice width(sl) == 36)

  set slice x(ch, -50)
  w
  assert(slice width(sl) == 136)
  set slice x(ch, 50)
  w
  assert(slice width(sl) == 136)
  
  # Now reset the sprite positions and test covering only in one axis
  realign slice(ch, edge:left, edge:top, edge:left, edge:top)
  set slice x(ch, 32)
  set slice y(ch, 0)
  w
  assert(slice width(sl) == 68)
  assert(slice height(sl) == 44)

  # changing size manually would error out and we can't assert error yet
  #set slice width(sl, 10) #expected to error
  #set slice height(sl, 10) #expected to error
  #w
  
  assert(slice width(sl) == 68)
  assert(slice height(sl) == 44)
  # Change to horiz covering. No change to size yet
  set cover children(sl, cover:horiz)
  w
  assert(slice width(sl) == 68)
  assert(slice height(sl) == 44)
  # Change vertical size
  set slice height(sl, 10)
  assert(slice width(sl) == 68)
  assert(slice height(sl) == 10)
  w
  # Change to vertical covering
  set cover children(sl, cover:vert)
  w
  assert(slice width(sl) == 68)
  assert(slice height(sl) == 44)
  # Change horiz size
  set slice width(sl, 10)
  assert(slice width(sl) == 10)
  assert(slice height(sl) == 44)

  # Now turn off covering, we will test filling next
  set cover children(sl, cover:none)
  set fill parent(sl, fill:all)
  w
  assert(slice width(sl) == 152)
  assert(slice height(sl) == 142)
  # Shrink the parent a bit
  set slice width(par, 120)
  set slice height(par, 110)
  w
  assert(slice width(sl) == 112)
  assert(slice height(sl) == 102)
  # Test filling only horizontally
  set fill parent(sl, fill:horiz)
  w
  # No change yet
  assert(slice width(sl) == 112)
  assert(slice height(sl) == 102)
  # Now shrink vertically
  set slice height(sl, 10)
  w
  assert(slice width(sl) == 112)
  assert(slice height(sl) == 10)
  # Next fill vertically
  set fill parent(sl, fill:vert)
  w
  assert(slice width(sl) == 112)
  assert(slice height(sl) == 102)
  # Now shrink horizontally
  set slice width(sl, 10)
  w
  assert(slice width(sl) == 10)
  assert(slice height(sl) == 102)
  
  # Now check filling and covering at the same time
  # Opposite axises coexist peacefully
  set fill parent(sl, fill:horiz)
  set cover children(sl, cover:vert)
  w
  assert(slice width(sl) == 112)
  assert(slice height(sl) == 44)
  # And the other way...
  set fill parent(sl, fill:vert)
  set cover children(sl, cover:horiz)
  w
  assert(slice width(sl) == 68)
  assert(slice height(sl) == 102)
  
  # When both are active at the same time, fill takes priority, and cover does nothing
  set fill parent(sl, fill:all)
  set cover children(sl, cover:all)
  w
  assert(slice width(sl) == 112)
  assert(slice height(sl) == 102)
  # That should remain true even if a covered child goes outside of the parent that is being filled
  set slice x(ch, 150)
  w
  assert(slice width(sl) == 112)
  assert(slice height(sl) == 102)
  
  # Now test covering grandchildren (they don't get covered, only children!)
  set fill parent(sl, fill:none)
  set cover children(sl, cover:all)
  set slice x(ch, 0)
  set parent(ch2, ch)
  set slice y(ch2, 30)
  w
  assert(slice width(sl) == 36)
  assert(slice height(sl) == 44)
  # change the grandchild back into a child, it will be covered
  gently reparent(ch2, sl)
  w
  assert(slice width(sl) == 36)
  assert(slice height(sl) == 74)

  # Test non-visible children are ignored
  set slice visible(ch2, false)
  w
  assert(slice width(sl) == 36)
  assert(slice height(sl) == 44)
  set slice visible(ch2, true)

  # Test recursive covering, where a child (ch3) is also set to cover (covering ch)
  # ch is a sprite, we need a resizable slice
  grandch := ch
  ch := create rect(10, 10)
  set parent(ch, sl)
  set parent(grandch, ch)
  # Tree:
  # par -> sl -> ch -> grandch
  #          '-> ch2
  put slice(ch, 30, 40)
  set cover children(ch, cover:vert)
  w
  assert(slice width(ch) == 10)
  assert(slice height(ch) == 40)
  assert(slice width(sl) == 44)   # ch's x=30 + w=10 + sl's padding=4
  assert(slice height(sl) == 84)  # ch's y=40 + h=40 + sl's padding=4
  set cover children(ch, cover:all)
  set slice y(grandch, -18)
  w
  assert(slice width(ch) == 32)
  assert(slice height(ch) == 22)  # grandch's h=40 + y=-18
  assert(slice width(sl) == 66)   # ch's x=30 + w=32 + sl's padding=4
  assert(slice height(sl) == 74)  # From ch2 again

  # Test covering is applied by a refresh, not just a screen draw
  # Check refreshing the slice (sl) set to cover
  set slice y(ch2, 39)
  slice collide point(sl, 0, 0)  # Causes refresh
  assert(slice height(sl) == 83)  # grandch'2 y=39 + h=40 + sl.padding=4
  # Check refreshing a child (ch) & grandchild of the slice set to cover
  # (not the one that determines the cover size, ch2)
  set slice y(ch2, 40)
  slice collide point(grandch, 0, 0)
  assert(slice height(sl) == 84)  # grandch'2 y=40 + h=40 + sl.padding=4
  # Check refreshing a slice (sl) that's recursively covering
  set slice y(grandch, 10)
  slice collide point(sl, 0, 0)
  # FIXME: these fail because RefreshSliceAscend doesn't refresh children of covering slices
  #assert(slice height(ch) == 50)  # grandch's h=40 + y=10
  #assert(slice height(sl) == 94)   # ch's y=40 + h=50 + sl's padding=4
  # Check refreshing a grandchild of the slice that's recursively covering, the one actually determining the size
  set slice y(grandch, 11)
  slice collide point(ch, 0, 0)
  assert(slice height(ch) == 51)  # granch's h=40 + y=11
  assert(slice height(sl) == 95)   # ch's y=40 + h=51 + sl's padding=4
  # Check refreshing a child (ch2) of the slice that's recursively covering, not one determining the size (ch+grandch)
  set slice y(ch, 55)
  set slice y(grandch, 12)
  slice collide point(ch2, 0, 0)
  # FIXME: these fail because RefreshSliceAscend doesn't refresh children of covering slices
  #assert(slice height(ch) == 52)  # grandch's h=40 + y=12
  #assert(slice height(sl) == 111)  # ch's y=55 + h=52 + sl's padding=4

  # Now verify that "fill parent" has no effect on fill type
  # and that "is filling parent" doesn't care about fill type
  set fill parent(sl, fill:all)
  set cover children(sl, cover:none)
  w
  assert(slice width(sl) == 112)
  assert(slice height(sl) == 102)
  assert(is filling parent(sl) == true)
  fill parent(sl, false)
  set slice width(sl, 10)
  set slice height(sl, 10)
  assert(is filling parent(sl) == false)
  w
  assert(slice width(sl) == 10)
  assert(slice height(sl) == 10)
  set fill parent(sl, fill:horiz)
  w
  assert(slice width(sl) == 112)
  assert(slice height(sl) == 10)
  assert(is filling parent(sl) == true)
  w
  
  free slice(par)
end

# Tests the edgecases of slice refreshing: when exactly it occurs, in which order things
# happen, and how features interact (if not tested elsewhere, e.g. recursive cover children
# and fill parent + cover children are in "slice filling and covering tests")
script, slice refresh tests, begin
  $0="slice refresh tests"

  variable(par, vispar, spr, text, rect, dot, dot2, select)
  par := create rect(50, 51, 0)
  set slice visible(par, false)
  vispar := create rect(60, 61, 0)
  rect := create rect(0, 0)
  dot := create container(0, 0)
  dot2 := create container(0, 0)

  # Setup & preconditions
  assert(slice collide point(rect, 0, 0) == false)  # Zero-size slices don't collide with points
  # Setting fill doesn't have an immediate effect on size
  set parent(rect, par)
  fill parent(rect, true)
  assert(slice width(rect) == 0)
  assert(slice collide point(rect, 0, 0) == true)  # Cause refresh
  assert(slice width(rect) == 50)
  assert(slice height(rect) == 51)

  # Setting x/y of a slice set to fill has no effect on screenx/y but x/y are preserved
  put slice(rect, 10, 20)
  assert(slice screen x(rect) == 0)  # Cause refresh
  assert(slice x(rect) == 10)
  set fill parent(rect, fill:none)
  assert(slice screen x(rect) == 10)  # Cause refresh
  # Size unchanged after removing fill
  assert(slice width(rect) == 50)

  # The following two blocks check Cover children and Fill aren't applied to invisible slices,
  # except if they are refreshed

  set parent(dot, rect)
  put slice(dot, 10, 20)
  # No immediate effect of setting cover
  set cover children(rect, cover:all)
  assert(slice width(rect) == 50)
  # par isn't visible, so a wait to draw the screen doesn't refresh it or rect
  wait
  assert(slice width(rect) == 50)
  assert(slice screen x(rect) == 10)  # Cause refresh
  # After refresh, size determined by cover children
  assert(slice width(rect) == 10)

  # Test Sprites set to Fill
  # They can be set to fill only in the slice editor (kept only for backcompat)
  # but not with using scripting. (FIXME: Even fill parent(spr, false) is disallowed.)

  # The root of collection 3 is a sprite set to fill. It's size 320x200
  # in the editor but gets momentarily reset to 32x40 when loaded.
  spr := load slice collection(3)
  assert(slice width(spr) == 32)  # Current behaviour, not sure whether to enforce
  assert(slice height(spr) == 40)
  # Fill an invisible parent
  # Changing parent doesn't have an immediate effect
  set parent(spr, par)
  assert(slice width(spr) == 32)
  assert(slice height(spr) == 40)
  # Drawing the screen has no effect either, because the slice isn't visible
  wait
  assert(slice width(spr) == 32)
  assert(slice height(spr) == 40)
  assert(slice collide point(spr, 49, 49) == true)  # Cause refresh
  # Fill overrides sprite size
  assert(slice width(spr) == 50)
  assert(slice height(spr) == 51)
  # Filling a visible parent
  set parent(spr, vispar)
  # No immediate effect
  assert(slice width(spr) == 50)
  # Drawing refreshes visible slices
  w
  assert(slice width(spr) == 60)

  # Can't set cover children on a sprite slice, so nothing to test.
  #assert error(set cover children(spr, cover:all))

  #### Test refresh timing of wrapping and non-wrapping text slices

  text := create text
  set slice text(text, $2="a b c d e f")
  set parent(text, par)
  set slice x(text, 17)
  assert(slice width(text) == 88)
  assert(slice height(text) == 10)
  assert(slice screen x(text) == 17)
  #assert error(set slice width(text, 30))  # Doesn't wrap, can't set width
  # Test fill parent + wrapping text
  set wrap(text, true)
  # Turning on wrap by itself has no effect, because we haven't changed the width
  # ("set slice width" fails on a wrapping slice)
  assert(slice width(text) == 88)
  assert(slice height(text) == 10)
  # No immediate effect of setting width or fill
  set slice width(text, 60)
  set fill parent(text, fill:all)
  assert(slice width(text) == 60)
  assert(slice height(text) == 10)
  # Drawing the screen has no effect either, because the slice isn't visible
  wait
  assert(slice width(text) == 60)
  assert(slice height(text) == 10)
  # Refresh but no draw: fill takes effect
  assert(slice screen x(text) == 0)  # Cause refresh
  assert(slice width(text) == 50)
  #assert(slice height(text) == 51)  # Current behaviour: fill sets the height
  #assert(slice height(text) == 20)  # FIXME: desired behaviour

  # Text slices set their height when drawn (FIXME: should be on refresh instead).
  # Therefore they don't get resized vertically by Fill (tested below) unless refreshed when not visible
  # (FIXME: that's undesired)
  set parent(text, vispar)
  w
  assert(slice width(text) == 60)
  assert(slice height(text) == 20)
  # Removing fill from a text slice changes nothing
  set fill parent(text, fill:none)
  wait
  assert(slice width(text) == 60)
  assert(slice height(text) == 20)
  # Setting align/anchor doesn't cause a refresh
  set slice width(text, 100)
  set vert align(text, edge:bottom)
  set vert anchor(text, edge:bottom)
  assert(slice width(text) == 100)
  # On refresh, the height is updated before screen pos is calculated using alignment.
  put slice(dot, 0, 0)
  set parent(dot, text)  # Use dot to get the screen y of the top edge of text
  # FIXME: this fails, have to draw the text slice, as SliceRefreshScreenPos doesn't update text slices
  #assert(slice screen y(dot) == 61 -- 10)  # Cause refresh
  wait
  assert(slice screen y(dot) == 61 -- 10)
  assert(slice width(text) == 100)
  assert(slice height(text) == 10)

  # It's possible to change the height of a text slice but the effect disappears on refresh
  set slice height(text, 99)
  #assert(slice height(text) == 99)  # Current behaviour but might want to change that
  # FIXME: assert fails without an extra draw (screen y is 61 - 99 = -38) because SliceRefreshScreenPos doesn't update text slices
  wait  # Draw 'text'
  assert(slice screen y(dot) == 61 -- 10)  # Cause refresh
  assert(slice height(text) == 10)

  # The following causes a one-tick-visible glitch: a wrapping text slice with vert anchor Center or
  # Bottom (align doesn't matter), when turning on horizontal fill (not vert, as that overrides vert
  # anchor). Requires a draw and a 2nd refresh
  # Preconditions & checks
  set slice width(text, 24)
  w
  assert(slice height(text) == 30)
  assert(slice screen y(dot) == 61 -- 30)  # Cause refresh and sanity check
  set fill parent(text, fill:horiz)  # Width 60 Height 20 after wrap
  wait  # FIXME: assert fails without this extra wait
  assert(slice screen y(dot) == 61 -- 20)  # Cause refresh

  # Changing *any* text property such calls UpdateTextSlice which immediately recomputes
  # the size, which is silly
  set slice height(text, 99)
  assert(slice height(text) == 99)
  set text color(text, 1)
  #assert(slice height(text) == 20) # Current behaviour, but don't want to enforce

  # Test fill parent without wrapping text
  set fill parent(text, fill:all)
  wait  # Cause refresh
  # In particular, changing wrap calls UpdateTextSlice, immediately recomputing
  # size but doesn't refresh, so it overrides fill
  set wrap(text, false)
  #assert(slice width(text) == 88)  # Current behaviour, but don't want to enforce
  assert(slice height(text) == 10)

  # Special case for Text: although a text slice set to fill parent does not have its height set by fill,
  # it does have its screen y set by fill, disregarding Y and alignment/anchor.
  put slice(text, 200, 200)
  assert(slice screen x(text) == 0)  # Cause refresh
  assert(slice screen y(text) == 61)
  assert(slice width(text) == 60)
  assert(slice height(text) == 61)  # FIXME: Current behaviour, undesired
  #assert(slice height(text) == 10)  # FIXME: desired behaviour
  # dot measures top edge of text
  #assert(slice screen y(dot) == 61 -- 10)  # FIXME: desired behaviour
  w  # Cause refresh+draw
  # Same as wrapping text, Fill overrides width but not height, which is quite bizarre
  assert(slice width(text) == 60)
  assert(slice height(text) == 10)

  #### Select slices

  select := create select(110, 111)
  set parent(dot, select)
  set parent(dot2, select)
  # Setting the select index doesn't cause a refresh
  set select slice index(select, 1)
  assert(get slice visible(dot) == true)
  assert(get slice visible(dot2) == true)
  slice screen x(dot)  # Cause refresh
  assert(get slice visible(dot) == false)
  assert(get slice visible(dot2) == true)

  #### Autosorting

  set child autosort(select, autosort: top y)
  set slice y(dot, 20)
  assert(slice child index(dot) == 0)
  assert(slice child index(dot2) == 1)
  # A refresh by itself doesn't autosort
  slice screen y(dot)  # Cause refresh
  assert(slice child index(dot) == 0)
  assert(slice child index(dot2) == 1)
  assert(get slice visible(dot) == false)
  assert(get slice visible(dot2) == true)
  # Drawing does
  w
  assert(slice child index(dot) == 1)
  assert(slice child index(dot2) == 0)
  assert(get slice visible(dot) == true)
  assert(get slice visible(dot2) == false)

  # Test autosorting grid slices happens before the children are refreshed
  variable(grid, cell1, cell2)
  grid := create grid(20, 20)
  cell1 := create container
  cell2 := create container
  set grid rows(grid, 2)
  set grid columns(grid, 2)
  set child autosort(grid, autosort: custom)
  set parent(cell1, grid)
  set parent(cell2, grid)
  set sort order(cell1, 10)
  set sort order(cell2, 0)
  # TODO: uncomment once this command is implemented (untl then, need a wait)
  #refresh slice tree(grid, true)  # Refresh with autosorting
  wait # TODO: remove
  assert(slice screen x(cell1) == 10)
  assert(slice screen x(cell2) == 0)


  #### Test certain commands cause refreshes

  subscript, set filling slice width, sl, wid, begin
    fill parent(sl, false)
    set slice width(sl, wid)
    fill parent(sl, true)
    assert(slice width(sl) == wid)
  end

  # Preconditions
  set cover children(rect, cover:none)
  set parent(rect, par)
  fill parent(par, false)
  slice screen y(rect)  # Cause refresh
  fill parent(rect, false)
  set slice width(rect, 30)
  set parent(dot, rect)
  put slice(dot, 40, 1)
  set slice width(rect, 30)
  assert(slice collide(rect, dot) == false)

  # Test each command
  # (Determine that a refresh happened by setting the width, applying Fill Parent,
  # and checking that the width changes to 50)

  set filling slice width(rect, 33)
  assert(slice collide(rect, dot) == true)  # Refreshes
  assert(slice width(rect) == 50)

  set filling slice width(rect, 33)
  move slice with wallchecking(rect, 1, 1)  # Refreshes
  assert(slice width(rect) == 50)

  set filling slice width(rect, 33)
  clamp slice(dot, par)  # Refreshes ancestors of dot and par
  assert(slice width(rect) == 50)

  set filling slice width(rect, 33)
  assert(slice collide point(rect, 1, 40) == true)  # Refreshes
  assert(slice width(rect) == 50)

  set filling slice width(rect, 33)
  assert(slice at pixel(par, 1, 40, 0, true, false) == rect)  # Refreshes
  assert(slice width(rect) == 50)

  set filling slice width(rect, 33)
  put slice(dot, 40, 1)
  assert(find colliding slice(par, dot, 0, true, false) == rect)  # Refreshes
  assert(slice width(rect) == 50)

  free slice(rect)
  free slice(par)
  free slice(vispar)
  free slice(dot)
  free slice(grid)
end

script, slice handle tests, begin
  $0="Slice handle tests"

  # slice is valid
  variable (sl)
  assert(slice is valid(0) == false)
  sl := create container
  assert(slice is valid(sl))
  free slice(sl)
  assert(slice is valid(sl) == false)
  assert(slice is valid(sprite layer))
  assert(slice is valid(-1) == false)
  assert(slice is valid(4242) == false)

  # Check that slice handles aren't reused quickly
  # (The actual minimum number of frees before a handle is reused
  # is currently 256*4000 = 1,024,000)
  variable(i, sl2)
  sl := create container
  free slice(sl)
  for (i, 0, 5100) do (
    sl2 := create container
    assert(sl2 <> sl)
    free slice(sl2)
  )
end

########################################################################

script, sprite frameid tests, begin
  $0="sprite frameid tests"

  variable(spr)
  # A set with frame group sizes: 0, 1, 2, 3, 0, 6, 1
  spr := load walkabout sprite(18)

  # Check frame counts
  assert(sprite frame count(spr) == 13)
  assert(sprite frame group size(spr, -1) == 0)
  assert(sprite frame group size(spr, 0) == 0)
  assert(sprite frame group size(spr, 1) == 1)
  assert(sprite frame group size(spr, 2) == 2)
  assert(sprite frame group size(spr, 3) == 3)
  assert(sprite frame group size(spr, 4) == 0)
  assert(sprite frame group size(spr, 5) == 6)
  assert(sprite frame group size(spr, 6) == 1)
  assert(sprite frame group size(spr, 7) == 0)

  # frame id 0 doesn't exist, so preserves 100
  assert(get sprite frame(spr) == 0)
  assert(get sprite frameid(spr) == 100)

  # frame and frameid should indicate the same frame
  subscript, set and check frame, frame, frameid, begin
    set sprite frame(spr, frame)
    assert (get sprite frame(spr) == frame)
    assert (get sprite frameid(spr) == frameid)
    set sprite frame(spr, 0)
    assert (find sprite frameid(spr, frameid) == frame)
    assert (set sprite frameid(spr, frameid) == frame)
    assert (get sprite frameid(spr) == frameid)
    assert (get sprite frame(spr) == frame)
  end

  # set a frameid which doesn't exist
  subscript, set and check missing frame, frameid, numframes, begin
    variable(group)
    group := frameid / 100
    set sprite frame(spr, 0)
    assert (find sprite frameid(spr, frameid) == -1)
    assert (set sprite frameid(spr, frameid, true) == -1)  # exact=true: shouldn't be found
    assert (get sprite frame(spr) == 0)
    variable(frame)
    frame := set sprite frameid(spr, frameid)  # exact=false
    if (numframes > 0) then (
      # Should have switched to last frame in group
      assert (frame == get sprite frame(spr))
      assert (get sprite frameid(spr) == group * 100 + numframes -- 1)
    ) else (
      # Should have done nothing
      assert (frame == -1)
      assert (get sprite frameid(spr) == 100)  # ID of frame 0
    )
  end

  variable(group, numframes, fr, fridx)
  # fridx is the frame index in the spriteset

  for (group, 0, 7) do (
    numframes := sprite frame group size(spr, group)
    for (fr, 0, numframes -- 1) do (
      set and check frame(fridx, group * 100 + fr)
      fridx += 1
    )
    # Missing frameids
    for (fr, numframes, 99) do (
      set and check missing frame(group * 100 + fr, numframes)
    )
  )

  free slice(spr)
end

script, sprite slice tests, begin
  sprite frameid tests
end

########################################################################

script, rect slice tests, begin
  $0="Rect Slice tests"

  variable (sl)

  # Test rect transparency
  sl := create rect(100, 100), w
  assert(slice is rect(sl))
  assert(slice type(sl) == slicetype:rect)
  assert(get rect trans(sl) == trans:solid)
  assert(get rect fuzziness(sl) == 100)
  set rect trans(sl, trans:hollow), w
  assert(get rect trans(sl) == trans:hollow)
  assert(get rect fuzziness(sl) == 0)
  set rect fuzziness(sl, 25), w
  assert(get rect trans(sl) == trans:fuzzy)
  assert(get rect fuzziness(sl) == 25)
  assert(get rect opacity(sl) == 25)  # It's an alias
  set rect fuzziness(sl, 0), w
  assert(get rect trans(sl) == trans:hollow)
  assert(get rect fuzziness(sl) == 0)
  set rect fuzziness(sl, 100), w
  assert(get rect trans(sl) == trans:solid)
  assert(get rect fuzziness(sl) == 100)
  set rect opacity(sl, 70), w
  assert(get rect trans(sl) == trans:blend)
  assert(get rect opacity(sl) == 70)
  assert(get rect fuzziness(sl) == 70)  # Alias
  set rect trans(sl, trans:solid)
  assert(get rect opacity(sl) == 100)
  set rect trans(sl, trans:hollow)
  assert(get rect opacity(sl) == 0)
  set rect trans(sl, trans:fuzzy), w
  assert(get rect trans(sl) == trans:fuzzy)
  # Using setrecttrans preserves previous opacity to 70%
  assert(get rect fuzziness(sl) == 70)
  set rect trans(sl, trans:blend), w
  assert(get rect opacity(sl) == 70)
  assert(get rect fuzzy zoom(sl) == 1)  # Default value
  set rect fuzzy zoom(sl, 1000)
  assert(get rect fuzzy zoom(sl) == 1000)
  assert(get rect stationary pattern(sl) == false)  # Default value
  set rect stationary pattern(sl, true)
  assert(get rect stationary pattern(sl) == true)
  free slice(sl)

  # Test rect box border and raw box border
  sl := create rect(99, 99), w
  set slice x(sl, 10)
  set slice y(sl, 10)
  assert(get rect style(sl) == 0)
  assert(get rect border(sl) == 0)
  assert(get rect raw border(sl) == border:line)
  set rect raw border(sl, 0), w
  assert(get rect style(sl) == -1)
  assert(get rect border(sl) == border:raw)
  assert(get rect raw border(sl) == 0)
  set rect border(sl, border:none), w
  assert(get rect border(sl) == border:none)
  assert(get rect raw border(sl) == border:none)
  set rect border(sl, border:line), w
  assert(get rect border(sl) == border:line)
  assert(get rect raw border(sl) == border:line)
  set rect raw border(sl, 1), w
  assert(get rect border(sl) == border:raw)
  assert(get rect raw border(sl) == 1)
  set rect border(sl, 13), w
  assert(get rect border(sl) == 13)
  assert(get rect raw border(sl) == 1)
  set rect raw border(sl, border:none), w
  assert(get rect border(sl) == border:none)
  assert(get rect raw border(sl) == border:none)
  set rect raw border(sl, border:line), w
  assert(get rect border(sl) == border:line)
  assert(get rect raw border(sl) == border:line)
  free slice(sl)

  # Test rect style
  sl := create rect(120, 120, 14), w
  assert(get rect style(sl) == 14)
  # Testing that the style is as in the box style editor
  assert(get rect border(sl) == 14)
  assert(get rect raw border(sl) == 1)
  assert(get rect fg col(sl) == 252)
  assert(get rect bg col(sl) == 65)
  set rect fg col(sl, 42), w
  assert(get rect style(sl) == -1)
  set rect style(sl, 0), w
  assert(get rect style(sl) == 0)
  assert(get rect border(sl) == 0)
  assert(get rect raw border(sl) == border:line)
  assert(get rect fg col(sl) == 249)
  assert(get rect bg col(sl) == 33)
  free slice(sl)
end

########################################################################

# Note: there are lots of tests about the sizing of Text slices in "slice refresh tests",
# some redundant to these.
script, text slice tests, begin
  $0="Text Slice tests"

  # Check initial state as expected
  variable(sl)
  sl := create text
  assert(slice is text(sl))
  assert(slice type(sl) == slicetype:text)
  get slice text(2, sl)
  assert(string length(2) == 0)
  assert(get wrap(sl) == false)
  assert(get outline(sl) == false)
  assert(get text color(sl) == 0)  # which means uiText
  assert(get text bg(sl) == 0)

  $2="fee Fi\n \n foe"
  set slice text(sl, 2)
  get slice text(3, sl)
  assert(string compare(2, 3))

  # Non-wrapping slices have fixed size
  tracevalue(slice width(sl),slice height(sl))
  assert(slice width(sl) == 48)
  assert(slice height(sl) == 30)  # 3 lines
  #set slice width(sl, 10)  # throws an error
  #set slice height(sl, 10)  # throws an error
  assert(slice width(sl) == 48)
  assert(slice height(sl) == 30)  # 3 lines

  # Wrapping slices recompute height based on width
  set wrap(sl, true)
  assert(get wrap(sl) == true)
  assert(slice width(sl) == 48) # shouldn't have changed
  set slice width(sl, 35)
  set wrap(sl, true)   # Modifying text slice data calls UpdateTextSlice (wait also would)
  assert(slice width(sl) == 35)
  assert(slice height(sl) == 40)
  set slice height(sl, 10)  # Doesn't show an error but will be overridden when updated
  set wrap(sl, true)   # Modifying text slice data calls UpdateTextSlice (wait also would)
  assert(slice height(sl) == 40)
  set wrap(sl, false)
  assert(slice width(sl) == 48)  # Recomputes immediately

  # TODO: test recomputing of embed codes

  $2=""
  $3=""
  free slice(sl)
end

########################################################################

script, panel slice tests, begin
  $0="Panel Slice tests"

  variable(sl)
  sl := create panel
  assert(slice is panel(sl))
  assert(slice type(sl) == slicetype:panel)
  # Check initial state as expected
  assert(get panel primary index(sl) == 0)
  assert(get panel percent as int(sl) == 50)
  assert(get panel padding(sl) == 0)
  assert(get panel is vertical(sl) == false)
  assert(get panel pixels(sl) == 0)
  assert(get panel sized by child(sl) == false)

  set panel primary index(sl, 1)
  assert(get panel primary index(sl) == 1)

  set panel percent(sl, 40)
  assert(get panel percent as int(sl) == 40)

  set panel padding(sl, 4)
  assert(get panel padding(sl) == 4)

  set panel pixels(sl, 5)
  assert(get panel pixels(sl) == 5)

  set panel is vertical(sl, true)
  assert(get panel is vertical(sl) == true)

  # check children are sized correctly

  # First make the panel a 200x200 square
  set slice width(sl, 200)
  set slice height(sl, 200)
  set panel primary index(sl, 0)

  # Create a sprite and a box and put them in the panel. Make the box fill.  
  variable(sprite, box)
  sprite := load hero sprite(1)
  box := create rect(10, 10, 0)
  set parent(sprite, sl)
  set parent(box, sl)
  fill parent(box, true)
  w  # refresh
  assert(slice width(box) == 200)
  assert(slice height(box) == 113)

  # Now make the panel take its size from the primary child slice, 'sprite'
  set panel sized by child(sl, true)
  set panel percent(sl, 0)
  set panel pixels(sl, 0)
  assert(get panel sized by child(sl) == true)
  w
  assert(slice width(box) == 200)
  assert(slice height(box) == 156)

  # Change the size of the first child
  replace large enemy sprite(sprite, 1)
  w
  assert(slice width(box) == 200)
  assert(slice height(box) == 116)

  # Check split size is max(primary size, fixed size)
  set panel pixels(sl, 80)  # Smaller than 'sprite'
  w
  assert(slice height(box) == 116)
  set panel pixels(sl, 110)  # Bigger
  w
  assert(slice height(box) == 200 -- 110 -- 4)

  free slice(sl)
end

########################################################################

script, ellipse slice tests, begin
  $0="Ellipse Slice tests"
  variable(sl)
  sl := create ellipse(10, 10, 15, 0), w
  assert(slice is ellipse(sl))
  assert(get ellipse border col(sl) == 15)
  set ellipse border col(sl, 14), w
  assert(get ellipse border col(sl) == 14)
  assert(get ellipse fill col(sl) == 0)
  set ellipse fill col(sl, 1), w
  assert(get ellipse fill col(sl) == 1)
  set slice x(sl, 20), w
  set slice y(sl, 20), w
  set slice width(sl, 30), w
  set slice height(sl, 30), w
  free slice(sl)
end

########################################################################

script, slice clone tests, begin
  # Test cloneslice
  variable(sl, sl2, ch, ch2)
  sl := get npc slice(next npc reference)
  set slice extra(sl, extra 2, 42)
  sl2 := clone slice(sl)
  assert(next sibling(sl2) == sl)
  # Check that things got copied
  assert(slice screen x(sl) == slice screen x(sl2))
  assert(slice screen y(sl) == slice screen y(sl2))
  assert(get slice extra(sl2, extra 2) == 42)
  assert(child count(sl) == child count(sl2))
  ch := first child(sl)
  ch2 := first child(sl2)
  assert(get slice lookup(ch) < 0)
  assert(get slice lookup(ch2) == 0)  # special lookup codes removed
  free slice(sl2)
  sl := sprite layer
  assert(slice type(sl) == slicetype:special)
  sl2 := clone slice(sl)
  assert(slice is container(sl2))  # Special slices turn into containers
  assert(previous sibling(sl) == sl2)
  assert(child count(sl) == child count(sl2))
  free slice(sl2)
  sl2 := clone slice(sl, false)  # recurse == false
  assert(child count(sl2) == 0)  # Didn't recurse
  free slice(sl2)

  # More detailed checks that extra arrays are copied
  sl := create container
  sl2 := clone slice(sl)  # Copy a default NULL array
  assert(extra length(sl2) == 3)
  free slice(sl2)
  resize extra(sl, 0)  # Copy a length 0 array
  sl2 := clone slice(sl)
  assert(extra length(sl2) == 0)
  free slice(sl2)
  resize extra(sl, 50000)
  set slice extra(sl, -1, 12345)
  sl2 := clone slice(sl)  # Copy a large array
  assert(extra length(sl2) == 50000)
  assert(get slice extra(sl2, 49999) == 12345)
  # Check the array, not its pointer, is copied
  set slice extra(sl2, 49998, 16)
  assert(get slice extra(sl2, 49998) == 16)
  assert(get slice extra(sl, 49998) == 0)
  free slice(sl2)
  free slice(sl)
end

########################################################################

# Example from plotdict
script, remove extra, obj, value, begin
  variable (idx)
  while (true) do (
    idx := find extra (obj, value)
    if (idx == -1) then (exit)   # All deleted
    delete extra (obj, idx)
    # Normally we'd do "idx += 1" to look for the next occurrence of 'value'
    # after idx, but because we deleted it we don't need to.
  )
end

########################################################################

# The following sets of testcases for slice & generic extra data commands
# were accidentally duplicated. Most also get tested in many other places.

script, slice extra tests, begin
  variable(sl)
  sl := create line
  assert(extra length(sl) == 3)
  set slice extra(sl, 0, 100)
  set slice extra(sl, 2, 102)
  assert(get slice extra(sl, 2) == 102)
  assert(get slice extra(sl, -1) == 102)
  assert(get slice extra(sl, -3) == 100)
  assert(get extra(sl, 2) == 102)
  assert(get extra(sl, -1) == 102)
  assert(get extra(sl, -3) == 100)
  set extra(sl, 1, 4242)
  assert(get slice extra(sl, 1) == 4242)
  assert(get extra(sl, -2) == 4242)
  resize extra(sl, 1)
  assert(extra length(sl) == 1)
  assert(get slice extra(sl, -1) == 100)
  resize extra(sl, 3)
  assert(get slice extra(sl, -1) == 0)
  append extra(sl, 103)
  assert(extra length(sl) == 4)
  assert(get slice extra(sl, 3) == 103)
  resize extra(sl, 999999)
  set slice extra(sl, 999998, 998)
  assert(get slice extra(sl, -1) == 998)
  append extra(sl, 999)
  set slice extra(sl, 999999, 999)
  assert(get slice extra(sl, -1000000) == 100)
  free slice(sl)

  # Also, loading slice extra data is tested in "slice collection tests"
  # And clone extra data is tested in "slice clone tests"
end

script, extra array tests, begin
 $0="extra array tests"
 variable(xt)
 show string($1="Make a testing slice and use its extra data")
 xt := create container(0,0)
 show string($1="Test the 3 default elements")
 assert(extra length(xt)==3)
 assert(get extra(xt,0)==0)
 assert(get extra(xt,1)==0)
 assert(get extra(xt,2)==0)
 show string($1="Test setting data")
 set extra(xt, 0, 100)
 set extra(xt, 1, 101)
 set extra(xt, 2, 102)
 assert(get extra(xt,0)==100)
 assert(get extra(xt,1)==101)
 assert(get extra(xt,2)==102)
 show string($1="Test end-based indexing")
 assert(get extra(xt,-1)==102)
 assert(get extra(xt,-2)==101)
 assert(get extra(xt,-3)==100)
 show string($1="Test appending")
 assert(append extra(xt, 999)==4)
 assert(get extra(xt,3)==999)
 show string($1="Test resizing")
 resize extra(xt,6)
 assert(extra length(xt)==6)
 assert(get extra(xt,0)==100)
 assert(get extra(xt,3)==999)
 assert(get extra(xt,4)==0)
 assert(get extra(xt,5)==0)
 resize extra(xt,3)
 assert(extra length(xt)==3)
 assert(get extra(xt,-1)==102)
 resize extra(xt,0)
 assert(extra length(xt)==0)
 resize extra(xt,2)
 assert(get extra(xt,0)==0)
 assert(get extra(xt,1)==0)
 show string($1="Test extra insertion")
 set extra(xt,0,100)
 set extra(xt,1,200)
 assert(extra length(xt)==2)
 insert extra(xt,1,111)
 assert(extra length(xt)==3)
 assert(get extra(xt,0)==100)
 assert(get extra(xt,1)==111)
 assert(get extra(xt,2)==200)
 insert extra(xt,0,99)
 assert(get extra(xt,0)==99)
 assert(get extra(xt,1)==100)
 assert(get extra(xt,2)==111)
 assert(get extra(xt,3)==200)
 show string($1="Test extra insertion from negative index")
 insert extra(xt,-1,222)
 assert(get extra(xt,0)==99)
 assert(get extra(xt,1)==100)
 assert(get extra(xt,2)==111)
 assert(get extra(xt,3)==222)
 assert(get extra(xt,4)==200)
 append extra(xt,100)

 show string($1="Test find extra")
 assert(find extra(xt,1)==-1)
 assert(find extra(xt,111)==2)
 assert(find extra(xt,111,2)==2)
 assert(find extra(xt,111,3)==-1)
 assert(find extra(xt,100)==1)
 assert(find extra(xt,100,2)==5)
 assert(find extra(xt,100,-1)==5)
 assert(find extra(xt,100,-4)==5)
 assert(find extra(xt,100,-5)==1)
 assert(find extra(xt,100,-40)==1)
 assert(find extra(xt,100,99)==-1)

 show string($1="Test deletion")
 assert(delete extra(xt,3)==222)
 assert(delete extra(xt,1)==100)
 assert(delete extra(xt,-1)==100)
 assert(get extra(xt,0)==99)
 assert(get extra(xt,1)==111)
 assert(get extra(xt,2)==200)
 assert(extra length(xt)==3)
 show string($1="Test delete range")
 append extra(xt,333)
 append extra(xt,444)
 assert(extra length(xt)==5)
 assert(delete extra range(xt, 1, 4) == 111)
 assert(extra length(xt)==2)
 assert(get extra(xt,0)==99)
 assert(get extra(xt,1)==444)
 show string($1="Delete empty range (nothing happens)")
 delete extra range(xt, 1, 1)
 assert(extra length(xt)==2)
 assert(get extra(xt,0)==99)
 assert(get extra(xt,1)==444)
 delete extra range(xt, 0, 0)
 assert(extra length(xt)==2)
 delete extra range(xt, 2, 2)  # [length, length)
 assert(extra length(xt)==2)
 delete extra range(xt, 3, 3)  # out of bounds is OK
 assert(extra length(xt)==2)
 assert(get extra(xt,0)==99)
 assert(get extra(xt,1)==444)
 show string($1="Delete range from end")
 append extra(xt,555)
 append extra(xt,666)
 assert(extra length(xt)==4)
 assert(delete extra range(xt, -3, -1) == 444)
 assert(extra length(xt)==2)
 assert(get extra(xt,0)==99)
 assert(get extra(xt,1)==666)
 show string($1="Delete range up to end")
 assert(delete extra range(xt, 1, 2) == 666)
 assert(extra length(xt)==1)
 assert(get extra(xt,0)==99)

 show string($1="Test remove extra")
 append extra(xt, 100)
 append extra(xt, 99)
 remove extra(xt, 100)
 assert(extra length(xt)==2)
 assert(get extra(xt,0)==99)
 assert(get extra(xt,1)==99)
 remove extra(xt, 99)
 assert(extra length(xt)==0)

 show no value
 free slice(xt)
end

########################################################################

# Tests loading slices from file.
# A lot more could be added to this
script, slice collection tests, begin
  $0="slice collection tests"

  variable(sl, sl2)

  # This is a slice collection with the root slice edited
  sl := load slice collection(0)
  w
  assert(sl)

  assert(parent slice(sl) == sprite layer)  # default parent
  # Test "check parentage"
  assert(check parentage(sl, sprite layer) == true)
  assert(check parentage(sl, sl) == false)  # Defined behaviour
  assert(check parentage(sprite layer, sl) == false)
  assert(check parentage(sl, lookup slice(sl:root)) == true)
  sl2 := create rect
  assert(check parentage(sl, sl2) == false)
  assert(check parentage(sl2, sl) == false)
  free slice(sl2)

  assert(slice is rect(sl) == true)
  assert(is filling parent(sl) == false)
  assert(slice x(sl) == -10)
  assert(slice y(sl) == -20)
  assert(slice width(sl) == 100)
  assert(slice height(sl) == 50)
  assert(get slice visible(sl) == true)
  assert(extra length(sl) == 3)
  assert(get slice extra(sl, 0) == 10)
  assert(get slice extra(sl, 1) == 20)
  assert(get slice extra(sl, 2) == 30)
  assert(get top padding(sl) == 1)
  assert(get right padding(sl) == 2)
  assert(get bottom padding(sl) == 3)
  assert(get left padding(sl) == 4)
  assert(get slice clipping(sl) == true)
  assert(get slice lookup(sl) == sli:my root slice)
  assert(get rect fuzziness(sl) == 48)
  assert(get rect fg col(sl) == 101)
  assert(get rect bg col(sl) == 71)
  assert(get rect style(sl) == -1)
  assert(get horiz align(sl) == edge:right)
  assert(get vert align(sl) == edge:middle)
  assert(get horiz anchor(sl) == edge:right)
  assert(get vert anchor(sl) == edge:top)
  assert(slice screen x(sl) == 310)   # position of the anchor point
  assert(slice screen y(sl) == 80)

  sl2 := lookup slice(sli:some lookup)
  assert(sl2)
  assert(parent slice(sl2) == sl)
  assert(slice is sprite(sl2) == true)
  assert(get sprite type(sl2) == spritetype:walkabout)
  assert(get spriteset number(sl2) == 6)
  assert(get sprite palette(sl2) == 1)
  assert(get sprite frame(sl2) == 4)
  assert(get sprite trans(sl2) == false)
  assert(sprite is horiz flipped(sl2) == true)
  assert(sprite is vert flipped(sl2) == false)
  assert(sprite is dissolving(sl2) == false)
  assert(slice width(sl2) == 20)
  assert(slice height(sl2) == 20)
  assert(slice x(sl2) == 10)
  assert(slice y(sl2) == -4)
  assert(slice screen x(sl2) == 224)
  assert(slice screen y(sl2) == 77)

  sl2 := lookup slice(sli:no extras)
  assert(extra length(sl2) == 0)
  sl2 := lookup slice(sli:default extras)
  assert(extra length(sl2) == 3)
  assert(get slice extra(sl2, 0) == 0)
  assert(get slice extra(sl2, 1) == 0)
  assert(get slice extra(sl2, 2) == 0)
  sl2 := lookup slice(sli:one extra)
  # Check the length is saved although no non-default values
  assert(extra length(sl2) == 1)
  assert(get slice extra(sl2, 0) == 0)
  sl2 := lookup slice(sli:lotsa extras)
  assert(extra length(sl2) == 10000)
  # This tests the encoding of runs of identical values as "repeat" nodes
  # A "repeat" before any "int" defaults to 0
  assert(get slice extra(sl2, 0) == 0)
  assert(get slice extra(sl2, 2) == 0)
  assert(get slice extra(sl2, 3) == -98)
  assert(get slice extra(sl2, 4) == -98)
  assert(get slice extra(sl2, 5) == -98)
  assert(get slice extra(sl2, 6) == 0)
  assert(get slice extra(sl2, 9997) == 0)
  # A run which goes to the end of the array is a special case
  assert(get slice extra(sl2, 9998) == -99)
  assert(get slice extra(sl2, 9999) == -99)

  free slice(sl)

  # A simpler collection
  sl := load slice collection(1)
  assert(sl)
  assert(get slice visible(sl) == false) # edited
  assert(is filling parent(sl) == true)  # the default
  free slice(sl)

  # Test loading slice extra from obsolete file format
  sl := load slice collection(1300)
  assert(extra length(sl) == 3)
  assert(get slice extra(sl, 0) == 0)
  assert(get slice extra(sl, 1) == 0)
  assert(get slice extra(sl, 2) == 0)
  sl2 := first child(sl)
  assert(extra length(sl) == 3)
  assert(get slice extra(sl2, 0) == 10)
  assert(get slice extra(sl2, 1) == 20)
  assert(get slice extra(sl2, 2) == 30)
  free slice(sl)

  # Loading a missing collection in range 0-32767 returns 0 without error
  sl := load slice collection(32700)
  assert(sl == 0)
end

########################################################################

script, slice search tests, begin
  $0="Slice search tests"

  variable(sl, sl1, sl11, sl111, sl1111, sl1112, sl2)

  # Build a tree. X1 is the first child of X, X2 is the second child
  sl1 := create container
  sl11 := create container, set parent(sl11, sl1)
  sl111 := create container, set parent(sl111, sl11)
  set slice lookup(sl111, 14)
  sl1111 := create container, set parent(sl1111, sl111)
  sl1112 := create container, set parent(sl1112, sl111)
  set slice lookup(sl1112, 14)
  sl2 := create container
  set slice lookup(sl2, 14)

  # next slice in tree
  sl := 0
  sl := next slice in tree(sl, sl1),  assert(sl == sl1)
  sl := next slice in tree(sl, sl1),  assert(sl == sl11)
  assert(next slice in tree(sl, sl1, false) == 0)  # sl11 has no sibling
  sl := next slice in tree(sl, sl1),  assert(sl == sl111)
  sl := next slice in tree(sl, sl1),  assert(sl == sl1111)
  assert(next slice in tree(sl, sl1, false) == sl1112)
  sl := next slice in tree(sl, sl1),  assert(sl == sl1112)
  assert(next slice in tree(sl, sl1, false) == 0)
  sl := next slice in tree(sl, sl1),  assert(sl == 0)
  # Iterate whole tree
  sl := next slice in tree(sl1112),  assert(sl == sl2)
  # Starting at the root slice
  sl := next slice in tree(0, 0),  assert(sl == lookup slice(sl:root))
  sl := next slice in tree(sl, 0, false),  assert(sl == 0)

  # lookup next slice
  assert(lookup next slice(sl:walkabout sprite component) == lookup slice(sl:walkabout sprite component))

  sl := 0
  assert(lookup slice(14, sl1) == sl111)
  sl := lookup next slice(14, sl, sl1), assert(sl == sl111)
  sl := lookup next slice(14, sl, sl1), assert(sl == sl1112)
  sl := lookup next slice(14, sl, sl1), assert(sl == 0)
  sl := lookup next slice(14, sl1112), assert(sl == sl2)
  sl := lookup next slice(14, sl1111, sl1), assert(sl == sl1112)  # Starting from a slice without lookup 14

  sl := 0
  while (true) do (
    sl := lookup next slice(sl:walkabout sprite component, sl)
    if (sl == 0) then (break)
    assert(get sprite type(sl) == spritetype:walkabout)
  )

  free slice(sl1)
  free slice(sl2)
end

########################################################################

# Unfinished tests of findcollidingslice and sliceatpixel
script, slice collision tests, begin
  $0="Slice collision tests"

  variable(parent, sl1, sl2, sl3, sl4, sl5, temp)

  parent := create select
  # f
  put slice (parent, 100, 200)

  sl1 := create container(0, 0)
  set parent(sl1, parent)
  assert(slice at pixel(parent, 0, 0) == 0)  # can't be hit
  assert(slice at pixel(parent, 0, 0, get count) == 0)

  sl2 := create container(10, 10)
  set parent(sl2, parent)

  sl3 := create container(1, 1)
  set parent(sl3, parent)
  put slice(sl3, 4, 5)

  sl4 := create container(1, 1)
  set parent(sl4, sl3)

  wait(1)  # update select visibility

  ### Testing slice at pixel
  # Unfinished: no testing of slices with negative width/height.

  # Only sl1 should be visible...
  assert(get slice visible(sl1) == true)
  assert(get slice visible(sl2) == false)
  assert(get slice visible(sl3) == false)
  assert(get slice visible(sl4) == true)

  assert(slice at pixel(parent, 100, 200) == sl2)  # topleft corner
  assert(slice at pixel(parent, 105, 205) == sl2) # inside
  assert(slice at pixel(parent, 100, 210) == 0)  # on the edge of sl2
  assert(slice at pixel(parent, 110, 200) == 0)  # on the edge of sl2
  assert(slice at pixel(parent, 100, 200, get count) == 1)

  # sliceatpixel and findcollidingslice must return slices from bottommost to topmost
  assert(slice at pixel(parent, 104, 205, get count) == 3)
  assert(slice at pixel(parent, 104, 205, 0) == sl2)
  assert(slice at pixel(parent, 104, 205, 1) == sl3)
  assert(slice at pixel(parent, 104, 205, 2) == sl4)
  assert(slice at pixel(parent, 104, 205, 3) == 0)

  # now without descending:
  assert(slice at pixel(parent, 104, 205, get count, false) == 2)
  assert(slice at pixel(parent, 104, 205, 0, false) == sl2)
  assert(slice at pixel(parent, 104, 205, 1, false) == sl3)
  assert(slice at pixel(parent, 104, 205, 2, false) == 0)

  # test visibleonly
  assert(slice at pixel(parent, 104, 205, get count, true, true) == 0)
  # visibility of the 'parent' slice doesn't affect anything (sl3 is not visible)
  assert(slice at pixel(sl3, 104, 205, get count, true, true) == 1)
  # change visible slice to sl3
  set select slice index(parent, 2)
  wait(1)  # update select visibility
  assert(get slice visible(sl3) == true)
  # now sl3 and sl4 should be hit
  assert(slice at pixel(parent, 104, 205, get count, true, true) == 2)
  assert(slice at pixel(parent, 104, 205, 0, true, true) == sl3)
  assert(slice at pixel(parent, 104, 205, 1, true, true) == sl4)

  # ##Testing find colliding slice
  # Unfinished: don't yet check all the cases of an AABB collision test.
  # (Easier to test manually with collisiontest.rpg)
  # Also, no testing of slices with negative width/height.

  sl5 := load walkabout sprite(0)
  assert(slice height(sl5) == 20)
  put slice(sl5, 100, 190)

  # sliceatpixel and findcollidingslice must return slices from bottommost to topmost
  #tracevalue(parent, sl1, sl2, sl3, sl4, sl5)

  assert(find colliding slice(parent, sl5, get count) == 3)
  # Don't find sl1 which is zero size (FIXME: which I think probably isn't correct, since "slice contains" would say yes)
  #assert(find colliding slice(parent, sl5, 0) == sl1)
  assert(find colliding slice(parent, sl5, 0) == sl2)
  assert(find colliding slice(parent, sl5, 1) == sl3)
  assert(find colliding slice(parent, sl5, 2) == sl4)
  assert(find colliding slice(parent, sl5, 3) == 0)

  # Test non-overlap of adjacent slices
  set parent(sl5, parent)
  put slice(sl5, 0, 5 -- 20)  # positioned so sl3 on bottom edge
  # Create another couple slices adjacent to sl5 on the top and left
  temp := create container(1, 1)
  set parent(temp, parent)
  put slice(temp, -1, 0)  # 1 left of sl5
  temp := create container(1, 1)
  set parent(temp, parent)
  put slice(temp, 0, 5 -- 20 -- 1)  # 1 up from sl5
  assert(find colliding slice(parent, sl5, 0) == sl2)  # doesn't find sl1, zero size
  assert(find colliding slice(parent, sl5, 1) == 0)  # should not find sl3, nor sl5, nor the temp slices

  # Now overlap by one
  put slice(sl5, 0, 5 -- 20 + 1)  # now sl3 overlaps
  assert(find colliding slice(parent, sl5, 0) == sl2)   # doesn't find sl1, zero size
  assert(find colliding slice(parent, sl5, 1) == sl3)
  assert(find colliding slice(parent, sl5, 2) == sl4)
  assert(find colliding slice(parent, sl5, 3) == 0)  # should not find sl5

  # try using a 0x0 slice to search for overlaps - should find nothing.
  # (FIXME: This is probably not correct; as an infinitesimal point it overlaps sl2 and sl5.
  assert(find colliding slice(parent, sl1, 0) == 0)

  # Test no descending
  # Don't find sl1 which is zero size (FIXME: which I think probably isn't correct, since "slice contains" would say yes)
  assert(find colliding slice(parent, sl5, 0, false) == sl2)
  assert(find colliding slice(parent, sl5, 1, false) == sl3)
  assert(find colliding slice(parent, sl5, 2, false) == 0)  # shouldn't find sl4
  assert(find colliding slice(parent, sl5, get count, false) == 2)

  # Test visible only (only sl3 is visible)
  assert(find colliding slice(parent, sl5, get count, true, true) == 2)
  assert(find colliding slice(parent, sl5, 0, true, true) == sl3)
  assert(find colliding slice(parent, sl5, 1, true, true) == sl4)
  assert(find colliding slice(parent, sl5, 2, true, true) == 0)

  # Do two 0x0 slices at the same point overlap? (they shouldn't)
  free slice children(parent)
  temp := create container(0, 0)
  set parent(temp, parent)
  temp := create container(0, 0)
  set parent(temp, parent)
  assert(find colliding slice(parent, temp, get count) == 0)

  free slice(parent)
end

########################################################################

# Return index of a child of the root slice.
script, root child index, lookup code, begin
  variable(sl)
  clear string(err arg string)
  append number(err arg string, lookup code)
  sl := lookup slice(lookup code)
  assert(sl)
  assert(parent slice(sl) == lookup slice(sl:root))
  return(slice child indeX(sl))
end

# Unfinished, only inspects children of root.
# But see also "slice correspondence tests" for NPC & hero slices
script, slice tree tests, begin
  $0="Slice tree tests"

  assert(sprite layer == lookup slice(sl:script layer))

  # Ensure the major slice layers appear in a fixed order.
  # Probably OK for new slices to get added between these (anyone making assumptions about the n-th
  # child here may be too much pain to support)

  variable (scr, map, textbox, backdrop, strings)

  map := root child index(sl:map root)
  backdrop := root child index(sl:backdrop)
  scr := root child index(sl:script layer)
  textbox := root child index(sl:textbox layer)
  strings := root child index(sl:string layer)

  assert(map < backdrop)
  assert(backdrop < scr)
  assert(scr < textbox)
  assert(textbox < strings)
end

########################################################################

# This tests commands for converting between slices and hero/npc references
script, slice correspondence tests, begin

  #### Test NPC slices

  variable(npc, sl, sl2, walkabout layer)
  walkabout layer := lookup slice(sl:npc layer)
  npc := next npc reference
  while (npc) do (
    sl := get npc slice(npc)
    assert(sl)
    assert(npc reference from slice(sl) == npc)
    # Should be the top-level walkabout container
    assert(slice parent(sl) == walkabout layer)
    assert(slice x(sl) == npc pixel x(npc))
    assert(slice y(sl) == npc pixel y(npc))
    sl2 := lookup slice(sl:walkabout sprite component, sl)
    assert(sl2)
    assert(sl2 == get npc sprite(npc))
    assert(npc reference from slice(sl2) == 0)

    npc := next npc reference(npc)
  )

  sl := get hero slice(0)
  assert(sl && npc reference from slice(sl) == 0)

  #### Test hero slices
  # Preconditions
  assert(hero by slot(0) == hero:Freki)
  assert(hero by slot(1) == -1)
  assert(hero by slot(2) == -1)
  assert(hero by slot(3) == -1)

  sl := get hero slice(0)
  assert(sl)
  assert(slice parent(sl) == lookup slice(sl:hero layer))
  assert(slice x(sl) == hero pixel x(0))
  assert(slice y(sl) == hero pixel y(0))
  assert(sl == get hero slice by slot(0))
  assert(hero rank from slice(sl) == 0)
  assert(hero slot from slice(sl) == 0)
  sl2 := lookup slice(sl:walkabout sprite component, sl)
  assert(sl2)
  assert(sl2 == get hero sprite(0))

  # Defaults to -1
  assert(hero rank from slice(get npc slice(next npc reference)) == -1)
  assert(hero slot from slice(get npc slice(next npc reference)) == -1)

  # Check it returns rank, not party slot
  swap by position(0, 1)
  sl := get hero slice(0)
  assert(sl)
  assert(hero rank from slice(sl) == 0)
  assert(hero slot from slice(sl) == 1)
  # Compare with 'by slot'
  assert(get hero slice by slot(0) == 0)
  assert(get hero slice by slot(1) == sl)

  # Test rank > 0
  add hero(hero:Bram)
  sl := get hero slice(1)
  assert(sl)
  assert(get hero slice by slot(1) == sl)
  assert(hero rank from slice(sl) == 1)
  assert(hero slot from slice(sl) == 1)

  # Hero slices belonging to non-existent heroes shouldn't exist, but before
  # gorgonzola they did exist. (Already tested "get hero slice by slot")
  assert(get hero slice(2) == 0)

  # Restore
  delete hero(hero:Bram)
  swap by position(0, 1)
end

########################################################################

script, slice blending tests, begin
  variable(sl, collection)

  sl := load hero sprite(0, 1)
  assert(get blending enabled(sl) == false)
  assert(get opacity(sl) == 100)
  assert(get blend mode(sl) == blend:normal)

  # Test opacity
  set opacity(sl, 50), w
  assert(get opacity(sl) == 50)
  assert(get blending enabled(sl) == true)
  # And blend mode
  set blend mode(sl, blend:multiply), w
  assert(get blend mode(sl) == blend:multiply)

  set blending enabled(sl, false), w
  assert(get blending enabled(sl) == false)
  assert(get opacity(sl) == 100)
  assert(get blend mode(sl) == blend:normal)

  # Check blending settings preserved when blending is disabled
  set blending enabled(sl, true), w
  assert(get opacity(sl) == 50)
  assert(get blend mode(sl) == blend:multiply)
  free slice(sl)

  sl := create container
  # Allowed on non-blendable slices
  assert(get blending enabled(sl) == false)
  assert(get opacity(sl) == 100)
  assert(get blend mode(sl) == blend:normal)
  free slice(sl)

  # Check Map slice blending
  sl := lookup slice(sl:map layer 1)
  set opacity(sl, 30), w
  assert(get opacity(sl) == 30)
  set opacity(sl, 100)

  # Check blending settings loaded from file correctly
  collection := load slice collection(2)
  sl := first sprite child(collection)
  assert(get opacity(sl) == 45)
  assert(get blend mode(sl) == blend:add)
  free slice(collection)
end

########################################################################

script, slice sort tests, begin
  # Very incomplete

  variable(parent, ch1, ch2, ch3, ch4)
  parent := create container(100,100)
  ch1 := create container(10,10)
  set parent(ch1, parent)
  ch2 := create container(10,10)
  set parent(ch2, parent)
  ch3 := create container(10,20)
  set parent(ch3, parent)
  ch4 := create container(10,100)
  set parent(ch4, parent)

  #### get/set child autosort

  # Preconditions
  assert(current map == 0)
  assert(read gmap(16) == 0)  # Map 0 is set to heroes over NPCs.

  assert(get child autosort(lookup slice(sl:walkabout layer)) == autosort:off)
  write gmap(16, 2)  # Layering: NPCs and heroes together
  assert(read gmap(16) == 2)
  # This might actually change in future when more sophisticated layering is implemented
  assert(get child autosort(lookup slice(sl:walkabout layer)) == autosort:Y)

  write gmap(16, 0)  # Reset to heroes over NPCs

  set child autosort(parent, autosort:bottomY)
  assert(get child autosort(parent) == autosort:bottomY)

  # Test bottomY sorting
  set slice edge y(ch1, edge:bottom, 100)
  assert(slice y(ch1) == 100 -- 10)
  set vert anchor(ch2, edge:middle)
  set slice edge y(ch2, edge:bottom, 50)
  assert(slice y(ch2) == 50 -- 10/2)
  set vert anchor(ch3, edge:bottom)
  set slice edge y(ch3, edge:bottom, 80)
  assert(slice y(ch3) == 80)
  set vert anchor(ch4, edge:middle)
  set vert align(ch4, edge:bottom)
  set slice edge y(ch4, edge:bottom, 50)
  wait(1)
  assert(slice child(parent, 0) == ch2)
  assert(slice child(parent, 1) == ch4)
  assert(slice child(parent, 2) == ch3)
  assert(slice child(parent, 3) == ch1)

  free slice(parent)
end

########################################################################

define constant(1, zone:center)
define constant(2, zone:wholegrass)
define constant(3, zone:rightedge)
define constant(4, zone:leftedge)
define constant(5, zone:everything)
define constant(6, zone:topedge)
define constant(7, zone:bottomedge)

# Equivalent to readzone, but checks it agrees with zoneatspot
script, check zone, id, x, y, begin
  variable(i, what)
  for (i, 0, zone at spot(x, y, getcount) -- 1) do (
    what := zone at spot(x, y, i)
    assert(what > 0)
    if (what == id) then (
      assert(read zone(id, x, y) == true)
      exit returning (true)
    )
  )
  assert(read zone(id, x, y) == false)
  return(false)
end

script, zone tests, begin
  $0="zone tests"
  # Could do with more tests of the internal complexities of ZoneMap

  # Read some existing tiles (green plot of grass)
  assert(zone at spot(17, 15, getcount) == 3)
  assert(check zone(1, 17, 15) == 1)
  assert(check zone(2, 17, 15) == 1)
  assert(check zone(3, 17, 15) == 0)
  assert(check zone(5, 17, 15) == 1)

  assert(zone at spot(16, 16, getcount) == 4)
  assert(check zone(1, 16, 16) == 0)
  assert(check zone(2, 16, 16) == 1)
  assert(check zone(3, 16, 16) == 0)
  assert(check zone(4, 16, 16) == 1)
  assert(check zone(5, 16, 16) == 1)
  assert(check zone(7, 16, 16) == 1)

  # zone number of tiles
  assert(zone number of tiles(1) == 1)
  assert(zone number of tiles(2) == 9)
  assert(zone number of tiles(5) == 800)
  assert(zone number of tiles(8) == 0)
  assert(zone number of tiles(9988) == 0)

  # Zone name
  get zone name (95, zone:leftedge)
  $96="left edge"
  assert(string compare(95, 96))

  # Test zone handles (except extra data)
  variable (zone5)
  zone5 := get zone(5)
  assert(zone5 && zone5 <> 5)
  clearstring(95)
  get zone name (95, get zone(zone:leftedge))
  assert(string compare(95, 96))
  assert(read zone(zone5, 16, 16) == 1)
  write zone(zone5, 16, 16, 0)
  assert(read zone(5, 16, 16) == 0)
  write zone(5, 16, 16, 1)
  assert(zone number of tiles(zone5) == 800)

  # Zone extra
  #assert(get zone(zone 5) == zone 5)  # Meant to be idempotent
  assert(get zone extra(5, 0) == 10)
  assert(get zone extra(5, 1) == -2147483648)
  assert(get zone extra(5, 2) == 2147483647)
  assert(get zone extra(5, -1) == 2147483647)
  assert(get extra(zone5, -1) == 2147483647)
  set extra(zone5, -2, 32)
  assert(get zone extra(5, 1) == 32)
  assert(extra length(zone5) == 3)
  # A zone with zero length extra
  variable (zone10)
  zone10 := get zone(10)
  assert(extra length(zone10) == 0)
  append extra(zone10, 2)
  assert(extra length(zone10) == 1)
  assert(get extra(zone10, -1) == 2)
  # A zone with more extra
  assert(extra length(get zone(11)) == 13)
  assert(get zone extra(11, 12) == 999)

  # A nonexistent zone
  get zone name (95, 9000)
  assert(string length(95) == 0)
  assert(get zone extra(9000, 0) == 0)
  assert(get zone extra(9000, 1) == 0)
  set zone extra (9000, 2, 32)
  assert(get zone extra(9000, 2) == 32)

  # Erasing a zone shouldn't affect others on the same tile
  write zone(1, 17, 15, 0)
  assert(check zone(1, 17, 15) == 0)
  assert(check zone(2, 17, 15) == 1)
  assert(check zone(5, 17, 15) == 1)
  assert(zone at spot(17, 15, getcount) == 2)
  write zone(1, 17, 15, 1)
  assert(check zone(1, 17, 15) == 1)

  # Write a zone that doesn't exist at all yet
  write zone(1003, 16, 14, 1)
  assert(check zone(1003, 16, 14) == 1)

  variable(idx, idx2, count)
  # Already 3 zones here; place 15 zones on one tile (the max)
  count := 3
  for (idx, 1001, 1012) do (
    write zone(idx, 17, 15, 1)
    assert(check zone(idx, 17, 15) == 1)
    count += 1
    assert(zone at spot(17, 15, getcount) == count)
  )
  # Remove them again in FIFO order
  for (idx, 1001, 1012) do (
    write zone(idx, 17, 15, 0)
    assert(check zone(idx, 17, 15) == 0)
    count -= 1
    assert(zone at spot(17, 15, getcount) == count)
    for (idx2, idx + 1, 1012) do (
      assert(check zone(idx2, 17, 15) == 1)
    )
  )

  # Check other zones within the same 4x4 block are undisturbed
  assert(zone at spot(16, 14, getcount) == 5)
  assert(check zone(1, 16, 14) == 0)
  assert(check zone(2, 16, 14) == 1)
  assert(check zone(3, 16, 14) == 0)
  assert(check zone(4, 16, 14) == 1)
  assert(check zone(5, 16, 14) == 1)
  assert(check zone(6, 16, 14) == 1)
  assert(check zone(1003, 16, 14) == 1)

  # Test next zone tile x/y
  # Zone 8 (empty zone)
  assert(next zone tile x(8, -1, -1) == -1)
  assert(next zone tile y(8, -1, -1) == -1)
  # Zone 5 (everywhere)
  assert(next zone tile x(5, -1, -1) == 0)
  assert(next zone tile y(5, -1, -1) == 0)
  assert(next zone tile x(5, 0, 0) == 1)
  assert(next zone tile y(5, 0, 0) == 0)
  assert(next zone tile x(5, mapwidth -- 1, 0) == 0)
  assert(next zone tile y(5, mapwidth -- 1, 0) == 1)
  assert(next zone tile x(5, mapwidth -- 1, mapheight -- 1) == -1)
  assert(next zone tile y(5, mapwidth -- 1, mapheight -- 1) == -1)
  # An invalid coordinate over map edge: should just continue from the next row
  assert(next zone tile x(5, mapwidth + 2, 3) == 0)
  assert(next zone tile y(5, mapwidth + 2, 3) == 4)
  assert(next zone tile x(5, 0, mapheight) == -1)
  assert(next zone tile y(5, 0, mapheight) == -1)
  # Test with zone:OneWayExit which contains (4,10), (5,10) only
  assert(next zone tile x(zone:OneWayExit, -1, -1) == 4)
  assert(next zone tile y(zone:OneWayExit, -1, -1) == 10)
  assert(next zone tile x(zone:OneWayExit, 4, 10) == 5)
  assert(next zone tile y(zone:OneWayExit, 4, 10) == 10)
  assert(next zone tile x(zone:OneWayExit, 5, 10) == -1)
  assert(next zone tile y(zone:OneWayExit, 5, 10) == -1)
  # Test starting from position before the zone
  assert(next zone tile x(zone:OneWayExit, 0, 0) == 4)
  assert(next zone tile y(zone:OneWayExit, 0, 0) == 10)

  zones randomized test
end

script, zones randomized test, begin
  # Do a randomised test of writing and reading zones in a 8x8 area
  # (four 4x4 blocks) at top-left corner of the map).
  # The idea is that the zonemap code has a lot of edge cases as a result of
  # implementation details which are a huge pain to write tests for, and those
  # tests wouldn't be worth much if the implementation changes.
  # Unfortunately these tests still aren't worth much; changing the various magic
  # constants results in them not hitting the edge cases anyway! :(
  # We check which zones we expect to see in 64 globals which act as bitvectors,
  # the i-th bit set if that tile should be in zone i.

  show value(1)
  wait

  # Returns true or false. id >= 1
  subscript, readarray, id, x, y, begin
    return((read global(array:zones + x + y * 8) / 2^(id--1)), and, 1)
  end

  # id >= 1
  subscript, writearray, id, x, y, value, begin
    variable(bits)
    bits := read global(array:zones + x + y * 8)
    bits := bits, and, (-1 -- 2^(id--1))
    if (value) then (bits += 2^(id--1))
    write global(array:zones + x + y * 8, bits)
  end

  # Num zones at tile
  subscript, array count zones, x, y, begin
    return(bitcount(read global(array:zones + x + y * 8)))
  end

  # Check the whole area is as it should be
  subscript, test area, begin
    variable(x, y, id)
    for (x, 0, 7) do (
      for (y, 0, 7) do (
        for (id, 1, 31) do (
          assert(readarray(id, x, y) == read zone(id, x, y))
        )
        assert(zone at spot(x, y, getcount) == array count zones(x, y))
      )
    )
  end

  # Initial state
  variable(x,y)
  for (x, 0, 7) do (
    for (y, 0, 7) do (writearray(zone:everything, x, y, 1))
  )
  #test area

  # Stick to 15 zones in the whole area, so no overflow occurs
  variable(it, id, newvalue)
  for (it, 0, 300) do (
    show value(it)
    x := random(0,4)
    y := random(0,5)
    id := random(1,15)
    newvalue := random(0,1000)<it #random(0,2) > 0
    assert(readarray(id, x, y) == readzone(id, x, y))
    writezone(id, x, y, newvalue)
    writearray(id, x, y, newvalue)
    if (it, mod, 5 == 0) then (test area)
  )
  show value(2)
  wait

  # Now start using more zones, so that overcrowding occurs
  # But don't use more than 15 per tile
  for (it, 0, 5000) do (
    show value(10000+it)
    x := random(0,7)
    y := random(0,3)
    id := random(1,31)
    newvalue := random(0,2) > 0
    if (newvalue == 1 && array count zones(x, y) == 15) then (continue)
    assert(readarray(id, x, y) == readzone(id, x, y))
    writezone(id, x, y, newvalue)
    writearray(id, x, y, newvalue)
    if (it, mod, 200 == 0) then (test area)
  )
  shownovalue
  wait
end

########################################################################

script, master palette tests, begin
  $0="master palette tests"

  # Test reading master palette
  assert(get color(0) == RGB(255, 0, 255))
  assert(get color(6) == RGB(155, 83, 3))
  assert(extract color(get color(19), color:red) == 37)
  assert(extract color(get color(19), color:green) == 139)
  assert(extract color(get color(19), color:blue) == 255)

  # Test searching master palette
  assert(find color(255, 0, 255) == 0)
  assert(find color(0, 0, 0, 255) == 255)
  assert(find color(0, 0, 0, 256) == 0)
  assert(find color(255, 0, 255, 1) == 185)
  assert(find color(255, 0, 255, 185) == 185)
  assert(find color(255, 0, 255, 186) == 200)
  assert(find color(180, 180, 0) == 103)
  assert(find color(512, 100, -512) == 168)

  # Test get/set ui color
  assert(get ui color(ui:background) == 240)
  assert(get ui color(0) == 0)
  assert(get ui color(255) == 255)
  assert(get ui color(ui:MPBarFlash) == 37)  # Currently last UI col
  open menu(0)
  set ui color(ui:menu item, 99)
  assert(get ui color(ui:menu item) == 99)
  w
  close menu(top menu)
  set ui color(ui:menu item, ui:background)
  assert(get ui color(ui:menu item) == 240)
  set ui color(ui:menu item, 250)  # Reset

  # Test "get ui color" autotoggling
  variable(c1, c2, tog1, tog2, )
  c1 := get ui color(ui:selected item)
  c2 := get ui color(ui:selected item 2)
  assert(c1 == 14)
  assert(c2 == 15)
  tog1 := get ui color(ui:selected item 2, true)
  wait(1)
  assert(get ui color(ui:selected item 2) == c2)
  tog2 := get ui color(ui:selected item 2, true)
  # Currently it changes every tick
  assert((tog1 == c1 && tog2 == c2) || (tog1 == c2 && tog2 == c1))

  # Test box styles
  assert(get box style color(14) == 65)
  assert(get box style edge color(14) == 252)
  assert(get box style border(14) == 1)
  assert(get box style color(0) == 33)

  # Lots of other color commands could be tested
end

########################################################################

script, maptile tests, begin
  $0="maptile tests"
  # Test some tilemap, passmap, foemap commands
  # (Zones are tested separately in 'zone tests', and wallchecking
  # in 'wallchecking tests' and 'do hero cater tests')

  # Read some tiles on the left edge of the house (of map 0)
  assert(read map block(6, 5, 0) == 112)
  assert(read map block(6, 4, 0) == 96)
  assert(read map block(6, 4, 1) == 0)
  assert(read map block(6, 4, 2) == 58)
  assert(read pass block(6, 5) == westwall + southwall)

  # Oneway doors
  assert(read zone(zone:OneWayExit, 4, 10) == 1)
  assert(read pass block(4, 10) == southwall + northwall)
  assert(read pass block(5, 10) == southwall)

  # Write tilemap
  write map block(3, 2, 255, 1)
  assert(read map block(3, 2, 1) == 255)
  reset map state(mapstate:tilemap)
  assert(read map block(3, 2, 1) == 0)

  # Write passmap
  write pass block(3, 2, 255)
  assert(read pass block(3, 2) == 255)
  write pass block(3, 2, 0)

  # Read/write wall bit
  write pass block(3, 2, east wall + harm tile)
  assert(read wall bit(3, 2, east wall) == true)
  assert(read wall bit(3, 2, west wall) == false)
  write wall bit(3, 2, east wall, false)
  write wall bit(3, 2, vehicle a, true)
  assert(read pass block(3, 2) == vehicle a + harm tile)
  write pass block(3, 2, 0)

  # Read foemap
  assert(read foe map(14, 5) == 0)
  assert(read foe map(14, 6) == 1)
  assert(read foe map(-1, 0) == 0)

  # Test tile animations
  # An animated tile (water
  assert(read map block(3, 3, 0) == 192)

  # animation start tile (tile number, layer)
  assert(animation start tile (192, 0) == 144)
  # get tile animation offset (animation pattern, layer)
  # Second animation pattern isn't used, so this should be zero
  assert(get tile animation offset(1, 0) == 0)
  assert(currentdisplaytile(208) == 0)  # first tile of the range
  assert(currentdisplaytile(192) >= 144 && currentdisplaytile(192) <= 147)
  assert(currentdisplaytile(192) == 144 + get tile animation offset(0, 0))
  #set tile animation offset (animation pattern, offset, layer) not tested
  
  # These checks will have to change if any layers are added to map 0 or 1
  variable(rem map, rem x, rem y)
  rem map := current map
  rem x := hero x(me)
  rem y := hero y(me)
  
  teleport to map(0, 0, 0)
  w
  assert(last layer id == 2)
  assert(layer id under walkabouts == 1)
  w
  teleport to map(1, 0, 0)
  w
  assert(last layer id == 1)
  assert(layer id under walkabouts == 0)
  w
  # go back to the map we were on before
  teleport to map(rem map, rem x, rem y)
end

########################################################################

script, map state tests, begin
  $0="map state tests"

  # Preconditions
  assert(current map == 0)

  # These tests are quite incomplete:
  # No tests of NPC definition or map settings
  # No tests of saving/loading data when leaving/entering map
  # No tests that load/saving/deleting one state doesn't change the others
  # The RELOAD NPC instance file format is used only by map state, so it should be fully tested somewhere

  variable(mapstate:tilepass)
  mapstate:tilepass := mapstate:tilemap + mapstate:passmap

  # Test maptiles and walls, saving to a custom save state file
  assert(read map block(7, 2, 2) == 42)  # Precondition
  assert(read pass block(7, 2) == northwall)  # Precondition
  write map block(7, 2, 11, 2)
  write pass block(7, 2, eastwall)
  assert(read map block(7, 2, 2) == 11)
  assert(read pass block(7, 2) == eastwall)
  # "load map state" with a custom ID should do nothing if the file doesn't exist
  load map state(mapstate:tilepass, 1)
  assert(read map block(7, 2, 2) == 11)
  assert(read pass block(7, 2) == eastwall)
  # save/reset/load
  save map state(mapstate:tilepass, 1)
  reset map state(mapstate:tilepass)
  assert(read map block(7, 2, 2) == 42)
  assert(read pass block(7, 2) == northwall)
  load map state(mapstate:tilemap, 1)  # Half-hearted test of loading tiles and walls separately
  assert(read map block(7, 2, 2) == 11)
  assert(read pass block(7, 2) == northwall)
  load map state(mapstate:passmap, 1)
  assert(read pass block(7, 2) == eastwall)
  write map block(7, 2, 12, 2)
  write pass block(7, 2, westwall)
  # delete state
  delete map state(mapstate:tilepass, 1)
  # "load map state" with a custom ID should again do nothing
  load map state(mapstate:tilepass, 1)
  assert(read map block(7, 2, 2) == 12)
  assert(read pass block(7, 2) == westwall)
  # "load map state" with no ID should default to resetting
  load map state(mapstate:tilepass)
  assert(read map block(7, 2, 2) == 42)
  assert(read pass block(7, 2) == northwall)

  # Test zones
  assert(read zone(6, 16, 14) == true)  # Precondition
  assert(read zone(6, 15, 14) == false)  # Precondition
  assert(get zone extra(5, 0) == 10)  # Precondition
  write zone(6, 16, 14, false)
  write zone(6, 15, 14, true)
  set zone extra(5, 0, 11)
  write zone(765, 10, 20, true)  # an unused zone
  assert(read zone(6, 16, 14) == false)
  assert(read zone(6, 15, 14) == true)
  assert(read zone(765, 10, 20) == true)
  # "load map state" with a custom ID should do nothing if the file doesn't exist
  load map state(mapstate:zonemap, 1)
  assert(read zone(6, 16, 14) == false)
  assert(read zone(6, 15, 14) == true)
  assert(read zone(765, 10, 20) == true)
  assert(get zone extra(5, 0) == 11)
  # save/reset/load
  save map state(mapstate:zonemap, 1)
  reset map state(mapstate:zonemap)
  assert(read zone(6, 16, 14) == true)
  assert(read zone(6, 15, 14) == false)
  assert(read zone(765, 10, 20) == false)
  assert(get zone extra(5, 0) == 10)
  load map state(mapstate:zonemap, 1)
  assert(read zone(6, 16, 14) == false)
  assert(read zone(6, 15, 14) == true)
  assert(read zone(765, 10, 20) == true)
  assert(get zone extra(5, 0) == 11)
  write zone(6, 16, 14, true)
  write zone(6, 15, 14, true)
  set zone extra(5, 0, 12)
  # delete state
  delete map state(mapstate:zonemap, 1)
  # "load map state" with a custom ID should again do nothing
  load map state(mapstate:zonemap, 1)
  assert(read zone(6, 16, 14) == true)
  assert(read zone(6, 15, 14) == true)
  assert(read zone(765, 10, 20) == true)
  assert(get zone extra(5, 0) == 12)
  # "load map state" with no ID should default to resetting
  load map state(mapstate:zonemap)
  assert(read zone(6, 16, 14) == true)
  assert(read zone(6, 15, 14) == false)
  assert(read zone(765, 10, 20) == false)
  assert(get zone extra(5, 0) == 10)

  # Test NPC instances
  variable(npc, newnpc)
  npc := npc reference(4, 2)
  assert(npc x(npc) == 3 && npc y(npc) == 13)  # Precondition
  assert(npc copy count(7) == 0)  # Precondition
  set npc position(npc, 5, 6)
  newnpc := create NPC(7, 1, 2)
  assert(npc copy count(7) == 1)
  # "load map state" with a custom ID should do nothing if the file doesn't exist
  load map state(mapstate:npcs, 1)
  assert(npc x(npc) == 5 && npc y(npc) == 6)
  assert(npc x(newnpc) == 1 && npc y(newnpc) == 2)
  # save/load/reset
  save map state(mapstate:npcs, 1)
  # Check deleted NPCs are restored, with same npcref
  delete NPC(npc)
  assert(get NPC id(npc) == -1)  # Check the npcref is invalid
  reset map state(mapstate:npcs)
  # NPC references should stay valid if it's the same NPC
  assert(npc x(npc) == 3 && npc y(npc) == 13)
  # Check created NPCs are deleted
  assert(npc copy count(7) == 0)
  assert(get NPC id(newnpc) == -1)
  load map state(mapstate:npcs, 1)
  assert(npc x(npc) == 5 && npc y(npc) == 6)
  # Check created NPCs are loaded
  assert(npc copy count(7) == 1)
  assert(npc x(newnpc) == 1 && npc y(newnpc) == 2)
  # delete state
  delete map state(mapstate:npcs, 1)
  # "load map state" with a custom ID should again do nothing
  load map state(mapstate:npcs, 1)
  assert(npc x(npc) == 5 && npc y(npc) == 6)
  assert(npc x(newnpc) == 1 && npc y(newnpc) == 2)
  # "load map state" with no ID should default to resetting
  load map state(mapstate:npcs)
  assert(npc x(npc) == 3 && npc y(npc) == 13)
  assert(get NPC id(newnpc) == -1)
end

########################################################################

script, obsolete map data tests, begin
  $0="obsolete map data tests"

  variable(rem map, rem x, rem y)
  rem map := current map
  rem x := hero x(me)
  rem y := hero y(me)

  teleport to map(map: Obsolete map data DO NOT EDIT, 0, 0)
  # This map should not be opened in Custom, so that the map lumps aren't loaded and re-saved.

  # Copied from zone tests
  # Other zone data commands
  get zone name (95, 5)
  $96="extra data test"
  assert(string compare(95, 96))
  assert(extra length(get zone(5)) == 3)
  assert(get zone extra(5, 0) == 10)
  assert(get zone extra(5, 1) == -2147483648)
  assert(get zone extra(5, 2) == 2147483647)
  # A nonexistent zone
  get zone name (95, 9000)
  assert(string length(95) == 0)
  assert(extra length(get zone(9000)) == 3)
  assert(get zone extra(9000, 0) == 0)
  assert(get zone extra(9000, 1) == 0)
  assert(get zone extra(9000, 2) == 0)

  teleport to map(rem map, rem x, rem y)
end

########################################################################

script, wallchecking tests, begin
  $0="wallchecking tests"
  # This tests moveslicewithwallchecking and checkwallcollisionx/y commands.
  # This follows previous tests of basic hero movement ("do hero cater tests")
  # which check hero movement obeys walls.

  variable(tilex, tiley)
  tilex := 6
  tiley := 15
  # The tile at tilex,tiley is called the center tile

  # hitbox size
  variable(hitboxw, hitboxh)
  variable(hitbox)
  hitbox := create container
  set parent(hitbox, lookupslice(sl:map layer 0))

  subscript, set hitbox size, wide, high, begin
    hitboxw := wide
    hitboxh := high
    set slice size(hitbox, wide, high)
  end

  subscript, testcollide, testnum, x, y, xgo, ygo, expectedx, expectedy, expectedret = -1, begin
    #show value(testnum)
    #tracevalue(testnum, x, y, xgo, ygo, expectedx, expectedy)
    variable(resultx, resulty, ret)
    # if (false) then (
    #   resultx := check wall collision x(x, y, hitboxw, hitboxh, xgo, ygo)
    #   resulty := check wall collision y(x, y, hitboxw, hitboxh, xgo, ygo)
    # ) else (
      putslice(hitbox, x, y)
      ret := moveslicewithwallchecking(hitbox, xgo, ygo)
      resultx := slicex(hitbox) -- x
      resulty := slicey(hitbox) -- y
    # )
    #tracevalue(resultx, resulty)
    if (expectedret <> -1) then (
      #trace(string sprintf(98, $99="ret: %b  wanted %b", ret, expectedret))
      assert(ret == expectedret)
    )
    assert(resultx == expectedx)
    assert(resulty == expectedy)
  end


  # Test 45 degree and near-45 degree collisions with one of the 4 corners of the center tile
  # cornerx and cornery are 0 or 1 for left/up or right/bottom,
  # and also possibly the far corner if there are no walls at the first one.
  subscript, test corner, cornerx, cornery, cornermask, begin
    #tracevalue(cornerx, cornery)

    # The four walls of the center tile
    variable(wall up, wall left, wall right, wall down)
    wall left :=  (readpassblock(tilex,    tiley), and, westwall) ||
                  (readpassblock(tilex--1, tiley), and, eastwall)
    wall right := (readpassblock(tilex,    tiley), and, eastwall) ||
                  (readpassblock(tilex+1,  tiley), and, westwall)
    wall up    := (readpassblock(tilex, tiley   ), and, northwall) ||
                  (readpassblock(tilex, tiley--1), and, southwall)
    wall down  := (readpassblock(tilex, tiley   ), and, southwall) ||
                  (readpassblock(tilex, tiley+1 ), and, northwall)

    # whether there is a wall on the vertical or horizontal side of
    # the center tile adjacent to the corner
    variable(vside, hside, far vside, far hside)

    if (cornerx == 0) then (
      vside := wall left
      far vside := wall right
    ) else (
      vside := wall right
      far vside := wall left
    )

    if (cornery == 0) then (
      hside := wall up
      far hside := wall down
    ) else (
      hside := wall down
      far hside := wall up
    )

    # position in pixels of the corner
    variable(x, y)
    x := (tilex + cornerx) * 20
    y := (tiley + cornery) * 20
    # Convert to position of (top-left of) collider when at the corner
    if (cornerx == 0) then (x -= hitboxw)
    if (cornery == 0) then (y -= hitboxh)

    # Distance moved by collider before reaching corner
    variable(strikedist)
    strikedist := 22  # should be >= 2

    # 1 or -1, the sign of xgo/ygo of the collider
    variable(xgodir, ygodir)
    if (cornerx == 0) then (xgodir := 1) else (xgodir := -1)
    if (cornery == 0) then (ygodir := 1) else (ygodir := -1)

    # xgo/ygo are 10 pixels beyond the corner
    variable(xgo, ygo)
    xgo := xgodir * 2 * strikedist #(10 + strikedist)
    ygo := ygodir * 2 * strikedist #(10 + strikedist)

    # start position of collider (top-left), in pixels
    variable(startx, starty)
    startx := x -- xgodir * strikedist
    starty := y -- ygodir * strikedist

    if (vside || hside) then (
      # Hit the corner at 45 degree
      testcollide(1, startx, starty, xgo, ygo, xgodir * strikedist, ygodir * strikedist, cornermask)

      # Move parallel to the corner, so we miss it by 1 pixel, but still at 45 degree
      testcollide(2, startx + xgodir, starty,          xgo, ygo, xgodir * strikedist, ygodir * strikedist)
      testcollide(3, startx,          starty + ygodir, xgo, ygo, xgodir * strikedist, ygodir * strikedist)

      # Try angles slightly above and below 45 degree but which still hit the corner at the same time
      testcollide(4, startx+1, starty,   xgo -- 2, ygo,      xgodir * strikedist -- 1, ygodir * strikedist, cornermask)
      testcollide(5, startx,   starty+1, xgo,      ygo -- 2, xgodir * strikedist, ygodir * strikedist -- 1, cornermask)
      testcollide(6, startx--1, starty,   xgo + 2, ygo,      xgodir * strikedist + 1, ygodir * strikedist, cornermask)
      testcollide(7, startx,   starty--1, xgo,      ygo + 2, xgodir * strikedist, ygodir * strikedist + 1, cornermask)

      # Hit the corner in one axis a fraction of a pixel before the other
      testcollide(8, startx+1, starty,   xgo -- 1, ygo,      xgodir * strikedist -- 1, ygodir * strikedist, cornermask)
      testcollide(9, startx--1, starty,   xgo + 1, ygo,      xgodir * strikedist + 1, ygodir * strikedist, cornermask)
      testcollide(10, startx, starty -- 1,  xgo, ygo + 1,  xgodir * strikedist, ygodir * strikedist + 1, cornermask)
      testcollide(11, startx, starty + 1,  xgo, ygo -- 1,  xgodir * strikedist, ygodir * strikedist -- 1, cornermask)

    ) else if (strikedist > 20 && (far vside || farhside)) then (
      # Pass through the corner, but hit a wall on the opposite side

      variable(expectx, expecty, expect ret)
      expectx := xgodir * (strikedist+20)
      expecty := ygodir * (strikedist+20)

      # Hit the inside corner at 45 degree
      if (far vside) then (
        if (cornerx == 0) then (expectret += eastwall) else (expectret += westwall)
      )
      if (far hside) then (
        if (cornery == 0) then (expectret += southwall) else (expectret += northwall)
      )
      testcollide(20, startx, starty, xgo, ygo, expectx, expecty, expectret)

      # Hit the inside corner in one axis a fraction of a pixel before the other
      testcollide(21, startx + 1,  starty,      xgo -- 1, ygo,      expectx -- 1, expecty)
      testcollide(22, startx -- 1, starty,      xgo + 1,  ygo,      expectx + 1, expecty)
      testcollide(23, startx,      starty + 1,  xgo,      ygo -- 1, expectx, expecty -- 1)
      testcollide(24, startx,      starty -- 1, xgo,      ygo + 1,  expectx, expecty + 1)

    ) else (
      # Hit no walls
      testcollide(30, startx, starty, xgo, ygo, xgo, ygo, 0)
    )

  end

  # Some simple manually written test configurations
  # The tile at tilex,tiley has walls on all sides
  variable(x,y)
  x := tilex*20
  y := tiley*20
  set hitbox size(10, 10)
  testcollide(100, x--20, y--5,  20, 0, 10, 0, eastwall)  # hit wall from west
  testcollide(101, x+40,  y--5, -40, 0, -20, 0, westwall) # ...east
  testcollide(102, x--5,  y--15,  0, 20, 0, 5, southwall) # ...north
  testcollide(103, x--5,  y+35,   0, -20, 0, -15, northwall) # ...south

  # Diagonal movement that hits the corner in one axis a fraction of a pixel
  # before the other axis
  variable(hitx, hity)  # position of the the corner to hit
  hitx := tilex*20 + 20
  hity := tiley*20 + 20
  testcollide(110, hitx + 2,  hity + 3, -5, -6, -2, -3, northwest corner)
  testcollide(111, hitx + 3,  hity + 2, -6, -5, -3, -2, northwest corner)
  hitx := tilex*20 -- hitboxw
  testcollide(112, hitx -- 3, hity + 2,  6, -5,  3, -2, northeast corner)
  testcollide(113, hitx -- 2, hity + 3,  5, -6,  2, -3, northeast corner)
  hity := tiley*20 -- hitboxh
  testcollide(114, hitx -- 3, hity -- 2, 6,  5,  3,  2, southeast corner)
  testcollide(115, hitx -- 2, hity -- 3, 5,  6,  2,  3, southeast corner)

  # Test collision with the ends and center of a large hitbox (only x axis)
  set hitbox size(1, 58)
  variable(dx)  # the sign of xgo (whether hit from left or right)
  variable(expected)  # expected return value (wall mask)
  for(dx, 1, -1, -2) do (
    x := tilex*20
    if (dx == 1) then (x -= 10 + hitboxw) else (x += 10 + 20)
    if (dx == 1) then (expected := eastwall) else (expected := westwall)
    testcollide(104, x, y--19,dx*30, 9,   dx*10,   3, expected) # hitting with the center of the hitbox
    testcollide(105, x, y+9,  dx*30, 30,  dx*10,  10, expected) # just hit the corner w/ the top by 1 pixel
    testcollide(106, x, y+10, dx*30, 30,  dx*30,  30, 0)        # just miss
    testcollide(107, x, y--47,dx*30, -30, dx*10, -10, expected) # just hit the corner w/ the bottom by 1 pixel
    testcollide(108, x, y--58,dx*30, -30, dx*30, -30, 0)        # just miss
  )

  # Test hitting diagonal hits of corners of various wall configurations walls.
  # We test multiple hitbox sizes, but we're not actually doing a serious test of that
  for (hitboxw, 10, 30, 10) do (
    for (hitboxh, 10, 30, 10) do (
      set hitbox size(hitboxw, hitboxh)
      variable(tt, walls)
      tt := microseconds
      # Try all possible walls on the center tile
      for (walls, 15, 0, -1) do (
        #tracevalue(hitboxw, hitboxh, walls, 1)
        write pass block(tilex, tiley, walls)
        testcorner(0,0, southeast corner)
        testcorner(0,1, northeast corner)
        testcorner(1,0, southwest corner)
        testcorner(1,1, northwest corner)
      )
      # Test walls in the same places, but on the adjacent 4 tiles instead
      for (walls, 15, 0, -1) do (
        #tracevalue(hitboxw, hitboxh, walls, 2)
        write pass block(tilex+1, tiley, walls,and,westwall)
        write pass block(tilex, tiley+1, walls,and,northwall)
        write pass block(tilex--1, tiley, walls,and,eastwall)
        write pass block(tilex, tiley--1, walls,and,southwall)
        testcorner(0,0, southeast corner)
        testcorner(0,1, northeast corner)
        testcorner(1,0, southwest corner)
        testcorner(1,1, northwest corner)
      )
    )
  )
  tracevalue(microseconds--tt)
  show no value
  free slice(hitbox)
end

########################################################################

script, platform tests, begin
  $0="platform tests"

  # Blackbox, but we test reading from autotest_config.ini
  assert(read environment(1, $2="nonexistent") == 0)
  assert(string equal(1, $3=""))
  # Test case insensitivity
  assert(read environment(1, $2="Has_Value_42") == 42)
  assert(string equal(1, $3="42"))

  # Blackbox only. These don't do or return anything.
  assert(xbox request account picker == 0)
  assert(ps5 start story == 0)
  assert(ps5 end story == 0)

  # Steam or blackbox.
  # achievements.rpgdir can be used to test this actually works, here we just
  # test it throws no error.
  set rich presence($1="status")
  set rich presence($1="status", $2="substitution")
end

########################################################################

script, save slot tests, begin
  $0="save slot tests"
  # This includes tests for commands which operate on save slots,
  # but doesn't test load saves itself; that's in "savegame tests"

  delete save(1)
  assert(save slot used(1) == false)

  # import/export globals

  export global 1 := -123456789
  export globals(1, @export global 1, @export global 1)
  export global 1 := -2
  assert(import globals(1, @export global 1) == -123456789)
  assert(export global 1 == -2)
  import globals(1, @export global 1, @export global 2)
  assert(export global 1 == -123456789)
  assert(export global 2 == 0)
  export global 2 := -100
  export globals(1, @export global 2, @export global 2)
  save in slot(2)
  export global 1 := 0
  export global 2 := 0
  export globals(1, @export global 1, @export global 2)
  export global 1 := 100
  export global 2 := 100
  import globals(1, @export global 1, @export global 2)
  assert(export global 1 == 0)
  assert(export global 2 == 0)
  
  # high global id numbers
  # (Export this to slot 3 so I can inspect autotest.saves/2.rsav)
  the global with the biggest id := 123
  assert(the global with the biggest id == 123)
  assert(@the global with the biggest id == 50000)
  export globals(3, @the global with the biggest id, @the global with the biggest id)
  write global(@the global with the biggest id, 321)
  assert(import globals(3, @the global with the biggest id) == 123)
  assert(the global with the biggest id == 321)

  # exporting globals should not cause a slot to appear as used
  assert(save slot used(1) == false)
  assert(save slot used(2) == true)

  # testing saveinslot earlier
  import globals(2, @export global 1, @export global 2)
  assert(export global 1 == -123456789)
  assert(export global 2 == -100)

  # different form
  export global 1 := -3
  assert(import globals(2, @export global 1, @export global 1) == 0)
  assert(export global 1 == -123456789)

  # loading from nonexistent save... must zero out the globals without throwing an error
  import globals(28, @export global 1, @export global 2)
  assert(export global 1 == 0)
  assert(export global 2 == 0)

  # Check interaction between deletesave and importglobals
  # Originally, deletesave was doucmented as preserving stored global variables, so they could
  # still be read with importglobals. Since switching to RSAV in Zenzizenzic (2011), deletesave
  # now deletes the globals too!
  # Using rpgbatch, it seems no games are affected by the change (see svn commit message)
  export global 1 := 100
  export global 2 := 100
  export globals(1, @export global 1, @export global 2)
  delete save(1)   # Slot 1 only had global variables
  delete save(2)   # Slot 2 had a full save
  assert(save slot used(1) == false)
  assert(save slot used(2) == false)
  export global 1 := -1
  export global 2 := -2
  import globals(1, @export global 1, @export global 2)
  # Pre-Zenzizenzic behaviour
  #assert(export global 1 == -123456789)
  #assert(export global 2 == -100)
  # New behaviour
  assert(export global 1 == 0)
  assert(export global 2 == 0)
  import globals(2, @export global 1, @export global 2)
  # Pre-Zenzizenzic behaviour
  #assert(export global 1 == -123456789)
  #assert(export global 2 == -100)
  # New behaviour
  assert(export global 1 == 0)
  assert(export global 2 == 0)
end

########################################################################

script, string load tests, begin
  $0="string load tests"

  # attack string tests in "attack tests"

  # Stat names
  assert(get stat name(1, stat:hp) == true)
  assert(string compare(1, $2="HP"))
  get stat name(1, stat:hits)  # Last stat
  assert(string compare(1, $2="Hits"))
  get stat name(1, 15)  # Last register
  assert(string compare(1, $2="mute register"))
  assert(get stat name(1, 16) == false)  # Out of bounds
  assert(string compare(1, $2=""))

  embedding global := -1234
  $3="<embedded> "

  # Test the obsolete stringfromtextbox: 4th arg ignored
  stringfromtextbox(1, 8, 2)
  $2="."
  assert(string compare(1, 2))
  stringfromtextbox(1, 8, 3)
  $2=""
  assert(string compare(1, 2))
  stringfromtextbox(1, 8, 4)
  $2="##"
  assert(string compare(1, 2))
  stringfromtextbox(1, 8, 5, false)
  $2=",-1234. ,<embedded> "
  assert(string compare(1, 2))
  stringfromtextbox(1, 8, 5, true)
  assert(string compare(1, 2))

  # textbox line
  textboxline(1, 8, 2)
  $2="."
  assert(string compare(1, 2))
  textboxline(1, 8, 3)
  $2=""
  assert(string compare(1, 2))
  textboxline(1, 8, 4)
  $2=" ## "
  assert(string compare(1, 2))
  textboxline(1, 8, 4, true, false)
  assert(string compare(1, 2))
  textboxline(1, 8, 4, true, true)
  $2="##"
  assert(string compare(1, 2))
  textboxline(1, 8, 5, false)
  $2=" ,${V105}. ,${S3} "
  assert(string compare(1, 2))
  textboxline(1, 8, 5, true)
  $2=" ,-1234. ,<embedded>  "
  assert(string compare(1, 2))
  textboxline(1, 8, 5, true, true)
  $2=",-1234. ,<embedded> "
  assert(string compare(1, 2))

  # textbox text
  $2=" This box is for testing textboxline\n and stringfromtextbox\n.\n\n ## \n ,-1234. ,<embedded>  "
  textboxtext(1, 8, true, false)
  assert(string compare(1, 2))

  # get scancode name tests
  $1="Left Bracket"
  get scancode name(2, key:leftbracket)
  assert(string compare(1,2))
  $1="["
  get scancode name(2, key:leftbracket, false)
  assert(string compare(1,2))
  $1="Flee"  # Previously "Run"
  get scancode name(2, flee key)  # AKA run key
  assert(string compare(1,2))
  $1="Menu"
  get scancode name(2, menu key)
  assert(string compare(1,2))
  $1="Cancel"
  get scancode name(2, cancel key)
  assert(string compare(1,2))
  $1="None"
  get scancode name(2, 0)
  assert(string compare(1,2))
  $1="Space"
  get scancode name(2, key:space, false)
  assert(string compare(1,2))
  $1="Left Shift"
  get scancode name(2, key:left shift, false)
  assert(string compare(1,2))
  $1="Gamepad B"
  get scancode name(2, joy:B, false)
  tracevalue(joy:B)
  trace(2)
  assert(string compare(1,2))
  $1="Gamepad Up"
  get scancode name(2, joy:up, false)
  assert(string compare(1,2))
  $1="Gamepad Button 16"
  get scancode name(2, joy:button16, false)
  assert(string compare(1,2))

  # get slice lookup name tests
  $1=""
  get slice lookup name(2, 0, true)
  assert(string compare(1,2))
  get slice lookup name(2, 0, false)
  assert(string compare(1,2))
  $1="map layer0"
  get slice lookup name(2, sl:maplayer0)
  assert(string compare(1,2))
  $1="Some lookup"
  get slice lookup name(2, sli:Some lookup)
  assert(string compare(1,2))
  $1="Lookup-40"
  get slice lookup name(2, -40)
  assert(string compare(1,2))
  $1=""
  get slice lookup name(2, -40, false)
  assert(string compare(1,2))
end

########################################################################

script, savegame tests, begin
  $0="saving/loading tests 1"
  trace(0)
  # Partially split into savegame tests 2

  # Preconditions
  assert(current map == 0)
  delete save(1)
  assert(save slot used(1) == false)

  savegame map := current map
  savegame slice := create rect(15, 20)
  put slice(savegame slice, 40, 40)
  savegame npc := createnpc(0, 5, 5)

  # Saving of string state
  # There are no commands to return the current colour and style
  # of a string (and I don't see any need to add them), so that can only be
  # tested by screenshot.
  $5 = "This is a green string"
  show string at(5, 100, 100)
  string color(5, 72)
  # Should default to 'string:outline' style
  $6 = "This is flat yellow-on-purple"
  show string at(6, 100, 110)
  string style(6, string:flat)
  string color(6, 14, 194)  # yellow, purple
  $7="This should be plain style"
  show string at(7, 100, 120)
  w

  start tick := ticknumber  # Checked on loading
  trace(string sprintf(99, $99="before save in slot(1), ticknumber = %d", ticknumber))

  hook loadgame := 1
  save in slot(1)
  hook loadgame := 0
  assert(save slot used(1) == true)

  # Change state a bit to check it's actually reset and loaded
  hide string(5)
  $5 = ""
  $6 = "Now it's plain"
  string color(6)
  $7 = "Now it's yellow-on-purple"
  string style(7, string:flat)
  string color(7, 14, 194)  # yellow, purple
  show string at(8, 0, 0)
  teleport to map(1)
  savegame map := -1
  free slice(savegame slice)
  w

  load from slot(1)
  # continues from "after loadgame"

  $0="Loadgame didn't happen!"
  crash
end

script, after loadgame, arg1, arg2, arg3, begin
  # "tick counter slice" is loaded so ticknumber should keep working uninterrupted
  assert(ticknumber == start tick)

  if (hook loadgame == 1) then (
    assert(arg1 == 0)
    assert(arg2 == 0)
    assert(arg3 == 0)
    savegame tests 2
    crash
  ) else (
    assert(arg1 == 30)
    assert(arg2 == -100)
    assert(arg3 == 0)
    hook loadgame := 0
  )
end


# This is called from the loadgame plotscript
script, savegame tests 2, begin
  $0="saving/loading tests 2"
  trace(0)

  # Check globals, map number, slices loaded (game is set to save slices)
  assert(savegame map == current map)
  assert(slicewidth(savegame slice) == 15)
  assert(ticknumber == start tick)  # Reads a slice's X

  # Check NPCs were NOT saved
  assert(get npc id(savegame npc) == -1)  # doesn't exist

  # Check strings were loaded
  assert(string is visible(5))
  assert(string is visible(6))
  assert(string is visible(8) == false)
  $8 = "This is a green string"
  assert(string compare(5, 8))
  assert(string x(6) == 100)
  assert(string y(6) == 110)
  # Can't test colour, style
  w

  # Clean up
  hide string(5)
  hide string(6)
  hide string(7)
  free slice(savegame slice)

  start tick := ticknumber
  trace(string sprintf(99, $99="before save in slot(1), ticknumber = %d", ticknumber))

  # Now try passing args to the loadgame script
  hook loadgame := 2
  save in slot(1)
  hook loadgame := 0
  load from slot(1, 30, -100)
  # continues from "after loadgame"

  $0="Loadgame didn't happen!"
  crash
end


########################################################################

script, reset game tests, begin
  $0="reset game tests"
  trace(0)

  # Test with no args
  called resetgame := true
  export globals(save slot: persistent, @called resetgame, @called resetgame)
  start tick := ticknumber
  export globals(save slot: persistent, @start tick, @start tick)
  reset game
  $0="resetgame didn't happen!"
  crash
end

# Called with default args
plotscript, after resetgame, arg1, arg2, arg3, begin
  $0="reset game tests"
  trace(0)

  # Lets say that one tick has passed
  import globals(save slot: persistent, @start tick, @start tick)
  assert(tick counter slice == 0)
  start tick counter(start tick + 1)

  called resetgame := false
  export globals(save slot: persistent, @called resetgame, @called resetgame)

  if (arg1 == 0) then (
    # after resetting with no args
    assert(arg2 == 0)
    assert(arg3 == 0)

    reset game(10, 20)
    $0="resetgame didn't happen!"
    crash
  ) else (
    # after resetting with args
    assert(arg1 == 10)
    assert(arg2 == 20)
    assert(arg3 == 0)
    # exit the script and continue to next tests
  )
end

########################################################################

script, map autorun tests, begin
  $0="map autorun tests"
  autorun counter := 0
  variable(last map, last x, last y)
  last map := current map
  last x := hero x(me)
  last y := hero y(me)
  w
  
  teleport to map(map:map with an autorun, 1, 1)
  assert(autorun counter == 1)

  # teleporting to a map you are already on does still run the autorun
  teleport to map(map:map with an autorun, 2, 2)
  assert(autorun counter == 2)
  
  # force a re-run of the autorun
  run script by id(get current map autorun script, get current map autorun script argument)
  assert(autorun counter == 3)
  
  w
  teleport to map(last map, last x, last y)

end

plotscript, simple autorun, arg, begin
  assert(arg == 444)
  autorun counter += 1
end

########################################################################


# rungame is not actually tested!
script, run game tests, begin
  $0="run game tests"

  assert(check game exists($1="") == false)
  assert(check game exists($1="test.rpg"))
  assert(check game exists($1="foo.rpg") == false)
  #assert(check game exists($1="test.rpgdir"))
  assert(check game exists($1="autotest.rpgdir"))
  assert(check game exists($1="autotest.hss") == false)
  assert(check game exists($1="../testgame") == false)
  assert(check game exists($1="../testgame/autotest.rpgdir"))
  assert(check game exists($1="..\\testgame/Autotest.rpgdir"))
  assert(check game exists($1="../testgame\\autotest.rpgdir"))
  assert(check game exists($1="autotest.rpg") == false)
  #assert(check game exists($1="./testgame\\test.rpgDIR")) # Also check .rpg if given .rpgdir (not implemented)
  assert(check game exists($1=".//..\\TESTGAME\\autotest.RPGdir"))
  #assert(check game exists($1="../vikings/vikings.rpg"))  # Also check .rpgdir if given .rpg (not implemented)
  # Should trailing slashes be allowed?
  #assert(check game exists($1="../vikings/vikings.rpgdir/") == false)
  #assert(check game exists($1="..\\vikings\\vikings.rpgdir\\") == false)
end


########################################################################
#### COMMANDS THAT STILL NEED TESTS
#### (Actually, this list is out of date)
#### (Note: interactive commands should be in interactivetest.hss, and
#### fundamental HS tests are in hstests.hss)
#allow minimap (setting)
#allow save anywhere (setting)
#append ascii (ID, char)
#ascii from string (ID, position)
#autosave
#camera follows hero (who)
#camera follows NPC (who)
#camera pixel X
#camera pixel Y
#cancel map name display
#center slice (handle)
#change tileset (tileset, layer)
#check NPC wall (who, direction)
#check parentage (handle, parent handle)
#clamp slice (handle1, handle2)
#clone sprite (handle)
#concatenate strings (dest, source)
#copy string (dest, source)
#create ellipse (width, height, border color, fill color)
#create grid (width, height, rows, columns)
#days of play
#delete char (ID, position)
#delete save (slot)
#destroy NPC (reference)
#dismount vehicle
#draw NPCs above heroes (setting)
#expand string(ID)
#extended scancodes enabled
#extract color(color, component)
#fade screen in
#fade screen out (red,green,blue)
#first child(handle)
#first container child(handle)
#first rect child(handle)
#first sprite child(handle)
#focus camera (x,y,speed)
#formation probability (formation set, formation)
#formation set frequency (formation set)
#free slice children (handle)
#free sprite (handle)
#game over
#get ambient music
#get color(index)
#get damage cap
#get each step script
#get ellipse border col (handle)
#get ellipse fill col (handle)
#get foot offset
#get global string (ID, global)
#get grid columns (handle)
#get grid rows (handle)
#get instead of battle script
#get load script
#get map edge mode
#get map name (ID, map)
#get map tileset
#get money (amount)
#get NPC ignores walls (who)
#get NPC moves (who)
#get NPC obstructs (who)
#get NPC usable (who)
#get on keypress script
#get outline(handle)
#get slice velocity x (handle)
#get slice velocity y (handle)
#get song name (ID, song)
#get sort order (handle)
#get text bg(handle)
#get text color(handle)
#get victory music
#get wrap(handle)
#globals to string(ID, starting global, length)
#greyscale palette (first, last)
#grid is shown (handle)
#hide battle health meter (state)
#hide battle ready meter (state)
#horiz flip sprite (handle, flip)
#hours of play
#last child(handle)
#last formation
#last save slot
#layer tileset (layer)
#leader
#load attack sprite (num, palette)
#load backdrop sprite (num)
#load border sprite (num, palette)
#load hero sprite (num, palette)
#load large enemy sprite (num, palette)
#load medium enemy sprite (num, palette)
#load palette (palette number)
#load portrait sprite (num, palette)
#load small enemy sprite (num, palette)
#load tileset (tileset, layer)
#load walkabout sprite (num, palette)
#load weapon sprite (num, palette)
#lose money (amount)
#map cure (attack, target, attacker)
#map height (map)
#map width (map)
#milliseconds
#minutes of play
#move slice above (handle, above what handle)
#move slice below (handle, below what handle)
#move slice by (handle, relative x, relative y, ticks)
#move slice to (handle, x, y, ticks)
#next container sibling(handle)
#next rect sibling(handle)
#next sibling(handle)
#next sprite sibling(handle)
#NPC at pixel (x, y, number)
#NPC at spot (x, y, number)
#number from string (ID, default)
#order menu
#outside battle cure
#overhead tile
#pan camera (direction,distance,pixelstep)
#party money
#pay money (amount)
#place sprite
#previous sibling(handle)
#put camera (x,y)
#put slice screen (handle, x, y)
#put sprite (handle, x, y)
#random formation (formation set)
#read color (index, element)
#realign slice (handle, horiz align, vert align, horiz anchor, vert anchor)
#replace attack sprite (handle, num, palette)
#replace backdrop sprite (handle, num)
#replace border sprite (handle, num, palette)
#replace char (ID, position, char)
#replace hero sprite (handle, num, palette)
#replace large enemy sprite (handle, num, palette)
#replace medium enemy sprite (handle, num, palette)
#replace portrait sprite (handle, num, palette)
#replace small enemy sprite (handle, num, palette)
#replace walkabout sprite (handle, num, palette)
#replace weapon sprite (handle, num, palette)
#reset palette
#resume map music
#resume NPCs
#resume NPC walls
#resume obstruction
#resume overlay
#resume random enemies
#resume random enemys
#resume timers
#RGB(red, green, blue)
#run game
#run script by ID (id, argument1, argument2, argument3...)
#save menu (reallysave)
#search string (ID1, ID2, start)
#seconds of play
#seed random (new seed)
#set battle wait mode (state)
#set bottom padding (handle, pixels)
#set color(index, value)
#set damage cap (cap)
#set days of play (days)
#set debug keys disable (state)
#set ellipse border col (handle, color)
#set ellipse fill col (handle, color)
#set foot offset (offset)
#set grid columns (handle, columns)
#set grid rows (handle, rows)
#set harm tile damage (amount)
#set harm tile flash (color)
#set horiz align (handle, edge)
#set horiz anchor (handle, edge)
#set hours of play (hours)
#set inn no revive mode (state)
#set instead of battle script (id)
#set inventory size (new size)
#set left padding (handle, pixels)
#set load script (id)
#set map edge mode (mode, default tile)
#set minutes of play (min)
#set money (amount)
#set no HP level up restore (state)
#set no MP level up restore (state)
#set NPC ignores walls (who, value)
#set NPC moves (who, value)
#set NPC obstructs (who, value)
#set NPC speed (who, speed)
#set NPC usable (who, value)
#set on keypress script (id)
#set outline(handle, outline)
#set padding (handle, pixels)
#set rect bgcol (handle, color)
#set rect fgcol (handle, color)
#set right padding (handle, pixels)
#set seconds of play (sec)
#set slice clipping (handle, clip)
#set slice edge x (handle, edge, value)
#set slice lookup (handle, code)
#set slice screen x (handle, x)
#set slice screen y (handle, y)
#set slice velocity (handle, horiz pixels per tick, vert pixels per tick, ticks)
#set slice velocity x (handle, pixels per tick, ticks)
#set slice velocity y (handle, pixels per tick, ticks)
#set slice visible (handle, vis)
#set sort order (handle, order)
#set sprite frame (handle, num)
#set sprite palette (handle, num)
#set sprite trans (handle, drawtransparent)
#set sprite visible
#set tag (tag,value)
#set text bg(handle, color)
#set text color(handle, color)
#set tile animation offset (animation pattern, offset, layer)
#set timer (id, count, speed, trigger, string, flags)
#set top padding (handle, pixels)
#set vert align (handle, edge)
#set vert anchor (handle, edge)
#show backdrop (number)
#show grid (handle, shown)
#show map
#show no value
#show value (number)
#slice collide (handle1, handle2)
#slice collide point (handle, x, y)
#slice contains (handle1, handle2)
#slice edge x (handle, edge)
#slice edge y (handle, edge)
#slice is ellipse (handle)
#slice is grid (handle)
#slice is moving (handle)
#slice to back (handle)
#slice to front (handle)
#sort children (handle, wipe)
#sqrt (number)
#stop slice (handle)
#stop timer (id)
#string to globals (ID, starting global, length)
#suspend catapillar
#suspend map music
#suspend NPCs
#suspend NPC walls
#suspend obstruction
#suspend overlay
#suspend random enemies
#suspend random enemys
#suspend timers
#system day
#system hour
#system minute
#system month
#system second
#system year
#tweak palette (red, green, blue, first, last)
#update palette
#use item (item)
#use item in slot(slot)
#vert flip sprite (handle, flip)
#wait for all
#wait for camera
#wait for NPC (who)
#wait for slice (handle)
#walk NPC to X (who, X)
#walk NPC to Y (who, Y)
#write color (index, element, value)
#Y sort children (handle)
