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  ;