'OHRRPGCE GAME - Main battle-related routines '(C) Copyright 1997-2005 James Paige and Hamster Republic Productions 'Please read LICENSE.txt for GPL License details and disclaimer of liability 'See README.txt for code docs and apologies for crappyness of this code ;) '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" '--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 anim_flinchdone(byval who as integer, bslot() as BattleSprite, byref attack as AttackData) DECLARE SUB update_battle_slices(bslot() as BattleSprite) DECLARE SUB draw_damage_text(bslot() as BattleSprite, 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) DECLARE SUB battle_meters (byref bat as BattleState, bslot() as BattleSprite, formdata as Formation) DECLARE SUB battle_display (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 hero_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_hit(byval targ as integer, 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_hero_turns(byref bat as BattleState, bslot() as BattleSprite) DECLARE FUNCTION battle_check_a_hero_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 populate_battle_menu_menudef (byval hero_id as integer, menu as MenuDef, hero as HeroDef) DECLARE SUB update_battle_menu (bat as BattleState, bslot() as BattleSprite) DECLARE SUB init_spell_menu (bat as BattleState, bslot() as BattleSprite, st() as HeroDef) DECLARE FUNCTION enemy_is_weak(byval who as integer, bslot() as BattleSprite) as bool DECLARE FUNCTION check_for_unhittable_invisible_foe(byval index as integer, byref attack as AttackData, byref bat as BattleState, bslot() as BattleSprite, t() as integer) 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 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 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() DIM page as integer = compatpage() '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 IF running_as_slave THEN try_to_reload_files_inbattle setkeys 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 /' Currently unused... IF keyval(scF12) > 1 THEN loopvar bat.test_view_mode, 0, 2 SELECT CASE bat.test_view_mode CASE 0: bat.test_future = NO : debuginfo "bat.test_view_mode Classic" CASE 1: debuginfo "bat.test_view_mode Flicker" CASE 2: bat.test_future = YES : debuginfo "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() 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 enemy_ai bat, bslot(), formdata IF bat.hero_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 heromenu bat, bslot(), st() END IF IF bat.hero_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 -------------------- 'compatpage is a view onto vpage, but vpage might change, so recreate every tick freepage page page = compatpage() clearpage vpage '--display the backdrop and BattleSprites UpdateRootSliceSize bat.root_sl, page 'Match size of page, not vpage update_battle_slices bslot() DrawSlice bat.root_sl, page 'Not converted to slices yet draw_damage_text bslot(), page '--display menus and meters battle_display bat, bslot(), st(), page IF bat.vic.state = vicEXITDELAY THEN bat.vic.state = vicEXIT IF bat.vic.state > vicNONE THEN show_victory bat, bslot(), page IF bat.show_info_mode = 1 THEN show_enemy_meters bat, bslot(), formdata, page ELSEIF bat.show_info_mode = 2 THEN display_attack_queue bslot() END IF IF bat.death_mode = deathENEMIES AND bat.vic.state = vicNONE THEN IF count_dissolving_enemies(bslot()) = 0 THEN trigger_victory bat, bslot() END IF IF bat.vic.state = vicEXIT THEN 'Victory result = YES EXIT DO END IF IF bat.death_mode = deathHEROES THEN IF formdata.death_action = 0 THEN 'normal game over/trigger death script fatal = YES ELSEIF formdata.death_action = -1 THEN 'continue game END IF result = NO EXIT DO END IF ' Display CANNOT RUN message (attack captions are drawn in battle_display!) IF bat.alert_ticks > 0 THEN bat.alert_ticks -= 1 centerfuz rCenter, rBottom - 10, textwidth(bat.alert) + 16, 16, 3, page edgeprint bat.alert, pCentered, rBottom - 15, uilook(uiSelectedItem + bat.tog), page END IF IF dotimerbattle THEN result = NO EXIT DO END IF show_first_battle_timer(page) setvispage vpage check_for_queued_fade_in bat.ticks += 1 dowait LOOP battle_cleanup bat, bslot(), result freepage page 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).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 '--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 '--special handling when we are waiting for all motion to stop IF bat.wait_frames = -1 THEN FOR i = 0 TO 23 IF bslot(i).xmov <> 0 OR bslot(i).ymov <> 0 OR bslot(i).zmov <> 0 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 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).flee = 0 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 'setmove(who,xticks,yticks,xstep,ystep) ww = popdw bslot(ww).xmov = popdw bslot(ww).ymov = popdw bslot(ww).xspeed = popdw bslot(ww).yspeed = popdw 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,xticks,yticks) ww = popdw DIM destpos as XYPair destpos.x = popdw destpos.y = popdw DIM moveticks as XYPair moveticks.x = popdw moveticks.y = popdw bslot(ww).xspeed = (destpos.x - bslot(ww).x) / moveticks.x bslot(ww).yspeed = (destpos.y - bslot(ww).y) / moveticks.y bslot(ww).xmov = moveticks.x bslot(ww).ymov = moveticks.y 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 'zmove(who,zticks,zstep) ww = popdw bslot(ww).zmov = popdw bslot(ww).zspeed = popdw 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, xticks, yticks) ww = popdw 'who DIM movedist as XYPair movedist.x = popdw movedist.y = popdw DIM moveticks as XYPair moveticks.x = popdw moveticks.y = popdw with bslot(ww) if moveticks.x <> 0 then .xspeed = movedist.x / moveticks.x else .xspeed = 0 if moveticks.y <> 0 then .yspeed = movedist.y / moveticks.y else .yspeed = 0 .xmov = moveticks.x .ymov = moveticks.y 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 DIM moveticks as integer = popdw bslot(ww).zspeed = (destz - bslot(ww).z) / moveticks bslot(ww).zmov = moveticks CASE 25 'anim_checkpoint IF autotestmode THEN write_checkpoint 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 checkTagCond attack.tagset(0), atktagOnUse checkTagCond attack.tagset(1), 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 checkTagCond attack.tagset(0), atktagOnHit checkTagCond attack.tagset(1), atktagOnHit IF bslot(targ).stat.cur.hp = 0 THEN checkTagCond attack.tagset(0), atktagOnKill checkTagCond attack.tagset(1), atktagOnKill END IF IF trytheft(bat, bat.acting, targ, attack, bslot()) THEN IF bat.hero_turn >= 0 THEN checkitemusability bat.iuse(), bslot(), bat.hero_turn END IF END IF ELSE checkTagCond attack.tagset(0), atktagOnMiss checkTagCond attack.tagset(1), 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_hit targ, 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 evalitemtags END IF .consume_item = -1 END IF END WITH '--set the flag to prevent re-consuming costs 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_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 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 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_targetting(byref bat as BattleState, bslot() as BattleSprite) 'Just for heroes 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.hero_turn END IF autotarget bat.hero_turn, bslot(bat.hero_turn).attack - 1, bslot() hero_attack_targetting_done bat, bslot() EXIT SUB END IF 'check to see if the hero 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) IF NOT atkallowed(bat.targ.atk, bat.hero_turn, 0, 0, bslot()) THEN bslot(bat.hero_turn).attack = 0 bslot(bat.hero_turn).consume_lmp = -1 bat.targ.mode = targNONE clearkeys EXIT SUB END IF 'no valid targs available IF targetmaskcount(bat.targ.mask()) = 0 THEN EXIT SUB END IF '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 carray(ccMenu) > 1 THEN do_cancel = YES IF do_cancel THEN menusound gen(genCancelSFX) bslot(bat.hero_turn).attack = 0 bslot(bat.hero_turn).consume_lmp = -1 bat.targ.mode = targNONE clearkeys EXIT SUB END IF 'confirm IF carray(ccUse) > 1 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 '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.hero_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.hero_turn).attack - 1, bat.hero_turn, targs() hero_attack_targetting_done bat, bslot() END SUB 'This is called to cleanup after a hero attack has been targetted and put on atkq SUB hero_attack_targetting_done(byref bat as BattleState, bslot() as BattleSprite) IF bat.turn.mode = turnACTIVE THEN 'For debug only bslot(bat.hero_turn).active_turn_num += 1 'debug "Hero " & bat.hero_turn & " " & bslot(bat.hero_turn).name & " turn #" & bslot(bat.hero_turn).active_turn_num END IF bslot(bat.hero_turn).attack = 0 bslot(bat.hero_turn).ready_meter = 0 bslot(bat.hero_turn).ready = NO bat.hero_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 (byref bat as BattleState, bslot() as BattleSprite, st() as HeroDef, page as integer) 'display: '--this sub currently draws the user-interface. In the future it will update '--user interface slices DIM i as integer DIM col as integer DIM bgcol as integer IF bat.vic.state = vicNONE THEN 'only display interface till you win FOR i = 0 TO 3 '--for each hero IF gam.hero(i).id >= 0 THEN '--FIXME: should use some battle state instead of global state to '--determine if the hero is present. IF prefbit(6) = NO THEN '"Hide ready-meter" off col = uilook(uiTimeBar) IF bslot(i).ready = YES THEN col = uilook(uiTimeBarFull) edgeboxstyle 1, 4 + i * 10, 132, 11, 0, page, YES, YES IF bslot(i).stat.cur.hp > 0 THEN DIM j as integer = bslot(i).ready_meter / 7.7 IF blocked_by_attack(bat, i) OR bslot(i).attack > 0 OR (bat.atk.id >= 0 AND bat.acting = i) THEN col = uilook(uiTimeBar) j = 130 END IF rectangle 2, 5 + i * 10, j, 9, col, page END IF END IF DIM hp_text_y as integer = 5 + i * 10 WITH bslot(i) DIM hidehp as bool = should_hide_hero_stat(gam.hero(i).id, statHP) IF prefbit(7) = NO ANDALSO hidehp = NO THEN '"Hide health meter" off update_stat_meter bat, .lifemeter, 87, .stat.cur.hp, .stat.max.hp, uiHealthBar, col edgeboxstyle 136, 4 + i * 10, 89, 11, 0, page, YES, YES rectangle 137, 5 + i * 10, .lifemeter, 9, col, page END IF IF prefbit(49) ANDALSO should_hide_hero_stat(gam.hero(i).id, statMP) = NO THEN '"Show MP meter" update_stat_meter bat, .mpmeter, 87, .stat.cur.mp, .stat.max.mp, uiMPBar, col rectangle 137, 12 + i * 10, .mpmeter, 2, col, page 'Overlaps the HP meter 'Move the hp text up a pixel so that it doesn't completely cover the MP meter hp_text_y -= 1 END IF '--name-- IF i = bat.hero_turn ANDALSO bat.menu_mode = batMENUHERO THEN col = uilook(uiSelectedItem + bat.tog) ELSE col = uilook(uiMenuItem) END IF edgeprint .name, 128 - LEN(.name) * 8, 5 + i * 10, col, page '--hp-- IF hidehp = NO THEN edgeprint .stat.cur.hp & "/" & .stat.max.hp, 136, hp_text_y, col, page END IF END WITH WITH bslot(i).stat DIM indicatorpos as integer = 217 'poison indicator IF .cur.poison < .max.poison THEN edgeprint CHR(gen(genPoisonChar)), indicatorpos, hp_text_y, col, page indicatorpos -= 8 END IF 'stun indicator IF .cur.stun < .max.stun THEN edgeprint CHR(gen(genStunChar)), indicatorpos, hp_text_y, col, page indicatorpos -= 8 END IF 'mute indicator IF .cur.mute < .max.mute THEN edgeprint CHR(gen(genMuteChar)), indicatorpos, hp_text_y, col, page indicatorpos -= 8 END IF 'regen indicator IF .cur.regen < .max.regen THEN edgeprint CHR(gen(genRegenChar)), indicatorpos, hp_text_y, col, page END IF END WITH END IF NEXT i '--Display caption and count-down time IF bat.caption_time > 0 THEN bat.caption_time -= 1 IF bat.caption_delay > 0 THEN bat.caption_delay -= 1 ELSE centerbox rCenter, rBottom - 14, 310, 16, 1, page edgeprint bat.caption, pCentered, rBottom - 19, uilook(uiText), page END IF END IF '--Draw menus for a hero that is currently taking a turn IF bat.hero_turn >= 0 THEN WITH bslot(bat.hero_turn) update_battle_menu bat, bslot() .menust.active = (bat.menu_mode = batMENUHERO) draw_menu .batmenu, .menust, page IF bat.targ.mode = targNONE AND prefbit(14) = NO THEN '"Disable Hero's Battle Cursor" off 'Show cursor above hero edgeprint CHR(24), .x + (.w / 2) - 4, .y - 5 + (bat.tog * 2), uilook(uiSelectedItem + bat.tog), page END IF END WITH 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 IF bat.targ.mode > targNONE THEN FOR i = 0 TO 11 WITH bslot(i) DIM as integer cursx, cursy 'Subtract (-3,-6) to align at bottom-center edge of the cursor icon cursx = bound(.x + .cursorpos.x + .w \ 2 - 3, 10, gen(genResolutionX) - 10) cursy = bound(.y + .cursorpos.y - 6, 10, gen(genResolutionY) - 10) IF bat.targ.hover = i THEN edgeprint IIF(bat.targ.mask(i),CHR(24),"X"), cursx - 4, cursy, uilook(uiSelectedDisabled + bat.tog), page END IF IF bat.targ.selected(i) = YES ORELSE bat.targ.pointer = i THEN edgeprint CHR(24), cursx - 4, cursy, uilook(uiSelectedItem + bat.tog), page edgeprint .name, cursx + ancCenter + showLeft, cursy - 10, uilook(uiSelectedItem + bat.tog), page END IF END WITH NEXT i END IF END IF END IF'--end if bat.vic.state = vicNONE IF gam.debug_showtags THEN tagdisplay page 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 IF has_blocking_turn_delayed_attacks(i) THEN 'debug "Unit " & i & " " & bslot(i).name & " blocked turn #" & bslot(i).active_turn_num bslot(i).ready_meter = 0 update_turn_delays_in_attack_queue i ELSE bslot(i).ready = YES END IF 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.hero_turn = i THEN bat.hero_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 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) '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 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 bslot(i).frame = frameVICTORYB THEN bslot(i).frame = frameVICTORYA ELSE bslot(i).frame = frameVICTORYB END IF NEXT i '--Then apply movement forces for all things, heroes, enemies, attacks, weapons FOR i = 0 TO 23 WITH bslot(i) IF .xmov <> 0 THEN .x = .x + (.xspeed * SGN(.xmov)): .xmov = .xmov - SGN(.xmov) IF .ymov <> 0 THEN .y = .y + (.yspeed * SGN(.ymov)): .ymov = .ymov - SGN(.ymov) IF .zmov <> 0 THEN .z = .z + (.zspeed * SGN(.zmov)): .zmov = .zmov - SGN(.zmov) END WITH NEXT i '--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 FOR i = 0 TO 11 WITH bslot(i) IF .dissolve > 0 THEN 'ENEMIES DEATH THROES IF is_enemy(i) THEN IF .flee 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.hero_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 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 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 not set to Fill because it should fill the page, not the screen slice '(UpdateRootSliceSize called instead) .backdrop_sl = NewSliceOfType(slSprite, .root_sl) CenterSlice .backdrop_sl .battlefield_sl = NewSliceOfType(slContainer, .root_sl) .battlefield_sl->Fill = YES '.battlefield_sl->AutoSort = slAutoSortBottomY .battlefield_sl->AutoSort = slAutoSortCustom 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 SUB update_battle_slices(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 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 DissolveSpriteSlice .sprite, .appeartype, .appeartime, .appeartime - .dissolve_appear, , NO ELSEIF is_enemy(i) andalso .dissolve > 0 andalso .flee = 0 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 END WITH NEXT i END SUB SUB draw_damage_text(bslot() as BattleSprite, page as integer) FOR i as integer = 0 TO 11 WITH bslot(i).harm IF .ticks > 0 THEN DIM harm_text_offset as integer IF gen(genDamageDisplayTicks) <> 0 THEN harm_text_offset = gen(genDamageDisplayRise) / gen(genDamageDisplayTicks) * (gen(genDamageDisplayTicks) - .ticks) ELSE harm_text_offset = 0 'Avoid div by zero (which shouldn't be possible anyway) END IF edgeprint .text, .pos.x + ancCenter, .pos.y - harm_text_offset, .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 DIM attack as AttackData 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 + XY(240, 82) - XY(.w \ 2, .h) (.pos) = .basepos .vis = YES .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 = 87 * bound(.stat.cur.hp / large(.stat.max.hp, 1), 0., 1.) .mpmeter = 87 * 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 ClearMenuData menu WITH menu .offset.x = -8 .offset.y = 5 .alignhoriz = alignRight .alignvert = alignTop .anchorhoriz = alignRight .anchorvert = alignTop .textalign = alignLeft .bordersize = -5 .itemspacing = -2 .highlight_selection = YES .min_chars = 10 .maxrows = 23 END WITH 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) ELSEIF k."attack".exists THEN atk_id = k."attack".integer caption = readattackname(atk_id) mitem = append_menu_item(menu, caption, batmenu_ATTACK, atk_id) 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) 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) 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 ' Updates which menu items are enabled SUB update_battle_menu (bat as BattleState, bslot() as BattleSprite) WITH bslot(bat.hero_turn) DIM atk as AttackData FOR i as integer = 0 TO .batmenu.numitems - 1 WITH *.batmenu.items[i] IF .t = batmenu_ATTACK THEN .disabled = NO loadattackdata atk, .sub_t ' FIXME: isn't this bitset misnamed, since it affects all battle menu attacks? IF atk.check_costs_as_weapon THEN IF atkallowed(atk, bat.hero_turn, 0, 0, bslot()) = 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 ' Update selection, etc init_menu_state .menust, .batmenu 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 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 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 '--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 "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 FOR i = 0 TO UBOUND(gam.hero) IF gam.hero(i).id >= 0 THEN giveheroexperience i, xp_mult(i) * exper 'debug "hero " & i & " got " & CINT(xp_mult(i) * exper) updatestatslevelup i, NO END IF NEXT 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 carray(ccUse) > 1 ORELSE carray(ccMenu) > 1 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 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 .ticks = 0 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 .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 .hero_turn = -1 .enemy_turn = -1 .next_hero = 0 .next_enemy = 0 .menu_mode = batMENUHERO .laststun = 0 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()) 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 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 NO 'npc_visibility=NO 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 ' For ABS(xmove_ticks) ticks, move xstep pixels in the direction SGN(xmove_ticks), and ditto for ymove_ticks/ystep. SUB anim_setmove(who as integer, xmove_ticks as integer, ymove_ticks as integer, xstep as integer, ystep as integer) pushdw 2: pushdw who: pushdw xmove_ticks: pushdw ymove_ticks: pushdw xstep: pushdw ystep END SUB ' Move to an X position in ABS(xticks) ticks, ditto for Y. SUB anim_absmove(byval who as integer, byval tox as integer, byval toy as integer, byval xticks as integer, byval yticks as integer) pushdw 8: pushdw who: pushdw tox: pushdw toy: pushdw xticks: pushdw yticks END SUB ' Move to an Z position in ABS(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 ' For ABS(zticks) ticks, move zstep pixels in the direction SGN(zticks) SUB anim_zmove(byval who as integer, byval zticks as integer, byval zstep as integer) pushdw 15: pushdw who: pushdw zticks: pushdw zstep 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 ' Move relative from the current X position in ABS(xticks) ticks, ditto for Y. SUB anim_relmove(byval who as integer, byval byx as integer, byval byy as integer, byval xticks as integer, byval yticks as integer) pushdw 20: pushdw who: pushdw byx: pushdw byy: pushdw xticks: pushdw yticks 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_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 flinch_x_dist as integer flinch_x_dist = 3 IF is_enemy(who) THEN flinch_x_dist = -3 anim_setmove who, flinch_x_dist, 0, 2, 0 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 END IF END SUB SUB anim_flinchdone(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 flinch_x_dist as integer flinch_x_dist = -3 IF is_enemy(who) THEN flinch_x_dist = 3 anim_setmove who, flinch_x_dist, 0, 2, 0 anim_setframe who, frameSTAND END IF END SUB SUB anim_checkpoint() pushdw 25 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 > 1 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 = YES THEN bslot(who).flee = 1 '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.hero_turn = deadguy THEN bat.hero_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(), 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 .flee = 0 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 '============================================================================== ' Enemy AI FUNCTION enemy_is_weak(byval who as integer, bslot() as BattleSprite) as bool DIM weakhp as integer = gen(genEnemyWeakHP) IF bslot(who).stat.cur.hp < 0.01 * bslot(who).stat.max.hp * weakhp THEN RETURN YES RETURN NO END FUNCTION SUB enemy_ai (byref bat as BattleState, bslot() as BattleSprite, formdata as Formation) 'Which attack list to use DIM ai as EnemyAIEnum = aiNormal 'if HP is less than the threshold, go into desperation mode IF enemy_is_weak(bat.enemy_turn, bslot()) THEN ai = aiWeak 'Go into alone mode if no other enemies without "Ignored for "Alone" AI" bitset IF targenemycount(bslot(), YES) <= 1 THEN ai = aiAlone DIM slot as integer = 0 'spawn allies when alone WITH bslot(bat.enemy_turn) IF ai = aiAlone AND .enemy.spawn.when_alone > 0 THEN FOR j as integer = 1 TO .enemy.spawn.how_many slot = 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 'make sure that the current ai set is valid 'otherwise fall back on another IF count_attacks_in_ai_list(ai, bat.enemy_turn, bslot()) = 0 THEN ai = aiNormal IF enemy_is_weak(bat.enemy_turn, bslot()) THEN ai = aiWeak IF count_attacks_in_ai_list(ai, bat.enemy_turn, bslot()) = 0 THEN ai = aiNormal END IF END IF 'if no valid ai set is available, the enemy loses its turn IF count_attacks_in_ai_list(ai, bat.enemy_turn, bslot()) = 0 THEN bat.enemy_turn = -1 : EXIT SUB '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()) 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() 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.hero_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 tirn in turnACTIVE mode bat.next_hero = bat.hero_turn bat.hero_turn = -1 EXIT SUB END IF IF carray(ccMenu) > 1 THEN do_close = YES usemenusounds usemenu .menust IF carray(ccUse) > 1 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.hero_turn bat.hero_turn = -1 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 Menu to allow time to pass IF carray(ccMenu) 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.hero_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.hero_turn bat.item_desc = "" IF inventory(bat.item.pt).used THEN bat.item_desc = readitemdescription(inventory(bat.item.pt).id) END IF 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.hero_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.hero_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.hero_turn, list_type, i \ 3) .enable = atk.useable_inside_battle ANDALSO atkallowed(atk, bat.hero_turn, list_type, i \ 3, bslot()) END IF .name = rpad(.name, " ", 10) END WITH NEXT i ELSEIF list_type = 2 THEN '-- this is a random spell list '-- loop through the spell list storing attack ID numbers DIM spells as integer vector v_new spells FOR i = 0 TO UBOUND(bat.spell.slot) DIM atkid as integer = gam.hero(bat.hero_turn).spells(bat.listslot, i) - 1 IF atkid >= 0 THEN loadattackdata atk, atkid 'FIXME (bug sf#2039): why don't we even call atkallowed here?? IF atk.useable_inside_battle THEN v_append spells, atkid END IF NEXT i IF v_len(spells) > 0 THEN bslot(bat.hero_turn).attack = spells[randint(v_len(spells))] + 1 bat.targ.mode = targSETUP clearkeys END IF v_free spells END IF END SUB 'Handle player controls while a spell menu is open. 'The menu is setup in init_spell_menu and actually drawn in battle_display. 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 carray(ccMenu) > 1 THEN do_cancel = YES IF carray(ccUse) > 1 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.hero_turn).list_type(bat.listslot) DIM lmp as integer = bat.sptr \ 3 IF atk.useable_inside_battle ANDALSO _ atkallowed(atk, bat.hero_turn, list_type, lmp, bslot()) THEN '--attack is allowed menusound gen(genAcceptSFX) '--if lmp then set lmp consume flag IF list_type = 1 THEN bslot(bat.hero_turn).consume_lmp = lmp '--queue attack bslot(bat.hero_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 init_spell_menu and actually drawn in battle_display. 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 carray(ccMenu) > 1 THEN do_cancel = YES IF carray(ccUse) > 1 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.hero_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.hero_turn).consume_item = IIF(itembuf(73) = 1, bat.item.pt, -1) bslot(bat.hero_turn).attack = itembuf(47) bat.targ.mode = targSETUP bat.menu_mode = batMENUHERO clearkeys END IF END IF END SUB '============================================================================== ' Animation system 'Calculate the absolute position at which an attack should be drawn at on top of 'a certain target. This position might instead be used as a waypoint for projectiles. 'Called from within generate_atkscript. FUNCTION attack_placement_over_target(attack as AttackData, targslot as integer, bat as BattleState, bslot() as BattleSprite) as XYZTriple 'Load the size of the sprite DIM temp_sl as Slice Ptr temp_sl = NewSliceOfType(slSprite) ChangeSpriteSlice temp_sl, sprTypeAttack, attack.picture DIM as integer attackw = temp_sl->width, attackh = temp_sl->height DeleteSlice @temp_sl DIM as integer xt, yt, zt IF prefbit(36) THEN ' "Old attack positioning at bottom-left of target" ' Position attack animation aligned with bottom-left of target (?!) and down 2 pixels xt = 0 yt = (bslot(targslot).h - attackh) + 2 zt = 0 ELSE ' Visually align center of attack and target, while bottom-y position is forward several pixels of the ' bottom-y of the target to ensure the attack appears in front (with 4 pixel margin to protect against ' rounding error in anim_absmove, etc.) ' (The +4's cancel out because z increases towards top of screen) xt = (bslot(targslot).w - attackw) \ 2 yt = (bslot(targslot).h - attackh) + 4 zt = (bslot(targslot).h - attackh) \ 2 + 4 END IF ' The following case is a simple fix for the fact that bslot() contains the *initial* positions ' of everyone at the start of the attack, not the actual position at this point of the animation. IF targslot = bat.acting AND is_hero(bat.acting) THEN SELECT CASE attack.attacker_anim ' Heroes move forward 20 pixels for these attacker animations (see anim_advance) CASE atkrAnimStrike, atkrAnimCast, atkrAnimSpinStrike, atkrAnimJump xt -= 20 END SELECt END IF xt += bslot(targslot).x yt += bslot(targslot).y zt += bslot(targslot).z RETURN TYPE(xt, yt, zt) 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) as XYZTriple DIM ret as XYZTriple ret = attack_placement_over_target(attack, attacker, bat, bslot()) 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 't(index)' is unhittable and invisible FUNCTION check_for_unhittable_invisible_foe(byval index as integer, byref attack as AttackData, byref bat as BattleState, bslot() as BattleSprite, t() as integer) as bool IF bslot(t(index)).vis THEN RETURN NO IF attack_can_hit_dead(t(index), attack, bslot(bat.acting).stored_targs_can_be_dead) THEN RETURN NO 'Attack can hit dead targets IF bslot(bat.acting).self_bequesting THEN 'This is a self-bequest attack IF bat.acting = t(index) 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()) = 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 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(i, attack, bat, bslot(), t()) 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 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 'ABORT IF TARGETLESS IF tcount = 0 THEN bat.atk.id = -1 EXIT SUB END IF 'Kill old target history FOR i = 0 TO 11 bslot(bat.acting).last_targs(i) = NO NEXT i '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 wpic = gam.hero(bat.acting).wep_pic wpal = gam.hero(bat.acting).wep_pal 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) ELSE 'I don't know whether resetting this here is actually necessary, just duplicating old beheviour .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 will 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_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 FOR i = 0 TO tcount - 1 targetpos = attack_placement_over_target(attack, t(i), bat, bslot()) anim_setpos 12 + i, targetpos.x, targetpos.y, atkimgdirection anim_setz 12 + i, targetpos.z IF attack.attack_anim = atkAnimDrop THEN anim_setz 12 + i, targetpos.z + 180 END IF 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()) 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, 3 END IF IF attack.attack_anim = atkAnimDrop THEN anim_zmove 12 + i, -10, 20 END IF IF attack.attack_anim = atkAnimScatter THEN ' Move to a random point (FIXME for non-320*200 resolutions) anim_absmove 12 + i, randint(270), randint(150), 6, 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_setframe bat.acting, frameSTAND anim_disappear 24 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()) anim_absmove 12 + i, targetpos.x, targetpos.y + bslot(t(i)).w, 3, 3 NEXT i anim_waitforall FOR i = 0 TO tcount - 1 targetpos = attack_placement_over_target(attack, t(i), bat, bslot()) anim_absmove 12 + i, targetpos.x + bslot(t(i)).w, targetpos.y, 3, 3 NEXT i anim_waitforall FOR i = 0 TO tcount - 1 targetpos = attack_placement_over_target(attack, t(i), bat, bslot()) anim_absmove 12 + i, targetpos.x, targetpos.y - bslot(t(i)).w, 3, 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 IF attack.attack_anim <> atkAnimRing THEN anim_wait 3 END IF FOR i = 0 TO tcount - 1 anim_disappear 12 + i anim_flinchdone t(i), bslot(), attack 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()) anim_setpos 12, projectile_start.x, projectile_start.y, atkimgdirection anim_setz 12 + i, 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()) 'make the projectile move to the target anim_absmove 12, targetpos.x, targetpos.y, 5, 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 anim_flinchdone t(i), bslot(), attack IF i = 0 THEN 'attacker's weapon picture vanishes after the first hit anim_disappear 24 END IF NEXT i 'after all hits are done, projectile flies off the side of the screen '(FIXME for non-320*200 resolutions) IF is_hero(bat.acting) THEN anim_absmove 12, -50, 100, 5, 5 END IF IF is_enemy(bat.acting) THEN anim_absmove 12, 320, 100, 5, 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 projectile_start as XYZTriple projectile_start = projectile_start_position(attack, bat.acting, bat, bslot()) anim_advance bat.acting, attack, bslot(), t() FOR j as integer = 1 TO numhits FOR i = 0 TO tcount - 1 targetpos = attack_placement_over_target(attack, t(i), bat, bslot()) 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 'FIXME for non-320*200 resolutions IF is_hero(bat.acting) THEN anim_setpos 12 + i, 320, 100, atkimgdirection END IF IF is_enemy(bat.acting) THEN anim_setpos 12 + i, -50, 100, atkimgdirection END IF anim_setz 12 + i, 180 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()) IF attack.attack_anim = atkAnimProjectile OR attack.attack_anim = atkAnimMeteor THEN anim_absmove 12 + i, targetpos.x, targetpos.y, 6, 6 anim_abszmove 12 + i, targetpos.z, 6 END IF IF attack.attack_anim = atkAnimReverseProjectile THEN anim_absmove 12 + i, projectile_start.x, projectile_start.y, 6, 6 anim_abszmove 12 + i, projectile_start.z, 6 END IF IF attack.attack_anim = atkAnimMeteor THEN anim_zmove 12 + i, -6, 30 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 anim_flinchdone t(i), bslot(), attack 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 anim_advance bat.acting, attack, bslot(), t() FOR j as integer = 1 TO numhits ' Create attack sprites FOR i = 0 TO tcount - 1 targetpos = attack_placement_over_target(attack, t(i), bat, bslot()) IF is_hero(bat.acting) THEN anim_setpos 12 + i, 320, targetpos.y, atkimgdirection END IF IF is_enemy(bat.acting) THEN anim_setpos 12 + i, -50, 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()) anim_absmove 12 + i, targetpos.x, targetpos.y, 8, 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()) IF is_hero(bat.acting) THEN anim_absmove 12 + i, -50, targetpos.y, 5, 7 END IF IF is_enemy(bat.acting) THEN anim_absmove 12 + i, 320, targetpos.y, 5, 7 END IF ' Z values are constant NEXT i anim_waitforall ' Destroy attacks FOR i = 0 TO tcount - 1 anim_disappear 12 + i anim_flinchdone t(i), bslot(), attack 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 '----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()) ' 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, 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 FOR i = 0 TO tcount - 1 ' Note tcount = 1 anim_flinchdone t(i), bslot(), attack 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 ' Note tcount = 1 anim_setframe t(i), frameSTAND NEXT i anim_end END IF '----WAVE IF attack.attack_anim = atkAnimWave THEN DIM wave_start_x as integer wave_start_x = -50 IF is_hero(bat.acting) THEN wave_start_x = 320 DIM pushback_x as integer pushback_x = 24 IF is_hero(bat.acting) THEN pushback_x = -24 ' Only use targetpos.y, and then only if there's just one target. targetpos = attack_placement_over_target(attack, t(0), bat, bslot()) 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 anim_setpos 12 + i, wave_start_x, i * 15, atkimgdirection anim_setz 12 + i, 0 'Can tweak this to change the effect ELSE 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) FOR i = 0 TO 11 anim_appear 12 + i anim_setmove 12 + i, pushback_x, 0, 16, 0 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 FOR i = 0 TO tcount - 1 anim_flinchdone t(i), bslot(), attack 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 1 ' Destroy attacks anim_disappear 12 FOR i = 0 TO tcount - 1 anim_flinchdone t(i), bslot(), attack 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 '--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 only) 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.hero_turn).attack - 1 get_valid_targs bat.targ.mask(), bat.hero_turn, bat.targ.atk, bslot() bat.targ.hit_dead = attack_can_hit_dead(bat.hero_turn, bat.targ.atk, bslot(bat.hero_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 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.hero_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(), , 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 ' This is drawn at the top of the screen, rather than the top of the compatpage SUB display_attack_queue (bslot() as BattleSprite) 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.hero_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" DIM isenemytargs as bool = (targenemycount(bslot()) > 0) IF isenemytargs THEN IF pause_on_all THEN IF bat.menu_mode >= 0 ORELSE bat.targ.mode > targNONE THEN 'A menu is open or an attack is being targetted RETURN NO END IF 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 .xmov = 10 .xspeed = 6 .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(.attacker, .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(), .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 'Get the next ready hero, if any, to start their turn SUB battle_check_for_hero_turns(byref bat as BattleState, bslot() as BattleSprite) loopvar bat.next_hero, 0, 3 IF bat.hero_turn > -1 THEN '--somebody is already taking their 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_hero_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_hero_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_hero_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.hero_turn = index bslot(bat.hero_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 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. IF bat.hero_turn = targ THEN bat.targ.mode = targNONE bat.hero_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.acting, 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(.attacker, .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(), .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 > 11 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 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(), YES, blocking 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(), YES, blocking 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 'counterattacks are forced non-blocking autotarget who, bslot(who).stat_counter_attack(i) - 1, bslot(), YES, blocking 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 '--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) ' FIXME: Why?! This doesn't look correct - won't it cause us to the next ' round even if there are attacks left? IF .blocking THEN RETURN YES 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_hero_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. IF bat.hero_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) WITH bslot(bat.next_hero) IF .stat.cur.hp > 0 ANDALSO .no_attack_this_turn = NO 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_hero_turn(bat, bslot(), bat.next_hero) THEN 'debug "Hero " & bat.hero_turn & " " & bslot(bat.hero_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 DIM isenemytargs as integer = (targenemycount(bslot()) > 0) IF isenemytargs THEN IF bat.menu_mode > 0 THEN EXIT SUB '--no time on spell/item menus IF bat.menu_mode >= 0 AND bat.hero_turn >= 0 THEN EXIT SUB '--no time if hero menu is open END IF '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 'Random addition to break ties speeds(i) = 100 * bslot(i).stat.cur.spd + randint(100) '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. '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 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) '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 dbg.def( , scF4, "Tag debugger (F4)") THEN gam.debug_showtags = (gam.debug_showtags + 1) MOD 3 IF dbg.def(scCtrl, scF4, "View/edit slice tree (Ctrl-F4)") THEN slice_editor bat.root_sl IF dbg.def( , scF5, "Give million experience (F5)") THEN bat.rew.exper = 1000000 IF dbg.def(scCtrl, scF5, "Run instantly (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( , 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( , scF8) THEN battle_debug_menu bat, bslot(), formdata dbg.def( , , "Debug menu (F8)") 'Does nothing, but document F8. IF dbg.def( , scF10, "Show enemy meters (F10)") THEN bat.show_info_mode = IIF(bat.show_info_mode = 1, 0, 1) IF dbg.def( , scF11, "Show attack queue (F11)") THEN bat.show_info_mode = IIF(bat.show_info_mode = 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 .flee = 0 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 flee = " & .flee & _ !"\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 slot >= 0 ANDALSO slot <= 3 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