lib/vhyl-rpg-lite-health.t¶
Reference index ยท View on GitHub
1 /** Structured injury, healing, fatigue and elapsed-time result with any resistance checks. */ 2 class RpgLiteHealthResult: object 3 status = 'resolved' 4 injury = 0 5 healed = 0 6 fatigue = 0 7 check = nil 8 checks = [] 9 seconds = 0 10 ; 11 12 /** Apply injury, shock, major wounds, death thresholds and mortal wounds. */ 13 rpgLiteWound(profile, amount) 14 { 15 rpgInteger(amount, 0, 100000); 16 local result = new RpgLiteHealthResult(); 17 if (profile.dead || amount == 0) return result; 18 local oldHP = profile.hp(); profile.injury += amount; result.injury = amount; 19 profile.woundSerial += 1; 20 profile.aidUsed = nil; 21 if (profile.level('high-pain-threshold') == 0) 22 { profile.shock += amount; if (profile.shock > 4) profile.shock = 4; } 23 if (amount * 2 > profile.maxHP && !profile.unconscious) 24 { 25 result.check = rpgLiteAttributeCheck(profile, rpgHT, 'major-wound', 0); 26 result.checks = result.checks + [result.check]; 27 if (!result.check.achieved) 28 { 29 profile.stunned = true; profile.posture = 'lying'; 30 foreach (local item in profile.equipment) if (!item.worn) item.ready = nil; 31 if (result.check.margin <= -5 || result.check.critical == 'failure') profile.unconscious = true; 32 } 33 } 34 for (local multiple = 1; multiple <= 4 && !profile.dead; multiple += 1) 35 if (oldHP > -multiple * profile.maxHP && profile.hp() <= -multiple * profile.maxHP) 36 { 37 result.check = rpgLiteAttributeCheck(profile, rpgHT, 'death', 0); 38 result.checks = result.checks + [result.check]; 39 if (!result.check.achieved) 40 { 41 if (profile.mortal || result.check.margin < -2) profile.dead = true; 42 else { profile.mortal = true; profile.unconscious = true; } 43 } 44 else if (result.check.roll > profile.ht) 45 { profile.apparentDeath = true; profile.unconscious = true; } 46 } 47 if (profile.hp() <= -5 * profile.maxHP) profile.dead = true; 48 if (profile.dead) profile.unconscious = true; 49 return result; 50 } 51 52 /** Apply fatigue and overflow injury, preserving recovery restrictions for named causes. */ 53 rpgLiteFatigue(profile, amount, cause) 54 { 55 rpgInteger(amount, 0, 10000); 56 local result = new RpgLiteHealthResult(); 57 if (profile.dead) return result; 58 if (holds(['exertion','battle','thirst'], cause) && amount > 0) amount += profile.heatExtra; 59 local remaining = amount; 60 while (remaining > 0) 61 { 62 if (profile.fp() <= 0) result.injury += rpgLiteWound(profile, 1).injury; 63 if (profile.fp() > -profile.maxEnergy) 64 { 65 profile.fatigue += 1; result.fatigue += 1; 66 if (cause == 'hunger') profile.hunger += 1; 67 if (cause == 'thirst') profile.thirst += 1; 68 if (cause == 'sleep') profile.sleepDebt += 1; 69 } 70 remaining -= 1; 71 } 72 if (profile.fp() <= -profile.maxEnergy) { profile.unconscious = true; profile.fatigueCollapse = true; } 73 return result; 74 } 75 76 /** Check consciousness and effort when necessary; a failed check may incapacitate the character. */ 77 rpgLiteCanAct(profile, doNothing) 78 { 79 if (profile.dead || profile.unconscious || profile.mortal) return nil; 80 if (doNothing) return true; 81 if (profile.fp() <= 0) 82 { 83 local check = rpgLiteAttributeCheck(profile, rpgWill, 'effort', 0); 84 if (!check.achieved) { profile.unconscious = true; profile.fatigueCollapse = true; return nil; } 85 } 86 if (profile.hp() <= 0) 87 { 88 local check = rpgLiteAttributeCheck(profile, rpgHT, 'consciousness', -(-profile.hp() / profile.maxHP)); 89 if (!check.achieved) { profile.unconscious = true; return nil; } 90 } 91 return true; 92 } 93 94 /** Apply direct healing and the associated mortal-wound and awakening checks. */ 95 rpgLiteHeal(profile, amount) 96 { 97 rpgInteger(amount, 0, 10000); 98 if (profile.dead) return 0; 99 if (amount > profile.injury) amount = profile.injury; 100 profile.injury -= amount; 101 if (amount > 0 && profile.mortal) 102 { 103 local check = rpgLiteAttributeCheck(profile, rpgHT, 'mortal-recovery', 0); 104 if (check.achieved) 105 { 106 profile.mortal = nil; 107 if (!rpgLiteAttributeCheck(profile, rpgHT, 'permanent-health', 0).achieved && profile.ht > 1) profile.ht -= 1; 108 } 109 } 110 if (amount > 0 && profile.unconscious && !profile.mortal && profile.fp() > 0 111 && rpgLiteAttributeCheck(profile, rpgHT, 'wake', 0).achieved) 112 { profile.unconscious = nil; profile.apparentDeath = nil; profile.needsMedicalHelp = nil; } 113 return amount; 114 } 115 116 /** Persistent first-aid session with supplies, bandaging and TL-dependent treatment time. */ 117 class RpgLiteTreatment: object 118 patient = nil 119 healer = nil 120 skill = nil 121 techLevel = 3 122 elapsed = 0 123 bandaged = nil 124 completed = nil 125 modifier = 0 126 supplies = 1 127 bandageHealing = 0 128 advance(seconds) 129 { 130 rpgInteger(seconds, 1, 86400); 131 local result = new RpgLiteHealthResult(); 132 if (self.completed || self.patient.aidUsed || self.patient.dead 133 || self.healer.dead || self.healer.unconscious || self.healer.stunned || self.supplies < 1) 134 { result.status = 'blocked'; return result; } 135 rpgInteger(self.techLevel, 0, 12); 136 local extra = self.techLevel <= 4 ? 1800 : self.techLevel <= 7 ? 1200 : 600; 137 local remaining = extra + 60 - self.elapsed; 138 if (seconds > remaining) seconds = remaining; 139 self.elapsed += seconds; rpgSpendTime(seconds); result.seconds = seconds; 140 if (self.elapsed >= 60 && !self.bandaged) 141 { 142 self.bandaged = true; 143 if (self.patient.bandagedSerial != self.patient.woundSerial) 144 { 145 self.bandageHealing = rpgLiteHeal(self.patient, 1); result.healed += self.bandageHealing; 146 self.patient.bandagedSerial = self.patient.woundSerial; 147 } 148 } 149 if (self.elapsed < extra + 60) return result; 150 local context = new RpgLiteContext(); 151 context.skill = self.skill == nil ? rpgLiteTraining(self.healer, 'first-aid', self.techLevel, nil) : self.skill; 152 context.modifier = self.modifier; 153 result.check = rpgLiteCheck(self.healer, context); self.completed = true; self.supplies -= 1; 154 if (result.check.critical == 'failure') result.injury = rpgLiteWound(self.patient, 2 + self.bandageHealing).injury; 155 else if (result.check.achieved) 156 { 157 local bonus = self.techLevel <= 1 ? -4 : self.techLevel <= 3 ? -3 158 : self.techLevel <= 5 ? -2 : self.techLevel <= 7 ? -1 : self.techLevel == 8 ? 0 : 1; 159 local total = (result.check.critical == 'success' ? 6 : rpgRandom.die()) + bonus; 160 if (total > 1) result.healed += rpgLiteHeal(self.patient, total - 1); 161 } 162 self.patient.aidUsed = true; return result; 163 } 164 ; 165 166 /** Attempt end-of-turn stun recovery; fixed Fright durations cannot be shortened. */ 167 rpgLiteRecoverStun(profile) 168 { 169 if ((!profile.stunned && !profile.mentalStun) || profile.unconscious || profile.dead || profile.fearSeconds > 0) return nil; 170 local check = rpgLiteAttributeCheck(profile, profile.mentalStun ? rpgIQ : rpgHT, 171 profile.mentalStun ? 'mental-stun' : 'stun', 0); 172 if (check.achieved) { profile.stunned = nil; profile.mentalStun = nil; } 173 return check; 174 } 175 176 /** Advance quiet rest or sleep and eligible recovery; consumes the stated game time. */ 177 rpgLiteRest(profile, seconds, food, water, sleeping, healingAllowed) 178 { 179 rpgInteger(seconds, 1, 86400); 180 local result = new RpgLiteHealthResult(); result.seconds = seconds; 181 if (profile.dead) { result.status = 'blocked'; return result; } 182 rpgSpendTime(seconds); 183 for (local elapsed = 0; elapsed < seconds && (profile.stunned || profile.mentalStun) 184 && !profile.unconscious && profile.fearSeconds == 0; elapsed += 1) rpgLiteRecoverStun(profile); 185 profile.shock = 0; 186 if (food) profile.hunger = 0; 187 if (water) 188 { 189 profile.waterRestClock += seconds; 190 if (profile.waterRestClock >= 86400) 191 { profile.fatigue -= profile.thirst; result.fatigue -= profile.thirst; profile.thirst = 0; profile.waterRestClock = 0; } 192 } 193 else profile.waterRestClock = 0; 194 if (sleeping) 195 { 196 profile.sleepClock += seconds; 197 if (profile.sleepClock >= 28800) { profile.awakeClock = 0; profile.sleepCheckClock = 0; profile.sleepPenalty = nil; } 198 while (profile.sleepDebt > 0 && profile.sleepClock >= (profile.sleepRecovered ? 3600 : 28800)) 199 { 200 profile.sleepClock -= profile.sleepRecovered ? 3600 : 28800; 201 profile.sleepRecovered = true; profile.sleepDebt -= 1; profile.fatigue -= 1; 202 } 203 } 204 else { profile.sleepClock = 0; profile.sleepRecovered = nil; } 205 profile.restClock += seconds; 206 local locked = profile.hunger + profile.thirst + profile.sleepDebt + profile.temperatureFatigue; 207 while (profile.restClock >= 600) 208 { 209 profile.restClock -= 600; 210 if (profile.fatigue > locked) { profile.fatigue -= 1; result.fatigue -= 1; } 211 } 212 if (profile.fatigue == locked) profile.restClock = 0; 213 if (food && healingAllowed) 214 { 215 profile.healingClock += seconds; 216 while (profile.healingClock >= 86400) 217 { 218 profile.healingClock -= 86400; 219 result.check = rpgLiteAttributeCheck(profile, rpgHT, 'healing', 0); 220 if (result.check.achieved && profile.injury > profile.diseaseInjury + profile.temperatureInjury) 221 result.healed += rpgLiteHeal(profile, 1); 222 } 223 } 224 rpgLiteHealthTime(profile, seconds); 225 return result; 226 } 227 228 /** Advance condition clocks without charging game time; compose with one activity clock. */ 229 rpgLiteHealthTime(profile, seconds) 230 { 231 if (profile.dead) return nil; 232 profile.conditionTime(seconds); 233 if (profile.fatigueCollapse && profile.fp() > 0 && !profile.mortal) 234 { profile.unconscious = nil; profile.fatigueCollapse = nil; } 235 if (profile.fearSeconds > 0) 236 { 237 profile.fearSeconds -= seconds; 238 if (profile.fearSeconds <= 0) 239 { profile.fearSeconds = 0; profile.stunned = nil; if (profile.hp() > 0 && profile.fp() > 0) profile.unconscious = nil; } 240 } 241 if (profile.mortal) 242 { 243 profile.mortalClock += seconds; 244 while (profile.mortalClock >= 1800 && !profile.dead && profile.mortal) 245 { 246 profile.mortalClock -= 1800; 247 local check = rpgLiteAttributeCheck(profile, rpgHT, 'mortal-wound', 0); 248 if (!check.achieved) profile.dead = true; 249 else if (check.critical == 'success') 250 { 251 profile.mortal = nil; 252 if (!rpgLiteAttributeCheck(profile, rpgHT, 'permanent-health', 0).achieved && profile.ht > 1) profile.ht -= 1; 253 } 254 } 255 } 256 if (profile.unconscious && !profile.mortal && !profile.dead && profile.fearSeconds == 0) 257 { 258 profile.unconsciousClock += seconds; 259 if (profile.fp() > 0 && profile.hp() > 0 && profile.unconsciousClock >= 900) 260 { profile.unconscious = nil; profile.apparentDeath = nil; } 261 else if (profile.fp() > 0 && profile.hp() <= 0) 262 { 263 local interval = profile.hp() > -profile.maxHP ? 3600 : 43200; 264 while (profile.unconsciousClock >= interval && profile.unconscious && !profile.dead) 265 { 266 profile.unconsciousClock -= interval; 267 local check = rpgLiteAttributeCheck(profile, rpgHT, 'wake', 0); 268 if (check.achieved && !profile.needsMedicalHelp) profile.unconscious = nil; 269 else if (profile.hp() <= -profile.maxHP) 270 { 271 if (profile.needsMedicalHelp && !check.achieved) profile.dead = true; 272 profile.needsMedicalHelp = true; 273 } 274 } 275 } 276 } 277 if (!profile.unconscious) profile.unconsciousClock = 0; 278 return nil; 279 } 280 281 /** Resolve fear and set the Lite stun or fainting duration on failure. */ 282 rpgLiteFright(profile, modifier) 283 { 284 local result = rpgLiteAttributeCheck(profile, rpgWill, 'fright', modifier); 285 if (!result.achieved) 286 { 287 if (result.critical == 'failure') 288 { profile.fearSeconds = (-result.margin + rpgRandom.die()) * 60; profile.unconscious = true; } 289 else { profile.fearSeconds = -result.margin + rpgRandom.die() + rpgRandom.die(); profile.stunned = true; } 290 } 291 return result; 292 } 293 294 modify RpgLiteProfile 295 sleepClock = 0 296 sleepRecovered = nil 297 needsMedicalHelp = nil 298 fatigueCollapse = nil 299 woundSerial = 0 300 bandagedSerial = -1 301 waterRestClock = 0 302 awakeClock = 0 303 sleepCheckClock = 0 304 sleepPenalty = nil 305 burningPenalty = 0 306 diseaseInjury = 0 307 temperatureInjury = 0 308 temperatureFatigue = 0 309 heatExtra = 0 310 ; 311 312 /** Authored poison or disease parameters: resistance, delay, cycle, damage and recovery modifier. */ 313 class RpgLiteHazard: object 314 kind = 'poison' 315 resistanceModifier = 0 316 delay = 0 317 interval = 1 318 cycles = 1 319 dice = 1 320 adds = 0 321 fatigueDamage = nil 322 resistible = true 323 recoveryModifier = 0 324 ; 325 326 /** Persistent exposure timeline; partial intervals and disease recovery locks survive saves. */ 327 class RpgLiteExposure: object 328 profile = nil 329 hazard = nil 330 elapsed = 0 331 remaining = 0 332 started = nil 333 finished = nil 334 lockedInjury = 0 335 advance(seconds) 336 { 337 rpgInteger(seconds, 0, 86400); 338 local result = new RpgLiteHealthResult(); 339 if (self.finished || self.profile.dead) return result; 340 rpgInteger(self.hazard.interval, 1, 31536000); rpgInteger(self.hazard.cycles, 1, 10000); 341 rpgInteger(self.hazard.delay, 0, 31536000); 342 if (!self.started) 343 { 344 self.remaining = self.hazard.cycles; self.started = true; 345 if (self.hazard.kind == 'disease' && self.hazard.resistible) 346 { 347 result.check = rpgLiteAttributeCheck(self.profile, rpgHT, 'disease', self.hazard.resistanceModifier); 348 if (result.check.achieved) { self.finished = true; return result; } 349 } 350 } 351 self.elapsed += seconds; 352 local due = self.hazardDelayDone ? self.hazard.interval : self.hazard.delay; 353 while (self.elapsed >= due && !self.finished) 354 { 355 self.elapsed -= due; 356 result.check = self.hazard.resistible && (self.hazard.kind != 'disease' || self.hazardDelayDone) 357 ? rpgLiteAttributeCheck(self.profile, rpgHT, self.hazard.kind, 358 self.hazard.resistanceModifier + (self.hazard.kind == 'disease' ? self.hazard.recoveryModifier : 0)) : nil; 359 if (self.hazard.kind == 'disease' && result.check != nil && result.check.achieved) 360 { self.finished = true; break; } 361 if (result.check == nil || !result.check.achieved) 362 { 363 local amount = rpgLiteRollDamage(self.hazard.dice, self.hazard.adds, 'cr', nil); 364 if (self.hazard.fatigueDamage) result.fatigue += rpgLiteFatigue(self.profile, amount, self.hazard.kind).fatigue; 365 else 366 { 367 local injury = rpgLiteWound(self.profile, amount).injury; result.injury += injury; 368 if (self.hazard.kind == 'disease') 369 { self.lockedInjury += injury; self.profile.diseaseInjury += injury; } 370 } 371 } 372 self.remaining -= 1; 373 if (self.remaining == 0 || self.profile.dead) self.finished = true; 374 due = self.hazard.interval; 375 self.hazardDelayDone = true; 376 } 377 if (self.finished && self.lockedInjury > 0) 378 { self.profile.diseaseInjury -= self.lockedInjury; self.lockedInjury = 0; } 379 return result; 380 } 381 hazardDelayDone = nil 382 ; 383 384 /** Apply seconds without air, or resume clean air and check lasting injury. */ 385 rpgLiteSuffocate(profile, seconds, cleanAir) 386 { 387 rpgInteger(seconds, 0, 86400); 388 if (cleanAir) 389 { 390 if (profile.airClock > 120 && !rpgLiteAttributeCheck(profile, rpgHT, 'brain-injury', 0).achieved && profile.iq > 1) 391 profile.iq -= 1; 392 profile.airClock = 0; return nil; 393 } 394 for (local elapsed = 0; elapsed < seconds && !profile.dead; elapsed += 1) 395 { 396 profile.airClock += 1; rpgLiteFatigue(profile, 1, 'air'); 397 if (profile.fp() <= 0 && !profile.unconscious 398 && !rpgLiteAttributeCheck(profile, rpgWill, 'air', 0).achieved) 399 { profile.unconscious = true; profile.fatigueCollapse = true; } 400 if (profile.airClock >= 240) { profile.dead = true; profile.unconscious = true; } 401 } 402 return nil; 403 } 404 405 /** Persistent heat/cold exposure and shelter-dependent recovery restrictions. */ 406 class RpgLiteClimate: object 407 profile = nil 408 fahrenheit = 70 409 wind = 0 410 clothing = 0 411 wet = nil 412 active = true 413 sheltered = nil 414 elapsed = 0 415 advance(seconds) 416 { 417 rpgInteger(seconds, 0, 86400); 418 self.profile.heatExtra = self.fahrenheit >= 121 ? 2 : self.fahrenheit >= 91 ? 1 : 0; 419 if (self.sheltered || self.fahrenheit >= 35 && self.fahrenheit <= 80) 420 { 421 self.elapsed = 0; self.profile.temperatureFatigue = 0; self.profile.temperatureInjury = 0; 422 if (self.sheltered) self.profile.heatExtra = 0; 423 return 0; 424 } 425 if (self.fahrenheit > 80 && !self.active) return 0; 426 local interval = self.fahrenheit < 35 ? (self.wind >= 30 ? 600 : self.wind >= 10 ? 900 : 1800) : 1800; 427 local modifier = self.fahrenheit < 35 ? self.clothing - (self.wet ? 5 : 0) 428 - (self.fahrenheit < 0 ? -self.fahrenheit / 10 : 0) : 0; 429 local total = 0; self.elapsed += seconds; 430 while (self.elapsed >= interval) 431 { 432 self.elapsed -= interval; 433 local check = rpgLiteAttributeCheck(self.profile, rpgHT, 'temperature', modifier); 434 if (!check.achieved) 435 { 436 local amount = self.fahrenheit > 80 && check.critical == 'failure' ? rpgRandom.die() : 1; 437 local damage = rpgLiteFatigue(self.profile, amount, 'temperature'); total += damage.fatigue; 438 self.profile.temperatureFatigue += damage.fatigue; self.profile.temperatureInjury += damage.injury; 439 } 440 } 441 return total; 442 } 443 ; 444 445 /** Advance food, water and waking-time needs using the day's supplied meals and quarts. */ 446 class RpgLiteNeeds: object 447 profile = nil 448 waterQuarts = 2 449 requiredQuarts = 2 450 meals = 3 451 active = true 452 elapsed = 0 453 advance(seconds) 454 { 455 rpgInteger(seconds, 0, 86400); rpgInteger(self.meals, 0, 3); 456 rpgInteger(self.waterQuarts, 0, 100); rpgInteger(self.requiredQuarts, 1, 100); 457 for (local index = 0; index < seconds && !self.profile.dead; index += 1) 458 { 459 self.elapsed += 1; self.profile.awakeClock += 1; 460 if (self.elapsed % 28800 == 0 && self.waterQuarts < self.requiredQuarts) 461 rpgLiteFatigue(self.profile, 1, 'thirst'); 462 if (self.elapsed >= 86400) 463 { 464 self.elapsed = 0; rpgLiteFatigue(self.profile, 3 - self.meals, 'hunger'); 465 if (self.waterQuarts < 1) 466 { rpgLiteFatigue(self.profile, 1, 'thirst'); rpgLiteWound(self.profile, 1); } 467 } 468 if (self.profile.awakeClock >= 57600 && (self.profile.awakeClock - 57600) % 14400 == 0) 469 rpgLiteFatigue(self.profile, 1, 'sleep'); 470 local interval = self.profile.sleepDebt > 0 && self.profile.fp() * 3 < self.profile.maxEnergy 471 ? (self.active ? 7200 : 1800) : self.profile.sleepDebt * 2 >= self.profile.maxEnergy && !self.active ? 7200 : 0; 472 if (interval > 0) 473 { 474 self.profile.sleepCheckClock += 1; 475 if (self.profile.sleepCheckClock >= interval) 476 { 477 self.profile.sleepCheckClock = 0; 478 if (!rpgLiteAttributeCheck(self.profile, rpgWill, 'sleep', 0).achieved) 479 { self.profile.unconscious = true; return nil; } 480 self.profile.sleepPenalty = true; 481 } 482 } 483 else self.profile.sleepCheckClock = 0; 484 } 485 return nil; 486 } 487 ; 488 489 /** Track burning clothes, distraction, damage and extinguishing maneuvers. */ 490 class RpgLiteFire: object 491 profile = nil 492 burning = 0 493 extinguishProgress = 0 494 ignite(basicDamage) 495 { 496 if (basicDamage >= 10) self.burning = 2; 497 else if (basicDamage >= 3 && self.burning == 0) self.burning = 1; 498 self.profile.burningPenalty = self.burning == 2 ? 3 : self.burning == 1 ? 2 : 0; 499 return self.burning; 500 } 501 tick(seconds) 502 { 503 rpgInteger(seconds, 0, 86400); 504 for (local elapsed = 0; elapsed < seconds && self.burning > 0 && !self.profile.dead; elapsed += 1) 505 { 506 local damage = rpgLiteRollDamage(1, self.burning == 1 ? -4 : -1, 'burn', nil); 507 self.profile.burningPenalty = damage > rpgLiteArmor(self.profile) ? (self.burning == 1 ? 2 : 3) : 0; 508 rpgLiteWound(self.profile, rpgLitePenetration(damage, rpgLiteArmor(self.profile), 1, 'burn', nil)); 509 } 510 return nil; 511 } 512 extinguish(water) 513 { 514 if (water) { self.burning = 0; self.extinguishProgress = 0; self.profile.burningPenalty = 0; return true; } 515 self.extinguishProgress += 1; 516 if (self.extinguishProgress < (self.burning == 2 ? 3 : 1)) return nil; 517 self.extinguishProgress = 0; 518 if (rpgLiteAttributeCheck(self.profile, rpgDX, 'extinguish', 0).achieved) self.burning = 0; 519 if (self.burning == 0) self.profile.burningPenalty = 0; 520 return self.burning == 0; 521 } 522 ;