'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 reset_rewards_state (byref rew as RewardsState) 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 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 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) 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 '--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 show_victory bat, bslot(), uipage 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 '--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 act = popdw SELECT CASE act CASE 0 '--end() 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 bat.atk.id = -1 CASE 1 '??? Reset hero positions, I guess? FIXME: figure out when this is used FOR i = 0 TO 3 formdata.slots(i).pos = bslot(4 + i).pos NEXT i bat.atk.id = -1 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 '???() '--undefined 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() 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 'unimplemented 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 END SELECT LOOP UNTIL bat.wait_frames <> 0 OR 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) '--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 bslot(bat.acting).self_bequesting = NO 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 keyval(scS) > 1 then gen(genMaxInventory) += 3 'if keyval(scA) > 1 then gen(genMaxInventory) -= 3 IF bat.menu_mode = batMENUITEM THEN '--draw item menu 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 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 AND bat.wait_frames = 0 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 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 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_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 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 itembuf(dimbinsize(binITM)) as integer 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 itembuf(), wep_id atk_id = itembuf(48) - 1 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 '--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 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 show_victory (byref bat as BattleState, bslot() as BattleSprite, page as integer) DIM tempstr as string WITH bat.vic IF .box THEN centerfuz 160, 30, 280, 50, 1, page .display_ticks += 1 SELECT CASE .state CASE vicGOLDEXP '--print acquired gold and experience IF bat.rew.plunder > 0 OR bat.rew.exper > 0 THEN .box = YES: centerfuz 160, 30, 280, 50, 1, 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 numlevelled = large(numlevelled, 4) centerfuz 160, 10 + 5 * numlevelled, 280, 10 + 10 * numlevelled, 1, page .box = YES END IF 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 .box = NO 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 .box = 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 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) '--actually acquire the item getitem pickedup.id, pickedup.num '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 IF LEN(tempstr) THEN centerfuz 160, 30, 280, 50, 1, 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 .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 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_rewards_state (byref rew as RewardsState) 'This could become a constructor for RewardsState when we support the -lang fb dialect DIM i as integer WITH rew .plunder = 0 .exper = 0 FOR i = 0 TO UBOUND(.found) .found(i).id = 0 'Note: id is not offset; num=0 indicates slot not used .found(i).num = 0 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 .box = NO .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 itembuf(dimbinsize(binITM)) as integer 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 itembuf(), inventory(i).id IF itembuf(47) > 0 THEN ' This item is usable in battle loadattackdata attack, itembuf(47) - 1 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 SUB anim_waitforall() pushdw 9 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 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) 'After 3 ticks we 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 .flinch_anim -= 1 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 DIM already_bequested as bool = NO IF bslot(deadguy).bequesting = YES THEN IF has_queued_attacks(deadguy) THEN EXIT SUB END IF bslot(deadguy).bequesting = NO already_bequested = YES 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 IF autotarget(deadguy, .enemy.bequest_attack - 1, bslot(), bat, beq_t()) THEN '--Bequest attack only happens if it has a valid target .bequesting = YES .self_bequesting = is_among_targets(deadguy, beq_t()) .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 itembuf(dimbinsize(binITM)) as integer DIM attack as AttackData FOR i as integer = 0 TO inventoryMax IF inventory(i).used THEN loaditemdata itembuf(), inventory(i).id IF itembuf(47) > 0 THEN ' This item is usable in battle loadattackdata attack, itembuf(47) - 1 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 itembuf(dimbinsize(binITM)) as integer loaditemdata itembuf(), inventory(bat.item.pt).id bslot(bat.player_turn).consume_item = IIF(itembuf(73) = 1, bat.item.pt, -1) bslot(bat.player_turn).attack = itembuf(47) bat.targ.mode = targSETUP bat.menu_mode = batMENUHERO clearkeys END IF END IF 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 anim_setframe t(i), frameSTAND NEXT i 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 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 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 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 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 tcount = 1 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 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(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 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 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 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 [= 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 [= 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 [= 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 [= 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 bat.wait_frames = 0 bat.anim_ready = NO 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 integer=-2, dont_retarget as bool = NO) DIM atk as AttackData loadattackdata atk, attack DIM blocking as bool = (atk.nonblocking = NO) IF override_blocking > -2 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 start_next_turn bat, bslot(), formdata 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 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.next_hero = 0 bat.next_enemy = 4 ready_all_valid_units bslot(), formdata 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 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