'OHRRPGCE GAME - Additional mostly battle-related routines '(C) Copyright 1997-2020 James Paige, Ralph Versteegen, and the OHRRPGCE Developers 'Dual licensed under the GNU GPL v2+ and MIT Licenses. Read LICENSE.txt for terms and disclaimer of liability. #include "config.bi" #include "bmod.bi" #include "bmodsubs.bi" #include "game.bi" #include "allmodex.bi" #include "common.bi" #include "loading.bi" #include "gglobals.bi" #include "const.bi" #include "uiconst.bi" #include "udts.bi" #include "battle_udts.bi" #include "moresubs.bi" #include "menustuf.bi" #include "scriptcommands.bi" #include "yetmore2.bi" #include "bcommon.bi" DECLARE SUB confirm_auto_spread (byval who as integer, tmask() as bool, bslot() as BattleSprite, t() as integer) DECLARE SUB confirm_auto_focus (byval who as integer, tmask() as bool, byref atk as AttackData, bslot() as BattleSprite, t() as integer) DECLARE SUB confirm_auto_first (byval who as integer, tmask() as bool, bslot() as BattleSprite, t() as integer) DECLARE FUNCTION quick_battle_distance(byval who1 as integer, byval who2 as integer, bslot() as BattleSprite) as integer DECLARE FUNCTION battle_distance(byval who1 as integer, byval who2 as integer, bslot() as BattleSprite) as integer DECLARE SUB transfer_enemy_bits(byref bspr as BattleSprite) DECLARE SUB transfer_enemy_counterattacks (byref bspr as BattleSprite) DECLARE SUB setup_non_volatile_enemy_state(byref bspr as BattleSprite) DECLARE SUB reset_enemy_state(byref bspr as BattleSprite, byref bat as BattleState, transmogging as bool=NO) DECLARE SUB change_foe_stat(bspr as BattleSprite, byval stat_num as integer, byval new_max as integer, byval stat_rule as TransmogStatsRule) DIM SHARED XY00 as XYpair PROPERTY BattleSprite.pos() byref as XYPair IF sl THEN RETURN sl->Pos ELSE RETURN XY00 END PROPERTY PROPERTY BattleSprite.x() as integer IF sl THEN RETURN sl->X END PROPERTY PROPERTY BattleSprite.x(val as integer) IF sl THEN sl->X = val END PROPERTY PROPERTY BattleSprite.y() as integer IF sl THEN RETURN sl->Y END PROPERTY PROPERTY BattleSprite.y(val as integer) IF sl THEN sl->Y = val END PROPERTY PROPERTY BattleSprite.z() as integer IF sprite THEN RETURN -sprite->Y END PROPERTY PROPERTY BattleSprite.z(val as integer) IF sprite THEN sprite->Y = -val END PROPERTY PROPERTY BattleSprite.size() byref as XYPair IF sl THEN RETURN sl->Size ELSE RETURN XY00 END PROPERTY PROPERTY BattleSprite.w() as integer IF sl THEN RETURN sl->Width END PROPERTY PROPERTY BattleSprite.w(val as integer) IF sl THEN sl->Width = val END PROPERTY PROPERTY BattleSprite.h() as integer IF sl THEN RETURN sl->Height END PROPERTY PROPERTY BattleSprite.h(val as integer) IF sl THEN sl->Height = val END PROPERTY PROPERTY BattleSprite.frame() as integer IF sprite ANDALSO sprite->SliceType = slSprite THEN RETURN sprite->SpriteData->frame END IF END PROPERTY PROPERTY BattleSprite.frame(fr as integer) IF sprite THEN ChangeSpriteSlice sprite, , , , fr END PROPERTY ' For xticks ticks, move xspeed pixels/tick SUB BattleSprite.set_vel_x(xspeed as integer, xticks as integer) IF sl THEN sl->Velocity.X = xspeed sl->VelTicks.X = xticks END IF END SUB SUB BattleSprite.set_vel_y(yspeed as integer, yticks as integer) IF sl THEN sl->Velocity.Y = yspeed sl->VelTicks.Y = yticks END IF END SUB SUB BattleSprite.set_vel_z(zspeed as integer, zticks as integer) IF sprite THEN sprite->Velocity.Y = -zspeed sprite->VelTicks.Y = zticks END IF END SUB FUNCTION BattleSprite.deathtime() as integer IF _deathtime <= 0 THEN RETURN default_dissolve_time(deathtype, w, h) RETURN _deathtime END FUNCTION FUNCTION BattleSprite.appeartime() as integer IF _appeartime <= 0 THEN RETURN default_dissolve_time(appeartype, w, h) RETURN _appeartime END FUNCTION 'FIXME: Use this everywhere in battles! FUNCTION is_hero(byval who as integer) as bool IF who >= 0 AND who <= 3 THEN RETURN YES RETURN NO END FUNCTION 'FIXME: Use this everywhere in battles! FUNCTION is_enemy(byval who as integer) as bool IF who >= 4 AND who <= 11 THEN RETURN YES RETURN NO END FUNCTION 'FIXME: This will eventually be phased out (but until then, use it!) FUNCTION is_attack(byval who as integer) as bool IF who >= 12 AND who <= 23 THEN RETURN YES RETURN NO END FUNCTION 'FIXME: This will eventually be phased out (but until then, use it!) FUNCTION is_weapon(byval who as integer) as bool IF who = 24 THEN RETURN YES RETURN NO END FUNCTION '============================================================================== ' Attack costs ' 'See also attack_cost_info in bcommon.bas 'In-battle function '--attackerslot = bslot() index of hero or enemy who is attacking '--spclass = 0 for normal attacks, 1 for level-MP spells '--lmplev = which level-MP level to use FUNCTION atkallowed (attack as AttackData, attackerslot as integer, spclass as integer, lmplev as integer, attacker as BattleSprite) as bool '--check for mutedness IF attack.mutable AND attacker.stat.cur.mute < attacker.stat.max.mute THEN RETURN NO END IF '--Check for sufficient MP (You can cast with negative MP as long as the attack costs no MP) IF large(attacker.stat.cur.mp, 0) - focuscost(attack.mp_cost, attacker.stat.cur.focus) < 0 THEN RETURN NO END IF 'NOTE: hp_cost is not checked! 'NOTE: money_cost is not checked! '--check for level-MP (heroes only) IF is_hero(attackerslot) AND spclass = 1 THEN IF gam.hero(attackerslot).levelmp(lmplev) <= 0 THEN RETURN NO END IF END IF '--check for sufficient items DIM itemid as integer DIM itemcount as integer FOR i as integer = 0 TO UBOUND(attack.item) itemid = attack.item(i).id itemcount = attack.item(i).number IF itemid > 0 THEN 'this slot is used ' Only hero items are checked ' However if an enemy uses this attack, it will add/subtract items from the player! IF is_hero(attackerslot) THEN IF countitem(itemid - 1) < itemcount THEN 'yes, this still works for adding items. RETURN NO END IF END IF END IF NEXT i '--succeed RETURN YES END FUNCTION 'Out-of-battle function 'Note: this function is used only for the OOB Spells menu, not for "map cure", etc. 'See also chkOOBtarg, which is used for "map cure". 'attackerslot = party slot of hero who wants to cast the spell 'spclass = 0 for normal attacks, 1 for level-MP spells FUNCTION atkallowed(attack as AttackData, attackerslot as integer, spclass as integer, lmplev as integer) as bool DIM byref attacker as HeroState = gam.hero(attackerslot) DIM cost as integer = focuscost(attack.mp_cost, attacker.stat.cur.focus) IF spclass = 0 AND attacker.stat.cur.mp < cost THEN RETURN NO END IF IF spclass = 1 AND attacker.levelmp(lmplev) = 0 THEN RETURN NO END IF IF attacker.stat.cur.hp <= 0 THEN RETURN NO END IF RETURN YES END FUNCTION 'In-battle routine SUB subtract_attack_costs(attack as AttackData, attackerslot as integer, byref bat as BattleState, bslot() as BattleSprite) DIM byref attacker as BattleSprite = bslot(attackerslot) '--if the attack costs MP, we want to actually consume MP IF attack.mp_cost > 0 THEN WITH attacker .stat.cur.mp = large(.stat.cur.mp - focuscost(attack.mp_cost, .stat.cur.focus), 0) END WITH END IF '--Level-MP WITH attacker IF attackerslot <= 3 AND .consume_lmp >= 0 THEN gam.hero(attackerslot).levelmp(.consume_lmp) -= 1 .consume_lmp = -1 END IF END WITH '--ditto for HP IF attack.hp_cost > 0 THEN WITH attacker .stat.cur.hp = large(.stat.cur.hp - attack.hp_cost, 0) .harm.ticks = gen(genDamageDisplayTicks) .harm.pos.x = .x + (.w * .5) .harm.pos.y = .y + (.h * .5) .harm.text = STR(attack.hp_cost) END WITH 'This triggerfade is needed because the hp cost might have killed the attacker triggerfade attackerslot, bslot() END IF '--ditto for money IF attack.money_cost <> 0 THEN gold = large(gold - attack.money_cost, 0) WITH attacker .harm.ticks = gen(genDamageDisplayTicks) .harm.pos.x = .x + (.w * .5) .harm.pos.y = .y + (.h * .5) .harm.text = price_string(ABS(attack.money_cost)) IF attack.money_cost < 0 THEN .harm.text = "+" & .harm.text END WITH IF gold > 2000000000 THEN gold = 2000000000 IF gold < 0 THEN gold = 0 END IF '--if the attack consumes items, we want to consume those too FOR i as integer = 0 TO UBOUND(attack.item) WITH attack.item(i) IF .id > 0 THEN 'this slot is used IF .number > 0 THEN 'remove items delitem(.id - 1, .number) ELSEIF .number < 0 THEN 'add items getitem(.id - 1, abs(.number)) END IF 'Update tags when items have changed because it could affect chain conditionals evalitemtags tag_updates END IF END WITH NEXT i END SUB 'Out-of-battle routine SUB subtract_attack_costs(attack as AttackData, attackerslot as integer, spclass as integer, lmplev as integer) DIM byref attacker as HeroState = gam.hero(attackerslot) '--deduct/add MP DIM cost as integer WITH attacker.stat cost = focuscost(attack.mp_cost, .cur.focus) 'Don't increase MP past max, but if it is already past max don't clamp it. .cur.mp = bound(.cur.mp - cost, 0, large(.max.mp, .cur.mp)) END WITH IF spclass = 1 THEN '--deduct LMP attacker.levelmp(lmplev) -= 1 END IF END SUB '============================================================================== 'Returns 0 for fail, id+1 on success. (item and rareitem are non-offset ids) FUNCTION checktheftchance (byval item as integer, byval itemP as integer, byval rareitem as integer, byval rareitemP as integer) as integer IF randint(100) < itemP THEN '--success! getitem item checktheftchance = item + 1 ELSE IF randint(100) < rareitemP THEN '--rare success! getitem rareitem checktheftchance = rareitem + 1 ELSE checktheftchance = 0 END IF END IF 'Update tags, this may matter for attack chaining evalitemtags tag_updates END FUNCTION 'Count number of attacks in one of an enemy's attack lists which it can currently use. 'Note, attacks are excluded even if the enemy is an MP Idiot but hasn't got enough MP. 'ai: which attack list FUNCTION count_available_attacks_in_ai_list (byval ai as EnemyAIEnum, byval slot as integer, bslot() as BattleSprite) as integer DIM atk as AttackData DIM atk_id as integer DIM count as integer = 0 WITH bslot(slot).enemy FOR i as integer = 0 TO 4 atk_id = -1 SELECT CASE ai CASE aiNormal: atk_id = .regular_ai(i) - 1 CASE aiWeak: atk_id = .desperation_ai(i) - 1 CASE aiAlone: atk_id = .alone_ai(i) - 1 END SELECT IF atk_id >= 0 THEN loadattackdata atk, atk_id IF atkallowed(atk, slot, 0, 0, bslot(slot)) THEN 'this attack is allowed right now count += 1 END IF END IF NEXT i END WITH RETURN count END FUNCTION 'Do an attack (in-battle only). 'Handled here: '- hit or miss or fail '- stat reset bits '- damage calculation and inflict '- stat bounds (0 to (optionally) max), but not stat caps '- harm text '- records stored, last, revenge & thankvenge targets & damage 'Not handled here: '- 'instead' chains '- costs '- counterattacks and spawning '- other non-damaging effects like tags, force-run, erase rewards '- death checks '- stat caps (bug 980) FUNCTION inflict (byref h as integer = 0, byref targstat as integer = 0, attackerslot as integer, targetslot as integer, byref attacker as BattleSprite, byref target as BattleSprite, attack as AttackData, tcount as integer) as AttackResult attacker.attack_succeeded = NO 'remember this target attacker.last_targs(targetslot) = YES 'stored targs IF attack.add_store_targ ORELSE attack.replace_store_targ THEN attacker.stored_targs(targetslot) = YES attacker.stored_targs_can_be_dead = attack_can_hit_dead(attack) END IF 'Note that deleting stored targs overrides both adding and replacing IF attack.delete_stored_targs THEN FOR i as integer = 0 TO UBOUND(attacker.stored_targs) attacker.stored_targs(i) = NO NEXT i attacker.stored_targs_can_be_dead = NO END IF WITH target .harm.text = "" .harm.ticks = gen(genDamageDisplayTicks) .harm.col = uilook(uiBattleDamage) .harm.pos.x = .x + (.w * .5) .harm.pos.y = .y + (.h * .5) END WITH 'If not 'no damage' (which skips hit/miss/fail checking) IF attack.damage_math <> 4 THEN DIM target_is_register as bool = NO 'init WITH target targstat = bound(attack.targ_stat, 0, UBOUND(.stat.cur.sta)) 'backcompat MP-targstat IF attack.obsolete_damage_mp THEN IF targstat = statHP THEN targstat = statMP END IF IF targstat > statLast AND targstat <= statLastRegister THEN target_is_register = YES END WITH 'accuracy DIM acc as integer = attacker.stat.cur.acc DIM dog as integer = target.stat.cur.dog DIM accmult as single = 1 DIM dogmult as single = .25 'dodge modifier IF attack.aim_math = 1 THEN dogmult = 0.5 IF attack.aim_math = 2 THEN dogmult = 1.0 IF attack.aim_math = 4 THEN dogmult = 1.25 IF attack.aim_math = 4 OR attack.aim_math = 7 OR attack.aim_math = 8 THEN acc = attacker.stat.cur.mag dog = target.stat.cur.wil ELSEIF attack.aim_math > 8 THEN accmult = attack.acc_mult dogmult = attack.dog_mult SELECT CASE attack.base_acc_stat CASE -1 acc = 1 CASE 0 TO statLast acc = attacker.stat.cur.sta(attack.base_acc_stat) CASE 256 TO 256 + statLast acc = target.stat.cur.sta(attack.base_acc_stat - 256) CASE 512 TO 512 + statLast acc = attacker.stat.cur.sta(attack.base_acc_stat - 512) accmult = accmult / tcount CASE ELSE debug "Invalid Base ACC stat" END SELECT SELECT CASE attack.base_dog_stat CASE -1 dog = 1 CASE 0 TO statLast dog = attacker.stat.cur.sta(attack.base_acc_stat) CASE 256 TO 256 + statLast dog = target.stat.cur.sta(attack.base_acc_stat - 256) CASE ELSE debug "Invalid Base DOG stat" END SELECT END IF DIM attackhit as integer attackhit = range(acc, 75) >= range(dog * dogmult, 75) IF attack.aim_math = 3 THEN attackhit = YES IF attack.aim_math = 5 OR attack.aim_math = 7 THEN attackhit = randint(100) < (acc * (100 - dog)) / 100 IF attack.aim_math = 6 OR attack.aim_math = 8 THEN attackhit = randint(100) < acc IF attack.aim_math = 9 THEN attackhit = range(acc * accmult + attack.aim_extra, 75) >= range(dog * dogmult, 75) IF attack.aim_math = 10 THEN attackhit = randint(100) < attack.aim_extra + (acc * accmult * (100 - dog * dogmult)) / 100 IF attack.aim_math = 11 THEN attackhit = rando() < (1 + erf(attack.aim_extra + acc * accmult - dog * dogmult)) / 2 IF attack.aim_math = 12 THEN attackhit = randint(100) < ((attack.aim_extra + acc * accmult) * (100 - dog * dogmult)) / 100 IF attackhit = NO THEN RETURN atkMiss END IF WITH target IF attack.fail_if_targ_poison = YES AND .stat.cur.poison < .stat.max.poison THEN RETURN atkFail END IF IF attack.fail_if_targ_regen = YES AND .stat.cur.regen < .stat.max.regen THEN RETURN atkFail END IF IF attack.fail_if_targ_stun = YES AND .stat.cur.stun <> .stat.max.stun THEN RETURN atkFail END IF IF attack.fail_if_targ_mute = YES AND .stat.cur.mute <> .stat.max.mute THEN RETURN atkFail END IF END WITH 'attack power and defense power DIM ap as integer = attacker.stat.cur.str DIM dp as integer = target.stat.cur.def SELECT CASE attack.base_atk_stat CASE 1 ap = attacker.stat.cur.mag dp = target.stat.cur.wil CASE 2 ap = attacker.stat.cur.hp CASE 3 ap = attacker.stat.max.hp - attacker.stat.cur.hp CASE 4 ap = randint(1000) '0 to 999 CASE 5 ap = 100 CASE 6 TO 17 ap = attacker.stat.cur.sta(attack.base_atk_stat - 6) CASE 18 ap = attacker.repeatharm CASE 19 ap = attacker.revengeharm CASE 20 ap = target.revengeharm CASE 21 ap = attacker.thankvengecure CASE 22 ap = target.thankvengecure CASE 23 TO 34 ap = target.stat.cur.sta(attack.base_atk_stat - 23) CASE 35 TO 46 ap = attacker.stat.max.sta(attack.base_atk_stat - 35) CASE 47 TO 58 ap = target.stat.max.sta(attack.base_atk_stat - 47) CASE 59 ap = tcount * 100 CASE 60 ap = attacker.stat.max.mp - attacker.stat.cur.mp CASE 61 ap = target.stat.max.mp - target.stat.cur.mp CASE 62 ap = target.stat.max.hp - target.stat.cur.hp CASE IS >= 63 debug "Unknown base stat " & attack.base_atk_stat & " for attack " & attack.id END SELECT '--defense base IF attack.base_def_stat > 0 AND attack.base_def_stat <= UBOUND(target.stat.cur.sta) + 1 THEN dp = target.stat.cur.sta(attack.base_def_stat - 1) 'Attack and Defense multipliers DIM as double atkmult, defmult IF attack.damage_math = 0 THEN atkmult = 1.0 : defmult = 0.5 'atk*.8-def*.1 IF attack.damage_math = 1 THEN atkmult = 0.8 : defmult = 0.1 'atk*.8-def*.1 IF attack.damage_math = 2 THEN atkmult = 1.3 : defmult = 1.0 'atk-1.3-def IF attack.damage_math = 3 THEN atkmult = 1.0 : defmult = 0.0 'atk ' damage_math = 4 is No Damage ' damage_math = 5 is set/damage % of max (special handling) ' damage_math = 6 is set/damage % of current (special handling) IF attack.damage_math = 7 THEN atkmult = attack.atk_mult : defmult = attack.def_mult 'Temporarily use floating point for damage DIM harmf as double = ap * atkmult - dp * defmult DIM elemental_absorb as bool = NO DIM immune as bool = NO 'elemental damage, and also failure conditions FOR i as integer = 0 TO gen(genNumElements) - 1 IF attack.elemental_damage(i) = YES THEN harmf *= ABS(target.elementaldmg(i)) 'What's a good cut off for immunity? When we switch to 32bit HP values, maybe 'you'll want to be able to do 1/1000,000 normal damage without triggering immunity? IF ABS(target.elementaldmg(i)) < 0.000005 THEN immune = YES ELSEIF target.elementaldmg(i) < 0.0 THEN elemental_absorb = YES 'absorb END IF END IF WITH attack.elemental_fail_conds(i) DIM fail as bool = NO DIM effectiveval as single = target.elementaldmg(i) DIM t as CompType = .comp AND 15 IF .comp AND 16 THEN effectiveval = ABS(effectiveval) IF .comp AND 64 THEN effectiveval = rando() * effectiveval IF t = compLt OR t = compLe THEN '"Simulate old fail vs element resist bit": 'The old bit checked only the target's Strong bits, ignoring their Absorb bits '(For consistency, this bit also affects compLe, although it didn't exist at the time. 'But we can't 100% safely apply it to compGt, because we didn't used to do that in the past.) IF prefbit(25) THEN effectiveval = ABS(effectiveval) END IF IF t = compLt THEN fail = (effectiveval < .value - 0.000005) IF t = compLe THEN fail = (effectiveval <= .value + 0.000005) IF t = compGt THEN fail = (effectiveval > .value + 0.000005) IF t = compGe THEN fail = (effectiveval >= .value - 0.000005) IF fail THEN IF .comp AND 32 THEN RETURN atkMiss ELSE RETURN atkFail END IF END IF END WITH NEXT 'extra damage harmf *= (1.0 + attack.extra_damage / 100) 'Now we convert damage back to integer (for backcompat) h = bound(harmf, 1. * LONG_MIN, 1. * LONG_MAX) ' + or - some amount. (Note: due to how range is programmed, the average ' result is 0.5 less than h) h = range(h, attack.randomization) 'spread damage IF attack.divide_spread_damage = YES THEN h = h / tcount 'cap under IF immune ANDALSO prefbit(26) THEN '"0 damage when immune to attack elements", even without attack.damage_can_be_zero h = 0 ELSEIF h <= 0 THEN IF attack.damage_can_be_zero = NO THEN h = 1 ELSE h = 0 END IF DIM byref tstat_cur as integer = target.stat.cur.sta(targstat) DIM tstat_max as integer = target.stat.max.sta(targstat) DIM byref astat_cur as integer = attacker.stat.cur.sta(targstat) DIM astat_max as integer = attacker.stat.max.sta(targstat) IF attack.show_damage_without_inflicting = NO THEN 'resetting IF attack.reset_targ_stat_before_hit = YES THEN tstat_cur = tstat_max END IF END IF 'remember target stat DIM tstat_original as integer = tstat_cur DIM astat_original as integer = astat_cur IF attack.percent_damage_not_set = YES THEN 'percentage attacks do damage 'Note that even with this bitset, these damage types ignore lots of stuff like randomization. 'It was once suggested (see bug 134) to move this block up the function, but 'we decided to just leave it as it is; you should use a different damage type 'with the target's stat as Base ATK Stat instead. SELECT CASE attack.damage_math CASE 5'% of max h = tstat_max + (attack.extra_damage * tstat_max / 100) elemental_absorb = NO CASE 6'% of cur h = tstat_cur + (attack.extra_damage * tstat_cur / 100) elemental_absorb = NO END SELECT END IF 'h should always be nonnegative at this point IF h < 0 THEN debug "inflict: negative h!" IF elemental_absorb THEN h *= -1 'elemental absorb ELSEIF attack.cure_instead_of_harm = YES AND target.harmed_by_cure = NO THEN h *= -1 'cure bit END IF DIM capdamage as bool = (attack.ignore_damage_cap = NO) IF attack.percent_damage_not_set = NO THEN 'percentage attacks set stat 'and by set, we really mean set, ignore nearly all attack settings, 'that's my interpretation of intent anyway - TMC '...And mine to. - James SELECT CASE attack.damage_math CASE 5'% of max h = tstat_cur - (tstat_max + (attack.extra_damage * tstat_max / 100)) capdamage = NO CASE 6'% of cur h = tstat_cur - (tstat_cur + (attack.extra_damage * tstat_cur / 100)) capdamage = NO END SELECT END IF 'Maybe cap damage to max possible damage/cure 'Note that unlike the damage cap, this still happens if not inflicting IF attack.do_not_exceed_targ_stat AND capdamage THEN IF h > 0 THEN 'damage h = small(h, tstat_cur) ELSEIF h < 0 THEN ' cure 'Note: this cure can no exceed max regardless of "allow cure to exceed maximum" bit DIM diff as integer = tstat_max - tstat_cur IF diff >= 0 THEN h = large(h, diff * -1) END IF END IF END IF 'inflict IF attack.show_damage_without_inflicting = NO THEN 'I think that not applying the damage cap when only showing damage is pretty dodgy... IF gen(genDamageCap) > 0 AND capdamage THEN IF h > gen(genDamageCap) THEN h = gen(genDamageCap) IF h < -gen(genDamageCap) THEN h = -gen(genDamageCap) END IF tstat_cur = safesubtract(tstat_cur, h) IF attack.absorb_damage THEN WITH attacker '--drain DIM absorb_amount as integer = h * attack.absorb_rate astat_cur += absorb_amount IF attack.dont_display_damage = NO THEN .harm.text = STR(ABS(absorb_amount)) IF h > 0 THEN .harm.text = "+" + .harm.text END IF .harm.ticks = gen(genDamageDisplayTicks) .harm.col = uilook(uiBattleAbsorb) .harm.pos.x = .x + (.w * .5) .harm.pos.y = .y + (.h * .5) END WITH END IF IF attack.poison_is_negative_regen AND (targstat = statPoison OR targstat = statRegen) THEN 'Healing poison causes regen, and vice versa DIM negatedstat as integer = IIF(targstat = statPoison, statRegen, statPoison) ' For both of target.stat and attacker.stat: FOR itr as integer = 0 TO 1 WITH *IIF(itr, @target.stat, @attacker.stat) DIM abovemax as integer ' Healing poison register above max becomes regen and vice versa abovemax = .cur.sta(targstat) - .max.sta(targstat) IF abovemax > 0 THEN .cur.sta(negatedstat) -= abovemax .cur.sta(targstat) = .max.sta(targstat) END IF END WITH NEXT END IF 'Enforce max/min stat bounds. 'FIXME: Why do we ignore prefbit(43) "Cap minimum stats at zero"? We're probably going to need a new prefbit. 'Normally cap below at zero, but if the stat is already below zero make sure an attempt to decrease it further 'doesn't increase it to zero. tstat_cur = large(tstat_cur, small(0, tstat_original)) astat_cur = large(astat_cur, small(0, astat_original)) IF target_is_register OR attack.allow_cure_to_exceed_maximum = NO THEN 'Cap to max. But if the stat was already above max then instead don't allow 'it to go higher. tstat_cur = small(tstat_cur, large(tstat_max, tstat_original)) astat_cur = small(astat_cur, large(astat_max, astat_original)) END IF 'FIXME: stat caps aren't observed (bug 980) 'HOWEVER, when bug 980 is fixed, need to be careful not to break out-of-battle permanent stat 'boosting items, which rely on the caps being exceeded, then update hero base stats as a result. 'See oobcure. 'remember revenge data 'NOTE: revenge & thankvenge record difference AFTER "reset target stat to max" takes effect IF tstat_original > tstat_cur THEN target.revengemask(attackerslot) = YES target.revenge = attackerslot target.revengeharm = tstat_original - tstat_cur attacker.repeatharm = tstat_original - tstat_cur END IF 'remember thankvenge data IF tstat_original < tstat_cur THEN target.thankvengemask(attackerslot) = YES target.thankvenge = attackerslot target.thankvengecure = ABS(tstat_original - tstat_cur) END IF END IF 'set damage display IF attack.dont_display_damage = NO THEN target.harm.text = STR(ABS(h)) IF h < 0 THEN '--if cure, show + sign and change color target.harm.text = "+" + target.harm.text target.harm.col = uilook(uiBattleHeal) END IF END IF END IF 'skips to here for No Damage attacks IF attack.damage_color > 0 THEN target.harm.col = attack.damage_color END IF 'remember "Counter" target-class data target.counter_target = attackerslot IF attack.show_name = YES THEN IF LEN(target.harm.text) > 0 THEN target.harm.text += " " target.harm.text += attack.name END IF 'reset registers as per convenience bits IF attack.reset_poison = YES THEN target.stat.cur.poison = target.stat.max.poison IF attack.reset_regen = YES THEN target.stat.cur.regen = target.stat.max.regen IF attack.reset_stun = YES THEN target.stat.cur.stun = target.stat.max.stun IF attack.reset_mute = YES THEN target.stat.cur.mute = target.stat.max.mute IF attack.empty_target_ready_meter THEN target.ready_meter = 0 target.ready = NO 'See also the logic in battle_attack_cancel_target_attack() END IF IF attack.fill_target_ready_meter THEN target.ready_meter = 1000 make_ready_or_update_turn_delays target END IF '--success! attacker.attack_succeeded = YES RETURN atkHit END FUNCTION FUNCTION liveherocount (bslot() as BattleSprite) as integer '--with bslot() counts heros in-battle HP state DIM i as integer = 0 FOR o as integer = 0 TO 3 IF gam.hero(o).id >= 0 AND bslot(o).stat.cur.hp > 0 THEN i = i + 1 NEXT o RETURN i END FUNCTION FUNCTION liveherocount () as integer '--with no argument, counts live heroes in outside-of-battle active party DIM liveheroes as integer = 0 FOR o as integer = 0 TO 3 IF gam.hero(o).id >= 0 THEN IF gam.hero(o).stat.cur.hp > 0 OR gam.hero(o).stat.max.hp <= 0 THEN liveheroes += 1 END IF NEXT o RETURN liveheroes END FUNCTION 'TODO: no need to limit to 16 bit arithmetic. Also 'at the moment there's no need for this function at all. 'But if we change to 32 bit stats then this must be changed 'to use long longs instead of doubles. FUNCTION safesubtract (byval number as integer, byval minus as integer) as integer DIM longnumber as double = number DIM longminus as double = minus DIM longresult as double = longnumber - longminus IF longresult > 32767 THEN longresult = 32767 IF longresult < -32768 THEN longresult = -32768 DIM result as integer = longresult RETURN result END FUNCTION 'TODO: See safesubtract FUNCTION safemultiply (byval number as integer, byval by as single) as integer dim as integer longnumber = number dim as single longby = by dim as integer longresult = longnumber * longby IF longresult > 32767 THEN longresult = 32767 IF longresult < -32768 THEN longresult = -32768 dim result as integer = longresult return result END FUNCTION SUB setbatcap (bat as BattleState, cap as string, byval captime as integer, byval capdelay as integer) bat.caption = cap embedtext(bat.caption) bat.caption_time = captime bat.caption_delay = capdelay END SUB 'This picks out the targets which are within a 90 degree wide sector in the 'specified direction. Returns number of in-range targets. FUNCTION battle_target_arrows_sector_mask (inrange() as integer, byval d as integer, byval axis as integer, bslot() as BattleSprite, targ as TargettingState, foredistance() as integer, sidedistance() as integer) as integer DIM as integer xdistance, ydistance, count FOR i as integer = 0 TO 11 IF targ.mask(i) THEN ydistance = bslot(i).y - bslot(targ.pointer).y xdistance = bslot(i).x - bslot(targ.pointer).x IF axis THEN foredistance(i) = ydistance * d sidedistance(i) = ABS(xdistance) ELSE foredistance(i) = xdistance * d sidedistance(i) = ABS(ydistance) END IF IF foredistance(i) > 0 THEN 'Angle in degrees from the forward direction. Always nonnegative DIM angle as double = ATAN2(sidedistance(i), foredistance(i)) * 180 / M_PI '? "angle from " & bslot(targ.pointer).pos & " to " & i & " " & bslot(i).pos & " is " & angle & " foredist " & foredistance(i) & " sidedist " & sidedistance(i) & IIF(angle <= 45, " IN SECTOR", "") 'WARNING: allowing angles >= 45 can result in unselectable targets IF angle < 45 THEN 'To also allow targets within a 40x40 box: ORELSE sidedistance(i) < 20 setbit inrange(), 0, i, 1 count += 1 END IF END IF END IF NEXT i RETURN count END FUNCTION 'Move targ.pointer, the target currently selected by the player, or turn on/off optional spread. 'axis: 0 for x (left/right), 1 for y (up/down) movement 'd: -1 (left/up) or 1 (right/down) SUB battle_target_arrows (byval d as integer, byval axis as integer, bslot() as BattleSprite, targ as TargettingState, byval allow_spread as bool = NO) DIM newptr as integer = targ.pointer 'First, special case for target at same position as current: 'Right and Down keys loop over them from lowest to highest index, and Left and Up keys 'from highest to lowest. Only after looping past the last one do we do normal target selection. DIM idx as integer = targ.pointer FOR i as integer = 1 TO 11 idx += d 'search through slots according to direction, but don't loop IF idx < 0 OR idx > UBOUND(targ.mask) THEN EXIT FOR IF targ.mask(idx) = NO THEN CONTINUE FOR IF bslot(idx).pos = bslot(targ.pointer).pos THEN targ.pointer = idx EXIT SUB END IF NEXT 'Look for a nearby target within a 90 degree wide sector in the right direction DIM foredistance(11) as integer DIM sidedistance(11) as integer DIM inrange(0) as integer DIM best as integer = 99999 IF battle_target_arrows_sector_mask(inrange(), d, axis, bslot(), targ, foredistance(), sidedistance()) THEN 'At least one target is in the sector, pick the closest FOR i as integer = 0 TO 11 IF readbit(inrange(), 0, i) THEN IF foredistance(i) < best THEN best = foredistance(i) newptr = i END IF END IF NEXT i ELSE 'If there's none, allow targets which are at any angle, and pick the closest one FOR i as integer = 0 TO 11 IF targ.mask(i) THEN IF foredistance(i) > 0 THEN DIM dist as integer = foredistance(i) + sidedistance(i) 'Both are non-negative IF dist < best THEN best = dist newptr = i END IF END IF END IF NEXT i END IF IF newptr = targ.pointer THEN 'Spread attack IF allow_spread = YES AND targ.opt_spread = 1 THEN FOR i as integer = 0 TO 11 targ.selected(i) = targ.mask(i) NEXT i targ.opt_spread = 2 END IF END IF targ.pointer = newptr END SUB FUNCTION targetmaskcount (tmask() as bool) as integer DIM n as integer = 0 FOR i as integer = 0 TO UBOUND(tmask) IF tmask(i) THEN n += 1 NEXT i RETURN n END FUNCTION SUB traceshow (s as string) textcolor uilook(uiText), uilook(uiOutline) s += STRING(40 - LEN(s), " ") printstr s, 0, 191, 0 printstr s, 0, 191, 1 END SUB FUNCTION trytheft (bat as BattleState, byval who as integer, byval targ as integer, attack as AttackData, bslot() as BattleSprite) as bool IF is_hero(who) ANDALSO is_enemy(targ) THEN '--a hero is attacking an enemy IF attack.can_steal_item THEN '--steal bitset is on for this attack WITH bslot(targ).enemy.steal IF .thievability >= 0 ANDALSO (.item_rate > 0 ORELSE .rare_item_rate > 0) THEN '--enemy is theftable DIM stole as integer = checktheftchance(.item, .item_rate, .rare_item, .rare_item_rate) IF stole THEN '--success! IF .thievability = 0 THEN '--only one theft permitted .thievability = -1 END IF setbatcap bat, readglobalstring(117, "Stole", 30) + " " + readitemname(stole - 1), 40, 0 menusound gen(genStealSuccessSFX) RETURN YES '--success ELSE '--steal failed setbatcap bat, readglobalstring(114, "Cannot Steal", 30), 40, 0 menusound gen(genStealFailSFX) END IF ELSE '--has nothing to steal / steal disabled setbatcap bat, readglobalstring(111, "Has Nothing", 30), 40, 0 menusound gen(genStealNoItemSFX) END IF END WITH END IF END IF RETURN NO '--return false by default END FUNCTION FUNCTION hero_total_exp (byval hero_slot as integer) as integer RETURN total_exp_to_level(gam.hero(hero_slot).lev, gam.hero(hero_slot).exp_mult) + gam.hero(hero_slot).exp_cur END FUNCTION SUB updatestatslevelup (byval hero_slot as integer, byval allowforget as bool) ' hero_slot = who ' allowforget = forget spells if level dropped below requirement WITH gam.hero(hero_slot) 'wipe learnmask for this hero 'note that this gets wiped again later, but that is okay. 'this makes sure that learnmask gets cleared for this battle 'even if the hero *doesn't* get a level-up. flusharray .learnmask() 'THIS PART UPDATES STATS FOR A LEVEL UP IF .lev_gain THEN DIM her as HeroDef loadherodata her, gam.hero(hero_slot).id 'stat increase/decrease FOR statnum as integer = 0 TO statLast DIM n0 as integer = her.Lev0.sta(statnum) DIM nMax as integer = her.LevMax.sta(statnum) DIM statgain as integer statgain = atlevel(.lev, n0, nMax) - atlevel(.lev - .lev_gain, n0, nMax) .stat.base.sta(statnum) += statgain NEXT IF prefbit(9) THEN '"Simulate Old Levelup bonus-accretion bug" DIM bonuses(statLast) as integer hero_total_equipment_bonuses hero_slot, bonuses() FOR statnum as integer = 0 TO statLast .stat.base.sta(statnum) += bonuses(statnum) * .lev_gain NEXT END IF recompute_hero_max_stats hero_slot 'stat restoration IF prefbit(2) = NO THEN '"Don't restore HP on level-up" OFF '--HP restoration .stat.cur.hp = .stat.max.hp 'set external cur to external max END IF IF prefbit(3) = NO THEN '"Don't restore MP on level-up" OFF '--MP restoration .stat.cur.mp = .stat.max.mp 'set external cur to external max reset_levelmp gam.hero(hero_slot) END IF 'make current stats match max stats FOR statnum as integer = 2 TO 11 .stat.cur.sta(statnum) = .stat.max.sta(statnum) NEXT statnum learn_spells_for_current_level(hero_slot, allowforget) END IF END WITH END SUB 'Load sum of bonuses for all of a hero's equip in bonuses() (which should be sized to statLast) SUB hero_total_equipment_bonuses(byval hero_slot as integer, bonuses() as integer) flusharray bonuses() DIM itembuf(dimbinsize(binITM)) as integer WITH gam.hero(hero_slot) FOR slot as integer = 0 TO UBOUND(.equip) IF .equip(slot).id >= 0 THEN loaditemdata itembuf(), .equip(slot).id FOR statnum as integer = 0 TO statLast bonuses(statnum) += itembuf(54 + statnum) NEXT statnum END IF NEXT slot END WITH END SUB 'Recompute max stats from base '(See update_hero_max_and_cur_stats for wrapper that updates cur values too) SUB recompute_hero_max_stats(byval hero_slot as integer) WITH gam.hero(hero_slot).stat DIM bonuses(statLast) as integer hero_total_equipment_bonuses hero_slot, bonuses() FOR statnum as integer = 0 TO statLast .max.sta(statnum) = .base.sta(statnum) + bonuses(statnum) DIM cap as integer = gen(genStatCap + statnum) IF cap > 0 THEN .max.sta(statnum) = small(.max.sta(statnum), cap) IF prefbit(43) THEN 'Cap minimum stats at zero .max.sta(statnum) = large(.max.sta(statnum), 0) .cur.sta(statnum) = large(.cur.sta(statnum), 0) END IF NEXT END WITH END SUB 'This is used for backcompat SUB compute_hero_base_stats_from_max(byval hero_slot as integer) WITH gam.hero(hero_slot).stat DIM bonuses(statLast) as integer hero_total_equipment_bonuses hero_slot, bonuses() FOR statnum as integer = 0 TO statLast .base.sta(statnum) = .max.sta(statnum) - bonuses(statnum) NEXT END WITH END SUB SUB learn_spells_for_current_level(byval who as integer, byval allowforget as bool) 'Teaches all spells that can be learned from level ' up to the hero's current level. '(but should not overwrite any spells learned with "write spell") 'wipe learnmask for this hero flusharray gam.hero(who).learnmask() DIM her as HeroDef loadherodata her, gam.hero(who).id 'learn spells FOR j as integer = 0 TO 3 FOR o as integer = 0 TO 23 WITH her.spell_lists(j,o) '--if slot is empty and slot accepts a spell and learn-by-level condition is true IF gam.hero(who).spells(j, o) = 0 AND .attack > 0 AND .learned - 1 <= gam.hero(who).lev AND .learned > 0 THEN gam.hero(who).spells(j, o) = .attack setbit gam.hero(who).learnmask(), 0, j * 24 + o, 1 END IF IF allowforget THEN '--plotscripts may lower level, forget spells if drop below requirement and know the spell specified IF gam.hero(who).spells(j, o) = .attack AND .learned - 1 > gam.hero(who).lev THEN gam.hero(who).spells(j, o) = 0 END IF END IF END WITH NEXT o NEXT j END SUB FUNCTION allowed_to_gain_levels(byval heroslot as integer) as bool IF heroslot < 0 THEN RETURN NO 'out of range IF heroslot > UBOUND(gam.hero) THEN RETURN NO ' out of range IF gam.hero(heroslot).id = -1 THEN RETURN NO ' no hero in this slot IF gam.hero(heroslot).lev >= current_max_level THEN RETURN NO RETURN YES END FUNCTION 'Returns true if hero gained XP FUNCTION giveheroexperience (byval who as integer, byval exper as integer) as bool WITH gam.hero(who) 'reset levels gained .lev_gain = 0 IF allowed_to_gain_levels(who) THEN .exp_cur += exper 'levelups WHILE .exp_cur >= .exp_next .exp_cur -= .exp_next .lev += 1 'current level .lev_gain += 1 'levelup counter .exp_next = exptolevel(.lev + 1, .exp_mult) IF .lev >= current_max_level THEN 'You can't gain experience once you've hit the level cap .exp_cur = 0 EXIT WHILE END IF WEND RETURN YES END IF END WITH END FUNCTION SUB setheroexperience (byval who as integer, byval amount as integer, byval allowforget as bool) 'unlike giveheroexperience, this can cause delevelling DIM orig_lev as integer = gam.hero(who).lev DIM total as integer = 0 DIM lostlevels as bool = NO FOR i as integer = 0 TO gam.hero(who).lev - 1 total += exptolevel(i + 1, gam.hero(who).exp_mult) NEXT IF total > amount THEN 'losing levels; lvl up from level 0 gam.hero(who).lev = 0 gam.hero(who).exp_next = exptolevel(1, gam.hero(who).exp_mult) lostlevels = YES ELSE 'set spell learnt bits correctly amount -= total orig_lev = 0 lostlevels = NO END IF gam.hero(who).exp_cur = 0 giveheroexperience who, amount updatestatslevelup who, allowforget gam.hero(who).lev_gain -= orig_lev IF lostlevels THEN 'didn't learn spells, wipe mask flusharray gam.hero(who).learnmask() END IF 'Update tags evalherotags tag_updates END SUB FUNCTION visibleandalive (byval who as integer, bslot() as BattleSprite) as bool RETURN (bslot(who).vis ANDALSO bslot(who).stat.cur.hp > 0) END FUNCTION SUB export_battle_hero_stats (bslot() as BattleSprite) 'Export a few specific hero battle stats to the out-of-battle party 'This may be used frequently in battle FOR i as integer = 0 TO 3 IF gam.hero(i).id >= 0 THEN '--set out-of-battle HP and MP equal to in-battle HP and MP gam.hero(i).stat.cur.hp = bslot(i).stat.cur.hp gam.hero(i).stat.cur.mp = bslot(i).stat.cur.mp END IF NEXT i END SUB SUB import_battle_hero_stats (bslot() as BattleSprite) 'Import a few specific stats to the temporary hero battle stats 'This would normally only be used at victory, in case out-of-battle 'stats have changed due to a level-up, and would never be used without 'following an accompanying call to export_battle_hero_stats FOR i as integer = 0 TO 3 IF gam.hero(i).id >= 0 THEN '--set in-battle HP and MP equal to out-of-battle HP and MP bslot(i).stat.cur.hp = gam.hero(i).stat.cur.hp bslot(i).stat.cur.mp = gam.hero(i).stat.cur.mp END IF NEXT i END SUB 'If a target isn't a foe, then they are an ally FUNCTION is_foe_of(target as integer, attacker as integer, bslot() as BattleSprite) as bool 'This function could be simpler, but this complexity may be 'needed when extra bits are added? 'If this prefbit is on, turncoat and defector always consider themselves to be an ally IF prefbit(55) ANDALSO attacker = target THEN RETURN NO DIM attacker_as_hero as bool = is_hero(attacker) DIM attacker_as_enemy as bool = is_enemy(attacker) IF bslot(attacker).turncoat_attacker THEN attacker_as_hero = NOT attacker_as_hero attacker_as_enemy = NOT attacker_as_enemy END IF DIM targ_as_hero as bool = is_hero(target) DIM targ_as_enemy as bool = is_enemy(target) IF bslot(target).defector_target THEN targ_as_hero = NOT targ_as_hero targ_as_enemy = NOT targ_as_enemy END IF IF attacker_as_hero THEN RETURN targ_as_enemy ELSE 'IF attacker_as_enemy THEN RETURN targ_as_hero END IF END FUNCTION FUNCTION has_valid_targs(byval who as integer, byval atk_id as integer, bslot() as BattleSprite, bat as BattleState) as bool DIM attack as AttackData loadattackdata attack, atk_id RETURN has_valid_targs(who, attack, bslot(), bat) END FUNCTION FUNCTION has_valid_targs(byval who as integer, byref atk as AttackData, bslot() as BattleSprite, bat as BattleState) as bool DIM tmask(11) as bool ' Which targets are valid for the currently targetting attack get_valid_targs tmask(), who, atk, bslot(), bat FOR i as integer = 0 TO UBOUND(tmask) IF tmask(i) THEN RETURN YES NEXT i RETURN NO END FUNCTION 'who: attacker SUB get_valid_targs(tmask() as bool, byval who as integer, byref atk as AttackData, bslot() as BattleSprite, bat as BattleState) DIM i as integer FOR i = 0 TO 11 tmask(i) = NO ' clear list of available targets NEXT i DIM attacker_as_hero as bool = is_hero(who) DIM attacker_as_enemy as bool = is_enemy(who) IF bslot(who).turncoat_attacker THEN attacker_as_hero = NOT attacker_as_hero attacker_as_enemy = NOT attacker_as_enemy END IF DIM foe(11) as bool FOR i = 0 to 11 foe(i) = is_foe_of(i, who, bslot()) NEXT SELECT CASE atk.targ_class CASE 0 'foe FOR i = 0 TO 11 IF foe(i) THEN tmask(i) = bslot(i).vis NEXT i CASE 1 'ally FOR i = 0 TO 11 IF foe(i) = NO THEN tmask(i) = bslot(i).vis NEXT i CASE 2 'self tmask(who) = YES CASE 3 'all (not dead) FOR i = 0 TO 11 tmask(i) = bslot(i).vis NEXT i CASE 4 'ally-including-dead FOR i = 0 TO 11 IF foe(i) = NO THEN tmask(i) = (bslot(i).vis ORELSE (is_hero(i) ANDALSO gam.hero(i).id >= 0)) END IF NEXT i CASE 5 'ally-not-self FOR i = 0 TO 11 IF foe(i) = NO ANDALSO i <> who THEN tmask(i) = bslot(i).vis NEXT i CASE 6 'revenge-one IF bslot(who).revenge >= 0 THEN tmask(bslot(who).revenge) = bslot(bslot(who).revenge).vis END IF CASE 7 'revenge-all FOR i = 0 TO 11 IF bslot(who).revengemask(i) = YES AND bslot(i).vis THEN tmask(i) = YES END IF NEXT i CASE 8 'previous FOR i = 0 TO 11 IF bslot(who).last_targs(i) = YES AND bslot(i).vis THEN tmask(i) = YES END IF NEXT i CASE 9 'stored FOR i = 0 TO 11 IF bslot(who).stored_targs(i) = YES AND (bslot(i).vis OR bslot(who).stored_targs_can_be_dead) THEN tmask(i) = YES END IF NEXT i CASE 10 'dead-ally (heroes only) FOR i = 0 TO 11 IF foe(i) = NO THEN 'I'm not sure this considers heroes to be dead in the same circumstances as other target classes, 'since it doesn't check .vis tmask(i) = (is_hero(i) ANDALSO gam.hero(i).id >= 0 ANDALSO bslot(i).stat.cur.hp <= 0) END IF NEXT i CASE 11 'thankvenge-one IF bslot(who).thankvenge >= 0 THEN tmask(bslot(who).thankvenge) = bslot(bslot(who).thankvenge).vis END IF CASE 12 'thankvenge-all FOR i = 0 TO 11 IF bslot(who).thankvengemask(i) = YES AND bslot(i).vis THEN tmask(i) = YES END IF NEXT i CASE 13 'counter DIM counter as integer = bslot(who).counter_target IF counter >= 0 THEN tmask(counter) = bslot(counter).vis END IF CASE 14 'all-including-dead FOR i = 0 TO 11 IF bslot(i).vis ORELSE (is_hero(i) ANDALSO gam.hero(i).id >= 0) THEN tmask(i) = YES NEXT i CASE 15 'dead foe (heroes only) FOR i = 0 TO 11 IF foe(i) THEN IF is_hero(i) ANDALSO gam.hero(i).id >= 0 ANDALSO bslot(i).stat.cur.hp <= 0 THEN tmask(i) = YES END IF NEXT i CASE 16 'foe-including-dead FOR i = 0 TO 11 IF foe(i) THEN IF bslot(i).vis ORELSE (is_hero(i) ANDALSO gam.hero(i).id >= 0) THEN tmask(i) = YES END IF NEXT i CASE 17 'spawned by last attack IF bat.most_recently_spawned_by_attack > -1 THEN IF bslot(bat.most_recently_spawned_by_attack).vis THEN tmask(bat.most_recently_spawned_by_attack) = YES END IF END IF 'Consider updating chkOOBtarg when adding new target classes concerning dead allies... 'but OOB nearly all are treated as 'All'. END SELECT 'enforce attack's disabled enemy target slots FOR i = 0 TO 7 IF atk.cannot_target_enemy_slot(i) THEN tmask(4 + i) = NO NEXT i 'enforce attack's disabled hero target slots FOR i = 0 TO 3 IF atk.cannot_target_hero_slot(i) THEN tmask(i) = NO NEXT i FOR i = 0 TO 11 'If a target is doing an on-death bequest attack, ' it cannot be targeted by anyone but itself IF bslot(i).bequesting AND i <> who THEN tmask(i) = NO END IF NEXT i 'Some restrictions are only applied when the target class is not "self" FOR i = 0 TO 11 'Hidden targets can't be targetted IF should_enforce_hidden_untargetability(atk) THEN IF bslot(i).hidden THEN tmask(i) = NO END IF IF should_enforce_untargetability(atk) THEN 'enforce untargetability IF attacker_as_hero THEN IF bslot(i).hero_untargetable = YES THEN tmask(i) = NO ELSEIF attacker_as_enemy THEN IF bslot(i).enemy_untargetable = YES THEN tmask(i) = NO END IF END IF NEXT i END SUB FUNCTION should_enforce_hidden_untargetability(atk as AttackData) as bool IF atk.always_unhide_attacker ORELSE atk.attacker_anim = 5 ORELSE atk.attacker_anim = 11 THEN 'Any attack that does "Unhide" is likely to need to ignore untargetability 'This fixes jump-land chains when they jump on self, or jump on another jumper RETURN NO END IF RETURN YES END FUNCTION FUNCTION should_enforce_untargetability(atk as AttackData) as bool IF atk.targ_class = 2 THEN RETURN NO 'Self-targetting attacks should override untargetability RETURN YES END FUNCTION ' Note: attack_placement_over_target has a special case for the walk-forward-20-pixels behaviour SUB anim_advance (byval who as integer, attack as AttackData, bslot() as BattleSprite, t() as integer) DIM d as integer ' Enemy faces right, hero faces left DIM target as BattleSprite ptr = @bslot(t(0)) IF attack.always_unhide_attacker THEN anim_unhide who END IF SELECT CASE attack.attacker_anim CASE atkrAnimStrike, atkrAnimCast, atkrAnimSpinStrike, atkrAnimJump IF is_hero(who) THEN ' Walk forward 20 pixels anim_walktoggle who anim_velocity who, -4, 0, 5 anim_waitforall END IF CASE atkrAnimDashIn ' Don't backstab yourself. IF t(0) = who THEN EXIT SUB anim_walktoggle who IF is_enemy(who) THEN anim_absmove who, target->x - bslot(who).w, target->y + target->h - bslot(who).h + 2, 6 ELSE anim_absmove who, target->x + target->w, target->y + target->h - bslot(who).h + 2, 6 END IF anim_waitforall CASE atkrAnimTeleport ' Don't backstab yourself. IF t(0) = who THEN EXIT SUB IF is_enemy(who) THEN anim_setpos who, target->x - bslot(who).w, target->y + target->h - bslot(who).h, 0 ELSE anim_setpos who, target->x + target->w, target->y + target->h - bslot(who).h, 0 END IF CASE atkrAnimLand, atkrAnimNull, atkrAnimStandingCast, atkrAnimStandingStrike, _ atkrAnimRunAndHide, atkrAnimRunInUnHide ' Do nothing END SELECT END SUB 'Generate attacker animation when hero attacks SUB anim_hero (byval who as integer, attack as AttackData, bslot() as BattleSprite, t() as integer) SELECT CASE attack.attacker_anim ' Animations using the cast animation CASE atkrAnimCast, atkrAnimStandingCast anim_setframe who, frameSTAND anim_wait 3 anim_setframe who, frameCAST anim_wait 3 CASE atkrAnimStrike, atkrAnimDashIn, atkrAnimTeleport, atkrAnimStandingStrike ' Animations which show the weapon graphic anim_setframe who, frameSTAND anim_wait 3 'wait 3 ticks anim_setframe who, frameATTACKA DIM as XYPair hand = gam.hero(who).hand_pos(0) DIM as XYPair wep = bslot(24).hand(0) DIM as XYPair wepoff = hand - wep anim_align2 24, who, 0, 0, wepoff.x, 16 anim_setz 24, 16 - wepoff.y anim_setframe 24, 0 anim_appear 24 anim_wait 3 anim_setframe who, frameATTACKB hand = gam.hero(who).hand_pos(1) wep = bslot(24).hand(1) wepoff = hand - wep anim_align2 24, who, 0, 0, wepoff.x, 16 anim_setz 24, 16 - wepoff.y anim_setframe 24, 1 CASE atkrAnimSpinStrike FOR ii as integer = 0 TO 2 anim_setdir who, 1 anim_wait 1 anim_setdir who, 0 anim_wait 1 NEXT ii CASE atkrAnimJump anim_setframe who, frameJUMP anim_relmove who, -26, 0, 13 anim_zvelocity who, 18, 13 anim_waitforall anim_hide who anim_setframe who, frameSTAND CASE atkrAnimLand anim_setz who, 200 anim_setframe who, frameLAND anim_unhide who anim_setcenter who, t(0), 0, 0 anim_align who, t(0), dirDown, 0 anim_zvelocity who, -20, 10 anim_waitforall anim_setframe who, frameHURT CASE atkrAnimRunAndHide anim_setframe who, frameJUMP anim_setdir who, 1 anim_absmove who, 320 + bslot(t(0)).w / 2, bslot(t(0)).y, 10 anim_waitforall anim_hide who anim_setframe who, frameSTAND anim_setdir who, 0 CASE atkrAnimRunInUnhide anim_setz who, 0 anim_setpos who, 320 + bslot(t(0)).w / 2, bslot(t(0)).y, 0 anim_setframe who, frameSTAND anim_unhide who anim_absmove who, bslot(t(0)).x, bslot(t(0)).y, 10 anim_waitforall CASE atkrAnimNull 'Nothing END SELECT END SUB 'Generate attacker animation when an enemy attacks SUB anim_enemy (byval who as integer, attack as AttackData, bslot() as BattleSprite, t() as integer) SELECT CASE attack.attacker_anim CASE atkrAnimStrike, atkrAnimCast ' twitch anim_setz who, 2 anim_wait 1 anim_setz who, 0 CASE atkrAnimSpinStrike FOR ii as integer = 0 TO 2 anim_setdir who, 1 anim_wait 1 anim_setdir who, 0 anim_wait 1 NEXT ii CASE atkrAnimJump anim_absmove who, bslot(who).x + 50, bslot(who).y, 7 anim_zvelocity who, 20, 10 anim_waitforall anim_hide who CASE atkrAnimLand anim_setz who, 200 anim_unhide who anim_setpos who, bslot(t(0)).x, bslot(t(0)).y, 0 anim_zvelocity who, -20, 10 anim_waitforall CASE atkrAnimRunAndHide anim_setdir who, 1 anim_absmove who, 0 - bslot(t(0)).w / 2, bslot(t(0)).y, 10 anim_waitforall anim_hide who anim_setdir who, 0 CASE atkrAnimRunInUnhide anim_setz who, 0 anim_setpos who, 0 - bslot(t(0)).w / 2, bslot(t(0)).y, 0 anim_unhide who anim_absmove who, bslot(t(0)).x, bslot(t(0)).y, 10 anim_waitforall CASE atkrAnimDashIn, atkrAnimNull, atkrAnimStandingCast, atkrAnimTeleport, atkrAnimStandingStrike ' nothing END SELECT END SUB ' Undoes anim_advance SUB anim_retreat (byval who as integer, attack as AttackData, bslot() as BattleSprite) IF is_enemy(who) THEN IF attack.attacker_anim = atkrAnimDashIn OR attack.attacker_anim = atkrAnimLand THEN anim_setz who, 0 anim_absmove who, bslot(who).x, bslot(who).y, 6 anim_waitforall END IF END IF IF is_hero(who) THEN SELECT CASE attack.attacker_anim CASE atkrAnimStrike, atkrAnimCast ' Walk back 20 pixels anim_walktoggle who anim_velocity who, 4, 0, 5 anim_waitforall anim_setframe who, frameSTAND CASE atkrAnimDashIn, atkrAnimLand anim_setframe who, frameSTAND anim_walktoggle who anim_setz who, 0 anim_absmove who, bslot(who).x, bslot(who).y, 6 anim_waitforall anim_setframe who, frameSTAND CASE atkrAnimStandingCast, atkrAnimStandingStrike anim_setframe who, frameSTAND CASE atkrAnimNull, atkrAnimSpinStrike, atkrAnimJump, atkrAnimTeleport, atkrAnimRunAndHide, atkrAnimRunInUnHide ' Do nothing END SELECT END IF IF attack.always_hide_attacker THEN anim_unhide who END IF END SUB FUNCTION attack_can_hit_dead(atk_id as integer, stored_targs_can_be_dead as bool=NO) as bool DIM attack as AttackData loadattackdata attack, atk_id RETURN attack_can_hit_dead(attack, stored_targs_can_be_dead) END FUNCTION FUNCTION attack_can_hit_dead(attack as AttackData, stored_targs_can_be_dead as bool=NO) as bool SELECT CASE attack.targ_class CASE 4 'ally-including-dead (hero only) RETURN YES CASE 9 'stored targets IF stored_targs_can_be_dead THEN RETURN YES CASE 10 'dead-ally (hero only) RETURN YES CASE 14 'all-including-dead RETURN YES CASE 15, 16 'dead foe (enemy only), foe-incluidng-dead RETURN YES END SELECT RETURN NO END FUNCTION FUNCTION autotarget (who as integer, atk_id as integer, bslot() as BattleSprite, bat as BattleState, queue as bool=YES, override_blocking as integer=-2, dont_retarget as bool=NO, is_counterattack as bool=NO) as bool DIM t(11) as integer RETURN autotarget(who, atk_id, bslot(), bat, t(), queue, override_blocking, dont_retarget, is_counterattack) END FUNCTION FUNCTION autotarget (who as integer, atk_id as integer, bslot() as BattleSprite, bat as BattleState, t() as integer, queue as bool=YES, override_blocking as integer=-2, dont_retarget as bool=NO, is_counterattack as bool=NO) as bool DIM attack as AttackData loadattackdata attack, atk_id RETURN autotarget(who, attack, bslot(), bat, t(), queue, override_blocking, dont_retarget, is_counterattack) END FUNCTION FUNCTION autotarget (who as integer, byref atk as AttackData, bslot() as BattleSprite, bat as BattleState, queue as bool=YES, override_blocking as integer=-2, dont_retarget as bool=NO, is_counterattack as bool=NO) as bool DIM t(11) as integer RETURN autotarget(who, atk, bslot(), bat, t(), queue, override_blocking, dont_retarget, is_counterattack) END FUNCTION FUNCTION autotarget (who as integer, byref atk as AttackData, bslot() as BattleSprite, bat as BattleState, t() as integer, queue as bool=YES, override_blocking as integer=-2, dont_retarget as bool=NO, is_counterattack as bool=NO) as bool '--Returns true if the targetting was successful, or false if it failed for some reason ' such as no valid targets being available. IF is_counterattack THEN IF atk.blocking_counterattack THEN override_blocking = YES END IF DIM tmask(11) as bool ' Which targets are valid for the currently targetting attack get_valid_targs tmask(), who, atk, bslot(), bat flusharray t(), 11, YES SELECT CASE atk.targ_set CASE 0, 3: '--focus and random roulette confirm_auto_focus who, tmask(), atk, bslot(), t() CASE 1: '--spread attack confirm_auto_spread who, tmask(), bslot(), t() CASE 2: '-- optional spread IF randint(100) < 33 THEN confirm_auto_spread who, tmask(), bslot(), t() ELSE confirm_auto_focus who, tmask(), atk, bslot(), t() END IF CASE 4: '--first target confirm_auto_first who, tmask(), bslot(), t() END SELECT '--check to see if any targets were actually chosen DIM result as bool = NO FOR i as integer = 0 TO UBOUND(t) IF t(i) >= 0 THEN result = YES NEXT i '--Now copy the target into the queue '(Note! this is done even if the attack has no targets, which will result in it being 'cancelled when it gets dequeued, wasting the attacker's turn) IF queue THEN queue_attack atk.id, who, t(), override_blocking, dont_retarget END IF IF result = NO THEN checkAtkTagConds atk, atktagOnTargettingFailed END IF RETURN result END FUNCTION SUB confirm_auto_spread (byval who as integer, tmask() as bool, bslot() as BattleSprite, t() as integer) DIM i as integer DIM targetptr as integer = 0 FOR i = 0 TO 11 IF tmask(i) THEN t(targetptr) = i targetptr = targetptr + 1 END IF NEXT i END SUB SUB confirm_auto_focus (byval who as integer, tmask() as bool, byref atk as AttackData, bslot() as BattleSprite, t() as integer) t(0) = find_preferred_target(tmask(), who, atk, bslot()) END SUB SUB confirm_auto_first (byval who as integer, tmask() as bool, bslot() as BattleSprite, t() as integer) DIM i as integer FOR i = 0 TO 11 IF tmask(i) THEN t(0) = i EXIT SUB END IF NEXT i END SUB FUNCTION find_preferred_target(tmask() as bool, byval who as integer, atk as AttackData, bslot() as BattleSprite) as integer DIM i as integer DIM best as integer DIM search as integer DIM found as integer DIM prefstat as integer IF atk.prefer_targ_stat = 0 THEN 'Weak/Strong pref stat defaults to target stat prefstat = atk.targ_stat ELSE prefstat = atk.prefer_targ_stat - 1 END IF SELECT CASE atk.prefer_targ ' Preferred target type CASE 0 '--Default IF bslot(who).under_player_control THEN atk.prefer_targ = 1 ' heroes default to first target IF atk.targ_set = 3 THEN 'unless the target setting is random roulette, in which case random atk.prefer_targ = 4 END IF ELSE atk.prefer_targ = 4 ' enemies default to a random target END IF found = find_preferred_target(tmask(), who, atk, bslot()) atk.prefer_targ = 0 RETURN found CASE 1 '--First 'special handling for heroes using attacks that target all/all-including-dead IF bslot(who).under_player_control AND (atk.targ_class = 3 OR atk.targ_class = 14) THEN FOR i = 0 to 11 IF is_foe_of(i, who, bslot()) THEN IF tmask(i) THEN RETURN i END IF NEXT i ELSE ' normal first-target handling FOR i = 0 to 11 IF tmask(i) THEN RETURN i NEXT i END IF CASE 2 '--Closest best = -1 found = 200000 FOR i = 0 TO 11 IF tmask(i) THEN search = quick_battle_distance(who, i, bslot()) IF search < found THEN best = i found = search END IF END IF NEXT i IF best >= 0 THEN RETURN best CASE 3 '--Farthest best = -1 found = -1 FOR i = 0 TO 11 IF tmask(i) THEN search = quick_battle_distance(who, i, bslot()) IF search > found THEN best = i found = search END IF END IF NEXT i IF best >= 0 THEN RETURN best CASE 4 '--Random search = 0 DO found = randint(12) IF tmask(found) THEN RETURN found search = search + 1 LOOP UNTIL search > 999 ' safety CASE 5 'Weakest (absolute) best = -1 found = 32767 FOR i = 0 TO 11 IF tmask(i) THEN search = bslot(i).stat.cur.sta(prefstat) IF search < found THEN best = i found = search END IF END IF NEXT i IF best >= 0 THEN RETURN best CASE 6 'Strongest (absolute) best = -1 found = -1 FOR i = 0 TO 11 IF tmask(i) THEN search = bslot(i).stat.cur.sta(prefstat) IF search > found THEN best = i found = search END IF END IF NEXT i IF best >= 0 THEN RETURN best CASE 7 'Weakest (percent) best = -1 found = 10001 'use ten-thousands rather than hundreds to simulate two fixed-precision decmal places FOR i = 0 TO 11 IF tmask(i) THEN search = INT(10000 / bslot(i).stat.max.sta(prefstat) * bslot(i).stat.cur.sta(prefstat)) IF search < found THEN best = i found = search END IF END IF NEXT i IF best >= 0 THEN RETURN best CASE 8 'Strongest (percent) best = -1 found = -1 FOR i = 0 TO 11 IF tmask(i) THEN search = INT(10000 / bslot(i).stat.max.sta(prefstat) * bslot(i).stat.cur.sta(prefstat)) IF search > found THEN best = i found = search END IF END IF NEXT i IF best >= 0 THEN RETURN best END SELECT '-- If all else fails, make sure the default target is valid FOR i = 0 TO 11 IF tmask(i) THEN RETURN i NEXT i ' If no valid targets were found, fail with -1 RETURN -1 END FUNCTION FUNCTION quick_battle_distance(byval who1 as integer, byval who2 as integer, bslot() as BattleSprite) as integer ' For speed, this function only implements the exponent part of Pythagoras' formula ' and not the square root part of it, making the results only directly useful for ' quick comparisons of distance DIM as integer distx, disty distx = ABS((bslot(who1).x + bslot(who1).w \ 2) - (bslot(who2).x + bslot(who2).w \ 2)) disty = ABS((bslot(who1).y + bslot(who1).h) - (bslot(who2).y + bslot(who2).h)) RETURN distx ^ 2 + disty ^ 2 END FUNCTION FUNCTION battle_distance(byval who1 as integer, byval who2 as integer, bslot() as BattleSprite) as integer 'Returns exact distance between two battlesprites ' Square root is a bit slow, so don't over-use this function RETURN SQR(quick_battle_distance(who1, who2, bslot())) END FUNCTION 'Count the number of live allies of_whom (excluding self) not Ignored For Alone. 'Hidden allies are counted. FUNCTION count_allies(of_whom as integer, bslot() as BattleSprite) as integer DIM count as integer = 0 FOR i as integer = 0 TO 11 WITH bslot(i) IF i <> of_whom ANDALSO is_foe_of(i, of_whom, bslot()) = NO _ ANDALSO .stat.cur.hp > 0 ANDALSO .vis ANDALSO .ignore_for_alone = NO THEN count += 1 END IF END WITH NEXT i RETURN count END FUNCTION 'Count the number of live and non-hidden foes of_whom FUNCTION count_foes(of_whom as integer, bslot() as BattleSprite) as integer DIM count as integer = 0 FOR i as integer = 0 TO 11 WITH bslot(i) IF is_foe_of(i, of_whom, bslot()) _ ANDALSO .stat.cur.hp > 0 ANDALSO .vis ANDALSO .hidden = NO THEN count += 1 END IF END WITH NEXT i RETURN count END FUNCTION 'Called to load an enemy from a Formation slot. Can be called even if the 'slot is empty, but can not be called to cleanup a loaded enemy. SUB loadfoe (byval slot as integer, formdata as Formation, byref bat as BattleState, bslot() as BattleSprite, byval allow_dead as bool = NO) '--slot is the enemy formation slot DIM byref bspr as BattleSprite = bslot(4 + slot) IF formdata.slots(slot).id >= 0 THEN '-- if this slot is occupied WITH bspr loadenemydata .enemy, formdata.slots(slot).id setup_non_volatile_enemy_state bspr reset_enemy_state bspr, bat '--Special handling for spawning already-dead enemies IF allow_dead = NO THEN 'enemies which spawn already-dead should be killed off immediately 'die without boss or 0 hp? IF dieWOboss(4 + slot, bslot()) OR .enemy.stat.hp <= 0 THEN 'rewards and spawn enemies on death 'enemy is only partially constructed, but already have everything needed. dead_enemy 4 + slot, -1, bat, bslot(), formdata EXIT SUB END IF END IF setup_enemy_slice bspr, bat '--Position .basepos = formdata.slots(slot).pos (.pos) = .basepos '--targetting state .revenge = -1 .thankvenge = -1 .counter_target = -1 FOR i as integer = 0 TO 11 .revengemask(i) = NO .last_targs(i) = NO .stored_targs(i) = NO .thankvengemask(i) = NO NEXT i .active_turn_num = 0 'Start spawned enemies with empty ready meters rather than randomising like at start of battle .ready_meter = 0 '--update stats FOR i as integer = 0 TO 11 .stat.cur.sta(i) = .enemy.stat.sta(i) .stat.max.sta(i) = .enemy.stat.sta(i) NEXT i END WITH END IF END SUB SUB transfer_enemy_bits(byref bspr as BattleSprite) '--Copy elemental bits and other bits from bslot().enemy to bslot() WITH bspr .harmed_by_cure = .enemy.harmed_by_cure .mp_idiot = .enemy.mp_idiot .is_boss = .enemy.is_boss .unescapable = .enemy.unescapable .die_without_boss = .enemy.die_without_boss .flee_instead_of_die = .enemy.flee_instead_of_die .enemy_untargetable = .enemy.enemy_untargetable .hero_untargetable = .enemy.hero_untargetable .death_unneeded = .enemy.death_unneeded .never_flinch = .enemy.never_flinch .ignore_for_alone = .enemy.ignore_for_alone .give_rewards_even_if_alive = .enemy.give_rewards_even_if_alive FOR i as integer = 0 TO gen(genNumElements) - 1 .elementaldmg(i) = .enemy.elementals(i) NEXT END WITH END SUB SUB transfer_enemy_counterattacks (byref bspr as BattleSprite) '--transfer counterattacks WITH bspr FOR j as integer = 0 TO gen(genNumElements) - 1 .elem_counter_attack(j) = .enemy.elem_counter_attack(j) NEXT j .non_elem_counter_attack = .enemy.non_elem_counter_attack FOR j as integer = 0 TO statLast .stat_counter_attack(j) = .enemy.stat_counter_attack(j) NEXT j END WITH END SUB ' Called when loading or reloading (transmogrification) an enemy, ' sets up anything that should be reset when transmogrifying SUB reset_enemy_state(byref bspr as BattleSprite, byref bat as BattleState, transmogging as bool=NO) WITH bspr .vis = YES .d = 0 .dissolve = 0 .dissolve_appear = -1 'One tick delay before starting to appear, to happen after battle fade-in .fleeing = NO .bequesting = NO .self_bequesting = NO IF NOT transmogging THEN .ready = NO .ready_meter = YES .under_player_control = bspr.enemy.controlled_by_player .turncoat_attacker = bspr.enemy.turncoat_attacker .defector_target = bspr.enemy.defector_target END IF IF bat.debug_player_control THEN .under_player_control = YES END WITH END SUB ' Called when loading or reloading (transmogrification) an enemy, ' sets everything that doesn't change SUB setup_non_volatile_enemy_state(byref bspr as BattleSprite) WITH bspr .name = .enemy.name .deathtype = .enemy.dissolve - 1 IF .deathtype = -1 THEN .deathtype = gen(genEnemyDissolve) ._deathtime = .enemy.dissolve_length .appeartype = .enemy.dissolve_in - 1 ._appeartime = .enemy.dissolve_in_length .cursorpos.x = -.enemy.cursor_offset.x '--negated because enemy sprite was originally h-flipped in the editor! .cursorpos.y = .enemy.cursor_offset.y .death_sfx = .enemy.death_sound transfer_enemy_bits bspr transfer_enemy_counterattacks bspr END WITH END SUB SUB changefoe(bat as BattleState, byval slot as integer, transmog as TransmogData, formdata as Formation, bslot() as BattleSprite) IF formdata.slots(slot).id = -1 THEN showbug "changefoe doesn't work on empty slot " & slot & " (enemy " & transmog.enemy & ")" EXIT SUB END IF DIM byref bspr as BattleSprite = bslot(4 + slot) IF transmog.rewards_rule = 1 THEN 'Give rewards before transmogrification enemy_death_rewards bat, bspr END IF formdata.slots(slot).id = transmog.enemy WITH bspr BUG_IF(.sl = NULL, "foe not loaded") loadenemydata .enemy, formdata.slots(slot).id setup_non_volatile_enemy_state bspr reset_enemy_state bspr, bat, YES DIM old_w as integer = .w DIM old_h as integer = .h setup_enemy_slice bspr, bat, YES '--adjust position DIM shift as XYPair = XY(old_w / 2 - .w / 2, old_h - .h) .basepos += shift .pos += shift '--update stats change_foe_stat bspr, 0, .enemy.stat.hp, transmog.hp_rule FOR i as integer = 1 TO statLast change_foe_stat bspr, i, .enemy.stat.sta(i), transmog.other_stats_rule NEXT i END WITH END SUB SUB change_foe_stat(bspr as BattleSprite, byval stat_num as integer, byval new_max as integer, byval stat_rule as TransmogStatsRule) WITH bspr.stat '--selectively alter current stat SELECT CASE stat_rule CASE transmogKeepCurrent '--keep old current CASE transmogUseNewMax '--use new max .cur.sta(stat_num) = new_max CASE transmogKeepCurrentPercent '--preserve % of max IF .max.sta(stat_num) > 0 THEN .cur.sta(stat_num) = CINT(new_max / .max.sta(stat_num) * .cur.sta(stat_num)) ELSE .cur.sta(stat_num) = new_max END IF CASE transmogKeepCurrentCropToMax '--keep old current, crop to new max .cur.sta(stat_num) = small(.cur.sta(stat_num), new_max) CASE transmogKeepCurrentAndMax '--keep old current and max EXIT SUB END SELECT '--All but transmogKeepCurrentAndMax use new max stat .max.sta(stat_num) = new_max END WITH END SUB #IFNDEF NO_TEST_GAME 'There's no need to handle any lumps here which don't have effect on battles such as map data. 'Any lumps that aren't handled here stay in the modified_lumps vector and are handled 'after leaving battle. SUB try_to_reload_files_inbattle () 'calls receive_file_updates try_reload_lumps_anywhere DIM i as integer = 0 WHILE i < v_len(modified_lumps) DIM handled as bool = NO DIM basename as string = trimextension(modified_lumps[i]) DIM extn as string = justextension(modified_lumps[i]) 'Nothing here yet!! IF handled THEN v_delete_slice modified_lumps, i, i + 1 ELSE i += 1 END IF WEND END SUB #ENDIF