lib/vhyl-rpg-lite-exploration.t

Reference index ยท View on GitHub

   1  /** Return [surface, modifier, combat rate, regular rate, unit]; rates are feet or rungs per minute. */
   2  rpgLiteClimbData(surface)
   3  {
   4      local rows = [['ladder-up',nil,180,60,'rungs'],['ladder-down',nil,120,60,'rungs'],
   5          ['tree',5,60,20,'feet'],['mountain',0,30,10,'feet'],['stone-wall',-3,12,4,'feet'],
   6          ['building',-3,6,2,'feet'],['rope-up',-2,60,20,'feet'],['rope-down',-1,120,30,'feet'],
   7          ['rappel',-1,720,720,'feet']];
   8      foreach (local row in rows) if (row[1] == surface) return row;
   9      throw new RpgError();
  10  }
  11  
  12  /** Calculate high-jump inches or broad-jump feet, including run-up and trained Jumping. */
  13  rpgLiteJumpDistance(profile, skill, runUp, high)
  14  {
  15      rpgInteger(runUp, 0, 10000);
  16      local movementScore = profile.basicMove(); local rating = skill == nil ? nil : profile.skillScore(skill);
  17      if (rating != nil && rating / 2 > movementScore) movementScore = rating / 2;
  18      local standing = high ? 6 * movementScore - 10 : 2 * movementScore - 3;
  19      if (standing < 0) standing = 0;
  20      local running = high ? 6 * (movementScore + runUp) - 10 : 2 * (movementScore + runUp) - 3;
  21      return running > 2 * standing ? 2 * standing : running;
  22  }
  23  
  24  /** Return lift, shove or shift capacity in hundredths of a pound. */
  25  rpgLiteLift(profile, kind, running)
  26  {
  27      local factor = kind == 'one-hand' ? 2 : kind == 'two-hands' ? 8
  28          : kind == 'shove' ? (running ? 24 : 12) : kind == 'back' ? 15 : kind == 'shift' ? 50 : 0;
  29      if (factor == 0) throw new RpgError();
  30      return profile.basicLiftHundredths() * factor;
  31  }
  32  
  33  /** Return throwing range in hundredths of a yard for weight in hundredths of a pound. */
  34  rpgLiteThrowDistance(profile, weight)
  35  {
  36      rpgInteger(weight, 1, 1000000);
  37      local lift = profile.basicLiftHundredths();
  38      if (weight > lift * 8) return 0;
  39      local table = [[5,350],[10,250],[15,200],[20,150],[40,100],[50,80],[100,60],[150,40],
  40          [200,30],[250,25],[300,20],[400,15],[600,10],[1000,6],[1200,5]];
  41      foreach (local row in table) if (weight * 100 <= lift * row[1]) return profile.st * row[2];
  42      return 0;
  43  }
  44  
  45  /** Roll crushing damage for a thrown object's weight relative to Basic Lift. */
  46  rpgLiteThrowDamage(profile, weight, maximum)
  47  {
  48      local lift = profile.basicLiftHundredths();
  49      if (weight < 1 || weight > lift * 8) throw new RpgError();
  50      local damage = rpgLiteDamageDice(profile.st, nil); local dice = damage[1]; local adds = damage[2];
  51      if (weight * 8 <= lift) adds -= 2 * dice;
  52      else if (weight * 4 <= lift) adds -= dice;
  53      else if (weight * 2 <= lift) adds += 0;
  54      else if (weight <= lift) adds += dice;
  55      else if (weight <= lift * 2) adds += 0;
  56      else if (weight <= lift * 4) adds -= (dice + 1) / 2;
  57      else adds -= dice;
  58      return rpgLiteRollDamage(dice, adds, 'cr', maximum);
  59  }
  60  
  61  /** Look up impact yards per second, rounding height to the next supplied table row. */
  62  rpgLiteFallVelocity(yards)
  63  {
  64      rpgInteger(yards, 0, 1000);
  65      if (yards == 0) return 0;
  66      local table = [[1,5],[2,7],[3,8],[4,9],[5,10],[10,15],[15,18],[20,21],
  67          [30,26],[40,30],[50,33],[75,40],[100,47],[1000,146]];
  68      foreach (local row in table) if (yards <= row[1]) return row[2];
  69      throw new RpgError();
  70  }
  71  
  72  /** Roll collision damage from HP and yards per second. */
  73  rpgLiteCollisionDamage(hp, velocity)
  74  {
  75      rpgInteger(hp, 1, 10000); rpgInteger(velocity, 0, 10000);
  76      if (velocity == 0) return 0;
  77      local dice = hp * velocity / 100;
  78      return rpgLiteRollDamage(dice < 1 ? 1 : dice, dice < 1 ? -2 : 0, 'cr', nil);
  79  }
  80  
  81  /** Apply falling injury, breakable-obstacle cap, armor and eligible Catfall. */
  82  rpgLiteFall(profile, yards, freeToTwist, obstacleHP, obstacleDR)
  83  {
  84      local catfall = profile.level('catfall') > 0 && freeToTwist;
  85      if (catfall) { yards -= 5; if (yards < 0) yards = 0; }
  86      local damage = rpgLiteCollisionDamage(profile.maxHP, rpgLiteFallVelocity(yards));
  87      if (obstacleHP != nil && damage > obstacleHP + obstacleDR) damage = obstacleHP + obstacleDR;
  88      if (catfall && rpgLiteAttributeCheck(profile, rpgDX, 'catfall', 0).achieved) damage /= 2;
  89      return rpgLiteWound(profile, rpgLitePenetration(damage, rpgLiteArmor(profile), 1, 'cr', nil));
  90  }
  91  
  92  /** Calculate daily mileage in hundredths, with terrain, weather and successful Hiking. */
  93  rpgLiteHikingMilesHundredths(profile, terrain, weatherPercent, succeeded)
  94  {
  95      rpgInteger(weatherPercent, 0, 100);
  96      local multiplier = terrain == 'very-bad' ? 20 : terrain == 'bad' ? 50
  97          : terrain == 'average' ? 100 : terrain == 'good' ? 125 : 0;
  98      if (multiplier == 0) throw new RpgError();
  99      return profile.move() * 10 * multiplier * weatherPercent / 100 * (succeeded ? 120 : 100) / 100;
 100  }
 101  
 102  /** Persistent travel/exertion activity with periodic climbing, running and swimming checks. */
 103  class RpgLiteJourney: object
 104      profile = nil
 105      skill = nil
 106      kind = 'hiking'
 107      elapsed = 0
 108      clock = 0
 109      swimmingInterval = 300
 110      struggling = nil
 111      intentional = true
 112      entered = nil
 113      secured = nil
 114      combatPace = nil
 115      surfaceModifier = 0
 116      surface = 'mountain'
 117      swimClock = 0
 118      fallen = nil
 119      checks = []
 120      advance(seconds)
 121      {
 122          rpgInteger(seconds, 0, 86400);
 123          if (!holds(['hiking','sprint','paced','swimming','swim-fast','climbing','overload'], self.kind)) throw new RpgError();
 124          local result = new RpgLiteHealthResult();
 125          if (self.profile.dead || self.profile.unconscious || self.fallen)
 126          { result.status = 'blocked'; return result; }
 127          if (!rpgLiteCanAct(self.profile, nil)) { result.status = 'blocked'; return result; }
 128          if (self.skill == nil && holds(['climbing','swimming','swim-fast'], self.kind))
 129              self.skill = rpgLiteTraining(self.profile, self.kind == 'climbing' ? 'climbing' : 'swimming', nil, nil);
 130          self.profile.restClock = 0;
 131          if (!self.entered && holds(['swimming','swim-fast','climbing'], self.kind))
 132          {
 133              self.entered = true;
 134              if (self.kind == 'climbing') self.check(0, result);
 135              else self.afloat(self.intentional ? 3 : 0, result);
 136          }
 137          for (local elapsed = 0; elapsed < seconds && !self.profile.dead && !self.profile.unconscious && !self.fallen; elapsed += 1)
 138          {
 139              self.clock += 1; self.elapsed += 1; result.seconds += 1;
 140              if (self.kind == 'swim-fast')
 141              {
 142                  self.swimClock += 1;
 143                  if (self.swimClock >= self.swimmingInterval)
 144                  { self.swimClock = 0; self.afloat(0, result); }
 145              }
 146              local interval = self.kind == 'hiking' ? 3600 : self.kind == 'sprint' ? 15
 147                  : self.kind == 'paced' || self.kind == 'swim-fast' ? 60
 148                  : self.kind == 'swimming' ? self.swimmingInterval : self.kind == 'climbing' ? 300 : 1;
 149              if (self.clock >= interval)
 150              {
 151                  self.clock = 0;
 152                  if (self.kind == 'hiking') result.fatigue += rpgLiteFatigue(self.profile, 1 + self.profile.encumbrance(), 'exertion').fatigue;
 153                  else if (self.kind == 'overload')
 154                  {
 155                      if (self.profile.loadHundredths() > self.profile.basicLiftHundredths() * 10)
 156                          result.fatigue += rpgLiteFatigue(self.profile, 1, 'exertion').fatigue;
 157                  }
 158                  else if (self.kind == 'swimming') self.afloat(0, result);
 159                  else self.check(0, result);
 160              }
 161              if (self.struggling && self.profile.fp() <= 0
 162                  && !rpgLiteAttributeCheck(self.profile, rpgWill, 'drowning', 0).achieved) self.profile.unconscious = true;
 163          }
 164          rpgSpendTime(result.seconds); return result;
 165      }
 166      afloat(modifier, result)
 167      {
 168          local context = new RpgLiteContext(); context.skill = self.skill; context.modifier = modifier;
 169          result.check = rpgLiteCheck(self.profile, context); self.checks = self.checks + [result.check];
 170          if (!result.check.achieved)
 171          { result.fatigue += rpgLiteFatigue(self.profile, 1, 'exertion').fatigue; self.struggling = true; self.swimmingInterval = 5; }
 172          else { self.swimmingInterval = self.struggling ? 60 : 300; self.struggling = nil; }
 173          return nil;
 174      }
 175      check(modifier, result)
 176      {
 177          local context = new RpgLiteContext(); context.skill = self.skill; context.modifier = modifier;
 178          if (self.kind == 'sprint' || self.kind == 'paced') { context.skill = nil; context.attribute = rpgHT; }
 179          if (self.kind == 'swim-fast' && (self.skill == nil || self.profile.skillScore(self.skill) < self.profile.ht))
 180          { context.skill = nil; context.attribute = rpgHT; }
 181          if (self.kind == 'climbing')
 182          {
 183              local data = rpgLiteClimbData(self.surface);
 184              if (data[2] == nil) return nil;
 185              context.modifier += self.surfaceModifier + data[2];
 186          }
 187          result.check = rpgLiteCheck(self.profile, context); self.checks = self.checks + [result.check];
 188          if (self.kind == 'climbing')
 189          {
 190              if (self.combatPace) result.fatigue += rpgLiteFatigue(self.profile, 1, 'exertion').fatigue;
 191              if (!result.check.achieved) self.fallen = !self.secured || result.check.critical == 'failure';
 192          }
 193          else if (!result.check.achieved)
 194          {
 195              result.fatigue += rpgLiteFatigue(self.profile, 1, 'exertion').fatigue;
 196              if (self.kind == 'swimming') { self.struggling = true; self.swimmingInterval = 5; }
 197          }
 198          else if (self.kind == 'swimming')
 199          {
 200              self.swimmingInterval = self.struggling ? 60 : 300; self.struggling = nil;
 201          }
 202          return nil;
 203      }
 204      speedHundredths()
 205      {
 206          if (self.kind == 'climbing')
 207          {
 208              local data = rpgLiteClimbData(self.surface);
 209              return data[self.combatPace ? 3 : 4] * 100 / (data[5] == 'feet' ? 180 : 60);
 210          }
 211          if (self.kind == 'swimming' || self.kind == 'swim-fast')
 212          {
 213              local movementScore = self.profile.basicMove() / 5;
 214              if (movementScore < 1) movementScore = 1;
 215              return movementScore * 100 / (self.profile.fp() * 3 <= self.profile.maxEnergy ? 2 : 1);
 216          }
 217          local movementScore = self.profile.move() * 100;
 218          if (self.profile.fp() * 3 == self.profile.maxEnergy) movementScore /= 2;
 219          if (self.kind == 'sprint' && self.elapsed >= 1 && self.profile.posture == 'standing') movementScore = movementScore * 120 / 100;
 220          if (self.kind == 'paced') movementScore = movementScore * 60 / 100;
 221          return movementScore;
 222      }
 223  ;
 224  
 225  /** Timed or opposed skill activity with repeated attempts and skill-specific outcomes. */
 226  class RpgLiteSkillTask: object
 227      profile = nil
 228      skill = nil
 229      context = nil
 230      opponentScore = nil
 231      attempts = 0
 232      certified = nil
 233      reactionModifier = 0
 234      companions = []
 235      resolve()
 236      {
 237          if (self.skill == nil) throw new RpgError();
 238          if (self.context == nil) { self.context = new RpgLiteContext(); self.context.skill = self.skill; }
 239          local score = rpgLiteScore(self.profile, self.context);
 240          if (score == nil || score < 3) return nil;
 241          if (self.skill.id == 'forgery' && self.certified) return rpgCheck(score, 3);
 242          local ownRoll = rpgRandom.roll();
 243          if (self.context.reroll(self.profile,rpgCheck(score,ownRoll))) ownRoll = rpgLiteReroll(self.profile,ownRoll,3,nil);
 244          local result = rpgEvaluate(score, ownRoll, self.opponentScore,
 245              self.opponentScore == nil ? nil : rpgRandom.roll(), 0);
 246          local seconds = self.skill.id == 'escape' ? (self.attempts == 0 ? 60 : 600)
 247              : self.skill.id == 'lockpicking' ? 60 : self.skill.id == 'interrogation' ? 300
 248              : self.skill.id == 'scrounging' ? 3600 : self.skill.id == 'shadowing' ? 600
 249              : self.skill.id == 'tracking' ? (self.attempts == 0 ? 0 : 900) : self.skill.id == 'survival' ? 86400 : self.skill.interval;
 250          if (self.skill.id == 'lockpicking' && result.achieved)
 251          { seconds -= result.margin * 5; if (seconds < 0) seconds = 0; }
 252          if (self.skill.id == 'forgery' && self.attempts == 0 && result.critical == 'success') self.certified = true;
 253          if (self.skill.id == 'carousing') self.reactionModifier = result.achieved ? 2 : -2;
 254          if (self.skill.id == 'survival' && !result.achieved)
 255          {
 256              rpgLiteWound(self.profile, rpgLiteRollDamage(2, -4, 'cr', nil));
 257              foreach (local companion in self.companions) rpgLiteWound(companion, rpgLiteRollDamage(2, -4, 'cr', nil));
 258          }
 259          rpgSpendTime(seconds); self.attempts += 1; return result;
 260      }
 261  ;