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 ;