lib/vhyl-rpg-lite-combat.t

Reference index ยท View on GitHub

   1  /** Persistent encounter participant, ready weapons, shield, defenses and aim/concentration. */
   2  class RpgLiteFighter: object
   3      profile = nil
   4      weapon = nil
   5      modeIndex = 1
   6      offWeapon = nil
   7      offModeIndex = 1
   8      shield = nil
   9      shieldSkill = nil
  10      unarmedSkill = nil
  11      maneuver = 'nothing'
  12      defenseOption = 'dodge'
  13      blocks = 0
  14      parries = 0
  15      offParries = 0
  16      attacked = nil
  17      participated = nil
  18      aimSeconds = 0
  19      aimTarget = nil
  20      aimWeapon = nil
  21      concentration = 0
  22      initiativeTie = 0
  23      freeHands = 2
  24      mode() { return self.weapon.modes[self.modeIndex]; }
  25  ;
  26  
  27  /** Authored tactical facts, availability and consequence hooks for one combat turn. */
  28  class RpgLiteManeuver: object
  29      kind = 'nothing'
  30      option = nil
  31      target = nil
  32      distance = 1
  33      speed = 0
  34      sizeModifier = 0
  35      rangeModifier = nil
  36      movement = 0
  37      posture = nil
  38      offHand = nil
  39      hands = 2
  40      braced = nil
  41      boots = nil
  42      aware = true
  43      fromBehind = nil
  44      shieldSide = true
  45      visible = true
  46      immobilized = nil
  47      inReach = true
  48      targetThreatens = true
  49      humanTarget = true
  50      deadly = nil
  51      faceVisible = true
  52      preflight = nil
  53      attackModifier = 0
  54      defenseModifier = 0
  55      darkness = 0
  56      someLight = true
  57      defense = 'dodge'
  58      secondDefense = 'parry'
  59      available(fighter) { return nil; }
  60      apply(fighter, result) { return nil; }
  61      reroll(profile, kind, roll) { return nil; }
  62  ;
  63  
  64  /** Attack, defense, damage and injury receipt; blocked results include a reason. */
  65  class RpgLiteCombatResult: object
  66      status = 'blocked'
  67      reason = nil
  68      attack = nil
  69      defenses = []
  70      damage = 0
  71      injury = 0
  72      strikes = []
  73      outcome = nil
  74      counterInjury = 0
  75  ;
  76  
  77  /** Look up the printed Lite range table through 100 yards; no missing Basic Set rows are inferred. */
  78  rpgLiteRangeModifier(yards)
  79  {
  80      rpgInteger(yards, 0, 100);
  81      local table = [[2,0],[3,-1],[5,-2],[10,-4],[15,-5],[20,-6],[30,-7],[50,-8],[70,-9],[100,-10]];
  82      foreach (local row in table) if (yards <= row[1]) return row[2];
  83      throw new RpgError();
  84  }
  85  
  86  /** Return the melee attack or defense adjustment for a named posture. */
  87  rpgLitePostureModifier(posture, defense)
  88  {
  89      if (posture == 'standing') return 0;
  90      if (posture == 'crouching') return defense ? 0 : -2;
  91      if (posture == 'sitting' || posture == 'kneeling') return -2;
  92      if (posture == 'lying' || posture == 'crawling') return defense ? -3 : -4;
  93      throw new RpgError();
  94  }
  95  
  96  /** Roll an active defense, preserving automatic success on 3 or 4 even below score 3. */
  97  rpgLiteDefenseCheck(score)
  98  {
  99      rpgInteger(score, -1000, 1000);
 100      local roll = rpgRandom.roll();
 101      local value = rpgCheck(score < 3 ? 3 : score, roll);
 102      value.score = score; value.margin = score - roll;
 103      value.achieved = roll <= 4 || roll < 17 && roll <= score;
 104      value.outcome = value.achieved ? 'success' : 'setback'; return value;
 105  }
 106  
 107  /** Find the skill identity used by the fighter's selected attack mode. */
 108  rpgLiteCombatSkill(fighter, mode)
 109  {
 110      local profile = fighter.profile;
 111      local techLevel = mode.skillId == 'guns' || mode.skillId == 'gunner' ? fighter.weapon.techLevel : nil;
 112      return rpgLiteTraining(profile, mode.skillId, techLevel, mode.specialty);
 113  }
 114  
 115  /** Return the highest applicable DX, Brawling or Karate rating. */
 116  rpgLiteUnarmedScore(fighter)
 117  {
 118      local score = fighter.profile.dx;
 119      foreach (local id in ['brawling', 'karate'])
 120      {
 121          local skill = rpgLiteTraining(fighter.profile, id, nil, nil);
 122          local value = fighter.profile.skillScore(skill);
 123          if (value != nil && value > score) score = value;
 124      }
 125      return score;
 126  }
 127  
 128  /** Query defense eligibility and score without rolling or consuming a defense. */
 129  rpgLiteDefenseScore(fighter, attacker, request, kind)
 130  {
 131      local profile = fighter.profile; local incoming = attacker.mode();
 132      if (!request.aware || request.fromBehind || request.immobilized || profile.dead || profile.unconscious
 133          || fighter.maneuver == 'all-out-attack') return nil;
 134      local score = 0;
 135      if (kind == 'dodge')
 136      {
 137          score = profile.basicSpeedQuarters() / 4 + 3 - profile.encumbrance();
 138          if (score < 1) score = 1;
 139          score += profile.level('enhanced-dodge');
 140          if (profile.hp() * 3 < profile.maxHP) score = (score + 1) / 2;
 141          if (profile.fp() * 3 < profile.maxEnergy) score = (score + 1) / 2;
 142      }
 143      else if (kind == 'block')
 144      {
 145          if (fighter.blocks > 0 || fighter.shield == nil || !fighter.shield.ready
 146              || incoming.ranged && incoming.base == 'fixed') return nil;
 147          local skill = fighter.shieldSkill == nil ? rpgLiteTraining(profile, 'shield', nil, nil) : fighter.shieldSkill;
 148          local rating = profile.skillScore(skill);
 149          if (rating == nil) return nil;
 150          score = 3 + rating / 2 + profile.level('enhanced-block');
 151          if (incoming.skillId == 'flail') score -= 2;
 152      }
 153      else if (kind == 'parry' || kind == 'parry-other')
 154      {
 155          local otherHand = kind == 'parry-other';
 156          local parryWeapon = otherHand ? fighter.offWeapon : fighter.weapon;
 157          local parryMode = parryWeapon == nil ? nil : parryWeapon.modes[otherHand ? fighter.offModeIndex : fighter.modeIndex];
 158          if ((otherHand ? fighter.offParries : fighter.parries) > 0 || fighter.maneuver == 'move-attack'
 159              || incoming.ranged && !incoming.thrown && !request.inReach) return nil;
 160          local unarmed = parryWeapon == nil || parryWeapon.id == 'punch' || parryWeapon.id == 'kick';
 161          if (unarmed)
 162          {
 163              if (fighter.freeHands < (otherHand && fighter.weapon == nil ? 2 : 1)) return nil;
 164              score = 3 + rpgLiteUnarmedScore(fighter) / 2;
 165              local karate = profile.skillScore(rpgLiteTraining(profile, 'karate', nil, nil));
 166              if (attacker.weapon.id != 'punch' && attacker.weapon.id != 'kick'
 167                  && incoming.base != 'thrust' && karate == nil) score -= 3;
 168              if (karate != nil && karate >= rpgLiteUnarmedScore(fighter)) score -= profile.encumbrance();
 169          }
 170          else
 171          {
 172              if (!parryWeapon.ready || parryMode.ranged
 173                  || parryMode.skillId == 'axe-mace' && fighter.attacked) return nil;
 174              local rating = profile.skillScore(rpgLiteTraining(profile, parryMode.skillId, nil, parryMode.specialty));
 175              if (rating == nil) return nil;
 176              if (otherHand && profile.level('ambidexterity') == 0) rating -= 4;
 177              score = 3 + rating / 2;
 178              if (parryWeapon.id == 'quarterstaff') score += 2;
 179              if (parryMode.skillId == 'knife') score -= 1;
 180          }
 181          score += profile.level('enhanced-parry-all');
 182          local enhanced = profile.trait('enhanced-parry');
 183          if (enhanced != nil && enhanced.scope == (unarmed ? 'unarmed' : parryMode.skillId)) score += 1;
 184          if (incoming.thrown) score -= attacker.weapon.weight <= 100 ? 2 : 1;
 185          if (incoming.skillId == 'flail') score -= 4;
 186      }
 187      else throw new RpgError();
 188      score += profile.level('combat-reflexes') + request.defenseModifier;
 189      if (fighter.shield != nil && fighter.shield.ready && request.shieldSide) score += fighter.shield.db;
 190      score += rpgLitePostureModifier(profile.posture, true);
 191      if (profile.stunned || profile.mentalStun) score -= 4;
 192      if (fighter.maneuver == 'all-out-defense' && (fighter.defenseOption == kind
 193          || kind == 'parry-other' && fighter.defenseOption == 'parry')) score += 2;
 194      if (!request.visible) score -= 4;
 195      return score;
 196  }
 197  
 198  /** Lose aim and check whether ongoing concentration survives distraction. */
 199  rpgLiteDistract(fighter)
 200  {
 201      fighter.aimSeconds = 0;
 202      if (fighter.concentration > 0 && !rpgLiteAttributeCheck(fighter.profile, rpgWill, 'concentration', -3).achieved)
 203          fighter.concentration = 0;
 204      return nil;
 205  }
 206  
 207  /** Resolve one legal strike; normally invoked by the battle turn scheduler. */
 208  rpgLiteStrike(attacker, defender, request)
 209  {
 210      local result = new RpgLiteCombatResult();
 211      local profile = attacker.profile; local weapon = attacker.weapon;
 212      if (weapon == nil || !weapon.ready) { result.reason = 'weapon-not-ready'; return result; }
 213      local mode = attacker.mode();
 214      if (profile.stunned || profile.mentalStun || profile.unconscious || profile.dead)
 215      { result.reason = 'unable-to-attack'; return result; }
 216      if (!mode.ranged && !request.inReach) { result.reason = 'out-of-reach'; return result; }
 217      if (request.deadly && !request.targetThreatens && profile.level('cannot-harm-innocents') > 0)
 218      { result.reason = 'cannot-harm-innocents'; return result; }
 219      if (mode.hands == 2 && request.hands < 2 && (mode.requiresTwoHands || profile.st * 2 < mode.minimumST * 3)
 220          || request.hands == 2 && attacker.shield != nil && attacker.shield.ready)
 221      { result.reason = 'hands-unavailable'; return result; }
 222      local strength = weapon.id == 'crossbow' ? weapon.weaponST : profile.st;
 223      local maximum = mode.rangeScale == 10 ? mode.maximumRange * strength / 10 : mode.maximumRange;
 224      local half = mode.rangeScale == 10 ? mode.halfRange * strength / 10 : mode.halfRange;
 225      if (mode.ranged && request.distance > maximum) { result.reason = 'out-of-range'; return result; }
 226      if (mode.ranged && !mode.thrown && weapon.ammunition < 1) { result.reason = 'empty'; return result; }
 227      local context = new RpgLiteContext(); context.kind = 'attack';
 228      context.skill = rpgLiteCombatSkill(attacker, mode); context.offHand = request.offHand;
 229      context.darkness = request.darkness; context.someLight = request.someLight;
 230      context.modifier = request.attackModifier;
 231      local unarmed = holds(['punch','kick','brass-knuckles'], weapon.id);
 232      if (unarmed || weapon.id == 'blackjack')
 233      {
 234          context.skill = nil; context.attribute = rpgDX;
 235          local rating = weapon.id == 'blackjack' ? profile.skillScore(rpgLiteTraining(profile, 'brawling', nil, nil)) : rpgLiteUnarmedScore(attacker);
 236          context.modifier += rating == nil || rating < profile.dx ? 0 : rating - profile.dx;
 237      }
 238      if (weapon.id == 'kick') context.modifier -= 2;
 239      local effectiveST = profile.fp() * 3 < profile.maxEnergy ? (profile.st + 1) / 2 : profile.st;
 240      local minimumST = weapon.bipod && profile.posture == 'lying' ? (mode.minimumST * 2 + 2) / 3 : mode.minimumST;
 241      if (effectiveST < minimumST) context.modifier -= minimumST - effectiveST;
 242      if (mode.ranged)
 243      {
 244          context.modifier += request.sizeModifier + (request.rangeModifier == nil
 245              ? rpgLiteRangeModifier(request.distance + request.speed) : request.rangeModifier);
 246          if (defender.profile.posture != 'standing') context.modifier -= 2;
 247          if (attacker.aimSeconds > 0 && attacker.aimTarget == defender && attacker.aimWeapon == weapon
 248              && request.kind != 'move-attack')
 249          {
 250              context.modifier += mode.acc + (attacker.aimSeconds >= 3 ? 2 : attacker.aimSeconds == 2 ? 1 : 0);
 251              context.modifier += attacker.aimSeconds < weapon.scopeBonus ? attacker.aimSeconds : weapon.scopeBonus;
 252              if ((request.braced || weapon.bipod && profile.posture == 'lying' || mode.hands == 1 && request.hands == 2)
 253                  && (mode.base == 'fixed' || weapon.id == 'crossbow')) context.modifier += 1;
 254          }
 255      }
 256      else
 257      {
 258          context.modifier += rpgLitePostureModifier(profile.posture, nil);
 259          if (attacker.shield != nil && attacker.shield.ready && attacker.shield.db == 3) context.modifier -= 2;
 260      }
 261      if (request.kind == 'all-out-attack' && (request.option == 'determined' || mode.ranged)) context.modifier += mode.ranged ? 1 : 4;
 262      if (request.kind == 'move-attack') context.modifier -= 4;
 263      if (profile.level('reluctant-killer') > 0 && request.deadly && request.humanTarget)
 264          context.modifier -= request.faceVisible ? 4 : 2;
 265      local score = rpgLiteScore(profile, context);
 266      if (score == nil) { result.reason = 'training-required'; return result; }
 267      if (request.kind == 'move-attack' && !mode.ranged && score > 9) score = 9;
 268      if (score < 3) { result.reason = 'too-difficult'; return result; }
 269      if (request.preflight) { result.status = 'ready'; return result; }
 270      result.status = 'resolved'; result.attack = rpgCheck(score, rpgRandom.roll());
 271      if (request.reroll(profile, 'attack', result.attack.roll))
 272          result.attack = rpgCheck(score, rpgLiteReroll(profile, result.attack.roll, 3, nil));
 273      else if (request.reroll(defender.profile, 'enemy-attack', result.attack.roll))
 274          result.attack = rpgCheck(score, rpgLiteReroll(defender.profile, result.attack.roll, 3, true));
 275      attacker.attacked = true; attacker.participated = true;
 276      if (mode.ranged && !mode.thrown) weapon.ammunition -= 1;
 277      if (mode.thrown || mode.unready && profile.st * 2 < mode.minimumST * 3
 278          || mode.hands == 2 && request.hands == 1 && profile.st < mode.minimumST * (mode.unready ? 3 : 2)) weapon.ready = nil;
 279      attacker.aimSeconds = 0;
 280      if (!result.attack.achieved)
 281      {
 282          result.outcome = 'miss';
 283          if (weapon.id == 'kick' && !rpgLiteAttributeCheck(profile, rpgDX, 'keep-feet', 0).achieved) profile.posture = 'lying';
 284          return result;
 285      }
 286      if (result.attack.critical != 'success')
 287      {
 288          local choices = [request.defense];
 289          if (defender.maneuver == 'all-out-defense' && defender.defenseOption == 'double'
 290              && request.secondDefense != request.defense) choices = choices + [request.secondDefense];
 291          foreach (local kind in choices)
 292          {
 293              local defense = rpgLiteDefenseScore(defender, attacker, request, kind);
 294              if (defense == nil) continue;
 295              if (!request.visible && kind != 'dodge'
 296                  && !rpgLiteAttributeCheck(defender.profile, rpgPerception, 'hearing', -2).achieved) continue;
 297              if (defender.maneuver == 'nothing' && defender.profile.hp() <= 0 && !rpgLiteCanAct(defender.profile, nil)) break;
 298              if (kind == 'block') defender.blocks += 1;
 299              if (kind == 'parry') defender.parries += 1;
 300              if (kind == 'parry-other') defender.offParries += 1;
 301              defender.participated = true; rpgLiteDistract(defender);
 302              local check = rpgLiteDefenseCheck(defense);
 303              if (request.reroll(defender.profile, 'defense', check.roll))
 304              {
 305                  local rolled = rpgLiteReroll(defender.profile, check.roll, 3, nil);
 306                  check = rpgCheck(defense < 3 ? 3 : defense, rolled);
 307                  check.score = defense; check.margin = defense - rolled;
 308                  check.achieved = rolled <= 4 || rolled < 17 && rolled <= defense;
 309              }
 310              result.defenses = result.defenses + [check];
 311              if (check.achieved)
 312              {
 313                  result.outcome = 'defended';
 314                  if (kind == 'parry' && unarmed && defender.weapon != nil
 315                      && !holds(['punch','kick','brass-knuckles'], defender.weapon.id))
 316                  {
 317                      local counter = new RpgLiteContext(); counter.kind = 'attack';
 318                      counter.skill = rpgLiteCombatSkill(defender, defender.mode());
 319                      if (profile.skillScore(rpgLiteTraining(profile, 'karate', nil, nil)) != nil) counter.modifier = -4;
 320                      if (rpgLiteCheck(defender.profile, counter).achieved)
 321                      {
 322                          local retaliation = defender.mode();
 323                          local damage = rpgLiteDamageDice(defender.profile.st, retaliation.base == 'swing');
 324                          local rolled = rpgLiteRollDamage(damage[1], damage[2] + retaliation.adds, retaliation.damageType, nil);
 325                          result.counterInjury = rpgLiteWound(profile, rpgLitePenetration(rolled, rpgLiteArmor(profile), 1, retaliation.damageType, nil)).injury;
 326                      }
 327                  }
 328                  return result;
 329              }
 330          }
 331      }
 332      local dice = mode.dice; local adds = mode.adds;
 333      if (mode.base != 'fixed')
 334      {
 335          if (mode.minimumST > 0 && !mode.ranged && strength > mode.minimumST * 3) strength = mode.minimumST * 3;
 336          local damage = rpgLiteDamageDice(strength, mode.base == 'swing'); dice = damage[1]; adds += damage[2];
 337          if (unarmed)
 338          {
 339              local karate = profile.skillScore(rpgLiteTraining(profile, 'karate', nil, nil));
 340              if (karate != nil && karate >= profile.dx) adds += dice * (karate > profile.dx ? 2 : 1);
 341              if (weapon.id == 'kick' && request.boots) adds += 1;
 342          }
 343          if (request.kind == 'all-out-attack' && request.option == 'strong' && !mode.ranged) adds += dice > 2 ? dice : 2;
 344      }
 345      local rawDamage = 0;
 346      for (local index = 0; index < dice; index += 1) rawDamage += result.attack.roll == 3 ? 6 : rpgRandom.die();
 347      if (result.attack.roll != 3 && request.reroll(profile, 'damage', rawDamage))
 348          rawDamage = rpgLiteReroll(profile, rawDamage, dice, true);
 349      result.damage = rawDamage + adds;
 350      if (result.damage < (mode.damageType == 'cr' ? 0 : 1)) result.damage = mode.damageType == 'cr' ? 0 : 1;
 351      result.injury = rpgLitePenetration(result.damage, rpgLiteArmor(defender.profile), mode.armorDivisor,
 352          mode.damageType, mode.ranged && half > 0 && request.distance >= half);
 353      rpgLiteWound(defender.profile, result.injury);
 354      if (result.injury > 0) rpgLiteDistract(defender);
 355      result.outcome = 'hit'; return result;
 356  }
 357  
 358  /** One-second turn scheduler with stable initiative, maneuvers and end-of-battle fatigue. */
 359  class RpgLiteBattle: object
 360      fighters = []
 361      order = []
 362      index = 1
 363      seconds = 0
 364      active = nil
 365      begin()
 366      {
 367          if (self.active || self.order.length() > 0 || self.fighters.length() < 2) throw new RpgError();
 368          foreach (local fighter in self.fighters)
 369          {
 370              if (holds(self.order, fighter)) throw new RpgError();
 371              fighter.initiativeTie = rpgRandom.roll();
 372              local updated = []; local inserted = nil;
 373              foreach (local other in self.order)
 374              {
 375                  local ownSpeed = fighter.profile.basicSpeedQuarters(); local otherSpeed = other.profile.basicSpeedQuarters();
 376                  if (!inserted && (ownSpeed > otherSpeed || ownSpeed == otherSpeed &&
 377                      (fighter.profile.dx > other.profile.dx || fighter.profile.dx == other.profile.dx
 378                      && fighter.initiativeTie > other.initiativeTie)))
 379                  { updated = updated + [fighter]; inserted = true; }
 380                  updated = updated + [other];
 381              }
 382              if (!inserted) updated = updated + [fighter];
 383              self.order = updated;
 384          }
 385          self.active = true; return self.order[1];
 386      }
 387      current() { return self.active ? self.order[self.index] : nil; }
 388      step(fighter, request)
 389      {
 390          local result = new RpgLiteCombatResult();
 391          if (fighter != self.current()) { result.reason = 'not-your-turn'; return result; }
 392          if (!holds(['nothing','move','posture','aim','attack','all-out-attack','move-attack',
 393              'all-out-defense','concentrate','ready'], request.kind)) throw new RpgError();
 394          local limit = holds(['move','move-attack','all-out-defense'], request.kind) ? fighter.profile.move()
 395              : request.kind == 'all-out-attack' ? fighter.profile.move() / 2 : 1;
 396          if (request.movement < 0 || request.movement > limit
 397              || request.kind == 'all-out-defense' && request.option != 'dodge' && request.movement > 1)
 398          { result.reason = 'movement-limit'; return result; }
 399          result.reason = request.available(fighter); if (result.reason != nil) return result;
 400          if (request.target != nil && !holds(self.fighters, request.target)) throw new RpgError();
 401          if ((fighter.profile.stunned || fighter.profile.mentalStun) && request.kind != 'nothing')
 402          { result.reason = 'stunned'; return result; }
 403          if (request.kind == 'posture' && (request.posture == nil
 404              || fighter.profile.posture == 'lying' && request.posture == 'standing'))
 405          { result.reason = 'invalid-posture-change'; return result; }
 406          if (request.kind == 'posture') rpgLitePostureModifier(request.posture, nil);
 407          if (holds(['attack','all-out-attack','move-attack','aim'], request.kind) && request.target == nil)
 408              throw new RpgError();
 409          if (request.kind == 'all-out-attack' && !holds(['determined','double','strong'], request.option)
 410              || request.kind == 'all-out-defense' && !holds(['dodge','block','parry','double'], request.option))
 411              throw new RpgError();
 412          if (request.kind == 'aim' && (fighter.weapon == nil || !fighter.weapon.ready || !fighter.mode().ranged || !request.visible))
 413          { result.reason = 'cannot-aim'; return result; }
 414          if (request.kind == 'ready' && fighter.weapon == nil)
 415          { result.reason = 'no-weapon'; return result; }
 416          if (request.kind == 'ready' && request.option == 'reload'
 417              && rpgLiteReloadSeconds(fighter.profile, fighter.weapon, fighter.mode()) == nil)
 418          { result.reason = 'cannot-reload'; return result; }
 419          if (holds(['attack','all-out-attack','move-attack'], request.kind))
 420          {
 421              request.preflight = true; result = rpgLiteStrike(fighter, request.target, request); request.preflight = nil;
 422              if (result.status != 'ready') return result;
 423          }
 424          if (!rpgLiteCanAct(fighter.profile, request.kind == 'nothing'))
 425          { result.status = 'resolved'; result.outcome = 'incapacitated'; self.advance(); return result; }
 426          fighter.blocks = 0; fighter.parries = 0; fighter.offParries = 0; fighter.attacked = nil;
 427          if (request.kind != 'concentrate') fighter.concentration = 0;
 428          fighter.maneuver = request.kind; fighter.defenseOption = request.option;
 429          if (holds(['attack','all-out-attack','move-attack'], request.kind))
 430          {
 431              result = rpgLiteStrike(fighter, request.target, request);
 432              if (result.status != 'resolved') return result;
 433              if (request.kind == 'all-out-attack' && request.option == 'double' && !fighter.mode().ranged
 434                  && (fighter.weapon.ready || fighter.offWeapon != nil && fighter.offWeapon.ready)
 435                  && !fighter.profile.stunned && !fighter.profile.unconscious && !request.target.profile.dead)
 436              {
 437                  local primary = fighter.weapon; local modeIndex = fighter.modeIndex; local offHand = request.offHand;
 438                  if (fighter.offWeapon != nil && fighter.offWeapon.ready)
 439                  { fighter.weapon = fighter.offWeapon; fighter.modeIndex = fighter.offModeIndex; request.offHand = true; }
 440                  result.strikes = result.strikes + [rpgLiteStrike(fighter, request.target, request)];
 441                  fighter.weapon = primary; fighter.modeIndex = modeIndex; request.offHand = offHand;
 442              }
 443          }
 444          else
 445          {
 446              result.status = 'resolved'; result.outcome = request.kind;
 447              if (request.kind == 'aim')
 448              {
 449                  if (fighter.aimTarget != request.target || fighter.aimWeapon != fighter.weapon) fighter.aimSeconds = 0;
 450                  fighter.aimTarget = request.target; fighter.aimWeapon = fighter.weapon; fighter.aimSeconds += 1;
 451              }
 452              else fighter.aimSeconds = 0;
 453              if (request.kind == 'posture') fighter.profile.posture = request.posture;
 454              if (request.kind == 'concentrate') fighter.concentration += 1;
 455              if (request.kind == 'ready')
 456              {
 457                  if (request.option == 'reload') rpgLiteReload(fighter.profile, fighter.weapon, fighter.mode());
 458                  else fighter.weapon.ready = true;
 459              }
 460              if (request.kind == 'nothing' && fighter.profile.fearSeconds == 0 && (fighter.profile.stunned || fighter.profile.mentalStun))
 461                  rpgLiteRecoverStun(fighter.profile);
 462          }
 463          fighter.profile.shock = fighter.profile.level('high-pain-threshold') > 0 ? 0
 464              : result.counterInjury > 4 ? 4 : result.counterInjury;
 465          request.apply(fighter, result); self.advance(); return result;
 466      }
 467      advance()
 468      {
 469          self.index += 1;
 470          if (self.index > self.order.length())
 471          {
 472              self.index = 1; self.seconds += 1; rpgSpendTime(1);
 473              foreach (local fighter in self.order) rpgLiteHealthTime(fighter.profile, 1);
 474          }
 475          return nil;
 476      }
 477      finish()
 478      {
 479          if (!self.active) return nil;
 480          self.active = nil;
 481          if (self.seconds > 10)
 482              foreach (local fighter in self.fighters)
 483                  if (fighter.participated)
 484                  {
 485                      local cost = 1 + fighter.profile.encumbrance();
 486                      if (fighter.weapon != nil && fighter.mode().minimumST > fighter.profile.st) cost += 1;
 487                      rpgLiteFatigue(fighter.profile, cost, 'battle');
 488                  }
 489          return true;
 490      }
 491  ;