mirror of
https://github.com/rr-/pq-cli.git
synced 2026-11-12 22:40:36 -07:00
mechanic: move word lists to data files
The word lists (monsters, spells, equipment, races, classes, titles and
so on) now live in pqcli/data/*.json instead of pqcli/config.py. A
GameData object loaded at startup is passed to the simulation and UIs.
Users can replace any list by putting NAME.json in
$XDG_CONFIG_HOME/pqcli/data/ or a directory given with --data-dir.
--dump-data DIR copies the built-in files out for editing. Malformed
files are reported and pqcli exits before starting the UI.
Combat task descriptions come from the new combat_verbs list, with
{target} standing for the enemy. The built-in list holds only "Executing
{target}", and a single verb consumes no random roll, so stock gameplay
is unchanged. Recorded traces in tests/fixtures pin this down.
Monster, Race and Class stay in pqcli.config so existing saves still
load.
Resolves #3
This commit is contained in:
38 files changed
+7143
-778
No files matched your search
+45
-4
@@ -5,12 +5,19 @@ from pathlib import Path
|
||||
|
||||
from xdg_base_dirs import xdg_config_home
|
||||
|
||||
from pqcli.game_data import (
|
||||
DataError,
|
||||
GameData,
|
||||
dump_builtin_data,
|
||||
load_game_data,
|
||||
)
|
||||
from pqcli.mechanic import Player
|
||||
from pqcli.roster import Roster
|
||||
from pqcli.ui.basic import BasicUserInterface
|
||||
from pqcli.ui.curses import CursesUserInterface
|
||||
|
||||
SAVE_PATH = Path(xdg_config_home()) / "pqcli" / "save.dat"
|
||||
DATA_PATH = Path(xdg_config_home()) / "pqcli" / "data"
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
@@ -58,6 +65,22 @@ def parse_args() -> argparse.Namespace:
|
||||
metavar="NUM",
|
||||
help="play chosen character",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--data-dir",
|
||||
type=Path,
|
||||
default=DATA_PATH,
|
||||
metavar="DIR",
|
||||
help=(
|
||||
"directory with custom word lists; each NAME.json replaces "
|
||||
f"the built-in list of that name (default: {DATA_PATH})"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dump-data",
|
||||
type=Path,
|
||||
metavar="DIR",
|
||||
help="copy the built-in word lists to DIR for editing and exit",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
@@ -66,12 +89,14 @@ def list_players(roster: Roster, file: T.Optional[T.Any] = None) -> None:
|
||||
print(f"{i}. {player.name}", file=file)
|
||||
|
||||
|
||||
def bootstrap_first_run(roster: Roster, args: argparse.Namespace) -> None:
|
||||
def bootstrap_first_run(
|
||||
roster: Roster, args: argparse.Namespace, data: GameData
|
||||
) -> None:
|
||||
if roster.players or args.list_saves or args.load_save:
|
||||
return
|
||||
|
||||
print("No saved characters found. Starting first-run character creation.")
|
||||
ui = BasicUserInterface(roster, None, args)
|
||||
ui = BasicUserInterface(roster, None, args, data)
|
||||
while not roster.players:
|
||||
player = ui.create_player(auto_play=False)
|
||||
if player:
|
||||
@@ -83,6 +108,22 @@ def bootstrap_first_run(roster: Roster, args: argparse.Namespace) -> None:
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
|
||||
if args.dump_data:
|
||||
try:
|
||||
paths = dump_builtin_data(args.dump_data)
|
||||
except DataError as ex:
|
||||
print(f"pqcli: {ex}", file=sys.stderr)
|
||||
exit(1)
|
||||
print(f"Wrote {len(paths)} files to {args.dump_data}")
|
||||
exit(0)
|
||||
|
||||
try:
|
||||
data = load_game_data(args.data_dir)
|
||||
except DataError as ex:
|
||||
print(f"pqcli: {ex}", file=sys.stderr)
|
||||
exit(1)
|
||||
|
||||
roster = Roster.load(SAVE_PATH)
|
||||
|
||||
if args.list_saves:
|
||||
@@ -98,10 +139,10 @@ def main() -> None:
|
||||
list_players(roster, file=sys.stderr)
|
||||
exit(1)
|
||||
|
||||
bootstrap_first_run(roster, args)
|
||||
bootstrap_first_run(roster, args, data)
|
||||
|
||||
try:
|
||||
ui = args.ui(roster, player, args)
|
||||
ui = args.ui(roster, player, args, data)
|
||||
ui.run()
|
||||
finally:
|
||||
if args.use_saves:
|
||||
|
||||
+3
-676
@@ -39,56 +39,9 @@ PRIME_STATS = [
|
||||
|
||||
ALL_STATS = PRIME_STATS + [StatType.hp_max, StatType.mp_max]
|
||||
|
||||
|
||||
SPELLS = [
|
||||
"Slime Finger",
|
||||
"Rabbit Punch",
|
||||
"Hastiness",
|
||||
"Good Move",
|
||||
"Sadness",
|
||||
"Seasick",
|
||||
"Shoelaces",
|
||||
"Inoculate",
|
||||
"Cone of Annoyance",
|
||||
"Magnetic Orb",
|
||||
"Invisible Hands",
|
||||
"Revolting Cloud",
|
||||
"Aqueous Humor",
|
||||
"Spectral Miasma",
|
||||
"Clever Fellow",
|
||||
"Lockjaw",
|
||||
"History Lesson",
|
||||
"Hydrophobia",
|
||||
"Big Sister",
|
||||
"Cone of Paste",
|
||||
"Mulligan",
|
||||
"Nestor's Bright Idea",
|
||||
"Holy Batpole",
|
||||
"Tumor (Benign)",
|
||||
"Braingate",
|
||||
"Summon a Bitch",
|
||||
"Nonplus",
|
||||
"Animate Nightstand",
|
||||
"Eye of the Troglodyte",
|
||||
"Curse Name",
|
||||
"Dropsy",
|
||||
"Vitreous Humor",
|
||||
"Roger's Grand Illusion",
|
||||
"Covet",
|
||||
"Black Idaho",
|
||||
"Astral Miasma",
|
||||
"Spectral Oyster",
|
||||
"Acrid Hands",
|
||||
"Angioplasty",
|
||||
"Grognor's Big Day Off",
|
||||
"Tumor (Malignant)",
|
||||
"Animate Tunic",
|
||||
"Ursine Armor",
|
||||
"Holy Roller",
|
||||
"Tonsillectomy",
|
||||
"Curse Family",
|
||||
"Infinite Confusion",
|
||||
]
|
||||
# The word lists live in pqcli/data/*.json (see pqcli.game_data).
|
||||
# Saves pickle Monster, Race and Class instances by import path, so these
|
||||
# classes must stay in this module.
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -97,329 +50,12 @@ class Modifier:
|
||||
quality: int
|
||||
|
||||
|
||||
OFFENSE_ATTRIB = [
|
||||
Modifier("Polished", 1),
|
||||
Modifier("Serrated", 1),
|
||||
Modifier("Heavy", 1),
|
||||
Modifier("Pronged", 2),
|
||||
Modifier("Steely", 2),
|
||||
Modifier("Vicious", 3),
|
||||
Modifier("Venomed", 4),
|
||||
Modifier("Stabbity", 4),
|
||||
Modifier("Dancing", 5),
|
||||
Modifier("Invisible", 6),
|
||||
Modifier("Vorpal", 7),
|
||||
]
|
||||
|
||||
DEFENSE_ATTRIB = [
|
||||
Modifier("Studded", 1),
|
||||
Modifier("Banded", 2),
|
||||
Modifier("Gilded", 2),
|
||||
Modifier("Festooned", 3),
|
||||
Modifier("Holy", 4),
|
||||
Modifier("Cambric", 1),
|
||||
Modifier("Fine", 4),
|
||||
Modifier("Impressive", 5),
|
||||
Modifier("Custom", 3),
|
||||
]
|
||||
|
||||
OFFENSE_BAD = [
|
||||
Modifier("Dull", -2),
|
||||
Modifier("Tarnished", -1),
|
||||
Modifier("Rusty", -3),
|
||||
Modifier("Padded", -5),
|
||||
Modifier("Bent", -4),
|
||||
Modifier("Mini", -4),
|
||||
Modifier("Rubber", -6),
|
||||
Modifier("Nerf", -7),
|
||||
Modifier("Unbalanced", -2),
|
||||
]
|
||||
|
||||
DEFENSE_BAD = [
|
||||
Modifier("Holey", -1),
|
||||
Modifier("Patched", -1),
|
||||
Modifier("Threadbare", -2),
|
||||
Modifier("Faded", -1),
|
||||
Modifier("Rusty", -3),
|
||||
Modifier("Motheaten", -3),
|
||||
Modifier("Mildewed", -2),
|
||||
Modifier("Torn", -3),
|
||||
Modifier("Dented", -3),
|
||||
Modifier("Cursed", -5),
|
||||
Modifier("Plastic", -4),
|
||||
Modifier("Cracked", -4),
|
||||
Modifier("Warped", -3),
|
||||
Modifier("Corroded", -3),
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class EquipmentPreset:
|
||||
name: str
|
||||
quality: int
|
||||
|
||||
|
||||
SHIELDS = [
|
||||
EquipmentPreset("Parasol", 0),
|
||||
EquipmentPreset("Pie Plate", 1),
|
||||
EquipmentPreset("Garbage Can Lid", 2),
|
||||
EquipmentPreset("Buckler", 3),
|
||||
EquipmentPreset("Plexiglass", 4),
|
||||
EquipmentPreset("Fender", 4),
|
||||
EquipmentPreset("Round Shield", 5),
|
||||
EquipmentPreset("Carapace", 5),
|
||||
EquipmentPreset("Scutum", 6),
|
||||
EquipmentPreset("Propugner", 6),
|
||||
EquipmentPreset("Kite Shield", 7),
|
||||
EquipmentPreset("Pavise", 8),
|
||||
EquipmentPreset("Tower Shield", 9),
|
||||
EquipmentPreset("Baroque Shield", 11),
|
||||
EquipmentPreset("Aegis", 12),
|
||||
EquipmentPreset("Magnetic Field", 18),
|
||||
]
|
||||
|
||||
ARMORS = [
|
||||
EquipmentPreset("Lace", 1),
|
||||
EquipmentPreset("Macrame", 2),
|
||||
EquipmentPreset("Burlap", 3),
|
||||
EquipmentPreset("Canvas", 4),
|
||||
EquipmentPreset("Flannel", 5),
|
||||
EquipmentPreset("Chamois", 6),
|
||||
EquipmentPreset("Pleathers", 7),
|
||||
EquipmentPreset("Leathers", 8),
|
||||
EquipmentPreset("Bearskin", 9),
|
||||
EquipmentPreset("Ringmail", 10),
|
||||
EquipmentPreset("Scale Mail", 12),
|
||||
EquipmentPreset("Chainmail", 14),
|
||||
EquipmentPreset("Splint Mail", 15),
|
||||
EquipmentPreset("Platemail", 16),
|
||||
EquipmentPreset("ABS", 17),
|
||||
EquipmentPreset("Kevlar", 18),
|
||||
EquipmentPreset("Titanium", 19),
|
||||
EquipmentPreset("Mithril Mail", 20),
|
||||
EquipmentPreset("Diamond Mail", 25),
|
||||
EquipmentPreset("Plasma", 30),
|
||||
]
|
||||
|
||||
WEAPONS = [
|
||||
EquipmentPreset("Stick", 0),
|
||||
EquipmentPreset("Broken Bottle", 1),
|
||||
EquipmentPreset("Shiv", 1),
|
||||
EquipmentPreset("Sprig", 1),
|
||||
EquipmentPreset("Oxgoad", 1),
|
||||
EquipmentPreset("Eelspear", 2),
|
||||
EquipmentPreset("Bowie Knife", 2),
|
||||
EquipmentPreset("Claw Hammer", 2),
|
||||
EquipmentPreset("Handpeen", 2),
|
||||
EquipmentPreset("Andiron", 3),
|
||||
EquipmentPreset("Hatchet", 3),
|
||||
EquipmentPreset("Tomahawk", 3),
|
||||
EquipmentPreset("Hackbarm", 3),
|
||||
EquipmentPreset("Crowbar", 4),
|
||||
EquipmentPreset("Mace", 4),
|
||||
EquipmentPreset("Battleadze", 4),
|
||||
EquipmentPreset("Leafmace", 5),
|
||||
EquipmentPreset("Shortsword", 5),
|
||||
EquipmentPreset("Longiron", 5),
|
||||
EquipmentPreset("Poachard", 5),
|
||||
EquipmentPreset("Baselard", 5),
|
||||
EquipmentPreset("Whinyard", 6),
|
||||
EquipmentPreset("Blunderbuss", 6),
|
||||
EquipmentPreset("Longsword", 6),
|
||||
EquipmentPreset("Crankbow", 6),
|
||||
EquipmentPreset("Blibo", 7),
|
||||
EquipmentPreset("Broadsword", 7),
|
||||
EquipmentPreset("Kreen", 7),
|
||||
EquipmentPreset("Warhammer", 7),
|
||||
EquipmentPreset("Morning Star", 8),
|
||||
EquipmentPreset("Pole-adze", 8),
|
||||
EquipmentPreset("Spontoon", 8),
|
||||
EquipmentPreset("Bastard Sword", 9),
|
||||
EquipmentPreset("Peen-arm", 9),
|
||||
EquipmentPreset("Culverin", 10),
|
||||
EquipmentPreset("Lance", 10),
|
||||
EquipmentPreset("Halberd", 11),
|
||||
EquipmentPreset("Poleax", 12),
|
||||
EquipmentPreset("Bandyclef", 15),
|
||||
]
|
||||
|
||||
SPECIALS = [
|
||||
"Diadem",
|
||||
"Festoon",
|
||||
"Gemstone",
|
||||
"Phial",
|
||||
"Tiara",
|
||||
"Scabbard",
|
||||
"Arrow",
|
||||
"Lens",
|
||||
"Lamp",
|
||||
"Hymnal",
|
||||
"Fleece",
|
||||
"Laurel",
|
||||
"Brooch",
|
||||
"Gimlet",
|
||||
"Cobble",
|
||||
"Albatross",
|
||||
"Brazier",
|
||||
"Bandolier",
|
||||
"Tome",
|
||||
"Garnet",
|
||||
"Amethyst",
|
||||
"Candelabra",
|
||||
"Corset",
|
||||
"Sphere",
|
||||
"Sceptre",
|
||||
"Ankh",
|
||||
"Talisman",
|
||||
"Orb",
|
||||
"Gammel",
|
||||
"Ornament",
|
||||
"Brocade",
|
||||
"Galoon",
|
||||
"Bijou",
|
||||
"Spangle",
|
||||
"Gimcrack",
|
||||
"Hood",
|
||||
"Vulpeculum",
|
||||
]
|
||||
|
||||
ITEM_ATTRIB = [
|
||||
"Golden",
|
||||
"Gilded",
|
||||
"Spectral",
|
||||
"Astral",
|
||||
"Garlanded",
|
||||
"Precious",
|
||||
"Crafted",
|
||||
"Dual",
|
||||
"Filigreed",
|
||||
"Cruciate",
|
||||
"Arcane",
|
||||
"Blessed",
|
||||
"Reverential",
|
||||
"Lucky",
|
||||
"Enchanted",
|
||||
"Gleaming",
|
||||
"Grandiose",
|
||||
"Sacred",
|
||||
"Legendary",
|
||||
"Mythic",
|
||||
"Crystalline",
|
||||
"Austere",
|
||||
"Ostentatious",
|
||||
"One True",
|
||||
"Proverbial",
|
||||
"Fearsome",
|
||||
"Deadly",
|
||||
"Benevolent",
|
||||
"Unearthly",
|
||||
"Magnificent",
|
||||
"Iron",
|
||||
"Ormolu",
|
||||
"Puissant",
|
||||
]
|
||||
|
||||
ITEM_OFS = [
|
||||
"Foreboding",
|
||||
"Foreshadowing",
|
||||
"Nervousness",
|
||||
"Happiness",
|
||||
"Torpor",
|
||||
"Danger",
|
||||
"Craft",
|
||||
"Silence",
|
||||
"Invisibility",
|
||||
"Rapidity",
|
||||
"Pleasure",
|
||||
"Practicality",
|
||||
"Hurting",
|
||||
"Joy",
|
||||
"Petulance",
|
||||
"Intrusion",
|
||||
"Chaos",
|
||||
"Suffering",
|
||||
"Extroversion",
|
||||
"Frenzy",
|
||||
"Sisu",
|
||||
"Solitude",
|
||||
"Punctuality",
|
||||
"Efficiency",
|
||||
"Comfort",
|
||||
"Patience",
|
||||
"Internment",
|
||||
"Incarceration",
|
||||
"Misapprehension",
|
||||
"Loyalty",
|
||||
"Envy",
|
||||
"Acrimony",
|
||||
"Worry",
|
||||
"Fear",
|
||||
"Awe",
|
||||
"Guile",
|
||||
"Prurience",
|
||||
"Fortune",
|
||||
"Perspicacity",
|
||||
"Domination",
|
||||
"Submission",
|
||||
"Fealty",
|
||||
"Hunger",
|
||||
"Despair",
|
||||
"Cruelty",
|
||||
"Grob",
|
||||
"Dignard",
|
||||
"Ra",
|
||||
"the Bone",
|
||||
"Diamonique",
|
||||
"Electrum",
|
||||
"Hydragyrum",
|
||||
]
|
||||
|
||||
BORING_ITEMS = [
|
||||
"nail",
|
||||
"lunchpail",
|
||||
"sock",
|
||||
"I.O.U.",
|
||||
"cookie",
|
||||
"pint",
|
||||
"toothpick",
|
||||
"writ",
|
||||
"newspaper",
|
||||
"letter",
|
||||
"plank",
|
||||
"hat",
|
||||
"egg",
|
||||
"coin",
|
||||
"needle",
|
||||
"bucket",
|
||||
"ladder",
|
||||
"chicken",
|
||||
"twig",
|
||||
"dirtclod",
|
||||
"counterpane",
|
||||
"vest",
|
||||
"teratoma",
|
||||
"bunny",
|
||||
"rock",
|
||||
"pole",
|
||||
"carrot",
|
||||
"canoe",
|
||||
"inkwell",
|
||||
"hoe",
|
||||
"bandage",
|
||||
"trowel",
|
||||
"towel",
|
||||
"planter box",
|
||||
"anvil",
|
||||
"axle",
|
||||
"tuppence",
|
||||
"casket",
|
||||
"nosegay",
|
||||
"trinket",
|
||||
"credenza",
|
||||
"writ",
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Monster:
|
||||
name: str
|
||||
@@ -427,322 +63,13 @@ class Monster:
|
||||
item: T.Optional[str]
|
||||
|
||||
|
||||
MONSTERS = [
|
||||
Monster("Anhkheg", 6, "chitin"),
|
||||
Monster("Ant", 0, "antenna"),
|
||||
Monster("Ape", 4, "ass"),
|
||||
Monster("Baluchitherium", 14, "ear"),
|
||||
Monster("Beholder", 10, "eyestalk"),
|
||||
Monster("Black Pudding", 10, "saliva"),
|
||||
Monster("Blink Dog", 4, "eyelid"),
|
||||
Monster("Cub Scout", 1, "neckerchief"),
|
||||
Monster("Girl Scout", 2, "cookie"),
|
||||
Monster("Boy Scout", 3, "merit badge"),
|
||||
Monster("Eagle Scout", 4, "merit badge"),
|
||||
Monster("Bugbear", 3, "skin"),
|
||||
Monster("Bugboar", 3, "tusk"),
|
||||
Monster("Boogie", 3, "slime"),
|
||||
Monster("Camel", 2, "hump"),
|
||||
Monster("Carrion Crawler", 3, "egg"),
|
||||
Monster("Catoblepas", 6, "neck"),
|
||||
Monster("Centaur", 4, "rib"),
|
||||
Monster("Centipede", 0, "leg"),
|
||||
Monster("Cockatrice", 5, "wattle"),
|
||||
Monster("Couatl", 9, "wing"),
|
||||
Monster("Crayfish", 0, "antenna"),
|
||||
Monster("Demogorgon", 53, "tentacle"),
|
||||
Monster("Jubilex", 17, "gel"),
|
||||
Monster("Manes", 1, "tooth"),
|
||||
Monster("Orcus", 27, "wand"),
|
||||
Monster("Succubus", 6, "bra"),
|
||||
Monster("Vrock", 8, "neck"),
|
||||
Monster("Hezrou", 9, "leg"),
|
||||
Monster("Glabrezu", 10, "collar"),
|
||||
Monster("Nalfeshnee", 11, "tusk"),
|
||||
Monster("Marilith", 7, "arm"),
|
||||
Monster("Balor", 8, "whip"),
|
||||
Monster("Yeenoghu", 25, "flail"),
|
||||
Monster("Asmodeus", 52, "leathers"),
|
||||
Monster("Baalzebul", 43, "pants"),
|
||||
Monster("Barbed Devil", 8, "flame"),
|
||||
Monster("Bone Devil", 9, "hook"),
|
||||
Monster("Dispater", 30, "matches"),
|
||||
Monster("Erinyes", 6, "thong"),
|
||||
Monster("Geryon", 30, "cornucopia"),
|
||||
Monster("Malebranche", 5, "fork"),
|
||||
Monster("Ice Devil", 11, "snow"),
|
||||
Monster("Lemure", 3, "blob"),
|
||||
Monster("Pit Fiend", 13, "seed"),
|
||||
Monster("Ankylosaurus", 9, "tail"),
|
||||
Monster("Brontosaurus", 30, "brain"),
|
||||
Monster("Diplodocus", 24, "fin"),
|
||||
Monster("Elasmosaurus", 15, "neck"),
|
||||
Monster("Gorgosaurus", 13, "arm"),
|
||||
Monster("Iguanadon", 6, "thumb"),
|
||||
Monster("Megalosaurus", 12, "jaw"),
|
||||
Monster("Monoclonius", 8, "horn"),
|
||||
Monster("Pentasaurus", 12, "head"),
|
||||
Monster("Stegosaurus", 18, "plate"),
|
||||
Monster("Triceratops", 16, "horn"),
|
||||
Monster("Tyrannosaurus Rex", 18, "forearm"),
|
||||
Monster("Djinn", 7, "lamp"),
|
||||
Monster("Doppelganger", 4, "face"),
|
||||
Monster("Black Dragon", 7, None),
|
||||
Monster("Plaid Dragon", 7, "sporrin"),
|
||||
Monster("Blue Dragon", 9, None),
|
||||
Monster("Beige Dragon", 9, None),
|
||||
Monster("Brass Dragon", 7, "pole"),
|
||||
Monster("Tin Dragon", 8, None),
|
||||
Monster("Bronze Dragon", 9, "medal"),
|
||||
Monster("Chromatic Dragon", 16, "scale"),
|
||||
Monster("Copper Dragon", 8, "loafer"),
|
||||
Monster("Gold Dragon", 8, "filling"),
|
||||
Monster("Green Dragon", 8, None),
|
||||
Monster("Platinum Dragon", 21, None),
|
||||
Monster("Red Dragon", 10, "cocktail"),
|
||||
Monster("Silver Dragon", 10, None),
|
||||
Monster("White Dragon", 6, "tooth"),
|
||||
Monster("Dragon Turtle", 13, "shell"),
|
||||
Monster("Dryad", 2, "acorn"),
|
||||
Monster("Dwarf", 1, "drawers"),
|
||||
Monster("Eel", 2, "sashimi"),
|
||||
Monster("Efreet", 10, "cinder"),
|
||||
Monster("Sand Elemental", 8, "glass"),
|
||||
Monster("Bacon Elemental", 10, "bit"),
|
||||
Monster("Porn Elemental", 12, "lube"),
|
||||
Monster("Cheese Elemental", 14, "curd"),
|
||||
Monster("Hair Elemental", 16, "follicle"),
|
||||
Monster("Swamp Elf", 1, "lilypad"),
|
||||
Monster("Brown Elf", 1, "tusk"),
|
||||
Monster("Sea Elf", 1, "jerkin"),
|
||||
Monster("Ettin", 10, "fur"),
|
||||
Monster("Frog", 0, "leg"),
|
||||
Monster("Violet Fungi", 3, "spore"),
|
||||
Monster("Gargoyle", 4, "gravel"),
|
||||
Monster("Gelatinous Cube", 4, "jam"),
|
||||
Monster("Ghast", 4, "vomit"),
|
||||
Monster("Ghost", 10, None),
|
||||
Monster("Ghoul", 2, "muscle"),
|
||||
Monster("Humidity Giant", 12, "drops"),
|
||||
Monster("Beef Giant", 11, "steak"),
|
||||
Monster("Quartz Giant", 10, "crystal"),
|
||||
Monster("Porcelain Giant", 9, "fixture"),
|
||||
Monster("Rice Giant", 8, "grain"),
|
||||
Monster("Cloud Giant", 12, "condensation"),
|
||||
Monster("Fire Giant", 11, "cigarettes"),
|
||||
Monster("Frost Giant", 10, "snowman"),
|
||||
Monster("Hill Giant", 8, "corpse"),
|
||||
Monster("Stone Giant", 9, "hatchling"),
|
||||
Monster("Storm Giant", 15, "barometer"),
|
||||
Monster("Mini Giant", 4, "pompadour"),
|
||||
Monster("Gnoll", 2, "collar"),
|
||||
Monster("Gnome", 1, "hat"),
|
||||
Monster("Goblin", 1, "ear"),
|
||||
Monster("Grid Bug", 1, "carapace"),
|
||||
Monster("Jellyrock", 9, "seedling"),
|
||||
Monster("Beer Golem", 15, "foam"),
|
||||
Monster("Oxygen Golem", 17, "platelet"),
|
||||
Monster("Cardboard Golem", 14, "recycling"),
|
||||
Monster("Rubber Golem", 16, "ball"),
|
||||
Monster("Leather Golem", 15, "fob"),
|
||||
Monster("Gorgon", 8, "testicle"),
|
||||
Monster("Gray Ooze", 3, "gravy"),
|
||||
Monster("Green Slime", 2, "sample"),
|
||||
Monster("Griffon", 7, "nest"),
|
||||
Monster("Banshee", 7, "larynx"),
|
||||
Monster("Harpy", 3, "mascara"),
|
||||
Monster("Hell Hound", 5, "tongue"),
|
||||
Monster("Hippocampus", 4, "mane"),
|
||||
Monster("Hippogriff", 3, "egg"),
|
||||
Monster("Hobgoblin", 1, "patella"),
|
||||
Monster("Homunculus", 2, "fluid"),
|
||||
Monster("Hydra", 8, "gyrum"),
|
||||
Monster("Imp", 2, "tail"),
|
||||
Monster("Invisible Stalker", 8, None),
|
||||
Monster("Iron Peasant", 3, "chaff"),
|
||||
Monster("Jumpskin", 3, "shin"),
|
||||
Monster("Kobold", 1, "penis"),
|
||||
Monster("Leprechaun", 1, "wallet"),
|
||||
Monster("Leucrotta", 6, "hoof"),
|
||||
Monster("Lich", 11, "crown"),
|
||||
Monster("Lizard Man", 2, "tail"),
|
||||
Monster("Lurker", 10, "sac"),
|
||||
Monster("Manticore", 6, "spike"),
|
||||
Monster("Mastodon", 12, "tusk"),
|
||||
Monster("Medusa", 6, "eye"),
|
||||
Monster("Multicell", 2, "dendrite"),
|
||||
Monster("Pirate", 1, "booty"),
|
||||
Monster("Berserker", 1, "shirt"),
|
||||
Monster("Caveman", 2, "club"),
|
||||
Monster("Dervish", 1, "robe"),
|
||||
Monster("Merman", 1, "trident"),
|
||||
Monster("Mermaid", 1, "gills"),
|
||||
Monster("Mimic", 9, "hinge"),
|
||||
Monster("Mind Flayer", 8, "tentacle"),
|
||||
Monster("Minotaur", 6, "map"),
|
||||
Monster("Yellow Mold", 1, "spore"),
|
||||
Monster("Morkoth", 7, "teeth"),
|
||||
Monster("Mummy", 6, "gauze"),
|
||||
Monster("Naga", 9, "rattle"),
|
||||
Monster("Nebbish", 1, "belly"),
|
||||
Monster("Neo-Otyugh", 11, "organ "),
|
||||
Monster("Nixie", 1, "webbing"),
|
||||
Monster("Nymph", 3, "hanky"),
|
||||
Monster("Ochre Jelly", 6, "nucleus"),
|
||||
Monster("Octopus", 2, "beak"),
|
||||
Monster("Ogre", 4, "talon"),
|
||||
Monster("Ogre Mage", 5, "apparel"),
|
||||
Monster("Orc", 1, "snout"),
|
||||
Monster("Otyugh", 7, "organ"),
|
||||
Monster("Owlbear", 5, "feather"),
|
||||
Monster("Pegasus", 4, "aileron"),
|
||||
Monster("Peryton", 4, "antler"),
|
||||
Monster("Piercer", 3, "tip"),
|
||||
Monster("Pixie", 1, "dust"),
|
||||
Monster("Man-o-war", 3, "tentacle"),
|
||||
Monster("Purple Worm", 15, "dung"),
|
||||
Monster("Quasit", 3, "tail"),
|
||||
Monster("Rakshasa", 7, "pajamas"),
|
||||
Monster("Rat", 0, "tail"),
|
||||
Monster("Remorhaz", 11, "protrusion"),
|
||||
Monster("Roc", 18, "wing"),
|
||||
Monster("Roper", 11, "twine"),
|
||||
Monster("Rot Grub", 1, "eggsac"),
|
||||
Monster("Rust Monster", 5, "shavings"),
|
||||
Monster("Satyr", 5, "hoof"),
|
||||
Monster("Sea Hag", 3, "wart"),
|
||||
Monster("Silkie", 3, "fur"),
|
||||
Monster("Shadow", 3, "silhouette"),
|
||||
Monster("Shambling Mound", 10, "mulch"),
|
||||
Monster("Shedu", 9, "hoof"),
|
||||
Monster("Shrieker", 3, "stalk"),
|
||||
Monster("Skeleton", 1, "clavicle"),
|
||||
Monster("Spectre", 7, "vestige"),
|
||||
Monster("Sphinx", 10, "paw"),
|
||||
Monster("Spider", 0, "web"),
|
||||
Monster("Sprite", 1, "can"),
|
||||
Monster("Stirge", 1, "proboscis"),
|
||||
Monster("Stun Bear", 5, "tooth"),
|
||||
Monster("Stun Worm", 2, "trode"),
|
||||
Monster("Su-monster", 5, "tail"),
|
||||
Monster("Sylph", 3, "thigh"),
|
||||
Monster("Titan", 20, "sandal"),
|
||||
Monster("Trapper", 12, "shag"),
|
||||
Monster("Treant", 10, "acorn"),
|
||||
Monster("Triton", 3, "scale"),
|
||||
Monster("Troglodyte", 2, "tail"),
|
||||
Monster("Troll", 6, "hide"),
|
||||
Monster("Umber Hulk", 8, "claw"),
|
||||
Monster("Unicorn", 4, "blood"),
|
||||
Monster("Vampire", 8, "pancreas"),
|
||||
Monster("Wight", 4, "lung"),
|
||||
Monster("Will-o'-the-Wisp", 9, "wisp"),
|
||||
Monster("Wraith", 5, "finger"),
|
||||
Monster("Wyvern", 7, "wing"),
|
||||
Monster("Xorn", 7, "jaw"),
|
||||
Monster("Yeti", 4, "fur"),
|
||||
Monster("Zombie", 2, "forehead"),
|
||||
Monster("Wasp", 0, "stinger"),
|
||||
Monster("Rat", 1, "tail"),
|
||||
Monster("Bunny", 0, "ear"),
|
||||
Monster("Moth", 0, "dust"),
|
||||
Monster("Beagle", 0, "collar"),
|
||||
Monster("Midge", 0, "corpse"),
|
||||
Monster("Ostrich", 1, "beak"),
|
||||
Monster("Billy Goat", 1, "beard"),
|
||||
Monster("Bat", 1, "wing"),
|
||||
Monster("Koala", 2, "heart"),
|
||||
Monster("Wolf", 2, "paw"),
|
||||
Monster("Whippet", 2, "collar"),
|
||||
Monster("Uruk", 2, "boot"),
|
||||
Monster("Poroid", 4, "node"),
|
||||
Monster("Moakum", 8, "frenum"),
|
||||
Monster("Fly", 0, None),
|
||||
Monster("Hogbird", 3, "curl"),
|
||||
Monster("Wolog", 4, "lemma"),
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Race:
|
||||
name: str
|
||||
attr: T.List[StatType]
|
||||
|
||||
|
||||
RACES = [
|
||||
Race("Half Orc", [StatType.hp_max]),
|
||||
Race("Half Man", [StatType.charisma]),
|
||||
Race("Half Halfling", [StatType.dexterity]),
|
||||
Race("Double Hobbit", [StatType.strength]),
|
||||
Race("Hob-Hobbit", [StatType.dexterity, StatType.condition]),
|
||||
Race("Low Elf", [StatType.condition]),
|
||||
Race("Dung Elf", [StatType.wisdom]),
|
||||
Race("Talking Pony", [StatType.mp_max, StatType.intelligence]),
|
||||
Race("Gyrognome", [StatType.dexterity]),
|
||||
Race("Lesser Dwarf", [StatType.condition]),
|
||||
Race("Crested Dwarf", [StatType.charisma]),
|
||||
Race("Eel Man", [StatType.dexterity]),
|
||||
Race("Panda Man", [StatType.condition, StatType.strength]),
|
||||
Race("Trans-Kobold", [StatType.wisdom]),
|
||||
Race("Enchanted Motorcycle", [StatType.mp_max]),
|
||||
Race("Will o' the Wisp", [StatType.wisdom]),
|
||||
Race("Battle-Finch", [StatType.dexterity, StatType.intelligence]),
|
||||
Race("Double Wookiee", [StatType.strength]),
|
||||
Race("Skraeling", [StatType.wisdom]),
|
||||
Race("Demicanadian", [StatType.condition]),
|
||||
Race("Land Squid", [StatType.strength, StatType.hp_max]),
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Class:
|
||||
name: str
|
||||
attr: T.List[StatType]
|
||||
|
||||
|
||||
CLASSES = [
|
||||
Class("Ur-Paladin", [StatType.wisdom, StatType.condition]),
|
||||
Class("Voodoo Princess", [StatType.intelligence, StatType.charisma]),
|
||||
Class("Robot Monk", [StatType.strength]),
|
||||
Class("Mu-Fu Monk", [StatType.dexterity]),
|
||||
Class("Mage Illusioner", [StatType.intelligence, StatType.mp_max]),
|
||||
Class("Shiv-Knight", [StatType.dexterity]),
|
||||
Class("Inner Mason", [StatType.condition]),
|
||||
Class("Fighter/Organist", [StatType.charisma, StatType.strength]),
|
||||
Class("Puma Burgular", [StatType.dexterity]),
|
||||
Class("Runeloremaster", [StatType.wisdom]),
|
||||
Class("Hunter Strangler", [StatType.dexterity, StatType.intelligence]),
|
||||
Class("Battle-Felon", [StatType.strength]),
|
||||
Class("Tickle-Mimic", [StatType.wisdom, StatType.intelligence]),
|
||||
Class("Slow Poisoner", [StatType.condition]),
|
||||
Class("Bastard Lunatic", [StatType.condition]),
|
||||
Class("Lowling", [StatType.wisdom]),
|
||||
Class("Birdrider", [StatType.wisdom]),
|
||||
Class("Vermineer", [StatType.intelligence]),
|
||||
]
|
||||
|
||||
TITLES = [
|
||||
"Mr.",
|
||||
"Mrs.",
|
||||
"Sir",
|
||||
"Sgt.",
|
||||
"Ms.",
|
||||
"Captain",
|
||||
"Chief",
|
||||
"Admiral",
|
||||
"Saint",
|
||||
]
|
||||
|
||||
IMPRESSIVE_TITLES = [
|
||||
"King",
|
||||
"Queen",
|
||||
"Lord",
|
||||
"Lady",
|
||||
"Viceroy",
|
||||
"Mayor",
|
||||
"Prince",
|
||||
"Princess",
|
||||
"Chief",
|
||||
"Boss",
|
||||
"Archbishop",
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
[
|
||||
{"name": "Lace", "quality": 1},
|
||||
{"name": "Macrame", "quality": 2},
|
||||
{"name": "Burlap", "quality": 3},
|
||||
{"name": "Canvas", "quality": 4},
|
||||
{"name": "Flannel", "quality": 5},
|
||||
{"name": "Chamois", "quality": 6},
|
||||
{"name": "Pleathers", "quality": 7},
|
||||
{"name": "Leathers", "quality": 8},
|
||||
{"name": "Bearskin", "quality": 9},
|
||||
{"name": "Ringmail", "quality": 10},
|
||||
{"name": "Scale Mail", "quality": 12},
|
||||
{"name": "Chainmail", "quality": 14},
|
||||
{"name": "Splint Mail", "quality": 15},
|
||||
{"name": "Platemail", "quality": 16},
|
||||
{"name": "ABS", "quality": 17},
|
||||
{"name": "Kevlar", "quality": 18},
|
||||
{"name": "Titanium", "quality": 19},
|
||||
{"name": "Mithril Mail", "quality": 20},
|
||||
{"name": "Diamond Mail", "quality": 25},
|
||||
{"name": "Plasma", "quality": 30}
|
||||
]
|
||||
@@ -0,0 +1,44 @@
|
||||
[
|
||||
"nail",
|
||||
"lunchpail",
|
||||
"sock",
|
||||
"I.O.U.",
|
||||
"cookie",
|
||||
"pint",
|
||||
"toothpick",
|
||||
"writ",
|
||||
"newspaper",
|
||||
"letter",
|
||||
"plank",
|
||||
"hat",
|
||||
"egg",
|
||||
"coin",
|
||||
"needle",
|
||||
"bucket",
|
||||
"ladder",
|
||||
"chicken",
|
||||
"twig",
|
||||
"dirtclod",
|
||||
"counterpane",
|
||||
"vest",
|
||||
"teratoma",
|
||||
"bunny",
|
||||
"rock",
|
||||
"pole",
|
||||
"carrot",
|
||||
"canoe",
|
||||
"inkwell",
|
||||
"hoe",
|
||||
"bandage",
|
||||
"trowel",
|
||||
"towel",
|
||||
"planter box",
|
||||
"anvil",
|
||||
"axle",
|
||||
"tuppence",
|
||||
"casket",
|
||||
"nosegay",
|
||||
"trinket",
|
||||
"credenza",
|
||||
"writ"
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
[
|
||||
{"name": "Ur-Paladin", "attr": ["WIS", "CON"]},
|
||||
{"name": "Voodoo Princess", "attr": ["INT", "CHA"]},
|
||||
{"name": "Robot Monk", "attr": ["STR"]},
|
||||
{"name": "Mu-Fu Monk", "attr": ["DEX"]},
|
||||
{"name": "Mage Illusioner", "attr": ["INT", "MP Max"]},
|
||||
{"name": "Shiv-Knight", "attr": ["DEX"]},
|
||||
{"name": "Inner Mason", "attr": ["CON"]},
|
||||
{"name": "Fighter/Organist", "attr": ["CHA", "STR"]},
|
||||
{"name": "Puma Burgular", "attr": ["DEX"]},
|
||||
{"name": "Runeloremaster", "attr": ["WIS"]},
|
||||
{"name": "Hunter Strangler", "attr": ["DEX", "INT"]},
|
||||
{"name": "Battle-Felon", "attr": ["STR"]},
|
||||
{"name": "Tickle-Mimic", "attr": ["WIS", "INT"]},
|
||||
{"name": "Slow Poisoner", "attr": ["CON"]},
|
||||
{"name": "Bastard Lunatic", "attr": ["CON"]},
|
||||
{"name": "Lowling", "attr": ["WIS"]},
|
||||
{"name": "Birdrider", "attr": ["WIS"]},
|
||||
{"name": "Vermineer", "attr": ["INT"]}
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
[
|
||||
"Executing {target}"
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{"name": "Studded", "quality": 1},
|
||||
{"name": "Banded", "quality": 2},
|
||||
{"name": "Gilded", "quality": 2},
|
||||
{"name": "Festooned", "quality": 3},
|
||||
{"name": "Holy", "quality": 4},
|
||||
{"name": "Cambric", "quality": 1},
|
||||
{"name": "Fine", "quality": 4},
|
||||
{"name": "Impressive", "quality": 5},
|
||||
{"name": "Custom", "quality": 3}
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
[
|
||||
{"name": "Holey", "quality": -1},
|
||||
{"name": "Patched", "quality": -1},
|
||||
{"name": "Threadbare", "quality": -2},
|
||||
{"name": "Faded", "quality": -1},
|
||||
{"name": "Rusty", "quality": -3},
|
||||
{"name": "Motheaten", "quality": -3},
|
||||
{"name": "Mildewed", "quality": -2},
|
||||
{"name": "Torn", "quality": -3},
|
||||
{"name": "Dented", "quality": -3},
|
||||
{"name": "Cursed", "quality": -5},
|
||||
{"name": "Plastic", "quality": -4},
|
||||
{"name": "Cracked", "quality": -4},
|
||||
{"name": "Warped", "quality": -3},
|
||||
{"name": "Corroded", "quality": -3}
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
[
|
||||
"King",
|
||||
"Queen",
|
||||
"Lord",
|
||||
"Lady",
|
||||
"Viceroy",
|
||||
"Mayor",
|
||||
"Prince",
|
||||
"Princess",
|
||||
"Chief",
|
||||
"Boss",
|
||||
"Archbishop"
|
||||
]
|
||||
@@ -0,0 +1,35 @@
|
||||
[
|
||||
"Golden",
|
||||
"Gilded",
|
||||
"Spectral",
|
||||
"Astral",
|
||||
"Garlanded",
|
||||
"Precious",
|
||||
"Crafted",
|
||||
"Dual",
|
||||
"Filigreed",
|
||||
"Cruciate",
|
||||
"Arcane",
|
||||
"Blessed",
|
||||
"Reverential",
|
||||
"Lucky",
|
||||
"Enchanted",
|
||||
"Gleaming",
|
||||
"Grandiose",
|
||||
"Sacred",
|
||||
"Legendary",
|
||||
"Mythic",
|
||||
"Crystalline",
|
||||
"Austere",
|
||||
"Ostentatious",
|
||||
"One True",
|
||||
"Proverbial",
|
||||
"Fearsome",
|
||||
"Deadly",
|
||||
"Benevolent",
|
||||
"Unearthly",
|
||||
"Magnificent",
|
||||
"Iron",
|
||||
"Ormolu",
|
||||
"Puissant"
|
||||
]
|
||||
@@ -0,0 +1,54 @@
|
||||
[
|
||||
"Foreboding",
|
||||
"Foreshadowing",
|
||||
"Nervousness",
|
||||
"Happiness",
|
||||
"Torpor",
|
||||
"Danger",
|
||||
"Craft",
|
||||
"Silence",
|
||||
"Invisibility",
|
||||
"Rapidity",
|
||||
"Pleasure",
|
||||
"Practicality",
|
||||
"Hurting",
|
||||
"Joy",
|
||||
"Petulance",
|
||||
"Intrusion",
|
||||
"Chaos",
|
||||
"Suffering",
|
||||
"Extroversion",
|
||||
"Frenzy",
|
||||
"Sisu",
|
||||
"Solitude",
|
||||
"Punctuality",
|
||||
"Efficiency",
|
||||
"Comfort",
|
||||
"Patience",
|
||||
"Internment",
|
||||
"Incarceration",
|
||||
"Misapprehension",
|
||||
"Loyalty",
|
||||
"Envy",
|
||||
"Acrimony",
|
||||
"Worry",
|
||||
"Fear",
|
||||
"Awe",
|
||||
"Guile",
|
||||
"Prurience",
|
||||
"Fortune",
|
||||
"Perspicacity",
|
||||
"Domination",
|
||||
"Submission",
|
||||
"Fealty",
|
||||
"Hunger",
|
||||
"Despair",
|
||||
"Cruelty",
|
||||
"Grob",
|
||||
"Dignard",
|
||||
"Ra",
|
||||
"the Bone",
|
||||
"Diamonique",
|
||||
"Electrum",
|
||||
"Hydragyrum"
|
||||
]
|
||||
@@ -0,0 +1,234 @@
|
||||
[
|
||||
{"name": "Anhkheg", "level": 6, "item": "chitin"},
|
||||
{"name": "Ant", "level": 0, "item": "antenna"},
|
||||
{"name": "Ape", "level": 4, "item": "ass"},
|
||||
{"name": "Baluchitherium", "level": 14, "item": "ear"},
|
||||
{"name": "Beholder", "level": 10, "item": "eyestalk"},
|
||||
{"name": "Black Pudding", "level": 10, "item": "saliva"},
|
||||
{"name": "Blink Dog", "level": 4, "item": "eyelid"},
|
||||
{"name": "Cub Scout", "level": 1, "item": "neckerchief"},
|
||||
{"name": "Girl Scout", "level": 2, "item": "cookie"},
|
||||
{"name": "Boy Scout", "level": 3, "item": "merit badge"},
|
||||
{"name": "Eagle Scout", "level": 4, "item": "merit badge"},
|
||||
{"name": "Bugbear", "level": 3, "item": "skin"},
|
||||
{"name": "Bugboar", "level": 3, "item": "tusk"},
|
||||
{"name": "Boogie", "level": 3, "item": "slime"},
|
||||
{"name": "Camel", "level": 2, "item": "hump"},
|
||||
{"name": "Carrion Crawler", "level": 3, "item": "egg"},
|
||||
{"name": "Catoblepas", "level": 6, "item": "neck"},
|
||||
{"name": "Centaur", "level": 4, "item": "rib"},
|
||||
{"name": "Centipede", "level": 0, "item": "leg"},
|
||||
{"name": "Cockatrice", "level": 5, "item": "wattle"},
|
||||
{"name": "Couatl", "level": 9, "item": "wing"},
|
||||
{"name": "Crayfish", "level": 0, "item": "antenna"},
|
||||
{"name": "Demogorgon", "level": 53, "item": "tentacle"},
|
||||
{"name": "Jubilex", "level": 17, "item": "gel"},
|
||||
{"name": "Manes", "level": 1, "item": "tooth"},
|
||||
{"name": "Orcus", "level": 27, "item": "wand"},
|
||||
{"name": "Succubus", "level": 6, "item": "bra"},
|
||||
{"name": "Vrock", "level": 8, "item": "neck"},
|
||||
{"name": "Hezrou", "level": 9, "item": "leg"},
|
||||
{"name": "Glabrezu", "level": 10, "item": "collar"},
|
||||
{"name": "Nalfeshnee", "level": 11, "item": "tusk"},
|
||||
{"name": "Marilith", "level": 7, "item": "arm"},
|
||||
{"name": "Balor", "level": 8, "item": "whip"},
|
||||
{"name": "Yeenoghu", "level": 25, "item": "flail"},
|
||||
{"name": "Asmodeus", "level": 52, "item": "leathers"},
|
||||
{"name": "Baalzebul", "level": 43, "item": "pants"},
|
||||
{"name": "Barbed Devil", "level": 8, "item": "flame"},
|
||||
{"name": "Bone Devil", "level": 9, "item": "hook"},
|
||||
{"name": "Dispater", "level": 30, "item": "matches"},
|
||||
{"name": "Erinyes", "level": 6, "item": "thong"},
|
||||
{"name": "Geryon", "level": 30, "item": "cornucopia"},
|
||||
{"name": "Malebranche", "level": 5, "item": "fork"},
|
||||
{"name": "Ice Devil", "level": 11, "item": "snow"},
|
||||
{"name": "Lemure", "level": 3, "item": "blob"},
|
||||
{"name": "Pit Fiend", "level": 13, "item": "seed"},
|
||||
{"name": "Ankylosaurus", "level": 9, "item": "tail"},
|
||||
{"name": "Brontosaurus", "level": 30, "item": "brain"},
|
||||
{"name": "Diplodocus", "level": 24, "item": "fin"},
|
||||
{"name": "Elasmosaurus", "level": 15, "item": "neck"},
|
||||
{"name": "Gorgosaurus", "level": 13, "item": "arm"},
|
||||
{"name": "Iguanadon", "level": 6, "item": "thumb"},
|
||||
{"name": "Megalosaurus", "level": 12, "item": "jaw"},
|
||||
{"name": "Monoclonius", "level": 8, "item": "horn"},
|
||||
{"name": "Pentasaurus", "level": 12, "item": "head"},
|
||||
{"name": "Stegosaurus", "level": 18, "item": "plate"},
|
||||
{"name": "Triceratops", "level": 16, "item": "horn"},
|
||||
{"name": "Tyrannosaurus Rex", "level": 18, "item": "forearm"},
|
||||
{"name": "Djinn", "level": 7, "item": "lamp"},
|
||||
{"name": "Doppelganger", "level": 4, "item": "face"},
|
||||
{"name": "Black Dragon", "level": 7, "item": null},
|
||||
{"name": "Plaid Dragon", "level": 7, "item": "sporrin"},
|
||||
{"name": "Blue Dragon", "level": 9, "item": null},
|
||||
{"name": "Beige Dragon", "level": 9, "item": null},
|
||||
{"name": "Brass Dragon", "level": 7, "item": "pole"},
|
||||
{"name": "Tin Dragon", "level": 8, "item": null},
|
||||
{"name": "Bronze Dragon", "level": 9, "item": "medal"},
|
||||
{"name": "Chromatic Dragon", "level": 16, "item": "scale"},
|
||||
{"name": "Copper Dragon", "level": 8, "item": "loafer"},
|
||||
{"name": "Gold Dragon", "level": 8, "item": "filling"},
|
||||
{"name": "Green Dragon", "level": 8, "item": null},
|
||||
{"name": "Platinum Dragon", "level": 21, "item": null},
|
||||
{"name": "Red Dragon", "level": 10, "item": "cocktail"},
|
||||
{"name": "Silver Dragon", "level": 10, "item": null},
|
||||
{"name": "White Dragon", "level": 6, "item": "tooth"},
|
||||
{"name": "Dragon Turtle", "level": 13, "item": "shell"},
|
||||
{"name": "Dryad", "level": 2, "item": "acorn"},
|
||||
{"name": "Dwarf", "level": 1, "item": "drawers"},
|
||||
{"name": "Eel", "level": 2, "item": "sashimi"},
|
||||
{"name": "Efreet", "level": 10, "item": "cinder"},
|
||||
{"name": "Sand Elemental", "level": 8, "item": "glass"},
|
||||
{"name": "Bacon Elemental", "level": 10, "item": "bit"},
|
||||
{"name": "Porn Elemental", "level": 12, "item": "lube"},
|
||||
{"name": "Cheese Elemental", "level": 14, "item": "curd"},
|
||||
{"name": "Hair Elemental", "level": 16, "item": "follicle"},
|
||||
{"name": "Swamp Elf", "level": 1, "item": "lilypad"},
|
||||
{"name": "Brown Elf", "level": 1, "item": "tusk"},
|
||||
{"name": "Sea Elf", "level": 1, "item": "jerkin"},
|
||||
{"name": "Ettin", "level": 10, "item": "fur"},
|
||||
{"name": "Frog", "level": 0, "item": "leg"},
|
||||
{"name": "Violet Fungi", "level": 3, "item": "spore"},
|
||||
{"name": "Gargoyle", "level": 4, "item": "gravel"},
|
||||
{"name": "Gelatinous Cube", "level": 4, "item": "jam"},
|
||||
{"name": "Ghast", "level": 4, "item": "vomit"},
|
||||
{"name": "Ghost", "level": 10, "item": null},
|
||||
{"name": "Ghoul", "level": 2, "item": "muscle"},
|
||||
{"name": "Humidity Giant", "level": 12, "item": "drops"},
|
||||
{"name": "Beef Giant", "level": 11, "item": "steak"},
|
||||
{"name": "Quartz Giant", "level": 10, "item": "crystal"},
|
||||
{"name": "Porcelain Giant", "level": 9, "item": "fixture"},
|
||||
{"name": "Rice Giant", "level": 8, "item": "grain"},
|
||||
{"name": "Cloud Giant", "level": 12, "item": "condensation"},
|
||||
{"name": "Fire Giant", "level": 11, "item": "cigarettes"},
|
||||
{"name": "Frost Giant", "level": 10, "item": "snowman"},
|
||||
{"name": "Hill Giant", "level": 8, "item": "corpse"},
|
||||
{"name": "Stone Giant", "level": 9, "item": "hatchling"},
|
||||
{"name": "Storm Giant", "level": 15, "item": "barometer"},
|
||||
{"name": "Mini Giant", "level": 4, "item": "pompadour"},
|
||||
{"name": "Gnoll", "level": 2, "item": "collar"},
|
||||
{"name": "Gnome", "level": 1, "item": "hat"},
|
||||
{"name": "Goblin", "level": 1, "item": "ear"},
|
||||
{"name": "Grid Bug", "level": 1, "item": "carapace"},
|
||||
{"name": "Jellyrock", "level": 9, "item": "seedling"},
|
||||
{"name": "Beer Golem", "level": 15, "item": "foam"},
|
||||
{"name": "Oxygen Golem", "level": 17, "item": "platelet"},
|
||||
{"name": "Cardboard Golem", "level": 14, "item": "recycling"},
|
||||
{"name": "Rubber Golem", "level": 16, "item": "ball"},
|
||||
{"name": "Leather Golem", "level": 15, "item": "fob"},
|
||||
{"name": "Gorgon", "level": 8, "item": "testicle"},
|
||||
{"name": "Gray Ooze", "level": 3, "item": "gravy"},
|
||||
{"name": "Green Slime", "level": 2, "item": "sample"},
|
||||
{"name": "Griffon", "level": 7, "item": "nest"},
|
||||
{"name": "Banshee", "level": 7, "item": "larynx"},
|
||||
{"name": "Harpy", "level": 3, "item": "mascara"},
|
||||
{"name": "Hell Hound", "level": 5, "item": "tongue"},
|
||||
{"name": "Hippocampus", "level": 4, "item": "mane"},
|
||||
{"name": "Hippogriff", "level": 3, "item": "egg"},
|
||||
{"name": "Hobgoblin", "level": 1, "item": "patella"},
|
||||
{"name": "Homunculus", "level": 2, "item": "fluid"},
|
||||
{"name": "Hydra", "level": 8, "item": "gyrum"},
|
||||
{"name": "Imp", "level": 2, "item": "tail"},
|
||||
{"name": "Invisible Stalker", "level": 8, "item": null},
|
||||
{"name": "Iron Peasant", "level": 3, "item": "chaff"},
|
||||
{"name": "Jumpskin", "level": 3, "item": "shin"},
|
||||
{"name": "Kobold", "level": 1, "item": "penis"},
|
||||
{"name": "Leprechaun", "level": 1, "item": "wallet"},
|
||||
{"name": "Leucrotta", "level": 6, "item": "hoof"},
|
||||
{"name": "Lich", "level": 11, "item": "crown"},
|
||||
{"name": "Lizard Man", "level": 2, "item": "tail"},
|
||||
{"name": "Lurker", "level": 10, "item": "sac"},
|
||||
{"name": "Manticore", "level": 6, "item": "spike"},
|
||||
{"name": "Mastodon", "level": 12, "item": "tusk"},
|
||||
{"name": "Medusa", "level": 6, "item": "eye"},
|
||||
{"name": "Multicell", "level": 2, "item": "dendrite"},
|
||||
{"name": "Pirate", "level": 1, "item": "booty"},
|
||||
{"name": "Berserker", "level": 1, "item": "shirt"},
|
||||
{"name": "Caveman", "level": 2, "item": "club"},
|
||||
{"name": "Dervish", "level": 1, "item": "robe"},
|
||||
{"name": "Merman", "level": 1, "item": "trident"},
|
||||
{"name": "Mermaid", "level": 1, "item": "gills"},
|
||||
{"name": "Mimic", "level": 9, "item": "hinge"},
|
||||
{"name": "Mind Flayer", "level": 8, "item": "tentacle"},
|
||||
{"name": "Minotaur", "level": 6, "item": "map"},
|
||||
{"name": "Yellow Mold", "level": 1, "item": "spore"},
|
||||
{"name": "Morkoth", "level": 7, "item": "teeth"},
|
||||
{"name": "Mummy", "level": 6, "item": "gauze"},
|
||||
{"name": "Naga", "level": 9, "item": "rattle"},
|
||||
{"name": "Nebbish", "level": 1, "item": "belly"},
|
||||
{"name": "Neo-Otyugh", "level": 11, "item": "organ "},
|
||||
{"name": "Nixie", "level": 1, "item": "webbing"},
|
||||
{"name": "Nymph", "level": 3, "item": "hanky"},
|
||||
{"name": "Ochre Jelly", "level": 6, "item": "nucleus"},
|
||||
{"name": "Octopus", "level": 2, "item": "beak"},
|
||||
{"name": "Ogre", "level": 4, "item": "talon"},
|
||||
{"name": "Ogre Mage", "level": 5, "item": "apparel"},
|
||||
{"name": "Orc", "level": 1, "item": "snout"},
|
||||
{"name": "Otyugh", "level": 7, "item": "organ"},
|
||||
{"name": "Owlbear", "level": 5, "item": "feather"},
|
||||
{"name": "Pegasus", "level": 4, "item": "aileron"},
|
||||
{"name": "Peryton", "level": 4, "item": "antler"},
|
||||
{"name": "Piercer", "level": 3, "item": "tip"},
|
||||
{"name": "Pixie", "level": 1, "item": "dust"},
|
||||
{"name": "Man-o-war", "level": 3, "item": "tentacle"},
|
||||
{"name": "Purple Worm", "level": 15, "item": "dung"},
|
||||
{"name": "Quasit", "level": 3, "item": "tail"},
|
||||
{"name": "Rakshasa", "level": 7, "item": "pajamas"},
|
||||
{"name": "Rat", "level": 0, "item": "tail"},
|
||||
{"name": "Remorhaz", "level": 11, "item": "protrusion"},
|
||||
{"name": "Roc", "level": 18, "item": "wing"},
|
||||
{"name": "Roper", "level": 11, "item": "twine"},
|
||||
{"name": "Rot Grub", "level": 1, "item": "eggsac"},
|
||||
{"name": "Rust Monster", "level": 5, "item": "shavings"},
|
||||
{"name": "Satyr", "level": 5, "item": "hoof"},
|
||||
{"name": "Sea Hag", "level": 3, "item": "wart"},
|
||||
{"name": "Silkie", "level": 3, "item": "fur"},
|
||||
{"name": "Shadow", "level": 3, "item": "silhouette"},
|
||||
{"name": "Shambling Mound", "level": 10, "item": "mulch"},
|
||||
{"name": "Shedu", "level": 9, "item": "hoof"},
|
||||
{"name": "Shrieker", "level": 3, "item": "stalk"},
|
||||
{"name": "Skeleton", "level": 1, "item": "clavicle"},
|
||||
{"name": "Spectre", "level": 7, "item": "vestige"},
|
||||
{"name": "Sphinx", "level": 10, "item": "paw"},
|
||||
{"name": "Spider", "level": 0, "item": "web"},
|
||||
{"name": "Sprite", "level": 1, "item": "can"},
|
||||
{"name": "Stirge", "level": 1, "item": "proboscis"},
|
||||
{"name": "Stun Bear", "level": 5, "item": "tooth"},
|
||||
{"name": "Stun Worm", "level": 2, "item": "trode"},
|
||||
{"name": "Su-monster", "level": 5, "item": "tail"},
|
||||
{"name": "Sylph", "level": 3, "item": "thigh"},
|
||||
{"name": "Titan", "level": 20, "item": "sandal"},
|
||||
{"name": "Trapper", "level": 12, "item": "shag"},
|
||||
{"name": "Treant", "level": 10, "item": "acorn"},
|
||||
{"name": "Triton", "level": 3, "item": "scale"},
|
||||
{"name": "Troglodyte", "level": 2, "item": "tail"},
|
||||
{"name": "Troll", "level": 6, "item": "hide"},
|
||||
{"name": "Umber Hulk", "level": 8, "item": "claw"},
|
||||
{"name": "Unicorn", "level": 4, "item": "blood"},
|
||||
{"name": "Vampire", "level": 8, "item": "pancreas"},
|
||||
{"name": "Wight", "level": 4, "item": "lung"},
|
||||
{"name": "Will-o'-the-Wisp", "level": 9, "item": "wisp"},
|
||||
{"name": "Wraith", "level": 5, "item": "finger"},
|
||||
{"name": "Wyvern", "level": 7, "item": "wing"},
|
||||
{"name": "Xorn", "level": 7, "item": "jaw"},
|
||||
{"name": "Yeti", "level": 4, "item": "fur"},
|
||||
{"name": "Zombie", "level": 2, "item": "forehead"},
|
||||
{"name": "Wasp", "level": 0, "item": "stinger"},
|
||||
{"name": "Rat", "level": 1, "item": "tail"},
|
||||
{"name": "Bunny", "level": 0, "item": "ear"},
|
||||
{"name": "Moth", "level": 0, "item": "dust"},
|
||||
{"name": "Beagle", "level": 0, "item": "collar"},
|
||||
{"name": "Midge", "level": 0, "item": "corpse"},
|
||||
{"name": "Ostrich", "level": 1, "item": "beak"},
|
||||
{"name": "Billy Goat", "level": 1, "item": "beard"},
|
||||
{"name": "Bat", "level": 1, "item": "wing"},
|
||||
{"name": "Koala", "level": 2, "item": "heart"},
|
||||
{"name": "Wolf", "level": 2, "item": "paw"},
|
||||
{"name": "Whippet", "level": 2, "item": "collar"},
|
||||
{"name": "Uruk", "level": 2, "item": "boot"},
|
||||
{"name": "Poroid", "level": 4, "item": "node"},
|
||||
{"name": "Moakum", "level": 8, "item": "frenum"},
|
||||
{"name": "Fly", "level": 0, "item": null},
|
||||
{"name": "Hogbird", "level": 3, "item": "curl"},
|
||||
{"name": "Wolog", "level": 4, "item": "lemma"}
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
[
|
||||
{"name": "Polished", "quality": 1},
|
||||
{"name": "Serrated", "quality": 1},
|
||||
{"name": "Heavy", "quality": 1},
|
||||
{"name": "Pronged", "quality": 2},
|
||||
{"name": "Steely", "quality": 2},
|
||||
{"name": "Vicious", "quality": 3},
|
||||
{"name": "Venomed", "quality": 4},
|
||||
{"name": "Stabbity", "quality": 4},
|
||||
{"name": "Dancing", "quality": 5},
|
||||
{"name": "Invisible", "quality": 6},
|
||||
{"name": "Vorpal", "quality": 7}
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{"name": "Dull", "quality": -2},
|
||||
{"name": "Tarnished", "quality": -1},
|
||||
{"name": "Rusty", "quality": -3},
|
||||
{"name": "Padded", "quality": -5},
|
||||
{"name": "Bent", "quality": -4},
|
||||
{"name": "Mini", "quality": -4},
|
||||
{"name": "Rubber", "quality": -6},
|
||||
{"name": "Nerf", "quality": -7},
|
||||
{"name": "Unbalanced", "quality": -2}
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
[
|
||||
{"name": "Half Orc", "attr": ["HP Max"]},
|
||||
{"name": "Half Man", "attr": ["CHA"]},
|
||||
{"name": "Half Halfling", "attr": ["DEX"]},
|
||||
{"name": "Double Hobbit", "attr": ["STR"]},
|
||||
{"name": "Hob-Hobbit", "attr": ["DEX", "CON"]},
|
||||
{"name": "Low Elf", "attr": ["CON"]},
|
||||
{"name": "Dung Elf", "attr": ["WIS"]},
|
||||
{"name": "Talking Pony", "attr": ["MP Max", "INT"]},
|
||||
{"name": "Gyrognome", "attr": ["DEX"]},
|
||||
{"name": "Lesser Dwarf", "attr": ["CON"]},
|
||||
{"name": "Crested Dwarf", "attr": ["CHA"]},
|
||||
{"name": "Eel Man", "attr": ["DEX"]},
|
||||
{"name": "Panda Man", "attr": ["CON", "STR"]},
|
||||
{"name": "Trans-Kobold", "attr": ["WIS"]},
|
||||
{"name": "Enchanted Motorcycle", "attr": ["MP Max"]},
|
||||
{"name": "Will o' the Wisp", "attr": ["WIS"]},
|
||||
{"name": "Battle-Finch", "attr": ["DEX", "INT"]},
|
||||
{"name": "Double Wookiee", "attr": ["STR"]},
|
||||
{"name": "Skraeling", "attr": ["WIS"]},
|
||||
{"name": "Demicanadian", "attr": ["CON"]},
|
||||
{"name": "Land Squid", "attr": ["STR", "HP Max"]}
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
[
|
||||
{"name": "Parasol", "quality": 0},
|
||||
{"name": "Pie Plate", "quality": 1},
|
||||
{"name": "Garbage Can Lid", "quality": 2},
|
||||
{"name": "Buckler", "quality": 3},
|
||||
{"name": "Plexiglass", "quality": 4},
|
||||
{"name": "Fender", "quality": 4},
|
||||
{"name": "Round Shield", "quality": 5},
|
||||
{"name": "Carapace", "quality": 5},
|
||||
{"name": "Scutum", "quality": 6},
|
||||
{"name": "Propugner", "quality": 6},
|
||||
{"name": "Kite Shield", "quality": 7},
|
||||
{"name": "Pavise", "quality": 8},
|
||||
{"name": "Tower Shield", "quality": 9},
|
||||
{"name": "Baroque Shield", "quality": 11},
|
||||
{"name": "Aegis", "quality": 12},
|
||||
{"name": "Magnetic Field", "quality": 18}
|
||||
]
|
||||
@@ -0,0 +1,39 @@
|
||||
[
|
||||
"Diadem",
|
||||
"Festoon",
|
||||
"Gemstone",
|
||||
"Phial",
|
||||
"Tiara",
|
||||
"Scabbard",
|
||||
"Arrow",
|
||||
"Lens",
|
||||
"Lamp",
|
||||
"Hymnal",
|
||||
"Fleece",
|
||||
"Laurel",
|
||||
"Brooch",
|
||||
"Gimlet",
|
||||
"Cobble",
|
||||
"Albatross",
|
||||
"Brazier",
|
||||
"Bandolier",
|
||||
"Tome",
|
||||
"Garnet",
|
||||
"Amethyst",
|
||||
"Candelabra",
|
||||
"Corset",
|
||||
"Sphere",
|
||||
"Sceptre",
|
||||
"Ankh",
|
||||
"Talisman",
|
||||
"Orb",
|
||||
"Gammel",
|
||||
"Ornament",
|
||||
"Brocade",
|
||||
"Galoon",
|
||||
"Bijou",
|
||||
"Spangle",
|
||||
"Gimcrack",
|
||||
"Hood",
|
||||
"Vulpeculum"
|
||||
]
|
||||
@@ -0,0 +1,49 @@
|
||||
[
|
||||
"Slime Finger",
|
||||
"Rabbit Punch",
|
||||
"Hastiness",
|
||||
"Good Move",
|
||||
"Sadness",
|
||||
"Seasick",
|
||||
"Shoelaces",
|
||||
"Inoculate",
|
||||
"Cone of Annoyance",
|
||||
"Magnetic Orb",
|
||||
"Invisible Hands",
|
||||
"Revolting Cloud",
|
||||
"Aqueous Humor",
|
||||
"Spectral Miasma",
|
||||
"Clever Fellow",
|
||||
"Lockjaw",
|
||||
"History Lesson",
|
||||
"Hydrophobia",
|
||||
"Big Sister",
|
||||
"Cone of Paste",
|
||||
"Mulligan",
|
||||
"Nestor's Bright Idea",
|
||||
"Holy Batpole",
|
||||
"Tumor (Benign)",
|
||||
"Braingate",
|
||||
"Summon a Bitch",
|
||||
"Nonplus",
|
||||
"Animate Nightstand",
|
||||
"Eye of the Troglodyte",
|
||||
"Curse Name",
|
||||
"Dropsy",
|
||||
"Vitreous Humor",
|
||||
"Roger's Grand Illusion",
|
||||
"Covet",
|
||||
"Black Idaho",
|
||||
"Astral Miasma",
|
||||
"Spectral Oyster",
|
||||
"Acrid Hands",
|
||||
"Angioplasty",
|
||||
"Grognor's Big Day Off",
|
||||
"Tumor (Malignant)",
|
||||
"Animate Tunic",
|
||||
"Ursine Armor",
|
||||
"Holy Roller",
|
||||
"Tonsillectomy",
|
||||
"Curse Family",
|
||||
"Infinite Confusion"
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
[
|
||||
"Mr.",
|
||||
"Mrs.",
|
||||
"Sir",
|
||||
"Sgt.",
|
||||
"Ms.",
|
||||
"Captain",
|
||||
"Chief",
|
||||
"Admiral",
|
||||
"Saint"
|
||||
]
|
||||
@@ -0,0 +1,41 @@
|
||||
[
|
||||
{"name": "Stick", "quality": 0},
|
||||
{"name": "Broken Bottle", "quality": 1},
|
||||
{"name": "Shiv", "quality": 1},
|
||||
{"name": "Sprig", "quality": 1},
|
||||
{"name": "Oxgoad", "quality": 1},
|
||||
{"name": "Eelspear", "quality": 2},
|
||||
{"name": "Bowie Knife", "quality": 2},
|
||||
{"name": "Claw Hammer", "quality": 2},
|
||||
{"name": "Handpeen", "quality": 2},
|
||||
{"name": "Andiron", "quality": 3},
|
||||
{"name": "Hatchet", "quality": 3},
|
||||
{"name": "Tomahawk", "quality": 3},
|
||||
{"name": "Hackbarm", "quality": 3},
|
||||
{"name": "Crowbar", "quality": 4},
|
||||
{"name": "Mace", "quality": 4},
|
||||
{"name": "Battleadze", "quality": 4},
|
||||
{"name": "Leafmace", "quality": 5},
|
||||
{"name": "Shortsword", "quality": 5},
|
||||
{"name": "Longiron", "quality": 5},
|
||||
{"name": "Poachard", "quality": 5},
|
||||
{"name": "Baselard", "quality": 5},
|
||||
{"name": "Whinyard", "quality": 6},
|
||||
{"name": "Blunderbuss", "quality": 6},
|
||||
{"name": "Longsword", "quality": 6},
|
||||
{"name": "Crankbow", "quality": 6},
|
||||
{"name": "Blibo", "quality": 7},
|
||||
{"name": "Broadsword", "quality": 7},
|
||||
{"name": "Kreen", "quality": 7},
|
||||
{"name": "Warhammer", "quality": 7},
|
||||
{"name": "Morning Star", "quality": 8},
|
||||
{"name": "Pole-adze", "quality": 8},
|
||||
{"name": "Spontoon", "quality": 8},
|
||||
{"name": "Bastard Sword", "quality": 9},
|
||||
{"name": "Peen-arm", "quality": 9},
|
||||
{"name": "Culverin", "quality": 10},
|
||||
{"name": "Lance", "quality": 10},
|
||||
{"name": "Halberd", "quality": 11},
|
||||
{"name": "Poleax", "quality": 12},
|
||||
{"name": "Bandyclef", "quality": 15}
|
||||
]
|
||||
@@ -0,0 +1,249 @@
|
||||
import json
|
||||
import sys
|
||||
import typing as T
|
||||
from dataclasses import dataclass, fields
|
||||
from importlib import resources
|
||||
from pathlib import Path
|
||||
|
||||
from pqcli.config import (
|
||||
Class,
|
||||
EquipmentPreset,
|
||||
Modifier,
|
||||
Monster,
|
||||
Race,
|
||||
StatType,
|
||||
)
|
||||
|
||||
TARGET_PLACEHOLDER = "{target}"
|
||||
|
||||
|
||||
class DataError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GameData:
|
||||
spells: T.Tuple[str, ...]
|
||||
offense_attrib: T.Tuple[Modifier, ...]
|
||||
defense_attrib: T.Tuple[Modifier, ...]
|
||||
offense_bad: T.Tuple[Modifier, ...]
|
||||
defense_bad: T.Tuple[Modifier, ...]
|
||||
shields: T.Tuple[EquipmentPreset, ...]
|
||||
armors: T.Tuple[EquipmentPreset, ...]
|
||||
weapons: T.Tuple[EquipmentPreset, ...]
|
||||
specials: T.Tuple[str, ...]
|
||||
item_attrib: T.Tuple[str, ...]
|
||||
item_ofs: T.Tuple[str, ...]
|
||||
boring_items: T.Tuple[str, ...]
|
||||
monsters: T.Tuple[Monster, ...]
|
||||
races: T.Tuple[Race, ...]
|
||||
classes: T.Tuple[Class, ...]
|
||||
titles: T.Tuple[str, ...]
|
||||
impressive_titles: T.Tuple[str, ...]
|
||||
combat_verbs: T.Tuple[str, ...]
|
||||
|
||||
|
||||
DATA_FILES = tuple(field.name for field in fields(GameData))
|
||||
|
||||
|
||||
class _Context:
|
||||
def __init__(self, path: str) -> None:
|
||||
self.path = path
|
||||
|
||||
def error(self, message: str) -> DataError:
|
||||
return DataError(f"{self.path}: {message}")
|
||||
|
||||
|
||||
def _check_keys(
|
||||
ctx: _Context, where: str, obj: T.Any, keys: T.Sequence[str]
|
||||
) -> T.Dict[str, T.Any]:
|
||||
if not isinstance(obj, dict):
|
||||
raise ctx.error(f"{where} must be an object")
|
||||
missing = [key for key in keys if key not in obj]
|
||||
if missing:
|
||||
raise ctx.error(f"{where} is missing {', '.join(map(repr, missing))}")
|
||||
extra = [key for key in obj if key not in keys]
|
||||
if extra:
|
||||
raise ctx.error(f"{where} has unknown {', '.join(map(repr, extra))}")
|
||||
return obj
|
||||
|
||||
|
||||
def _str(ctx: _Context, where: str, value: T.Any) -> str:
|
||||
if not isinstance(value, str):
|
||||
raise ctx.error(f"{where} must be a string")
|
||||
return value
|
||||
|
||||
|
||||
def _int(ctx: _Context, where: str, value: T.Any) -> int:
|
||||
if isinstance(value, bool) or not isinstance(value, int):
|
||||
raise ctx.error(f"{where} must be an integer")
|
||||
return value
|
||||
|
||||
|
||||
def _stat(ctx: _Context, where: str, value: T.Any) -> StatType:
|
||||
_str(ctx, where, value)
|
||||
for stat in StatType:
|
||||
if value.lower() in (stat.value.lower(), stat.name):
|
||||
return stat
|
||||
allowed = ", ".join(stat.value for stat in StatType)
|
||||
raise ctx.error(f"{where}: unknown stat {value!r} (use one of {allowed})")
|
||||
|
||||
|
||||
def _parse_str(ctx: _Context, where: str, item: T.Any) -> str:
|
||||
return _str(ctx, where, item)
|
||||
|
||||
|
||||
def _parse_verb(ctx: _Context, where: str, item: T.Any) -> str:
|
||||
verb = _str(ctx, where, item)
|
||||
if TARGET_PLACEHOLDER not in verb:
|
||||
raise ctx.error(f"{where} must contain {TARGET_PLACEHOLDER}")
|
||||
return verb
|
||||
|
||||
|
||||
def _parse_modifier(ctx: _Context, where: str, item: T.Any) -> Modifier:
|
||||
obj = _check_keys(ctx, where, item, ["name", "quality"])
|
||||
return Modifier(
|
||||
_str(ctx, f"{where}.name", obj["name"]),
|
||||
_int(ctx, f"{where}.quality", obj["quality"]),
|
||||
)
|
||||
|
||||
|
||||
def _parse_preset(ctx: _Context, where: str, item: T.Any) -> EquipmentPreset:
|
||||
obj = _check_keys(ctx, where, item, ["name", "quality"])
|
||||
return EquipmentPreset(
|
||||
_str(ctx, f"{where}.name", obj["name"]),
|
||||
_int(ctx, f"{where}.quality", obj["quality"]),
|
||||
)
|
||||
|
||||
|
||||
def _parse_monster(ctx: _Context, where: str, item: T.Any) -> Monster:
|
||||
obj = _check_keys(ctx, where, item, ["name", "level", "item"])
|
||||
loot = obj["item"]
|
||||
return Monster(
|
||||
_str(ctx, f"{where}.name", obj["name"]),
|
||||
_int(ctx, f"{where}.level", obj["level"]),
|
||||
None if loot is None else _str(ctx, f"{where}.item", loot),
|
||||
)
|
||||
|
||||
|
||||
def _parse_attrs(ctx: _Context, where: str, value: T.Any) -> T.List[StatType]:
|
||||
if not isinstance(value, list):
|
||||
raise ctx.error(f"{where} must be a list of stats")
|
||||
return [_stat(ctx, f"{where}[{i}]", stat) for i, stat in enumerate(value)]
|
||||
|
||||
|
||||
def _parse_race(ctx: _Context, where: str, item: T.Any) -> Race:
|
||||
obj = _check_keys(ctx, where, item, ["name", "attr"])
|
||||
return Race(
|
||||
_str(ctx, f"{where}.name", obj["name"]),
|
||||
_parse_attrs(ctx, f"{where}.attr", obj["attr"]),
|
||||
)
|
||||
|
||||
|
||||
def _parse_class(ctx: _Context, where: str, item: T.Any) -> Class:
|
||||
obj = _check_keys(ctx, where, item, ["name", "attr"])
|
||||
return Class(
|
||||
_str(ctx, f"{where}.name", obj["name"]),
|
||||
_parse_attrs(ctx, f"{where}.attr", obj["attr"]),
|
||||
)
|
||||
|
||||
|
||||
_PARSERS: T.Dict[str, T.Callable[[_Context, str, T.Any], T.Any]] = {
|
||||
"spells": _parse_str,
|
||||
"offense_attrib": _parse_modifier,
|
||||
"defense_attrib": _parse_modifier,
|
||||
"offense_bad": _parse_modifier,
|
||||
"defense_bad": _parse_modifier,
|
||||
"shields": _parse_preset,
|
||||
"armors": _parse_preset,
|
||||
"weapons": _parse_preset,
|
||||
"specials": _parse_str,
|
||||
"item_attrib": _parse_str,
|
||||
"item_ofs": _parse_str,
|
||||
"boring_items": _parse_str,
|
||||
"monsters": _parse_monster,
|
||||
"races": _parse_race,
|
||||
"classes": _parse_class,
|
||||
"titles": _parse_str,
|
||||
"impressive_titles": _parse_str,
|
||||
"combat_verbs": _parse_verb,
|
||||
}
|
||||
assert set(_PARSERS) == set(DATA_FILES)
|
||||
|
||||
|
||||
def _parse(name: str, path: str, text: str) -> T.Tuple[T.Any, ...]:
|
||||
ctx = _Context(path)
|
||||
try:
|
||||
value = json.loads(text)
|
||||
except json.JSONDecodeError as ex:
|
||||
raise ctx.error(f"invalid JSON: {ex}") from ex
|
||||
if not isinstance(value, list):
|
||||
raise ctx.error("must be a list")
|
||||
if not value:
|
||||
raise ctx.error("must not be empty")
|
||||
parser = _PARSERS[name]
|
||||
return tuple(parser(ctx, f"[{i}]", item) for i, item in enumerate(value))
|
||||
|
||||
|
||||
def _builtin_dir() -> T.Any:
|
||||
return resources.files("pqcli") / "data"
|
||||
|
||||
|
||||
def _read_text(path: T.Any, display: str) -> str:
|
||||
try:
|
||||
return T.cast(str, path.read_text(encoding="utf-8"))
|
||||
except UnicodeDecodeError as ex:
|
||||
raise DataError(f"{display}: not valid UTF-8: {ex}") from ex
|
||||
except OSError as ex:
|
||||
raise DataError(f"{display}: cannot read: {ex}") from ex
|
||||
|
||||
|
||||
def load_game_data(override_dir: T.Optional[Path] = None) -> GameData:
|
||||
"""Load the built-in word lists, replacing any that override_dir has.
|
||||
|
||||
A missing override_dir is fine. Each <name>.json in it replaces the
|
||||
whole built-in list of that name.
|
||||
"""
|
||||
overrides: T.Set[str] = set()
|
||||
if override_dir is not None and override_dir.exists():
|
||||
if not override_dir.is_dir():
|
||||
raise DataError(f"{override_dir}: not a directory")
|
||||
for path in sorted(override_dir.glob("*.json")):
|
||||
if path.stem in DATA_FILES:
|
||||
overrides.add(path.stem)
|
||||
else:
|
||||
print(
|
||||
f"pqcli: ignoring {path}: not a known data file "
|
||||
f"(known: {', '.join(DATA_FILES)})",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
values: T.Dict[str, T.Tuple[T.Any, ...]] = {}
|
||||
for name in DATA_FILES:
|
||||
if name in overrides:
|
||||
assert override_dir is not None
|
||||
source: T.Any = override_dir / f"{name}.json"
|
||||
display = str(source)
|
||||
else:
|
||||
source = _builtin_dir() / f"{name}.json"
|
||||
display = f"<built-in>/{name}.json"
|
||||
values[name] = _parse(name, display, _read_text(source, display))
|
||||
return GameData(**values)
|
||||
|
||||
|
||||
def dump_builtin_data(target_dir: Path) -> T.List[Path]:
|
||||
"""Copy the built-in data files to target_dir, never overwriting."""
|
||||
targets = [target_dir / f"{name}.json" for name in DATA_FILES]
|
||||
existing = [path for path in targets if path.exists()]
|
||||
if existing:
|
||||
raise DataError(
|
||||
"refusing to overwrite "
|
||||
+ ", ".join(str(path) for path in existing)
|
||||
)
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
for name, path in zip(DATA_FILES, targets):
|
||||
source = _builtin_dir() / f"{name}.json"
|
||||
path.write_text(
|
||||
_read_text(source, f"<built-in>/{name}.json"), encoding="utf-8"
|
||||
)
|
||||
return targets
|
||||
+84
-77
@@ -1,4 +1,5 @@
|
||||
import datetime
|
||||
import functools
|
||||
import itertools
|
||||
import logging
|
||||
import math
|
||||
@@ -8,24 +9,7 @@ from dataclasses import dataclass
|
||||
|
||||
from pqcli import random
|
||||
from pqcli.config import (
|
||||
ARMORS,
|
||||
BORING_ITEMS,
|
||||
CLASSES,
|
||||
DEFENSE_ATTRIB,
|
||||
DEFENSE_BAD,
|
||||
IMPRESSIVE_TITLES,
|
||||
ITEM_ATTRIB,
|
||||
ITEM_OFS,
|
||||
MONSTERS,
|
||||
OFFENSE_ATTRIB,
|
||||
OFFENSE_BAD,
|
||||
PRIME_STATS,
|
||||
RACES,
|
||||
SHIELDS,
|
||||
SPECIALS,
|
||||
SPELLS,
|
||||
TITLES,
|
||||
WEAPONS,
|
||||
Class,
|
||||
EquipmentPreset,
|
||||
EquipmentType,
|
||||
@@ -34,6 +18,7 @@ from pqcli.config import (
|
||||
Race,
|
||||
StatType,
|
||||
)
|
||||
from pqcli.game_data import TARGET_PLACEHOLDER, GameData
|
||||
from pqcli.lingo import (
|
||||
act_name,
|
||||
big,
|
||||
@@ -388,7 +373,7 @@ class Player(SignalMixin):
|
||||
def equip_price(self) -> int:
|
||||
return 5 * (self.level**2) + 10 * self.level + 20
|
||||
|
||||
def level_up(self) -> None:
|
||||
def level_up(self, data: GameData) -> None:
|
||||
self.level += 1
|
||||
logger.info("Leveled up to level %d!", self.level)
|
||||
self.stats.increment(
|
||||
@@ -401,7 +386,7 @@ class Player(SignalMixin):
|
||||
)
|
||||
self.win_stat()
|
||||
self.win_stat()
|
||||
self.win_spell()
|
||||
self.win_spell(data)
|
||||
self.exp_bar.reset(level_up_time(self.level))
|
||||
self.emit("level_up")
|
||||
|
||||
@@ -427,31 +412,34 @@ class Player(SignalMixin):
|
||||
self.inventory.set_capacity(10 + self.stats[StatType.strength])
|
||||
return True
|
||||
|
||||
def win_spell(self) -> None:
|
||||
def win_spell(self, data: GameData) -> None:
|
||||
spells = data.spells
|
||||
self.spell_book.add(
|
||||
SPELLS[
|
||||
spells[
|
||||
random.below_low(
|
||||
min(self.stats[StatType.wisdom] + self.level, len(SPELLS))
|
||||
min(self.stats[StatType.wisdom] + self.level, len(spells))
|
||||
)
|
||||
],
|
||||
1,
|
||||
)
|
||||
|
||||
def win_equipment(self) -> None:
|
||||
def win_equipment(self, data: GameData) -> None:
|
||||
choice = random.choice(list(EquipmentType))
|
||||
|
||||
stuff: T.List[EquipmentPreset]
|
||||
better: T.List[Modifier]
|
||||
worse: T.List[Modifier]
|
||||
stuff: T.Sequence[EquipmentPreset]
|
||||
better: T.Sequence[Modifier]
|
||||
worse: T.Sequence[Modifier]
|
||||
|
||||
if choice == EquipmentType.weapon:
|
||||
stuff = WEAPONS
|
||||
better = OFFENSE_ATTRIB
|
||||
worse = OFFENSE_BAD
|
||||
stuff = data.weapons
|
||||
better = data.offense_attrib
|
||||
worse = data.offense_bad
|
||||
else:
|
||||
stuff = SHIELDS if choice == EquipmentType.shield else ARMORS
|
||||
better = DEFENSE_ATTRIB
|
||||
worse = DEFENSE_BAD
|
||||
stuff = (
|
||||
data.shields if choice == EquipmentType.shield else data.armors
|
||||
)
|
||||
better = data.defense_attrib
|
||||
worse = data.defense_bad
|
||||
|
||||
equipment = pick_equipment(stuff, self.level)
|
||||
name = equipment.name
|
||||
@@ -479,13 +467,14 @@ class Player(SignalMixin):
|
||||
logger.info("Gained %s %s", name, choice.value)
|
||||
self.equipment.put(choice, name)
|
||||
|
||||
def win_item(self) -> None:
|
||||
self.inventory.add(special_item(), 1)
|
||||
def win_item(self, data: GameData) -> None:
|
||||
self.inventory.add(special_item(data), 1)
|
||||
|
||||
|
||||
class Simulation:
|
||||
def __init__(self, player: Player) -> None:
|
||||
def __init__(self, player: Player, data: GameData) -> None:
|
||||
self.player = player
|
||||
self.data = data
|
||||
self.last_tick = datetime.datetime.now()
|
||||
|
||||
def tick(self, elapsed: float = 100.0) -> None:
|
||||
@@ -526,7 +515,7 @@ class Simulation:
|
||||
gain = self.player.task and isinstance(self.player.task, KillTask)
|
||||
if gain:
|
||||
if self.player.exp_bar.done:
|
||||
self.player.level_up()
|
||||
self.player.level_up(self.data)
|
||||
else:
|
||||
self.player.exp_bar.increment(self.player.task_bar.max_ / 1000)
|
||||
|
||||
@@ -561,7 +550,7 @@ class Simulation:
|
||||
or self.player.task.monster.item is None
|
||||
):
|
||||
# npc
|
||||
self.player.win_item()
|
||||
self.player.win_item(self.data)
|
||||
elif self.player.task.monster.item:
|
||||
self.player.inventory.add(
|
||||
(
|
||||
@@ -575,7 +564,7 @@ class Simulation:
|
||||
elif isinstance(self.player.task, BuyTask):
|
||||
# buy some equipment
|
||||
self.player.inventory.add_gold(-self.player.equip_price())
|
||||
self.player.win_equipment()
|
||||
self.player.win_equipment(self.data)
|
||||
|
||||
elif isinstance(self.player.task, (HeadingToMarketTask, SellTask)):
|
||||
if isinstance(self.player.task, SellTask):
|
||||
@@ -623,7 +612,9 @@ class Simulation:
|
||||
else:
|
||||
self.player.set_task(
|
||||
monster_task(
|
||||
self.player.level, self.player.quest_book.monster
|
||||
self.data,
|
||||
self.player.level,
|
||||
self.player.quest_book.monster,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -633,8 +624,8 @@ class Simulation:
|
||||
60 * 60 * (1 + 5 * self.player.quest_book.act)
|
||||
)
|
||||
if self.player.quest_book.act > 1:
|
||||
self.player.win_item()
|
||||
self.player.win_equipment()
|
||||
self.player.win_item(self.data)
|
||||
self.player.win_equipment(self.data)
|
||||
|
||||
def complete_quest(self) -> None:
|
||||
self.player.quest_book.quest_bar.reset(50 + random.below_low(1000))
|
||||
@@ -644,10 +635,10 @@ class Simulation:
|
||||
)
|
||||
random.choice(
|
||||
[
|
||||
self.player.win_spell,
|
||||
self.player.win_equipment,
|
||||
functools.partial(self.player.win_spell, self.data),
|
||||
functools.partial(self.player.win_equipment, self.data),
|
||||
self.player.win_stat,
|
||||
self.player.win_item,
|
||||
functools.partial(self.player.win_item, self.data),
|
||||
]
|
||||
)()
|
||||
|
||||
@@ -656,19 +647,21 @@ class Simulation:
|
||||
choice = random.below(5)
|
||||
if choice == 0:
|
||||
self.player.quest_book.monster = unnamed_monster(
|
||||
self.player.level, iterations=3
|
||||
self.data, self.player.level, iterations=3
|
||||
)
|
||||
caption = "Exterminate " + definite(
|
||||
self.player.quest_book.monster.name, 2
|
||||
)
|
||||
elif choice == 1:
|
||||
caption = "Seek " + definite(interesting_item(), 1)
|
||||
caption = "Seek " + definite(interesting_item(self.data), 1)
|
||||
elif choice == 2:
|
||||
caption = "Deliver this " + boring_item()
|
||||
caption = "Deliver this " + boring_item(self.data)
|
||||
elif choice == 3:
|
||||
caption = "Fetch me " + indefinite(boring_item(), 1)
|
||||
caption = "Fetch me " + indefinite(boring_item(self.data), 1)
|
||||
elif choice == 4:
|
||||
monster = unnamed_monster(self.player.level, iterations=1)
|
||||
monster = unnamed_monster(
|
||||
self.data, self.player.level, iterations=1
|
||||
)
|
||||
caption = "Placate " + definite(monster.name, 2)
|
||||
else:
|
||||
raise AssertionError
|
||||
@@ -711,7 +704,7 @@ class Simulation:
|
||||
)
|
||||
)
|
||||
|
||||
nemesis = named_monster(self.player.level + 3)
|
||||
nemesis = named_monster(self.data, self.player.level + 3)
|
||||
|
||||
enqueue(
|
||||
RegularTask(
|
||||
@@ -760,7 +753,7 @@ class Simulation:
|
||||
)
|
||||
|
||||
elif choice == 2:
|
||||
nemesis = impressive_guy()
|
||||
nemesis = impressive_guy(self.data)
|
||||
enqueue(
|
||||
RegularTask(
|
||||
"Oh sweet relief! "
|
||||
@@ -777,7 +770,8 @@ class Simulation:
|
||||
)
|
||||
enqueue(
|
||||
RegularTask(
|
||||
f"You forget your {boring_item()} and go back to get it",
|
||||
f"You forget your {boring_item(self.data)} "
|
||||
"and go back to get it",
|
||||
2000,
|
||||
)
|
||||
)
|
||||
@@ -807,46 +801,50 @@ class Simulation:
|
||||
)
|
||||
|
||||
|
||||
def special_item() -> str:
|
||||
return interesting_item() + " of " + T.cast(str, random.choice(ITEM_OFS))
|
||||
|
||||
|
||||
def interesting_item() -> str:
|
||||
def special_item(data: GameData) -> str:
|
||||
return (
|
||||
T.cast(str, random.choice(ITEM_ATTRIB))
|
||||
+ " "
|
||||
+ T.cast(str, random.choice(SPECIALS))
|
||||
interesting_item(data)
|
||||
+ " of "
|
||||
+ T.cast(str, random.choice(data.item_ofs))
|
||||
)
|
||||
|
||||
|
||||
def boring_item() -> str:
|
||||
return T.cast(str, random.choice(BORING_ITEMS))
|
||||
def interesting_item(data: GameData) -> str:
|
||||
return (
|
||||
T.cast(str, random.choice(data.item_attrib))
|
||||
+ " "
|
||||
+ T.cast(str, random.choice(data.specials))
|
||||
)
|
||||
|
||||
|
||||
def impressive_guy() -> str:
|
||||
return T.cast(str, random.choice(IMPRESSIVE_TITLES)) + (
|
||||
" of the " + T.cast(Race, random.choice(RACES)).name
|
||||
def boring_item(data: GameData) -> str:
|
||||
return T.cast(str, random.choice(data.boring_items))
|
||||
|
||||
|
||||
def impressive_guy(data: GameData) -> str:
|
||||
return T.cast(str, random.choice(data.impressive_titles)) + (
|
||||
" of the " + T.cast(Race, random.choice(data.races)).name
|
||||
if random.below(2)
|
||||
else " of " + generate_name()
|
||||
)
|
||||
|
||||
|
||||
def unnamed_monster(level: int, iterations: int) -> Monster:
|
||||
result = T.cast(Monster, random.choice(MONSTERS))
|
||||
def unnamed_monster(data: GameData, level: int, iterations: int) -> Monster:
|
||||
result = T.cast(Monster, random.choice(data.monsters))
|
||||
for _ in range(iterations):
|
||||
alternative = T.cast(Monster, random.choice(MONSTERS))
|
||||
alternative = T.cast(Monster, random.choice(data.monsters))
|
||||
if abs(level - alternative.level) < abs(level - result.level):
|
||||
result = alternative
|
||||
return result
|
||||
|
||||
|
||||
def named_monster(level: int) -> str:
|
||||
monster = unnamed_monster(level, iterations=4)
|
||||
def named_monster(data: GameData, level: int) -> str:
|
||||
monster = unnamed_monster(data, level, iterations=4)
|
||||
return generate_name() + " the " + monster.name
|
||||
|
||||
|
||||
def pick_equipment(
|
||||
source: T.List[EquipmentPreset], goal: int
|
||||
source: T.Sequence[EquipmentPreset], goal: int
|
||||
) -> EquipmentPreset:
|
||||
result = T.cast(EquipmentPreset, random.choice(source))
|
||||
for _ in range(5):
|
||||
@@ -857,7 +855,7 @@ def pick_equipment(
|
||||
|
||||
|
||||
def monster_task(
|
||||
player_level: int, quest_monster: T.Optional[Monster]
|
||||
data: GameData, player_level: int, quest_monster: T.Optional[Monster]
|
||||
) -> KillTask:
|
||||
level = player_level
|
||||
for _ in range(level):
|
||||
@@ -870,12 +868,14 @@ def monster_task(
|
||||
monster: T.Optional[Monster] = None
|
||||
if random.odds(1, 25):
|
||||
# use an NPC every once in a while
|
||||
race = random.choice(RACES)
|
||||
race = random.choice(data.races)
|
||||
if random.odds(1, 2):
|
||||
result = "passing " + race.name + " " + random.choice(CLASSES).name
|
||||
result = (
|
||||
"passing " + race.name + " " + random.choice(data.classes).name
|
||||
)
|
||||
else:
|
||||
result = (
|
||||
random.choice_low(TITLES)
|
||||
random.choice_low(data.titles)
|
||||
+ " "
|
||||
+ generate_name()
|
||||
+ " the "
|
||||
@@ -890,7 +890,7 @@ def monster_task(
|
||||
lev = monster.level
|
||||
else:
|
||||
# pick the monster out of so many random ones closest to the level we want
|
||||
monster = unnamed_monster(level, iterations=5)
|
||||
monster = unnamed_monster(data, level, iterations=5)
|
||||
result = monster.name
|
||||
lev = monster.level
|
||||
|
||||
@@ -929,7 +929,14 @@ def monster_task(
|
||||
result = indefinite(result, qty)
|
||||
|
||||
duration = (2 * 3 * level * 1000) // player_level
|
||||
return KillTask(f"Executing {result}", duration, monster=monster)
|
||||
# with a single verb, skip the roll so the stock RNG stream is unchanged
|
||||
verb = (
|
||||
data.combat_verbs[0]
|
||||
if len(data.combat_verbs) == 1
|
||||
else T.cast(str, random.choice(data.combat_verbs))
|
||||
)
|
||||
description = verb.replace(TARGET_PLACEHOLDER, result)
|
||||
return KillTask(description, duration, monster=monster)
|
||||
|
||||
|
||||
class StatsBuilder:
|
||||
|
||||
@@ -2,6 +2,7 @@ import abc
|
||||
import argparse
|
||||
import typing as T
|
||||
|
||||
from pqcli.game_data import GameData
|
||||
from pqcli.mechanic import Player
|
||||
from pqcli.roster import Roster
|
||||
|
||||
@@ -12,8 +13,10 @@ class BaseUserInterface(abc.ABC):
|
||||
roster: Roster,
|
||||
player: T.Optional[Player],
|
||||
args: argparse.Namespace,
|
||||
data: GameData,
|
||||
) -> None:
|
||||
self.roster = roster
|
||||
self.data = data
|
||||
self.args = args
|
||||
self.player = player
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ import typing as T
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from pqcli import lingo
|
||||
from pqcli.config import CLASSES, PRIME_STATS, RACES
|
||||
from pqcli.config import PRIME_STATS
|
||||
from pqcli.game_data import GameData
|
||||
from pqcli.mechanic import Player, Simulation, StatsBuilder, create_player
|
||||
from pqcli.roster import Roster
|
||||
from pqcli.ui.base import BaseUserInterface
|
||||
@@ -37,8 +38,9 @@ class BasicUserInterface(BaseUserInterface):
|
||||
roster: Roster,
|
||||
player: T.Optional[Player],
|
||||
args: argparse.Namespace,
|
||||
data: GameData,
|
||||
) -> None:
|
||||
super().__init__(roster, player, args)
|
||||
super().__init__(roster, player, args, data)
|
||||
|
||||
def signal_handler(sig: T.Any, frame: T.Any) -> None:
|
||||
logging.info("Quitting")
|
||||
@@ -107,8 +109,10 @@ class BasicUserInterface(BaseUserInterface):
|
||||
print("Cancelled.")
|
||||
return None
|
||||
|
||||
race = self.menu([(race, race.name) for race in RACES])
|
||||
class_ = self.menu([(class_, class_.name) for class_ in CLASSES])
|
||||
race = self.menu([(race, race.name) for race in self.data.races])
|
||||
class_ = self.menu(
|
||||
[(class_, class_.name) for class_ in self.data.classes]
|
||||
)
|
||||
|
||||
stats_builder = StatsBuilder()
|
||||
while True:
|
||||
@@ -131,7 +135,7 @@ class BasicUserInterface(BaseUserInterface):
|
||||
|
||||
def play(self, player: Player) -> None:
|
||||
print(f"Playing as {player.name}")
|
||||
simulation = Simulation(player)
|
||||
simulation = Simulation(player, self.data)
|
||||
last_tick = datetime.now()
|
||||
last_level = 0
|
||||
while True:
|
||||
|
||||
@@ -7,6 +7,7 @@ import typing as T
|
||||
|
||||
from pqcli import lingo
|
||||
from pqcli.config import Class, Race
|
||||
from pqcli.game_data import GameData
|
||||
from pqcli.mechanic import Player, Stats, create_player
|
||||
from pqcli.roster import Roster
|
||||
from pqcli.ui.base import BaseUserInterface
|
||||
@@ -40,9 +41,13 @@ class StopMainLoop(Exception):
|
||||
|
||||
class CursesUserInterface(BaseUserInterface):
|
||||
def __init__(
|
||||
self, roster: Roster, player: Player, args: argparse.Namespace
|
||||
self,
|
||||
roster: Roster,
|
||||
player: Player,
|
||||
args: argparse.Namespace,
|
||||
data: GameData,
|
||||
) -> None:
|
||||
super().__init__(roster, player, args)
|
||||
super().__init__(roster, player, args, data)
|
||||
os.environ.setdefault("ESCDELAY", "25")
|
||||
self._screen = curses.initscr()
|
||||
self._screen.nodelay(True)
|
||||
@@ -119,7 +124,7 @@ class CursesUserInterface(BaseUserInterface):
|
||||
def _switch_to_create_char_race_view(
|
||||
self, player_name: str, race: T.Optional[Race] = None
|
||||
) -> None:
|
||||
view = ChooseCharacterRaceView(self._screen, race)
|
||||
view = ChooseCharacterRaceView(self._screen, self.data.races, race)
|
||||
view.on_cancel += lambda: self._switch_to_create_char_name_view(
|
||||
player_name
|
||||
)
|
||||
@@ -131,7 +136,9 @@ class CursesUserInterface(BaseUserInterface):
|
||||
def _switch_to_create_char_class_view(
|
||||
self, player_name: str, race: Race, class_: T.Optional[Class] = None
|
||||
) -> None:
|
||||
view = ChooseCharacterClassView(self._screen, class_)
|
||||
view = ChooseCharacterClassView(
|
||||
self._screen, self.data.classes, class_
|
||||
)
|
||||
view.on_cancel += lambda: self._switch_to_create_char_race_view(
|
||||
player_name, race
|
||||
)
|
||||
@@ -177,7 +184,9 @@ class CursesUserInterface(BaseUserInterface):
|
||||
self._switch_view(view)
|
||||
|
||||
def _switch_to_game_view(self, player: Player) -> None:
|
||||
view = GameView(self._screen, self.roster, player, self.args)
|
||||
view = GameView(
|
||||
self._screen, self.roster, player, self.args, self.data
|
||||
)
|
||||
view.on_exit += self._switch_to_roster_view
|
||||
self._switch_view(view)
|
||||
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import functools
|
||||
import typing as T
|
||||
|
||||
from pqcli.config import CLASSES, Class
|
||||
from pqcli.config import Class
|
||||
from pqcli.ui.curses.util import KEYS_CANCEL, Choice
|
||||
from pqcli.ui.curses.views.menu_view import MenuView
|
||||
|
||||
|
||||
class ChooseCharacterClassView(MenuView):
|
||||
def __init__(
|
||||
self, screen: T.Any, class_: T.Optional[Class] = None
|
||||
self,
|
||||
screen: T.Any,
|
||||
classes: T.Sequence[Class],
|
||||
class_: T.Optional[Class] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
screen,
|
||||
"Choose character class",
|
||||
CLASSES.index(class_) if class_ is not None else 0,
|
||||
classes.index(class_) if class_ is not None else 0,
|
||||
)
|
||||
|
||||
for y, class_ in enumerate(CLASSES, 1):
|
||||
for y, class_ in enumerate(classes, 1):
|
||||
key: T.Optional[str] = str(y % 10) if y <= 10 else None
|
||||
self._choices.append(
|
||||
Choice(
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
import functools
|
||||
import typing as T
|
||||
|
||||
from pqcli.config import RACES, Race
|
||||
from pqcli.config import Race
|
||||
from pqcli.ui.curses.util import KEYS_CANCEL, Choice
|
||||
from pqcli.ui.curses.views.menu_view import MenuView
|
||||
|
||||
|
||||
class ChooseCharacterRaceView(MenuView):
|
||||
def __init__(self, screen: T.Any, race: T.Optional[Race] = None) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
screen: T.Any,
|
||||
races: T.Sequence[Race],
|
||||
race: T.Optional[Race] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
screen,
|
||||
"Choose character race",
|
||||
RACES.index(race) if race is not None else 0,
|
||||
races.index(race) if race is not None else 0,
|
||||
)
|
||||
|
||||
for y, race in enumerate(RACES, 1):
|
||||
for y, race in enumerate(races, 1):
|
||||
key: T.Optional[str] = str(y % 10) if y <= 10 else None
|
||||
self._choices.append(
|
||||
Choice(
|
||||
|
||||
@@ -3,6 +3,7 @@ import curses
|
||||
import datetime
|
||||
import typing as T
|
||||
|
||||
from pqcli.game_data import GameData
|
||||
from pqcli.mechanic import Player, SignalMixin, Simulation
|
||||
from pqcli.roster import Roster
|
||||
from pqcli.ui.curses.event_handler import EventHandler
|
||||
@@ -33,6 +34,7 @@ class GameView(BaseView):
|
||||
roster: Roster,
|
||||
player: Player,
|
||||
args: argparse.Namespace,
|
||||
data: GameData,
|
||||
) -> None:
|
||||
super().__init__(screen)
|
||||
self.on_exit = EventHandler()
|
||||
@@ -41,7 +43,7 @@ class GameView(BaseView):
|
||||
self._player = player
|
||||
self._args = args
|
||||
|
||||
self._simulation = Simulation(player)
|
||||
self._simulation = Simulation(player, data)
|
||||
self._last_tick = datetime.datetime.now()
|
||||
|
||||
def keypress(self, key: int) -> None:
|
||||
|
||||
Reference in new issue
Block a user