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

include "gamelib.plh"

///////////////////////////////////////////////////////////////////////////////////////////////////
// Fixed memory locations
const seed          = $4E     // Incremented continuously by keyboard read routine
const displayEngine = $6000   // main mem (raycaster and tile engine at same location)
const expandVec     = $2000   // aux mem (only for raycaster)
const fontEngine    = $E000   // main mem
const heapStart     = $F000   // main mem

const heapSize      = $800

///////////////////////////////////////////////////////////////////////////////////////////////////
// Other constants
const kbd       = $C000
const kbdStrobe = $C010

const CHAR_WND_LIFE_X = 91
const CHAR_WND_GUN_X  = 114

const ANIM_FLAG_FWD      = $20
const ANIM_FLAG_FWD_BKWD = $40
const ANIM_FLAG_RANDOM   = $80

const ANIM_PAUSE_MAX = 300

include "playtype.plh"
include "gen_images.plh"
include "gen_modules.plh"
include "gen_enemies.plh"
include "globalScripts.plh"      // old-style
include "gen_globalScripts.plh"  // new-style
include "combat.plh"

///////////////////////////////////////////////////////////////////////////////////////////////////
// Data structures
include "playtype.pla"

word global  // the global heap object, from which all live objects must be reachable

///////////////////////////////////////////////////////////////////////////////////////////////////
// Predefined functions, for circular calls or out-of-order calls
predef setWindow2, initCmds, nextAnimFrame, checkEncounter

///////////////////////////////////////////////////////////////////////////////////////////////////
// Global variables
byte mapNum = -1
byte mapIs3D = -1
word mapNameHash = 0
word totalMapWidth
word totalMapHeight

byte needRender = FALSE
byte renderLoaded = FALSE
byte textDrawn = FALSE
byte isPlural = FALSE

word skyNum = 9
word groundNum = 10
byte portraitNum = 0

word triggerOriginX, triggerOriginY
word triggerTbl

word cmdTbl[64]
byte frameLoaded = 0

// Queue setMap / teleport, since otherwise script might be replaced while executing
byte q_mapIs3D = 0
byte q_mapNum = 1
word q_x = 0
word q_y = 0
byte q_dir = 0

// For decimal conversion and display tabbing
byte decimalBuf[7]
byte tabBuf[5]

// Animation tracking
word curPortrait = 0
byte animFlags
byte animDir
byte animDirCt
byte animNumFrames
byte animFrame
word animPauseCt

// Game library functions
predef _setScriptInfo, _scriptDisplayStr, _scriptDisplayStrNL, _getYN
predef _queue_setMap, _setSky, _setGround, _queue_teleport, _setPortrait, _clearPortrait
predef _moveBackward, _getUpperKey, _clearWindow, _setPortrait
predef _getGlobals, _rand16
predef _printf1, _printf2, _printf3
predef _displayf1, _displayf2, _displayf3
predef _buildString, _addToString, _finishString, _rawDisplayStr, _displayStr
predef _puts, _min, _max
predef _countList, _countListFiltered, _randomFromListFiltered, _addToList, _beep
predef _showParty, _mmgr, _setWindow1, _setWindow2, _setWindow3
predef _reboot, _brk, _encodeDice, _rollDice
predef _setPlural, _makeEnemy, _getStringResponse, _strcmpi, _addEncounterZone, _fatal

word gameLib_addrs = @_setScriptInfo, @_scriptDisplayStr, @_scriptDisplayStrNL, @_getYN
word = @_queue_setMap, @_setSky, @_setGround, @_queue_teleport, @_setPortrait, @_clearPortrait
word = @_moveBackward, @_getUpperKey, @_clearWindow
word = @_getGlobals, @_rand16
word = @_printf1, @_printf2, @_printf3
word = @_displayf1, @_displayf2, @_displayf3
word = @_buildString, @_addToString, @_finishString, @_rawDisplayStr, @_displayStr
word = @_puts, @_min, @_max
word = @_countList, @_countListFiltered, @_randomFromListFiltered, @_addToList, @_beep
word = @_showParty, @_mmgr, @_setWindow1, @_setWindow2, @_setWindow3
word = @_reboot, @_brk, @_encodeDice, @_rollDice
word = @_setPlural, @_makeEnemy, @_getStringResponse, @_strcmpi, @_addEncounterZone, @_fatal

word = 0  // end of library functions

///////////////////////////////////////////////////////////////////////////////////////////////////
// Definitions used by assembly code
asm __defs

; Use hi-bit ASCII for Apple II
!convtab "../../include/hiBitAscii.ct"

; Headers
!source "../../include/global.i"
!source "../../include/plasma.i"
!source "../../include/mem.i"
!source "../../include/fontEngine.i"

; Optional debug printing support
DEBUG   = 0

; General use
tmp         = $2
pTmp        = $4
ysav        = $34
ysav1       = $35

; 16-bit random number seed - incremented by ROM kbd routine
seed        = $4E
magic       = $2227 ; there are 2048 magic values that work; this one caught my eye. - MH

; NOTE ABOUT ABSOLUTE CODE ADDRESSING (e.g. STA .var, JMP .label, etc.)
; We cannot use it: this code, including variable space, can be loaded *anywhere*.
; So don't JMP to labels, declare any variables as !byte or !word here, etc.

end

///////////////////////////////////////////////////////////////////////////////////////////////////
// API to call rendering engine (same API for raycaster and tile engine)
asm initDisplay   // params: mapNum, pMapData, x, y, dir
        +asmPlasm 5
        jmp $6000
end
asm flipToPage1  // no params
        +asmPlasm 0
        jmp $6003
end
asm getPos       // params: @x, @y
        +asmPlasm 2
        jmp $6006
end
asm setPos       // params: x, y
        +asmPlasm 2
        jmp $6009
end
asm getDir       // no params; returns: dir (0-15)
        +asmPlasm 0
        jmp $600C
end
asm setDir       // params: dir (0-15)
        +asmPlasm 1
        jmp $600F
end
asm advance      // no params; return: 0 if same pos, 1 if new pos, 2 if new pos and scripted
        +asmPlasm 0
        jmp $6012
end
asm setColor     // params: slot (0=sky/1=ground), color (0-17)
        +asmPlasm 2
        jmp $6015
end
asm render      // no params
        +asmPlasm 0
        jmp $6018
end
asm texControl  // params: load (1=load, 0=unload)
        +asmPlasm 1
        jmp $601B
end

///////////////////////////////////////////////////////////////////////////////////////////////////
asm setAuxCopy
        rts
end

///////////////////////////////////////////////////////////////////////////////////////////////////
asm readAuxByte     // params: ptr; ret: char
        +asmPlasm 1

        ; Create the following subroutine, used to copy one char from aux to main:
        ;0010-   8D 03 C0    STA   $C003
        ;0013-   AD XX XX    LDA   $XXXX
        ;0016-   8D 02 C0    STA   $C002
        ;0019-   60          RTS

        sta $14
        sty $15
        lda #$8D
        sta $10
        sta $16
        ldx #2
        stx $17
        inx
        stx $11
        lda #$C0
        sta $12
        sta $18
        lda #$AD
        sta $13
        lda #$60
        sta $19

        ; And call the routine
        ldy #0
        jmp $10
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// String building for display with the font engine. Includes plurality processing to handily 
// handle things like "Dirt bag(s)" and "his/their"
asm _buildString
        +asmPlasm 1
        sta cswl
        sty cswh
        lda #0
        sta inbuf
        rts
end

asm _addToString
        sty ysav1
        inc inbuf
        ldy inbuf
        sta inbuf,y
        ldy ysav1
        rts
end

asm _finishString
!zone {
        +asmPlasm 1
        sta tmp     ; save isPlural flag
        jsr setvid  ; put the cout vector back to default
        bit monrts  ; V flag for prev-is-punctuation
        ldy #1      ; dest offset in Y
        ldx #1      ; source offset in X
        cpx inbuf
        beq +       ; only process if string has at least 1 char
        bcs .done
+       sty tmp+1   ; offset of last punctuation
.fetch  lda inbuf,x
        cmp #"("
        bne .notpar
        bvs .notpar ; skip paren processing right punctuation
        lda tmp     ; check isPlural flag
        bne .plurpr
-       lda inbuf,x ; it's singular, so skip everything in parens
        cmp #")"
        beq .next
        inx
        cpx inbuf
        bne -
        beq .done   ; handle missing trailing paren
.plurpr inx         ; it's plural, so copy everything within the parens
        lda inbuf,x ; copy characters
        cpx inbuf   ; handle missing trailing paren
        beq .store
        cmp #")"    ; go until we reach ending paren
        beq .next
        sta inbuf,y
        iny
        bne .plurpr ; always taken

.notpar cmp #"/"
        bne .notsl
        bvs .notsl  ; skip slash processing right after punctuation
        lda tmp     ; check isPlural flag
        bne .plursl
-       inx         ; loop that skips plural form
        cpx inbuf
        beq +
        bcs .done   ; handle end of string
+       lda inbuf,x
        cmp #"A"    ; eat letters (and stop when we hit punctuation)
        bcs -
        bcc .store  ; copy the ending punctuation and continue normal processing
.plursl ldy tmp+1   ; erase singular form by backing up to prev punc
        iny         ;     plus 1 to retain prev punc
        bne .next   ; resume regular copying of the plural form

.notsl  cmp #"A"    ; if <= ASCII "A", consider it punctuation
        bcc +
        clv         ; clear last-is-punc flag
        bvc .store  ; always taken
+       bit monrts  ; set prev-is-punc flag
        sty tmp+1   ; save dest offset of last punctuation

.store  sta inbuf,y ; save to dest
        iny

.next   inx
        cpx inbuf   ; compare src offset to length
        bcc .fetch  ; loop while less than
        beq .fetch  ;       or equal

.done   dey
        sty inbuf   ; save new length
        lda #<inbuf
        ldy #>inbuf
        rts
}
end

///////////////////////////////////////////////////////////////////////////////////////////////////
asm blitPortrait      // params: srcData, dstScreenPtr
        +asmPlasm 2

        ; Save the dest pointer
        sta pTmp
        sty pTmp+1

        ; Save the source pointer
        lda evalStkL+1,x
        sta tmp
        lda evalStkH+1,x
        sta tmp+1

        ; Create the following subroutine, used to copy pixels from aux to main:
        ; 0010-   8D 03 C0    STA   $C003
        ; 0013-   B1 02       LDA   ($02),Y
        ; 0015-   91 04       STA   ($04),Y
        ; 0017-   88          DEY
        ; 0018-   10 F9       BPL   $0013
        ; 001A-   8D 02 C0    STA   $C002
        ; 001D-   60          RTS
        lda #$8D
        sta $10
        sta $1A
        ldx #2
        stx $14
        stx $1B
        inx
        stx $11
        lda #$C0
        sta $12
        sta $1C
        lda #$B1
        sta $13
        lda #$91
        sta $15
        inx
        stx $16
        lda #$88
        sta $17
        lda #$10
        sta $18
        lda #$F9
        sta $19
        lda #$60
        sta $1D

        ; Outer copy loop
        ldx #128  ; line count
.loop   ldy #17   ; byte count minus 1. There are 18 bytes per line
        jsr $10   ; copy pixel bytes

        ; Advance to next row of data
        lda tmp
        clc
        adc #18
        sta tmp
        bcc +
        inc tmp+1
+
        ; Fun code to advance to the next hi-res line
        ldy pTmp+1
        iny
        iny
        iny
        iny
        cpy #$40
        bcc .ok2
        tya
        sbc #$20  ; carry already set
        tay
        lda pTmp
        eor #$80
        bmi .ok
        iny
        cpy #$24
        bcc .ok
        ldy #$20
        adc #$27  ; carry was set, so actually adding $28
.ok     sta pTmp
.ok2    sty pTmp+1

        ; Loop until we've done all rows.
        dex
        bne .loop

        rts
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Simply retrieve the X register. Used to double-check that we're not leaking PLASMA eval
// stack entries.
asm getXReg
        +asmPlasm 0
        txa
        ldy #0
        rts
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Calculate 16-bit hash of a string
asm hashString
        +asmPlasm 1
        sta pTmp
        sty pTmp+1
        ldy #0
        sty tmp+1
        lda (pTmp),y
        tax
        iny
-       clc
        adc (pTmp),y
        bcc +
        inc tmp+1
+       asl
        rol tmp+1
        bcc +
        ora #1
+       asl
        rol tmp+1
        bcc +
        ora #1
+       asl
        rol tmp+1
        bcc +
        ora #1
+       iny
        dex
        bne -
        ldy tmp+1
        rts
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Print a string to the current character output vector
asm _puts
        +asmPlasm 1
        sta pTmp
        sty pTmp+1
        ldy #0
        lda (pTmp),y
        tax
        iny
-       lda (pTmp),y
        ora #$80
        jsr cout
        iny
        dex
        bne -
        rts
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Get a character from the keyboard
asm rdkey
        +asmPlasm 0
        jmp rdkey
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Print part of a string, until we hit the end or a '%' code. Return how far we got, or -1 for end.
asm partialPrintf
!zone {
        +asmPlasm 2
        lda evalStkL+1,x    ; get string pointer
        sta pTmp
        lda evalStkH+1,x
        sta pTmp+1
        ldy #0
        lda (pTmp),y        ; get length byte
        sec
        sbc evalStkL,x      ; minus offset
        sta tmp             ; to count of characters left to print
        bcc .eos            ; avoid overrunning
        beq .eos
        lda evalStkL,x      ; get desired offset into string
        tay
        iny                 ; increment past length byte
-       lda (pTmp),y
        ora #$80
        cmp #'%'            ; stop if we hit % code
        beq +
        jsr cout
        iny
        dec tmp             ; otherwise go until end of string
        bne -
.eos    ldy #$FF            ; if we hit end of string, return -1
        tya
        rts
+       dey                 ; adjust back for length byte
        tya                 ; that's the lo byte of return
        ldy #0              ; hi byte of return is zero
        rts
}
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Print a 16-bit hex value
asm printHex
        +asmPlasm 1
        tax
        tya
        jmp prntax
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Print a single character
asm printChar
        +asmPlasm 1
        ora #$80
        jmp cout
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Print a carriage return
asm crout
        +asmPlasm 0
        jmp crout
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Ring the bell
asm _beep
        +asmPlasm 0
        jmp bell
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Read a string from the keyboard, turn it into a PLASMA string and return a pointer to the string.
asm readStr
        +asmPlasm 0
        jsr getln1
        txa
        pha
        beq +
-       lda inbuf-1,x
        and #$7F
        sta inbuf,x
        dex
        bne -
+       pla
        sta inbuf,x
        lda #<inbuf
        ldy #>inbuf
        rts
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Send a command to the memory manager
// Params: cmd, wordParam
asm _mmgr
        +asmPlasm 2
        lda evalStkL+1,x        ; command code
        pha
        ldy evalStkH,x          ; address (or other param)... hi byte in Y
        lda evalStkL,x
        tax                     ;     ...lo byte in X
        pla
        jsr mainLoader          ; ret value in X=lo/Y=hi
        txa                     ;    to A=lo/Y=hi for asmPlasm
        rts
end

// Aux version of memory manager command
asm auxMmgr
        +asmPlasm 2
        lda evalStkL+1,x        ; command code
        pha
        ldy evalStkH,x          ; address (or other param)
        lda evalStkL,x
        tax
        pla
        jsr auxLoader           ; ret value in X=lo/Y=hi
        txa                     ;    to A=lo/Y=hi for asmPlasm
        rts
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Jump straight to the system monitor
// Params: None
asm goMon
        jmp $FF69
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Execute a monitor breakpoint
// Params: None
asm _brk
        bit setText
        bit page1
        brk
end


///////////////////////////////////////////////////////////////////////////////////////////////////
// Clear the screen and reboot the machine
// Params: None
asm _reboot
        bit setROM
        jsr home
        bit setText
        bit page1
        inc $3F4    ; invalidate reset vector
        jmp $FA62   ; and reset
end


///////////////////////////////////////////////////////////////////////////////////////////////////
// Set up the font engine
// Params: pFont
asm setFont
  +asmPlasm_bank2 1
  jmp SetFont
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Use the font engine to clear the current text window.
// Parameters: top, bottom, left, right
asm setWindow
  +asmPlasm_bank2 4
  jmp SetWindow
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Save the cursor position (1 save slot)
asm saveCursor
  +asmPlasm_bank2 0
  jmp SaveCursor
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Restore the cursor position (1 save slot)
asm restoreCursor
  +asmPlasm_bank2 0
  jmp RestCursor
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Read a keyboard string up to 40 chars using the Font Engine. Returns a zero-terminated string
// in the input buffer at $200, with the length stored in $2FF.
asm rawGetStr
  +asmPlasm_bank2 0
  jmp GetStr
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Use the font engine to clear the current text window
// Params: None
asm _clearWindow
  +asmPlasm_bank2 0
  jmp ClearWindow
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Use the font engine to copy the current text window to hi-res page 2
// Params: None
asm copyWindow
  +asmPlasm_bank2 0
  jmp CopyWindow
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Display a character using the font engine.
// Params: ch
asm displayChar
  +asmPlasm_bank2 1
  jmp DisplayChar
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Display a string using the font engine.
// Params: pStr
asm _displayStr
  +asmPlasm_bank2 1
  jmp DisplayStr
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Calculate string width using the font engine.
// Params: pStr
asm calcWidth
  +asmPlasm_bank2 1
  jmp CalcWidth
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Display a string using the font engine but not its parser.
// Params: pStr
asm _rawDisplayStr
  +asmPlasm_bank2 1
  sta pTmp
  sty pTmp+1
  ldy #0
  lda (pTmp),y
  sta tmp
- cpy tmp
  bcc +
  rts
+ iny
  lda (pTmp),y
  ora #$80
  cmp #"^"
  bne +
  iny
  lda (pTmp),y
  and #$1F
  ora #$80
+ sty tmp+1
  jsr DisplayChar
  ldy tmp+1
  bne -
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Random number generator
// Adapted from http://codebase64.org/doku.php?id=base:small_fast_16-bit_prng
asm _rand16
  +asmPlasm 0

  lda seed
  beq .lowZero ; $0000 and $8000 are special values to test for
 
  ; Do a normal shift
  asl seed
  lda seed+1
  rol
  bcc .noEor
 
.doEor:
  ; high byte is in A
  eor #>magic
  sta seed+1
  tay        ; for asmPlasm, return hi byte in Y, lo byte in A
  lda seed
  eor #<magic
  sta seed
  rts
 
.lowZero:
  lda seed+1
  beq .doEor ; High byte is also zero, so apply the EOR
             ; For speed, you could store 'magic' into 'seed' directly
             ; instead of running the EORs
 
  ; wasn't zero, check for $8000
  asl
  beq .noEor ; if $00 is left after the shift, then it was $80
  bcs .doEor ; else, do the EOR based on the carry bit as usual
 
.noEor:
  sta seed+1
  tay        ; for asmPlasm, return hi byte in Y, lo byte in A
  lda seed
  rts

end

///////////////////////////////////////////////////////////////////////////////////////////////////
// General methods

///////////////////////////////////////////////////////////////////////////////////////////////////
// Fatal error: print message and stop the system.
def _fatal(msg)
  mmgr(FATAL_ERROR, msg)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Return the max of two signed 16-bit numbers
def _max(a, b)
  if a < b
    return b
  else
    return a
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Return the min of two signed 16-bit numbers
def _min(a, b)
  if a < b
    return a
  else
    return b
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Return the absolute value of a number
def abs(n)
  if n < 0
    return -n
  else
    return n
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Read a string from the keyboard using the font manager, and intern it to the heap.
def _getStringResponse()
  word p
  rawGetStr()
  rawDisplayStr("\n") // so Outlaw user doesn't have to remember to make a newline
  for p = ($1FF + ^$2FF) downto $200
    p->1 = (^p & $7F)
  next
  ^$200 = ^$2FF
  return mmgr(HEAP_INTERN, $200)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Setter functions for library use
def _setPlural(flg)
  isPlural = flg
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Convert signed decimal to string in decimalBuf
def convertDec(n)
  word n0
  word p
  p = @decimalBuf + 1
  if n < 0;     ^p = '-';           p=p+1; n = -n;      fin
  n0 = n
  if n0 > 9999; ^p = '0' + n/10000; p=p+1; n = n%10000; fin
  if n0 > 999;  ^p = '0' + n/1000;  p=p+1; n = n%1000;  fin
  if n0 > 99;   ^p = '0' + n/100;   p=p+1; n = n%100;   fin
  if n0 > 9;    ^p = '0' + n/10;    p=p+1; n = n%10;    fin
  ^p = '0' + n; p=p+1
  decimalBuf[0] = p - @decimalBuf - 1 // record final length of string
  return @decimalBuf
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Print a formatted string a'la C printf, with up to three parameters.
def _printf3(str, arg1, arg2, arg3)
  word pos
  word curArg
  word p
  pos = 0
  curArg = @arg1
  while TRUE
    pos = partialPrintf(str, pos)
    if pos < 0
      break
    fin
    p = str + pos + 2
    when ^p
      is 'c'
        printChar(*curArg); break
      is 'd'
        puts(convertDec(*curArg)); break
      is 's'
        puts(*curArg); break
      is 'x'
        printHex(*curArg); break
      is '%'
        printChar('%'); break
      otherwise
        printHex(^p); fatal("Unknown % code")
    wend
    curArg = curArg + 2
    pos = pos + 2
  loop
end

def _printf1(str, arg1); printf3(str, arg1, 0, 0); end
def _printf2(str, arg1, arg2); printf3(str, arg1, arg2, 0); end

// Like printf, but displays text using font engine
def _displayf3(str, arg1, arg2, arg3)
  buildString(@addToString)
  printf3(str, arg1, arg2, arg3)
  displayStr(finishString(isPlural))
end

def _displayf1(str, arg1); displayf3(str, arg1, 0, 0); end
def _displayf2(str, arg1, arg2); displayf3(str, arg1, arg2, 0); end

///////////////////////////////////////////////////////////////////////////////////////////////////
def parseDec(str)
  word n
  word pend
  word p
  byte neg
  neg = FALSE
  n = 0
  p = str + 1
  pend = p + ^str
  while p < pend
    if p == (str+1) and ^p == '-'
      neg = TRUE
    elsif ^p >= '0' and ^p <= '9'
      n = (n*10) + (^p - '0')
    else
      break
    fin
    p = p+1
  loop

  if neg; return -n; fin
  return n
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def parseDecWithDefault(str, default)
  if ^str == 0
    return default
  fin
  return parseDec(str)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Convert a lower-case character to upper-case (or return unchanged if it's not lower-case)
def charToUpper(c)
  if c >= 'a' and c <= 'z'
    return c - $20
  fin
  return c
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Get a keystroke and convert it to upper case
def _getUpperKey()
  byte key
  while ^kbd < 128
    *seed = *seed + 1
    animPauseCt = animPauseCt - 1
    if animPauseCt < 0
      if curPortrait and animNumFrames > 1
        nextAnimFrame()
      fin
      animPauseCt = ANIM_PAUSE_MAX
    fin
  loop
  key = ^kbd
  ^kbdStrobe
  return charToUpper(key & $7F)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def _encodeDice(nDice, dieSize, add)  // ndice=0..15, dieSize=0..15, add=0..255
  return (nDice << 12) | (dieSize << 8) | add
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def _rollDice(encoded)
  byte i, nDice, dieSize, add, result
  nDice = encoded >> 12
  dieSize = (encoded >> 8) & $F
  add = encoded & $F
  result = add
  for i = 1 to nDice
    add = (rand16() % dieSize) + 1
    result = result + add
  next
  return result
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Set the sky color (relevant to 3D display only)
def _setSky(num)
  skyNum = num
  setColor(0, skyNum)
  needRender = TRUE
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Switch to the next sky color (3D only)
def nextSky()
  setSky((skyNum + 1) % 18)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Set the ground color (relevant to 3D display only)
def _setGround(num)
  groundNum = num
  setColor(1, groundNum)
  needRender = TRUE
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Switch to the next ground color (3D only)
def nextGround()
  setGround((groundNum + 1) % 18)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Establish a keystroke -> command association in the command table
def initCmd(key, func)
  if key >= $60
    key = key - $20
  fin
  cmdTbl[key-$20] = func
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Load the Frame Image, and lock it.
def loadFrameImg()
  byte img
  img = mapIs3D+2
  if frameLoaded <> img
    if frameLoaded
      mmgr(UNLOCK_MEMORY,$2000)
      mmgr(FREE_MEMORY,  $2000)
    fin
    mmgr(SET_MEM_TARGET, $2000)
    mmgr(QUEUE_LOAD,     img<<8 | RES_TYPE_SCREEN)
    mmgr(LOCK_MEMORY,    $2000)
    frameLoaded = img
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Window for the map name bar
def _setWindow1()
  setWindow(8, 18, 35, 119) // Top, Bottom, Left, Right
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Window for the large upper right bar
def _setWindow2()
  displayChar('N'-$40) // Set normal mode - clear all special modes (like underline, etc.)
  setWindow(24, 132, 154, 269) // Top, Bottom, Left, Right
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Window for the mid-size lower right bar
def _setWindow3()
  setWindow(144, 180, 154, 269) // Top, Bottom, Left, Right
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Window for the map area (used for clearing it)
def setMapWindow()
  if mapIs3D
    setWindow(24, 153, 14, 140) // Top, Bottom, Left, Right
  else
    setWindow(24, 169, 14, 140) // Top, Bottom, Left, Right
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def rightJustifyStr(str, rightX)
  word x
  x = rightX - calcWidth(str)
  tabBuf[0] = 4  // length
  tabBuf[1] = 20 // Ctrl-T
  tabBuf[2] = (x / 100) + '0'
  tabBuf[3] = ((x / 10) % 10) + '0'
  tabBuf[4] = (x % 10) + '0'
  rawDisplayStr(@tabBuf)
  rawDisplayStr(str)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def rightJustifyNum(num, rightX)
  rightJustifyStr(convertDec(num), rightX)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def displayPlayerData(player)
  word pi

  // Display the player's name and health
  rawDisplayStr(player=>s_name)
  rightJustifyNum(player=>w_health, CHAR_WND_LIFE_X)
  
  // Locate the first weapon, and display it's clip
  pi = player=>p_items
  while pi and pi->t_type <> TYPE_WEAPON; pi = pi=>p_nextObj; loop
  if pi
    rightJustifyNum(pi->b_clipCurrent, CHAR_WND_GUN_X)
  else
    rightJustifyStr("---", CHAR_WND_GUN_X)
  fin

  // All done.
  rawDisplayStr("\n")
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Display the party data on the screen
def _showParty()
  word p

  saveCursor()
  setWindow3()
  clearWindow()

  // Display header
  rawDisplayStr("^LName")  // begin underline mode
  rightJustifyStr("Life", CHAR_WND_LIFE_X)
  rightJustifyStr("Gun", CHAR_WND_GUN_X)
  rawDisplayStr("^L\n")   // end underline mode

  // Display each character
  p = global=>p_players
  while p
    displayPlayerData(p)
    p = p=>p_nextObj
  loop

  // Finish up
  if mapIs3D; copyWindow(); fin
  setWindow2()
  restoreCursor()
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Load code and data, set up everything to display a 2D or 3D map
def initMap(x, y, dir)
  word pMap

  // Set up the command table
  initCmds()

  // Reset memory (our module will stay since memory manager locked it upon load)
  mmgr(RESET_MEMORY, 0)

  // Queue loading of the raycaster or tile engine and the map data
  mmgr(SET_MEM_TARGET, displayEngine)
  if mapIs3D
    mmgr(QUEUE_LOAD, CODE_RENDER<<8 | RES_TYPE_CODE)
    pMap = mmgr(QUEUE_LOAD, mapNum<<8 | RES_TYPE_3D_MAP)
    auxMmgr(SET_MEM_TARGET, expandVec)
    auxMmgr(QUEUE_LOAD, CODE_EXPAND<<8 | RES_TYPE_CODE)
  else
    mmgr(QUEUE_LOAD, CODE_TILE_ENGINE<<8 | RES_TYPE_CODE)
    pMap = mmgr(QUEUE_LOAD, mapNum<<8 | RES_TYPE_2D_MAP)
  fin
  renderLoaded = TRUE

  // Load everything that we just queued
  mmgr(FINISH_LOAD, 1) // 1 = keep open

  // Load the frame image (and lock it there)
  loadFrameImg()
  mmgr(FINISH_LOAD, 1) // 1 = keep open

  // Clear the list of encounter zones from any previous maps
  global=>p_encounterZones = NULL

  // Start up the display engine with map data and starting position. This will also load and
  // init the script module, if any, which will end up calling us back at the setScriptInfo
  triggerTbl = NULL
  setWindow2()
  initDisplay(mapNum, pMap, x, y, dir)
  needRender = FALSE
  curPortrait = 0

  // Display the party characters
  showParty()
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Check for script(s) attached to the given location, and call them if there are any.
// Returns TRUE if any were triggered.
def checkScripts(x, y)
  word p
  word pNext
  word script
  byte anyTriggered

  anyTriggered = FALSE
  x = x - triggerOriginX
  y = y - triggerOriginY
  p = triggerTbl
  while p
    if ^p == $FF
      break
    fin
    pNext = p + p->1
    if ^p == y
      p = p + 2
      while p < pNext
        if x == ^p
          script = p=>1
          setWindow2()
          script()
          anyTriggered = TRUE
        fin
        p = p + 3
      loop
    fin
    p = pNext
  loop
  return anyTriggered
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Display a portrait drawing (typically called from scripts)
def _clearPortrait()
  if curPortrait
    auxMmgr(FREE_MEMORY, curPortrait)
    curPortrait = 0
    if renderLoaded
      texControl(1) // 1=load
    fin
    needRender = TRUE
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Perform rendering, copy if necessary, clear appropriate flags
def doRender()
  if curPortrait; clearPortrait(); fin
  render()
  if textDrawn and mapIs3D; copyWindow(); fin
  needRender = FALSE
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Advance one step forward (3D maps only)
def moveForward()
  byte val
  word x, y
  val = advance()

  // If not blocked, render at the new position.
  if val > 0
    if !mapIs3D
      doRender()
    else
      needRender = TRUE
    fin
  fin

  // If we're on a new map tile, clear text from script(s) on the old tile.
  if val >= 2 and textDrawn
    clearWindow()
    if textDrawn and mapIs3D; copyWindow(); fin
    textDrawn = FALSE
  fin

  // If there are script(s) on the new tile, run them.
  if val == 3
    getPos(@x, @y)
    if !checkScripts(x, y)
      if global=>p_encounterZones
        checkEncounter(x, y)
      fin
    fin
  elsif global=>p_encounterZones
    getPos(@x, @y)
    checkEncounter(x, y)
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Adjust player's direction plus or minus n increments
def adjustDir(n)
  setDir((getDir() + n) & 15)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Move backward one step (3D mode)
def _moveBackward()
  adjustDir(8)
  moveForward()
  adjustDir(8)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Turn left (3D mode)
def rotateLeft()
  adjustDir(-1)
  needRender = TRUE
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Rotate to the right (3D mode)
def rotateRight()
  adjustDir(1)
  needRender = TRUE
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Sidestep to the right (3D mode)
def strafeRight()
  adjustDir(4)
  moveForward()
  adjustDir(-4)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Sidestep to the left (3D mode)
def strafeLeft()
  adjustDir(-4)
  moveForward()
  adjustDir(4)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def moveNorth()
  word x, y
  getPos(@x, @y)
  if y > 4
    setDir(0)
    moveForward()
  else
    beep()
  fin
end

def moveEast()
  word x, y
  getPos(@x, @y)
  if x < totalMapWidth-5
    setDir(4)
    moveForward()
  else
    beep()
  fin
end

def moveSouth()
  word x, y
  getPos(@x, @y)
  if y < totalMapHeight-5
    setDir(8)
    moveForward()
  else
    beep()
  fin
end

def moveWest()
  word x, y
  getPos(@x, @y)
  if x > 4
    setDir(12)
    moveForward()
  else
    beep()
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Switch to a new map (2D or 3D) and establish position on it
def setMap(is3D, num, x, y, dir)
  if is3D == mapIs3D and num == mapNum
    setPos(x, y)
    setDir(dir)
    needRender = TRUE
  else
    flipToPage1()
    setWindow1();   clearWindow()
    displayChar('Y'-$40) // center mode
    displayStr("Traveling...")
    mapNameHash = 0 // because we replaced the title text
    displayChar('N'-$40) // normal mode
    setMapWindow(); clearWindow()    
    setWindow2();   clearWindow()
    mapIs3D = is3D
    mapNum = num
    initMap(x, y, dir)
  fin
  // Don't check scripts, because we often land on an "Exit to wilderness?" script
  //NO:checkScripts()  
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def kbdTeleport()
  byte d3, num
  word x, y
  byte dir
  
  flipToPage1()
  ^$c053
  if ^$25 < 23; ^$25 = 23; fin
  getPos(@x, @y)
  printf3("\nCurrent: X=%d Y=%d Facing=%d\n", x, y, getDir())

  printf1("3D [%d]: ", mapIs3D)
  d3 = parseDecWithDefault(readStr(), mapIs3D)
  if d3 > 1; d3 = 1; fin
  printf1("Map [%d]: ", mapNum)
  num = parseDecWithDefault(readStr(), mapNum)
  printf1("X [%d]  : ", x)
  x = parseDecWithDefault(readStr(), x)
  printf1("Y [%d] : ", y)
  y = parseDecWithDefault(readStr(), y)
  printf1("Facing [%d]: ", dir)
  dir = parseDecWithDefault(readStr(), dir)

  ^$c052

  setMap(d3, num, x, y, dir)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def showPos()
  word x, y
  byte dir
  
  flipToPage1()
  ^$c053
  if ^$25 < 23; ^$25 = 23; fin
  getPos(@x, @y)
  printf2("\nX=%d Y=%d ", x, y)
  if mapIs3D
    printf3("Facing=%d Sky=%d Ground=%d", getDir(), skyNum, groundNum)
  fin
  puts("\nHit any key.\n")
  getUpperKey()
  ^$c052
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def _queue_setMap(is3D, num, x, y, dir)
  q_mapIs3D = is3D
  q_mapNum = num
  q_x = x
  q_y = y
  q_dir = dir
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def _queue_teleport(x, y, dir)
  queue_setMap(mapIs3D, mapNum, x, y, dir)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Get a key and dispatch it to a command. Then do it again, forever.
def kbdLoop()
  word key, func
  word errMsg
  byte xreg, tmp
  errMsg = "Error: x reg changed from %x to %x\n"
  xreg = getXReg()
  while TRUE
    tmp = getXReg()
    if tmp <> xreg
      printf2(errMsg, xreg, tmp)
      brk()
    fin
    key = getUpperKey()
    key = key - $20
    if key >= 0 and key < $40
      func = cmdTbl[key]
      if func; func(); fin
    fin
    if q_mapNum
      setMap(q_mapIs3D, q_mapNum, q_x, q_y, q_dir)
      q_mapNum = 0
    fin
    if needRender
      doRender()
    fin
  loop
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def showMapName(mapName)
  word newNameHash

  newNameHash = hashString(mapName)
  if newNameHash <> mapNameHash
    setWindow1()
    clearWindow()
    displayChar('Y'-$40) // center mode
    displayStr(mapName)
    displayChar('N'-$40) // normal mode
    if mapIs3D; copyWindow(); fin
    mapNameHash = newNameHash
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Set initial info for the scripts on this map: the name of the map, its trigger table, and the
// maximum extent (width, height). This is called by the init function for the scripts.
def _setScriptInfo(mapName, trigTbl, wdt, hgt)

  // Grab the trigger table origins (used so the table can be more compact)
  triggerOriginX = trigTbl=>0
  triggerOriginY = trigTbl=>2

  // Record the trigger table pointer
  triggerTbl = trigTbl + 4

  // Record the maximum width and height
  totalMapWidth = wdt
  totalMapHeight = hgt

  // Display map name
  showMapName(mapName)

  // Back to the main text window.
  setWindow2()
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Called by scripts to display a string. We set the flag noting that something has been
// displayed, then use an assembly routine to do the work.
def _scriptDisplayStr(str)
  textDrawn = TRUE
  flipToPage1()
  displayStr(str)
end

def _scriptDisplayStrNL(str)
  scriptDisplayStr(str)
  displayStr("\n")
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Get a key, and don't return until it's Y or N (or lower-case of those). Returns 1 for Y.
def _getYN()
  byte key
  while TRUE
    key = getUpperKey()
    if key == 'Y'
      return 1
    elsif key == 'N'
      clearWindow()
      if textDrawn and mapIs3D; clearWindow(); fin
      textDrawn = FALSE
      return 0
    fin
    beep()
  loop
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Show the current animation frame
def showAnimFrame()
  word pData

  // Determine data pointer based on current animation frame
  if !curPortrait; return; fin
  pData = curPortrait + 1 + (animFrame * 2304)   // 18*128 = 2304

  // Show it on-screen
  if mapIs3D
    blitPortrait(pData, $2182)  // start at 3rd text line
  else
    blitPortrait(pData, $2202)  // start at 4th text line
  fin

  needRender = FALSE  // suppress display of map for this frame
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Advance to next frame of current animation, if any
def nextAnimFrame()
  word oldAnimFrame
  if !curPortrait; return; fin
  oldAnimFrame = animFrame

  // Choose a new direction based on the flags. Do this the first time, and once every 3-7 frames.
  animDirCt = animDirCt - 1
  if !animDir or animDirCt == 0
    if animFlags & ANIM_FLAG_FWD
      animDir = 'F'
    elsif animFlags & ANIM_FLAG_FWD_BKWD
      if animDir == 'B'
        animDir = 'F'
      else
        animDir = 'B'
      fin
    elsif (animFlags & ANIM_FLAG_RANDOM)
      animDir = 'R'
    else
      animDir = 0
    fin
    animDirCt = (rand16() % 5) + 3
  fin

  // Advance in the current direction
  if animDir == 'F' // forward
    animFrame = animFrame + 1
    if animFrame >= animNumFrames
      animFrame = 0
    fin
  elsif animDir == 'B' // backward
    if animFrame == 0
      animFrame = animNumFrames - 1
    else
      animFrame = animFrame - 1
    fin
  elsif animDir == 'R' // random
    animFrame = rand16() % animNumFrames
  fin

  // And show it.
  if animFrame <> oldAnimFrame
    showAnimFrame()
  fin

  // Reset the animation pause
  animPauseCt = ANIM_PAUSE_MAX

end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Display a portrait drawing (typically called from scripts)
def _setPortrait(portraitNum)
  word srcData
  byte cx, cy

  clearPortrait()
  flipToPage1()
  
  // We're going to switch windows. Save the cursor pos in the text window.
  saveCursor()

  // Make room by unloading the textures (only if renderer is loaded)
  if renderLoaded
    texControl(0)
  fin

  // Now clear out the map area
  setMapWindow()
  clearWindow()

  // Restore the cursor position
  setWindow2()
  restoreCursor()

  // Load the portrait image and display it
  curPortrait = auxMmgr(QUEUE_LOAD, portraitNum<<8 | RES_TYPE_PORTRAIT)
  mmgr(FINISH_LOAD, 0) // 0 = close
  animFrame = 0
  animFlags = readAuxByte(curPortrait)
  animNumFrames = animFlags & $F
  animDir = 0
  animDirCt = 1
  animPauseCt = ANIM_PAUSE_MAX

  // And show the first frame
  showAnimFrame()

  // Do not render over the portrait
  needRender = FALSE
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Test out portrait drawing
def nextPortrait()
  portraitNum = portraitNum + 1
  printf1("Portrait #%d\n", portraitNum)
  setPortrait(portraitNum)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Test out portrait drawing
def prevPortrait()
  if portraitNum > 1
    portraitNum = portraitNum - 1
  fin
  setPortrait(portraitNum)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def _countList(p)
  byte n
  n = 0
  while p
    n = n+1
    p = p=>p_nextObj
  loop
  return n
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def _countListFiltered(p, offset, filterFunc)
  byte n
  n = 0
  while p
    if filterFunc(p)
      n = n+1
    fin
    p = *(p + offset)
  loop
  return n
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def _randomFromListFiltered(p, offset, filterFunc)
  byte n
  n = rand16() % countListFiltered(p, offset, filterFunc)
  while p
    if filterFunc(p)
      if n == 0; return p; fin
      n = n+1
    fin
    p = *(p + offset)
  loop
  return NULL
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Call like this: addToList(@player=>p_items, itemToAdd)
def _addToList(addTo, p)
  // Get to the end of the list
  while *addTo
    addTo = (*addTo) + p_nextObj
  loop
  p=>p_nextObj = *addTo
  *addTo = p
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Call like this: removeFromList(player + items, itemToRemove)
def _removeFromList(pList, toRemove)
  word p

  p = *pList
  while p and p <> toRemove
    pList = p + p_nextObj
    p = *pList
  loop

  if p
    *pList = p=>p_nextObj
    p=>p_nextObj = NULL
  else
    fatal("InvalUnlink")
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Show player data
def showPlayerSheet(num)
  word pl
  word x, y
  byte dir

  pl = global=>p_players
  while num > 0
    pl = pl=>p_nextObj
    num = num - 1
  loop

  getPos(@x, @y)
  dir = getDir()

  // First, display the player's name in the title bar
  showMapName(pl=>s_name)

  // Next, show stats in the main map area
  setMapWindow()
  clearWindow()
  displayStr("Stats")

  // Show inventory in the right hand area
  setWindow2()
  clearWindow()
  displayStr("Inventory")

  // Wait, then back to normal
  getUpperKey()

  setWindow2()
  clearWindow()
  initMap(x, y, dir)
end

def showPlayer1()
  showPlayerSheet(0)
end
def showPlayer2()
  showPlayerSheet(1)
end
def showPlayer3()
  showPlayerSheet(2)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def saveMapPos()
  global->b_mapIs3D = mapIs3D
  global->b_mapNum = mapNum
  getPos(@global=>w_mapX, @global=>w_mapY)
  global->b_mapDir = getDir()
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def restoreMapPos()
  mapIs3D = global->b_mapIs3D
  mapNum = global->b_mapNum
  initMap(global=>w_mapX, global=>w_mapY, global->b_mapDir)  
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def _makeEnemy(name, healthDice, image0, image1, attackType, attackText, attackRange, chanceToHit, dmg, groupSize)
  word p; p = mmgr(HEAP_ALLOC, TYPE_ENEMY)
  p=>s_name = mmgr(HEAP_INTERN, name)
  p=>w_health = rollDice(healthDice) // 4d6
  if !image1 or (rand16() % 2)
    p->b_image = image0
  else
    p->b_image = image1
  fin
  p->b_attackType = attackType
  p=>s_attackText = mmgr(HEAP_INTERN, attackText)
  p->b_enemyAttackRange = attackRange
  p->b_chanceToHit = chanceToHit
  p=>r_enemyDmg = dmg
  p=>r_groupSize = groupSize
  return p
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def _addEncounterZone(code, x, y, dist, chance)
  word p; p = mmgr(HEAP_ALLOC, TYPE_ENCOUNTER_ZONE)
  p=>s_name = mmgr(HEAP_INTERN, code)
  p=>w_encX = x
  p=>w_encY = y
  p=>w_encMaxDist = dist
  p=>w_encChance = chance
  addToList(@global=>p_encounterZones, p)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def _strcmpi(a, b)
  word limit, lenDiff, diff
  lenDiff = ^a - ^b
  limit = a + min(^a, ^b)
  while a < limit
    a++
    b++
    diff = charToUpper(^a) - charToUpper(^b)
    if diff <> 0
      return diff
    fin
  loop
  return lenDiff
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def doCombat(mapCode)
  word combatEngine
  word x, y
  byte dir

  saveMapPos()

  // Load the combat engine
  flipToPage1()
  mmgr(RESET_MEMORY, 0)
  renderLoaded = FALSE
  combatEngine = mmgr(QUEUE_LOAD, MODULE_COMBAT<<8 | RES_TYPE_MODULE)
  mmgr(FINISH_LOAD, 1) // 1 = keep open

  // Run the combat engine
  combatEngine()=>combat_zoneEncounter(mapCode)

  // Clear enemies out of the heap
  global=>p_enemyGroups = NULL
  mmgr(HEAP_COLLECT, 0)

  // Resume where we left off
  textDrawn = TRUE
  clearPortrait()
  setWindow2()
  restoreMapPos()
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Check for a random encounter at this position
def checkEncounter(x, y)
  word p
  word p_bestZone, bestDist
  word d

  // Find the zone that's closest, but not too far.
  bestDist = INT_MAX
  p_bestZone = NULL
  p = global=>p_encounterZones
  while p
    d = min(abs(x - p=>w_encX), abs(y - p=>w_encY))
    if d < bestDist and (p=>w_encMaxDist == 0 or d < p=>w_encMaxDist))
      p_bestZone = p
      bestDist = d
    fin
    p = p=>p_nextObj
  loop

  // Roll for an encounter in the zone.
  d = rand16() % 1000
  if p_bestZone and d < p_bestZone=>w_encChance
    // Encounter!
    doCombat(p_bestZone=>s_name)
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def testCombat
  if global=>p_encounterZones
    doCombat(global=>p_encounterZones=>s_name)
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Set up the command table for 3D mode
def initCmds()

  // Clear the command table
  byte i
  for i = 0 to 63
    cmdTbl[i] = 0
  next

  // Commands common to both 2D and 3D
  initCmd('T', @kbdTeleport)
  initCmd('P', @showPos)
  initCmd('/', @nextPortrait)
  initCmd('?', @prevPortrait)
  initCmd('!', @testCombat)
  initCmd('1', @showPlayer1)
  initCmd('2', @showPlayer2)
  initCmd('3', @showPlayer3)

  // Commands handled differently in 3D vs 2D
  if mapIs3D
    initCmd('W', @moveForward)
    initCmd('A', @rotateLeft)
    initCmd('D', @rotateRight)
    initCmd('S', @moveBackward)
    initCmd('X', @moveBackward)
    initCmd('Z', @strafeLeft)
    initCmd('C', @strafeRight)

    initCmd('I', @moveForward)
    initCmd('J', @rotateLeft)
    initCmd('L', @rotateRight)
    initCmd('K', @moveBackward)
    initCmd(',', @moveBackward)
    initCmd('M', @strafeLeft)
    initCmd('.', @strafeRight)

    initCmd('Y', @nextSky)
    initCmd('G', @nextGround)
  else
    initCmd('W', @moveNorth)
    initCmd('D', @moveEast)
    initCmd('S', @moveSouth)
    initCmd('X', @moveSouth)
    initCmd('A', @moveWest)

    initCmd('I', @moveNorth)
    initCmd('J', @moveWest)
    initCmd('L', @moveEast)
    initCmd('K', @moveSouth)
    initCmd(',', @moveSouth)
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Load and display the title screen.
def loadTitle()
  word pFont

  puts("Loading Lawless Legends.\n")

  // Load the title screen
  mmgr(UNLOCK_MEMORY,  $2000)
  mmgr(FREE_MEMORY,    $2000)
  mmgr(SET_MEM_TARGET, $2000)
  mmgr(QUEUE_LOAD,     1<<8 | RES_TYPE_SCREEN)  // title screen is fixed at #1
  mmgr(LOCK_MEMORY,    $2000)
  mmgr(FINISH_LOAD, 1) // 1 = keep open
  frameLoaded = 1
  ^$C050     // graphics
  ^$C057     // hi-res
  ^$C054     // page 1
  ^$C052     // full screen
  // Hack for real (not emulated) IIc: sometimes displays only lo-bit graphics
  // unless we do this. *HUGE* thanks to Brendan Robert for the fix!
  ^$C07E=0   // disable double-hi-res
  ^$C05F     // disable double-hi-res

  // Load the font engine and its font, and lock them in place forever.
  mmgr(SET_MEM_TARGET, fontEngine)
  mmgr(QUEUE_LOAD,     CODE_FONT_ENGINE<<8 | RES_TYPE_CODE)
  mmgr(LOCK_MEMORY,    fontEngine)

  mmgr(SET_MEM_TARGET, $9000)
  pFont = mmgr(QUEUE_LOAD, 1<<8 | RES_TYPE_FONT)
  mmgr(LOCK_MEMORY, pFont)
  mmgr(FINISH_LOAD, 1) // 1 = keep open

  // Tell the font engine where to find its font
  setFont(pFont)

  // Wait for a key.
  getUpperKey()
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Set up the small-object heap
def initHeap()
  byte i

  mmgr(SET_MEM_TARGET, heapStart)
  mmgr(REQUEST_MEMORY, heapSize)
  mmgr(LOCK_MEMORY,    heapStart)
  mmgr(HEAP_SET,       heapStart)
  i = 0
  while typeTbls[i]
    mmgr(HEAP_ADD_TYPE, typeTbls[i])
    i = i+1
  loop
  global = mmgr(HEAP_ALLOC, TYPE_GLOBAL)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def _getGlobals()
  return global
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Set up the library vectors so we can share the library routines
def setLibVecs()
  word sp, dp

  // We're going to put the vectors at a known location, and lock that space
  _mmgr(SET_MEM_TARGET, gameLibVecs)
  _mmgr(REQUEST_MEMORY, $100)
  _mmgr(LOCK_MEMORY,    gameLibVecs)

  // Now set up the jump vectors
  sp = @gameLib_addrs
  dp = gameLibVecs
  while *sp
    dp->0 = $4c
    dp=>1 = *sp
    sp = sp + 2
    dp = dp + 3
  loop

end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Create the party
def initParty()
  word globalScripts

  // Old-style
  globalScripts = mmgr(QUEUE_LOAD, MODULE_GLOBAL_SCRIPTS<<8 | RES_TYPE_MODULE)
  mmgr(FINISH_LOAD, 1) // 1 = keep open
  addToList(@global=>p_players, globalScripts()=>script_new_Player_Hue_Hauser())
  addToList(@global=>p_players, globalScripts()=>script_new_Player_Mokahnu())
  mmgr(FREE_MEMORY, globalScripts)

  // New-style
  globalScripts = mmgr(QUEUE_LOAD, MODULE_GEN_GLOBAL_SCRIPTS<<8 | RES_TYPE_MODULE)
  mmgr(FINISH_LOAD, 1) // 1 = keep open
  globalScripts()=>sc_newGame()
  mmgr(FREE_MEMORY, globalScripts)
  mapIs3D = q_mapIs3D
  mapNum = q_mapNum
  q_mapNum = 0
  initMap(q_x, q_y, q_dir)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Main code.
//
setLibVecs()
initHeap()
loadTitle()
initParty()
kbdLoop()

done