///////////////////////////////////////////////////////////////////////////////////////////////////
// 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"
include "globalDefs.plh"
include "playtype.plh"
include "gen_images.plh"
include "gen_items.plh"
include "gen_enemies.plh"
include "gen_modules.plh"
include "combat.plh"
include "itemutil.plh"

struc EnemyData
  word s_en_name
  word r_en_hitDice
  byte b_en_img0
  byte b_en_img1
  byte b_en_attType
  word s_en_attText
  word r_en_attRange
  byte b_en_chanceToHit
  word r_en_dmgDice
  word r_en_expDice
  word r_en_groupSize
  byte b_en_lootChance0
  byte b_en_lootChance1
  byte b_en_lootCode0
  byte b_en_lootCode1
  word r_en_goldLoot
  byte b_en_gangChance
end

const COMBAT_CHOICE_Y = 54

predef _combat_zoneEncounter(s_encZone)#1
word[] funcTbl = @_combat_zoneEncounter

// Combat variables that don't need to be saved in game state
byte nPartyFighting
byte nPlayersFighting
byte nEnemiesFighting
byte isFleeing
byte isAdvancing
//combatDebug// byte combatDebug

byte[] S_AN = "an "
byte[] S_A = "a "
byte[] S_EMPTY = ""
byte[] S_COMMA = ", "
byte[] S_AND = " and "

// To save time, we preload global funcs
const NUM_PRELOADS = 4
byte[] toPreload = MOD_ITEMUTIL, GS_COMBAT_PROMPT, GS_ENEMY_INTRO, GS_COMBAT_WIN
word[4] preloads
word pItemUtil

const MAX_GROUPS = 3
byte nEnemyGroups

///////////////////////////////////////////////////////////////////////////////////////////////////
def preload()#0
  byte i
  for i = 0 to NUM_PRELOADS-1
    preloads[i] = mmgr(QUEUE_LOAD, toPreload[i]<<8 | RES_TYPE_MODULE)
  next
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def freePreloaded()#0
  byte i
  for i = 0 to NUM_PRELOADS-1
    mmgr(FREE_MEMORY, preloads[i])
  next
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def rollDiceWithLuck(encoded, luck)#1
  byte nDice, dieSize, droll
  word result
  nDice = (encoded >> 12) & $F  // must mask off replicated hi-bits
  dieSize = (encoded >> 8) & $F
  result = encoded & $F
  while nDice
    droll = (rand16() % dieSize) + 1
    if luck > 0
      if (rand16() % 100) < (luck * 2)
        droll = max(droll, (rand16() % dieSize) + 1)
      fin
    elsif luck < 0
      if (rand16() % 100) < (luck * -2)
        droll = min(droll, (rand16() % dieSize) + 1)
      fin
    fin
    result = result + droll
    nDice--
  loop
  return result
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def rollPercentileWithLuck(luck)#1
  byte result
  result = rand16() % 100
  if luck > 0
    if (rand16() % 100) < (luck * 2)
      result = max(result, rand16() % 100)
    fin
  elsif luck < 0
    if (rand16() % 100) < (luck * -2)
      result = min(result, rand16() % 100)
    fin
  fin
  return result
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def combatPause()#0
  byte key

  pause(global=>w_combatPauseCt)

  // Check for speed-up / slow-down
  key = ^kbd & $7F
  if key == 11 or key == '-' // up-arrow or minus
    ^kbdStrobe
    global=>w_combatPauseCt = max(100, global=>w_combatPauseCt - addPercent(global=>w_combatPauseCt, 20))
  elsif key == 10 or key == '+' // down-arrow or plus
    ^kbdStrobe
    global=>w_combatPauseCt = min(9999, global=>w_combatPauseCt + addPercent(global=>w_combatPauseCt, 20))
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def canFight(p)#1
  return p=>w_health > 0 ?? 1 :: 0
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Phase 0: count alive matching enemies
// Phase 1: select matching enemy
def enemyLoop(maxDist, gangChoice, phase, choiceNum)
  byte n
  word pGroup, pEnemy

  n = 0
  pGroup = global=>p_enemyGroups
  while pGroup
    if !gangChoice or pGroup->b_enemyNum == gangChoice
      pEnemy = pGroup=>p_enemies
      while pEnemy
        if pEnemy->t_type == TYPE_ENEMY and canFight(pEnemy) and pEnemy->b_enemyAttackRange <= maxDist
          if phase == 1 and n == choiceNum; return pEnemy; fin
          n++
        fin
        pEnemy = pEnemy=>p_nextObj
      loop
    fin
    pGroup = pGroup=>p_nextObj
  loop
  return n
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def chooseEnemy(maxDist, gangChoice)#1
  byte n

  // See if there are any living enemies in the chosen gang that are in striking distance
  n = enemyLoop(maxDist, gangChoice, 0, 0)
  if n == 0
    // Chosen gang may be all dead. Expand the selection to all gangs.
    gangChoice = 0
    n = enemyLoop(maxDist, gangChoice, 0, 0)
  fin

  // If nobody in range, ack!
  if !n; return NULL; fin

  // Pick one of the in-range enemies to attack.
  return enemyLoop(maxDist, gangChoice, 1, rand16() % n)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def scanModifiers(pMod, s_match)
  ctx = s_match; return sum(pMod, &(p) streqi(p=>s_name, ctx), &(p) p=>w_modValue)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def rollPlayerHit(pPlayer, pWeapon, pEnemy, sAction)
  word chance
  byte agil, roll, bonus

  // Chance to hit:
  // * agil *4
  // * plus aiming attrib
  // * add in any aiming bonus from the weapon
  // * plus skill modifier for the kind of weapon
  // * clamped to range 5% - 90%
  // * then reduced 1% per 5 feet range beyond 5'
  // * clamped to range 0% - 90%
  //combatDebug// if combatDebug; displayStr("Player hit calcs:\n"); fin
  agil = pPlayer->b_agility
  if pWeapon
    agil = agil + scanModifiers(pWeapon=>p_modifiers, @S_AGILITY)
  fin
  //combatDebug// if combatDebug; displayf3("Agil = %d+%d = %d\n", pPlayer->b_agility, agil - pPlayer->b_agility, agil); fin
  chance = (agil * 9) + (pPlayer->b_aiming * 5)
  //combatDebug// if combatDebug; displayf3("Base chnc = %d*9 + %d*5 = %d%%\n", agil, pPlayer->b_aiming, chance); fin
  if pWeapon
    chance = chance + (scanModifiers(pWeapon=>p_modifiers, @S_AIMING)*5)
    //combatDebug// if combatDebug; displayf1(" weap aim=%d*5\n", scanModifiers(pWeapon=>p_modifiers, @S_AIMING)); fin
    chance = chance + (scanModifiers(pPlayer=>p_skills, pWeapon=>s_itemKind)*5)
    //combatDebug// if combatDebug; displayf1(" plyr skill=%d*5\n", scanModifiers(pPlayer=>p_skills, pWeapon=>s_itemKind)); fin
  else
    chance = chance + scanModifiers(pPlayer=>p_skills, @S_HAND_TO_HAND)*5
    //combatDebug// if combatDebug; displayf1(" h-t-h skill=%d*5\n", scanModifiers(pPlayer=>p_skills, @S_HAND_TO_HAND)); fin
  fin
  chance = chance - (max(0, pEnemy->b_enemyAttackRange - 5)) / 5)
  //combatDebug// if combatDebug; displayf1(" rng mod=%d\n", -max(0, pEnemy->b_enemyAttackRange - 5) / 5); fin
  chance = max(5, min(90, chance))
  //combatDebug// if combatDebug; displayf1("Final chnc = %d%%\n", chance); fin

  // See if it's a hit
  roll = rollPercentileWithLuck(-(pPlayer->b_luck)) // luck can reduce roll = increase chance to hit
  //combatDebug// if combatDebug; displayf2("Roll=%d, need <%d\n", roll, chance); getUpperKey(); fin
  if roll >= chance
    isPlural = FALSE
    displayf3("\n%s %s at %s but misses.\n", pPlayer=>s_name, sAction, pEnemy=>s_name)
    return FALSE
  fin
  return TRUE
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def rollEnemyDodge(pPlayer, pEnemy, sAction)
  // Enemy chance to dodge is taken from their chance to hit divided by 2
  if (rand16() % 100) < (pEnemy->b_chanceToHit / 2)
    isPlural = FALSE
    displayf3("\n%s %s at %s, ", pPlayer=>s_name, sAction, pEnemy=>s_name)
    displayf1("but %s dodges.\n", pEnemy=>s_name)
    return TRUE
  fin
  return FALSE
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def killEnemy(pEnemy)#0
  pEnemy=>w_health = 0
  printf1(" %s is killed!", pEnemy=>s_name)
  nEnemiesFighting = nEnemiesFighting - 1
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def damageEnemy(pPlayer, pEnemy, dmg, sAction)#0
  //combatDebug// if combatDebug
  //combatDebug//   displayf3("\nenemy health: %d-%d=%d\n", pEnemy=>w_health, dmg, pEnemy=>w_health-dmg)
  //combatDebug//   getUpperKey
  //combatDebug// fin
  isPlural = FALSE
  buildString()
  printf3("\n%s %s %s ", pPlayer=>s_name, sAction, pEnemy=>s_name)
  printf1("for %d damage.", dmg)
  if pEnemy=>w_health > dmg
    pEnemy=>w_health = pEnemy=>w_health - dmg
  else
    killEnemy(pEnemy)
  fin
  puts("\n")
  displayStr(finishString(0))
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def playerMelee(pPlayer, pWeapon)#0
  word pEnemy, dmg, sAction, bonus
  byte agil

  // Maybe in the future we'll put this under control of a global script
  if pWeapon
    // If weapon takes ammo, it's not a melee weapon, so use generic "bashes"
    sAction = pWeapon=>s_ammoKind ?? "bashes" :: pWeapon=>s_combatText
  else
    sAction = (rand16() % 2) ?? "punches" :: "kicks"
  fin

  pEnemy = chooseEnemy(5, pPlayer->b_gangChoice) // max distance 5 feet for melee
  if !pEnemy
    if nEnemiesFighting
      displayf2("\n%s foolishly %s, misses by a mile.\n", pPlayer=>s_name, sAction)
    fin
    return
  fin

  if !rollPlayerHit(pPlayer, pWeapon, pEnemy, sAction)
    return
  fin
  if rollEnemyDodge(pPlayer, pEnemy, sAction)
    return
  fin

  if pWeapon
    dmg = pWeapon=>r_meleeDmg
  else
    // If using just fists, damage is nd4, where n is hand-to-hand skill + 1
    dmg = encodeDice(scanModifiers(pPlayer=>p_skills, @S_HAND_TO_HAND)+1, 4, 0)
  fin
  //combatDebug// if combatDebug; displayf3("Base dmg: %dd%d+%d = ", (dmg>>12) & $F, (dmg >> 8) & $F, dmg & $FF); fin
  dmg = rollDiceWithLuck(dmg, pPlayer->b_luck)
  //combatDebug// if combatDebug; displayf1("%d\n", dmg); fin

  // Damage bonus is:
  // 1% per point of combined player and weapon agility
  // 2% per skill level in the weapon type
  // 5% per point of strength, but only if at least 1 pt in hand-to-hand skill
  // (enemies don't have armor)
  bonus = pPlayer->b_agility
  //combatDebug// if combatDebug; displayf1("Dmg bonus%%:\n agil=%d\n", pPlayer->b_agility); fin
  if pWeapon
    bonus = bonus + scanModifiers(pWeapon=>p_modifiers, @S_AGILITY)
    //combatDebug// if combatDebug; displayf1(" weap agil=%d\n", scanModifiers(pWeapon=>p_modifiers, @S_AGILITY)); fin
    bonus = bonus + (scanModifiers(pPlayer=>p_skills, pWeapon=>s_itemKind)*2)
    //combatDebug// if combatDebug; displayf1(" plyr skill*2=%d\n", scanModifiers(pPlayer=>p_skills, pWeapon=>s_itemKind)*2); fin
  fin
  if scanModifiers(pPlayer=>p_skills, @S_HAND_TO_HAND)
    bonus = bonus + (pPlayer->b_strength * 5)
    //combatDebug// if combatDebug; displayf1(" strngth*5=%d\n", pPlayer->b_strength * 5); fin
  fin
  //combatDebug// if combatDebug; displayf1("Final bonus=%d%%\n", bonus); fin
  dmg = dmg + addPercent(dmg, bonus)
  //combatDebug// if combatDebug; displayf1("Final dmg=%d\n", dmg); getUpperKey(); fin
  damageEnemy(pPlayer, pEnemy, dmg, sAction)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def genShotSound(kindOrText)#0
  if streqi(kindOrText, "handguns") or streqi(kindOrText, "rifles") or streqi(kindOrText, "shoots")
    genSound(63, 100, 0, 15, 0, 0, 300)
  elsif streqi(kindOrText, "bows")
    genSound(15, 500, 0, 15, 0, 0, 100)
  elsif streqi(kindOrText, "explosives")
    genSound(255, 300, 0, 127, 0, 0, 400)
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def playerShoot(pPlayer, pWeapon)#0
  word pEnemy, pSkill, dmg
  word chance
  word sAction
  byte agil, roll

  sAction = pWeapon=>s_combatText

  // See if there's anybody in range
  pEnemy = chooseEnemy(pWeapon->b_weaponRange, pPlayer->b_gangChoice)
  if !pEnemy
    displayf2("\n%s %s but falls short.\n", pPlayer=>s_name, sAction)
    genShotSound(pWeapon=>s_itemKind)
    return
  fin

  // Figure out combined player and weapon agility
  agil = pPlayer->b_agility + scanModifiers(pWeapon=>p_modifiers, @S_AGILITY)

  // Roll for hitting and dodging
  if !rollPlayerHit(pPlayer, pWeapon, pEnemy, sAction)
    genShotSound(pWeapon=>s_itemKind)
    return
  fin
  if rollEnemyDodge(pPlayer, pEnemy, sAction)
    genShotSound(pWeapon=>s_itemKind)
    return
  fin

  // Calculate damage
  //combatDebug// if combatDebug
  //combatDebug//   displayf3("Base dmg: %dd%d+%d = ", (pWeapon=>r_projectileDmg>>12) & $F, (pWeapon=>r_projectileDmg >> 8) & $F, pWeapon=>r_projectileDmg & $FF)
  //combatDebug// fin
  dmg = rollDiceWithLuck(pWeapon=>r_projectileDmg, pPlayer->b_luck)
  //combatDebug// if combatDebug; displayf1("%d\n", dmg); fin

  // Damage bonus is:
  // * 1% per point of combined player and weapon agility
  // * 2% per skill level in the weapon type
  // (no contribution of strength)
  // (enemies don't have armor)
  dmg = dmg + addPercent(dmg, agil + scanModifiers(pPlayer=>p_skills, pWeapon=>s_itemKind))
  //combatDebug// if combatDebug
  //combatDebug//   displayf1("dmg bonus:\n agil=%d\n", agil)
  //combatDebug//   displayf1("  plyr skill=%d\n", scanModifiers(pPlayer=>p_skills, pWeapon=>s_itemKind))
  //combatDebug//   displayf1("final bonus=%d%%\n", agil + scanModifiers(pPlayer=>p_skills, pWeapon=>s_itemKind))
  //combatDebug//   displayf1("final dmg=%d\n", dmg)
  //combatDebug//   getUpperKey()
  //combatDebug// fin

  // Okay, do that damage.
  damageEnemy(pPlayer, pEnemy, dmg, sAction)
  genShotSound(pWeapon=>s_itemKind)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def consumeAmmo(pl, pWeapon)#0
  if !pWeapon->b_clipCurrent; fatal("clip zero"); fin // sanity check
  pWeapon->b_clipCurrent = pWeapon->b_clipCurrent - 1
  //combatDebug// if combatDebug; displayf1("Clip has %d\n", pWeapon->b_clipCurrent); fin

  // Special (for e.g. bows): clipSize zero means weapon reloads automatically
  if !pWeapon->b_clipCurrent and !pWeapon->b_clipSize
    pItemUtil=>itemutil_reloadWeapon(pl, pWeapon, FALSE) // silently reload
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def playerDodge(pPlayer)#0
  // no need to display anything. Actual dodging mechanics handled in enemy attack function.
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def groupCanFight(p)#1
  return first(p=>p_enemies, @canFight)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def formatGroupName(p, withDist)#1
  byte count
  count = sum(p=>p_enemies, NULL, @canFight)
  isPlural = count <> 1
  if (p=>p_enemies=>r_groupSize == 0)
    if withDist
      return sprintf2("%s at %d'", p=>p_enemies=>s_name, p=>p_enemies->b_enemyAttackRange)
    else
      return sprintf1("%s", p=>p_enemies=>s_name)
    fin
  fin
  if withDist
    return sprintf3("%d %s at %d'", count, p=>p_enemies=>s_name, p=>p_enemies->b_enemyAttackRange)
  fin
  return sprintf2("%d %s", count, p=>p_enemies=>s_name)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def displayOpponents()#0
  word p
  byte firstTime

  displayStr("You face ")
  firstTime = TRUE
  p = global=>p_enemyGroups
  while p
    if groupCanFight(p)
      if !firstTime
        displayStr(p=>p_nextObj ?? ", " :: " and ")
      fin
      firstTime = FALSE
      displayStr(formatGroupName(p, TRUE))
    fin
    p = p=>p_nextObj
  loop
  displayStr(".\n")
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def displayOption(key, str)#0
  rawDisplayStr("\n (")
  displayChar(key)
  rawDisplayStr(")^T028")
  displayStr(str)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def minEnemyDist()#1
  word p, minDist
  minDist = 9999

  // Process each group
  p = global=>p_enemyGroups
  while p
    if groupCanFight(p)
      minDist = min(minDist, p=>p_enemies->b_enemyAttackRange)
    fin
    p = p=>p_nextObj
  loop
  return minDist
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def noneInRange()#1
  rawDisplayStr("^D") // clear to end of page
  displayStr("No enemy in range.")
  combatPause
  return FALSE
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Phase 0: count alive & in-range gangs
// Phase 1: display gangs
// Phase 2: select gang by number, return enemyNum
def groupLoop(maxDist, phase, choiceNum)#1
  word p
  byte n

  p = global=>p_enemyGroups
  n = 0
  while p
    if p=>p_enemies->b_enemyAttackRange <= maxDist and groupCanFight(p)
      if phase == 1
        displayOption(n + 'A', formatGroupName(p, FALSE))
      elsif phase == 2 and n == choiceNum
        return p->b_enemyNum
      fin
      n++
    fin
    p = p=>p_nextObj
  loop
  return n
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def chooseGroup(pl, maxDist)#1
  word p
  byte nAvailGroups, choiceNum

  // First, determine how many groups are alive and within striking distance
  nAvailGroups = groupLoop(maxDist, 0, 0)

  // If nobody in range, ack!
  if !nAvailGroups; return noneInRange(); fin

  // If only one group, the choice is trivially obvious.
  if nAvailGroups == 1
    choiceNum = 0
  else
    rawDisplayf1("^D%s attack:", pl=>s_name) // ctrl-D = clear to end of page
    groupLoop(maxDist, 1, 0)
    while TRUE
      choiceNum = getUpperKey()
      if choiceNum == $1B // esc
        return FALSE
      fin
      choiceNum = choiceNum - 'A'
      if choiceNum >= 0 and choiceNum < nAvailGroups; break; fin
      beep()
    loop
  fin

  // Success.
  pl->b_gangChoice = groupLoop(maxDist, 2, choiceNum)
  return TRUE
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Determine number of shots. Only gets called for projectile attack
def chooseShotNumber(pl, pWeapon)#0
  byte i, nChoices, k

  // Treat single-use weapons as having infinite ammo
  if pWeapon->b_flags & WEAPON_FLAG_SINGLE_USE
    pWeapon->b_clipCurrent = 99
  fin

  // Determine number of shot choices this weapon has
  nChoices = 0
  pl->b_shotChoice = 0
  for i = 0 to 2
    if pWeapon->ba_attacks[i] and (pWeapon->ba_attacks[i] == 1 or pWeapon->b_clipCurrent > 1)
      nChoices++
      pl->b_shotChoice = pWeapon->ba_attacks[i]  // in case only one choice
    fin
  next

  // If it's a double-shot weapon but player has only 1 ammo, override to allow single-shot
  if nChoices == 0 and pWeapon->ba_attacks[1] == 2 and pWeapon->b_clipCurrent == 1
    pl->b_shotChoice = 1
    return
  fin

  // If only one choice, we're done.
  if nChoices <= 1; return; fin

  // Let player choose
  rawDisplayf1("^D%s shot type:", pl=>s_name) // ctrl-D = clear to end of page
  if pWeapon->ba_attacks[0]; displayOption('O', "One shot"); fin
  if pWeapon->ba_attacks[1] == 2
    displayOption('D', "Double-shot")
  elsif pWeapon->ba_attacks[1] > 2
    displayOption('S', "Semi-auto")
  fin
  if pWeapon->ba_attacks[2]; displayOption('A', "Automatic"); fin

  while TRUE
    when getUpperKey()
      is 'O'; i = 0; break
      is 'D'
      is 'S'; i = 1; break
      is 'A'; i = 2; break
      otherwise i = 99; break
    wend
    if i <= 2 and pWeapon->ba_attacks[i]
      pl->b_shotChoice = pWeapon->ba_attacks[i]
      break
    fin
    beep
  loop
  return
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def chooseWeapon(pl, pWeapon)#1
  byte nChoices, i, k
  word pi, sel
  pi = pl=>p_items
  sel = NULL
  while pi
    if pi->t_type == TYPE_WEAPON
      pi->b_flags = pi->b_flags & !ITEM_FLAG_EQUIP // de-equip all weapons before selecting one
      nChoices++
      sel = pi // in case only one choice
    fin
    pi = pi=>p_nextObj
  loop

  if nChoices > 1
    rawDisplayf1("^D%s weapon:", pl=>s_name) // ctrl-D = clear to end of page
    i = 'A'
    pi = pl=>p_items
    while pi
      if pi->t_type == TYPE_WEAPON
        displayOption(i, sprintf1(pi==pWeapon ?? "*%s" :: "%s", pi=>s_name))
        i++
      fin
      pi = pi=>p_nextObj
    loop
    displayOption(i, sprintf1(pWeapon ?? "%s" :: "*%s", "Fists & Feet"))
    while TRUE
      k = getUpperKey
      sel = NULL
      pi = pl=>p_items
      while pi
        if pi->t_type == TYPE_WEAPON
          if k == 'A'; sel = pi; break; fin
          k--
        fin
        pi = pi=>p_nextObj
      loop
      if sel; break; fin
      if k == 'A'; break; fin  // selected Fists & Feet
      beep
    loop
  fin

  // Apply the final selection
  if sel
    sel->b_flags = sel->b_flags | ITEM_FLAG_EQUIP
  fin

  return sel <> pWeapon
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def killAllEnemies()#0
  buildString() // so we don't get bogus stuff to the screen
  forEach(global=>p_enemyGroups, &(g) forEach(g=>p_enemies, @killEnemy))
  finishString(0) // but don't display it
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def playerCombatChoose(pl)#0
  word p, pWeapon
  byte key
  byte canShoot, canReload, canChange, canAdvance, redisplayMenu

  // The party advances all at once
  if isAdvancing
    pl->b_combatChoice = 'A'
    return
  fin

  // Count all weapons and get currently equipped
  canShoot  = FALSE
  canReload = FALSE
  canChange = FALSE
  pWeapon   = NULL
  p = pl=>p_items
  while p
    if p->t_type == TYPE_WEAPON
      canChange = TRUE
      if p->b_flags & ITEM_FLAG_EQUIP
        pWeapon = p
      fin
    fin
    p = p=>p_nextObj
  loop

  redisplayMenu = TRUE

  // Loop until a valid option is chosen
  while TRUE

    // Let them know their options
    if redisplayMenu
      setCursor(0, COMBAT_CHOICE_Y)
      rawDisplayStr("^D") // clear to end of page
      callGlobalFunc(GS_COMBAT_PROMPT, 0, 0, 0)
      displayf1("%s:\nM)elee, ", pl=>s_name)
      if pWeapon
        // Special (for e.g. bows): clipSize zero means weapon reloads automatically
        if pWeapon=>r_projectileDmg and !pWeapon->b_clipCurrent and !pWeapon->b_clipSize
          pItemUtil=>itemutil_reloadWeapon(pl, pWeapon, FALSE) // silently reload
        fin
        if pWeapon->b_clipCurrent
          canShoot = TRUE
          if pWeapon->b_clipSize
            displayf2("S)hoot %d:%d, ", pWeapon->b_clipCurrent, pWeapon->b_clipSize))
          else
            displayStr("S)hoot, ")
          fin
        fin
        if pWeapon->b_clipCurrent < pWeapon->b_clipSize
          canReload = TRUE
          displayStr("R)eload, ")
        fin
      fin
      if canChange
        displayStr("C)hange weapon, ")
      fin
      displayStr("D)odge, ")
      canAdvance = (pl == global=>p_players) and (minEnemyDist() > 5) // only one advance per turn
      if canAdvance; displayStr("A)dvance, "); fin
      displayStr("F)lee")
      setCursor(0, COMBAT_CHOICE_Y)
    fin

    pl->b_combatChoice = getUpperKey()
    redisplayMenu = FALSE
    when pl->b_combatChoice
      is 'M'
        if !chooseGroup(pl, 5)
          redisplayMenu = TRUE
          break
        fin
        setCursor(0, COMBAT_CHOICE_Y)
        return
      is 'F'
        isFleeing = TRUE
        return
      is 'D'
        return
      is 'S'
        if canShoot
          if !chooseGroup(pl, pWeapon->b_weaponRange)
            redisplayMenu = TRUE
            break
          fin
          setCursor(0, COMBAT_CHOICE_Y)
          chooseShotNumber(pl, pWeapon)
          setCursor(0, COMBAT_CHOICE_Y)
          return
        fin
        break
      is 'R'
        if canReload; return; fin
        break
      is 'C'
        if canChange
          if chooseWeapon(pl, pWeapon)
            setCursor(0, COMBAT_CHOICE_Y)
            return
          fin
          redisplayMenu = TRUE
        fin
        break
      is 'A'
        if canAdvance
          isAdvancing = TRUE
          return
        fin
        break
      //combatDebug// is '%'
      //combatDebug//   if global->b_godmode
      //combatDebug//     clearWindow()
      //combatDebug//     combatDebug = 1-combatDebug
      //combatDebug//     rawDisplayf1("Combat debug: %d\n\n", combatDebug)
      //combatDebug//     return playerCombatChoose(pl) // to redisplay menu
      //combatDebug//   fin
      //combatDebug//   break
      is '#'
        if global->b_godmode
          killAllEnemies
          return
        fin
        break
    wend
    if !redisplayMenu; beep(); fin
  loop
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Called after a single-use weapon is used (regardless of whether it hit or not)
def checkSingleUse(pl, pWeapon)#0
  if !pWeapon; return; fin
  if !(pWeapon->b_flags & WEAPON_FLAG_SINGLE_USE); return; fin
  removeFromList(@pl=>p_items, pWeapon)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def getWeapon(pl)#1
  return first(pl=>p_items, &(p) p->t_type == TYPE_WEAPON and p->b_flags & ITEM_FLAG_EQUIP)
end


///////////////////////////////////////////////////////////////////////////////////////////////////
def calcNumAtt(pl, isMelee, pWeapon)
  byte skillPts

  skillPts = 0
  if pWeapon
    if pWeapon->b_flags & WEAPON_FLAG_SINGLE_USE; return 1; fin
    skillPts = scanModifiers(pl=>p_skills, pWeapon=>s_itemKind)
  elsif isMelee
    skillPts = scanModifiers(pl=>p_skills, @S_HAND_TO_HAND)
  fin

  if skillPts >= 9
    return 3
  elsif skillPts >= 6
    return 2
  fin
  return 1
end


///////////////////////////////////////////////////////////////////////////////////////////////////
def playerCombatTurn(pl)#1
  word pWeapon, pGroup, pEnemy
  byte i, j, nAttacks

  if !nEnemiesFighting; return 0; fin

  // Get weapon
  pWeapon = getWeapon(pl)

  // Execute the player's choice
  when pl->b_combatChoice
    is 'M'
      nAttacks = calcNumAtt(pl, TRUE, pWeapon) // TRUE=melee
      for i = 1 to nAttacks
        playerMelee(pl, pWeapon)
        if i < nAttacks; combatPause; fin
      next
      checkSingleUse(pl, pWeapon)
      break
    is 'F'
      return 0
    is 'S'
      nAttacks = calcNumAtt(pl, FALSE, pWeapon) // FALSE=not melee
      for i = 1 to nAttacks
        for j = 1 to pl->b_shotChoice
          if nEnemiesFighting and pWeapon->b_clipCurrent
            playerShoot(pl, pWeapon)
            consumeAmmo(pl, pWeapon)
            if j < pl->b_shotChoice; combatPause; fin
          fin
        next
        if i < nAttacks; combatPause; fin
      next
      checkSingleUse(pl, pWeapon)
      break
    is 'R'
      displayStr("\n")
      pItemUtil=>itemutil_reloadWeapon(pl, pWeapon, TRUE) // TRUE = echo
      displayStr("\n")
      break
    is 'C'
      // Choose a different weapon
      displayf3("\n%s switches to %s %s.\n", pl=>s_name, hisHerTheir(pl->c_gender), pWeapon ?? pWeapon=>s_name :: "fists & feet")
      break
    is 'A'
      // Advance 5 feet as a party (i.e. only the first player)
      if !isAdvancing; return 0; fin // only advance once per turn
      pGroup = global=>p_enemyGroups
      while pGroup
        pGroup=>p_enemies->b_enemyAttackRange = pGroup=>p_enemies->b_enemyAttackRange - 5
        // Copy new range to all enemies in the group
        pEnemy = pGroup=>p_enemies=>p_nextObj
        while pEnemy
          pEnemy->b_enemyAttackRange = pGroup=>p_enemies->b_enemyAttackRange
          pEnemy = pEnemy=>p_nextObj
        loop
        pGroup = pGroup=>p_nextObj
      loop
      displayStr("\nYou move forward 5'.\n")
      isAdvancing = FALSE  // only advance once per turn
      break
  wend
  return 1
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def enemyCombatTurn(pe)#1
  word pl, dodgeChance, pItem, damReduce
  byte roll, dam, needShow

  // Choose a random target from amongst the fighting players
  roll = rand16() % nPartyFighting
  pl = select(global=>p_players, @canFight, roll)
  isPlural = FALSE
  displayf3("\n%s %s %s ", pe=>s_name, pe=>s_attackText, pl=>s_name)

  // Roll to hit
  roll = rollPercentileWithLuck(pl->b_luck) // player luck can raise roll, reducing enemy chance to hit
  //combatDebug// if combatDebug; displayf2("\nenemy roll=%d, need <%d\n", roll, pe->b_chanceToHit); fin
  needShow = FALSE
  if roll < pe->b_chanceToHit

    // Check for dodge
    dodgeChance = pl->b_agility * 5
    //combatDebug// if combatDebug; displayf2("plyr dodge: agil %dx5 = %d%%\n", pl->b_agility, dodgeChance); fin
    if pl->b_combatChoice == 'D' or pl->b_combatChoice == 'A' // automatically dodge while advancing
      dodgeChance = dodgeChance + (pl->b_dodging * 5)
      //combatDebug// if combatDebug; displayf1(" +dodg-abil %dx5\n", pl->b_dodging); fin
    fin
    pItem = pl=>p_items
    while pItem
      if pItem->t_type == TYPE_ARMOR
        dodgeChance = dodgeChance + (scanModifiers(pItem=>p_modifiers, S_AGILITY) * 5)
        //combatDebug// if combatDebug
        //combatDebug//   if scanModifiers(pItem=>p_modifiers, S_AGILITY)
        //combatDebug//     displayf1(" +armor agil %dx5\n", scanModifiers(pItem=>p_modifiers, S_AGILITY))
        //combatDebug//   fin
        //combatDebug// fin
      fin
      pItem = pItem=>p_nextObj
    loop

    // Max dodge chance is 90%, min is 0.
    dodgeChance = max(0, min(90, dodgeChance))
    roll = rollPercentileWithLuck(-(pl->b_luck))  // negate so luck has chance of reducing roll = better
    //combatDebug// if combatDebug; displayf2("dodg roll %d, need <%d\n", roll, dodgeChance); fin
    if roll < dodgeChance
      displayf1("but %s dodges!", pl=>s_name)
    else
      dam = rollDiceWithLuck(pe=>r_enemyDmg, -(pl->b_luck)) // player luck can reduce damage inflicted by enemy
      //combatDebug// if combatDebug; displayf1("base dmg %d\n", dam); fin
      // Each point of armor-value reduces damage 2%, up to max of 90%
      damReduce = max(0, min(90, pl->b_armor * 2))
      //combatDebug// if combatDebug; displayf1(" - armor %dx2%%\n", pl->b_armor); fin
      dam = dam - addPercent(dam, damReduce)
      displayf1("and hits for %d damage!", dam)
      pl=>w_health = max(0, pl=>w_health - dam)
      if pl=>w_health == 0
        displayf1(" %s is killed!", pl=>s_name)
        nPartyFighting--
        if (!(pl->b_playerFlags & PLAYER_FLAG_NPC))
          nPlayersFighting--
        fin
      fin
      needShow = TRUE
    fin
  else
    displayStr("and misses.")
  fin
  displayStr("\n")
  genShotSound(pe=>s_attackText)
  //combatDebug// if combatDebug; getUpperKey; fin

  if needShow; showParty(); fin
  return TRUE
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def combatInsert(toAdd)#0
  word p, pPrev

  // Find the proper position based on combat order number (keep largest first in the list)
  pPrev = @global=>p_combatFirst
  while TRUE
    p = *pPrev
    if !p; break; fin
    if p->b_combatOrder < toAdd->b_combatOrder; break; fin
    pPrev = p + p_combatNext
  loop

  // Add it right there.
  toAdd=>p_combatNext = *pPrev
  *pPrev = toAdd
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def determineCombatOrder()#0
  word p, p2

  nPartyFighting   = 0
  nPlayersFighting = 0
  nEnemiesFighting = 0

  // Do each player character, with random chance based on their agility
  global=>p_combatFirst = NULL
  p = global=>p_players
  while p
    if canFight(p)
      p->b_combatOrder = rand16() % (p->b_agility * 10)
      combatInsert(p)
      nPartyFighting++
      if (!(p->b_playerFlags & PLAYER_FLAG_NPC)) // only count real players
        nPlayersFighting++
      fin
    fin
    p = p=>p_nextObj
  loop

  // Then all the enemies, with random chance based on their chance to hit.
  p = global=>p_enemyGroups
  while p
    p2 = p=>p_enemies
    while p2
      if canFight(p2)
        p2->b_combatOrder = rand16() % p2->b_chanceToHit
        p2->b_enemyAttackRange = p=>p_enemies->b_enemyAttackRange // force all to same range
        combatInsert(p2)
        nEnemiesFighting = nEnemiesFighting + 1
      fin
      p2 = p2=>p_nextObj
    loop
    p = p=>p_nextObj
  loop
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def makeEnemy(pData)
  word p; p = mmgr(HEAP_ALLOC, TYPE_ENEMY)
  p=>s_name = mmgr(HEAP_INTERN, pData=>s_en_name)
  p=>w_health = rollDice(pData=>r_en_hitDice) // e.g. 4d6
  if !pData->b_en_img1 or (rand16() % 2)
    p->b_image = pData->b_en_img0
  else
    p->b_image = pData->b_en_img1
  fin
  p->b_attackType = pData->b_en_attType
  p=>s_attackText = mmgr(HEAP_INTERN, pData=>s_en_attText)
  // Establish (possibly random) attack range.
  // The range will propagate up to group, then back down to overwrite range on all
  // enemies within the group to keep them consistent.
  p->b_enemyAttackRange = rollDice(pData=>r_en_attRange)
  p->b_chanceToHit = pData->b_en_chanceToHit
  p=>r_enemyDmg = pData=>r_en_dmgDice
  p=>r_enemyXP = pData=>r_en_expDice
  p=>r_groupSize = pData=>r_en_groupSize
  p->b_lootChance0 = pData->b_en_lootChance0
  p->b_lootChance1 = pData->b_en_lootChance1
  p->b_lootCode0 = pData->b_en_lootCode0
  p->b_lootCode1 = pData->b_en_lootCode1
  p=>r_goldLoot = pData=>r_en_goldLoot
  return p
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def makeEnemyGroup(enemyNum, pEnemyData)#1
  word p, enem, groupSize
  p = mmgr(HEAP_ALLOC, TYPE_ENEMY_GROUP)
  enem = makeEnemy(pEnemyData)
  p->b_enemyNum = enemyNum
  if enem=>r_groupSize == 0  // handle unique enemies
    groupSize = 1
  else
    groupSize = rollDice(enem=>r_groupSize)
  fin
  addToList(p + p_enemies, enem)
  while groupSize > 1
    addToList(p + p_enemies, makeEnemy(pEnemyData))
    groupSize--
  loop

  return p
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Return a random entry from an array which is terminated by a zero entry
def randomFromArray(arr, filter)#1
  byte siz, start, cur
  word p

  // Determine the array size by looking for the terminating zero
  siz = 0
  while *((siz << 1) + arr)
    siz++
  loop

  // Pick a random starting point in the array
  start = rand16() % siz

  // Advance (circularly) until we find a filter-approved entry or run out
  cur = start
  while TRUE
    p = *((cur << 1) + arr)
    if !filter; break; fin // no filter, this entry is good
    if filter(p); break; fin // filter approved, this entry is good
    cur++
    // Loop around circularly
    if cur >= siz; cur = 0; fin
    // Stop when we've come around to our starting point
    if cur == start
      p = NULL
      break
    fin
  loop
  return p
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def nonDupeEnemy(enemyNum)#1
  ctx = enemyNum
  return !first(global=>p_enemyGroups, &(p) p->b_enemyNum == ctx)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def makeRandomGroup(mapCode)#1
  word enemiesModule
  byte enemyNum, gangChance
  word pEnemyData, pGroup

  enemiesModule = mmgr(QUEUE_LOAD, MOD_GEN_ENEMIES<<8 | RES_TYPE_MODULE)
  mmgr(FINISH_LOAD, 0)
  enemyNum = randomFromArray(enemiesModule()=>enemies_forZone(mapCode), @nonDupeEnemy)
  mmgr(FREE_MEMORY, enemiesModule)
  if !enemyNum; return 0; fin // no non-dupe enemies left in mapCode zone

  enemiesModule = mmgr(QUEUE_LOAD, (MOD_GEN_ENEMIES + 1 + ((enemyNum-1)&1))<<8 | RES_TYPE_MODULE)
  mmgr(FINISH_LOAD, 0)
  pEnemyData = enemiesModule() + 1 + (((enemyNum-1)>>1) * EnemyData)
  gangChance = pEnemyData->b_en_gangChance
  pGroup = makeEnemyGroup(enemyNum, pEnemyData)
  addToList(@global=>p_enemyGroups, pGroup)
  mmgr(FREE_MEMORY, enemiesModule)

  return gangChance
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Determines if string should be prefixed by "an ", "a ", or "". Examples:
// an apple
// a gun
// boots
def anOrA(str)
  byte ch
  ch = charToUpper(^(str + ^str))
  if ch == 'S'
    return @S_EMPTY // assume plural
  fin
  ch = charToUpper(^(str+1))
  if ch == 'A' or ch == 'E' or ch == 'I' or ch == 'O' or ch == 'U'
    return @S_AN
  fin
  return @S_A
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def youFind(pItem, suffix)
  displayStr("You find ")
  if pItem->t_type == TYPE_FANCY_ITEM and pItem=>w_count > 1
    isPlural = TRUE
    displayf3("%d %s%s!\n", pItem=>w_count, pItem=>s_name, suffix)
  else
    isPlural = FALSE
    displayf3("%s%s%s!\n", anOrA(pItem=>s_name), pItem=>s_name, suffix)
  fin
  return TRUE
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def lootItem(ppItems, lootChance, lootCode)#0
  word pItemTblModule, itemNum, pItem, pComp

  if lootCode == 0; return; fin
  if rollPercentileWithLuck(-(global=>p_players->b_luck)) >= lootChance; return; fin

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

  itemNum = randomFromArray(pItemTblModule()=>items_forLootCode(lootCode), NULL)
  pItem = createItem(itemNum)
  if pItem->t_type == TYPE_FANCY_ITEM
    pItem=>w_count = rollDiceWithLuck(pItem=>r_lootAmount, global=>p_players->b_luck)
  fin

  mmgr(FREE_MEMORY, pItemTblModule)

  pComp = scanForNamedObj(*ppItems, pItem=>s_name)
  if pComp and pItem->t_type == TYPE_FANCY_ITEM
    pComp=>w_count = min(30000, pComp=>w_count + pItem=>w_count)
  elsif fullAddItem(pItem, FALSE) >= 1 // trial-run, don't add to main inv
    addToList(ppItems, pItem)
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def calcListSuffix(pThing, final)#1
  if pThing=>p_nextObj
    if pThing=>p_nextObj=>p_nextObj
      return @S_COMMA
    else
      return @S_AND
    fin
  fin
  return final
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def dispLoot(pItems)#0
  word pItem, final, suffix
  final = "!\n"
  displayStr("You find ")
  pItem = pItems
  while pItem
    suffix = calcListSuffix(pItem, final)
    if pItem->t_type == TYPE_FANCY_ITEM and pItem=>w_count > 1
      isPlural = TRUE
      displayf3("%d %s%s", pItem=>w_count, pItem=>s_name, suffix)
    else
      isPlural = FALSE
      displayf2("%s%s", pItem=>s_name, suffix)
    fin
    pItem = pItem=>p_nextObj
  loop
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def collectLootAndXP()#0
  word group, enemy, gold, xp, pItems, pItem, first

  gold = 0
  xp = 0
  pItems = NULL
  group = global=>p_enemyGroups
  while group
    enemy = group=>p_enemies
    if enemy
      lootItem(@pItems, enemy->b_lootChance0, enemy->b_lootCode0)
      lootItem(@pItems, enemy->b_lootChance1, enemy->b_lootCode1)
    fin
    while enemy
      gold = gold + rollDiceWithLuck(enemy=>r_goldLoot, global=>p_players->b_luck)
      xp = xp + rollDiceWithLuck(enemy=>r_enemyXP, global=>p_players->b_luck)
      enemy = enemy=>p_nextObj
    loop
    group = group=>p_nextObj
  loop

  if pItems
    dispLoot(pItems)
    // When taking out of one list and into another, we can't use forEach, because the p_nextObj
    // link gets destroyed as we go along.
    while pItems
      pItem = pItems
      pItems = pItems=>p_nextObj
      fullAddItem(pItem, TRUE)
    loop
  fin

  if gold > 0
    displayf1("You collect %d gold!\n", addGold(gold))
  fin

  if xp > 0
    displayf1("You earn %d experience!\n", xp); first = FALSE
    adjustPartyStat(@S_XP, xp)
  fin

  if pItems or gold or xp
    promptAnyKey(FALSE)
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Concatenate two strings to form a single one on the heap. It's unusual to need this, but we
// do to display the enemy prompt for multiple groups.
def concat(s1, s2)#1
  byte len
  word p
  len = ^s1 + ^s2
  p = mmgr(HEAP_ALLOC, len+1)
  ^p = len
  memcpy(s1+1, p+1, ^s1, FALSE)
  memcpy(s2+1, p+(^s1)+1, ^s2, FALSE)
  return p
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def startCombat(mapCode)#1
  word p, n, s
  byte n2

  // Pre-load some global funcs
  preload()

  // Setup
  isFleeing = FALSE
  //combatDebug// combatDebug = FALSE
  global=>p_enemyGroups = NULL

  // Make one or more enemy groups (rolling for gangChance on each to see if we need another)
  while TRUE
    // Generate a random, non-duplicate group. Returns chance for adding another group to gang
    n = makeRandomGroup(mapCode)
    if !n; break; fin

    // If maxed out on groups, or dice roll for gang fails, we're done
    if countList(global=>p_enemyGroups) >= MAX_GROUPS; break; fin
    if (rand16() % 100) >= n; break; fin
  loop

  // Display portrait of first group
  setPortrait(global=>p_enemyGroups=>p_enemies->b_image)
  mmgr(FINISH_LOAD, 0)
  pItemUtil = preloads[0]()

  // Clear keyboard stobe, because while wandering the map, the player may have
  // queued up movement keys, which are made obsolete by the surprise of combat.
  if !recordMode; ^kbdStrobe; fin

  // Say who we're fighting. This is tricky because the global ENEMY_INTRO script
  // is expecting a single group.
  p = global=>p_enemyGroups
  n = countList(p=>p_enemies)
  isPlural = n <> 1
  s = mmgr(HEAP_INTERN, sprintf1(p=>p_enemies=>s_name, 0))
  p = p=>p_nextObj
  while p
    n2 = countList(p=>p_enemies)
    isPlural = n2 <> 1
    s = concat(s, sprintf3("%s %d %s", p=>p_nextObj ?? "," :: " and", n2, p=>p_enemies=>s_name))
    isPlural = TRUE // for display on final string below
    p = p=>p_nextObj
  loop
  p = callGlobalFunc(GS_ENEMY_INTRO, 0, 0, 0)
  displayf2(p, n, s)

  rawDisplayStr("\n")
  displayOption('B', "Battle")
  displayOption('F', "Flee")
  while TRUE
    n = getUpperKey()
    if n == 'B'
      displayStr("\n\nBattle!")
      combatPause()
      return 1
    elsif n == 'F'
      displayStr("\n\nFleeing...\n")
      return 0
    // Secret option for testing: instant victory
    elsif n == '#' and global->b_godmode
      displayStr("\n\n")
      return 99
    // Secret option for testing: just die
    elsif n == '*' and global->b_godmode
      displayStr("\n\n")
      return -99
    fin
    beep()
  loop

  return 0 // just to keep compiler happy
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def autoReload(pl)#1
  if canFight(pl)
    return pItemUtil=>itemutil_reloadWeapon(pl, getWeapon(pl), TRUE) // TRUE = echo
  fin
  return 0
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def processWin()#0
  // Clear the window, then display the win message
  clearWindow
  callGlobalFunc(GS_COMBAT_WIN, 0, 0, 0)

  // Reload each player's weapon if possible. If anything actually happened, be sure to
  // wait for a keypress.
  displayStr("\n")
  if sum(global=>p_players, NULL, @autoReload)
    promptAnyKey(FALSE)
    clearWindow
  fin

  // Make max space avail for item tables, then grab the loot
  collectLootAndXP()
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Returns: zero if fled, non-zero if won
def _combat_zoneEncounter(s_encZone)#1
  word p, l, answer

  // Show portrait and threat details, find out if player wants to fight (vs. run)
  answer = startCombat(s_encZone)
  if answer == 99
    killAllEnemies
  elsif answer == -99
    return answer // special code for death
  elsif !answer
    isFleeing = TRUE
  fin

  // We don't need to actually determine combat order yet, but we do need it to calculate the
  // number of enemies.
  determineCombatOrder()

  // Do each round of combat until player wins, loses, or flees
  while TRUE

    // Tell what the party currently faces
    clearWindow()

    // Make sure portrait reflects a living enemy
    p = first(global=>p_enemyGroups, @groupCanFight)
    if p
      displayOpponents()
      setPortrait(p=>p_enemies->b_image)
    fin

    // Get the choice of each player or NPC
    isAdvancing = FALSE
    forSome(global=>p_players, &(pl) canFight(pl) and nEnemiesFighting and !isFleeing, @playerCombatChoose)
    clearWindow()

    determineCombatOrder()

    p = global=>p_combatFirst
    while TRUE // not just "while p", so that we always check nPlayersFighting etc.
      if !nPlayersFighting
        freePreloaded()
        return -99  // special code for death
      elsif !nEnemiesFighting
        processWin()  // also calls freePreloaded
        // Note: no need to clear heap -- the caller does that.
        return 1
      elsif !p
        break
      fin

      if canFight(p)
        when p->t_type
          is TYPE_PLAYER
            if isFleeing
              displayStr("\nYou have fled.")
              // Note: no need to clear heap -- the caller does that.
              freePreloaded()
              return 0
            fin
            if p->b_combatChoice <> 'D'
              if playerCombatTurn(p)
                combatPause()
              fin
            fin
            break
          is TYPE_ENEMY
            if enemyCombatTurn(p); combatPause(); fin
            break
        wend
      fin
      p = p=>p_combatNext
    loop
  loop
  return 0 // keep compiler happy
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// initialization code

return @funcTbl
done
