Skip to content

Commit

Permalink
Merge pull request #26 from brunns/veterancy
Browse files Browse the repository at this point in the history
Add veterancy skill bonuses.
  • Loading branch information
jimstorch authored Mar 7, 2024
2 parents afc94fa + e9a343a commit c3f5e34
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 24 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ in each of the following professions:

## Customising

### Professions

(Edit [`data/professions.json`](data/professions.json) to alter the professions generated, or the number of characters
generated per profession.)

Expand All @@ -51,6 +53,19 @@ Pre-built examples include [`data/professions-fbi.json`](data/professions-fbi.js
[`data/professions-cia.json`](data/professions-cia.json), [`data/professions-dea.json`](data/professions-dea.json), and
[`data/professions-socom.json`](data/professions-socom.json).

### Veterans

If desired, veteran Delta Green agent characters can be generated with the `--veterancy` flag. These characters will
have gained skill levels due to experience, but may also have suffered some decreases in physical characteristics, and
may also suffer some Damaged Veterans effects (from page 38 of the [AH](https://shop.arcdream.com/products/delta-green-agents-handbook)). Minimum and maximum ages can be set
with the `-a` and `-A` flags respectively.

If you'd prefer to get the veterancy experience checks and stat losses, but not the Damaged Veterans effects, use the
`--no-damaged` flag. This flag has no effect if veterancy is not enabled. This is probably the right thing to do if you
need veterans who are not DG agents.

## Credits

The following character sheet images were graciously provided by Simeon Cogswell, designer for Delta Green:
* Character Sheet NO BACKGROUND.pdf
* Character Sheet NO BACKGROUND BACK.jpg
Expand Down Expand Up @@ -94,6 +109,14 @@ Generate standard characters.
./generator.py
```

### generate-dg-veterans

Generate cells D-Z of Delta Green veterans.

```sh
for cell in {D..Z} ; do ./generator.py --type agent --count 3 --output "$cell Cell.pdf" --veterancy ; done
```

### generate-soldiers

Generate a group of Green Berets.
Expand Down
4 changes: 4 additions & 0 deletions data/distinguishing-features.csv
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ constitution,13,16,Perfect health
constitution,13,16,Resilient
constitution,13,16,Resistant
constitution,13,16,Robust
constitution,13,16,Never gets sick
constitution,17,18,Indefatigable
constitution,17,18,Tough
constitution,3,4,Ailing
Expand Down Expand Up @@ -66,6 +67,8 @@ intelligence,13,16,Ingenious
intelligence,13,16,Perceptive
intelligence,13,16,Quick witted
intelligence,13,16,Sharp
intelligence,13,16,Smart
intelligence,13,16,Clever
intelligence,17,18,Brilliant
intelligence,17,18,Genius
intelligence,3,4,Foolish
Expand Down Expand Up @@ -108,3 +111,4 @@ strength,3,4,Feeble
strength,3,4,Wasted
strength,5,8,Puny
strength,5,8,Weak
strength,5,8,Out of shape
222 changes: 198 additions & 24 deletions generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
from dataclasses import dataclass
from datetime import datetime
from itertools import islice, cycle, chain
from random import randint, shuffle, choice, sample
from math import floor
from random import randint, shuffle, choice, sample, choices
from textwrap import shorten, wrap
from typing import List, Any, Dict, Tuple

Expand Down Expand Up @@ -64,7 +65,9 @@ def main():
label_override=options.label,
employer_override=options.employer,
min_age=options.min_age,
max_age=options.max_age
max_age=options.max_age,
veterancy=options.veterancy,
damaged=options.damaged,
)
if options.equip:
c.equip(profession.get("equipment-kit", None))
Expand All @@ -79,6 +82,9 @@ def main():


class Need2KnowCharacter(object):
PHYSICAL_STATS = ["strength", "constitution", "dexterity"]
STATS = PHYSICAL_STATS + ["intelligence", "power", "charisma"]

stat_pools = [
[13, 13, 12, 12, 11, 11],
[15, 14, 12, 11, 10, 10],
Expand Down Expand Up @@ -157,7 +163,17 @@ class Need2KnowCharacter(object):
"language1",
]

def __init__(self, data, sex, profession, label_override=None, employer_override=None, min_age=24, max_age=55):
def __init__(self,
data,
sex,
profession,
label_override=None,
employer_override=None,
min_age=24,
max_age=55,
veterancy=True,
damaged=True,
):
self.data = data
self.profession = profession
self.sex = sex
Expand All @@ -172,10 +188,14 @@ def __init__(self, data, sex, profession, label_override=None, employer_override
).__next__
)

self.bonus_skills = []

self.generate_demographics(label_override, employer_override, min_age, max_age)
self.generate_stats()
self.generate_derived_attributes()
self.generate_skills()
if veterancy:
self.veterancy(damaged)
self.generate_derived_attributes()

def generate_demographics(self, label_override, employer_override, min_age, max_age):
if self.sex == "male":
Expand All @@ -195,26 +215,29 @@ def generate_demographics(self, label_override, employer_override, min_age, max_
if e
)
self.d["nationality"] = "(U.S.A.) " + choice(self.data.towns)
self.d["age"] = "%d (%s %d)" % (randint(min_age, max_age), choice(MONTHS), (randint(1, 28)))
self.age = randint(min_age, max_age)
self.d["age"] = "%d (%s %d)" % (self.age, choice(MONTHS), (randint(1, 28)))

def generate_stats(self):
rolled = [[sum(sorted([randint(1, 6) for _ in range(4)])[1:]) for _ in range(6)]]
pool = choice(self.stat_pools + rolled)
shuffle(pool)
for score, stat in zip(
pool, ["strength", "constitution", "dexterity", "intelligence", "power", "charisma"]
):
for score, stat in zip(pool, self.STATS):
self.d[stat] = score
self.d[f"{stat}_x5"] = score * 5
self.d[f"{stat}_distinguishing"] = self.distinguishing(stat, score)

def generate_derived_attributes(self):
self.d["hitpoints"] = int(round((self.d["strength"] + self.d["constitution"]) / 2.0))
self.d["willpower"] = self.d["power"]
self.d["sanity"] = self.d["power"] * 5
self.d["breaking point"] = self.d["power"] * 4
self.d["sanity"] = (self.d["power"] * 5) - getattr(self, "san_loss", 0)
self.d["breaking point"] = self.d["sanity"] - self.d["power"]
self.damage_bonus = ((self.d["strength"] - 1) >> 2) - 2
self.d["damage bonus"] = "DB=%d" % self.damage_bonus
for stat in self.STATS:
score = self.d[stat]
self.d[f"{stat}_x5"] = score * 5
self.d[f"{stat}_distinguishing"] = self.distinguishing(stat, score)
self.d["violence"] = " ".join("X" for _ in range(getattr(self, "adapted_to_violence", 0)))
self.d["helplessness"] = " ".join("X" for _ in range(getattr(self, "adapted_to_helplessness", 0)))

def generate_skills(self):
# Default skills
Expand All @@ -231,23 +254,128 @@ def generate_skills(self):
self.d[f"bond{i}"] = self.d["charisma"]

# Bonus skills
self.generate_bonus_skills(self.profession)
self.generate_bonus_skills()

def generate_bonus_skills(self, profession):
bonus_skills = self.potential_bonus_skills(profession)
def generate_bonus_skills(self):
potential_bonus_skills = [
s
for s in self.profession["skills"].get("bonus", [])
if randint(1, 100) <= SUGGESTED_BONUS_CHANCE
] + sample(self.ALL_BONUS, len(self.ALL_BONUS))
self.apply_bonuses(potential_bonus_skills, 8, 80)

def apply_bonuses(self, potential_bonus_skills, number_to_boost, max_level):
bonuses_applied = 0
while bonuses_applied < 8:
skill = bonus_skills.pop(0)
while bonuses_applied < number_to_boost:
skill = potential_bonus_skills.pop(0)
boosted = self.d.get(skill, 0) + 20
if boosted <= 80:
if boosted <= max_level:
self.d[skill] = boosted
bonuses_applied += 1
self.bonus_skills.append(skill)
logger.debug("%s, boosted %s to %s", self, skill, boosted)
else:
logger.info(
logger.debug(
"%s, Skipped boost - %s already at %s", self, skill, self.d.get(skill, 0)
)

def veterancy(self, damaged):
self.veterancy_skill_boosts()
self.veterancy_stat_losses()
if damaged:
self.damaged_veteran_changes()

@staticmethod
def skill_checks_at_age(age, earned_at_start=4, start_age=25, halve_rate=10):
return earned_at_start * (1 / 2 ** ((age - start_age) / halve_rate))

def veterancy_skill_boosts(self):
skills_to_check = set(list(self.profession['skills']['fixed'].keys()) +
list(self.profession['skills'].get('possible', {}).keys()) +
# list(self.DEFAULT_SKILLS.keys()) +
self.bonus_skills)
skill_checks = floor(sum(self.skill_checks_at_age(y) for y in range(25, self.age + 1)))
for skill in skills_to_check:
if isinstance(self.d.get(skill, 0), int) and self.d.get(skill, 0) > 0:
original = self.d[skill]
for _ in range(skill_checks):
current = self.d[skill]
roll = randint(1, 100)
if roll > current or roll == 100:
self.d[skill] += 1
logger.debug("%s, boosted %s from %s to %s by veterancy", self, skill, original, self.d[skill])

def veterancy_stat_losses(self):
losses = 0
if 40 <= self.age <= 49: losses = 1
elif 50 <= self.age <= 59: losses = 2
elif 60 <= self.age <= 69: losses = 4
elif 70 <= self.age <= 79: losses = 8
elif 80 <= self.age <= 89: losses = 16
elif 80 <= self.age: losses = 32
while losses and not all(self.d[stat] <= 1 for stat in self.PHYSICAL_STATS):
target = choice(self.PHYSICAL_STATS)
if self.d[target] > 1:
self.d[target] -= 1
losses -= 1
logger.debug("%s, %s decreased to %s by veterancy", self, target, self.d[target])

def damaged_veteran_changes(self):
damage_count = choices(range(5), weights=[80, 10, 5, 4, 1])[0]
if damage_count:
damage_methods = sample(
[self.extreme_violence_changes,
self.captivity_or_imprisonment_changes,
self.hard_experience_changes,
self.things_man_was_not_meant_to_know_changes],
k=damage_count
)
damage = ["Damaged Veteran:"]
for method in damage_methods:
method(damage)
for i, description in enumerate(damage):
self.e[f"detail{i}"] = description

def extreme_violence_changes(self, damage):
damage.append("• Extreme Violence")
self.d["occult"] += 10
self.san_loss = getattr(self, "san_loss", 0) + 5
self.d["charisma"] -= 3
for i in range(self.profession["bonds"]):
if f"bond{i}" in self.d:
self.d[f"bond{i}"] -= 3
self.adapted_to_violence = 3

def captivity_or_imprisonment_changes(self, damage):
damage.append("• Captivity or Imprisonment")
self.d["occult"] += 10
self.san_loss = getattr(self, "san_loss", 0) + 5
self.d["power"] -= 3
self.adapted_to_helplessness = 3

def hard_experience_changes(self, damage):
damage.append("• Hard Experience")
self.d["occult"] += 10
potential_bonus_skills = sample(self.ALL_BONUS, len(self.ALL_BONUS))
self.apply_bonuses(potential_bonus_skills, 5, 90)
self.san_loss = getattr(self, "san_loss", 0) + 5
del self.d[f"bond{self.profession['bonds']-1}"]

def things_man_was_not_meant_to_know_changes(self, damage):
damage.append("• Things Man Was Not Meant to Know")
self.d["unnatural"] = self.d.get("unnatural", 0) + 10
self.d["occult"] += 20
self.san_loss = getattr(self, "san_loss", 0) + self.d["power"]
self.d["disorder0"] = "Disorder: " + choice(
["Amnesia",
"Depersonalization",
"Depression",
"Dissociative Identity",
"Fugues",
"Megalomania",
"Paranoia",
"Sleep Disorder",
])
def potential_bonus_skills(self, profession):
return [
s
Expand Down Expand Up @@ -368,6 +496,10 @@ def equip_weapon(self, slot, weapon):
f" {damage_note_indicator}" if damage_note_indicator else ""
)

def store_footnote(self, note):
"""Returns indicator character"""
return self.footnotes[note] if note else None

def print_footnotes(self):
notes = list(
chain(
Expand All @@ -383,9 +515,14 @@ def print_footnotes(self):
for i, note in enumerate(notes[:12]):
self.e[f"note{i}"] = note

def store_footnote(self, note):
"""Returns indicator character"""
return self.footnotes[note] if note else None
def __str__(self):
return ", ".join(
[
self.d.get(i)
for i in ("name", "profession", "employer", "department", "age")
if self.d.get(i)
]
)


class Need2KnowPDF(object):
Expand Down Expand Up @@ -428,6 +565,9 @@ class Need2KnowPDF(object):
"bond1": (512, 586, 11),
"bond2": (512, 568, 11),
"bond3": (512, 550, 11),
"disorder0": (340, 482, 11),
"violence": (372, 384, 10),
"helplessness": (486, 384, 10),
# Applicable Skill Sets
"accounting": (200, 361, 11),
"alertness": (200, 343, 11),
Expand Down Expand Up @@ -596,6 +736,12 @@ class Need2KnowPDF(object):
"note9": (410, 30, 8),
"note10": (410, 20, 8),
"note11": (410, 10, 8),
"detail0": (75, 338, 8),
"detail1": (75, 328, 8),
"detail2": (75, 318, 8),
"detail3": (75, 308, 8),
"detail4": (75, 298, 8),
"detail5": (75, 288, 8),
}

# Fields that also get a multiplier
Expand Down Expand Up @@ -775,8 +921,36 @@ def get_options():
default="data/professions.json",
help="Data file for professions - defaults to %(default)s",
)
parser.add_argument("-A", "--max-age", action="store", type=int, help="Maximum age of characters", default=55)
parser.add_argument("-a", "--min-age", action="store", type=int, help="Minimum age of characters", default=25)
parser.add_argument(
"-a",
"--min-age",
action="store",
type=int,
help="Minimum age of characters - defaults to %(default)s.",
default=25
)
parser.add_argument(
"-A",
"--max-age",
action="store",
type=int,
help="Maximum age of characters - defaults to %(default)s.",
default=55,
)
parser.add_argument(
"--veterancy",
action="store_true",
dest="veterancy",
help="Grant additional experience due to age.",
default=False,
)
parser.add_argument(
"--no-damaged",
action="store_false",
dest="damaged",
help="Don't generate damaged veterans.",
default=True,
)

return parser.parse_args()

Expand Down

0 comments on commit c3f5e34

Please sign in to comment.