lib/vhyl-rpg-expanded-skills.t¶
Reference index ยท View on GitHub
1 /* Include after the expanded foundation. Source: full-2008, B170, B173, B175, B230. */ 2 3 /** Authored expanded skill. Defaults are explicit [skill, penalty] pairs. 4 * A catalogue is not implied by this definition type. */ 5 class RpgExpandedSkill: RpgLiteSkill 6 defaults = [] 7 wildcard = nil 8 prerequisite = nil 9 optionalParent = nil 10 optionalApproved = nil 11 maximumDifficulty() { return 3; } 12 ; 13 14 /** Owned investment. defaultFrom explicitly selects a paid-up default basis. */ 15 class RpgExpandedTraining: RpgTraining 16 defaultFrom = nil 17 ; 18 19 /** Locate a unique training slot without creating or changing one. */ 20 rpgExpandedTrainingFor(profile, skill) 21 { 22 local found = nil; 23 foreach (local training in profile.skills) 24 if (training.skill == skill) 25 { 26 if (found != nil) rpgExpandedReject('duplicate-training'); 27 found = training; 28 } 29 return found; 30 } 31 32 /** Point cost of a relative skill level; levels below the table cost zero. */ 33 rpgExpandedSkillCost(difficulty, relative, wildcard) 34 { 35 rpgInteger(difficulty, 0, 3); rpgInteger(relative, -1000, 1000); 36 if (wildcard && difficulty != 3) rpgExpandedReject('wildcard-difficulty'); 37 local row = relative + difficulty; 38 local cost = row < 0 ? 0 : row == 0 ? 1 : row == 1 ? 2 : (row - 1) * 4; 39 return cost * (wildcard ? 3 : 1); 40 } 41 42 /** Trained level from points; nil means no purchased level. */ 43 rpgExpandedSkillLevel(attribute, difficulty, points, wildcard) 44 { 45 rpgInteger(attribute, 1, 1000); rpgInteger(difficulty, 0, 3); 46 rpgInteger(points, 0, 1000000); 47 if (wildcard && difficulty != 3) rpgExpandedReject('wildcard-difficulty'); 48 if (wildcard) points /= 3; 49 if (points == 0) return nil; 50 local relative = points >= 4 ? 2 + (points - 4) / 4 : points >= 2 ? 1 : 0; 51 return attribute + relative - difficulty; 52 } 53 54 /** Paid level only. Unpaid default chains never become training. */ 55 rpgExpandedPaidSkill(profile, skill, visited) 56 { 57 if (holds(visited, skill)) rpgExpandedReject('cyclic-paid-default'); 58 rpgExpandedValidateSkill(skill); 59 local training = rpgExpandedTrainingFor(profile, skill); 60 if (training == nil || training.points == 0) return nil; 61 local wildcard = skill.ofKind(RpgExpandedSkill) && skill.wildcard; 62 if (wildcard && !profile.rules.wildcardSkills) rpgExpandedReject('wildcard-disabled'); 63 local base = profile.attributeScore(skill.attribute); local credit = 0; 64 if (training.ofKind(RpgExpandedTraining) && training.defaultFrom != nil) 65 { 66 if (!skill.ofKind(RpgExpandedSkill) || wildcard) rpgExpandedReject('invalid-paid-default'); 67 local penalty = nil; 68 foreach (local pair in rpgExpandedDefaultEdges(profile, skill)) 69 if (pair[1] == training.defaultFrom && (penalty == nil || pair[2] > penalty)) penalty = pair[2]; 70 if (penalty == nil) rpgExpandedReject('missing-paid-default'); 71 local source = rpgExpandedPaidSkill(profile, training.defaultFrom, visited + [skill]); 72 if (source == nil) rpgExpandedReject('untrained-paid-default'); 73 credit = rpgExpandedSkillCost(skill.difficulty, source + penalty - base, nil); 74 } 75 return rpgExpandedSkillLevel(base, skill.difficulty, training.points + credit, wildcard); 76 } 77 78 /** Best purchased or legal default level. No chained untrained defaults. */ 79 rpgExpandedSkillScore(profile, skill) 80 { 81 rpgExpandedValidate(profile); 82 rpgExpandedValidateSkill(skill); 83 if (skill.ofKind(RpgExpandedSkill) && skill.wildcard) 84 { 85 if (!profile.rules.wildcardSkills) rpgExpandedReject('wildcard-disabled'); 86 if (skill.defaultPenalty != nil || skill.defaults.length() != 0) 87 rpgExpandedReject('wildcard-default'); 88 } 89 local score = rpgExpandedPaidSkill(profile, skill, []); 90 if (skill.techLevel != nil && skill.techLevel > profile.techLevel && score == nil) return nil; 91 if (profile.familiarSkills != nil && !holds(profile.familiarSkills, skill) && score == nil) return nil; 92 if (skill.defaultPenalty != nil) 93 { 94 local which = skill.defaultAttribute == nil ? skill.attribute : skill.defaultAttribute; 95 local base = profile.attributeScore(which); 96 local candidate = (base > 20 ? 20 : base) + skill.defaultPenalty; 97 if (score == nil || candidate > score) score = candidate; 98 } 99 if (skill.ofKind(RpgExpandedSkill)) 100 foreach (local pair in rpgExpandedDefaultEdges(profile, skill)) 101 { 102 if (pair.length() != 2 || pair[1] == skill) rpgExpandedReject('invalid-skill-default'); 103 rpgInteger(pair[2], -100, 0); 104 local source = rpgExpandedPaidSkill(profile, pair[1], [skill]); 105 if (source != nil && (score == nil || source + pair[2] > score)) score = source + pair[2]; 106 } 107 return score; 108 } 109 110 modify RpgExpandedProfile 111 skillScore(skill) 112 { 113 if (skill.ofKind(RpgExpandedSkill)) return rpgExpandedSkillScore(self, skill); 114 return inherited(skill); 115 } 116 ; 117 118 /** Owned technique investment. Select exactly one default basis. Caps are 119 * relative to that basis; nil maximumAboveSkill explicitly means uncapped. */ 120 class RpgExpandedTechnique: RpgExpandedContribution 121 skill = nil 122 attribute = nil 123 parentTechnique = nil 124 penalty = 0 125 maximumAboveSkill = 0 126 hard = nil 127 points = 0 128 amounts(profile) { return [self.points]; } 129 validate(profile) { return self.validateChain(profile, []); } 130 validateChain(profile, visited) 131 { 132 if (holds(visited, self)) rpgExpandedReject('cyclic-technique-default'); 133 if (self.owner != profile) rpgExpandedReject('technique-ownership'); 134 local bases = (self.skill != nil ? 1 : 0) 135 + (self.attribute != nil ? 1 : 0) + (self.parentTechnique != nil ? 1 : 0); 136 if (bases != 1) rpgExpandedReject('technique-default-basis'); 137 rpgInteger(self.penalty, -100, 0); 138 if (self.maximumAboveSkill != nil) rpgInteger(self.maximumAboveSkill, 0, 100); 139 rpgInteger(self.points, 0, 1000000); 140 if (self.skill != nil) 141 { 142 local training = rpgExpandedTrainingFor(profile, self.skill); 143 if (training == nil || training.points < 1) rpgExpandedReject('technique-prerequisite'); 144 } 145 if (self.attribute != nil) profile.attributeScore(self.attribute); 146 if (self.parentTechnique != nil) 147 { 148 if (!self.parentTechnique.ofKind(RpgExpandedTechnique)) 149 rpgExpandedReject('technique-default-basis'); 150 self.parentTechnique.validateChain(profile, visited + [self]); 151 } 152 if (self.maximumAboveSkill != nil 153 && self.points > self.maximumAboveSkill - self.penalty + (self.hard ? 1 : 0)) 154 rpgExpandedReject('technique-cap'); 155 return nil; 156 } 157 score() 158 { 159 rpgExpandedValidate(self.owner); 160 self.validate(self.owner); 161 return self.resolvedScore(); 162 } 163 /* Internal recursion after the complete default graph has been validated. */ 164 resolvedScore() 165 { 166 local base = self.parentTechnique != nil ? self.parentTechnique.resolvedScore() 167 : self.attribute != nil ? self.owner.attributeScore(self.attribute) 168 : self.owner.skillScore(self.skill); 169 if (base == nil) rpgExpandedReject('technique-prerequisite'); 170 local improvement = self.points - (self.hard && self.points > 0 ? 1 : 0); 171 return base + self.penalty + improvement; 172 } 173 ; 174 175 /** Declarative learning requirement, B169. A skill leaf requires purchased 176 * training; its optional minimum is a paid level, never an untrained default. 177 * Trait leaves describe possession, not whether a power is currently active. */ 178 class RpgExpandedPrerequisite: object 179 kind = 'skill' 180 skill = nil 181 minimum = nil 182 traitId = nil 183 traitLevel = 1 184 children = [] 185 satisfied(profile, visited) 186 { 187 if (holds(visited, self)) rpgExpandedReject('cyclic-prerequisite'); 188 if (self.kind == 'all' || self.kind == 'any') 189 { 190 if (self.children.length() == 0) rpgExpandedReject('empty-prerequisite'); 191 local all = true; local any = nil; 192 foreach (local child in self.children) 193 { 194 if (!child.ofKind(RpgExpandedPrerequisite)) rpgExpandedReject('invalid-prerequisite'); 195 local matches = child.satisfied(profile, visited + [self]); 196 if (matches) any = true; else all = nil; 197 } 198 return self.kind == 'all' ? all : any; 199 } 200 if (self.kind == 'skill') 201 { 202 if (self.skill == nil || !self.skill.ofKind(RpgSkill)) 203 rpgExpandedReject('invalid-prerequisite'); 204 if (self.minimum != nil) rpgInteger(self.minimum, 1, 1000); 205 local training = rpgExpandedTrainingFor(profile, self.skill); 206 if (training == nil || training.points < 1) return nil; 207 if (self.minimum == nil) return true; 208 local score = rpgExpandedPaidSkill(profile, self.skill, []); 209 return score != nil && score >= self.minimum; 210 } 211 if (self.kind == 'trait') 212 { 213 if (self.traitId == nil) rpgExpandedReject('invalid-prerequisite'); 214 rpgInteger(self.traitLevel, 1, 1000); 215 local trait = profile.trait(self.traitId); 216 return trait != nil && trait.level >= self.traitLevel; 217 } 218 rpgExpandedReject('invalid-prerequisite'); 219 } 220 ; 221 222 modify RpgExpandedProfile 223 skillLearningReason(skill) 224 { 225 rpgExpandedValidateSkill(skill); 226 if (skill.ofKind(RpgExpandedSkill) && skill.wildcard && !self.rules.wildcardSkills) 227 return 'wildcard-disabled'; 228 if (skill.ofKind(RpgLiteSkill) && skill.techLevel != nil) 229 { 230 rpgInteger(skill.techLevel, 0, 12); rpgInteger(self.techLevel, 0, 12); 231 if (self.iq < 6) return 'technology-learning-cognition'; 232 if (skill.techLevel > self.techLevel) 233 { 234 if (!self.creationComplete) return 'technology-starting-access'; 235 if (skill.attribute == rpgIQ) return 'technology-personal-level'; 236 } 237 } 238 if (!skill.ofKind(RpgExpandedSkill) || skill.prerequisite == nil) return nil; 239 if (!skill.prerequisite.ofKind(RpgExpandedPrerequisite)) 240 rpgExpandedReject('invalid-prerequisite'); 241 return skill.prerequisite.satisfied(self, []) ? nil : 'skill-prerequisite'; 242 } 243 skillLearningWithTeacherReason(skill, teacher) 244 { 245 local reason = self.skillLearningReason(skill); 246 if (reason != nil) return reason; 247 if (!skill.ofKind(RpgLiteSkill) || skill.techLevel == nil || skill.techLevel <= self.techLevel) 248 return nil; 249 if (teacher == nil || !teacher.ofKind(RpgProfile) || teacher == self) 250 return 'technology-teacher-required'; 251 local training = rpgExpandedTrainingFor(teacher, skill); 252 if (training == nil || training.points < 1 || teacher.skillScore(skill) == nil) 253 return 'technology-teacher-subject'; 254 return nil; 255 } 256 ; 257 258 modify RpgExpandedProfile 259 familiarities = [] 260 validateFeatures() 261 { 262 inherited(); 263 local specialties = []; 264 foreach (local training in self.skills) 265 { 266 if (training == nil || !training.ofKind(RpgTraining) || training.skill == nil) 267 rpgExpandedReject('invalid-training'); 268 local skill = training.skill; rpgExpandedValidateSkill(skill); 269 if (skill.ofKind(RpgExpandedSkill) && skill.optionalParent != nil) 270 { 271 foreach (local other in specialties) 272 if (other.optionalParent == skill.optionalParent && other.specialty == skill.specialty) 273 rpgExpandedReject('duplicate-optional-specialty'); 274 specialties = specialties + [skill]; 275 } 276 } 277 foreach (local entry in self.familiarities) 278 { 279 if (!entry.ofKind(RpgExpandedFamiliarity) || entry.owner != self) 280 rpgExpandedReject('familiarity-ownership'); 281 entry.validate(); 282 } 283 return nil; 284 } 285 ; 286 287 /** One owned skill/equipment-type familiarity, B169. A type is an authored 288 * identity shared by equivalent models, not necessarily one inventory object. */ 289 class RpgExpandedFamiliarity: object 290 owner = nil 291 skill = nil 292 equipmentType = nil 293 initial = nil 294 requiredSeconds = 28800 295 penalty = -2 296 practicedSeconds = 0 297 recognized = nil 298 recognitionAttempted = nil 299 recognitionCheck = nil 300 practiceOwner = nil 301 practiceSkill = nil 302 practiceType = nil 303 available = nil 304 validate() 305 { 306 if (self.owner == nil || !self.owner.ofKind(RpgExpandedProfile) 307 || !holds(self.owner.familiarities, self) || self.skill == nil 308 || !self.skill.ofKind(RpgSkill) || self.equipmentType == nil) 309 rpgExpandedReject('familiarity-ownership'); 310 rpgInteger(self.requiredSeconds, 1, 1000000); 311 rpgInteger(self.practicedSeconds, 0, self.requiredSeconds); 312 rpgInteger(self.penalty, -100, 0); 313 if (self.practiceOwner != nil && (self.owner != self.practiceOwner 314 || self.skill != self.practiceSkill || self.equipmentType != self.practiceType)) 315 rpgExpandedReject('familiarity-retargeted'); 316 local initialCount = 0; local copies = 0; 317 foreach (local entry in self.owner.familiarities) 318 { 319 if (!entry.ofKind(RpgExpandedFamiliarity) || entry.owner != self.owner) 320 rpgExpandedReject('familiarity-ownership'); 321 if (entry.skill == self.skill) 322 { 323 if (entry.equipmentType == self.equipmentType) copies += 1; 324 if (entry.initial) initialCount += 1; 325 } 326 } 327 if (copies != 1) rpgExpandedReject('duplicate-familiarity'); 328 local training = rpgExpandedTrainingFor(self.owner, self.skill); 329 local points = training == nil ? 0 : training.points; 330 if (initialCount > points * 2) rpgExpandedReject('initial-familiarity-limit'); 331 return nil; 332 } 333 known() { return self.initial || self.recognized || self.practicedSeconds == self.requiredSeconds; } 334 modifier() 335 { 336 self.validate(); return self.known() ? 0 : self.penalty; 337 } 338 assess(seconds) 339 { 340 self.validate(); rpgInteger(seconds, 1, 86400); 341 local value = new RpgExpandedResult(); 342 if (self.known()) { value.status = 'reused'; return value; } 343 if (!self.available) { value.reason = 'familiarity-equipment-unavailable'; return value; } 344 if (self.owner.dead || self.owner.unconscious) 345 { value.reason = 'cannot-practice'; return value; } 346 if (self.owner.skillScore(self.skill) == nil) 347 { value.reason = 'familiarity-skill-unavailable'; return value; } 348 if (seconds > self.requiredSeconds - self.practicedSeconds) 349 { value.reason = 'familiarity-excess-time'; return value; } 350 value.status = 'ready'; value.seconds = seconds; return value; 351 } 352 advance(seconds) 353 { 354 local value = self.assess(seconds); 355 if (value.status != 'ready') return value; 356 rpgSpendTime(value.seconds); 357 self.practiceOwner = self.owner; self.practiceSkill = self.skill; 358 self.practiceType = self.equipmentType; self.practicedSeconds += value.seconds; 359 value.status = 'resolved'; return value; 360 } 361 ; 362 363 /** Use through rpgLiteScore/rpgLiteCheck so the familiarity penalty is applied 364 * once, alongside (rather than replacing) other contextual modifiers. */ 365 class RpgExpandedSkillContext: RpgLiteContext 366 familiarity = nil 367 equipmentTL = nil 368 rollAttribute = nil 369 fixedBase = nil 370 scoreFor(profile) 371 { 372 if (self.rollAttribute == nil && self.fixedBase == nil) return inherited(profile); 373 if (self.skill == nil || self.rollAttribute != nil && self.fixedBase != nil) 374 rpgExpandedReject('relative-skill-context'); 375 local relative = rpgExpandedRelativeSkill(profile, self.skill); 376 if (relative == nil) return nil; 377 if (self.fixedBase != nil) 378 { 379 rpgInteger(self.fixedBase, 1, 1000); return self.fixedBase + relative; 380 } 381 return profile.attributeScore(self.rollAttribute) + relative; 382 } 383 controllingAttribute() 384 { 385 if (self.fixedBase != nil) return nil; 386 if (self.rollAttribute != nil) return self.rollAttribute; 387 return inherited(); 388 } 389 technologyModifier() 390 { 391 if (self.equipmentTL == nil) return 0; 392 if (self.skill == nil || !self.skill.ofKind(RpgLiteSkill) || self.skill.techLevel == nil) 393 rpgExpandedReject('technology-skill-required'); 394 return rpgExpandedTechnologyModifier(self.skill.techLevel, self.equipmentTL, 395 self.skill.attribute == rpgIQ); 396 } 397 unavailableReason(profile) 398 { 399 if (self.technologyModifier() == nil) return 'technology-too-advanced'; 400 return inherited(profile); 401 } 402 skillUseModifier(profile) 403 { 404 local technology = self.technologyModifier(); 405 if (technology == nil) return nil; 406 if (self.familiarity == nil) return technology; 407 if (!self.familiarity.ofKind(RpgExpandedFamiliarity) 408 || self.familiarity.owner != profile || self.familiarity.skill != self.skill) 409 rpgExpandedReject('familiarity-context'); 410 return technology + self.familiarity.modifier(); 411 } 412 ; 413 414 /** Equipment-to-skill technology difference, B168. nil means impossible, 415 * rather than an arbitrarily large numeric penalty. No familiarity is included. */ 416 rpgExpandedTechnologyModifier(skillTL, equipmentTL, cognitive) 417 { 418 rpgInteger(skillTL, 0, 12); rpgInteger(equipmentTL, 0, 12); 419 local difference = equipmentTL - skillTL; 420 if (!cognitive) return difference < 0 ? difference : -difference; 421 if (difference >= 4) return nil; 422 if (difference > 0) return -5 * difference; 423 if (difference == 0) return 0; 424 return 2 * difference + 1; 425 } 426 427 /** Create an author-approved optional specialty, B169-B170. generalSkill may 428 * already represent a required specialty, but cannot itself be optional. */ 429 rpgExpandedOptionalSpecialty(generalSkill, id, specialty, approved) 430 { 431 local skill = new RpgExpandedSkill(); 432 skill.optionalParent = generalSkill; skill.optionalApproved = approved; 433 skill.id = id; skill.specialty = specialty; 434 if (generalSkill == nil || !generalSkill.ofKind(RpgExpandedSkill)) 435 rpgExpandedReject('optional-specialty-parent'); 436 skill.attribute = generalSkill.attribute; skill.difficulty = generalSkill.difficulty - 1; 437 skill.techLevel = generalSkill.techLevel; skill.prerequisite = generalSkill.prerequisite; 438 rpgExpandedValidateSkill(skill); return skill; 439 } 440 441 /** Validate the mechanical identity without evaluating skill scores. */ 442 rpgExpandedValidateSkill(skill) 443 { 444 if (skill == nil || !skill.ofKind(RpgSkill)) rpgExpandedReject('invalid-skill'); 445 rpgInteger(skill.difficulty, 0, skill.maximumDifficulty()); 446 rpgValidateAttribute(skill.attribute); 447 if (skill.defaultPenalty != nil) rpgInteger(skill.defaultPenalty, -100, 0); 448 if (skill.ofKind(RpgExpandedSkill) && skill.wildcard) 449 { 450 if (skill.difficulty != 3) rpgExpandedReject('wildcard-difficulty'); 451 if (skill.defaultPenalty != nil || skill.defaults.length() != 0) 452 rpgExpandedReject('wildcard-default'); 453 } 454 if (!skill.ofKind(RpgExpandedSkill) || skill.optionalParent == nil) return nil; 455 local parent = skill.optionalParent; 456 if (!parent.ofKind(RpgExpandedSkill) || parent.optionalParent != nil || parent.wildcard) 457 rpgExpandedReject('optional-specialty-parent'); 458 if (!skill.optionalApproved) rpgExpandedReject('optional-specialty-approval'); 459 if (parent.attribute != rpgIQ || parent.difficulty < 1 || parent.difficulty > 3 460 || skill.attribute != rpgIQ || skill.difficulty != parent.difficulty - 1 461 || skill.techLevel != parent.techLevel || skill.wildcard) 462 rpgExpandedReject('optional-specialty-definition'); 463 if (skill.id == nil || skill.specialty == nil || skill.specialty == '') 464 rpgExpandedReject('optional-specialty-identity'); 465 if (skill.prerequisite != parent.prerequisite) 466 rpgExpandedReject('optional-specialty-prerequisite'); 467 return nil; 468 } 469 470 /** Expand the explicit B170 optional-specialty edges. This does not traverse 471 * untrained defaults: each generated edge still requires purchased training. */ 472 rpgExpandedDefaultEdges(profile, skill) 473 { 474 local edges = skill.defaults; 475 foreach (local pair in edges) 476 { 477 if (pair.length() != 2 || pair[1] == skill) rpgExpandedReject('invalid-skill-default'); 478 rpgInteger(pair[2], -100, 0); 479 } 480 foreach (local training in profile.skills) 481 { 482 local candidate = training.skill; 483 rpgExpandedValidateSkill(candidate); 484 if (candidate.ofKind(RpgExpandedSkill) && candidate.optionalParent != nil && candidate != skill) 485 { 486 if (candidate.optionalParent == skill) edges = edges + [[candidate,-2]]; 487 foreach (local pair in skill.defaults) 488 if (pair[1] == candidate.optionalParent) edges = edges + [[candidate,pair[2]-2]]; 489 } 490 } 491 return edges; 492 } 493 494 /** Relative level from the selected skill rating, B171-B172. Unavailable 495 * skills stay unavailable; this query does not purchase or improve training. */ 496 rpgExpandedRelativeSkill(profile, skill) 497 { 498 local score = profile.skillScore(skill); 499 return score == nil ? nil : score - profile.attributeScore(skill.attribute); 500 } 501 502 /** Use only for procedures that add positive relative training to strength. 503 * Ordinary strength-based skill checks instead use rollAttribute = rpgST. */ 504 rpgExpandedSkillStrength(profile, skill) 505 { 506 local relative = rpgExpandedRelativeSkill(profile, skill); 507 if (relative == nil) return nil; 508 return profile.st + (relative > 0 ? relative : 0); 509 } 510 511 /** Authored B169 recognition decision. Similar/improved/obsolete decisions must 512 * identify a known type; optional rolls require six known types of this skill. 513 * One attempted recognition is retained on the target record, even on failure. */ 514 class RpgExpandedFamiliarityRecognition: object 515 familiarity = nil 516 mode = 'roll' 517 similarTo = nil 518 approved = nil 519 assess() 520 { 521 if (self.familiarity == nil || !self.familiarity.ofKind(RpgExpandedFamiliarity)) 522 rpgExpandedReject('familiarity-recognition-target'); 523 local target = self.familiarity; target.validate(); 524 rpgExpandedValidate(target.owner); 525 if (!holds(['roll','similar','improved','obsolete'], self.mode)) 526 rpgExpandedReject('familiarity-recognition-mode'); 527 local value = new RpgExpandedResult(); 528 if (target.known() || target.recognitionAttempted) 529 { value.status = 'reused'; value.check = target.recognitionCheck; return value; } 530 if (!self.approved) { value.reason = 'familiarity-recognition-unapproved'; return value; } 531 if (!target.available) { value.reason = 'familiarity-equipment-unavailable'; return value; } 532 if (target.owner.dead || target.owner.unconscious) 533 { value.reason = 'cannot-recognize'; return value; } 534 if (self.mode == 'roll') 535 { 536 local count = 0; 537 foreach (local entry in target.owner.familiarities) 538 if (entry.skill == target.skill && entry.known()) count += 1; 539 if (count < 6) { value.reason = 'familiarity-recognition-experience'; return value; } 540 local score = target.owner.skillScore(target.skill); 541 if (score == nil || score < 3) 542 { value.reason = 'familiarity-skill-unavailable'; return value; } 543 rpgInteger(score, 3, 1000); value.check = score; 544 } 545 else 546 { 547 if (self.similarTo == nil || !self.similarTo.ofKind(RpgExpandedFamiliarity) 548 || self.similarTo == target || self.similarTo.owner != target.owner 549 || self.similarTo.skill != target.skill) 550 rpgExpandedReject('familiarity-recognition-source'); 551 self.similarTo.validate(); 552 if (!self.similarTo.known()) 553 { value.reason = 'familiarity-recognition-source-unknown'; return value; } 554 } 555 value.status = 'ready'; return value; 556 } 557 resolve() 558 { 559 local value = self.assess(); 560 if (value.status != 'ready') return value; 561 local target = self.familiarity; 562 if (self.mode == 'roll') value.check = rpgCheck(value.check, rpgRandom.roll()); 563 target.practiceOwner = target.owner; target.practiceSkill = target.skill; 564 target.practiceType = target.equipmentType; 565 target.recognitionAttempted = true; target.recognitionCheck = value.check; 566 target.recognized = self.mode != 'roll' || value.check.achieved; 567 value.status = 'resolved'; return value; 568 } 569 ;