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

'misc
#include "config.bi"
#include "common.bi"
#include "loading.bi"
#INCLUDE "gglobals.bi"
#INCLUDE "const.bi"
#INCLUDE "uiconst.bi"
#INCLUDE "udts.bi"
#INCLUDE "battle_udts.bi"

'modules
#include "bmod.bi"
#include "bmodsubs.bi"
#include "bcommon.bi"
#include "game.bi"
#include "yetmore2.bi"
#include "moresubs.bi"
#include "allmodex.bi"
#include "scriptcommands.bi"
#include "menustuf.bi"
#include "sliceedit.bi"
#include "specialslices.bi"

'--local subs and functions
DECLARE FUNCTION count_dissolving_enemies(bslot() as BattleSprite) as integer
DECLARE FUNCTION find_empty_enemy_slot(formdata as Formation) as integer

DECLARE SUB spawn_on_death(byval deadguy as integer, byval killing_attack as integer, byref bat as BattleState, formdata as Formation, bslot() as BattleSprite)
DECLARE SUB enemy_death_sfx(battler as BattleSprite)
DECLARE SUB check_death(byval deadguy as integer, byval killing_attack as integer, byref bat as BattleState, bslot() as BattleSprite, formdata as Formation)

DECLARE SUB checkitemusability(iuse() as integer, bslot() as BattleSprite, byval who as integer)
DECLARE SUB reset_battle_state (byref bat as BattleState)
DECLARE SUB reset_targetting (byref bat as BattleState)
DECLARE SUB reset_attack (byref bat as BattleState)
DECLARE SUB reset_victory_state (byref vic as VictoryState)
DECLARE SUB show_victory (byref bat as BattleState, bslot() as BattleSprite, page as integer)
DECLARE SUB trigger_victory(byref bat as BattleState, bslot() as BattleSprite)
DECLARE SUB fulldeathcheck (byval killing_attack as integer, bat as BattleState, bslot() as BattleSprite, formdata as Formation)
DECLARE SUB anim_flinchstart(byval who as integer, bslot() as BattleSprite, byref attack as AttackData)
DECLARE SUB flinch_anim_eachtick(byval who as integer, bslot() as BattleSprite)
DECLARE SUB update_battle_slices(bat as BattleState, bslot() as BattleSprite)
DECLARE SUB update_battlesprite_slices(bat as BattleState, bslot() as BattleSprite)
DECLARE SUB update_battle_ui_slices(bat as BattleState, bslot() as BattleSprite)
DECLARE SUB draw_damage_text(bslot() as BattleSprite, battlefield_sl as Slice ptr, page as integer)
DECLARE FUNCTION battle_time_can_pass(byref bat as BattleState) as bool
DECLARE FUNCTION battle_meters_can_advance(byref bat as BattleState, bslot() as BattleSprite) as bool
DECLARE SUB battle_crappy_run_handler(byref bat as BattleState, bslot() as BattleSprite)
DECLARE SUB show_enemy_meters(bat as BattleState, bslot() as BattleSprite, formdata as Formation, page as integer)
DECLARE SUB battle_animate(byref bat as BattleState, bslot() as BattleSprite, st() as HeroDef)
DECLARE SUB battle_meters (byref bat as BattleState, bslot() as BattleSprite, formdata as Formation)
DECLARE SUB battle_display_menus (byref bat as BattleState, bslot() as BattleSprite, st() as HeroDef, page as integer)
DECLARE SUB battle_confirm_target(byref bat as BattleState, bslot() as BattleSprite)
DECLARE SUB battle_leave_targetting_mode(bat as BattleState, bslot() as BattleSprite)
DECLARE SUB player_attack_targetting_done(byref bat as BattleState, bslot() as BattleSprite)
DECLARE SUB battle_targetting(byref bat as BattleState, bslot() as BattleSprite)
DECLARE SUB battle_spawn_on_enemy_hit(byval targ as integer, byref bat as BattleState, bslot() as BattleSprite, formdata as Formation)
DECLARE SUB battle_spawn_on_first_hit(atk as AttackData, byref bat as BattleState, bslot() as BattleSprite, formdata as Formation)
DECLARE SUB battle_attack_anim_cleanup (byref attack as AttackData, byref bat as BattleState, bslot() as BattleSprite, formdata as Formation)
DECLARE SUB battle_attack_anim_playback (byref attack as AttackData, byref bat as BattleState, bslot() as BattleSprite, formdata as Formation)
DECLARE SUB battle_attack_do_inflict(byval targ as integer, byval tcount as integer, byref attack as AttackData, byref bat as BattleState, bslot() as BattleSprite, formdata as Formation)
DECLARE SUB battle_pause ()
DECLARE SUB battle_debug_menu (bat as BattleState, bslot() as BattleSprite, formdata as Formation)
DECLARE SUB check_battle_debug_keys (bat as BattleState, bslot() as BattleSprite, formdata as Formation)
DECLARE SUB dump_bslot(bat as BattleState, bslot() as BattleSprite, formdata as Formation)
DECLARE SUB battle_cleanup(byref bat as BattleState, bslot() as BattleSprite, battle_result as bool)
DECLARE SUB battle_init(byref bat as BattleState, bslot() as BattleSprite)
DECLARE SUB battle_background_anim(byref bat as BattleState, formdata as Formation)
DECLARE FUNCTION battle_run_away(byref bat as BattleState, bslot() as BattleSprite) as bool
DECLARE SUB battle_animate_running_away (bslot() as BattleSprite)
DECLARE SUB battle_check_delays(byref bat as BattleState, bslot() as BattleSprite)
DECLARE SUB battle_check_for_player_turns(byref bat as BattleState, bslot() as BattleSprite)
DECLARE FUNCTION battle_check_a_player_turn(byref bat as BattleState, bslot() as BattleSprite, byval index as integer) as bool
DECLARE SUB battle_check_for_enemy_turns(byref bat as BattleState, bslot() as BattleSprite)
DECLARE FUNCTION battle_check_an_enemy_turn(byref bat as BattleState, bslot() as BattleSprite, byval index as integer) as bool
DECLARE SUB battle_attack_cancel_target_attack(byval targ as integer, byref bat as BattleState, bslot() as BattleSprite, byref attack as AttackData)
DECLARE FUNCTION check_has_remaining_targets(bat as BattleState, bslot() as BattleSprite, targs() as integer) as bool
DECLARE SUB battle_reevaluate_dead_targets (byval deadguy as integer, byref bat as BattleState, bslot() as BattleSprite)
DECLARE SUB battle_sort_away_dead_t_target(byval deadguy as integer, t() as integer)
DECLARE SUB battle_counterattacks(bat as BattleState, byval h as integer, byval targstat as integer, byval who as integer, attack as AttackData, bslot() as BattleSprite, result as AttackResult)
DECLARE SUB show_first_battle_timer (page as integer)
DECLARE FUNCTION has_queued_attacks(byval who as integer) as bool
DECLARE FUNCTION pending_attacks_for_this_turn(bat as BattleState, bslot() as BattleSprite) as bool
DECLARE SUB decrement_attack_queue_delays(bslot() as BattleSprite)
DECLARE SUB turn_mode_decrement_attack_queue_delays(bslot() as BattleSprite)
DECLARE SUB ready_all_valid_units(bslot() as BattleSprite, formdata as Formation)
DECLARE SUB active_mode_state_machine (bat as BattleState, bslot() as BattleSprite, formdata as Formation)
DECLARE SUB turn_mode_state_machine (bat as BattleState, bslot() as BattleSprite, formdata as Formation)
DECLARE SUB do_poison(byval who as integer, bat as BattleState, bslot() as BattleSprite, formdata as Formation)
DECLARE SUB do_regen(byval who as integer, bat as BattleState, bslot() as BattleSprite, formdata as Formation)
DECLARE SUB start_next_turn (bat as BattleState, bslot() as BattleSprite, formdata as Formation)
DECLARE SUB trigger_turnmode_poison_and_regen (bat as BattleState, bslot() as BattleSprite, formdata as Formation)
DECLARE SUB calc_initiative_order (bslot() as BattleSprite, formdata as Formation)
DECLARE SUB apply_initiative_order (bslot() as BattleSprite)
DECLARE SUB turn_mode_time_passage (bat as BattleState, bslot() as battleSprite)
DECLARE FUNCTION hero_or_enemy_can_take_a_turn (byval who as integer, bat as BattleState, bslot() as BattleSprite) as bool
DECLARE SUB cancel_blocking_attacks_for_hero_or_enemy(byval who as integer)
DECLARE SUB update_turn_delays_in_attack_queue (byval who as integer)
DECLARE FUNCTION has_blocking_turn_delayed_attacks(byval who as integer) as bool
DECLARE SUB update_battle_menu (bspr as BattleSprite, bslot() as BattleSprite, bat as BattleState, st() as HeroDef)
DECLARE SUB init_spell_menu (bat as BattleState, bslot() as BattleSprite, st() as HeroDef)
DECLARE FUNCTION enemy_is_weak(byref bspr as BattleSprite) as bool
DECLARE FUNCTION check_for_unhittable_invisible_foe(attacker as integer, target as integer, byref attack as AttackData, byref bat as BattleState, bslot() as BattleSprite) as bool
DECLARE FUNCTION is_among_targets(byval slot as integer, t() as integer) as bool
DECLARE FUNCTION should_victory_advance(byref bat as BattleState) as bool
DECLARE SUB append_enemy_attacks_to_battle_menu(ai_attack_list() as integer, byref bspr as BattleSprite)
DECLARE FUNCTION random_attack_from_spell_menu(bat as BattleState, bspr as BattleSprite, spell_list_id as integer, st() as HeroDef, respect_costs as bool=YES, byval auto_battle as bool=NO) as integer
DECLARE FUNCTION spell_menu_is_usable(bat as BattleState, bspr as BattleSprite, spell_list_id as integer, st() as HeroDef, respect_costs as bool=YES) as bool
DECLARE FUNCTION find_attack_in_spell_menu(byval seek_atk_id as integer, bat as BattleState, bspr as BattleSprite, byval spell_list_id as integer, st() as HeroDef, byval respect_costs as bool) as integer
DECLARE FUNCTION items_menu_is_usable(bspr as BattleSprite) as bool
DECLARE SUB remove_auto_added_skip_turn_from_battle_menu (bspr as BattleSprite)
DECLARE SUB auto_add_skip_turn_to_battle_menu (bspr as BattleSprite)
DECLARE SUB create_default_battle_ui_layer(parent as Slice ptr)
DECLARE FUNCTION create_default_battle_ready_area() as Slice Ptr
DECLARE FUNCTION create_default_battle_hp_area() as Slice Ptr
DECLARE FUNCTION create_default_battle_caption_area () as Slice Ptr
DECLARE FUNCTION create_default_player_turn_overlay_area () as Slice Ptr
DECLARE FUNCTION create_default_targetting_overlay_area () as Slice Ptr
DECLARE FUNCTION create_default_targ_hover_overlay_area () as Slice Ptr
DECLARE FUNCTION create_default_battle_menu_area() as Slice Ptr
DECLARE FUNCTION create_default_battle_submenus_area() as Slice Ptr
DECLARE SUB update_battle_hero_or_enemy_info_slice(sl as Slice Ptr, bat as BattleState, bspr as BattleSprite)
DECLARE SUB update_indicator_slice (sl as Slice Ptr, lookup_code as integer, active as bool, indicator_string as string="", sort_index as integer=0)
DECLARE SUB update_battle_toggle_bounce (bat as BattleState, sl as Slice Ptr)
DECLARE SUB update_battle_targetting_slices (layer as Slice Ptr, bat as BattleState, bspr as BattleSprite)
DECLARE SUB instantiate_battle_targetting_slices (layer as Slice Ptr)
DECLARE SUB update_battle_targ_hover_slices (layer as Slice Ptr, bat as BattleState, bspr as BattleSprite)
DECLARE SUB instantiate_battle_targ_hover_slices (layer as Slice Ptr)
DECLARE SUB ExpandTextBattleSprite (code as string, result as string, byval arg0 as ANY ptr=0, byval arg1 as ANY ptr=0, byval arg2 as ANY ptr=0)
DECLARE SUB ExpandTextBattleState (code as string, result as string, byval arg0 as ANY ptr=0, byval arg1 as ANY ptr=0, byval arg2 as ANY ptr=0)
DECLARE SUB force_retargetting_for_attacker (byref bat as BattleState, bslot() as BattleSprite, index as integer)
DECLARE SUB create_battle_item_menu (bat as BattleState)


CONST ANIM_SKIP_SENTINEL = &h6EEFDEEF

'This should be larger than the attack queue length
CONST ATK_DELAY_MULT = 1000

'these are the battle global variables
DIM bstackstart as integer
REDIM atkq(15) as AttackQueue
DIM battl as BattleState ptr  ' Equal to @bat

FUNCTION battle (byval form as integer) as bool
 DIM result as bool  'Return value: whether victorious

 script_log_out !"\nFighting battle (formation " & form & ")"
 
 gam.pad.in_battle = YES
 remap_virtual_gamepad "virtual_gamepad_battle"
 set_animation_framerate 55

 DIM formdata as Formation
 DIM attack as AttackData
 DIM st(3) as HeroDef
 DIM bat as BattleState
 battl = @bat
 clear_attack_queue()
 DIM bslot(24) as BattleSprite

 bat.test_future = NO
 bat.test_view_mode = 0

 SELECT CASE gen(genBattleMode)
  CASE 0:
   bat.turn.mode = turnACTIVE
  CASE 1:
   bat.turn.mode = turnTURN
  CASE ELSE:
   debug "WARNING: invalid gen(genBattleMode) " & gen(genBattleMode) & " resorting to turnACTIVE"
   bat.turn.mode = turnACTIVE
 END SELECT

 '--lastformation is a global
 lastformation = form

 battle_init bat, bslot()

 'fade to near white
 fadeout uilook(uiFadeoutEnterBattle)
 queue_fade_in

 battle_loadall form, bat, bslot(), st(), formdata
 
 create_battle_item_menu bat

 '--main battle loop
 setkeys
 DO
  setwait 55  'Adjusting framerate in-battle is not implemented
  #IFNDEF NO_TEST_GAME
   IF running_under_Custom THEN try_to_reload_files_inbattle
  #ENDIF

  main_timer.substart TimerIDs.IOBackend
  setkeys
  main_timer.substop TimerIDs.IOBackend

  bat.tog XOR= 1
  playtimer

  '--background animation
  battle_background_anim bat, formdata

  IF always_enable_debug_keys <> 0 OR prefbit(8) = NO THEN '"Disable debugging keys" off
   check_battle_debug_keys bat, bslot(), formdata

   '--This will be disabled when it is no longer needed. If Ctrl/Shift+F10 is needed
   '--for something more important don't hesitate to claim it
   IF (keyval(scSHIFT) > 0 OR keyval(scCTRL) > 0) AND keyval(scF10) > 1 THEN
    loopvar bat.test_view_mode, 0, 2
    SELECT CASE bat.test_view_mode
     CASE 0: bat.test_future = NO : notification "bat.test_view_mode Classic"
     CASE 1: notification "bat.test_view_mode Flicker"
     CASE 2: bat.test_future = YES : notification "bat.test_view_mode Future"
    END SELECT
   END IF
  END IF
  
  IF keyval(scPause) > 1 THEN battle_pause
  IF battle_run_away(bat, bslot()) THEN
   result = NO
   EXIT DO
  END IF
  
  '--An attack should happen, prepare its animation
  IF bat.atk.id >= 0 AND bat.anim_ready = NO AND bat.vic.state = vicNONE THEN
   generate_atkscript attack, bat, bslot(), bat.anim_t()
  END IF
  
  '--Playback the current attack animation
  IF bat.atk.id >= 0 AND bat.anim_ready = YES AND bat.vic.state = vicNONE AND bat.away = 0 THEN
   battle_attack_anim_playback attack, bat, bslot(), formdata
  END IF
  
  '--Apply more generic animation effects. These are often triggered by
  '--battle_attack_anim_playback() but do not have to be
  battle_animate bat, bslot(), st()
  
  SELECT CASE bat.turn.mode
   CASE turnACTIVE: active_mode_state_machine bat, bslot(), formdata
   CASE turnTURN: turn_mode_state_machine bat, bslot(), formdata
  END SELECT
  
  IF bat.vic.state = vicNONE THEN
   IF bat.enemy_turn >= 0 THEN
    IF bslot(bat.enemy_turn).under_player_control AND is_enemy(bat.enemy_turn) THEN
     IF bat.targ.mode = targNONE ANDALSO bat.player_turn = -1 THEN
      bat.player_turn = bat.enemy_turn
      'Regenerate the enemy menu, in case it has changed
      populate_battle_menu_menudef_for_enemy bat, bslot(), bslot(bat.enemy_turn)
     END IF
    ELSE
     enemy_ai bat, bslot(), formdata
    END IF
   END IF
   IF bat.player_turn >= 0 ANDALSO is_hero(bat.player_turn) ANDALSO bslot(bat.player_turn).under_player_control = NO THEN
    update_battle_menu bslot(bat.player_turn), bslot(), bat, st()
    hero_ai bat, bslot(bat.player_turn), bat.player_turn, bslot(), st()
    bat.player_turn = -1
   END IF
   IF bat.player_turn >= 0 AND bat.targ.mode = targNONE THEN
    IF bat.menu_mode = batMENUITEM  THEN itemmenu bat, bslot()
    IF bat.menu_mode = batMENUSPELL THEN spellmenu bat, st(), bslot()
    IF bat.menu_mode = batMENUHERO  THEN
     update_battle_menu bslot(bat.player_turn), bslot(), bat, st()
     heromenu bat, bslot(), st()
    END IF
   END IF
   IF bat.player_turn >= 0 AND bat.targ.mode > targNONE THEN battle_targetting bat, bslot()
  END IF
 
  IF bat.test_view_mode = 1 THEN bat.test_future = NOT bat.test_future
 

  '-------------------- Draw the screen --------------------

  'Slices, the battle menu and debug displays are drawn to vpage, while for now
  'we continue to draw the unslicified submenus to a compatpage: uipage.
  'Most slicified UI is parented to SL_BATTLE_UI_LAYER, which covers the same
  'part of the screen as uipage (and it clips too).
  'Likewise viewpage covers the same area as bat.view_sl.

  'compatpage produces a view onto vpage, but vpage might change, so recreate every tick
  DIM as integer uipage, viewpage
  uipage = compatpage()
  IF prefbit(56) THEN  '"!Battles display at 320x200" off
   viewpage = vpage
  ELSE
   viewpage = uipage
  END IF

  IF gam.debug_timings THEN main_timer.switch TimerIDs.UpdateSlices

  update_battle_slices bat, bslot()

  'Clears the screen
  draw_timing_root_slice bat.root_sl, vpage

  IF gam.debug_timings THEN main_timer.switch TimerIDs.DrawOther

  'Not converted to slices yet
  draw_damage_text bslot(), bat.battlefield_sl, vpage

  '--Display toplevel battle (drawn to vpage, not uipage), item, spells menus
  battle_display_menus bat, bslot(), st(), uipage

  IF bat.vic.state = vicEXITDELAY THEN bat.vic.state = vicEXIT
  IF bat.vic.state > vicNONE THEN
   IF gen(genSkipBattleRewardsTicks) = -1 THEN
    bat.vic.state = vicEXITDELAY
   ELSE
    show_victory bat, bslot(), uipage
   END IF
  END IF

  IF bat.death_mode = deathENEMIES AND bat.vic.state = vicNONE THEN
   IF count_dissolving_enemies(bslot()) = 0 THEN trigger_victory bat, bslot()
  END IF
  
  IF bat.vic.state = vicEXIT THEN 'Victory
   result = YES
   EXIT DO
  END IF
  IF bat.death_mode = deathHEROES THEN
   IF formdata.death_action = 0 THEN
    'normal game over/trigger death script
    fatal = YES
   ELSEIF formdata.death_action = -1 THEN
    'continue game
   END IF
   result = NO
   EXIT DO
  END IF

  ' Display CANNOT RUN message (attack captions are drawn in battle_display_menus!)
  IF bat.alert_ticks > 0 THEN
   bat.alert_ticks -= 1
   centerfuz rCenter, rBottom - 10, textwidth(bat.alert) + 16, 16, 3, viewpage
   edgeprint bat.alert, pCentered, rBottom - 15, uilook(uiSelectedItem + bat.tog), viewpage
  END IF
 
  IF dotimerbattle THEN
   result = NO
   EXIT DO
  END IF

  show_first_battle_timer(viewpage)

  'Debug displays
  IF gam.debug_timings THEN main_timer.switch TimerIDs.DrawDebug
  IF gam.debug_showtags THEN tagdisplay vpage
  IF bat.debug_show_info = 1 THEN
   show_enemy_meters bat, bslot(), formdata, vpage
  ELSEIF bat.debug_show_info = 2 THEN
   display_attack_queue bslot()
  END IF
  IF gam.debug_timings THEN
   'main_timer.finish_timestep/begin_timestep called inside (switch to TimerIDs.Default)
   gam.cpu_usage_mode.display vpage
   main_timer.switch TimerIDs.Default
  END IF

  freepage uipage
  main_timer.substart(TimerIDs.UpdateScreen)
  setvispage vpage
  main_timer.substop(TimerIDs.UpdateScreen)
  check_for_queued_fade_in
  bat.ticks += 1
  dowait
 LOOP

 battle_cleanup bat, bslot(), result
 evalherotags
 evalitemtags
 IF formdata.victory_tag THEN
  settag formdata.victory_tag, result
 END IF
 tag_updates
 set_animation_framerate gen(genMillisecPerFrame)
 gam.pad.in_battle = NO
 remap_virtual_gamepad "virtual_gamepad"
 setkeys  'This only seems to matter in some contexts, such as triggered from a textbox
 RETURN result
END FUNCTION

'Note: most initialisation code is actually in battle_loadall.
SUB battle_init(byref bat as BattleState, bslot() as BattleSprite)

 '--prepare stack
 bstackstart = stackpos

 'Remember the music that was playing on the map so that the prepare_map() sub can restart it later
 gam.remembermusic = presentsong

 '--Init BattleState
 reset_battle_state bat

 '--Init BattleSprites
 FOR i as integer = 0 TO 11
  bslot(i).index = i
  bslot(i).consume_item = -1
  bslot(i).consume_lmp = -1
  bslot(i).revenge = -1
  bslot(i).thankvenge = -1
  bslot(i).counter_target = -1
  bslot(i).harm.col = uilook(uiBattleDamage)
  '--init affliction registers
  '--it should be clear by the fact that BattleStats is a separate type that
  '--that bslot().stat inside battle is not the same as gam.hero().stat outside battle
  WITH bslot(i).stat.cur
   .poison = 1000
   .regen  = 1000
   .stun   = 1000
   .mute   = 1000
  END WITH
  WITH bslot(i).stat.max
   .poison = 1000
   .regen  = 1000
   .stun   = 1000
   .mute   = 1000
  END WITH
  bslot(i).poison_repeat = randint(2000)
  bslot(i).regen_repeat = randint(2000)
 NEXT i

 '--sanity-check affliction indicators and set defaults for old games that don't have them yet. 
 IF gen(genPoisonChar) <= 0 THEN gen(genPoisonChar) = 161
 IF gen(genStunChar) <= 0 THEN gen(genStunChar) = 159
 IF gen(genMuteChar) <= 0 THEN gen(genMuteChar) = 163
 IF gen(genRegenChar) <= 0 THEN gen(genRegenChar) = 32
END SUB

SUB battle_cleanup(byref bat as BattleState, bslot() as BattleSprite, battle_result as bool)
 export_battle_hero_stats bslot()

 '--overflow checking for the battle stack
 IF stackpos() - bstackstart > 0 THEN
  '--an overflow is not unusual. This happens if the battle terminates
  '--while an attack is still going on
  DIM dummy as integer
  WHILE stackpos > bstackstart: dummy = popdw: WEND
 END IF

 '--underflow checking
 IF stackpos() - bstackstart < 0 THEN
  '--an underflow is bad. It used to mean that whatever script was on
  '--the top of the stack has been corrupted, but now scripts don't use this stack
  '--but an underflow is still bad in principle.
  showbug "bstack underflow " & stackpos & " " & bstackstart
 END IF

 IF fatal THEN
  fadeout uilook(uiFadeoutDeath)
 ELSEIF battle_result THEN
  fadeout uilook(uiFadeoutWonBattle)
 ELSE
  fadeout uilook(uiFadeoutExitBattle)
 END IF
 DeleteSlice @bat.root_sl
END SUB

SUB battle_pause ()
 DIM pause as string = readglobalstring(54, "PAUSE", 10)
 fuzzyrect 0, 0, , , boxlook(0).bgcol, vpage
 edgeprint pause, pCentered, pCentered, uilook(uiText), vpage
 setvispage vpage
 waitforanykey
END SUB

SUB battle_attack_anim_playback (byref attack as AttackData, byref bat as BattleState, bslot() as BattleSprite, formdata as Formation)
 '--this plays back the animation sequence built when the attack starts.

 DIM i as integer

 '--In future we will handle slice animations here. For now, only the flinch animation
 '--has been partially split out, and isn't a slice animation yet.
 FOR i = 0 TO UBOUND(bslot)
  IF bslot(i).flinch_anim THEN
   flinch_anim_eachtick i, bslot()
  END IF
 NEXT

 '--decrement the animation wait ticks, and don't proceed until they are zero
 IF bat.wait_frames > 0 THEN
  bat.wait_frames -= 1
  IF bat.wait_frames > 0 THEN EXIT SUB
 END IF

 '--anim_waitforall called? We are waiting for all motion to stop
 IF bat.wait_frames = -1 THEN
  FOR i = 0 TO UBOUND(bslot)
   'NULL slices are OK
   IF SliceIsMoving(bslot(i).sl) ORELSE SliceIsMoving(bslot(i).sprite) THEN EXIT SUB
  NEXT i
 END IF
 bat.wait_frames = 0

 DIM act as integer
 
 '--these are used to temporarily store "who" arguments
 DIM ww as integer
 DIM w1 as integer
 DIM w2 as integer
 DIM ticks as integer
 DIM speed as XYPair

 DO: 'INTERPRET THE ANIMATION SCRIPT

  '--anim_waitformove called? Waiting for a specific slot
  '--Unlike anim_waitforall, check this inside the DO so that minimum wait is 0 ticks not 1.
  IF bat.wait_for_slot <> -1 THEN
   WITH bslot(bat.wait_for_slot)
    IF SliceIsMoving(.sl) ORELSE SliceIsMoving(.sprite) THEN EXIT DO
   END WITH
   bat.wait_for_slot = -1
  END IF

  act = popdw
  SELECT CASE act
   CASE 0 '--end()
    'Wait for .sl slice moves to finish (but don't need to wait for .sprite slices). If we reset positions
    'before the movement is finished, it would continue moving and end up in the wrong position.
    'BTW, resetting to initial positions is probably entirely unnecessary. I think it was needed
    'before r13666 & r13667 switched to slice velocity, fixing some possible rounding errors.
    FOR i = 0 TO UBOUND(bslot)
     'NULL slices are OK
     IF SliceIsMoving(bslot(i).sl) THEN
      'Re-push anim_end
      pushdw 0
      EXIT DO
     END IF
    NEXT
    FOR i = 0 TO 3
     enforce_weak_picture i, bslot(), bat
     '--re-enforce party's X/Y positions...
     (bslot(i).pos) = bslot(i).basepos
    NEXT i
    FOR i = 0 TO 7
     IF bslot(4 + i).fleeing = NO THEN
      (bslot(4 + i).pos) = bslot(4 + i).basepos
     END IF
    NEXT i
    'Stops animation playback instantly
    bat.atk.id = -1
   CASE 1
    'unused
   CASE 2 'velocity(who,xstep,ystep,ticks)
    ww = popdw
    speed.x = popdw
    speed.y = popdw
    ticks = popdw
    bslot(ww).set_vel_x speed.x, ticks
    bslot(ww).set_vel_y speed.y, ticks
   CASE 3 'setpos(who,x,y,d)
    ww = popdw
    bslot(ww).x = popdw
    bslot(ww).y = popdw
    bslot(ww).d = popdw
   CASE 4
    'unused
   CASE 5 'appear(who)
    ww = popdw
    bslot(ww).vis = YES
   CASE 6 'disappear(who)
    ww = popdw
    bslot(ww).vis = NO
   CASE 7 'setframe(who,frame)
    ww = popdw
    DIM fr as integer = popdw
    bslot(ww).frame = fr
    IF is_hero(ww) THEN bslot(ww).walk = 0
   CASE 8 'absmove(who,x,y,ticks)
    ww = popdw
    DIM destpos as XYPair
    destpos.x = popdw
    destpos.y = popdw
    ticks = popdw
    'bslot(ww).set_vel_x (destpos.x - bslot(ww).x) / ticks, ticks
    'bslot(ww).set_vel_y (destpos.y - bslot(ww).y) / ticks, ticks
    IF bslot(ww).sl THEN
     SetSliceTarg bslot(ww).sl, destpos.x, destpos.y, ticks
    END IF
   CASE 9 'waitforall()
    'Breaks the DO, check next tick
    bat.wait_frames = -1
   CASE 10 'inflict(targ, target_count)
    DIM targ as integer = popdw
    DIM tcount as integer = popdw
    battle_attack_do_inflict targ, tcount, attack, bat, bslot(), formdata
   CASE 11 'setz(who,z)
    ww = popdw
    bslot(ww).z = popdw
   CASE 12
    'unused
   CASE 13 'wait(ticks)
    bat.wait_frames = popdw
   CASE 14 'walktoggle(who)
    ww = popdw
    bslot(ww).frame = frameSTAND
    IF is_hero(ww) THEN bslot(ww).walk XOR= 1
   CASE 15 'zvelocity(who,zstep,zticks)
    ww = popdw
    DIM as integer zspeed = popdw
    ticks = popdw
    bslot(ww).set_vel_z zspeed, ticks
   CASE 16 'sound(which)
    DIM soundnum as integer = popdw
    stopsfx(soundnum)  'Restart the sound effect
    playsfx(soundnum)
   CASE 17 'align(who, target, edge, offset)
    w1 = popdw
    w2 = popdw
    select case popdw 'which edge?
    case dirUp
     bslot(w1).y = bslot(w2).y + popdw
    case dirDown
     bslot(w1).y = bslot(w2).y + bslot(w2).h - bslot(w1).h + popdw
    case dirLeft
     bslot(w1).x = bslot(w2).x + popdw
    case dirRight
     bslot(w1).x = bslot(w2).x + bslot(w2).w - bslot(w1).w + popdw
    end select
   CASE 18 'setcenter(who, target, offx, offy)
    w1 = popdw
    w2 = popdw
    bslot(w1).x = (bslot(w2).w - bslot(w1).w) / 2 + bslot(w2).x + popdw
    bslot(w1).y = (bslot(w2).h - bslot(w1).h) / 2 + bslot(w2).y + popdw
  CASE 19 'align2(who, target, edgex, edgey, offx, offy)
   w1 = popdw
   w2 = popdw
   DIM xd as integer = popdw
   DIM yd as integer = popdw
   if xd then
    bslot(w1).x = bslot(w2).x + bslot(w2).w - bslot(w1).w + popdw
   else
    bslot(w1).x = bslot(w2).x + popdw
   end if
   if yd then
    bslot(w1).y = bslot(w2).y + bslot(w2).h - bslot(w1).h + popdw
   else
    bslot(w1).y = bslot(w2).y + popdw
   end if
  CASE 20 'relmove(who, x, y, ticks)
   ww = popdw  'who
   DIM movedist as XYPair
   movedist.x = popdw
   movedist.y = popdw
   DIM ticks as integer = popdw
   WITH bslot(ww)
    IF .sl THEN
     SetSliceTarg .sl, .x + movedist.x, .y + movedist.y, ticks
    END IF
   END WITH
  CASE 21 'setdir(who, d)
   ww = popdw
   DIM newdir as integer = popdw
   bslot(ww).d = newdir
  CASE 22 'anim_skip_ahead_if_targetless
   'For multihit attacks, skip rest of hits if no targets left.
   'The flaw is that dead-and-dissolved targets are still hit until the
   'whole attack is cancelled.
   IF check_has_remaining_targets(bat, bslot(), bat.anim_t()) THEN CONTINUE DO
   'Skip ahead, looking for command 23. We are looking for a 10 byte pattern
   DO
    IF stackpos <= 0 THEN
     debugc errBug, "anim_skip_ahead_if_targetless: couldn't find point to skip to"
     EXIT DO
    END IF
    DIM word as integer = popdw
    IF word = 23 AND readstackdw(-1) = ANIM_SKIP_SENTINEL AND readstackdw(-2) = ANIM_SKIP_SENTINEL THEN
     popdw
     popdw
     EXIT DO
    END IF
   LOOP
  CASE 23 'anim_skip_to_here
   'Pop the sentinel markers
   popdw
   popdw
  CASE 24 'anim_abszmove(who,z,zticks)
   ww = popdw
   DIM destz as integer = popdw
   ticks = popdw
   'bslot(ww).set_vel_z (destz - bslot(ww).z) / ticks, ticks
   IF bslot(ww).sprite THEN
    SetSliceTarg bslot(ww).sprite, 0, -destz, ticks
   END IF
  CASE 25 'anim_checkpoint
    IF autotestmode THEN write_checkpoint
  CASE 26 'hide(who)
    ww = popdw
    bslot(ww).hidden = YES
  CASE 27 'unhide(who)
    ww = popdw
    bslot(ww).hidden = NO
  CASE 28 'flinch(who)
    'The start of the flinch is setup by anim_flinchstart.
    'This command queues the end of the flinch 3 ticks later
    ww = popdw
    bslot(ww).flinch_anim = 6
  CASE 29 'waitformove(who)
    bat.wait_for_slot = popdw

  END SELECT
 LOOP UNTIL bat.wait_frames <> 0 ORELSE bat.atk.id = -1

 IF bat.atk.id = -1 THEN
  battle_attack_anim_cleanup attack, bat, bslot(), formdata
 END IF
END SUB

SUB battle_attack_do_inflict(byval targ as integer, byval tcount as integer, byref attack as AttackData, byref bat as BattleState, bslot() as BattleSprite, formdata as Formation)
 'targ is the target slot number
 'tcount is the total number of targets (used only for dividing spread damage)

 DIM i as integer
 'set tag, if there is one
 checkAtkTagConds attack, atktagOnUse
 
 '--attempt inflict the damage to the target
 DIM h as integer = 0 '--set inside inflict
 DIM targstat as integer = 0 '--set inside inflict
 DIM result as AttackResult
 result = inflict(h, targstat, bat.acting, targ, bslot(bat.acting), bslot(targ), attack, tcount)
 IF result = atkHit THEN
  '--attack succeeded
  IF attack.transmog.enemy >= 0 ANDALSO is_enemy(targ) THEN
   changefoe bat, targ - 4, attack.transmog, formdata, bslot()
  END IF
  battle_attack_cancel_target_attack targ, bat, bslot(), attack
  WITH bslot(targ).enemy.reward
   IF attack.erase_rewards = YES THEN
    .gold = 0
    .exper = 0
    .item_rate = 0
    .rare_item_rate = 0
   END IF
  END WITH
  IF attack.force_run = YES THEN
  'force heroes to run away
   IF checkNoRunBit(bslot()) THEN
    bat.alert = bat.cannot_run_caption
    bat.alert_ticks = 10
   ELSE
    bat.away = 1
   END IF
  ELSEIF attack.force_victory = YES THEN
   trigger_victory bat, bslot()
  ELSEIF attack.force_battle_exit = YES THEN
   bat.away = 11
  END IF
  checkAtkTagConds attack, atktagOnHit
  IF bslot(targ).stat.cur.hp <= 0 THEN
   checkAtkTagConds attack, atktagOnKill
  END IF

  IF trytheft(bat, bat.acting, targ, attack, bslot()) THEN
   IF is_hero(bat.player_turn) THEN
    checkitemusability bat.iuse(), bslot(), bat.player_turn
   END IF
  END IF
  
  SELECT CASE attack.change_control
   CASE 1: 'Player control
    bslot(targ).under_player_control = YES
   CASE 2: 'Auto control
    bslot(targ).under_player_control = NO
   CASE 3: 'Set to default
    IF is_hero(targ) THEN
     bslot(targ).under_player_control = NOT gam.hero(targ).auto_battle
    ELSEIF is_enemy(targ) THEN
     bslot(targ).under_player_control = bslot(targ).enemy.controlled_by_player
    END IF
  END SELECT

  DIM oldval as bool = bslot(targ).turncoat_attacker
  SELECT CASE attack.change_turncoat
   CASE 1: 'Turncoat
    bslot(targ).turncoat_attacker = YES
   CASE 2: 'Normal
    bslot(targ).turncoat_attacker = NO
   CASE 3: 'Set to default
    IF is_hero(targ) THEN
     bslot(targ).turncoat_attacker = NO
    ELSEIF is_enemy(targ) THEN
     bslot(targ).turncoat_attacker = bslot(targ).enemy.turncoat_attacker
    END IF
  END SELECT
  IF bslot(targ).turncoat_attacker <> oldval THEN
   'Turncoat status has changed, force a retarget of any current commands
   force_retargetting_for_attacker bat, bslot(), targ
  END IF

  SELECT CASE attack.change_defector
   CASE 1: 'Defector
    bslot(targ).defector_target = YES
   CASE 2: 'Normal
    bslot(targ).defector_target = NO
   CASE 3: 'Set to default
    IF is_hero(targ) THEN
     bslot(targ).defector_target = NO
    ELSEIF is_enemy(targ) THEN
     bslot(targ).defector_target = bslot(targ).enemy.defector_target
    END IF
  END SELECT

  SELECT CASE attack.change_flipped
   CASE 1: 'Flipped
    bslot(targ).flipped = YES
   CASE 2: 'Not flipped
    bslot(targ).flipped = NO
   CASE 3: 'Set to default
    IF is_hero(targ) THEN
     bslot(targ).flipped = NO
    ELSEIF is_enemy(targ) THEN
     bslot(targ).flipped = NO
    END IF
  END SELECT
  
 ELSE
  'Miss or Fail
  IF result = atkMiss ANDALSO attack.dont_display_miss = NO THEN
   bslot(targ).harm.text = readglobalstring(120, "miss", 20)
  ELSEIF result = atkFail ANDALSO attack.dont_display_fail = NO THEN
   bslot(targ).harm.text = readglobalstring(122, "fail", 20)
  END IF

  checkAtkTagConds attack, atktagOnMiss
 END IF
 triggerfade targ, bslot()
 IF bslot(targ).stat.cur.hp > 0 THEN
  '---REVIVE---
  bslot(targ).vis = YES
  bslot(targ).dissolve = 0
 END IF
 IF is_enemy(targ) AND attack.no_spawn_on_attack = NO THEN battle_spawn_on_enemy_hit targ, bat, bslot(), formdata
 battle_spawn_on_first_hit attack, bat, bslot(), formdata
 battle_counterattacks bat, h, targstat, targ, attack, bslot(), result

 IF bat.atk.has_consumed_costs = NO THEN
  subtract_attack_costs attack, bat.acting, bat, bslot()

  'Was the attack triggered by an item? That's outside normal attack costs
  WITH bslot(bat.acting)
   IF .consume_item >= 0 THEN
    IF consumeitem(.consume_item) THEN
     'Finished the item stack
     setbit bat.iuse(), 0, .consume_item, 0  'Update this in case Items menu happens to be open
     'Note that subtract_attack_costs already updated tags, so we only do it again in this case
     evalitemtags
     tag_updates
    END IF
    .consume_item = -1
   END IF
  END WITH

  '--set the flag to prevent re-consuming costs on multi-hit or multi-target attacks
  bat.atk.has_consumed_costs = YES
 END IF

 IF liveherocount(bslot()) = 0 THEN bat.atk.id = -1
 
END SUB

SUB battle_attack_anim_cleanup (byref attack as AttackData, byref bat as BattleState, bslot() as BattleSprite, formdata as Formation)

 bat.wait_frames = 0
 bat.wait_for_slot = -1
 bat.anim_ready = NO

 '--hide the caption when the animation is done
 IF attack.caption_time = 0 THEN
  '--clear duration-timed caption
  bat.caption_time = 0
  bat.caption_delay = 0
 END IF

 '--clean up .self_bequesting 
 ' (we don't have to clean up .bequesting here because it only stays on until the bequest attack is triggered)
 IF bat.acting >= 0 THEN
  WITH bslot(bat.acting)
   IF .self_bequesting THEN
    .self_bequesting = NO
    IF .stat.cur.hp > 0 THEN
     'Death was cancelled, so bequesting should be allowed to happen again later
     .already_bequested = NO
    END IF
   END IF
  END WITH
 ELSE
  debuginfo "cleaning up attack " & attack.name & " with no attacker set"
 END IF

 '--check to see if anybody is dead
 fulldeathcheck bat.atk.was_id, bat, bslot(), formdata
 
 '--FIXME: further cleanup to remove was_id entirely?
 bat.atk.was_id = -1

 '--clean up animation stack
 'DEBUG debug "discarding " & (stackpos - bstackstart) & " from stack"
 DIM dummy as integer
 WHILE stackpos > bstackstart: dummy = popdw: WEND
 
 '--spawn the next after-chained attack (if any)
 IF spawn_chained_attack(attack.chain, NO, attack, bat, bslot()) = NO THEN
  spawn_chained_attack(attack.elsechain, NO, attack, bat, bslot())
 END IF

END SUB

SUB battle_spawn_on_enemy_hit(byval targ as integer, byref bat as BattleState, bslot() as BattleSprite, formdata as Formation)
 DIM i as integer
 DIM j as integer
 DIM slot as integer

 'First check if the attack triggers any elemental spawning from the enemy
 WITH bslot(targ)
  '--non-elemental hit
  IF .enemy.spawn.non_elemental_hit > 0 AND bat.atk.non_elemental THEN
   FOR j = 1 TO .enemy.spawn.how_many
    slot = find_empty_enemy_slot(formdata)
    IF slot > -1 THEN
     formdata.slots(slot).id = .enemy.spawn.non_elemental_hit - 1
     loadfoe slot, formdata, bat, bslot()
    END IF
   NEXT j
  END IF
  '--check each different elemental
  FOR i = 0 TO gen(genNumElements) - 1
   IF .enemy.spawn.elemental_hit(i) > 0 AND bat.atk.elemental(i) THEN
    FOR j = 1 TO .enemy.spawn.how_many
     slot = find_empty_enemy_slot(formdata)
     IF slot > -1 THEN
      formdata.slots(slot).id = .enemy.spawn.elemental_hit(i) - 1
      loadfoe slot, formdata, bat, bslot()
     END IF
    NEXT j
    IF .enemy.spawn.all_elements_on_hit = NO THEN EXIT FOR  'Only first matching element
   END IF
  NEXT i
 END WITH
END SUB

SUB battle_spawn_on_first_hit(atk as AttackData, byref bat as BattleState, bslot() as BattleSprite, formdata as Formation)
 '--Finally check any see if the attack itself triggers any spawning
 IF bat.atk.has_spawned THEN EXIT SUB 'Only spawn once per attack
 DIM slot as integer
 IF atk.spawn_enemy THEN
  slot = find_empty_enemy_slot(formdata)
  IF slot > -1 THEN
   formdata.slots(slot).id = atk.spawn_enemy - 1
   loadfoe slot, formdata, bat, bslot()
   bat.most_recently_spawned_by_attack = 4 + slot
  END IF
 END IF
 bat.atk.has_spawned = YES 'This gets reset to NO when the next attack is loaded
END SUB

SUB battle_targetting(byref bat as BattleState, bslot() as BattleSprite)
 'Not just for heroes anymore!

 DIM do_cancel as bool = NO
 DIM do_pick as bool = NO

 IF bat.targ.mode = targSETUP THEN setup_targetting bat, bslot()

 IF bat.targ.mode = targAUTO THEN
  'battle_confirm_targets doesn't get called, so some logic from that needs to be duplicated here
  'NOTE: battle_confirm_targets should be updated to match, if this block is changed.
  IF bat.turn.mode = turnACTIVE THEN
   update_turn_delays_in_attack_queue bat.player_turn
  END IF
  autotarget bat.player_turn, bslot(bat.player_turn).attack - 1, bslot(), bat
  player_attack_targetting_done bat, bslot()
  EXIT SUB
 END IF
 
 'check to see if the attacker is still allowed to do the attack.
 'This may have changed since we started targetting because the attacker
 'might have been muted, or hit with an MP-damaging attack
 ' (FIXME: Does not attempt to re-check LMP)
 ' FIXME: We shouldn't be checking costs here in all cases, eg an item-triggered
 ' attack that doesn't use costs (bug #1161, #1098)
 IF NOT atkallowed(bat.targ.atk, bat.player_turn, 0, 0, bslot(bat.player_turn)) THEN
  battle_leave_targetting_mode bat, bslot()
  EXIT SUB
 END IF

 'We need to recalculate valid targs every tick in case we are in active mode
 'and do not pause when targetting
 get_valid_targs bat.targ.mask(), bat.player_turn, bat.targ.atk, bslot(), bat

 'no valid targs available
 IF targetmaskcount(bat.targ.mask()) = 0 THEN
  checkAtkTagConds bat.targ.atk, atktagOnTargettingFailed
  battle_leave_targetting_mode bat, bslot()
  EXIT SUB
 END IF

 'If the current target has become invalid, switch to the next valid one
 WHILE bat.targ.mask(bat.targ.pointer) = NO
  loopvar bat.targ.pointer, 0, 11
 WEND

 'random target
 IF bat.targ.roulette THEN
  'Advance cursor by 1 or 2 targets (this happens every tick)
  FOR i as integer = 0 TO randint(2)
   loopvar bat.targ.pointer, 0, 11
   WHILE bat.targ.mask(bat.targ.pointer) = NO
    loopvar bat.targ.pointer, 0, 11
   WEND
  NEXT i
 END IF

 'first target
 IF bat.targ.force_first THEN
  bat.targ.pointer = 0
  WHILE bat.targ.mask(bat.targ.pointer) = 0
   loopvar bat.targ.pointer, 0, 11
  WEND
 END IF

 'optional spread targetting
 IF bat.targ.opt_spread = 2 AND (carray(ccLeft) > 1 OR carray(ccRight) > 1) AND bat.targ.roulette = NO AND bat.targ.force_first = NO THEN
  menusound gen(genCursorSFX)
  FOR i as integer = 0 TO 11
   bat.targ.selected(i) = NO
  NEXT i
  bat.targ.opt_spread = 1
  clearkeys
 END IF
 
 'arrow keys to select
 IF bat.targ.interactive = YES AND bat.targ.opt_spread < 2 AND bat.targ.roulette = NO AND bat.targ.force_first = NO THEN
  IF carray(ccUp) > 1 THEN
   menusound gen(genCursorSFX)
   battle_target_arrows -1, 1, bslot(), bat.targ, NO
  END IF
  IF carray(ccDown) > 1 THEN
   menusound gen(genCursorSFX)
   battle_target_arrows 1, 1, bslot(), bat.targ, NO
  END IF
  IF carray(ccLeft) > 1 THEN
   menusound gen(genCursorSFX)
   battle_target_arrows -1, 0, bslot(), bat.targ, YES
  END IF
  IF carray(ccRight) > 1 THEN
   menusound gen(genCursorSFX)
   battle_target_arrows 1, 0, bslot(), bat.targ, YES
  END IF
 END IF

 bat.targ.hover = -1
 IF get_gen_bool("/mouse/mouse_battles") ANDALSO bat.targ.mode = targMANUAL THEN
  FOR i as integer = 0 TO 11
   WITH bslot(i)
    IF rect_collide_point(XYWH(.x, .y - .z, .w, .h), readmouse.pos) ANDALSO bslot(i).vis THEN
     bat.targ.hover = i
    END IF
   END WITH
  NEXT i
  WITH bat.targ
   IF .must_hover_valid_target THEN
    .must_hover_valid_target = NO
    IF .hover >= 0 ANDALSO .mask(.hover) = YES THEN
     'This target is okay
     IF .opt_spread = 0 ORELSE .opt_spread = 1 THEN
      .pointer = .hover
      FOR i as integer = 0 to 11: .selected(.hover) = NO : NEXT i  'FIXME
     END IF
    ELSE
     do_cancel = YES
    END IF
   END IF
   IF (readmouse.release AND mouseLeft) ANDALSO readmouse.drag_dist < 10 THEN
    IF .hover >= 0 THEN
     'Clicked on a battle slot
     IF .mask(.hover) = NO THEN
      'Not a valid target for this attack
      menusound gen(genCancelSFX)
     ELSEIF .hover = .pointer ORELSE .selected(.hover) = YES THEN
      'Clicked on an already-selected target
      do_pick = YES
     ELSEIF .opt_spread = 0 ORELSE .opt_spread = 1 THEN
      'Focus a specific target
      menusound gen(genCursorSFX)
      .pointer = .hover
      FOR i as integer = 0 to 11: .selected(.hover) = NO : NEXT i  'FIXME
     END IF
    ELSE
     'Clicked outside of any valid target
     do_cancel = YES
    END IF
   END IF
   IF (readmouse.dragging AND mouseLeft) ANDALSO readmouse.drag_dist > 20 THEN
    'A left-drag is happening
    IF .opt_spread = 1 ANDALSO .mouse_optional_spread = NO THEN
     'Toggling optional spread
     .mouse_optional_spread = YES
     DIM is_spread as bool = NO
     FOR i as integer = 0 to 11
      IF .selected(i) THEN is_spread = YES
     NEXT i
     IF is_spread THEN
      'unspread
      FOR i as integer = 0 to 11 : .selected(i) = NO : NEXT i
     ELSE
      'spread
      FOR i as integer = 0 to 11
       IF .mask(i) THEN .selected(i) = YES
      NEXT i
     END IF 
    END IF
   END IF
   IF (readmouse.dragging AND mouseLeft) = 0 THEN
    'No drag is happening
    .mouse_optional_spread = NO
   END IF
  END WITH
    
  IF (readmouse.release AND mouseRight) ANDALSO readmouse.drag_dist < 10 THEN do_cancel = YES
 END IF

 'cancel
 IF game_check_cancel_key() THEN do_cancel = YES
 IF do_cancel THEN
  battle_leave_targetting_mode bat, bslot()
  EXIT SUB
 END IF
 
 'confirm
 IF game_battle_check_use_key() THEN do_pick = YES
 IF do_pick THEN
  IF autotestmode THEN write_checkpoint
  menusound gen(genAcceptSFX)
  battle_confirm_target bat, bslot()
 END IF
 
END SUB

SUB battle_leave_targetting_mode(bat as BattleState, bslot() as BattleSprite)
  menusound gen(genCancelSFX)
  bslot(bat.player_turn).attack = 0
  bslot(bat.player_turn).consume_lmp = -1
  bat.targ.mode = targNONE
  clearkeys
END SUB

'Called when the player confirms targets for an attack
SUB battle_confirm_target(byref bat as BattleState, bslot() as BattleSprite)

 'Note: when changing this SUB you also need to update the 'bat.targ.mode = targAUTO' block above.

 IF bat.turn.mode = turnACTIVE THEN
  update_turn_delays_in_attack_queue bat.player_turn
 END IF

 bat.targ.selected(bat.targ.pointer) = YES

 DIM targs(11) as bool
 flusharray targs(), , YES
 DIM targnum as integer = 0
 FOR slot as integer = 0 TO 11
  IF bat.targ.selected(slot) THEN
   targs(targnum) = slot
   targnum += 1
  END IF
 NEXT slot
 queue_attack bslot(bat.player_turn).attack - 1, bat.player_turn, targs()

 player_attack_targetting_done bat, bslot()
END SUB

'This is called to cleanup after a hero attack has been targetted and put on atkq
SUB player_attack_targetting_done(byref bat as BattleState, bslot() as BattleSprite)
 IF bat.turn.mode = turnACTIVE THEN
  'For debug only
  bslot(bat.player_turn).active_turn_num += 1
  'debug "Hero " & bat.player_turn & " " & bslot(bat.player_turn).name & " turn #" & bslot(bat.player_turn).active_turn_num
 END IF

 bslot(bat.player_turn).attack = 0
 bslot(bat.player_turn).ready_meter = 0
 bslot(bat.player_turn).ready = NO
 IF bat.player_turn = bat.enemy_turn THEN bat.enemy_turn = 3
 bat.player_turn = -1
 bat.targ.mode = targNONE
 bat.targ.hit_dead = NO
END SUB

'Update HP or MP meter
'Cause meter to grow/shrink to the true value, and set col to the color to draw it.
SUB update_stat_meter(bat as BattleState, byref meter as double, length as integer, curstat as integer, maxstat as integer, uicol as integer, byref col as integer)
 DIM target_len as double = length * bound(curstat / large(maxstat, 1), 0., 1.)
 IF meter < target_len THEN meter = small(target_len, meter + 1 + (target_len - meter) / 15)
 IF meter > target_len THEN meter = large(target_len, meter - 1 - (meter - target_len) / 15)
 col = uiLook(uicol)
 IF meter = length ANDALSO curstat > maxstat THEN
  'Don't do any flickering if we might draw a transparent menu on top, it can look very bad
  col = uiLook(uicol + IIF(bat.menu_mode = batMENUHERO, bat.tog, 0))
 END IF
END SUB

SUB battle_display_menus (byref bat as BattleState, bslot() as BattleSprite, st() as HeroDef, page as integer)
 'display:
 'This sub historically draws the user-interface, and still handles some parts of it
 'TODO: It will be removed piece by piece
 'NOTE: draws to vpage as well as page
 DIM i as integer
 DIM col as integer
 DIM bgcol as integer
 IF bat.vic.state = vicNONE THEN 'only display interface till you win
  '--See also update_battle_ui_slices()
  '--more code from here will eventually be moved there

  '--Draw menus for a hero that is currently taking a turn
  IF bat.player_turn >= 0 THEN

   'Note that the main battle menu is drawn with slices using MenuDefSlice it is not drawn here
   IF bat.menu_mode = batMENUSPELL THEN '--draw spell menu
    edgeboxstyle 5, 6, 310, 95, 0, page
    IF bat.sptr < 24 THEN
     IF bat.spell.slot(bat.sptr).desc <> "" THEN rectangle 5, 74, 310, 1, boxlook(0).edgecol, page
    END IF
    rectangle 5, 87, 310, 1, boxlook(0).edgecol, page
    FOR i = 0 TO 23
     col = uilook(uiDisabledItem + bat.spell.slot(i).enable)
     bgcol = 0
     IF bat.sptr = i THEN
      col = uilook(uiSelectedDisabled - (2 * ABS(bat.spell.slot(i).enable)) + bat.tog)
      bgcol = uilook(uiHighlight)
     END IF
     IF bat.sptr_hover = i THEN
      col = mouse_hover_tinted_color(col)
     END IF
     textcolor col, bgcol
     printstr bat.spell.slot(i).name, 16 + (i MOD 3) * 104, 8 + (i \ 3) * 8, page
    NEXT i
    col = uilook(uiMenuItem)
    bgcol = 0
    IF bat.sptr = 24 THEN col = uilook(uiSelectedItem + bat.tog) : bgcol = uilook(uiHighlight)
    IF bat.sptr_hover = 24 THEN
     col = mouse_hover_tinted_color(col)
    END IF
    textcolor col, bgcol
    printstr bat.cancel_spell_caption, 9, 90, page
    textcolor uilook(uiDescription), 0
    IF bat.sptr < 24 THEN
     printstr bat.spell.slot(bat.sptr).desc, 9, 77, page
     DIM cost_caption as string = RIGHT(bat.spell.slot(bat.sptr).cost, 30)
     printstr cost_caption, 311 - LEN(cost_caption) * 8, 90, page
    END IF
   END IF
   IF bat.menu_mode = batMENUITEM THEN '--draw item menu
    IF bat.test_future THEN
     bat.itemst.ps.m->Visible = YES
    ELSE
    '--legacy items display
    DIM inv_height as integer = small(78, 8 + ((last_inv_slot() + 1) \ 3) * 8)
    WITH bat.inv_scroll
     .top = INT(bat.item.top / 3)
     .pt = INT(bat.item.pt / 3)
     .last = INT(last_inv_slot() / 3)
    END WITH
    bat.inv_scroll_rect.high = inv_height - 2
    edgeboxstyle 8, 4, 304, inv_height, 0, page
    draw_scrollbar bat.inv_scroll, bat.inv_scroll_rect, , page
    FOR i = bat.item.top TO small(bat.item.top + 26, last_inv_slot())
     if i < lbound(inventory) or i > ubound(inventory) then continue for
     col = uilook(uiDisabledItem - readbit(bat.iuse(), 0, i))
     bgcol = 0
     IF bat.item.pt = i THEN col = uilook(uiSelectedDisabled - (2 * readbit(bat.iuse(), 0, i)) + bat.tog) : bgcol = uilook(uiHighlight)
     IF bat.item.hover = i THEN col = mouse_hover_tinted_color(col)
     textcolor col, bgcol
     printstr inventory(i).text, 20 + 96 * (i MOD 3), 8 + 8 * ((i - bat.item.top) \ 3), page
    NEXT i
    edgeboxstyle 8, 4 + inv_height, 304, 16, 0, page
    textcolor uilook(uiDescription), 0
    printstr bat.item_desc, 12, 8 + inv_height, page
    END IF '--end legacy items display
   ELSE
    'Items menu is not displaying now
    bat.itemst.ps.m->Visible = NO
   END IF
  END IF
 END IF'--end if bat.vic.state = vicNONE
END SUB

SUB battle_meters (byref bat as BattleState, bslot() as BattleSprite, formdata as Formation)
 '--This advances time in turnACTIVE mode
 '--it also handles active-mode poison, regen, stun and mute
 
 IF bat.away > 0 THEN EXIT SUB '--skip all this if the heroes have already run away
 
 DIM i as integer

 FOR i = 0 TO 11
 
  '--poison
  WITH bslot(i).stat
   IF .cur.poison < .max.poison THEN
    bslot(i).poison_repeat += large(.cur.spd, 7)
    IF bslot(i).poison_repeat >= 1500 THEN
     bslot(i).poison_repeat = 0
     do_poison i, bat, bslot(), formdata
    END IF
   END IF
  END WITH
 
  '--regen
  WITH bslot(i).stat
   IF .cur.regen < .max.regen THEN
    bslot(i).regen_repeat += large(.cur.spd, 7)
    IF bslot(i).regen_repeat >= 1500 THEN
     bslot(i).regen_repeat = 0
     do_regen i, bat, bslot(), formdata
    END IF
   END IF
  END WITH

  '--if not doing anything, not dying, not ready, and not stunned
  IF ready_meter_may_grow(bat, bslot(), i) THEN
   '--increment ctr by speed
   bslot(i).ready_meter = small(1000, bslot(i).ready_meter + bslot(i).stat.cur.spd)
   IF bslot(i).ready_meter = 1000 THEN
    make_ready_or_update_turn_delays bslot(i)
   END IF
  END IF

 NEXT i

 '--decrement stun and mute

 IF bat.ticks >= bat.laststun + ideal_ticks_per_second() THEN
  FOR i = 0 TO 11
   WITH bslot(i).stat
    .cur.mute = small(.cur.mute + 1, .max.mute)
    .cur.stun = small(.cur.stun + 1, .max.stun)
    IF .cur.stun < .max.stun THEN
     'FIXME: this doesn't belong here, because this happens only once per second!
     'Stun should be a continuous block
     bslot(i).ready = NO
     IF bat.player_turn = i THEN bat.player_turn = -1
     IF bat.enemy_turn = i THEN bat.enemy_turn = -1
    END IF
   END WITH
  NEXT i
  bat.laststun = bat.ticks
 END IF
 
 decrement_attack_queue_delays bslot()

END SUB

SUB make_ready_or_update_turn_delays (bspr as BattleSprite)
 IF has_blocking_turn_delayed_attacks(bspr.index) THEN
  'debug "Unit " & bspr.index & " " & bspr.name & " blocked turn #" & bspr.active_turn_num
  bspr.ready_meter = 0
  update_turn_delays_in_attack_queue bspr.index
 ELSE
  bspr.ready = YES
 END IF
END SUB

SUB do_poison(byval who as integer, bat as BattleState, bslot() as BattleSprite, formdata as Formation)
 DIM harm as integer
 harm = bslot(who).stat.max.poison - bslot(who).stat.cur.poison
 harm = range(harm, 20)
 quickinflict harm, who, bslot(), uilook(uiBattlePoison)
 triggerfade who, bslot()
 fulldeathcheck -1, bat, bslot(), formdata
END SUB

SUB do_regen(byval who as integer, bat as BattleState, bslot() as BattleSprite, formdata as Formation)
 DIM heal as integer
 heal = bslot(who).stat.max.regen - bslot(who).stat.cur.regen
 heal = heal * -1
 heal = range(heal, 20)
 quickinflict heal, who, bslot(), uilook(uiBattleRegen)
 triggerfade who, bslot()
 fulldeathcheck -1, bat, bslot(), formdata
END SUB

'Whether an attack on atkq() is eligible to have its delay count down
FUNCTION atkq_attack_active(queuedatk as AttackQueue, bslot() as BattleSprite) as bool
 WITH queuedatk
  RETURN .used ANDALSO .turn_delay <= 0 ANDALSO .attacker >= 0 ANDALSO _
         bslot(.attacker).stat.cur.stun >= bslot(.attacker).stat.max.stun
 END WITH
END FUNCTION

'turnACTIVE only
SUB decrement_attack_queue_delays(bslot() as BattleSprite)
 FOR i as integer = 0 TO UBOUND(atkq)
  IF atkq_attack_active(atkq(i), bslot()) THEN
   WITH atkq(i)
    .delay = large(0, .delay - 1)
   END WITH
  END IF
 NEXT i
END SUB

SUB battle_animate(byref bat as BattleState, bslot() as BattleSprite, st() as HeroDef)
 'This sub is intended to apply animation effects triggered elsewhere.
 'FIXME: due to messy code, plenty of animation stuff might still happen elsewhere
 DIM i as integer
 '--First, things that only heroes can do
 FOR i = 0 TO 3
  IF bat.vic.state = vicNONE AND bslot(i).walk = 1 THEN bslot(i).frame = bslot(i).frame XOR bat.tog
  ' Change weak heroes to weak unless the hero is attacking.
  ' Note that enforce_weak_picture doesn't change from weak to normal frame if
  ' the hero is not weak; the whole method of deciding .frame is broken.
  ' enforce_weak_picture is also called at the end of an attack animation script,
  ' so I don't see under what conditions this check is even needed... a hero is
  ' injured in the middle of an animation? Battlescripting?
  IF (bat.atk.id >= 0 AND bat.acting = i) = NO THEN enforce_weak_picture i, bslot(), bat
  IF bat.vic.state > vicNONE AND bslot(i).stat.cur.hp > 0 AND bat.tog = 0 THEN
   IF st(i).skip_victory_dance THEN
    bslot(i).frame = frameVICTORYB
   ELSE
    IF bslot(i).frame = frameVICTORYB THEN bslot(i).frame = frameVICTORYA ELSE bslot(i).frame = frameVICTORYB
   END IF
  END IF
 NEXT i
 '--Then apply movement and animation for all things, heroes, enemies, attacks, weapons
 AdvanceSlice bat.root_sl
 '--then, stuff that only attacks can do
 FOR i = 12 TO 23 '--for each attack sprite
  IF bslot(i).vis THEN
   WITH bslot(i)
    .anim_index += 1
    '--each pattern ends with a -1. If we have found it, loop around
    IF bat.animpat(.anim_pattern).frame(.anim_index) = -1 THEN .anim_index = 0
    .frame = bat.animpat(.anim_pattern).frame(.anim_index)
    IF .frame = -1 THEN
     '--if the frame get set to -1 that indicates an empty pattern, so randomize instead
     .frame = randint(3)
    END IF
   END WITH
  END IF
 NEXT i
 '--Then death animations and fleeing enemies (fleeing heroes are handled in battle_animate_running_away)
 FOR i = 0 TO 11
  WITH bslot(i)
   IF .dissolve > 0 THEN
    'ENEMIES DEATH THROES
    IF is_enemy(i) THEN
     IF .fleeing THEN  'running away
      .x = .x - 10
      .d = 1
     END IF
     .dissolve -= 1
     IF .dissolve = 0 THEN
      'Make dead enemy invisible (this should have already happened in check_death)
      'The check_death code will actually do most removal/cleanup, which will
      'happen when the enemy starts fleeing/dissolving
      .vis = NO
     END IF
    END IF
    IF is_hero(i) THEN .frame = frameDEAD
   END IF
  END WITH
 NEXT i
 '--also appear animations for enemies
 FOR i = 0 TO 11
  IF bslot(i).appeartype >= 0 ANDALSO bslot(i).dissolve_appear <= bslot(i).appeartime THEN
   IF is_enemy(i) THEN
    bslot(i).dissolve_appear += 1
   END IF
  END IF
 NEXT i
END SUB

SUB show_enemy_meters(bat as BattleState, bslot() as BattleSprite, formdata as Formation, page as integer)
 'This shows meters and extra debug info info when you press F10 the first time
 DIM c as integer
 DIM info as string
 info = bat.targ.mode & " " & bat.player_turn & " " & bat.atk.id
 edgeprint info, 10, 70, uilook(uiText), page
 FOR i as integer = 0 TO 11
  WITH bslot(i)
   c = uilook(uiSelectedDisabled)
   IF is_hero(i) THEN c = uilook(uiSelectedItem)
   rectangle 0, 80 + (i * 10), .ready_meter / 10, 4, c, page
   info = "hp" & .stat.cur.hp & " tm" & bat.targ.mask(i) & " sr" & (.stat.cur.stun < .stat.max.stun) & " dz" & .dissolve & " a" & .attack & " r" & .ready & " v" & .vis & " h" & .hidden & " bq" & yesorno(.bequesting, "Y", "N") & yesorno(.self_bequesting, "Y", "N") & yesorno(.already_bequested, "Y", "N")
   IF is_enemy(i) THEN info &= " fm" & formdata.slots(i-4).id
   edgeprint info, 10, 80 + i * 10, c, page
  END WITH
 NEXT i
END SUB

SUB battle_crappy_run_handler(byref bat as BattleState, bslot() as BattleSprite)
 '--Current running system sucks about as bad as a running system conceivably CAN suck
 DIM try_run as bool = NO
 IF carray(ccRun) > 1 THEN try_run = YES
 'The delay of 10 ticks on bat.mouse_running similates the typematic repeat delay of holding ESC
 IF bat.mouse_running > 10 THEN try_run = YES
 IF try_run ANDALSO prefbit(17) = NO THEN '"Disable ESC to run from battle" off
  bat.flee = bat.flee + 1
 END IF
 DIM i as integer
 IF bat.flee > 0 AND bat.flee < 4 THEN
  IF try_run = NO THEN
   bat.flee = 0
   FOR i = 0 TO 3
    bslot(i).d = 0
    bslot(i).walk = 0
   NEXT i
  END IF
 END IF
 IF bat.flee = 4 THEN
  IF checkNoRunBit(bslot()) THEN
   bat.flee = 0
   bat.alert = bat.cannot_run_caption
   bat.alert_ticks = 10
  END IF
 END IF
 IF bat.flee > 4 THEN
  FOR i = 0 TO 3
   '--if alive turn around
   IF bslot(i).stat.cur.hp > 0 THEN bslot(i).d = 1
   bslot(i).walk = 1
   bslot(i).attack = 0
   bslot(i).ready = NO
   bslot(i).ready_meter = large(0, bslot(i).ready_meter - bslot(i).stat.cur.spd * 2)
  NEXT i
  IF try_run = NO THEN
   bat.flee = 0
   FOR i = 0 TO 3
    bslot(i).d = 0
    bslot(i).walk = 0
   NEXT i
  END IF
  DIM stupid_run_threshold as integer = 400
  FOR i = 4 TO 11
   stupid_run_threshold += bslot(i).stat.cur.spd
  NEXT i
  IF randint(stupid_run_threshold) < bat.flee THEN
   bat.away = 1
   bat.flee = 2
   FOR i = 0 TO 3
    bslot(i).ready_meter = 0
    bslot(i).ready = NO
   NEXT i
  END IF
 END IF
END SUB


'==============================================================================
'                                    Slices

SUB setup_battle_slices(byref bat as BattleState)
 WITH bat
  .root_sl = NewSliceOfType(slContainer)
  .root_sl->Fill = YES

  'Parent for everything affected by "Battles display at 320x200".
  'Its size is setup by update_battle_slices (supports changing things while testing game!)
  .view_sl = NewSliceOfType(slContainer, .root_sl)
  .view_sl->Clip = YES
  CenterSlice .view_sl

  .backdrop_sl = NewSliceOfType(slSprite, .view_sl)
  CenterSlice .backdrop_sl
  .battlefield_sl = NewSliceOfType(slContainer, .view_sl)
  'Coordinates (of BattleSprites, cursors, damage digits) are relative to battlefield_sl.
  'For now, battlefield_sl is positioned as if it is 320x200 (or smaller) centered
  'on the screen. This is to avoid breaking attack/attacker animations if the resolution
  'is increased only a bit. (It also avoids needing to offset battle formation coords.)
  .battlefield_sl->Size = get_battlefield_size()
  CenterSlice .battlefield_sl

  '.battlefield_sl->AutoSort = slAutoSortBottomY
  .battlefield_sl->AutoSort = slAutoSortCustom

  ' Battle UI layer for hero HUD and captions (not menus).
  ' Parented directly to root because it shouldn't be clipped to .view_sl
  create_default_battle_ui_layer(.root_sl)
 END WITH
END SUB

'keep_existing: if true, don't recreate slices
SUB setup_battlesprite_slice(byref bspr as BattleSprite, bat as BattleState, sprtype as SpriteType, sprset as integer, pal as integer, keep_existing as bool = NO)
 WITH bspr
  IF (keep_existing AND .sl <> NULL) = NO THEN
   DeleteSlice @.sl
   .sl = NewSliceOfType(slContainer, bat.battlefield_sl)
   .sprite = NewSliceOfType(slSprite, .sl)
  END IF
  ChangeSpriteSlice .sprite, sprtype, sprset, pal
  .sl->Size = .sprite->Size
 END WITH
END SUB

' Create/update slice. If called on transmogrification, then a slice will already exist.
SUB setup_enemy_slice(byref bspr as BattleSprite, bat as BattleState, keep_existing as bool = NO)
 WITH bspr
  setup_battlesprite_slice bspr, bat, sprTypeSmallEnemy + .enemy.size, .enemy.pic, .enemy.pal, keep_existing
 END WITH
END SUB

' Update all slices
SUB update_battle_slices(bat as BattleState, bslot() as BattleSprite)
 'This prefbit might change due to live-editing!
 IF prefbit(56) THEN '"!Battles display at 320x200" off
  bat.view_sl->Fill = YES
 ELSE
  bat.view_sl->Fill = NO
  bat.view_sl->Size = get_battle_res()  '320x200 or smaller
  'bat.view_sl->ClampHoriz = alignLeft  'Shouldn't be needed
  'bat.view_sl->ClampVert = alignTop
 END IF
 update_battlesprite_slices bat, bslot()
 update_battle_ui_slices bat, bslot()
END SUB

SUB update_battlesprite_slices(bat as BattleState, bslot() as BattleSprite)
 FOR i as integer = 0 TO 24
  WITH bslot(i)
   IF .sl = NULL THEN CONTINUE FOR  'Empty slot

   IF .vis OR .dissolve > 0 THEN
    .sl->Visible = YES

    DIM fliph as bool = .d <> 0
    IF (is_hero(i) ANDALSO bat.flee <= 4) ORELSE (is_enemy(i) ANDALSO .fleeing = NO) THEN
     'Flipped only applies when not running
     IF .flipped THEN fliph = NOT fliph
    END IF
    ChangeSpriteSlice .sprite, , , , , fliph

    IF is_enemy(i) andalso .appeartype >= 0 andalso .dissolve_appear <= .appeartime then
     'TODO: switch to auto-animating dissolve instead of manually setting the current tick
     '(but make sure to preserve the one tick delay at start of battle before appear starts)
     DissolveSpriteSlice .sprite, .appeartype, .appeartime, .appeartime - .dissolve_appear, , NO
    ELSEIF is_enemy(i) andalso .dissolve > 0 andalso .fleeing = NO then
     DissolveSpriteSlice .sprite, .deathtype, .deathtime, .deathtime - .dissolve, , NO
    ELSE
     CancelSpriteSliceDissolve .sprite
    END IF

    'Like slAutoSortBottomY, but break ties according to the order in bslot()
    .sl->Sorter = (.sl->Y + .sl->Height) * 1000 + i
   ELSE
    .sl->Visible = NO
   END IF

   IF .hidden THEN
    .sl->Visible = NO
   END IF
  END WITH
 NEXT i
END SUB

SUB update_battle_ui_slices(bat as BattleState, bslot() as BattleSprite)

 STATIC last_warning as double=0.0
 DIM warnings as string = ""

 DIM ui_layer as Slice Ptr
 ui_layer = LookupSlice(SL_BATTLE_UI_LAYER, bat.root_sl)
 IF ui_layer = 0 THEN  '--Should never happen
  warnings &= !"SL_BATTLE_UI_LAYER not found\n"
 END IF

 '--Update hero info areas, which repeat in a grid for each hero
 DIM area as Slice Ptr = 0
 area = LookupSlice(SL_BATTLE_HERO_INFO_AREA, ui_layer)
 IF area = 0 THEN
  warnings &= !"SL_BATTLE_HERO_INFO_AREA not found\n"
 END IF
 DO WHILE area '--For each hero info area
  
  '--Make sure the children exist
  DIM templ as Slice Ptr = LookupSlice(SL_BATTLE_HERO_INFO_TEMPLATE, area)
  DIM ch as Slice Ptr
  IF templ THEN
   DO WHILE SliceGetNumChildren(area) <= 4
    ch = CloneTemplate(templ)
    ch->Lookup = SL_BATTLE_HERO_INFO_INSTANCE
   LOOP
  ELSE
   warnings &= !"SL_BATTLE_HERO_INFO_TEMPLATE not found\n"
  END IF
  
  FOR i as integer = 0 TO 3 '--for each hero
   '--Get the child slice for this hero slot
   ch = SliceChildByIndex(area, i)
   IF ch THEN
    DIM hero_present as bool = gam.hero(i).id >= 0'--FIXME: should use some battle state instead of global state to
                               '--determine if the hero is present.
    ch->Visible = hero_present
    
    IF hero_present THEN
     update_battle_hero_or_enemy_info_slice ch, bat, bslot(i)
    END IF
   
   END IF
  NEXT i
  
  area = LookupSlice(SL_BATTLE_HERO_INFO_AREA, ui_layer, , area)
 LOOP

 '--Update the Caption areas
 area = LookupSlice(SL_BATTLE_CAPTION_AREA, ui_layer)
 IF area = 0 THEN
  warnings &= !"SL_BATTLE_CAPTION_AREA not found\n"
 END IF
 DO WHILE area '--For each caption area
  
  'Update strings
  embedslicetree area, , YES, @ExpandTextBattleState, @bat
  
  area->Visible = NO
  IF bat.caption_time > 0 THEN
   bat.caption_time -= 1
   IF bat.caption_delay > 0 THEN
    bat.caption_delay -= 1
   ELSE
    area->Visible = YES
   END IF
  END IF

  area = LookupSlice(SL_BATTLE_CAPTION_AREA, ui_layer, , area)
 LOOP

 '--Update the player turn areas
 area = LookupSlice(SL_BATTLE_PLAYER_TURN_OVERLAY, ui_layer)
 IF area = 0 THEN
  warnings &= !"SL_BATTLE_PLAYER_TURN_OVERLAY not found\n"
 END IF
 DO WHILE area '--For each player turn overlay area
  
  IF bat.player_turn >= 0 ANDALSO bat.targ.mode = targNONE ANDALSO prefbit(14) = NO THEN
   'Player turn is happening, but is not targetting
   '"Disable Hero's Battle Cursor" off
   WITH bslot(bat.player_turn)
    IF .sl <> 0 THEN
     area->Visible = YES
     area->Pos = .sl->Pos
     area->Size = .sl->Size
     'Player overlay area also functions as a hero info area
     update_battle_hero_or_enemy_info_slice area, bat, bslot(bat.player_turn)
    END IF
   END WITH
  ELSE
   area->Visible = NO
   area->Pos = XY(0, 0)
   area->Size = XY(0, 0)
  END IF
  
  update_battle_toggle_bounce bat, area

  area = LookupSlice(SL_BATTLE_PLAYER_TURN_OVERLAY, ui_layer, , area)
 LOOP

 '--Update the battle menu
 area = LookupSlice(SL_BATTLE_MENU_AREA, ui_layer)
 IF area = 0 THEN
  warnings &= !"SL_BATTLE_MENU_AREA not found\n"
 ELSE
  DIM mdsl AS MenuDefSlice Ptr = cast(MenuDefSlice Ptr, area->ClassInst)
  'Null out the menudef and menustate. They will only be set when the menu should be drawn
  mdsl->mdef = 0
  mdsl->st = 0
  IF bat.vic.state = vicNONE THEN 'only display battle menu till you win
   '--Draw menus for a hero that is currently taking a turn
   IF bat.player_turn >= 0 ANDALSO (prefbit(60) = NO ORELSE bat.targ.mode = targNONE) THEN
    WITH bslot(bat.player_turn)
     'If we actually want to draw this menu, mark it active...
     .menust.active = (bat.menu_mode = batMENUHERO)
     '...and then assign pointers to its menudef and state to the MenuDefSlice
     mdsl->mdef = @.batmenu
     mdsl->st = @.menust
    END WITH
   END IF
  END IF
 END IF

 '--Update mouse hover when targetting areas
 area = LookupSlice(SL_BATTLE_TARG_HOVER_LAYER, ui_layer)
 IF area = 0 THEN
  warnings &= !"SL_BATTLE_TARG_HOVER_LAYER not found\n"
 END IF
 DO WHILE area '--For each hover targetting layer
  instantiate_battle_targ_hover_slices area
  FOR i as integer = 0 to 11
   update_battle_targ_hover_slices area, bat, bslot(i)
  NEXT i
  area = LookupSlice(SL_BATTLE_TARG_HOVER_LAYER, ui_layer, , area)
 LOOP
 
 '--Update targetting areas
 area = LookupSlice(SL_BATTLE_TARGETTING_LAYER, ui_layer)
 IF area = 0 THEN
  warnings &= !"SL_BATTLE_TARGETTING_LAYER not found\n"
 END IF
 DO WHILE area '--For each targetting layer
  instantiate_battle_targetting_slices area
  FOR i as integer = 0 to 11
   update_battle_targetting_slices area, bat, bslot(i)
  NEXT i
  area = LookupSlice(SL_BATTLE_TARGETTING_LAYER, ui_layer, , area)
 LOOP

 '--Update UI layer visibility
 ui_layer->Visible = (bat.vic.state = vicNONE)
 
 IF warnings <> "" ANDALSO (last_warning = 0.0 ORELSE TIMER - last_warning > 600.0) THEN
  'Only bother to log warnings if we haven't logged them for the past 10 minutes
  'It might be perfectly normal for a customized battle ui to exclude some expected lookup codes
  'This is a compromise to know about that without having too much logspam if it was intentional
  debug warnings
  last_warning = TIMER
 END IF
 
END SUB

SUB update_battle_toggle_bounce (bat as BattleState, sl as Slice Ptr)
 DIM bounce as Slice Ptr
 bounce = LookupSlice(SL_BATTLE_TOGGLE_BOUNCE, sl)
 DO WHILE bounce '--For each bouncer
  IF bounce->SliceType = slSelect THEN
   DIM n as integer = bounce->SelectData->index
   IF bounce->NumChildren > 1 THEN
    loopvar n, 0, bounce->NumChildren - 1
    ChangeSelectSlice bounce, n
   END IF
  END IF
  bounce = LookupSlice(SL_BATTLE_TOGGLE_BOUNCE, sl, , bounce)
 LOOP
END SUB

SUB update_battle_hero_or_enemy_info_slice(sl as Slice Ptr, bat as BattleState, bspr as BattleSprite)
 IF sl = 0 THEN EXIT SUB 
 DIM col as integer
 
 DIM ready_meter as Slice Ptr = LookupSlice(SL_BATTLE_READY_METER, sl)
 IF ready_meter THEN
  ready_meter->Visible = (prefbit(6) = NO) '"Hide ready-meter" off
  'Prepare TimeBar colors
  col = uiTimeBar * -1 - 1
  IF bspr.ready = YES THEN col = uiTimeBarFull * -1 - 1
  'Update meter percent
  DIM meter_body as Slice Ptr = LookupSlice(SL_BATTLE_READY_METER_PANEL, ready_meter)
  IF meter_body THEN
   IF meter_body->SliceType = slPanel THEN
    DIM meter_val as double = bspr.ready_meter / 1000
    IF blocked_by_attack(bat, bspr.index) ORELSE bspr.attack > 0 ORELSE (bat.atk.id >= 0 ANDALSO bat.acting = bspr.index) THEN
     meter_val = 1.0
     col = uiTimeBar * -1 - 1
    END IF
    ChangePanelSlice meter_body, , , , meter_val
   END IF
  END IF
  'Update TimeBar colors
  RecursivelyUpdateColor ready_meter, col, uiTimeBar * -1 - 1, uiTimeBarFull * -1 - 1
 END IF
 
 DIM hp_meter as Slice Ptr = LookupSlice(SL_BATTLE_HP_METER, sl)
 IF hp_meter THEN
  DIM hidehp as bool = NO
  IF is_hero(bspr.index) THEN hidehp = should_hide_hero_stat(gam.hero(bspr.index).id, statHP)
  hp_meter->Visible = (prefbit(7) = NO ANDALSO hidehp = NO)
  'FIXME: last two color arguments don't matter as we re-do them right afterwards
  'They are a vestige of the old pre-slice drawing code
  update_stat_meter bat, bspr.lifemeter, 100, bspr.stat.cur.hp, bspr.stat.max.hp, uiHealthBar, col
  'Update color for real
  col = uiHealthBar * -1 - 1
  IF bspr.lifemeter = 100 ANDALSO bspr.stat.cur.hp > bspr.stat.max.hp ANDALSO bat.menu_mode = batMENUHERO ANDALSO bat.tog <> 0 THEN
   col = uiHealthBarFlash * -1 - 1
  END IF
  RecursivelyUpdateColor hp_meter, col, uiHealthBar * -1 - 1, uiHealthBarFlash * -1 - 1
  'Update meter percent
  DIM meter_body as Slice Ptr = LookupSlice(SL_BATTLE_HP_METER_PANEL, hp_meter)
  IF meter_body THEN
   IF meter_body->SliceType = slPanel THEN
    ChangePanelSlice meter_body, , , , bspr.lifemeter / 100
   END IF
  END IF
 END IF
  
 'Update MP meter
 DIM mp_meter as Slice Ptr = LookupSlice(SL_BATTLE_MP_METER, sl)
 IF mp_meter THEN
  DIM hidemp as bool = NO
  IF is_hero(bspr.index) THEN hidemp = should_hide_hero_stat(gam.hero(bspr.index).id, statMP)
  mp_meter->Visible = (prefbit(49) = YES ANDALSO hidemp = NO)
  update_stat_meter bat, bspr.mpmeter, 100, bspr.stat.cur.mp, bspr.stat.max.mp, uiMPBar, col
  'Update color for real
  col = uiMPBar * -1 - 1
  IF bspr.mpmeter = 100 ANDALSO bspr.stat.cur.mp > bspr.stat.max.mp ANDALSO bat.menu_mode = batMENUHERO ANDALSO bat.tog <> 0 THEN
   col = uiMPBarFlash * -1 - 1
  END IF
  RecursivelyUpdateColor mp_meter, col, uiMPBar * -1 - 1, uiMPBarFlash * -1 - 1
  'Update meter percent
  DIM mp_body as Slice Ptr = LookupSlice(SL_BATTLE_MP_METER_PANEL, mp_meter)
  IF mp_body THEN
   IF mp_body->SliceType = slPanel THEN
    ChangePanelSlice mp_body, , , , bspr.mpmeter / 100
   END IF
  END IF
 END IF

 'Update strings
 embedslicetree sl, , YES, @ExpandTextBattleSprite, @bspr

 'Update status icons
 DIM indicator_area as Slice Ptr = LookupSlice(SL_BATTLE_INDICATOR_AREA, sl)
 IF indicator_area THEN
  update_indicator_slice indicator_area, SL_BATTLE_REGEN_INDICATOR,  bspr.stat.cur.regen  < bspr.stat.max.regen,  CHR(gen(genRegenChar)),  1
  update_indicator_slice indicator_area, SL_BATTLE_MUTE_INDICATOR,   bspr.stat.cur.mute   < bspr.stat.max.mute,   CHR(gen(genMuteChar)),   2
  update_indicator_slice indicator_area, SL_BATTLE_STUN_INDICATOR,   bspr.stat.cur.stun   < bspr.stat.max.stun,   CHR(gen(genStunChar)),   3
  update_indicator_slice indicator_area, SL_BATTLE_POISON_INDICATOR, bspr.stat.cur.poison < bspr.stat.max.poison, CHR(gen(genPoisonChar)), 4
 END IF

 'Update string color for selected player name/hp/indicators
 col = uiMenuItem * -1 - 1
 IF bspr.index = bat.player_turn ANDALSO bat.menu_mode = batMENUHERO THEN
  col = (uiSelectedItem + bat.tog) * -1 - 1
 END IF
 RecursivelyUpdateColor sl, col, uiMenuItem * -1 - 1, uiSelectedItem * -1 - 1, (uiSelectedItem+1) * -1 - 1, SL_BATTLE_TEXT_SELECTED_PLAYER

END SUB

SUB update_indicator_slice (sl as Slice Ptr, lookup_code as integer, active as bool, indicator_string as string="", sort_index as integer=0)
 DIM ind_sl as Slice Ptr = LookupSlice(lookup_code, sl)
 IF NOT active ORELSE indicator_string = "" ORELSE indicator_string = " " THEN
  IF ind_sl <> 0 THEN DeleteSlice @ind_sl
 ELSE
  IF ind_sl = 0 THEN ind_sl = NewSliceOfType(slText, sl, lookup_code)
  ind_sl->AlignHoriz = alignCenter
  ind_sl->AlignVert = alignCenter
  ind_sl->AnchorHoriz = alignCenter
  ind_sl->AnchorVert = alignCenter
  ChangeTextSlice ind_sl, indicator_string, uiMenuItem * -1 - 1, YES
  ind_sl->Sorter = sort_index
 END IF
 
 'FIXME: want to support slLayout also, after they are supported in the slice collection editor
 IF sl->SliceType = slGrid THEN
  sl->AutoSort = slAutoSortCustom
  ChangeGridSlice sl, 1, sl->NumChildren
  sl->Size = XY(8 * sl->NumChildren, 8)
 END IF
 
END SUB

SUB instantiate_battle_targetting_slices (layer as Slice Ptr)
 IF layer = 0 THEN EXIT SUB
 DIM templ as Slice Ptr = LookupSlice(SL_BATTLE_TARGETTING_TEMPLATE, layer)
 IF templ = 0 THEN EXIT SUB
 DIM sl as Slice Ptr
 DO WHILE layer->NumChildren < 12 + 1 '12 slots + the template
  sl = CloneTemplate(templ)
  sl->Lookup = SL_BATTLE_TARGETTING_OVERLAY
 LOOP
END SUB

SUB update_battle_targetting_slices (layer as Slice Ptr, bat as BattleState, bspr as BattleSprite)
 DIM sl as Slice Ptr = SliceChildByIndex(layer, bspr.index)
 IF sl = 0 THEN EXIT SUB
 sl->Visible = NO
 WITH bspr
  IF .sl <> 0 THEN
   sl->Pos = .sl->Pos
   sl->Size = .sl->Size
   'Targetting area also functions as a hero or enemy info area
   update_battle_hero_or_enemy_info_slice sl, bat, bspr
  END IF
  DIM cursor as Slice Ptr = LookupSlice(SL_BATTLE_TARGETTING_CURSOR, sl)
  IF cursor <> 0 THEN cursor->Pos = .cursorpos
  IF bat.targ.mode > targNONE ANDALSO (bat.targ.selected(.index) = YES ORELSE bat.targ.pointer = .index) THEN
   sl->Visible = YES
  END IF
 END WITH
END SUB

SUB instantiate_battle_targ_hover_slices (layer as Slice Ptr)
 IF layer = 0 THEN EXIT SUB
 DIM templ as Slice Ptr = LookupSlice(SL_BATTLE_TARG_HOVER_TEMPLATE, layer)
 IF templ = 0 THEN EXIT SUB
 DIM sl as Slice Ptr
 DO WHILE layer->NumChildren < 12 + 1 '12 slots + the template
  sl = CloneTemplate(templ)
  sl->Lookup = SL_BATTLE_TARG_HOVER_OVERLAY
 LOOP
END SUB

SUB update_battle_targ_hover_slices (layer as Slice Ptr, bat as BattleState, bspr as BattleSprite)
 DIM sl as Slice Ptr = SliceChildByIndex(layer, bspr.index)
 IF sl = 0 THEN EXIT SUB
 sl->Visible = NO
 WITH bspr
  IF .sl <> 0 THEN
   sl->Pos = .sl->Pos
   sl->Size = .sl->Size
   'Targetting hover area also functions as a hero or enemy info area
   update_battle_hero_or_enemy_info_slice sl, bat, bspr
  END IF
  DIM cursor as Slice Ptr = LookupSlice(SL_BATTLE_TARG_HOVER_CURSOR, sl)
  IF cursor <> 0 THEN cursor->Pos = .cursorpos
  IF bat.targ.mode > targNONE ANDALSO bat.targ.hover = .index THEN
   sl->Visible = YES
  END IF
  DIM sel as Slice Ptr = LookupSlice(SL_BATTLE_TARG_HOVER_SELECT, sl)
  IF sel THEN
   ChangeSelectSlice sel, IIF(bat.targ.mask(.index),1, 0)
  END IF
 END WITH
END SUB

SUB ExpandTextBattleState (code as string, result as string, byval arg0 as ANY ptr=0, byval arg1 as ANY ptr=0, byval arg2 as ANY ptr=0)
'General battle text that is not associated with a specific hero or enemy
'arg0 must be a BattleState instance
 DIM bat as BattleState Ptr = CAST(BattleState ptr, arg0)

 WITH *bat

  SELECT CASE UCASE(code)
   CASE "CAPTION": result = .caption
  END SELECT

 END WITH 
END SUB

SUB ExpandTextBattleSprite (code as string, result as string, byval arg0 as ANY ptr=0, byval arg1 as ANY ptr=0, byval arg2 as ANY ptr=0)
'Battle text that is specific to a hero or enemy.
'arg0 must be a BattleSprite instance

 DIM bspr as BattleSprite Ptr = CAST(BattleSprite ptr, arg0)

 WITH *bspr

  SELECT CASE UCASE(code)
   CASE "NAME": result = .name
   CASE "HP": result = STR(.stat.cur.hp)
   CASE "MAXHP": result = STR(.stat.max.hp)
  END SELECT

 END WITH 
END SUB

SUB create_default_battle_ui_layer(parent as Slice ptr)
 DIM layer as Slice Ptr = NewSliceOfType(slContainer, parent, SL_BATTLE_UI_LAYER)
 'For now, restrict to central 320x200 (or smaller) of screen
 layer->Size = small(XY(320, 200), XY(gen(genResolutionX), gen(genResolutionY))) ' == get_battlefield_size()
 CenterSlice layer

 DIM area as Slice Ptr
 area = create_default_battle_ready_area()
 SetSliceParent area, layer
 area = create_default_battle_hp_area()
 SetSliceParent area, layer
 area = create_default_battle_caption_area()
 SetSliceParent area, layer
 area = create_default_player_turn_overlay_area()
 SetSliceParent area, layer
 area = create_default_battle_menu_area()
 SetSliceParent area, layer
 area = create_default_targ_hover_overlay_area()
 SetSliceParent area, layer
 area = create_default_targetting_overlay_area()
 SetSliceParent area, layer
 area = create_default_battle_submenus_area()
 SetSliceParent area, layer
END SUB

FUNCTION create_default_battle_menu_area() as Slice Ptr
 'This is the slice that makes the battle menu to be drawn in the correct order (after other ui, but before hover&targeting cursors)
 DIM menu_sl_cl as MenuDefSlice ptr = NEW MenuDefSlice()
 DIM menu_sl as Slice ptr
 menu_sl = NewClassSlice(0, menu_sl_cl)
 menu_sl->Lookup = SL_BATTLE_MENU_AREA
 RETURN menu_sl
END FUNCTION

FUNCTION create_default_battle_submenus_area() as Slice Ptr
 'This is the slice that makes the items menu will be drawn in the correct order
 'TODO: We could add the spells menu here too, when it gets converted
 
 'This is just a simple filling container. create_battle_item_menu() is where the
 'collection gets loaded into it.
 DIM submenu_sl as Slice ptr
 submenu_sl = NewSliceOfType(slContainer)
 submenu_sl->Lookup = SL_BATTLE_SUBMENU_AREA
 submenu_sl->Fill = sliceFillFull
 RETURN submenu_sl
END FUNCTION

FUNCTION create_default_battle_ready_area() as Slice Ptr
 DIM area as Slice Ptr = NewSliceOfType(slGrid, , SL_BATTLE_HERO_INFO_AREA)
 ChangeGridSlice area, 4, 1
 area->Pos = XY(1, 4)
 area->Size = XY(132, 40)
 DIM templ as Slice Ptr = NewSliceOfType(slContainer, area, SL_BATTLE_HERO_INFO_TEMPLATE)
 templ->Template = YES
 templ->Fill = YES
 DIM meter_box as Slice Ptr = NewSliceOfType(slRectangle, templ, SL_BATTLE_READY_METER)
 meter_box->Fill = YES
 meter_box->FillMode = sliceFillHoriz
 meter_box->Height = 11
 meter_box->PaddingTop = 1
 meter_box->PaddingRight = 1
 meter_box->PaddingBottom = 1
 meter_box->PaddingLeft = 1
 ChangeRectangleSlice meter_box, 0, , , borderLine, transFuzzy
 DIM meter_body as Slice Ptr = NewSliceOfType(slPanel, meter_box, SL_BATTLE_READY_METER_PANEL)
 meter_body->Fill = YES
 DIM meter_filled as Slice Ptr = NewSliceOfType(slRectangle, meter_body)
 meter_filled->Fill = YES
 ChangeRectangleSlice meter_filled, 1, uiTimeBarFull * -1 - 1, , borderNone, transOpaque
 DIM name_txt as Slice Ptr = NewSliceOfType(slText, templ, SL_BATTLE_TEXT_SELECTED_PLAYER)
 ChangeTextSlice name_txt, "${NAME}", uiMenuItem * -1 - 1, YES
 name_txt->AlignHoriz = alignRight
 name_txt->AnchorHoriz = alignRight
 name_txt->Pos = XY(-5, 1)
 RETURN area
END FUNCTION

FUNCTION create_default_battle_hp_area() as Slice Ptr
 DIM area as Slice Ptr = NewSliceOfType(slGrid, , SL_BATTLE_HERO_INFO_AREA)
 ChangeGridSlice area, 4, 1
 area->Pos = XY(136, 4)
 area->Size = XY(89, 40)
 DIM templ as Slice Ptr = NewSliceOfType(slContainer, area, SL_BATTLE_HERO_INFO_TEMPLATE)
 templ->Template = YES
 templ->Fill = YES
 'HP meter
 DIM meter_box as Slice Ptr = NewSliceOfType(slRectangle, templ, SL_BATTLE_HP_METER)
 meter_box->Fill = YES
 meter_box->FillMode = sliceFillHoriz
 meter_box->Height = 11
 meter_box->PaddingTop = 1
 meter_box->PaddingRight = 1
 meter_box->PaddingBottom = 1
 meter_box->PaddingLeft = 1
 ChangeRectangleSlice meter_box, 0, , , borderLine, transFuzzy
 DIM meter_body as Slice Ptr = NewSliceOfType(slPanel, meter_box, SL_BATTLE_HP_METER_PANEL)
 meter_body->Fill = YES
 DIM meter_filled as Slice Ptr = NewSliceOfType(slRectangle, meter_body)
 meter_filled->Fill = YES
 ChangeRectangleSlice meter_filled, , uiHealthBar * -1 - 1, , borderNone, transOpaque
 'Hack: when the MP meter is visible, move the HP text up 1 pixel.
 'Accomplished by parenting them to a Layout slice
 DIM layout_sl as Slice Ptr = NewSliceOfType(slLayout, templ)
 layout_sl->Fill = YES
 'Allow the column (primary_dir) to grow above the top of the layout slice (only needs to be -1)
 layout_sl->PaddingTop = -10
 layout_sl->AlignVert = alignBottom
 layout_sl->AnchorVert = alignBottom
 WITH *layout_sl->LayoutData
  .primary_dir = dirUp
  .secondary_dir = dirRight  'Not used
  .row_alignment = alignBottom  'MP meter (first child) at the bottom
  .skip_hidden = YES
  .primary_padding = -1  '2px high MP bar + -1px padding = HP text moves up 1px
 END WITH

 'MP meter
 DIM mp_meter as Slice Ptr = NewSliceOfType(slContainer, layout_sl, SL_BATTLE_MP_METER)
 mp_meter->Fill = YES
 mp_meter->FillMode = sliceFillHoriz
 mp_meter->Height = 2
 'mp_meter->Y = 1   'Sticks out 1 pixel below the bottom of this row
 mp_meter->AnchorVert = alignBottom
 mp_meter->AlignVert = alignBottom
 mp_meter->PaddingLeft = 1
 mp_meter->PaddingRight = 1
 DIM mp_body as Slice Ptr = NewSliceOfType(slPanel, mp_meter, SL_BATTLE_MP_METER_PANEL)
 mp_body->Fill = Yes
 DIM mp_filled as Slice Ptr = NewSliceOfType(slRectangle, mp_body)
 mp_filled->Fill = YES
 ChangeRectangleSlice mp_filled, , uiMPBar * -1 - 1, , borderNone, transOpaque

 'HP text
 DIM hp_txt as Slice Ptr = NewSliceOfType(slText, layout_sl, SL_BATTLE_TEXT_SELECTED_PLAYER)
 ChangeTextSlice hp_txt, "${HP}/${MAXHP}", uiMenuItem * -1 - 1, YES
 hp_txt->Pos = XY(0, 1)

 'Status indicators box
 DIM indicator_area as Slice Ptr = NewSliceOfType(slGrid, templ, SL_BATTLE_INDICATOR_AREA)
 indicator_area->AlignHoriz = alignRight
 indicator_area->AnchorHoriz = alignRight
 indicator_area->Pos = XY(0, 1)

 RETURN area
END FUNCTION

FUNCTION create_default_battle_caption_area () as Slice Ptr
 DIM area as Slice Ptr = NewSliceOfType(slRectangle, , SL_BATTLE_CAPTION_AREA)
 area->AnchorHoriz = alignCenter
 area->AnchorVert = alignCenter
 area->AlignHoriz = alignCenter
 area->AlignVert = alignBottom
 area->Pos = XY(0, -14)
 area->Size = XY(310, 16)
 ChangeRectangleSlice area, 0
 DIM cap_txt as Slice Ptr = NewSliceOfType(slText, area)
 cap_txt->AnchorHoriz = alignCenter
 cap_txt->AnchorVert = alignCenter
 cap_txt->AlignHoriz = alignCenter
 cap_txt->AlignVert = alignCenter
 ChangeTextSlice cap_txt, "${CAPTION}", uiText * -1 - 1, YES
 RETURN area
END FUNCTION

FUNCTION create_default_player_turn_overlay_area () as Slice Ptr
 'A slice with the SL_BATTLE_PLAYER_TURN_OVERLAY lookup code is magical.
 'It will change size and position to match the hero or enemy
 'who is currently taking a player-controlled turn
 'Or it will become invisible if not taking a turn
 DIM area as Slice Ptr = NewSliceOfType(slContainer, , SL_BATTLE_PLAYER_TURN_OVERLAY)
 DIM cursor_area as Slice Ptr = NewSliceOfType(slSelect, area, SL_BATTLE_TOGGLE_BOUNCE)
 cursor_area->Size = XY(8, 10)
 cursor_area->Pos = XY(0, 5)
 cursor_area->AnchorHoriz = alignCenter
 cursor_area->AlignHoriz = alignCenter
 cursor_area->AnchorVert = alignBottom
 cursor_area->AlignVert = alignTop
 DIM cursor as Slice Ptr
 cursor = NewSliceOfType(slText, cursor_area)
 cursor->Pos = XY(0, 0)
 ChangeTextSlice cursor, CHR(24), uiSelectedItem2 * -1 - 1, YES
 cursor = NewSliceOfType(slText, cursor_area)
 cursor->Pos = XY(0, 2)
 ChangeTextSlice cursor, CHR(24), uiSelectedItem2 * -1 - 1, YES
 RETURN area
END FUNCTION

FUNCTION create_default_targetting_overlay_area () as Slice Ptr
 DIM layer as Slice Ptr = NewSliceOfType(slContainer, , SL_BATTLE_TARGETTING_LAYER)
 layer->Fill = YES
 'Template for the targetting overlay
 DIM templ as Slice Ptr = NewSliceOfType(slContainer, layer, SL_BATTLE_TARGETTING_TEMPLATE)
 templ->Template = YES
 DIM cursor as Slice Ptr = NewSliceOfType(slContainer, templ, SL_BATTLE_TARGETTING_CURSOR)
 cursor->AnchorHoriz = alignCenter
 cursor->AlignHoriz = alignCenter
 cursor->AnchorVert = alignBottom
 cursor->AlignVert = alignTop
 cursor->Size = XY(0, 0)
 DIM curs as Slice Ptr = NewSliceOfType(slText, cursor)
 curs->Pos = XY(0, 4)
 curs->AnchorHoriz = alignCenter
 curs->AlignHoriz = alignCenter
 curs->AnchorVert = alignBottom
 curs->AlignVert = alignTop
 curs->ClampHoriz = alignBoth
 curs->ClampVert = alignBoth
 curs->ClampToScreen = YES
 ChangeTextSlice curs, CHR(24), uiSelectedItem2 * -1 - 1, YES
 DIM targname as Slice Ptr = NewSliceOfType(slText, cursor)
 targname->AnchorHoriz = alignCenter
 targname->AlignHoriz = alignTop
 targname->AnchorVert = alignBottom
 targname->AlignVert = alignTop
 targname->ClampHoriz = alignBoth
 targname->ClampVert = alignBoth
 targname->ClampToScreen = YES
 ChangeTextSlice targname, "${NAME}", uiSelectedItem2 * -1 - 1, YES
 targname->Pos = XY(-3, -6)
 RETURN layer
END FUNCTION

FUNCTION create_default_targ_hover_overlay_area () as Slice Ptr
 DIM layer as Slice Ptr = NewSliceOfType(slContainer, , SL_BATTLE_TARG_HOVER_LAYER)
 layer->Fill = YES
 'Template for the hover overlay (only relevant for mouse control)
 DIM hov_templ as Slice Ptr = NewSliceOfType(slContainer, layer, SL_BATTLE_TARG_HOVER_TEMPLATE)
 hov_templ->Template = YES
 DIM hov_cursor as Slice Ptr = NewSliceOfType(slContainer, hov_templ, SL_BATTLE_TARG_HOVER_CURSOR)
 hov_cursor->AnchorHoriz = alignCenter
 hov_cursor->AlignHoriz = alignCenter
 hov_cursor->AnchorVert = alignBottom
 hov_cursor->AlignVert = alignTop
 hov_cursor->Pos = XY(0, 0)
 hov_cursor->Size = XY(0, 0)
 
 DIM hov_select as Slice Ptr = NewSliceOfType(slSelect, hov_cursor, SL_BATTLE_TARG_HOVER_SELECT)
 hov_select->Pos = XY(0, 4)
 hov_select->Size = XY(8, 10)
 hov_select->AnchorHoriz = alignCenter
 hov_select->AlignHoriz = alignCenter
 hov_select->AnchorVert = alignBottom
 hov_select->AlignVert = alignTop
 hov_select->ClampHoriz = alignBoth
 hov_select->ClampVert = alignBoth
 hov_select->ClampToScreen = YES
 'Child 0 is for invalid targets, child 1 is for valid targets
 DIM hov_curs as Slice Ptr
 hov_curs = NewSliceOfType(slText, hov_select)
 ChangeTextSlice hov_curs, "X", uiSelectedDisabled2 * -1 - 1, YES
 hov_curs = NewSliceOfType(slText, hov_select)
 ChangeTextSlice hov_curs, CHR(24), uiSelectedDisabled2 * -1 - 1, YES
 RETURN layer
END FUNCTION

SUB draw_damage_text(bslot() as BattleSprite, battlefield_sl as Slice ptr, page as integer)
 FOR i as integer = 0 TO 11
  WITH bslot(i).harm
   IF LEN(.text) = 0 THEN .ticks = 0  'E.g. No Damage attacks that don't Show Attack Name
   IF .ticks > 0 THEN
    DIM pos as XYPair = battlefield_sl->ScreenPos + .pos
    DIM harm_text_offset as integer
    DIM numticks as integer = gen(genDamageDisplayTicks)
    IF numticks <> 0 THEN  'numticks=0 shouldn't be possible anyway
     pos.y -= gen(genDamageDisplayRise) * ((numticks - .ticks) / numticks)
    END IF
    edgeprint .text, pos.x + ancCenter, pos.y, .col, page
    .ticks -= 1
    IF .ticks = 0 THEN .col = uilook(uiBattleDamage) 'reset color when the timer finishes
   END IF
  END WITH
 NEXT i
END SUB

'==============================================================================

SUB battle_loadall(byval form as integer, byref bat as BattleState, bslot() as BattleSprite, st() as HeroDef, formdata as Formation)
 DIM i as integer

 LoadFormation formdata, form

 for i = 0 to 24
  bslot(i).frame = frameSTAND
  bslot(i).attack = 0
 next i

 IF formdata.music = -1 THEN stopsong
 IF formdata.music >= 0 THEN wrappedsong formdata.music
 'Otherwise formdata.music = -2: same music as map
 
 DIM hero_form as HeroFormation
 load_hero_formation hero_form, formdata.hero_form
 
 FOR i = 0 TO 3
  DIM byref hero as HeroState = gam.hero(i)
  IF hero.id >= 0 THEN
   loadherodata st(i), hero.id

   '--init bslot() for each hero
   WITH bslot(i)
    setup_battlesprite_slice bslot(i), bat, sprTypeHero, hero.battle_pic, hero.battle_pal

    'hero_form specifies bottom center relative to the original hero zone
    .basepos = hero_form.slots(i).pos + HERO_FORM_OFFSET - XY(.w \ 2, .h)
    (.pos) = .basepos
    .vis = YES

    .under_player_control = NOT gam.hero(i).auto_battle
    .death_sfx = -1 'No death sounds for heroes (for now)
    .cursorpos.x = 0
    .cursorpos.y = 0
    .bequesting = NO
    .self_bequesting = NO
    .already_bequested = NO
    populate_battle_menu_menudef i, .batmenu, st(i)
    .menust.active = YES
    FOR o as integer = 0 TO maxElements - 1
     .elem_counter_attack(o) = st(i).elem_counter_attack(o)
    NEXT o
    .non_elem_counter_attack = st(i).non_elem_counter_attack
    FOR o as integer = 0 TO statLast
     .stat_counter_attack(o) = st(i).stat_counter_attack(o)
    NEXT o
   END WITH
   
   '--copy hero's outside-battle stats to their inside-battle stats
   FOR o as integer = 0 TO statLast
    bslot(i).stat.cur.sta(o) = hero.stat.cur.sta(o)
    bslot(i).stat.max.sta(o) = hero.stat.max.sta(o)
    'BattleSprites don't have base stats, though we might want them to have them in future
    'if we allow changing equipment during battle
   NEXT o

   calc_hero_elementals bslot(i).elementaldmg(), i

   bslot(i).name = hero.name
  END IF
 NEXT i

 '--wipe spells learnt and levels gained for all heroes
 FOR i = 0 TO UBOUND(gam.hero)
  flusharray gam.hero(i).learnmask()
  gam.hero(i).lev_gain = 0
 NEXT

 '--load monsters
 FOR i = 0 TO 7
  loadfoe i, formdata, bat, bslot(), YES
 NEXT i

 FOR i = 0 TO 11
  IF prefbit(22) = NO THEN  '"Don't randomize battle ready meters" off
   bslot(i).ready_meter = randint(500)
  END IF
 NEXT i

 bat.curbg = formdata.background
 ChangeSpriteSlice bat.backdrop_sl, sprTypeBackdrop, bat.curbg

 '--This checks weak/dead status for heroes
 '-- who are already weak/dead at the beginning of battle
 FOR i = 0 TO 3
  WITH bslot(i)
   enforce_weak_picture i, bslot(), bat
   IF gam.hero(i).id >= 0 AND .stat.cur.hp <= 0 THEN
    '--hero starts the battle dead
    .dissolve = 1 'Keeps the dead hero from vanishing
    .frame = frameDEAD
   END IF
   .lifemeter = 100 * bound(.stat.cur.hp / large(.stat.max.hp, 1), 0., 1.)
   .mpmeter = 100 * bound(.stat.cur.mp / large(.stat.max.mp, 1), 0., 1.)
  END WITH
 NEXT i

 'trigger fades for dead enemies
 'fulldeathcheck fades out only enemies set to die without a boss
 'so additionally call triggerfade on 0 hp enemies here
 'or might that be expected behaviour in some games?
 FOR i = 4 TO 11
  IF bslot(i).stat.cur.hp <= 0 THEN
   triggerfade i, bslot()
  END IF
 NEXT i
 fulldeathcheck -1, bat, bslot(), formdata
END SUB

'==============================================================================

SUB populate_battle_menu_menudef (byval hero_id as integer, menu as MenuDef, hero as HeroDef)
 DIM hero_node as NodePtr
 hero_node = hero.reld
 DIM caption as string
 DIM k as NodePtr
 DIM wep_id as integer
 DIM atk_id as integer
 DIM spells_id as integer
 DIM item as ItemDef

 init_battle_menu menu, gen(genDefaultBattleMenu) - 1
 
 READNODE hero_node."battle_menus" as battle_menus
  WITHNODE battle_menus."menu" as m
   DIM mitem as integer = -1
   k = m."kind".ptr
   IF k."weapon".exists THEN
    wep_id = gam.hero(hero_id).equip(0).id
    loaditemdata item, wep_id
    atk_id = item.battle_weapon_attack
    IF atk_id < 0 THEN atk_id = 0
    caption = readattackname(atk_id)
    mitem = append_menu_item(menu, caption, batmenu_ATTACK, atk_id, m)
   ELSEIF k."attack".exists THEN
    atk_id = k."attack".integer
    caption = readattackname(atk_id)
    mitem = append_menu_item(menu, caption, batmenu_ATTACK, atk_id, m)
   ELSEIF k."spells".exists THEN
    spells_id = k."spells".integer
    caption = hero.list_name(spells_id)
    IF LEN(caption) > 0 THEN
     '--only add the spell list if it is non-empty or if the bitset says we don't care if it is empty
     IF readbit(hero.bits(),0,26) = 0 ORELSE count_available_spells(hero_id, spells_id) THEN
      mitem = append_menu_item(menu, caption, batmenu_SPELLS, spells_id, m)
     END IF
    END IF
   ELSEIF k."items".exists THEN
    caption = readglobalstring(34, "Item", 10)
    IF LEN(caption) > 0 THEN
     mitem = append_menu_item(menu, caption, batmenu_ITEMS, , m)
    END IF
   ELSEIF k."skip".exists THEN
    caption = readglobalstring(332, "Skip Turn", 20)
    IF LEN(caption) > 0 THEN
     mitem = append_menu_item(menu, caption, batmenu_SKIPTURN, , m)
    END IF
   ELSE
    debug "Unknown battle menu item kind"
   END IF

   IF mitem > -1 THEN
    ' Set up other data
    WITH *menu.items[mitem]
     IF LEN(m."caption".string) THEN .caption = m."caption".string
     .col = m."color".default(0)
     .tag1 = m."enable_tag1".default(0)
     .tag2 = m."enable_tag2".default(0)
     .hide_if_disabled = m."hide_disabled".bool.default(NO)
    END WITH
   END IF

  END WITHNODE
 END READNODE
END SUB

SUB populate_battle_menu_menudef_for_enemy (byref bat as BattleState, bslot() as BattleSprite,  byref bspr as BattleSprite)

 init_battle_menu bspr.batmenu, gen(genDefaultBattleMenu) - 1

 'Which attack list to use
 DIM ai as EnemyAIEnum = pick_enemy_attack_list(bat.enemy_turn, bslot())
 IF ai <> aiNone THEN

  SELECT CASE ai
   CASE aiNormal:
    append_enemy_attacks_to_battle_menu bspr.enemy.regular_ai(), bspr
   CASE aiWeak:
    append_enemy_attacks_to_battle_menu bspr.enemy.desperation_ai(), bspr
   CASE aiAlone:
    append_enemy_attacks_to_battle_menu bspr.enemy.alone_ai(), bspr
  END SELECT

 END IF

END SUB

SUB append_enemy_attacks_to_battle_menu(ai_attack_list() as integer, byref bspr as BattleSprite)
 DIM atk_id as integer
 DIM caption as string
 DIM already as integer vector
 v_new already
 FOR i as integer = 0 TO UBOUND(ai_attack_list)
  IF ai_attack_list(i) > 0 THEN
   atk_id = ai_attack_list(i) - 1
   IF v_append_once(already, atk_id) THEN
    caption = readattackname(atk_id)
    append_menu_item(bspr.batmenu, caption, batmenu_ATTACK, atk_id)
   END IF
  END IF
 NEXT i
 v_free already
END SUB

FUNCTION does_battle_menu_have_targets(byref bspr as BattleSprite, bslot() as BattleSprite, bat as BattleState, st() as HeroDef) as bool
 'This is mainly useful for player controlled enemies,
 'but also good for checking "Targetting Failed" tags
 'Does not attempt to check spell menus or items menu
 IF bspr.batmenu.numitems = 0 THEN
  'An empty menu is an unusable menu
  RETURN NO
 END IF
 FOR i as integer = 0 TO bspr.batmenu.numitems - 1
  WITH *bspr.batmenu.items[i]
   IF NOT.disabled THEN
    SELECT CASE .t
     CASE batmenu_ATTACK:
      DIM atk_id as integer = .sub_t
      IF has_valid_targs(bspr.index, atk_id, bslot(), bat) THEN
       RETURN YES
      ELSE
       checkAtkTagConds atk_id, atktagOnTargettingFailed
      END IF
     CASE batmenu_SKIPTURN:
      IF .sub_t = 1 THEN
       'This menu item was auto_added. The whole point of the does_battle_menu_have_targets()
       'function is to decide if we want to add it, so don't count this auto-added skip turn menu item
      ELSE
       'Always usable
       RETURN YES
      END IF
     CASE batmenu_SPELLS:
      'Don't attempt to support prefbit(54) for autotarget attacks in random spell lists here.
      'If that prefbit is on, the random spell menu might still be usable even if we find here that it isn't, but that is okay
      IF spell_menu_is_usable(bat, bspr, .sub_t, st(), YES) THEN RETURN YES
     CASE batmenu_ITEMS:
      IF items_menu_is_usable(bspr) THEN RETURN YES
     CASE ELSE
      'an unsupported menu item was found (item menu)
      'so assume the menu is usable
      RETURN YES
    END SELECT
   END IF
  END WITH
 NEXT i
 'Didn't find any usable (or unknown) menu items, so the menu has no targets
 RETURN NO
END FUNCTION

' Updates which menu items are enabled
SUB update_battle_menu (bspr as BattleSprite, bslot() as BattleSprite, bat as BattleState, st() as HeroDef)
 WITH bspr
  DIM atk as AttackData
  FOR i as integer = 0 TO .batmenu.numitems - 1
   WITH *.batmenu.items[i]
    .disabled = NO
    IF .t = batmenu_ATTACK THEN
     loadattackdata atk, .sub_t
     ' FIXME: isn't this bitset misnamed, since it affects all battle menu attacks? (Yes, I agree)
     IF atk.check_costs_as_weapon THEN
      IF atkallowed(atk, bspr.index, 0, 0, bspr) = 0 THEN
       .disabled = YES
      END IF
     END IF
    END IF

    IF NOT (istag(.tag1, YES) AND istag(.tag2, YES)) THEN .disabled = YES
   END WITH
  NEXT i

  IF NOT does_battle_menu_have_targets(bspr, bslot(), bat, st()) THEN
   auto_add_skip_turn_to_battle_menu bspr
  ELSE
   remove_auto_added_skip_turn_from_battle_menu bspr
  END IF

  ' Update selection, etc
  init_menu_state .menust, .batmenu
 END WITH
END SUB

SUB auto_add_skip_turn_to_battle_menu (bspr as BattleSprite)
 DIM already as bool = NO
 WITH bspr
  FOR i as integer = 0 TO .batmenu.numitems - 1
   WITH *.batmenu.items[i]
    IF .t = batmenu_SKIPTURN ANDALSO .sub_t = 1 THEN
     'sub_t=1 means this item was auto-added. sub_t will be =0 for manually added skip turn
     already = YES
    END IF
   END WITH
  NEXT i
 END WITH
 IF NOT already THEN
  append_menu_item(bspr.batmenu, readglobalstring(332, "Skip Turn", 20), batmenu_SKIPTURN, 1) 'Skip turn subtype 1 means auto-added
 END IF
END SUB

SUB remove_auto_added_skip_turn_from_battle_menu (bspr as BattleSprite)
 WITH bspr
  FOR i as integer = 0 TO .batmenu.numitems - 1
   WITH *.batmenu.items[i]
    IF .t = batmenu_SKIPTURN ANDALSO .sub_t = 1 THEN
     'sub_t=1 means this item was auto-added. sub_t will be =0 for manually added skip turn
     remove_menu_item bspr.batmenu, i
    END IF
   END WITH
  NEXT i
 END WITH
END SUB

'==============================================================================
'                                Victory & Defeat

SUB fulldeathcheck (byval killing_attack as integer, bat as BattleState, bslot() as BattleSprite, formdata as Formation)
 '--Runs check_death on all enemies, checks all heroes for death, and sets bat.death_mode if necessary
 'killing_attack is the attack ID that was just used, or -1 for none
 DIM deadguy as integer
 DIM dead_enemies as integer
 DIM dead_heroes as integer
 FOR deadguy = 4 TO 11
  IF bslot(deadguy).stat.cur.hp > 0 THEN
   'this enemy hasn't just spawned; it should fade out
   IF dieWOboss(deadguy, bslot()) THEN
    triggerfade deadguy, bslot()
   END IF
  END IF
 NEXT
 FOR deadguy = 0 TO 11
  check_death deadguy, killing_attack, bat, bslot(), formdata
 NEXT
 dead_enemies = 0
 FOR deadguy = 4 TO 11
  WITH bslot(deadguy)
   IF (.stat.cur.hp <= 0 ANDALSO .bequesting = NO ANDALSO .self_bequesting = NO) OR .hero_untargetable = YES OR .death_unneeded = YES OR .defector_target THEN dead_enemies += 1
  END WITH
 NEXT
 IF dead_enemies >= 8 THEN bat.death_mode = deathENEMIES
 '--Now check the heroes
 dead_heroes = 0
 FOR deadguy = 0 TO 3
  IF bslot(deadguy).stat.cur.hp <= 0 THEN dead_heroes += 1
 NEXT deadguy
 IF dead_heroes = 4 THEN bat.death_mode = deathHEROES
 export_battle_hero_stats bslot()
 evalherotags
 tag_updates
END SUB

SUB trigger_victory(byref bat as BattleState, bslot() as BattleSprite)
 '--Play the victory music
 IF gen(genVictMus) > 0 THEN wrappedsong gen(genVictMus) - 1

 '--Add rewards from still-alive enemies
 FOR slot as integer = 4 TO 11
  IF bslot(slot).stat.cur.hp > 0 ANDALSO bslot(slot).give_rewards_even_if_alive THEN
   enemy_death_rewards bat, bslot(slot)
  END IF
 NEXT

 '--Collect gold (which is capped at 2 billion max)
 gold = gold + bat.rew.plunder
 IF gold < 0 OR gold > 2000000000 THEN gold = 2000000000

 '--Give out experience
 export_battle_hero_stats bslot() 'Needed because distribute_party_experience() might level-up some heroes
 bat.rew.exper = distribute_party_experience(bat.rew.exper)
 import_battle_hero_stats bslot() 'Needed because we will likely be calling export_battle_hero_stats() again later

 '--Give items
 DIM i as integer = 0
 FOR i = 0 TO UBOUND(bat.rew.found)
  IF bat.rew.found(i).num > 0 THEN getitem bat.rew.found(i).id, bat.rew.found(i).num
 NEXT

 '-- Record rewards for after-battle usage
 gam.rew = bat.rew

 '--Trigger the display of end-of-battle rewards
 bat.vic.state = vicGOLDEXP
 bat.vic.display_ticks = 0
END SUB

FUNCTION distribute_party_experience (byval exper as integer) as integer
 '--Calculate amount of experience given to each hero, and returns the gained
 '--experience to display in the victory info: the "base" experience value,
 '--which is the amount given to live heroes in the active party
 DIM as double sumheroes, xp_mult(UBOUND(gam.hero))
 DIM as integer i

 FOR i = 0 TO UBOUND(gam.hero)
  IF gam.hero(i).id >= 0 THEN
   IF gam.hero(i).stat.cur.hp > 0 OR prefbit(19) THEN 'alive, or "Dead heroes gain share of experience" on
    IF i <= 3 THEN
     'active party
     xp_mult(i) = 1.0
    ELSEIF gam.hero(i).locked THEN
     'hero is in reserve, locked
     xp_mult(i) = gen(genLockedReserveXP) / 100.0
    ELSE
     'hero is in reserve, unlocked
     xp_mult(i) = gen(genUnlockedReserveXP) / 100.0
    END IF
   END IF
  END IF
  sumheroes += xp_mult(i)
 NEXT
 'debug "distribute_party_experience: exper = " & exper & " sumheroes = " & sumheroes

 'if there is more than one hero, and "Don't divide experience between heroes" is off,
 ' then divide the experience between the heroes
 IF sumheroes > 0 ANDALSO prefbit(30) = NO THEN exper = exper / sumheroes

 DIM gained_xp as bool = NO

 FOR i = 0 TO UBOUND(gam.hero)
  IF gam.hero(i).id >= 0 THEN
   gained_xp OR= giveheroexperience(i, xp_mult(i) * exper)
   'debug "hero " & i & " got " & CINT(xp_mult(i) * exper)
   updatestatslevelup i, NO
  END IF
 NEXT

 IF gained_xp = NO THEN exper = 0  'Everyone is at the level cap.

 RETURN exper
END FUNCTION

'Found an item, add to list of rewards
'TODO: convert found() to a variable length array
SUB RewardsState.add_item(itemid as integer, count as integer = 1)
 FOR j as integer = 0 TO UBOUND(found)
  WITH found(j)
   IF .num = 0 THEN .id = itemid
   IF .id = itemid THEN
    .num += count
    EXIT SUB
   END IF
  END WITH
 NEXT j
 debug "Found items array full!"
END SUB

FUNCTION should_victory_advance(byref bat as BattleState) as bool
 IF gen(genSkipBattleRewardsTicks) = -1 ORELSE game_check_use_key() ORELSE game_check_cancel_key() THEN RETURN YES
 IF (readmouse.release AND mouseLeft) ORELSE (readmouse.release AND mouseLeft) THEN RETURN YES
 IF gen(genSkipBattleRewardsTicks) > 0 THEN
  IF bat.vic.display_ticks > gen(genSkipBattleRewardsTicks) THEN
   bat.vic.display_ticks = 0
   RETURN YES
  END IF
 END IF
 RETURN NO
END FUNCTION

SUB draw_victory_box(numlines as integer = 4, page as integer)
 centerfuz 160, 10 + 5 * numlines, 280, 10 + 10 * numlines, 1, page
END SUB

SUB show_victory (byref bat as BattleState, bslot() as BattleSprite, page as integer)
 DIM tempstr as string
 WITH bat.vic
  .display_ticks += 1
  SELECT CASE .state
   CASE vicGOLDEXP
    '--print acquired gold and experience
    IF bat.rew.plunder > 0 OR bat.rew.exper > 0 THEN draw_victory_box 4, page
    IF bat.rew.plunder > 0 THEN
     tempstr = .gold_caption & " " & price_string(bat.rew.plunder) & "!"
     edgeprint tempstr, pCentered, 16, uilook(uiText), page
    END IF
    IF bat.rew.exper > 0 THEN
     tempstr = .exp_caption & " " & bat.rew.exper & " " & .exp_name & "!"
     edgeprint tempstr, pCentered, 28, uilook(uiText), page
    END IF
    IF should_victory_advance(bat) ORELSE (bat.rew.plunder = 0 ANDALSO bat.rew.exper = 0) THEN
     .state = vicLEVELUP
    END IF
   CASE vicLEVELUP
    '--print levelups
    DIM printed_lines as integer = 0
    DIM numlevelled as integer = 0
    FOR i as integer = 0 TO UBOUND(gam.hero)
     IF gam.hero(i).lev_gain <> 0 THEN numlevelled += 1
    NEXT
    IF numlevelled THEN draw_victory_box large(numlevelled, 4), page
    FOR i as integer = 0 TO UBOUND(gam.hero)
     SELECT CASE gam.hero(i).lev_gain
      CASE 1
       tempstr = .level_up_caption & " " & gam.hero(i).name
      CASE IS > 1
       tempstr = gam.hero(i).lev_gain & " " & .levels_up_caption & " " & gam.hero(i).name
     END SELECT
     IF gam.hero(i).lev_gain > 0 THEN
      edgeprint tempstr, pCenteredLeft, 12 + printed_lines * 10, uilook(uiText), page
      printed_lines += 1
     END IF
    NEXT i
    IF printed_lines = 0 ORELSE should_victory_advance(bat) THEN
     .state = vicSPELLS
     .showlearn = NO
     .learnwho = 0
     .learnlist = 0
     .learnslot = -1
    END IF
   CASE vicSPELLS
    '--print learned spells, one at a time
    IF .showlearn = NO THEN '--Not showing a spell yet. find the next one
     DO
      .learnslot += 1
      IF .learnslot > 23 THEN .learnslot = 0: .learnlist += 1
      IF .learnlist > 3 THEN .learnlist = 0: .learnwho += 1
      IF .learnwho > UBOUND(gam.hero) THEN ' We have iterated through all spell lists for all heroes, time to move on
       .state = vicITEMS
       .display_ticks = 0
       .item_name = ""
       .found_index = 0
       EXIT DO
      END IF
      IF readbit(gam.hero(.learnwho).learnmask(), 0, .learnlist * 24 + .learnslot) THEN
       'found a learned spell
       DIM learn_attack as AttackData
       loadattackdata learn_attack, gam.hero(.learnwho).spells(.learnlist, .learnslot) - 1
       .item_name = gam.hero(.learnwho).name + .learned_caption + learn_attack.name
       .showlearn = YES
       IF learn_attack.learn_sound_effect > 0 THEN playsfx learn_attack.learn_sound_effect - 1
       EXIT DO
      END IF
     LOOP
    ELSE' Found a learned spell to display, show it until a keypress
     IF should_victory_advance(bat) THEN
      .showlearn = NO ' hide the display (which causes us to search for the next learned spell)
     END IF
     draw_victory_box 4, page
     edgeprint .item_name, pCenteredLeft, 22, uilook(uiText), page
    END IF
   CASE vicITEMS
    '--print found items, one at a time
    DIM byref pickedup as RewardsStateItem = bat.rew.found(.found_index)
    '--check to see if we are currently displaying a gotten item
    IF .item_name = "" THEN
     '--if not, check to see if there are any more gotten items to display
     IF pickedup.num = 0 THEN
      .state = vicEXITDELAY
      .display_ticks = 0
      EXIT SUB
     END IF
     '--get the item name
     .item_name = readitemname(pickedup.id)
     'tag_updates called when exiting battle() to handle all at once
    END IF
    '--if the present item is gotten, show the caption
    IF pickedup.num = 1 THEN
     tempstr = .item_caption & " " & .item_name
    ELSE
     tempstr = .plural_item_caption & " " & pickedup.num & " " & .item_name
    END IF
    draw_victory_box 4, page
    edgeprint tempstr, pCenteredLeft, 22, uilook(uiText), page
    '--check for a keypress
    IF should_victory_advance(bat) THEN
     .found_index += 1
     .item_name = ""  'Load the next item
     '--if there are no further items, exit
     IF .found_index > UBOUND(bat.rew.found) THEN .state = vicEXITDELAY
    END IF
  END SELECT
 END WITH
END SUB

'==============================================================================

SUB reset_battle_state (byref bat as BattleState)
 'This could become a constructor for BattleState when we support the -lang fb dialect
 '... -_-

 WITH bat

  setup_battle_slices bat

  WITH .inv_scroll
   .first = 0
   .last = INT(last_inv_slot() / 3)
   .size = 8
  END WITH
  WITH .inv_scroll_rect
   .x = 20
   .y = 5
   .wide = 292
   '.high set later
  END WITH

  .anim_ready = NO
  .wait_frames = 0
  .wait_for_slot = -1
  .ticks = 0

  .level_mp_caption = readglobalstring(160, "Level MP", 20)
  .cancel_spell_caption = readglobalstring(51, "(CANCEL)", 10)
  .cannot_run_caption = readglobalstring(147, "CANNOT RUN!", 20)

  .caption_time = 0
  .caption_delay = 0
  .caption = ""

  .alert_ticks = 0
  .alert = ""

  .acting = 0
  .player_turn = -1
  .enemy_turn = -1
  .next_hero = 0
  .next_enemy = 0
  .menu_mode = batMENUHERO

  .laststun = 0
  .most_recently_spawned_by_attack = -1

 END WITH
 
 reset_targetting bat
 reset_attack bat
 reset_victory_state bat.vic
 reset_rewards_state bat.rew
 reset_rewards_state gam.rew
END SUB

SUB reset_targetting (byref bat as BattleState)
 WITH bat.targ
  .hit_dead = NO
 END WITH
END SUB

SUB reset_attack (byref bat as BattleState)
 DIM i as integer
 WITH bat.atk
  .id = -1
  .was_id = -1
  .non_elemental = NO
  FOR i = 0 TO UBOUND(.elemental)
  .elemental(i) = NO
  NEXT i
 END WITH
END SUB

SUB reset_victory_state (byref vic as VictoryState)
 'This could become a constructor for VictoryState when we support the -lang fb dialect
 WITH vic
  .state = 0
  .showlearn = NO
  .learnwho = 0
  .learnlist = 0
  .learnslot = -1
  .item_name = ""
  .found_index = 0
  '--Cache some global strings here
  .gold_caption = readglobalstring(125, "Found", 10)
  .exp_caption  = readglobalstring(126, "Gained", 10)
  .item_caption = readglobalstring(139, "Found a", 20)
  .plural_item_caption = readglobalstring(141, "Found", 20)
  .exp_name     = readglobalstring(33, "Experience", 10)
  .level_up_caption  = readglobalstring(149, "Level up for", 20)
  .levels_up_caption = readglobalstring(151, "levels for", 20)
  .learned_caption = " " + readglobalstring(124, "learned", 10) + " "
 END WITH
END SUB

SUB checkitemusability(iuse() as integer, bslot() as BattleSprite, byval who as integer)
 'Iterate through the iuse() bitfield and mark any items that are usable
 DIM i as integer
 DIM item as ItemDef
 DIM attack as AttackData

 FOR i = 0 TO inventoryMax
  setbit iuse(), 0, i, 0 ' Default each slot to unusable
  IF inventory(i).used THEN
   loaditemdata item, inventory(i).id
   IF item.battle_items_menu_attack >= 0 THEN ' This item is usable in battle
    loadattackdata attack, item.battle_items_menu_attack
    IF attack.check_costs_as_item THEN
     '--This attack has the bitset that requires cost checking when used from an item
     IF atkallowed(attack, who, 0, 0, bslot(who)) THEN
      setbit iuse(), 0, i, 1
     END IF
    ELSE
     '--No cost checking for this item
     setbit iuse(), 0, i, 1
    END IF
   END IF
  END IF
 NEXT i
END SUB

FUNCTION checkNoRunBit (bslot() as BattleSprite) as bool
 FOR i as integer = 4 TO 11
  IF bslot(i).stat.cur.hp > 0 AND bslot(i).vis = YES AND bslot(i).unescapable = YES THEN RETURN YES
 NEXT i
 RETURN NO
END FUNCTION

SUB checkAtkTagConds (atk_id as integer, byval check as AttackTagConditionEnum)
 DIM attack as AttackData
 loadattackdata attack, atk_id
 checkAtkTagConds attack, atktagOnTargettingFailed
END SUB

SUB checkAtkTagConds (attack as AttackData, byval check as AttackTagConditionEnum)
 ' If we add more than 2 tag checks to the attack data, add the new ones here
 checkTagCond attack.tagset(0), check
 checkTagCond attack.tagset(1), check
END SUB

SUB checkTagCond (byref t as AttackDataTag, byval check as AttackTagConditionEnum)
 't.condition - type
 'check = condition type
 't.tag - the tag to be set
 't.tagcheck - the tag to check
 IF t.condition = check THEN
  IF t.tagcheck <> 0 THEN
   IF NOT istag(t.tagcheck, 0) THEN EXIT SUB
  END IF
  settag t.tag  'Set the original damned tag!
  tag_updates
 END IF
END SUB

SUB calc_hero_elementals (elemental_resists() as single, byval who as integer)
 'Calculate a hero's elemental resists after taking equipment into account
 'elemental_resists() (the destination) should be sized up to at least gen(genNumElements) - 1;
 'who is a hero slot number.
 'This is used both here and in the status menu.

 REDIM itemelementals() as single
 DIM allelementals(5, gen(genNumElements) - 1) as single
 DIM oneelement(5) as single
 DIM byref hero as HeroState = gam.hero(who)

 DIM _NaN as single = 0.0f
 _NaN = 0.0f/_NaN

 '--first load data into allelementals()

 '--get native hero resistances
 FOR i as integer = 0 TO gen(genNumElements) - 1
  allelementals(0, i) = hero.elementals(i)
 NEXT i

 '--data from all equipped items fill rest of matrix
 FOR j as integer = 0 TO 4
  IF hero.equip(j).id >= 0 THEN
   LoadItemElementals hero.equip(j).id, itemelementals()

   FOR i as integer = 0 TO gen(genNumElements) - 1
    allelementals(j + 1, i) = itemelementals(i)
   NEXT i
  ELSE
   FOR i as integer = 0 TO gen(genNumElements) - 1
    allelementals(j + 1, i) = _NaN
   NEXT i
  END IF
 NEXT j

 '--transpose the matrix (I wish), and merge equipment elemental resists
 FOR i as integer = 0 TO gen(genNumElements) - 1
  FOR j as integer = 0 TO 5
   oneelement(j) = allelementals(j, i)
  NEXT j
  elemental_resists(i) = equip_elemental_merge(oneelement(), gen(genEquipMergeFormula))
 NEXT i
END SUB

SUB invertstack
'--this is a hack so I can use the stack like a fifo
DIM i as integer
DIM stackdepth as integer

stackdepth = stackpos() - bstackstart

FOR i = 0 TO stackdepth - 1
 buffer(i) = popdw
NEXT i

FOR i = 0 TO stackdepth - 1
 pushdw buffer(i)
NEXT i

END SUB

SUB quickinflict (byval harm as integer, byval targ as integer, bslot() as BattleSprite, byval col as integer=-1)
 '--quick damage infliction to hp. no bells and whistles
 DIM max_bound as integer
 WITH bslot(targ)

  IF gen(genDamageCap) > 0 THEN harm = small(harm, gen(genDamageCap))

  .harm.ticks = gen(genDamageDisplayTicks)
  .harm.pos.x = .x + (.w * .5)
  .harm.pos.y = .y + (.h * .5)
  IF harm < 0 THEN
   .harm.text = "+" & ABS(harm)
   .harm.col = uilook(uiBattleHeal)
  ELSE
   .harm.text = STR(harm)
  END IF
  IF col >= 0 THEN
   .harm.col = col
  END IF

  max_bound = large(.stat.cur.hp, .stat.max.hp)
  .stat.cur.hp = bound(.stat.cur.hp - harm, 0, max_bound)
 END WITH
END SUB

'==============================================================================
'                               Animation ops

SUB anim_end()
 pushdw 0
END SUB

SUB anim_inflict(byval who as integer, byval target_count as integer)
 pushdw 10: pushdw who: pushdw target_count
END SUB

SUB anim_disappear(byval who as integer)
 pushdw 6: pushdw who
END SUB

SUB anim_setpos(byval who as integer, byval x as integer, byval y as integer, byval direction as integer)
 pushdw 3: pushdw who: pushdw x: pushdw y: pushdw direction
END SUB

SUB anim_setz(byval who as integer, byval z as integer)
 pushdw 11: pushdw who: pushdw z
END SUB

SUB anim_appear(byval who as integer)
 pushdw 5: pushdw who
END SUB

SUB anim_hide(byval who as integer)
 pushdw 26: pushdw who
END SUB

SUB anim_unhide(byval who as integer)
 pushdw 27: pushdw who
END SUB

' For `ticks` ticks, move at velocity XY(xstep,ystep)
SUB anim_velocity(who as integer, xstep as integer, ystep as integer, ticks as integer)
 pushdw 2: pushdw who: pushdw xstep: pushdw ystep: pushdw ticks
END SUB

' Move to an XY position in `ticks` ticks
SUB anim_absmove(byval who as integer, byval tox as integer, byval toy as integer, byval ticks as integer)
 pushdw 8: pushdw who: pushdw tox: pushdw toy: pushdw ticks
END SUB

' Move relative from the current position in `ticks` ticks.
SUB anim_relmove(byval who as integer, byval byx as integer, byval byy as integer, byval ticks as integer)
 pushdw 20: pushdw who: pushdw byx: pushdw byy: pushdw ticks
END SUB

' For zticks ticks, move zstep pixels/tick
SUB anim_zvelocity(byval who as integer, byval zstep as integer, byval zticks as integer)
 pushdw 15: pushdw who: pushdw zstep: pushdw zticks
END SUB

' Move to an Z position in zticks ticks.
SUB anim_abszmove(byval who as integer, byval toz as integer, byval zticks as integer)
 pushdw 24: pushdw who: pushdw toz: pushdw zticks
END SUB

SUB anim_wait(byval ticks as integer)
 pushdw 13: pushdw ticks
END SUB

SUB anim_setframe(byval who as integer, byval frame as integer)
 pushdw 7: pushdw who: pushdw frame
END SUB

' Wait for all BattleSprite .sl and .sprite slices to stop moving
' Causes a 1 tick wait even if nothing is moving
SUB anim_waitforall()
 pushdw 9
END SUB

' Wait for a BattleSprite's .sl and .sprite slices to stop moving
SUB anim_waitformove(byval who as integer)
 pushdw 29: pushdw who
END SUB

SUB anim_walktoggle(byval who as integer)
 pushdw 14: pushdw who
END SUB

SUB anim_sound(byval which as integer)
 pushdw 16: pushdw which
END SUB

SUB anim_align(byval who as integer, byval target as integer, byval dire as integer, byval offset as integer)
 pushdw 17: pushdw who: pushdw target: pushdw dire: pushdw offset
END SUB

' Position 'who' is its center matches with 'target', plus an offset.
' Note: this doesn't make total sense, because it doesn't use Z position, so may be behind or infront.
SUB anim_setcenter(byval who as integer, byval target as integer, byval offx as integer, byval offy as integer)
 pushdw 18: pushdw who: pushdw target: pushdw offx: pushdw offy
END SUB

SUB anim_align2(byval who as integer, byval target as integer, byval edgex as integer, byval edgey as integer, byval offx as integer, byval offy as integer)
 pushdw 19: pushdw who: pushdw target: pushdw edgex: pushdw edgey: pushdw offx: pushdw offy
END SUB

SUB anim_setdir(byval who as integer, byval d as integer)
 pushdw 21: pushdw who: pushdw d
END SUB

SUB anim_skip_ahead_if_targetless()
 pushdw 22
END SUB

SUB anim_skip_to_here()
 'Place markers to look for later
 pushdw 23: pushdw ANIM_SKIP_SENTINEL: pushdw ANIM_SKIP_SENTINEL
END SUB

SUB anim_checkpoint()
 pushdw 25
END SUB

SUB anim_flinchstart(byval who as integer, bslot() as BattleSprite, byref attack as AttackData)
 '--If enemy can flinch and if attack allows flinching
 IF bslot(who).never_flinch = NO AND attack.targ_does_not_flinch = NO THEN
  'Starting a new flinch while a movement is in progress would result in not returning to the intended position.
  'Currently that never happens, this is future proofing for more customisable animations.
  anim_waitformove who

  'Move 6 pixels in 3 ticks
  DIM xvelocity as integer = IIF(is_enemy(who), -2, 2)
  anim_velocity who, xvelocity, 0, 3
  IF is_hero(who) THEN
   IF attack.cure_instead_of_harm = NO THEN
    '--Show Harmed frame
    anim_setframe who, frameHURT
   ELSE
    '--Show attack1 frame when healed
    anim_setframe who, frameATTACKA
   END IF
  END IF
  pushdw 28: pushdw who  'flinch(who)
 END IF
END SUB

SUB flinch_anim_eachtick(byval who as integer, bslot() as BattleSprite)
 WITH bslot(who)
  .flinch_anim -= 1
  'After 3 ticks of forward flinch, start the slide back to the initial position
  IF .flinch_anim = 3 THEN
   DIM xvelocity as integer = IIF(is_enemy(who), 2, -2)
   'anim_velocity who, xvelocity, 0, 3
   .set_vel_x xvelocity, 3
   'anim_setframe who, frameSTAND
   .frame = frameSTAND
   IF is_hero(who) THEN .walk = 0
  END IF
 END WITH
END SUB

'==============================================================================
'                                 Enemy death

FUNCTION count_dissolving_enemies(bslot() as BattleSprite) as integer
 'Counts enemies that are in the process of dying, but have not finished dying yet
 'This includes both dissolve animations and on-death bequest attacks
 DIM i as integer
 DIM count as integer = 0
 FOR i = 4 TO 11
  IF bslot(i).dissolve > 0 ORELSE bslot(i).bequesting THEN count += 1
 NEXT i
 RETURN count
END FUNCTION

SUB spawn_on_death(byval deadguy as integer, byval killing_attack as integer, byref bat as BattleState, formdata as Formation, bslot() as BattleSprite)
 'killing_attack is the id of the attack that killed the target or -1 if the target died without a specific attack
 DIM attack as AttackData
 DIM slot as integer
 DIM i as integer
 IF NOT is_enemy(deadguy) THEN EXIT SUB ' Only works for enemies
 IF killing_attack >= 0 THEN
  'This death is the result of an attack
  loadattackdata attack, killing_attack
  IF attack.no_spawn_on_kill <> 0 THEN
   'This attack had the "Do not trigger spawning on death" bitset
   EXIT SUB
  END IF
 END IF
 WITH bslot(deadguy)
  IF .enemy.spawn.non_elemental_death > 0 AND bat.atk.non_elemental = YES THEN ' spawn on non-elemental death
   FOR i = 1 TO .enemy.spawn.how_many
    slot = find_empty_enemy_slot(formdata)
    IF slot > -1 THEN
     formdata.slots(slot).id = .enemy.spawn.non_elemental_death - 1
     loadfoe(slot, formdata, bat, bslot())
    END IF
   NEXT i
   .enemy.spawn.non_elemental_death = 0
  END IF
  IF .enemy.spawn.on_death > 0 THEN ' spawn on death
   FOR i = 1 TO .enemy.spawn.how_many
    slot = find_empty_enemy_slot(formdata)
    IF slot > -1 THEN
     formdata.slots(slot).id = .enemy.spawn.on_death - 1
     loadfoe(slot, formdata, bat, bslot())
    END IF
   NEXT i
   .enemy.spawn.on_death = 0
  END IF
 END WITH
END SUB

FUNCTION find_empty_enemy_slot(formdata as Formation) as integer
 'Returns index of first empty slot, or -1 if none was found
 DIM i as integer
 FOR i = 0 TO 7
  IF formdata.slots(i).id = -1 THEN RETURN i
 NEXT i
 RETURN -1
END FUNCTION

'Whether an enemy should die, because there are no bosses
FUNCTION dieWOboss(byval who as integer, bslot() as BattleSprite) as bool
 '--count bosses
 FOR j as integer = 4 TO 11
  '--is it a boss?
  IF bslot(j).is_boss = YES THEN
   '-- is it alive?
   IF bslot(j).stat.cur.hp > 0 THEN
    RETURN NO
   END IF
  END IF
 NEXT j
 '--if there are no bossess...
 '--should it die without a boss?
 IF bslot(who).die_without_boss = YES THEN
  bslot(who).stat.cur.hp = 0
  RETURN YES
 END IF
END FUNCTION

'Start death/flee animation or switch to dead hero frame
SUB triggerfade(byval who as integer, bslot() as BattleSprite)
 'If the target is really dead...
 IF bslot(who).stat.cur.hp <= 0 THEN
  IF is_hero(who) THEN
   '--for heroes, a .dissolve > 0 is used to trigger the animate code
   '  to force the hero to use their death frame
   bslot(who).dissolve = 1
  END IF
  IF is_enemy(who) THEN
   'Don't restart the dissolve animation when hitting an enemy multiple times
   IF bslot(who).dissolve > 0 OR bslot(who).vis = NO THEN EXIT SUB
   bslot(who).dissolve = bslot(who).deathtime
   '--flee as alternative to death
   IF bslot(who).flee_instead_of_die THEN
    bslot(who).fleeing = YES
    'the number of ticks it takes an enemy to run away is based on its distance
    'from the left side of the screen and its width. Enemys flee at 10 pixels per tick
    bslot(who).dissolve = (bslot(who).w + bslot(who).x) / 10
   END IF
  END IF
 END IF
END SUB

FUNCTION is_among_targets(byval slot as integer, t() as integer) as bool
 FOR i as integer = 0 TO UBOUND(t)
  IF t(i) > -1 ANDALSO t(i) = slot THEN RETURN YES
 NEXT i
 RETURN NO
END FUNCTION

SUB check_death(byval deadguy as integer, byval killing_attack as integer, byref bat as BattleState, bslot() as BattleSprite, formdata as Formation)
'killing_attack contains attack id or -1 when no attack is relevant.

 '--Ignore empty hero/enemy slots.
 IF is_hero(deadguy) ANDALSO gam.hero(deadguy).id = -1 THEN EXIT SUB
 IF is_enemy(deadguy) ANDALSO formdata.slots(deadguy - 4).id = -1 THEN EXIT SUB

 '--Check to see if this is an already-dead enemy doing a bequest attack
 IF bslot(deadguy).bequesting = YES THEN
  IF has_queued_attacks(deadguy) THEN
   EXIT SUB
  END IF
  bslot(deadguy).bequesting = NO
  triggerfade deadguy, bslot()
 END IF
 IF bslot(deadguy).self_bequesting THEN
  'Must wait until self-bequest attack is completely finished
  EXIT SUB
 END IF

 '--Do not proceed unless the target is dead
 IF bslot(deadguy).stat.cur.hp > 0 THEN EXIT SUB

 '--deadguy is really dead (includes already dead heroes or empty hero slots)
 'Death animation is not done yet here, so be cautious about what gets cleaned up here.
 'Full cleanup of bslot() records doesn't happen until something else is loaded, in loadfoe
 '(but until then the record contains garbage, so it doesn't need cleaned up)
 'But for enemies, will now mark this formation slot as empty, so it might be possible
 'for an enemy to get spawned in this bslot before the fade is finished.
 '(Not sure if that actually happens)
 bslot(deadguy).vis = NO
 bslot(deadguy).ready = NO
 bslot(deadguy).attack = 0
 bslot(deadguy).d = 0
 '--reset poison/regen/stun/mute
 WITH bslot(deadguy).stat
  .cur.poison = .max.poison
  .cur.regen  = .max.regen
  .cur.stun   = .max.stun
  .cur.mute   = .max.mute
 END WITH
 '-- remove any attack queue entries
 'FIXME: actually we should only do this if the death isn't cancelled by an on-death attack,
 'because cancelling pending attacks will screw up a lot of stuff, such as buff-debuff chains!
 FOR i as integer = 0 TO UBOUND(atkq)
  WITH atkq(i)
   IF .used ANDALSO .attacker = deadguy THEN
    clear_attack_queue_slot i
   END IF
  END WITH
 NEXT i
 '-- if it is a dead hero's turn, cancel menu
 IF bat.player_turn = deadguy THEN
  bat.player_turn = -1
  bat.menu_mode = batMENUHERO
  bat.targ.mode = targNONE
 END IF
 '-- if it is a dead enemy's turn, cancel ai
 IF bat.enemy_turn = deadguy THEN bat.enemy_turn = -1

 IF is_enemy(deadguy) THEN
  WITH bslot(deadguy)

   IF .enemy.bequest_attack > 0 ANDALSO NOT .already_bequested THEN
    'Trigger an on-death bequest attack, and defer dying until later.
    DIM beq_t(11) as integer
    'Target the bequest attack, but force it to be blocking
    IF autotarget(deadguy, .enemy.bequest_attack - 1, bslot(), bat, beq_t(), , YES) THEN
     '--Bequest attack only happens if it has a valid target
     .bequesting = YES
     .self_bequesting = is_among_targets(deadguy, beq_t())
     .already_bequested = YES
     .vis = YES
     .dissolve = 0
     .fleeing = NO
    END IF
    EXIT SUB
   END IF
  END WITH

  enemy_death_sfx bslot(deadguy)
  'Plunder and on-death spawning, and mark formation slot empty
  dead_enemy deadguy, killing_attack, bat, bslot(), formdata
 END IF

 battle_reevaluate_dead_targets deadguy, bat, bslot()
END SUB

SUB dead_enemy(byval deadguy as integer, byval killing_attack as integer, byref bat as BattleState, bslot() as BattleSprite, formdata as Formation)
 'Do on-death effects which happen both on normal death and on instant-death-on-spawn:
 'give rewards, spawn enemies, and mark a formation slot as empty but NO other cleanup!
 'On-death bequest attacks are processed in check_death (they delay death)
 'NOTE: dissolve/flee animation will be ongoing.
 'killing_attack is the id of the attack that killed the target or -1 if the target died without a specific attack
 DIM enemynum as integer = deadguy - 4
 '--spawn enemies before freeing the formdata slot to avoid infinite loops. however this might need to be changed to fix morphing enemies?
 spawn_on_death deadguy, killing_attack, bat, formdata, bslot()

 enemy_death_rewards bat, bslot(deadguy)

 ' remove dead enemy from formation
 formdata.slots(enemynum).id = -1
END SUB

SUB enemy_death_rewards(byref bat as BattleState, battler as BattleSprite)
 WITH battler
  bat.rew.plunder += .enemy.reward.gold
  bat.rew.exper += .enemy.reward.exper
  IF bat.rew.exper > 1000000 THEN bat.rew.exper = 1000000 '--this one million limit is totally arbitrary
  IF randint(100) < .enemy.reward.item_rate THEN
   bat.rew.add_item(.enemy.reward.item)
  ELSEIF randint(100) < .enemy.reward.rare_item_rate THEN
   bat.rew.add_item(.enemy.reward.rare_item)
  END IF
 END WITH
END SUB

SUB enemy_death_sfx(battler as BattleSprite)
 IF battler.death_sfx = 0 THEN
  IF gen(genDefaultDeathSFX) > 0 THEN
   playsfx gen(genDefaultDeathSFX) - 1
  END IF
 ELSEIF battler.death_sfx > 0 THEN
  playsfx battler.death_sfx - 1
 END IF
END SUB

'==============================================================================
'                                   Hero AI

SUB hero_ai (byref bat as BattleState, bspr as BattleSprite, byval attacker_id as integer, bslot() as BattleSprite, st() as HeroDef)
 DIM valid_for_ai as integer vector 'attack id
 DIM list_type_v as integer vector
 DIM lmp_lev_v as integer vector
 v_new valid_for_ai
 v_new list_type_v
 v_new lmp_lev_v

 DIM atk_id as integer = -1
 DIM list_type as integer = 0
 DIM lmp_lev as integer = -1

 FOR i as integer = 0 TO bspr.batmenu.numitems - 1
  WITH *bspr.batmenu.items[i]
   IF NOT .disabled THEN
    DIM node as NodePtr = .dataptr
    IF GetChildNodeBool(node, "exclude_auto_battle") THEN CONTINUE FOR
    SELECT CASE .t
     CASE batmenu_ATTACK:
      v_append valid_for_ai, .sub_t
      v_append list_type_v, 0
      v_append lmp_lev_v, -1
     CASE batmenu_SPELLS:
      list_type = st(bspr.index).list_type(.sub_t)
      lmp_lev = -1
      SELECT CASE list_type
       CASE 0 ' Regular spells
        atk_id = random_attack_from_spell_menu(bat, bspr, .sub_t, st(), YES, YES)
       CASE 1 ' Level MP
        atk_id = random_attack_from_spell_menu(bat, bspr, .sub_t, st(), YES, YES)
        IF atk_id > -1 THEN
         DIM atk_index as integer = find_attack_in_spell_menu(atk_id, bat, bspr, .sub_t, st(), YES)
         lmp_lev = atk_index \ 3
        END IF
       CASE 2 ' Random spells
        'Don't bother trying to support prefbit(54) for random spell lists. That bug was fixed
        'before this feature was added, and costs will be re-checked in a moment anyway
        atk_id = random_attack_from_spell_menu(bat, bspr, .sub_t, st(), YES, YES)
      END SELECT
      IF atk_id > -1 THEN
       v_append valid_for_ai, atk_id
       v_append list_type_v, list_type
       v_append lmp_lev_v, lmp_lev
      END IF
     CASE batmenu_ITEMS:
      'Hero AI doesn't support using items
     CASE batmenu_SKIPTURN:
      'This will not be used (but the hero will skip their turn if no other action is possible)
    END SELECT
   END IF
  END WITH
 NEXT i

 DIM atk as AttackData
 DIM choice_count as integer
 DIM menu_id as integer
 atk_id = -1

 DO
  choice_count = v_len(valid_for_ai)
  IF choice_count > 0 THEN
   DIM index as integer = randint(choice_count)
   atk_id = *v_at(valid_for_ai, index)
   list_type = *v_at(list_type_v, index)
   lmp_lev = *v_at(lmp_lev_v, index)
   loadattackdata atk, atk_id
   IF NOT atk.exclude_from_hero_auto_battle ANDALSO atkallowed(atk, attacker_id, list_type, lmp_lev, bslot(attacker_id)) THEN
    'this attack is good, continue on to target selection
    EXIT DO
   END IF
   'If atkallowed failed, remove this item
   v_delete_slice valid_for_ai, index, index+1
   v_delete_slice list_type_v, index, index+1
   v_delete_slice lmp_lev_v, index, index+1
  ELSE
   'All choices eliminated, fail gracefully
   debuginfo "Hero AI: hero " & attacker_id & " gave up on finding an attack"
   atk_id = -1
   EXIT DO
  END IF
 LOOP

 'If a usable attack was found, target it
 IF atk_id >= 0 THEN

  IF bat.turn.mode = turnACTIVE THEN
   update_turn_delays_in_attack_queue attacker_id
  END IF

  'Note: if autotargetting fails the targetless attack gets queued anyway, wasting the hero's turn
  autotarget attacker_id, atk, bslot(), bat
  bspr.consume_lmp = lmp_lev

  IF bat.turn.mode = turnACTIVE THEN
   bslot(attacker_id).active_turn_num += 1
   'debug "Hero " & attacker_id & " " & bslot(attacker_id).name & " turn #" & bslot(attacker_id).active_turn_num
  END IF

  'ready for next attack
  bslot(attacker_id).ready = NO
  bslot(attacker_id).ready_meter = 0

 END IF
 
 v_free valid_for_ai
 v_free list_type_v
 v_free lmp_lev_v

END SUB

'==============================================================================
'                                   Enemy AI

'Used for enemies, not the enemy team
FUNCTION enemy_is_weak(byref bspr as BattleSprite) as bool
 DIM weakhp as integer = gen(genEnemyWeakHP)
 IF bspr.stat.cur.hp < 0.01 * bspr.stat.max.hp * weakhp THEN RETURN YES
 RETURN NO
END FUNCTION

'Figure out which enemy attack list should be used.
FUNCTION pick_enemy_attack_list(slot as integer, bslot() as BattleSprite) as EnemyAIEnum
 DIM ai as EnemyAIEnum = aiNormal

 'if HP is less than the threshold, go into desperation mode
 IF enemy_is_weak(bslot(slot)) THEN ai = aiWeak

 'Go into alone mode if no allies without "Ignored for "Alone" status" bit
 IF count_allies(slot, bslot()) = 0 THEN ai = aiAlone

 'Make sure that the current attack set contains attacks that can actually be
 'used, otherwise fall back. MP Idiot is ignored, so fallback even if they would
 'normally have wasted their turn, however the player might be in control.
 IF count_available_attacks_in_ai_list(ai, slot, bslot()) = 0 THEN
  ai = aiNormal
  IF enemy_is_weak(bslot(slot)) THEN
   ai = aiWeak
   IF count_available_attacks_in_ai_list(ai, slot, bslot()) = 0 THEN ai = aiNormal
  END IF
 END IF
 IF count_available_attacks_in_ai_list(ai, slot, bslot()) = 0 THEN ai = aiNone

 RETURN ai
END FUNCTION

SUB enemy_ai (byref bat as BattleState, bslot() as BattleSprite, formdata as Formation)

 'Pick attack list before spawning any enemies which would change Alone status
 DIM ai as EnemyAIEnum = pick_enemy_attack_list(bat.enemy_turn, bslot())

 'spawn allies when alone
 WITH bslot(bat.enemy_turn)
  'Any live allies without "Ignored for "Alone" status" bit?
  IF .enemy.spawn.when_alone > 0 ANDALSO count_allies(bat.enemy_turn, bslot()) = 0 THEN
   FOR j as integer = 1 TO .enemy.spawn.how_many
    DIM slot as integer = find_empty_enemy_slot(formdata)
    IF slot > -1 THEN
     formdata.slots(slot).id = .enemy.spawn.when_alone - 1
     loadfoe slot, formdata, bat, bslot()
    END IF
   NEXT j
  END IF
 END WITH

 IF ai = aiNone THEN
  'if no valid ai set is available, the enemy loses its turn
  bat.enemy_turn = -1
  EXIT SUB
 END IF

 'pick a random attack
 DIM atk_id as integer
 DIM atk as AttackData
 DIM safety as integer = 0
 DO
  WITH bslot(bat.enemy_turn)
   SELECT CASE ai
    CASE aiNormal: atk_id = .enemy.regular_ai(randint(5))
    CASE aiWeak:   atk_id = .enemy.desperation_ai(randint(5))
    CASE aiAlone:  atk_id = .enemy.alone_ai(randint(5))
   END SELECT
  END WITH
  IF atk_id > 0 THEN
   'load the data for this attack
   loadattackdata atk, atk_id - 1
  
   IF atkallowed(atk, bat.enemy_turn, 0, 0, bslot(bat.enemy_turn)) THEN
    'this attack is good, continue on to target selection
    EXIT DO
   ELSE
    'this attack is unusable
    atk_id = 0
    IF bslot(bat.enemy_turn).stat.cur.mp - atk.mp_cost < 0 THEN
     'inadequate MP was the reason for the failure
     'MP-idiot loses its turn
     IF bslot(bat.enemy_turn).mp_idiot = YES THEN
       bslot(bat.enemy_turn).ready = NO
       bslot(bat.enemy_turn).ready_meter = 0
       bat.enemy_turn = -1
       EXIT SUB
     END IF
    END IF
    'currently, item requirements are disregarded. should they be? Maybe they should
    'come out of theft items?
   END IF
  END IF
  safety += 1
  IF safety > 99 THEN
   'give up eventually
   debug "Enemy AI safety Warning: enemy " & bat.enemy_turn & " gave up after " & safety & " tries"
   bat.enemy_turn = -1
   EXIT SUB
  END IF
 LOOP

 IF bat.turn.mode = turnACTIVE THEN
  update_turn_delays_in_attack_queue bat.enemy_turn
 END IF

 'Note: if autotargetting fails the targetless attack gets queued anyway, wasting the enemy's turn
 autotarget bat.enemy_turn, atk, bslot(), bat

 IF bat.turn.mode = turnACTIVE THEN
  bslot(bat.enemy_turn).active_turn_num += 1
  'debug "Enemy " & bat.enemy_turn & " " & bslot(bat.enemy_turn).name & " turn #" & bslot(bat.enemy_turn).active_turn_num
 END IF

 'ready for next attack
 bslot(bat.enemy_turn).ready = NO
 bslot(bat.enemy_turn).ready_meter = 0
 bat.enemy_turn = -1

END SUB

'==============================================================================
'                                    Menus


SUB heromenu (byref bat as BattleState, bslot() as BattleSprite, st() as HeroDef)

 DIM do_pick as bool = NO
 DIM do_close as bool = NO
  
 WITH bslot(bat.player_turn)
  IF .stat.cur.stun < .stat.max.stun THEN
   'Normally a hero cannot get a turn when stunned, but this extra check is needed
   'for the race condition that happens when a hero gets stunned while it is their turn in turnACTIVE mode
   bat.next_hero = bat.player_turn
   bat.player_turn = -1
   EXIT SUB
  END IF

  IF game_check_cancel_key() THEN do_close = YES
 
  usemenusounds
  usemenu .menust
  IF game_battle_check_use_key() THEN do_pick = YES
  
  IF get_gen_bool("/mouse/mouse_battles") THEN
   IF .menust.hover >= 0 THEN
    IF (readmouse.release AND mouseLeft) ANDALSO .menust.hover = .menust.pt THEN
     do_pick = YES
     'Clear the mouse release because targetting starts on this same tick
     readmouse.clearclick(mouseLeft)
    END IF
   END IF
   IF .batmenu.items[.menust.pt]->t = batmenu_ATTACK THEN
    'The currently focused menu item is an attack
    IF (readmouse.clicks AND mouseLeft) ANDALSO .menust.hover = -1 THEN
     'Left mouse button down outside the menu, try to go to targetting mode
     do_pick = YES
     bat.targ.must_hover_valid_target = YES
    END IF
   END IF
   IF (readmouse.release AND mouseRight) ANDALSO readmouse.drag_dist < 10 THEN do_close = YES
   IF bat.mouse_running >= 10 THEN do_close = YES ' Always close the menu while mouse running
  END IF

  IF do_close THEN
   '--skip turn
   'debug "Requested to cancel picking attacks"
   bat.next_hero = bat.player_turn
   bat.player_turn = -1
   IF is_enemy(bat.next_hero) THEN
    bat.enemy_turn = -1
   END IF
   IF bat.turn.mode = turnTURN THEN
    IF prefbit(48) = NO THEN  '"!Press ESC to cancel/change a hero's attack"
     '--Switch into "reverse", meaning go back to the previous hero
     bat.turn.reverse = YES
    END IF
   END IF
   'Don't loop the sound effect while holding down Cancel to allow time to pass
   IF carray(ccCancel) AND 4 THEN menusound gen(genCancelSFX)
   EXIT SUB
  END IF
  
  IF do_pick THEN
   '--use menu item
   menusound gen(genAcceptSFX)
   WITH *.batmenu.items[.menust.pt]
    IF NOT .disabled THEN
     SELECT CASE .t
      CASE batmenu_ATTACK:
       bslot(bat.player_turn).attack = .sub_t + 1
       bat.targ.mode = targSETUP
       clearkeys
       EXIT SUB
      CASE batmenu_SPELLS:
       bat.listslot = .sub_t
       init_spell_menu bat, bslot(), st()
      CASE batmenu_ITEMS:
       bat.menu_mode = batMENUITEM
       bat.item.pt = 0
       bat.item.top = 0
       checkitemusability bat.iuse(), bslot(), bat.player_turn
       bat.item_desc = ""
       IF inventory(bat.item.pt).used THEN
        bat.item_desc = readitemdescription(inventory(bat.item.pt).id)
       END IF
      CASE batmenu_SKIPTURN:
       player_attack_targetting_done bat, bslot()
     END SELECT
    END IF
   END WITH
  END IF
 END WITH
END SUB

'Called when a spell menu is opened, either setting up the menu or picking a
'spell from a Random menu
SUB init_spell_menu (bat as BattleState, bslot() as BattleSprite, st() as HeroDef)
 DIM atk as AttackData
 DIM i as integer
 DIM list_type as integer = st(bat.player_turn).list_type(bat.listslot)
 IF list_type < 2 THEN '--the type of this spell list is one that displays a menu
  bat.menu_mode = batMENUSPELL
  bat.sptr = 0
  '--Set up menu items
  FOR i = 0 TO UBOUND(bat.spell.slot)
   WITH bat.spell.slot(i)
    .name = ""
    .desc = ""
    .cost = ""
    .enable = NO
    .atk_id = gam.hero(bat.player_turn).spells(bat.listslot, i) - 1
    IF .atk_id >= 0 THEN '--there is a spell in this slot
     loadattackdata atk, .atk_id
     .name = atk.name
     .desc = atk.description
     .cost = bslot_attack_cost_info(bslot(), atk, bat.player_turn, list_type, i \ spellsPerLMP)
     .enable = atk.useable_inside_battle ANDALSO atkallowed(atk, bat.player_turn, list_type, i \ spellsPerLMP, bslot(bat.player_turn))
    END IF
    .name = rpad(.name, " ", 10)
   END WITH
  NEXT i

 ELSEIF list_type = 2 THEN '-- this is a random spell list
  DIM atk_id as integer
  atk_id = random_attack_from_spell_menu(bat, bslot(bat.player_turn), bat.listslot, st(), prefbit(54), NO)
  IF atk_id >= 0 THEN
   bslot(bat.player_turn).attack = atk_id + 1
   bat.targ.mode = targSETUP
   clearkeys
  END IF
 END IF
END SUB

FUNCTION items_menu_is_usable(bspr as BattleSprite) as bool
 'Returns YES if at least one item in the items menu is usable
 'Iterate through the iuse() bitfield and mark any items that are usable
 DIM item as ItemDef
 DIM attack as AttackData

 FOR i as integer = 0 TO inventoryMax
  IF inventory(i).used THEN
   loaditemdata item, inventory(i).id
   IF item.battle_items_menu_attack >= 0 THEN ' This item is usable in battle
    loadattackdata attack, item.battle_items_menu_attack
    IF attack.check_costs_as_item THEN
     '--This attack has the bitset that requires cost checking when used from an item
     IF atkallowed(attack, bspr.index, 0, 0, bspr) THEN
      RETURN YES
     END IF
    ELSE
     '--No cost checking for this item
     RETURN YES
    END IF
   END IF
  END IF
 NEXT i
 RETURN NO
END FUNCTION

FUNCTION spell_menu_is_usable(bat as BattleState, bspr as BattleSprite, spell_list_id as integer, st() as HeroDef, respect_costs as bool=YES) as bool
 'Returns YES if at least one attack in the spell list is usable
 IF NOT is_hero(bspr.index) THEN RETURN NO 'only heroes have spell lists
 '-- loop through the spell searching for usable attacks
 DIM atk as AttackData
 FOR i as integer = 0 TO UBOUND(bat.spell.slot)
  DIM atkid as integer = gam.hero(bspr.index).spells(spell_list_id, i) - 1
  IF atkid >= 0 THEN
   loadattackdata atk, atkid
   DIM list_type as integer = st(bspr.index).list_type(spell_list_id)
   IF atk.useable_inside_battle ANDALSO (respect_costs=NO ORELSE atkallowed(atk, bspr.index, list_type, i \ 3, bspr)) THEN
    RETURN YES
   END IF
  END IF
 NEXT i
 RETURN NO
END FUNCTION

FUNCTION random_attack_from_spell_menu(bat as BattleState, bspr as BattleSprite, spell_list_id as integer, st() as HeroDef, respect_costs as bool=YES, byval auto_battle as bool=NO) as integer
 'Returns a random attack from a spell list or -1 if none was found

 IF NOT is_hero(bspr.index) THEN RETURN -1 'only heroes have spell lists
 
 '-- loop through the spell list storing attack ID numbers
 DIM atk as AttackData
 DIM spells as integer vector
 v_new spells
 FOR i as integer = 0 TO UBOUND(bat.spell.slot)
  DIM atkid as integer = gam.hero(bspr.index).spells(spell_list_id, i) - 1
  IF atkid >= 0 THEN
   loadattackdata atk, atkid
   IF auto_battle ANDALSO atk.exclude_from_hero_auto_battle THEN CONTINUE FOR
   DIM list_type as integer = st(bspr.index).list_type(spell_list_id)
   IF atk.useable_inside_battle ANDALSO (respect_costs = NO ORELSE atkallowed(atk, bspr.index, list_type, i \ 3, bspr)) THEN v_append spells, atkid
  END IF
 NEXT i
 
 DIM result as integer = -1
 IF v_len(spells) > 0 THEN
  result = spells[randint(v_len(spells))]
 END IF
 v_free spells
 RETURN result
END FUNCTION

FUNCTION find_attack_in_spell_menu(byval seek_atk_id as integer, bat as BattleState, bspr as BattleSprite, byval spell_list_id as integer, st() as HeroDef, byval respect_costs as bool) as integer
 'Search for an attack in a spell list, and return the index at which it was found, or -1 for not found.
 'If the spell is found more than once, only the first match is returned
 'Can optionally respect or not respect costs 
 IF NOT is_hero(bspr.index) THEN RETURN -1 'only heroes have spell lists
 
 '-- loop through the spell list.
 DIM atk as AttackData
 FOR i as integer = 0 TO UBOUND(bat.spell.slot)
  DIM atkid as integer = gam.hero(bspr.index).spells(spell_list_id, i) - 1
  IF atkid >= 0 ANDALSO atkid = seek_atk_id THEN
   IF respect_costs THEN
    loadattackdata atk, atkid
    DIM list_type as integer = st(bspr.index).list_type(spell_list_id)
    IF atk.useable_inside_battle ANDALSO atkallowed(atk, bspr.index, list_type, i \ 3, bspr) THEN RETURN i
   ELSE
    RETURN i
   END IF
  END IF
 NEXT i
 RETURN -1
END FUNCTION

'Handle player controls while a spell menu is open.
'The menu is setup in init_spell_menu and actually drawn in battle_display_menus.
SUB spellmenu (byref bat as BattleState, st() as HeroDef, bslot() as BattleSprite)
 
 DIM do_pick as bool = NO
 DIM do_cancel as bool = NO
 
 WITH bat
  usemenusounds
  IF carray(ccUp) > 1 THEN
   IF .sptr > 2 THEN .sptr -= 3 ELSE .sptr = 24
  END IF
  IF carray(ccDown) > 1 THEN
   IF .sptr < 24 THEN .sptr = small(.sptr + 3, 24) ELSE .sptr = 0
  END IF
  IF keyval(scHome) > 1 THEN
   .sptr = 0
  END IF
  IF keyval(scEnd) > 1 THEN
   .sptr = 24
  END IF
  IF keyval(scPageUp) > 1 THEN
   .sptr = .sptr MOD 3
  END IF
  IF .sptr < 24 THEN  'EXIT not selected
   IF keyval(scPageDown) > 1 THEN
    .sptr = (.sptr MOD 3) + 21
   END IF
   IF carray(ccLeft) > 1 AND .sptr > 0 THEN
    .sptr -= 1
    menusound gen(genCursorSFX)
   END IF
   IF carray(ccRight) > 1 THEN
    .sptr += 1
    menusound gen(genCursorSFX)
   END IF
  END IF
 END WITH

 IF game_check_cancel_key() THEN do_cancel = YES
 IF game_battle_check_use_key() THEN do_pick = YES

 IF get_gen_bool("/mouse/mouse_battles") THEN
  bat.sptr_hover = -1
  IF rect_collide_point(Type(5, 6, 310, 95), readmouse.pos) THEN
   'In spell menu box
   FOR i as integer = 0 to 23
    IF rect_collide_point(XYWH(16 + (i MOD 3) * 104, 8 + (i \ 3) * 8, 104, 8), readmouse.pos) THEN bat.sptr_hover = i
   NEXT i
   IF rect_collide_point(XYWH(9, 90, 104, 8), readmouse.pos) THEN bat.sptr_hover = 24
   IF bat.sptr_hover >= 0 THEN
    IF (readmouse.release AND mouseLeft) ANDALSO readmouse.drag_dist < 10 THEN
     IF bat.sptr = bat.sptr_hover ORELSE bat.sptr_hover = 24 THEN
      do_pick = YES
     END IF
     menusound gen(genCursorSFX)
     bat.sptr = bat.sptr_hover
    END IF
   END IF
  ELSE
   'Outside the spells box
   IF (readmouse.release AND mouseLeft) ANDALSO readmouse.drag_dist < 10 THEN do_cancel = YES
  END IF
  IF (readmouse.release AND mouseRight) ANDALSO readmouse.drag_dist < 10 THEN
   IF bat.sptr_hover >= 0 THEN
    bat.sptr = bat.sptr_hover
    menusound gen(genCursorSFX)
   ELSE
    do_cancel = YES
   END IF
  END IF

  IF do_cancel ORELSE do_pick THEN
   'Cancel the mouse click because further input such as targetting could happen on this tick
   readmouse.clearclick(mouseLeft)
   readmouse.clearclick(mouseRight)
  END IF

 END IF

 IF do_cancel THEN
  bat.menu_mode = batMENUHERO
  clearkeys
  menusound gen(genCancelSFX)
  EXIT SUB
 END IF
 
 IF do_pick THEN
  '--use selected spell
  IF bat.sptr = 24 THEN
   '--used cancel
   bat.menu_mode = batMENUHERO
   clearkeys
   menusound gen(genCancelSFX)
   EXIT SUB
  END IF

  'Note gam.hero(...).spells(bat.listslot, i) is offset by 1, and has been copied
  'into bat.spell.slot(...).atk_id, not offset by 1.
  DIM atk_id as integer = bat.spell.slot(bat.sptr).atk_id
  '--can-I-use-it? checking
  IF atk_id = -1 THEN
   '--list-entry is empty
   menusound gen(genCancelSFX)
  ELSE
   DIM atk as AttackData
   loadattackdata atk, atk_id
   DIM list_type as integer = st(bat.player_turn).list_type(bat.listslot)
   DIM lmp as integer = bat.sptr \ spellsPerLMP
   IF atk.useable_inside_battle ANDALSO _
      atkallowed(atk, bat.player_turn, list_type, lmp, bslot(bat.player_turn)) THEN
    '--attack is allowed
    menusound gen(genAcceptSFX)
    '--if lmp then set lmp consume flag
    IF list_type = 1 THEN bslot(bat.player_turn).consume_lmp = lmp
    '--queue attack
    bslot(bat.player_turn).attack = atk_id + 1
    '--exit spell menu
    bat.targ.mode = targSETUP
    bat.menu_mode = batMENUHERO
    clearkeys
   ELSE
    '--not allowed
    menusound gen(genCancelSFX)
   END IF
  END IF
 END IF
END SUB

'Handle player controls while the items menu is open.
'The menu is setup in heromenu and actually drawn in battle_display_menus.
SUB itemmenu (byref bat as BattleState, bslot() as BattleSprite)

 DIM do_pick as bool = NO
 DIM do_cancel as bool = NO

 DIM remember_pt as integer = bat.item.pt
 usemenusounds
 IF carray(ccUp) > 1 AND bat.item.pt > 2 THEN bat.item.pt = bat.item.pt - 3
 IF carray(ccDown) > 1 AND bat.item.pt <= last_inv_slot() - 3 THEN bat.item.pt = bat.item.pt + 3
 IF keyval(scPageUp) > 1 THEN
  bat.item.pt -= (bat.inv_scroll.size) * 3
  WHILE bat.item.pt < 0: bat.item.pt += 3: WEND
 END IF
 IF keyval(scPageDown) > 1 THEN
  bat.item.pt += (bat.inv_scroll.size) * 3
  WHILE bat.item.pt > last_inv_slot(): bat.item.pt -= 3: WEND
 END IF
 IF keyval(scHome) > 1 THEN
  bat.item.pt = 0
 END IF
 IF keyval(scEnd) > 1 THEN
  bat.item.pt = last_inv_slot()
 END IF
 IF carray(ccLeft) > 1 AND bat.item.pt > 0 THEN
  bat.item.pt = bat.item.pt - 1
 END IF
 IF carray(ccRight) > 1 AND bat.item.pt < last_inv_slot() THEN
  bat.item.pt = bat.item.pt + 1
 END IF

 IF game_check_cancel_key() THEN do_cancel = YES
 IF game_battle_check_use_key() THEN do_pick = YES

 bat.item.hover = -1
 IF get_gen_bool("/mouse/mouse_battles") THEN
  DIM inv_height as integer = small(78, 8 + ((last_inv_slot() + 1) \ 3) * 8)
  IF rect_collide_point(Type(8, 4, 304, inv_height), readmouse.pos) THEN
   FOR i as integer = bat.item.top TO small(bat.item.top + 26, last_inv_slot())
    if i < lbound(inventory) or i > ubound(inventory) then continue for
    IF rect_collide_point(XYWH(20 + 96 * (i MOD 3), 8 + 8 * ((i - bat.item.top) \ 3), 96, 8), readmouse.pos) THEN bat.item.hover = i
   NEXT i
   IF bat.item.hover >= 0 THEN
    IF (readmouse.release AND mouseLeft) ANDALSO readmouse.drag_dist < 10 THEN
     IF bat.item.hover = bat.item.pt THEN do_pick = YES
     bat.item.pt = bat.item.hover
    END IF
   END IF
  ELSE
   'outside the item box
   IF (readmouse.release AND mouseLeft) ANDALSO readmouse.drag_dist < 10 THEN do_cancel = YES
  END IF
  IF (readmouse.release AND mouseRight) ANDALSO readmouse.drag_dist < 10 THEN
   IF bat.item.hover >= 0 THEN
    bat.item.pt = bat.item.hover
   ELSE
    do_cancel = YES
   END IF
  END IF
  'Scrolling
  DIM old_top as integer = bat.item.top
  IF (readmouse.dragging AND mouseRight) THEN
   IF bat.item_drag_top = -1 THEN bat.item_drag_top = bat.item.top
   IF readmouse.drag_dist > 10 THEN
    DIM dist as integer = (readmouse.clickstart.y - readmouse.pos.y) / 8
    bat.item.top = bat.item_drag_top + dist * 9
   END IF
  ELSE
   bat.item_drag_top = -1
  END IF
  bat.item.top += readmouse.wheel_clicks * 9
  IF old_top <> bat.item.top THEN
   'Scrolling moved top, bound top and fix pt
   bat.item.top = bound(bat.item.top, 0, last_inv_slot() - ((bat.inv_scroll.size + 1) * 3 - 1))
   WHILE bat.item.pt < bat.item.top : bat.item.pt += 3: WEND
   WHILE bat.item.pt > bat.item.top + (bat.inv_scroll.size) * 3: bat.item.pt -= 3: WEND
  END IF
  
  IF do_cancel ORELSE do_pick THEN
   'Cancel the mouse click because further input such as targetting could happen on this tick
   readmouse.clearclick(mouseLeft)
   readmouse.clearclick(mouseRight)
  END IF

 END IF '--done with mouse

 '--scroll when past top or bottom
 WHILE bat.item.pt < bat.item.top : bat.item.top = bat.item.top - 3 : WEND
 WHILE bat.item.pt >= bat.item.top + (bat.inv_scroll.size+1) * 3 : bat.item.top = bat.item.top + 3 : WEND

 IF bat.item.pt > UBOUND(inventory) THEN
  debuginfo "battle item cursor out of range " & bat.item.pt & " > " & UBOUND(inventory)
  bat.item.pt = UBOUND(inventory)
 END IF

 IF remember_pt <> bat.item.pt THEN
  menusound gen(genCursorSFX)
  IF inventory(bat.item.pt).used THEN
   bat.item_desc = readitemdescription(inventory(bat.item.pt).id)
  ELSE
   bat.item_desc = ""
  END IF
 END IF

 IF do_cancel THEN
  bat.menu_mode = batMENUHERO
  clearkeys
  bslot(bat.player_turn).consume_item = -1 ' -1 here indicates that this hero will not consume any item
  menusound gen(genCancelSFX)
  EXIT SUB
 END IF

 IF do_pick THEN
  IF readbit(bat.iuse(), 0, bat.item.pt) = 1 THEN
   menusound gen(genAcceptSFX)

   DIM item as ItemDef
   loaditemdata item, inventory(bat.item.pt).id
   bslot(bat.player_turn).consume_item = IIF(item.consumed_by_use, bat.item.pt, -1)
   bslot(bat.player_turn).attack = item.battle_items_menu_attack + 1
   bat.targ.mode = targSETUP
   bat.menu_mode = batMENUHERO
   clearkeys
  END IF
 END IF
END SUB

SUB create_battle_item_menu (bat as BattleState)
 'This is called once when the battle is first set up.
 'It loads the slices for the battle item menu, and prepares the state.
 'The slices start hidden, and the state is partly re-initialized each
 'time a hero actually uses the items menu.

 bat.itemst.ps.is_plank_callback = @is_item_plank
 bat.itemst.ps.state_callback = @set_item_plank_state

 bat.itemst.ps.m = LoadSliceCollection(SL_COLLECT_BATTLE_ITEMSCREEN)
 IF bat.itemst.ps.m = NULL THEN
  debugc errShowError, "create_battle_item_menu : Couldn't load slices!"
 END IF
 
 'Gets drawn along with all the other battle stuff
 SetSliceParent bat.itemst.ps.m, LookupSliceSafe(SL_BATTLE_SUBMENU_AREA, bat.root_sl)

 'Hidden by default
 bat.itemst.ps.m->Visible = NO

END SUB

'==============================================================================
'                               Animation system

'Called from within generate_atkscript.
FUNCTION attack_placement_over_target(attack as AttackData, targslot as integer, bat as BattleState, bslot() as BattleSprite, byval reverse as integer=0) as XYZTriple
 DIM targpos as XYZTriple
 DIM targsize AS XYPair
 
 targpos.x = bslot(targslot).x
 targpos.y = bslot(targslot).y
 targpos.z = bslot(targslot).z
 targsize.w = bslot(targslot).w
 targsize.h = bslot(targslot).h

 DIM targ_is_acting_hero as bool = NO
 IF targslot = bat.acting ANDALSO is_hero(bat.acting) THEN targ_is_acting_hero = YES
 RETURN attack_placement_over_targetpos(attack, targpos, targsize, targ_is_acting_hero, reverse)
END FUNCTION

' Calculate where a projectile will start relative to the attacker (bat.acting):
' Center of attack aligned to center of left of right edge of attacker sprite.
FUNCTION projectile_start_position(attack as AttackData, attacker as integer, bat as BattleState, bslot() as BattleSprite, byval reverse as integer=0) as XYZTriple
 DIM ret as XYZTriple
 ret = attack_placement_over_target(attack, attacker, bat, bslot(), reverse)
 IF is_hero(attacker) THEN
  ret.x -= bslot(attacker).w \ 2
 ELSE
  ret.x += bslot(attacker).w \ 2
 END IF
 RETURN ret
END FUNCTION

'Return true if target is unhittable and invisible
FUNCTION check_for_unhittable_invisible_foe(attacker as integer, target as integer, byref attack as AttackData, byref bat as BattleState, bslot() as BattleSprite) as bool
 IF bslot(target).hidden THEN
  IF should_enforce_hidden_untargetability(attack) THEN
   RETURN YES
  END IF
 END IF
 IF bslot(target).vis THEN RETURN NO
 IF bslot(target).stat.cur.hp <= 0 ANDALSO attack_can_hit_dead(attack, bslot(attacker).stored_targs_can_be_dead) THEN RETURN NO 'Attack can hit dead targets
 IF bslot(attacker).self_bequesting THEN
  'This is a self-bequest attack
  IF attacker = target THEN RETURN NO 'Bequesting attacker is always allowed to hit self
 END IF
 RETURN YES
END FUNCTION

SUB generate_atkscript(byref attack as AttackData, byref bat as BattleState, bslot() as BattleSprite, t() as integer)
 DIM i as integer

 '--check for item consumption
 '(Note: this is different from attack item costs)
 IF bslot(bat.acting).consume_item >= 0 THEN
  IF inventory(bslot(bat.acting).consume_item).used = 0 THEN
   '--abort if item is gone
   bat.atk.id = -1
   EXIT SUB
  END IF
 END IF

 '--load attack
 loadattackdata attack, bat.atk.id

 DIM safety as integer = 0
 DO WHILE spawn_chained_attack(attack.instead, YES, attack, bat, bslot())
  'If the spawned attack is delayed, then stop and wait
  IF blocked_by_attack(bat, bat.acting) THEN EXIT SUB
  'Or possibly the chained attack has a delay but isn't blocking. Also stop and wait.
  '(That means the call to blocked_by_attack is pointless, unless there was already a
  'blocking attack on the attack queue - which is probably impossible!)
  'Or if the chained attack had no delay but had to be retargeted, it also got put on atkq(),
  'and we need to restart (with a new t() array), next tick.
  IF bat.atk.id = -1 THEN EXIT SUB
  loadattackdata attack, bat.atk.id
  safety += 1
  IF safety > 100 THEN
   debug "Endless instead-chain loop detected for " & attack.name
   bat.atk.id = -1 '--cancel attack
   EXIT SUB
  END IF
 LOOP

 IF attack.recheck_costs_after_delay THEN
  'The "Re-check costs after attack delay" is on, so cancel the attack if we can't afford it now
  '(Costs aren't subtracted until the moment of the attack against the first target, in battle_attack_do_inflict)
  IF atkallowed(attack, bat.acting, 0, 0, bslot(bat.acting)) = NO THEN
   bat.atk.id = -1
   EXIT SUB
  END IF
 END IF

 '--setup attack sprite slots
 FOR i = 12 TO 23
  'load battle sprites
  setup_battlesprite_slice bslot(i), bat, sprTypeAttack, attack.picture, attack.pal
 NEXT i
 
 DIM tcount as integer = 0
 DIM pdir as integer = 0
 
 bat.atk.has_consumed_costs = NO
 bat.atk.has_spawned = NO
 IF is_enemy(bat.acting) THEN pdir = 1
 
 'CANNOT HIT INVISIBLE FOES
 FOR i = 0 TO 11
  IF t(i) > -1 THEN
   IF check_for_unhittable_invisible_foe(bat.acting, t(i), attack, bat, bslot()) THEN
    t(i) = -1
   END IF
  END IF
 NEXT i
 'MOVE EMPTY TARGET SLOTS TO THE BACK
 FOR o as integer = 0 TO UBOUND(t) - 1
  FOR i = 0 TO 10
   IF t(i) = -1 THEN SWAP t(i), t(i + 1)
  NEXT i
 NEXT o
 'COUNT TARGETS
 FOR i = 0 TO 11
  IF t(i) > -1 THEN tcount += 1
 NEXT i
 'ABORT IF TARGETLESS
 IF tcount = 0 THEN
  bat.atk.id = -1
  EXIT SUB
 END IF

 bat.atk.non_elemental = YES
 FOR i = 0 TO gen(genNumElements) - 1
  bat.atk.elemental(i) = NO
  IF attack.elemental_damage(i) = YES THEN
   bat.atk.elemental(i) = YES
   IF NOT gam.non_elemental_elements(i) THEN
    bat.atk.non_elemental = NO
   END IF
  END IF
 NEXT i

 'Kill old target history
 FOR i = 0 TO 11
  bslot(bat.acting).last_targs(i) = NO
 NEXT i

 IF attack.replace_store_targ THEN
  'If we are replacing the store targets, reset them all now. They will be added back in inflict.
  'Note that deleting stored targets also happens in inflict, and overrides both adding and replacing
  FOR i as integer = 0 TO UBOUND(bslot(bat.acting).stored_targs)
   bslot(bat.acting).stored_targs(i) = NO
  NEXT i
  bslot(bat.acting).stored_targs_can_be_dead = NO
 END IF
 
 'BIG CRAZY SCRIPT CONSTRUCTION
 'DEBUG debug "begin script construction"
 IF attack.dramatic_pause > 0 THEN
  anim_wait attack.dramatic_pause
 END IF
 
 IF is_hero(bat.acting) THEN
  'load weapon sprites
  WITH bslot(24)
   DIM wpic as integer
   DIM wpal as integer
   IF attack.override_wep_pic THEN
    wpic = attack.wep_picture
    wpal = attack.wep_pal
    .hand(0) = attack.wep_handle(0)
    .hand(1) = attack.wep_handle(1)
   ELSE
    DIM weapon as integer = gam.hero(bat.acting).equip(0).id
    IF weapon >= 0 THEN
     DIM item as ItemDef
     loaditemdata item, weapon
     .hand(0) = item.wep_handle(0)
     .hand(1) = item.wep_handle(1)
     wpic = item.wep_pic
     wpal = item.wep_pal
    ELSE
     'I don't know whether resetting this here is actually necessary, just duplicating old beheviour
     'This is impossible, due to default weapons, right?
     .hand(0) = 0
     .hand(1) = 0
    END IF
   END IF
   setup_battlesprite_slice bslot(24), bat, sprTypeWeapon, wpic, wpal
  END WITH
 END IF
 
 DIM numhits as integer
 numhits = attack.hits
 ' "Attacks ignore extra hits stat" gen bitset
 IF prefbit(29) = NO AND attack.ignore_extra_hits = NO THEN
  numhits += randint(bslot(bat.acting).stat.cur.hits + 1)
 END IF
 IF numhits > 20 THEN numhits = 20

 DIM atkimgdirection as integer
 atkimgdirection = pdir
 IF attack.unreversable_picture = YES THEN atkimgdirection = 0

 ' Absolute position to place/target an attack at
 DIM as XYZTriple targetpos

 '----NULL ANIMATION
 IF attack.attack_anim = atkAnimNull THEN
  anim_advance bat.acting, attack, bslot(), t()
  IF attack.sound_effect > 0 THEN anim_sound(attack.sound_effect - 1)
  FOR j as integer = 1 TO numhits
   IF is_hero(bat.acting) THEN anim_hero bat.acting, attack, bslot(), t()
   IF is_enemy(bat.acting) THEN anim_enemy bat.acting, attack, bslot(), t()
   FOR i = 0 TO tcount - 1
    anim_inflict t(i), tcount
   NEXT i
   anim_checkpoint
   anim_disappear 24
   anim_setframe bat.acting, frameSTAND
   anim_skip_ahead_if_targetless
  NEXT j
  anim_skip_to_here
  anim_retreat bat.acting, attack, bslot()
  anim_end
 END IF

 '----NORMAL, DROP, SPREAD-RING, and SCATTER
 IF attack.attack_anim = atkAnimNormal OR attack.attack_anim = atkAnimDrop OR attack.attack_anim = atkAnimScatter OR (attack.attack_anim = atkAnimRing AND tcount > 1) THEN
  DIM offscreen as RectPoints = battle_offscreen_bounds(bslot(12).size)
  DIM batres as XYPair = get_battle_res()
  DIM drop_ticks as integer = 8
  DIM drop_speed as integer = (batres.h - 20) \ drop_ticks
  FOR i = 0 TO tcount - 1
   targetpos = attack_placement_over_target(attack, t(i), bat, bslot(), atkimgdirection)
   anim_setpos 12 + i, targetpos.x, targetpos.y, atkimgdirection
   anim_setz 12 + i, targetpos.z
   IF attack.attack_anim = atkAnimRing THEN
    anim_setpos 12 + i, targetpos.x, targetpos.y - bslot(t(i)).w, atkimgdirection
   END IF
  NEXT i
  anim_advance bat.acting, attack, bslot(), t()
  FOR j as integer = 1 TO numhits
   IF is_hero(bat.acting) THEN anim_hero bat.acting, attack, bslot(), t()
   IF is_enemy(bat.acting) THEN anim_enemy bat.acting, attack, bslot(), t()
   FOR i = 0 TO tcount - 1
    targetpos = attack_placement_over_target(attack, t(i), bat, bslot(), atkimgdirection)
    IF attack.attack_anim = atkAnimDrop THEN
     'Originally, dropped from z=180 at a speed of 20 for 8 ticks, so ended up at z=20.
     anim_setz 12 + i, targetpos.z + drop_speed * drop_ticks + 20
    END IF
    anim_appear 12 + i
    IF attack.attack_anim = atkAnimRing THEN
     ' Appear left of the target; will circle in a "ring"
     anim_absmove 12 + i, targetpos.x - bslot(t(i)).w, targetpos.y, 3
    END IF
    IF attack.attack_anim = atkAnimDrop THEN
     anim_zvelocity 12 + i, -drop_speed, drop_ticks
    END IF
    IF attack.attack_anim = atkAnimScatter THEN
     ' Move to a random point
     anim_relmove 12 + i, randint(240) - 120, randint(150) - 75, 6
    END IF
   NEXT i
   IF attack.sound_effect > 0 THEN anim_sound(attack.sound_effect - 1)
   anim_wait 2
   IF attack.attack_anim = atkAnimDrop THEN
    anim_wait 3
   END IF
   anim_checkpoint
   anim_disappear 24
   anim_setframe bat.acting, frameSTAND
   IF attack.attack_anim = atkAnimRing THEN
    ' The attack sprites follow a diamond path around their targets.
    ' We don't modify z position, set above. The attacks will circle behind the targets too.
    FOR i = 0 TO tcount - 1
     targetpos = attack_placement_over_target(attack, t(i), bat, bslot(), atkimgdirection)
     anim_absmove 12 + i, targetpos.x, targetpos.y + bslot(t(i)).w, 3
    NEXT i
    anim_waitforall
    FOR i = 0 TO tcount - 1
     targetpos = attack_placement_over_target(attack, t(i), bat, bslot(), atkimgdirection)
     anim_absmove 12 + i, targetpos.x + bslot(t(i)).w, targetpos.y, 3
    NEXT i
    anim_waitforall
    FOR i = 0 TO tcount - 1
     targetpos = attack_placement_over_target(attack, t(i), bat, bslot(), atkimgdirection)
     anim_absmove 12 + i, targetpos.x, targetpos.y - bslot(t(i)).w, 3
    NEXT i
    anim_waitforall
   END IF
   FOR i = 0 TO tcount - 1
    anim_inflict t(i), tcount
    anim_flinchstart t(i), bslot(), attack
   NEXT i
   anim_wait 3
   FOR i = 0 TO tcount - 1
    anim_disappear 12 + i
   NEXT i
   anim_wait 2
   anim_skip_ahead_if_targetless
  NEXT j
  anim_skip_to_here
  anim_retreat bat.acting, attack, bslot()
  FOR i = 0 TO tcount - 1
   'I think this does nothing? flinch_anim_eachtick resets to frameSTAND
   anim_setframe t(i), frameSTAND
  NEXT i
  'If there is no attacker retreat animation, then anim_end may need to wait one extra tick (6 total)
  'for flinches to finish
  anim_end
 END IF

 '----SEQUENTIAL PROJECTILE
 IF attack.attack_anim = atkAnimSequentialProjectile THEN
  'attacker steps forward
  anim_advance bat.acting, attack, bslot(), t()
  'repeat the following for each attack
  FOR j as integer = 1 TO numhits
   ' Attacker animates
   IF is_hero(bat.acting) THEN anim_hero bat.acting, attack, bslot(), t()
   IF is_enemy(bat.acting) THEN anim_enemy bat.acting, attack, bslot(), t()
   ' Set the projectile position
   DIM projectile_start as XYZTriple
   projectile_start = projectile_start_position(attack, bat.acting, bat, bslot(), atkimgdirection)
   anim_setpos 12, projectile_start.x, projectile_start.y, atkimgdirection
   anim_setz 12, projectile_start.z
   anim_appear 12
   ' Play the sound effect
   IF attack.sound_effect > 0 THEN anim_sound(attack.sound_effect - 1)
   'repeat the following for each target...
   FOR i = 0 TO tcount - 1
    'find the target's position
    targetpos = attack_placement_over_target(attack, t(i), bat, bslot(), atkimgdirection)
    'make the projectile move to the target
    anim_absmove 12, targetpos.x, targetpos.y, 5
    anim_abszmove 12, targetpos.z, 5
    'Wait for projectile and for flinch
    anim_waitforall
    'inflict damage
    anim_inflict t(i), tcount
    anim_flinchstart t(i), bslot(), attack
    anim_wait 3
    IF i = 0 THEN
     'attacker relaxes after the first hit
     anim_disappear 24
     anim_setframe bat.acting, frameSTAND
    END IF
   NEXT i
   'after all hits are done, projectile flies off the side of the screen (towards the center)
   DIM offscreen as RectPoints = battle_offscreen_bounds(bslot(12).size)
   IF is_hero(bat.acting) THEN
    anim_absmove 12, offscreen.left, 100, 5
   END IF
   IF is_enemy(bat.acting) THEN
    anim_absmove 12, offscreen.right, 100, 5
   END IF
   anim_waitforall
   anim_checkpoint
   'hide projectile
   anim_disappear 12
   anim_skip_ahead_if_targetless
  NEXT j
  anim_skip_to_here
  'attacker steps back
  anim_retreat bat.acting, attack, bslot()
  anim_end
 END IF
 
 '----PROJECTILE, REVERSE PROJECTILE and METEOR
 IF attack.attack_anim = atkAnimProjectile OR attack.attack_anim = atkAnimReverseProjectile OR attack.attack_anim = atkAnimMeteor THEN
  DIM offscreen as RectPoints = battle_offscreen_bounds(bslot(12).size)
  CONST flight_ticks as integer = 6  'Note actually displayed for 12 ticks, so a long hover time
  DIM meteor_speed as integer 'px/tick
  meteor_speed = (130 - offscreen.top) \ flight_ticks  'Originally 30
  DIM projectile_start as XYZTriple
  projectile_start = projectile_start_position(attack, bat.acting, bat, bslot(), atkimgdirection)
  anim_advance bat.acting, attack, bslot(), t()
  FOR j as integer = 1 TO numhits
   'Set initial positions but don't show the attack sprites yet
   FOR i = 0 TO tcount - 1
    targetpos = attack_placement_over_target(attack, t(i), bat, bslot(), atkimgdirection)
    IF attack.attack_anim = atkAnimProjectile THEN
     anim_setpos 12 + i, projectile_start.x, projectile_start.y, atkimgdirection
     anim_setz 12 + i, projectile_start.z
    END IF
    IF attack.attack_anim = atkAnimReverseProjectile THEN
     anim_setpos 12 + i, targetpos.x, targetpos.y, atkimgdirection
     anim_setz 12 + i, targetpos.z
    END IF
    IF attack.attack_anim = atkAnimMeteor THEN
     'TODO: Don't have all the meteors originating from the same point
     IF is_hero(bat.acting) THEN
      anim_setpos 12 + i, offscreen.right, 100, atkimgdirection
     END IF
     IF is_enemy(bat.acting) THEN
      anim_setpos 12 + i, offscreen.left, 100, atkimgdirection
     END IF
     anim_setz 12 + i, flight_ticks * meteor_speed
    END IF
   NEXT i
   IF is_hero(bat.acting) THEN anim_hero bat.acting, attack, bslot(), t()
   IF is_enemy(bat.acting) THEN anim_enemy bat.acting, attack, bslot(), t()
   FOR i = 0 TO tcount - 1
    anim_appear 12 + i
   NEXT i
   'Wait a tick, otherwise the initial position of the attack is never seen
   anim_wait 1
   FOR i = 0 TO tcount - 1
    targetpos = attack_placement_over_target(attack, t(i), bat, bslot(), atkimgdirection)
    IF attack.attack_anim = atkAnimProjectile OR attack.attack_anim = atkAnimMeteor THEN
     anim_absmove 12 + i, targetpos.x, targetpos.y, flight_ticks
     anim_abszmove 12 + i, targetpos.z, flight_ticks
    ELSE  'attack.attack_anim = atkAnimReverseProjectile
     anim_absmove 12 + i, projectile_start.x, projectile_start.y, flight_ticks
     anim_abszmove 12 + i, projectile_start.z, flight_ticks
    END IF
   NEXT i
   IF attack.sound_effect > 0 THEN anim_sound(attack.sound_effect - 1)
   anim_checkpoint
   anim_wait 8
   anim_checkpoint
   anim_disappear 24
   anim_setframe bat.acting, frameSTAND
   FOR i = 0 TO tcount - 1
    anim_inflict t(i), tcount
    anim_flinchstart t(i), bslot(), attack
   NEXT i
   anim_wait 3
   FOR i = 0 TO tcount - 1
    anim_disappear 12 + i
   NEXT i
   anim_wait 3
   anim_skip_ahead_if_targetless
  NEXT j
  anim_skip_to_here
  anim_retreat bat.acting, attack, bslot()
  FOR i = 0 TO tcount - 1
   'Does nothing?
   anim_setframe t(i), frameSTAND
  NEXT i
  anim_end
 END IF

 '----DRIVEBY
 ' Assuming the hero is attacking, this creates attacks off the right side of the
 ' screen, sends them to intercept the targets all at the same time, and then moves them
 ' to the same x. So the projectiles all move at different speeds during each half.
 IF attack.attack_anim = atkAnimDriveby THEN
  DIM offscreen as RectPoints = battle_offscreen_bounds(bslot(12).size)

  anim_advance bat.acting, attack, bslot(), t()
  FOR j as integer = 1 TO numhits
   ' Put sprites at initial positions
   FOR i = 0 TO tcount - 1
    targetpos = attack_placement_over_target(attack, t(i), bat, bslot(), atkimgdirection)
    IF is_hero(bat.acting) THEN
     anim_setpos 12 + i, offscreen.right, targetpos.y, atkimgdirection
    END IF
    IF is_enemy(bat.acting) THEN
     anim_setpos 12 + i, offscreen.left, targetpos.y, atkimgdirection
    END IF
    anim_setz 12 + i, targetpos.z
   NEXT i
   IF is_hero(bat.acting) THEN anim_hero bat.acting, attack, bslot(), t()
   IF is_enemy(bat.acting) THEN anim_enemy bat.acting, attack, bslot(), t()

   ' Intercept targets
   FOR i = 0 TO tcount - 1
    anim_appear 12 + i
    targetpos = attack_placement_over_target(attack, t(i), bat, bslot(), atkimgdirection)
    anim_absmove 12 + i, targetpos.x, targetpos.y, 8
    ' Z values are constant
   NEXT i
   anim_checkpoint
   IF attack.sound_effect > 0 THEN anim_sound(attack.sound_effect - 1)
   anim_wait 4
   anim_disappear 24
   anim_setframe bat.acting, frameSTAND
   anim_waitforall

   ' Drive off side of screen
   FOR i = 0 TO tcount - 1
    anim_inflict t(i), tcount
    anim_flinchstart t(i), bslot(), attack
    targetpos = attack_placement_over_target(attack, t(i), bat, bslot(), atkimgdirection)
    IF is_hero(bat.acting) THEN
     anim_absmove 12 + i, offscreen.left, targetpos.y, 5
    END IF
    IF is_enemy(bat.acting) THEN
     anim_absmove 12 + i, offscreen.right, targetpos.y, 5
    END IF
    ' Z values are constant
   NEXT i
   anim_waitforall

   ' Destroy attacks
   FOR i = 0 TO tcount - 1
    anim_disappear 12 + i
   NEXT i
   anim_wait 5
   anim_skip_ahead_if_targetless
  NEXT j
  anim_skip_to_here
  anim_retreat bat.acting, attack, bslot()
  FOR i = 0 TO tcount - 1
   'Does nothing?
   anim_setframe t(i), frameSTAND
  NEXT i
  anim_end
 END IF
 
 '----FOCUSED RING
 IF attack.attack_anim = atkAnimRing AND tcount = 1 THEN
  DIM target as integer = t(0)
  anim_advance bat.acting, attack, bslot(), t()
  FOR j as integer = 1 TO numhits
   targetpos = attack_placement_over_target(attack, target, bat, bslot(), atkimgdirection)
   ' Create 8 attacks in a circle
   FOR idx as integer = 0 TO 7
    anim_setz 12 + idx, targetpos.z
   NEXT
   anim_setpos 12 + 0, targetpos.x + 0, targetpos.y - 50, atkimgdirection
   anim_setpos 12 + 1, targetpos.x + 30, targetpos.y - 30, atkimgdirection
   anim_setpos 12 + 2, targetpos.x + 50, targetpos.y + 0, atkimgdirection
   anim_setpos 12 + 3, targetpos.x + 30, targetpos.y + 30, atkimgdirection
   anim_setpos 12 + 4, targetpos.x - 0, targetpos.y + 50, atkimgdirection
   anim_setpos 12 + 5, targetpos.x - 30, targetpos.y + 30, atkimgdirection
   anim_setpos 12 + 6, targetpos.x - 50, targetpos.y - 0, atkimgdirection
   anim_setpos 12 + 7, targetpos.x - 30, targetpos.y - 30, atkimgdirection
   IF is_hero(bat.acting) THEN anim_hero bat.acting, attack, bslot(), t()
   IF is_enemy(bat.acting) THEN anim_enemy bat.acting, attack, bslot(), t()
   ' Move attacks into the target
   FOR aslot as integer = 0 TO 7
    anim_appear 12 + aslot
    anim_absmove 12 + aslot, targetpos.x, targetpos.y, 4
    'Z values remain constant
   NEXT aslot
   IF attack.sound_effect > 0 THEN anim_sound(attack.sound_effect - 1)
   anim_wait 8
   anim_checkpoint
   anim_disappear 24
   anim_setframe bat.acting, frameSTAND
   FOR i = 0 TO tcount - 1  ' Note always tcount = 1
    anim_inflict t(i), tcount
    anim_flinchstart t(i), bslot(), attack
   NEXT i
   anim_wait 3
   FOR aslot as integer = 0 TO 7
    anim_disappear 12 + aslot
   NEXT aslot
   anim_wait 3
   anim_skip_ahead_if_targetless
  NEXT j
  anim_skip_to_here
  anim_retreat bat.acting, attack, bslot()
  FOR i = 0 TO tcount - 1  ' Note always tcount = 1
   'Does nothing?
   anim_setframe t(i), frameSTAND
  NEXT i
  anim_end
 END IF
 
 '----WAVE
 IF attack.attack_anim = atkAnimWave THEN
  DIM offscreen as RectPoints = battle_offscreen_bounds(bslot(12).size)
  DIM wave_start_x as integer
  wave_start_x = offscreen.left
  IF is_hero(bat.acting) THEN wave_start_x = offscreen.right
  DIM x_velocity as integer
  x_velocity = IIF(is_hero(bat.acting), -16, 16)
  ' Time it takes each sprite to move across screen, total time is more
  DIM ticks as integer = 1 + (offscreen.right - offscreen.left) \ ABS(x_velocity)
  ' Only use targetpos.y, and then only if there's just one target.
  targetpos = attack_placement_over_target(attack, t(0), bat, bslot(), atkimgdirection)
  anim_advance bat.acting, attack, bslot(), t()
  FOR j as integer = 1 TO numhits
   FOR i = 0 TO 11
    IF tcount > 1 OR attack.targ_set = 1 THEN
     'Spread: a diagonal wave
     anim_setpos 12 + i, wave_start_x, i * 15, atkimgdirection
     anim_setz 12 + i, 0   'Can tweak this to change the effect
    ELSE
     'Single target: a horizontal line
     anim_setpos 12 + i, wave_start_x, targetpos.y, atkimgdirection
     anim_setz 12 + i, targetpos.z
    END IF
   NEXT i
   IF is_hero(bat.acting) THEN anim_hero bat.acting, attack, bslot(), t()
   IF is_enemy(bat.acting) THEN anim_enemy bat.acting, attack, bslot(), t()
   IF attack.sound_effect > 0 THEN anim_sound(attack.sound_effect - 1)
   'Stagger the time each sprite starts to move
   FOR i = 0 TO 11
    anim_appear 12 + i
    anim_velocity 12 + i, x_velocity, 0, ticks
    anim_wait 1
   NEXT i
   anim_checkpoint
   anim_wait 15
   anim_disappear 24
   anim_setframe bat.acting, frameSTAND
   FOR i = 0 TO tcount - 1
    anim_inflict t(i), tcount
    anim_flinchstart t(i), bslot(), attack
   NEXT i
   anim_waitforall
   FOR i = 0 TO 11
    anim_disappear 12 + i
   NEXT i
   anim_wait 2
   anim_skip_ahead_if_targetless
  NEXT j
  anim_skip_to_here
  anim_retreat bat.acting, attack, bslot()
  FOR i = 0 TO tcount - 1
   'Does nothing?
   anim_setframe t(i), frameSTAND
  NEXT i
  anim_end
 END IF

 '----SCREEN CENTER
 ' Simply put the animation directly in the center of the screen
 IF attack.attack_anim = atkAnimScreenCenter THEN
  anim_advance bat.acting, attack, bslot(), t()
  FOR j as integer = 1 TO numhits
   ' Create one attack sprite
   targetpos = TYPE<XYZTriple>(320/2 - bslot(12).w/2, 200, 100 + bslot(12).h/2)
   anim_setpos 12, targetpos.x, targetpos.y, atkimgdirection
   anim_setz 12, targetpos.z
   IF is_hero(bat.acting) THEN anim_hero bat.acting, attack, bslot(), t()
   IF is_enemy(bat.acting) THEN anim_enemy bat.acting, attack, bslot(), t()
   anim_appear 12
   anim_checkpoint
   IF attack.sound_effect > 0 THEN anim_sound(attack.sound_effect - 1)
   anim_wait 4
   anim_disappear 24
   anim_setframe bat.acting, frameSTAND
   FOR i = 0 TO tcount - 1
    anim_inflict t(i), tcount
    anim_flinchstart t(i), bslot(), attack
   NEXT i
   anim_wait 3
   ' Destroy attacks
   anim_disappear 12
   anim_wait 3
   anim_skip_ahead_if_targetless
  NEXT j
  anim_skip_to_here
  anim_retreat bat.acting, attack, bslot()
  FOR i = 0 TO tcount - 1
   'Does nothing?
   anim_setframe t(i), frameSTAND
  NEXT i
  anim_end
 END IF
 
 '--setup animation pattern
 FOR i = 12 TO 23 '--for each attack sprite
  bslot(i).anim_index = 0
  bslot(i).anim_pattern = attack.anim_pattern
 NEXT i
 
 '--if there is a caption and display time isn't "Not at all"
 IF attack.caption <> "" AND attack.caption_time >= 0 THEN
  DIM ticks as integer = attack.caption_delay
  IF attack.caption_time = 0 THEN
   ticks += 16383  '--full duration
  ELSE
   ticks += attack.caption_time  '--timed
  END IF
  setbatcap bat, attack.caption, ticks, attack.caption_delay
 END IF
 
 'DEBUG debug "stackpos = " & (stackpos - bstackstart)
 invertstack
 
 '--Remember the attack ID for later call to fulldeathcheck
 bat.atk.was_id = bat.atk.id
 
 '--indicates that animation is set and that we should proceed to "action"
 bat.anim_ready = YES
END SUB

SUB enforce_weak_picture(byval who as integer, bslot() as BattleSprite, byref bat as BattleState)
 '--Heroes only, since enemies don't currently have a weak frame
 IF is_hero(who) THEN
  '--enforce weak picture
  DIM weakhp as integer = 0
  weakhp = gen(genHeroWeakHP)
  IF bslot(who).stat.cur.hp < 0.01 * bslot(who).stat.max.hp * weakhp - 1e-8 AND bat.vic.state = vicNONE THEN bslot(who).frame = frameWEAK
 END IF
END SUB

'==============================================================================

SUB setup_targetting (byref bat as BattleState, bslot() as BattleSprite)
 'setuptarg (heroes, and enemies under player control)
 DIM i as integer

 'init
 bat.targ.opt_spread = 0
 bat.targ.interactive = NO
 bat.targ.roulette = NO
 bat.targ.force_first = NO
 bat.targ.pointer = 0
 FOR i = 0 TO 11
  bat.targ.selected(i) = NO ' clear list of selected targets
 NEXT i

 bat.targ.hit_dead = NO

 'load attack
 loadattackdata bat.targ.atk, bslot(bat.player_turn).attack - 1

 get_valid_targs bat.targ.mask(), bat.player_turn, bat.targ.atk, bslot(), bat
 bat.targ.hit_dead = attack_can_hit_dead(bat.targ.atk, bslot(bat.player_turn).stored_targs_can_be_dead)

 '--attacks that can target all/all-including-dead should default to the first enemy
 IF bat.targ.atk.targ_class = 3 OR bat.targ.atk.targ_class = 14 THEN
  bat.targ.pointer = 4
 END IF

 'fail if there are no targets
 IF targetmaskcount(bat.targ.mask()) = 0 THEN
  checkAtkTagConds bat.targ.atk, atktagOnTargettingFailed
  bat.targ.mode = targNONE
  EXIT SUB
 END IF

 'autoattack
 IF bat.targ.atk.automatic_targ THEN
  bat.targ.mode = targAUTO
  EXIT SUB
 END IF

 IF bat.targ.atk.targ_set = 0 THEN bat.targ.interactive = YES
 IF bat.targ.atk.targ_set = 1 THEN FOR i = 0 TO 11: bat.targ.selected(i) = bat.targ.mask(i): NEXT i
 IF bat.targ.atk.targ_set = 2 THEN bat.targ.interactive = YES: bat.targ.opt_spread = 1
 IF bat.targ.atk.targ_set = 3 THEN bat.targ.roulette = YES
 IF bat.targ.atk.targ_set = 4 THEN bat.targ.force_first = YES

 bat.targ.pointer = find_preferred_target(bat.targ.mask(), bat.player_turn, bat.targ.atk, bslot())
 'fail if no targets are found
 IF bat.targ.pointer = -1 THEN
  bat.targ.mode = targNONE
  EXIT SUB
 END IF

 'ready to choose bat.targ.selected() from bat.targ.mask()
 bat.targ.mode = targMANUAL
END SUB

FUNCTION valid_statnum(byval statnum as integer, context as string) as bool
 RETURN bound_arg(statnum, 0, 15, "stat number", context, serrError)
END FUNCTION

FUNCTION check_attack_chain(byref ch as AttackDataChain, byref bat as BattleState, bslot() as BattleSprite) as bool
 'Returns YES if the chain may proceed, or NO if it fails
 
 IF randint(100) >= ch.rate THEN RETURN NO '--random percentage failed
 
 IF ch.must_know = YES THEN
  IF knows_attack(bat.acting, ch.atk_id - 1, bslot()) = NO THEN RETURN NO
 END IF
 
 DIM targ as integer
 DIM tcount as integer
 DIM tgood as integer
 DIM ret as bool = NO
 DIM ra as double
 
 SELECT CASE ch.mode
  CASE 0 '--no special conditions
   ret = YES
  CASE 1 '--tag checks
   IF ABS(ch.val1) <= max_tag() AND ABS(ch.val2) <= max_tag() THEN
    ret = istag(ch.val1, YES) AND istag(ch.val2, YES)
   ELSE
    debug "chain: invalid tag check " & ch.val1 & " " & ch.val2
   END IF
  CASE 2 '--attacker stat greater than value
   IF valid_statnum(ch.val1, "check_attack_chain() attacker [>n]") THEN
    ret = bslot(bat.acting).stat.cur.sta(ch.val1) > ch.val2
   END IF
  CASE 3 '--attacker stat less than value
   IF valid_statnum(ch.val1, "check_attack_chain() attacker [<n]") THEN
    ret = bslot(bat.acting).stat.cur.sta(ch.val1) < ch.val2
   END IF
  CASE 4 '--attacker stat greater than value % of max
   IF valid_statnum(ch.val1, "check_attack_chain() attacker [>n%]") THEN
    WITH bslot(bat.acting).stat
     ret = .cur.sta(ch.val1) > .max.sta(ch.val1) * (ch.val2 / 100)
    END WITH
   END IF
  CASE 5 '--attacker stat less than value % of max
   IF valid_statnum(ch.val1, "check_attack_chain() attacker [<n%]") THEN
    WITH bslot(bat.acting).stat
     ret = .cur.sta(ch.val1) < .max.sta(ch.val1) * (ch.val2 / 100)
    END WITH
   END IF
  CASE 6 '--any targets' stat greater than value
   IF valid_statnum(ch.val1, "check_attack_chain() any target [>n]") THEN
    FOR i as integer = 0 to 11
     targ = bat.anim_t(i)
     IF targ >= 0 THEN
      'debug "check stat " & ch.val1 & " any target " & targ & " > " & ch.val2
      IF bslot(targ).stat.cur.sta(ch.val1) > ch.val2 THEN ret = YES
     END IF
    NEXT i
   END IF
  CASE 7 '--any targets' stat less than value
   IF valid_statnum(ch.val1, "check_attack_chain() any target [<n]") THEN
    FOR i as integer = 0 to 11
     targ = bat.anim_t(i)
     IF targ >= 0 THEN
      'debug "check stat " & ch.val1 & " any target " & targ & " < " & ch.val2
      IF bslot(targ).stat.cur.sta(ch.val1) < ch.val2 THEN ret = YES
     END IF
    NEXT i
   END IF
  CASE 8 '--any targets' stat greater than % of max
   IF valid_statnum(ch.val1, "check_attack_chain() any target [>n%]") THEN
    FOR i as integer = 0 to 11
     targ = bat.anim_t(i)
     IF targ >= 0 THEN
      'debug "check stat " & ch.val1 & " any target " & targ & " > " & ch.val2 & "%"
      WITH bslot(targ).stat
       IF .cur.sta(ch.val1) > .max.sta(ch.val1) * (ch.val2 / 100) THEN ret = YES
      END WITH
     END IF
    NEXT i
   END IF
  CASE 9 '--any targets' stat less than % of max
   IF valid_statnum(ch.val1, "check_attack_chain() any target [<n%]") THEN
    FOR i as integer = 0 to 11
     targ = bat.anim_t(i)
     IF targ >= 0 THEN
      'debug "check stat " & ch.val1 & " any target " & targ & " < " & ch.val2 & "%"
      WITH bslot(targ).stat
       'debug ch.val1 & " " & ch.val2 & " " & .cur.sta(ch.val1) & " " & .max.sta(ch.val1)
       IF .cur.sta(ch.val1) < .max.sta(ch.val1) * (ch.val2 / 100) THEN ret = YES
      END WITH
     END IF
    NEXT i
   END IF
  CASE 10 '--all targets' stat greater than value
   IF valid_statnum(ch.val1, "check_attack_chain() all target [>n]") THEN
    tcount = 0
    tgood = 0
    FOR i as integer = 0 to 11
     targ = bat.anim_t(i)
     IF targ >= 0 THEN
      tcount += 1
      'debug "check stat " & ch.val1 & " all target " & targ & " > " & ch.val2
      IF bslot(targ).stat.cur.sta(ch.val1) > ch.val2 THEN tgood += 1
     END IF
    NEXT i
    IF tcount > 0 ANDALSO tgood = tcount THEN ret = YES
   END IF
  CASE 11 '--all targets' stat less than value
   IF valid_statnum(ch.val1, "check_attack_chain() all target [<n]") THEN
    tcount = 0
    tgood = 0
    FOR i as integer = 0 to 11
     targ = bat.anim_t(i)
     IF targ >= 0 THEN
      tcount += 1
      'debug "check stat " & ch.val1 & " all target " & targ & " < " & ch.val2
      IF bslot(targ).stat.cur.sta(ch.val1) < ch.val2 THEN tgood += 1
     END IF
    NEXT i
    IF tcount > 0 ANDALSO tgood = tcount THEN ret = YES
   END IF
  CASE 12 '--all targets' stat greater than % of max
   IF valid_statnum(ch.val1, "check_attack_chain() all target [>n%]") THEN
    tcount = 0
    tgood = 0
    FOR i as integer = 0 to 11
     targ = bat.anim_t(i)
     IF targ >= 0 THEN
      tcount += 1
      'debug "check stat " & ch.val1 & " all target " & targ & " > " & ch.val2 & "%"
      WITH bslot(targ).stat
       IF .cur.sta(ch.val1) > .max.sta(ch.val1) * (ch.val2 / 100) THEN tgood += 1
      END WITH
     END IF
    NEXT i
    IF tcount > 0 ANDALSO tgood = tcount THEN ret = YES
   END IF
  CASE 13 '--all targets' stat less than % of max
   IF valid_statnum(ch.val1, "check_attack_chain() all target [<n%]") THEN
    tcount = 0
    tgood = 0
    FOR i as integer = 0 to 11
     targ = bat.anim_t(i)
     IF targ >= 0 THEN
      tcount += 1
      'debug "check stat " & ch.val1 & " all target " & targ & " < " & ch.val2 & "%"
      WITH bslot(targ).stat
       IF .cur.sta(ch.val1) < .max.sta(ch.val1) * (ch.val2 / 100) THEN tgood += 1
      END WITH
     END IF
    NEXT i
    IF tcount > 0 ANDALSO tgood = tcount THEN ret = YES
   END IF
  CASE 14 '--all targets' stat greater than attacker stat
   IF valid_statnum(ch.val1, "check_attack_chain()") ANDALSO valid_statnum(ch.val2, "check_attack_chain()") THEN
    tcount = 0
    tgood = 0
    FOR i as integer = 0 to 11
     targ = bat.anim_t(i)
     IF targ >= 0 THEN
      tcount += 1
      'debug "check stat " & ch.val1 & " all target " & targ & " > " & ch.val2
      IF bslot(targ).stat.cur.sta(ch.val1) > bslot(bat.acting).stat.cur.sta(ch.val2) THEN tgood += 1
     END IF
    NEXT i
    IF tcount > 0 ANDALSO tgood = tcount THEN ret = YES
   END IF
  CASE 15 '--all targets' stat less than attacker stat
   IF valid_statnum(ch.val1, "check_attack_chain()") ANDALSO valid_statnum(ch.val2, "check_attack_chain()") THEN
    tcount = 0
    tgood = 0
    FOR i as integer = 0 to 11
     targ = bat.anim_t(i)
     IF targ >= 0 THEN
      tcount += 1
      'debug "check stat " & ch.val1 & " all target " & targ & " < " & ch.val2
      IF bslot(targ).stat.cur.sta(ch.val1) < bslot(bat.acting).stat.cur.sta(ch.val2) THEN tgood += 1
     END IF
    NEXT i
    IF tcount > 0 ANDALSO tgood = tcount THEN ret = YES
   END IF
  CASE 16 '--attacker stat less than attacker stat
   IF valid_statnum(ch.val1, "check_attack_chain()") ANDALSO valid_statnum(ch.val2, "check_attack_chain()") THEN
    ret = bslot(bat.acting).stat.cur.sta(ch.val1) < bslot(bat.acting).stat.cur.sta(ch.val2)
   END IF
  CASE 17 '--attacker stat probability
   IF valid_statnum(ch.val1, "check_attack_chain()") THEN
    ret = rando() < bslot(bat.acting).stat.cur.sta(ch.val1) / ch.val2
   END IF
  CASE 18 '--any target stat probability
   IF valid_statnum(ch.val1, "check_attack_chain()") THEN
    ra = rando()
    FOR i as integer = 0 to 11
     targ = bat.anim_t(i)
     IF targ >= 0 THEN
      IF ra < bslot(targ).stat.cur.sta(ch.val1) / ch.val2 THEN ret = YES
     END IF
    NEXT i
   END IF
  CASE 19 '--all target stat probability
   IF valid_statnum(ch.val1, "check_attack_chain()") THEN
    ra = rando()
    tcount = 0
    tgood = 0
    FOR i as integer = 0 to 11
     targ = bat.anim_t(i)
     IF targ >= 0 THEN
      tcount += 1
      IF ra < bslot(targ).stat.cur.sta(ch.val1) / ch.val2 THEN tgood += 1
     END IF
    NEXT i
    IF tcount > 0 ANDALSO tgood = tcount THEN ret = YES
   END IF
  'CASE 20 '--attacker local flags
   'TODO
  'CASE 21 '--any target local flags
   'TODO
  'CASE 22 '--all targets local flags
   'TODO
  'CASE 23 '--attacker local flag and any target local flag
   'TODO
  'CASE 24 '--attacker local flag and all target local flag
   'TODO
  CASE ELSE
   debug "attack chain mode " & ch.mode & " unsupported"
 END SELECT
 RETURN ret XOR ch.invert_condition
END FUNCTION

'Try to spawn a chained attack. The new attack might either immediately become the
'active attack (bat.atk.id), or be put on atkq.
'Returns true if we spawned (queued) an attack.
FUNCTION spawn_chained_attack(byref ch as AttackDataChain, instead_chain as bool, byref attack as AttackData, byref bat as battlestate, bslot() as BattleSprite) as bool
 IF ch.atk_id <= 0 THEN RETURN NO '--no chain defined
 IF bslot(bat.acting).stat.cur.hp <= 0 THEN RETURN NO '--attacker is dead
 IF instead_chain = NO ANDALSO attack.no_chain_on_failure = YES ANDALSO bslot(bat.acting).attack_succeeded = NO THEN
  'attack failed, and this chain configured to fail too
  '(.attack_succeeded is garbage for Instead chains)
  RETURN NO
 END IF
  
 IF check_attack_chain(ch, bat, bslot()) THEN
  '--The conditions for this chain are passed

  DIM chained_attack as AttackData
  loadattackdata chained_attack, ch.atk_id - 1

  DIM delayed_attack_id as integer = 0
  IF (chained_attack.attack_delay > 0 ORELSE chained_attack.turn_delay > 0) ANDALSO ch.no_delay = NO THEN
   '--chain is delayed, queue the attack
   'Note: in turnTURN mode, we're putting the attack on the attack queue without
   'adding the attacker's .initiative_order, which is correct - the delay is
   'relative to the spawning attack (since the delays of all attacks have been
   'shifted so that the current attack has delay 0) instead of the attacker.
   bat.atk.id = -1 '--terminate the attack that lead to this chain
   delayed_attack_id = ch.atk_id
  ELSE
   '--chain is immediate, prep it now!
   '(This includes attacks in turn-based mode with negative delays (happen earlier than normal)
   bat.atk.id = ch.atk_id - 1
   bat.anim_ready = NO
  END IF

  DIM blocking as bool
  IF bat.anim_blocking_delay = NO THEN
   '--chains from non-blocking attacks are always non-blocking
   blocking = NO
  ELSEIF ch.nonblocking THEN
   blocking = NO
  ELSE
   blocking = NOT chained_attack.nonblocking
  END IF

  IF chained_attack.targ_set <> attack.targ_set OR _
     chained_attack.targ_class <> attack.targ_class OR _
     chained_attack.targ_set = 3 OR chained_attack.prefer_targ > 0 THEN
   'if the chained attack has a different target class/type then re-target
   'also retarget if the chained attack has target setting "random roulette"
   'also retarget if the chained attack's preferred target is explicitly set.
   'This queues the attack.
   autotarget bat.acting, chained_attack, bslot(), bat, , blocking, ch.dont_retarget
   bat.atk.id = -1
  ELSEIF delayed_attack_id > 0 THEN
   'if the old target info is reused, and this is not an immediate chain, copy it to the queue right away
   queue_attack delayed_attack_id - 1, bat.acting, bat.anim_t(), blocking, ch.dont_retarget
  END IF

  RETURN YES '--chained attack okay
 END IF

 RETURN NO '--chained attack failed
END FUNCTION '.attack

FUNCTION knows_attack(who as integer, atk as integer, bslot() as BattleSprite) as bool
 'who is bslot index
 'atk is attack id
 'bslot() hero and enemy data

 '--different handling for heroes and monsters

 IF is_hero(who) THEN
  'Counter attacks do NOT count!
  FOR i as integer = 0 TO bslot(who).batmenu.numitems - 1
   WITH *bslot(who).batmenu.items[i]
    'The attack is known even if it's disabled by tags
    SELECT CASE .t
     CASE batmenu_ATTACK:
      IF atk = .sub_t THEN RETURN YES
     CASE batmenu_SPELLS:
      FOR j as integer = 0 TO 23
       IF gam.hero(who).spells(.sub_t, j) - 1 = atk THEN RETURN YES 'Knows the attack in a spell list
      NEXT j
    END SELECT
   END WITH
  NEXT i
 END IF
 
 IF is_enemy(who) THEN
  FOR i as integer = 0 TO 4
   'check if enemy knows this attack for one of the three ai sets
   'Counter attacks and on-death bequest attacks do NOT count!
   IF bslot(who).enemy.regular_ai(i) - 1 = atk THEN RETURN YES
   IF bslot(who).enemy.desperation_ai(i) - 1 = atk THEN RETURN YES
   IF bslot(who).enemy.alone_ai(i) - 1 = atk THEN RETURN YES
  NEXT i
 END IF
 
 RETURN NO
END FUNCTION

SUB queue_attack(attack as integer, who as integer, targs() as integer, override_blocking as optbool=NONBOOL, dont_retarget as bool = NO)
 DIM atk as AttackData
 loadattackdata atk, attack
 DIM blocking as bool = (atk.nonblocking = NO)
 IF override_blocking <> NONBOOL THEN blocking = override_blocking
 queue_attack attack, who, atk.attack_delay, atk.turn_delay, targs(), blocking, dont_retarget
END SUB

SUB queue_attack(attack as integer, who as integer, delay as integer, turn_delay as integer, targs() as integer, blocking as bool=YES, dont_retarget as bool = NO)
 'DIM targstr as string = ""
 'FOR i as integer = 0 TO UBOUND(targs)
 ' IF targs(i) > -1 THEN targstr &= " " & i & "=" & targs(i)
 'NEXT i
 'debug "queue_attack " & readattackname(attack) & ", " & who & ", " & targstr

 IF battl->turn.mode = turnTURN THEN
  'See apply_initiative_order for explanation
  delay = delay * (ATK_DELAY_MULT + 1)
 END IF

 FOR i as integer = 0 TO UBOUND(atkq)
  IF atkq(i).used = NO THEN
   'Recycle a queue slot
   set_attack_queue_slot i, attack, who, delay, turn_delay, targs(), blocking, dont_retarget
   EXIT SUB
  END IF
 NEXT i
 'No spaces to recycle, grow the queue
 DIM oldbound as integer = UBOUND(atkq)
 REDIM PRESERVE atkq(oldbound + 16) as AttackQueue
 FOR i as integer = oldbound + 2 TO UBOUND(atkq)
  clear_attack_queue_slot i
 NEXT i
 set_attack_queue_slot oldbound + 1, attack, who, delay, turn_delay, targs(), blocking, dont_retarget
END SUB

SUB set_attack_queue_slot(slot as integer, attack as integer, who as integer, delay as integer, turn_delay as integer, targs() as integer, blocking as bool=YES, dont_retarget as bool = NO)
 WITH atkq(slot)
  .used = YES
  .attack = attack
  .attacker = who
  .delay = delay   'Can be negative. In turnACTIVE mode, negative delay is equivalent to 0
  .turn_delay = turn_delay
  FOR i as integer = 0 TO UBOUND(.t)
   .t(i) = targs(i)
  NEXT i
  .blocking = blocking
  .dont_retarget = dont_retarget
 END WITH
END SUB

SUB clear_attack_queue()
 FOR i as integer = 0 TO UBOUND(atkq)
  clear_attack_queue_slot i
 NEXT i
END SUB

SUB clear_attack_queue_slot(byval slot as integer)
 WITH atkq(slot)
  .used = NO
  .attack = -1
  .attacker = -1
  .delay = 0
  .turn_delay = 0
  FOR i as integer = 0 TO UBOUND(.t)
   .t(i) = -1
  NEXT i
  .blocking = YES
  .dont_retarget = NO
 END WITH
END SUB

SUB display_attack_queue (bslot() as BattleSprite)
 ' Visualize the queue's targetting data with lines
 DIM as integer x1, y1, x2, y2, col
 FOR i as integer = 0 TO UBOUND(atkq)
  WITH atkq(i)
   IF .used THEN
    IF is_hero(.attacker) THEN
     col = findrgb(small(.attacker * 20, 255), 255, small(.attacker * 20, 255))
    ELSE
     col = findrgb(255, small(.attacker * 20, 255), small(.attacker * 20, 255))
    END IF
    x1 = bslot(.attacker).x + bslot(.attacker).w / 2
    y1 = bslot(.attacker).y + bslot(.attacker).h / 2
    FOR j as integer = 0 TO UBOUND(.t)
     IF .t(j) > -1 THEN
      x2 = bslot(.t(j)).x + bslot(.t(j)).w / 2
      y2 = bslot(.t(j)).y + bslot(.t(j)).h / 2
      drawline x1, y1, x2, y2, col, vpage
     END IF
    NEXT j
   END IF
  END WITH
 NEXT i

 ' Display the queue as text
 ' This is drawn at the top of the screen, rather than the top of the compatpage
 DIM s as string
 DIM targstr as string
 FOR i as integer = 0 TO UBOUND(atkq)
  WITH atkq(i)
   IF .used THEN
    s = .turn_delay & "/" & .delay & " " & bslot(.attacker).name & ":" & .attacker & " " & readattackname(.attack) & ":" & .attack & " "
    targstr = ""
    FOR j as integer = 0 TO UBOUND(.t)
     IF .t(j) > -1 THEN
      targstr &= CHR(24) & .t(j)
     END IF
    NEXT j
    s & = targstr & " " & yesorno(.blocking, "B", "N") & yesorno(.dont_retarget, "d", "")
   ELSE
    s = "-"
   END IF
   edgeprint s, 0, i * 10, uilook(uiText), vpage
  END WITH
 NEXT i
END SUB

FUNCTION has_queued_attacks(byval who as integer) as bool
 '--Returns YES if the specified hero or enemy has at least one attack queued.
 '--Returns NO if they do not.
 '--This is intended to check for both blocking and non-blocking queued attacks.
 FOR i as integer = 0 TO UBOUND(atkq)
  WITH atkq(i)
   IF .used THEN
    IF .attacker = who THEN RETURN YES
   END IF
  END WITH
 NEXT i
 RETURN NO
END FUNCTION

FUNCTION battle_time_can_pass(bat as BattleState) as bool
 ' Return YES if time is allowed to pass in active mode
 IF bat.atk.id <> -1 THEN RETURN NO 'an attack animation is going on right now
 IF bat.vic.state <> vicNONE THEN RETURN NO 'victory has already happened
 IF prefbit(21) AND bat.caption_time > 0 THEN RETURN NO ' "Attack captions pause battle meters"
 RETURN YES
END FUNCTION

FUNCTION battle_meters_can_advance(byref bat as BattleState, bslot() as BattleSprite) as bool
 '--Only used in turnACTIVE mode
 ' (Only relevant when battle_time_can_pass() = YES)
 ' Returns YES if battle_meters() should be called, which means:
 ' -regen, poison, stun, mute and attack delay timers should count down
 ' -when ready_meter_may_grow() = YES, ready meters for heroes and enemies can grow

 DIM hero_has_a_turn as bool = bat.player_turn >= 0
 
 IF NOT hero_has_a_turn THEN RETURN YES
 
 DIM pause_on_all as bool = prefbit(13) '"Pause on all battle menus & targeting"
 DIM pause_on_spell_and_item as bool = prefbit(0) '"Pause on spell & item menus"
 DIM pause_on_targeting as bool = prefbit(35) '"Pause when targeting attacks"

 'Try to avoid battle getting stuck if there are no possible actions
 'TODO: checking number of foes doesn't quite accomplish that
 IF count_foes(bat.player_turn, bslot()) > 0 THEN
  IF pause_on_all THEN
   /'
   IF bat.menu_mode >= 0 ORELSE bat.targ.mode > targNONE THEN
    'Always true during a player's turn, since bat.menu_mode is never < 0.
    'Intention was: "A menu is open or an attack is being targetted"
    'I'm not sure if/when that differs from player_turn >= 0, because this
    'function isn't called during an attack.
    RETURN NO
   END IF
   '/
   RETURN NO
  END IF
  IF pause_on_spell_and_item THEN
   IF bat.menu_mode > 0 THEN
    'Spell or item menu is open right now
    RETURN NO
   END IF
  END IF
  IF pause_on_targeting THEN
   IF bat.targ.mode > targNONE THEN
    'Somebody is currently manually targetting an attack
    RETURN NO
   END IF
  END IF
  
 END IF

 RETURN YES
END FUNCTION

SUB battle_background_anim(byref bat as BattleState, formdata as Formation)
 IF formdata.background_frames > 1 THEN
  loopvar bat.bg_tick, 0, formdata.background_ticks
  IF bat.bg_tick = 0 THEN
   loopvar bat.curbg, formdata.background, formdata.background + formdata.background_frames - 1
   ChangeSpriteSlice bat.backdrop_sl, , bat.curbg MOD gen(genNumBackdrops)
  END IF
 END IF
END SUB

FUNCTION battle_run_away(byref bat as BattleState, bslot() as BattleSprite) as bool
 '--this function is called every tick of battle. It returns YES if
 '-- a successful run has completed, thus ending battle.

 IF get_gen_bool("/mouse/mouse_battles") THEN
  'This is also used to close menus with the mouse
  IF (readmouse.buttons AND mouseRight) ANDALSO readmouse.drag_dist < 10 THEN
   bat.mouse_running += 1
  ELSE
   bat.mouse_running = 0
  END IF
 END IF
 
 IF bat.turn.mode = turnACTIVE ANDALSO bat.menu_mode = batMENUHERO THEN
  battle_crappy_run_handler bat, bslot()
 END IF
 
 '--bat.away will be set to a positive number if running has succeeded
 IF bat.away > 0 THEN
  battle_animate_running_away bslot()
  bat.away += 1
  IF bat.away > 10 THEN
   RETURN YES
  END IF
 END IF
 
 RETURN NO
END FUNCTION

SUB battle_animate_running_away (bslot() as BattleSprite)
 FOR i as integer = 0 TO 3
  '--if alive, animate running away
  IF gam.hero(i).id >= 0 ANDALSO bslot(i).stat.cur.hp > 0 THEN
   WITH bslot(i)
    IF .vis THEN
     .set_vel_x 6, 10
     .walk = 1
     .d = 1
    END IF
   END WITH
  END IF
 NEXT i
END SUB

SUB battle_check_delays(byref bat as BattleState, bslot() as BattleSprite)
 'If an attack in the attack queue is ready, start it.
 '--check the attack queue delays
 'NOTE: code here is partially duplicated with battle_reevaluate_dead_targets!
 FOR i as integer = 0 TO UBOUND(atkq)
  WITH atkq(i)
   IF atkq_attack_active(atkq(i), bslot()) THEN
    IF .delay <= 0 THEN
     'debug "queue trigger! " & bslot(.attacker).name & .attacker & ":" & readattackname(.attack)
     IF .t(0) = -1 THEN
      'debuginfo "queued attack " & readattackname(.attack) & " for " & bslot(.attacker).name & .attacker & " in slot " & i & " has null target."
      clear_attack_queue_slot i
      CONTINUE FOR
     END IF
     IF bslot(.t(0)).stat.cur.hp <= 0 AND NOT attack_can_hit_dead(.attack, bslot(.attacker).stored_targs_can_be_dead) THEN
      IF .t(0) = .attacker ANDALSO bslot(.attacker).bequesting THEN
       'If a bequesting attacker is targetting itself, we don't care that it is dead
      ELSE
       IF .dont_retarget THEN
        'debuginfo "queued attack " & readattackname(.attack) & " for " & bslot(.attacker).name & .attacker & " in slot " & i & " has dead target, and should not retarget, clearing."
        clear_attack_queue_slot i
       ELSE
        'debuginfo "queued attack " & readattackname(.attack) & " for " & bslot(.attacker).name & .attacker & " in slot " & i & " has dead target, retargetting."
        autotarget .attacker, .attack, bslot(), bat, .t(), NO
       END IF
      END IF
     END IF
     bat.atk.id = .attack
     bat.acting = .attacker
     FOR j as integer = 0 TO UBOUND(.t)
      bat.anim_t(j) = .t(j)
     NEXT j
     bat.anim_blocking_delay = .blocking
     bat.anim_ready = NO
     clear_attack_queue_slot i
     EXIT FOR
    END IF
   END IF
  END WITH
 NEXT i
END SUB

SUB force_retargetting_for_attacker (byref bat as BattleState, bslot() as BattleSprite, index as integer)
 FOR i as integer = 0 TO UBOUND(atkq)
  WITH atkq(i)
   IF .used THEN
    IF .attacker = index THEN
     autotarget .attacker, .attack, bslot(), bat, .t(), NO
    END IF
   END IF
  END WITH
 NEXT i
END SUB

'Get the next ready hero, if any, to start their turn
SUB battle_check_for_player_turns(byref bat as BattleState, bslot() as BattleSprite)
 loopvar bat.next_hero, 0, 3
 IF bat.player_turn > -1 THEN
  '--somebody is already taking their turn
  EXIT SUB
 END IF
 
 IF bat.enemy_turn > -1 ANDALSO bslot(bat.enemy_turn).under_player_control THEN
  'It is a player-controlled enemy's turn, so let them have a turn
  EXIT SUB
 END IF

 IF prefbit(23) AND bat.atk.id > -1 THEN  '"Battle menus wait for attack animations"
  '--an attack is currently animating, and we must wait for it
  EXIT SUB
 END IF

 '--if it is not currently any hero's turn, check to see if anyone is alive and ready
 FOR i as integer = 0 TO 3
  IF battle_check_a_player_turn(bat, bslot(), (i + bat.next_hero) MOD 4) THEN
   EXIT FOR
  END IF
 NEXT i
END SUB

'Note that this doesn't check everything - see battle_check_for_player_turns
FUNCTION hero_or_enemy_can_take_a_turn (byval who as integer, bat as BattleState, bslot() as BattleSprite) as bool
 IF bslot(who).ready = NO THEN RETURN NO
 IF bslot(who).stat.cur.hp <= 0 THEN RETURN NO
 IF bat.death_mode <> deathNOBODY THEN RETURN NO
 IF has_blocking_turn_delayed_attacks(who) THEN RETURN NO
 IF bslot(who).no_attack_this_turn THEN RETURN NO
 RETURN YES
END FUNCTION

'If this hero is ready, make them start their turn
FUNCTION battle_check_a_player_turn(byref bat as BattleState, bslot() as BattleSprite, byval index as integer) as bool
 IF hero_or_enemy_can_take_a_turn(index, bat, bslot()) THEN
  bat.player_turn = index
  bslot(bat.player_turn).menust.pt = 0
  bat.menu_mode = batMENUHERO
  RETURN YES
 END IF
 RETURN NO
END FUNCTION

SUB battle_check_for_enemy_turns(byref bat as BattleState, bslot() as BattleSprite)
 loopvar bat.next_enemy, 4, 11
 IF bat.enemy_turn = -1 THEN
  '--if no enemy is currently taking their turn, check to find an enemy who is ready
  DIM slot as integer = bat.next_enemy
  FOR i as integer = 4 TO 11
   IF battle_check_an_enemy_turn(bat, bslot(), slot) THEN EXIT FOR
   loopvar slot, 4, 11
  NEXT i
 END IF
END SUB

FUNCTION battle_check_an_enemy_turn(byref bat as BattleState, bslot() as BattleSprite, byval index as integer) as bool
 IF hero_or_enemy_can_take_a_turn(index, bat, bslot()) THEN
  bat.enemy_turn = index
  RETURN YES
 END IF
 RETURN NO
END FUNCTION

FUNCTION blocked_by_attack (bat as BattleState, byval who as integer) as bool
 FOR i as integer = 0 TO UBOUND(atkq)
  WITH atkq(i)
   IF .used ANDALSO .attacker = who ANDALSO .blocking THEN
    SELECT CASE bat.turn.mode
     CASE turnACTIVE:
      'Restrict to .turn_delay <= 0, because if there's an attack with a turn
      'delay the ready meter needs to continue to grow
      IF .turn_delay <= 0 ANDALSO .delay > 0 THEN RETURN YES
     CASE turnTURN:
      '...but that means this is inconsistent
      IF .delay > 0 ORELSE .turn_delay > 0 THEN RETURN YES
    END SELECT
   END IF
  END WITH
 NEXT i
 RETURN NO
END FUNCTION

FUNCTION ready_meter_may_grow (bat as BattleState, bslot() as BattleSprite, byval who as integer) as bool
 '--Only used in turnACTIVE mode
 'Note: this is also a list of conditions in which ready meters don't grow but
 'regen/poison/stun/mute and attack delays can countdown.
 WITH bslot(who)
  IF .attack <> 0 THEN RETURN NO
  IF .dissolve <> 0 THEN RETURN NO
  IF .stat.cur.stun < .stat.max.stun THEN RETURN NO
  IF .ready = YES THEN RETURN NO
 END WITH
 IF blocked_by_attack(bat, who) THEN RETURN NO
 RETURN YES
END FUNCTION

SUB battle_attack_cancel_target_attack(byval targ as integer, byref bat as BattleState, bslot() as BattleSprite, byref attack as AttackData)
 IF attack.cancel_targets_attack THEN
  '--try to cancel target's attack
  DIM targets_attack as AttackData
  FOR i as integer = 0 TO UBOUND(atkq)
   WITH atkq(i)
    IF .used ANDALSO .attacker = targ THEN
     loadattackdata targets_attack, .attack
     IF targets_attack.not_cancellable_by_attacks = NO THEN
      'Okay to cancel target's attack
      clear_attack_queue_slot i
     END IF
    END IF
   END WITH
  NEXT i
 END IF
 IF attack.cancel_targets_attack OR bslot(targ).stat.cur.stun < bslot(targ).stat.max.stun OR attack.empty_target_ready_meter THEN
  '--If the currently targeting hero is the one hit, stop targetting
  '--note that stunning implies cancellation of untargetted attacks,
  '--but does not imply cancellation of already-targeted attacks.
  '--emptying target ready meter also implies cancelling current targeting.
  IF bat.player_turn = targ THEN
   bat.targ.mode = targNONE
   bat.player_turn = -1
   bslot(targ).attack = 0
  END IF
 END IF
END SUB

FUNCTION check_has_remaining_targets(bat as BattleState, bslot() as BattleSprite, targs() as integer) as bool
 'Returns whether a multihit attack should continue: either it can hit dead targets, or
 'any of the targets are alive, or dying but have not finished dying yet.
 IF attack_can_hit_dead(bat.atk.id, YES) THEN RETURN YES
 FOR i as integer = 0 TO UBOUND(targs)
  IF targs(i) > -1 THEN
   IF bslot(targs(i)).stat.cur.hp > 0 ORELSE bslot(targs(i)).vis THEN RETURN YES
  END IF
 NEXT i
 RETURN NO
END FUNCTION

SUB battle_reevaluate_dead_targets (byval deadguy as integer, byref bat as BattleState, bslot() as BattleSprite)
 '--check for queued attacks that target the dead target
 'NOTE: code here is partially duplicated with battle_check_delays!
 FOR i as integer = 0 TO UBOUND(atkq)
  WITH atkq(i)
   IF .used THEN
    DIM attack as AttackData
    loadattackdata attack, .attack
    
    'DIM s as string
    's = attack.name & " of " & bslot(.attacker).name
    'dim showdebug as integer=NO
    'for j as integer = 0 to ubound(.t)
    ' if deadguy = .t(j) then s &= " was targeting " & bslot(deadguy).name & deadguy & " who died": showdebug=YES
    'next j
    'if showdebug then debug s
    
    IF NOT attack_can_hit_dead(.attack, bslot(.attacker).stored_targs_can_be_dead) THEN
     battle_sort_away_dead_t_target deadguy, .t()
    END IF
    IF .t(0) = -1 THEN
     'if no targets left, auto-re-target
     IF .dont_retarget THEN
      'debuginfo "queued " & readattackname(.attack) & " for " & bslot(.attacker).name & " - last targ died and should not retarget"
      clear_attack_queue_slot i
     ELSE
      autotarget .attacker, .attack, bslot(), bat, .t(), NO
     END IF
    END IF
   END IF
  END WITH
 NEXT i
 
 '--cancel current interactive targetting that points to the dead target (unless the attack is allowed to target dead)
 IF bat.targ.hit_dead = NO THEN
  IF bat.targ.mask(deadguy) = YES THEN bat.targ.mask(deadguy) = NO
  IF bat.targ.selected(deadguy) = YES THEN bat.targ.selected(deadguy) = NO
  '--if current interactive targeting points to the dead target, find a new target
  WITH bat.targ
   IF .pointer = deadguy THEN
    .pointer = 0
    WHILE .mask(.pointer) = NO
     .pointer += 1
     IF .pointer > UBOUND(.mask) THEN
      .mode = targNONE
      EXIT WHILE
     END IF
    WEND
   END IF
  END WITH
 END IF  '----END ONLY WHEN bat.targ.hit_dead = NO
END SUB

SUB battle_sort_away_dead_t_target(byval deadguy as integer, t() as integer)
 '--FIXME: la la la! James loves Bogo-sorts!
 FOR i as integer = 0 TO UBOUND(t) - 1
  '--crappy bogo-sort dead target away
  IF t(i) = deadguy THEN SWAP t(i), t(i + 1)
 NEXT i
 IF t(UBOUND(t)) = deadguy THEN t(UBOUND(t)) = -1
END SUB

SUB battle_counterattacks(bat as BattleState, byval h as integer, byval targstat as integer, byval who as integer, attack as AttackData, bslot() as BattleSprite, atkresult as AttackResult)

 DIM provoke as CounterProkeEnum = attack.counterattack_provoke
 IF provoke = provokeDefault THEN provoke = gen(genDefCounterProvoke)

 SELECT CASE provoke
  CASE provokeAlways
  CASE provokeNever     : EXIT SUB
  CASE provokeHit       : IF atkresult <> atkHit  THEN EXIT SUB
  CASE provokeMiss      : IF atkresult <> atkMiss THEN EXIT SUB
  CASE provokeFail      : IF atkresult <> atkFail THEN EXIT SUB
  CASE provokeDidntHit  : IF atkresult =  atkHit  THEN EXIT SUB
  CASE provokeDidntMiss : IF atkresult =  atkMiss THEN EXIT SUB
  CASE provokeDidntFail : IF atkresult =  atkFail THEN EXIT SUB
  CASE ELSE
   showbug "Invalid counterattack_provoke " & provoke & ", def: " & gen(genDefCounterProvoke)
 END SELECT

 DIM blocking as bool = NO
 'counterattacks are forced non-blocking for active-mode (can be overridden by a bit later in autotarget)
 IF bat.turn.mode = turnTURN THEN blocking = YES 'But in turn-based mode, they are blocking
 '--first elementals
 IF NOT attack.never_trigger_elemental_counterattacks THEN
  FOR i as integer = 0 TO gen(genNumElements) - 1
   IF attack.elemental_damage(i) THEN
    IF bslot(who).elem_counter_attack(i) > 0 THEN
     autotarget who, bslot(who).elem_counter_attack(i) - 1, bslot(), bat, YES, blocking, ,YES
     EXIT SUB '-- only one counterattack per trigger attack
    END IF
   END IF
  NEXT i
 END IF
 '--then non-elemental attack
 IF bat.atk.non_elemental THEN
  IF bslot(who).non_elem_counter_attack > 0 THEN
   autotarget who, bslot(who).non_elem_counter_attack - 1, bslot(), bat, YES, blocking, ,YES
   EXIT SUB '-- only one counterattack per trigger attack
  END IF
 END IF
 '-then stat damage 
 FOR i as integer = 0 TO 11
  IF h > 0 AND targstat = i THEN
   IF bslot(who).stat_counter_attack(i) > 0 THEN
    autotarget who, bslot(who).stat_counter_attack(i) - 1, bslot(), bat, YES, blocking, ,YES
    EXIT SUB '-- only one counterattack per trigger attack
   END IF
  END IF
 NEXT i
END SUB

SUB show_first_battle_timer (page as integer)
 '--show the timer
 ' Why doesn't this use the string's position, colour, or font?!
 IF prefbit(38) THEN EXIT SUB  '"Never show script timer during battles"
 FOR i as integer = 0 to UBOUND(timers)
  IF timers(i).speed > 0 ANDALSO timers(i).st > 0 ANDALSO timers(i).flags AND TIMERFLAG_BATTLE THEN
   edgeprint plotstr(timers(i).st-1).s, pRight - 10, pBottom - 5, uilook(uiText), page
   EXIT FOR 'Only print the first timer if there are many of them
  END IF
 NEXT i
END SUB

FUNCTION pending_attacks_for_this_turn(bat as BattleState, bslot() as BattleSprite) as bool
 'Returns whether there is a pending attack left for this round. Used by turnTURN mode
 
 'Check for a currently animating attack
 IF bat.atk.id >= 0 THEN RETURN YES

 IF bat.caption_time > 0 THEN RETURN YES 'Check for attack captions
 
 'Check for queued attacks
 FOR i as integer = 0 TO UBOUND(atkq)
  WITH atkq(i)
   'Ignore unused atkq() slots, attacks with a turn delay, and stunned attackers
   IF atkq_attack_active(atkq(i), bslot()) THEN
    IF prefbit(50) = NO THEN ' !Non-turn attack delays can also cause turn delays
     'Emulate bug #1118:
     '--only blocking queued attacks are considered part of the current
     ' turn (although it is always perfectly possible for a nonblocking
     ' attack to happen in the current turn)
     IF .blocking THEN RETURN YES
    ELSE
     ' Don't go to the next round until only attacks with turn delays are left
     RETURN YES
    END IF
   END IF
  END WITH
 NEXT i
 
 RETURN NO
END FUNCTION

SUB ready_all_valid_units(bslot() as BattleSprite, formdata as Formation)
 'In turnTURN mode, force all valid living heroes and enemies to be ready to take their turn
 FOR i as integer = 0 TO 11
  bslot(i).no_attack_this_turn = NO
 NEXT i
 FOR i as integer = 0 TO 3
  IF gam.hero(i).id >= 0 ANDALSO bslot(i).stat.cur.hp > 0 THEN
   bslot(i).ready = YES
   bslot(i).ready_meter = 1000 'Filling the ready meter only matters for visual indication
  END IF
 NEXT i
 FOR i as integer = 4 TO 11
  IF formdata.slots(i - 4).id >= 0 ANDALSO bslot(i).stat.cur.hp > 0 THEN
   bslot(i).ready = YES
   bslot(i).ready_meter = 1000 'Filling the ready meter only to be consistent with the heroes
  END IF
 NEXT i
END SUB

SUB active_mode_state_machine (bat as BattleState, bslot() as BattleSprite, formdata as Formation)
 IF battle_time_can_pass(bat) THEN
  'No attack already happening, not paused for a caption...
  IF battle_meters_can_advance(bat, bslot()) THEN
   'Advance ready meters, turn delays, poison/regen/etc, and decrement queued attack delays
   'Note that battle meters always advance at least one tick whenever a queued attack
   'happens (which was not intentional. See sf#2041)
   battle_meters bat, bslot(), formdata
  END IF
  'If a queued attack has no delay left, start it
  battle_check_delays bat, bslot()
 END IF
 fulldeathcheck -1, bat, bslot(), formdata
 battle_check_for_player_turns bat, bslot()
 battle_check_for_enemy_turns bat, bslot()
END SUB

SUB turn_mode_state_machine (bat as BattleState, bslot() as BattleSprite, formdata as Formation)

 IF bat.vic.state <> vicNONE THEN EXIT SUB 'victory has already happened
 IF bat.death_mode <> deathNOBODY THEN EXIT SUB 'Death animation is happening

 IF bat.atk.id > 0 THEN EXIT SUB 'an attack is animating now, wait patiently.

 'FIXME: the intention is to prevent battles from getting stuck if there are no
 'possible actions. But checking for lack of visible enemies isn't the same thing.
 /'
 IF bat.player_turn >= 0 THEN
  IF count_foes(bat.player_turn, bslot()) = 0 THEN
   'Player automatically loses turn? Add a Skip Turn option?
  END IF
 END IF
 '/

 IF bat.player_turn >= 0 THEN EXIT SUB 'somebody already taking a turn, so wait patiently
 IF bat.enemy_turn >= 0 THEN EXIT SUB

 IF bat.turn.choosing_attacks THEN
  
  IF bat.turn.reverse THEN
   'debug "Reverse! bat.next_hero=" & bat.next_hero
   DO
    bat.next_hero = large(0, bat.next_hero - 1)
    IF is_enemy(bat.next_hero) THEN
     bat.next_enemy = bat.next_hero
    ELSE
     bat.next_enemy = 4
    END IF
    WITH bslot(bat.next_hero)
     IF .stat.cur.hp > 0 ANDALSO .no_attack_this_turn = NO ANDALSO .under_player_control THEN
      cancel_blocking_attacks_for_hero_or_enemy bat.next_hero
      bslot(bat.next_hero).ready = YES
      bslot(bat.next_hero).ready_meter = 1000
      EXIT DO
     END IF
    END WITH
    IF bat.next_hero = 0 THEN EXIT DO
   LOOP
   'debug "  END LOOP bat.next_hero=" & bat.next_hero
   bat.turn.reverse = NO
  END IF
  
  DO WHILE bat.next_hero <= 3
   IF battle_check_a_player_turn(bat, bslot(), bat.next_hero) THEN
    'debug "Hero " & bat.player_turn & " " & bslot(bat.player_turn).name & " is picking attack"
    bat.next_hero += 1
    EXIT SUB
   END IF
   bat.next_hero += 1
  LOOP
  
  DO WHILE bat.next_enemy <= 11
   IF battle_check_an_enemy_turn(bat, bslot(), bat.next_enemy) THEN
    'debug "Enemy " & bat.enemy_turn & " " & bslot(bat.enemy_turn).name & " is picking attack"
    bat.next_enemy += 1
    EXIT SUB
   END IF
   bat.next_enemy += 1
  LOOP
  
  '--All attacks are chosen, update stun and mute.
  FOR i as integer = 0 to 11
   WITH bslot(i).stat
    .cur.mute = small(.cur.mute + 1, .max.mute)
    .cur.stun = small(.cur.stun + 1, .max.stun)
   END WITH
  NEXT i
  
  apply_initiative_order bslot()
  
  '--Attack selection is finished, animate this turn!
  bat.turn.choosing_attacks = NO
 END IF
  
 '--either start an attack, or go to the next round
 IF pending_attacks_for_this_turn(bat, bslot()) = NO THEN
  IF NOT bat.turn.end_of_round_happened THEN
   'At the end of the turn, make a single attempt to trigger poison and regen.
   trigger_turnmode_poison_and_regen bat, bslot(), formdata
   bat.turn.end_of_round_happened = YES
  ELSE
   'The next turn begins after all attacks are done, and after poison/regen are done, and after
   'any on-death-bequest attacks from poison are done
   start_next_turn bat, bslot(), formdata
  END IF
 ELSE
  turn_mode_time_passage bat, bslot()
 END IF

END SUB

'Start the next attack
SUB turn_mode_time_passage (bat as BattleState, bslot() as battleSprite)
 IF bat.atk.id >= 0 THEN EXIT SUB 'Check for a currently animating attack

 IF bat.caption_time > 0 THEN EXIT SUB 'Check for attack captions

 IF bat.away > 0 THEN EXIT SUB 'no time if the heroes have already run away

 'Shift the .delay for active atkq() attacks, so the next one has .delay = 0
 turn_mode_decrement_attack_queue_delays bslot()

 'Start the next attack (now has .delay = 0)
 battle_check_delays bat, bslot()
END SUB

'Shift the delays of queued attacks so that the next attack has delay 0, so it
'will be picked by battle_check_delays().
'(Compare to decrement_attack_queue_delays() for turnACTIVE.)
'By shifting, if a new attack is chained to and added to the queue with
'atkq(...).delay equal to .attack_delay, the delay will be relative to the
'current attack (eg the one that it chained from).
'Note: if an attacker is stunned, their attack isn't updated and languishes;
'it's also ignored by battle_check_delays()
SUB turn_mode_decrement_attack_queue_delays(bslot() as BattleSprite)
 DIM mindelay as integer = INT_MAX

 FOR i as integer = 0 TO UBOUND(atkq)
  IF atkq_attack_active(atkq(i), bslot()) THEN
   mindelay = small(mindelay, atkq(i).delay)
  END IF
 NEXT i

 FOR i as integer = 0 TO UBOUND(atkq)
  IF atkq_attack_active(atkq(i), bslot()) THEN
   atkq(i).delay -= mindelay
  END IF
 NEXT i
END SUB

SUB trigger_turnmode_poison_and_regen (bat as BattleState, bslot() as BattleSprite, formdata as Formation)
  FOR i as integer = 0 to 11
   '--update poison and regen
   WITH bslot(i).stat
    IF .cur.poison < .max.poison THEN do_poison i, bat, bslot(), formdata
    IF .cur.regen < .max.regen THEN do_regen i, bat, bslot(), formdata
   END WITH
  NEXT i
END SUB

SUB start_next_turn (bat as BattleState, bslot() as BattleSprite, formdata as Formation)
  'A new turn starts! (turnTURN mode only!)
  
  bat.turn.number += 1
  bat.turn.choosing_attacks = YES
  bat.turn.end_of_round_happened = NO
  
  bat.next_hero = 0
  bat.next_enemy = 4
  ready_all_valid_units bslot(), formdata

  FOR i as integer = 0 to 11
   WITH bslot(i).stat
    IF .cur.stun < .max.stun THEN
     '--note that stun and mute are updated after the attacks are chosen
     bslot(i).ready = NO
     bslot(i).ready_meter = 0 '--cosmetic
     bslot(i).no_attack_this_turn = YES
    END IF
   END WITH
   '--no turn for heroes with blocking turn-delayed attacks
   IF has_blocking_turn_delayed_attacks(i) THEN
    bslot(i).ready = NO
    bslot(i).ready_meter = 0 '--cosmetic
    bslot(i).no_attack_this_turn = YES
   END IF
  NEXT i

  '--figure out initiative_order based on speed
  calc_initiative_order bslot(), formdata

  '--update turn delays in attack queue
  FOR i as integer = 0 to 11
   update_turn_delays_in_attack_queue i
  NEXT I
  
  'debug "Turn #" & bat.turn.number & " has begun!"
END SUB

SUB calc_initiative_order (bslot() as BattleSprite, formdata as Formation)
 '--Only used for turnTURN mode
 
 '--first clear old initiative
 FOR i as integer = 0 to 11
  bslot(i).initiative_order = 0
 NEXT i
 
 '--Copy speeds into a temporary integer array
 DIM speeds(11) as integer
 FOR i as integer = 0 to 11
  IF is_hero(i) THEN
   IF NOT (gam.hero(i).id >= 0 ANDALSO bslot(i).stat.cur.hp > 0) THEN
    speeds(i) = -1
    CONTINUE FOR
   END IF
  END IF
  IF is_enemy(i) THEN
   IF NOT (formdata.slots(i - 4).id >= 0 ANDALSO bslot(i).stat.cur.hp > 0) THEN
    speeds(i) = -1
    CONTINUE FOR
   END IF
  END IF
  speeds(i) = bslot(i).stat.cur.spd
  IF prefbit(51) = NO THEN  '"Don't break Speed ties randomly"
   speeds(i) = 100 * speeds(i) + randint(100)
  END IF
  'debug bslot(i).name & " speed = " & bslot(i).stat.cur.spd & " sort index " & speeds(i)
 NEXT i
 
 '--Sort indexes by speed
 DIM order(11) as integer
 sort_integers_indices order(), @speeds(0)
 
 DIM j as integer = 0
 FOR i as integer = 11 TO 0 STEP -1
  IF speeds(order(i)) = -1 THEN EXIT FOR
  bslot(order(i)).initiative_order = j
  'debug "Initiative " & j & " " & bslot(order(i)).name
  j += 1
 NEXT i
END SUB

'Turn-based only. Finish setting .delay for each atkq - called once at the start of each round.
'The .delay of a queued attack is a key used to determine the attack order. Each
'attack will have a unique delay to avoid ties, because a tie would result in
'battle_check_delays picking the first one. (This is separate from breaking attacker Speed ties.)
'Initially, .delay = A * ATK_DELAY_MULT + B
' A is the primary sort key, equal to initiative order plus the attack's .attack_delay.
' B is the secondary sort key, equal to .attack_delay.
'As attacks are processed, .delays get shifted so the next attack has .delay = 0.
'Rationale:
'Delays specify how many places forwards or backwards in the queue an attack should
'be moved, but if two attacks want to move to the same place, the most delayed one
'should happen last, and the most advanced one first. E.g:
'  0,  1,  2,  3   <-- Attacks in initial order (the initiative order)
'  0,  1,  0,  0   <-- Delays: attack 1 should move back one place
'       \->
'There is a tie for the 3rd place, should be broken as:
'  0,  2,  1,  3   <-- Final attack order
'If we add an extra delay to attacks 1 and 2:
'  0,  1,  2,  3   <-- Initial attack order
'  0,  2,  1,  0   <-- Delays
'       \--\==>
'Then, adjusting the previous result, by moving both attacks 1 place, the result should be
'  0,  3,  2,  1
'This shows the most delayed attack should happen last in case of tie - .attack_delay
'should be the secondary key.
SUB apply_initiative_order (bslot() as BattleSprite)
 FOR j as integer = 0 to UBOUND(atkq)
  WITH atkq(j)
   'Don't apply to attacks with .turn_delay < 0, they've already been processed in a previous round
   '(such attacks occur only if "Non-turn attack delays can also cause turn delays" backcompat off)
   IF .used ANDALSO .turn_delay = 0 THEN
    DIM addend as integer = ATK_DELAY_MULT * bslot(.attacker).initiative_order
    'debug "Applying initiative: adjust " & bslot(.attacker).name & "'s attack " & .attack & ": attack_delay " & .delay & " + initiative " & addend & " = delay " & (.delay + addend)
    .delay += addend
   END IF
  END WITH
 NEXT j
END SUB

SUB cancel_blocking_attacks_for_hero_or_enemy(byval who as integer)
 FOR i as integer = 0 TO UBOUND(atkq)
  WITH atkq(i)
   IF .used ANDALSO .attacker = who ANDALSO .blocking THEN
    clear_attack_queue_slot i
   END IF
  END WITH
 NEXT i
END SUB

SUB update_turn_delays_in_attack_queue (byval who as integer)
 FOR i as integer = 0 TO UBOUND(atkq)
  WITH atkq(i)
   'If the "Non-turn attack delays can also cause turn delays" backcompat is on, we need to
   'allow turn_delay to go negative to mark non-blocking attacks in turn-based mode
   'that got shunted to the next round, which therefore shouldn't have initiative added again
   IF .used ANDALSO .attacker = who THEN
    .turn_delay -= 1
   END IF
  END WITH
 NEXT i
END SUB

FUNCTION has_blocking_turn_delayed_attacks(byval who as integer) as bool
 FOR i as integer = 0 TO UBOUND(atkq)
  WITH atkq(i)
   IF .used ANDALSO .attacker = who ANDALSO .turn_delay > 0 ANDALSO .blocking THEN
    RETURN YES
   END IF
  END WITH
 NEXT i
 RETURN NO
END FUNCTION

'==========================================================================================

' This sub does three different things, depending on the state of the DebugMenuDef:
' - Checks for debug key combos. dbg.def() returns true if that key is pressed.
' - Builds a list of available debug menu items. dbg.def() returns false.
' - Performs an action selected in the debug menu. dbg.def() returns true if selected.
' See debug_menu_functions() for more info.
SUB battle_debug_menu_functions(dbg as DebugMenuDef, bat as BattleState, bslot() as BattleSprite, formdata as Formation)

 IF gam.debug_timings THEN
  IF dbg.def(      , scF1, "Help for CPU usage (F1)") THEN show_help "game_cpu_usage"
 END IF

 IF dbg.def(      , scF4, "Tag debugger (F4)") THEN
  loopvar gam.debug_showtags, 0, 2
  gam.debug_scripts = 0
  gam.cpu_usage_mode.disable
 END IF

 IF dbg.def(SftCtl, scF4, "View/edit slice tree (S/C-F4)") THEN slice_editor bat.root_sl
 IF dbg.def(      , scF5, "Give million experience (F5)") THEN bat.rew.exper = 1000000
 IF dbg.def(SftCtl, scF5, "Run instantly (Shft/Ctrl-F5)") THEN bat.away = 11

 DIM temp as string
 IF bat.turn.mode = 0 THEN temp = "Switch to turn-based battles" ELSE temp = "Switch to active-time battles"
 IF dbg.def(      , scF6, temp & " (F6)") THEN
  bat.turn.mode XOR= 1
  notification "bat.turn.mode=" & bat.turn.mode
 END IF

 IF dbg.def(SftCtl, scF6, "Show CPU usage (Shft/Ctrl-F6)") THEN
  loopvar gam.debug_timings, 0, 2
  IF gam.debug_timings = 0 THEN gam.cpu_usage_mode.disable
  gam.debug_showtags = 0
  gam.debug_scripts = NO
 END IF

 IF dbg.def(      , scF7, "Kill all targetable enemies (F7)") THEN
  FOR slot as integer = 4 TO 11
   WITH bslot(slot)
    IF .hero_untargetable = NO THEN
     .stat.cur.hp = 0
     triggerfade slot, bslot()
    END IF
   END WITH
  NEXT
  fulldeathcheck -1, bat, bslot(), formdata
 END IF

 IF dbg.def(SftCtl, scF7, "Player control of enemies (Shft/Ctrl-F7)") THEN
  FOR slot as integer = 4 TO 11
   WITH bslot(slot)
    .under_player_control = YES
   END WITH
  NEXT
  bat.debug_player_control = YES ' makes spawned and transmogrified enemies player controlled also
  notification "Player Control Of All Enemies"
 END IF

 IF dbg.def(      , scF8) THEN battle_debug_menu bat, bslot(), formdata
 dbg.def(      ,     , "Debug menu (F8)")  'Does nothing, but document F8.

 'Shift/Ctrl+F8 handled in allmodex
 DIM note as string
 IF num_logged_errors THEN note = ": " & num_logged_errors & " errors" ELSE note = " log"
 IF dbg.def(      ,     , "View g_debug.txt" & note & " (S/C-F8)") THEN open_document log_dir & *app_log_filename

 IF dbg.def(      , scF10, "Show enemy meters (F10)") THEN bat.debug_show_info = IIF(bat.debug_show_info = 1, 0, 1)
 IF dbg.def(      , scF11, "Show attack queue (F11)") THEN bat.debug_show_info = IIF(bat.debug_show_info = 2, 0, 2)

 'Only for documentation, scPause is checked in main loop
 IF dbg.def(      , , "Pause battle (Pause)") THEN battle_pause

 'F12 for bat.test_view_mode handled in main loop

 IF dbg.def(      , , "Dump battle slots info") THEN dump_bslot bat, bslot(), formdata
END SUB

' Check for debug key combos.
SUB check_battle_debug_keys(bat as BattleState, bslot() as BattleSprite, formdata as Formation)
 DIM dbg as DebugMenuDef
 battle_debug_menu_functions(dbg, bat, bslot(), formdata)
END SUB

' Show a menu of debug functions.
SUB battle_debug_menu(bat as BattleState, bslot() as BattleSprite, formdata as Formation)
 ' Build
 DIM dbg as DebugMenuDef
 dbg.start_building_menu()
 battle_debug_menu_functions(dbg, bat, bslot(), formdata)
 DIM menu() as string
 vector_to_array menu(), dbg.menu

 ' Show
 DIM result as integer
 STATIC default as integer = 0
 result = multichoice("Battle Debug Menu", menu(), default, , "game_battle_debug_menu")
 IF result = -1 THEN EXIT SUB

 ' Enact
 default = result
 dbg.selected_item = menu(result)
 battle_debug_menu_functions(dbg, bat, bslot(), formdata)
END SUB

SUB dump_bslot(bat as BattleState, bslot() as BattleSprite, formdata as Formation)
 DIM info as string
 info = "----Dumping heroes & enemies bslot state----"
 FOR slot as integer = 0 TO 11
  WITH bslot(slot)
   'I don't know whether this is the right way to exclude empty slots or not...
   IF .vis = NO ANDALSO .dissolve = 0 ANDALSO .fleeing = NO ANDALSO .stat.cur.hp <= 0 ANDALSO .bequesting = NO THEN CONTINUE FOR

   info &= !"\nSlot " & slot & IIF(is_hero(slot), " (hero)", " (enemy)")
   IF is_enemy(slot) THEN info &= !"\n orig. formation enemy = " & formdata.slots(slot - 4).id
   info &= !"\n name = " & .name & _
           !"\n vis = " & .vis & _
           !"\n dissolve = " & .dissolve & _
           !"\n fleeing = " & yesorno(.fleeing) & _
           !"\n bequesting = " & yesorno(.bequesting) & _
           !"\n attack = " & .attack - 1
   IF bat.turn.mode = turnACTIVE THEN
    info &= !"\n ready_meter = " & .ready_meter & _
            !"\n ready = " & .ready
   END IF
   info &= !"\n Stats:  "
   FOR stat as integer = 0 TO UBOUND(.stat.cur.sta)
    info &= battle_statnames(stat) & ":" & .stat.cur.sta(stat) & "/" & .stat.max.sta(stat) & "  "
   NEXT stat
  END WITH
 NEXT
 debug info
 show_overlay_message "Wrote debug info to g_debug.txt"
END SUB

'==========================================================================================

FUNCTION hero_attack_cost_info(byref atk as AttackData, byval hero_slot as integer, byval magic_list_type as integer=0, byval lmp_level as integer=-1) as string
 DIM cur_lmp as integer = 0
 IF magic_list_type = 1 ANDALSO lmp_level > -1 THEN
  cur_lmp = gam.hero(hero_slot).levelmp(lmp_level)
 END IF
 RETURN attack_cost_info(atk,_
   gam.hero(hero_slot).stat.cur.focus,_
   gam.hero(hero_slot).stat.cur.mp,_
   gam.hero(hero_slot).stat.max.mp,_
   magic_list_type,_
   lmp_level,_
   cur_lmp)
END FUNCTION

FUNCTION bslot_attack_cost_info(bslot() as BattleSprite, byref atk as AttackData, byval slot as integer, byval magic_list_type as integer=0, byval lmp_level as integer=-1) as string
 DIM cur_lmp as integer = 0
 IF is_hero(slot) THEN
  IF magic_list_type = 1 ANDALSO lmp_level > -1 THEN
   cur_lmp = gam.hero(slot).levelmp(lmp_level)
  END IF
 END IF
 RETURN attack_cost_info(atk,_
   bslot(slot).stat.cur.focus,_
   bslot(slot).stat.cur.mp,_
   bslot(slot).stat.max.mp,_
   magic_list_type,_
   lmp_level,_
   cur_lmp)
END FUNCTION
