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
@@ -78,6 +78,49 @@ For server usage, run first-time setup without a forced slot (`pqcli-init`),
|
|||||||
then run the long-lived detached service (`pqcli`) that loads slot `1` by
|
then run the long-lived detached service (`pqcli`) that loads slot `1` by
|
||||||
default.
|
default.
|
||||||
|
|
||||||
|
## Customizing the adventure
|
||||||
|
|
||||||
|
All word lists (monsters, spells, equipment, races, classes, titles and so on)
|
||||||
|
live in JSON files. To change them, copy the built-in files somewhere and edit
|
||||||
|
them:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ pqcli --dump-data ~/.config/pqcli/data
|
||||||
|
```
|
||||||
|
|
||||||
|
On startup, pqcli reads `$XDG_CONFIG_HOME/pqcli/data/` (or the directory
|
||||||
|
given with `--data-dir DIR`). Each `NAME.json` there replaces the built-in
|
||||||
|
list of the same name. Files you leave out fall back to the built-in lists,
|
||||||
|
so you can delete the copies you don't change. If a file is malformed, pqcli
|
||||||
|
prints what is wrong and refuses to start.
|
||||||
|
|
||||||
|
The available files are `spells`, `offense_attrib`, `defense_attrib`,
|
||||||
|
`offense_bad`, `defense_bad`, `shields`, `armors`, `weapons`, `specials`,
|
||||||
|
`item_attrib`, `item_ofs`, `boring_items`, `monsters`, `races`, `classes`,
|
||||||
|
`titles`, `impressive_titles` and `combat_verbs`.
|
||||||
|
|
||||||
|
For example, to vary the combat descriptions, create `combat_verbs.json`.
|
||||||
|
`{target}` is replaced with the enemy:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
"Executing {target}",
|
||||||
|
"In a fierce battle to thwart {target}",
|
||||||
|
"Fighting bravely against {target}",
|
||||||
|
"Kicking the butt of {target}"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
A few things to keep in mind:
|
||||||
|
|
||||||
|
- Order matters in `spells.json`: spells are learned roughly from the top
|
||||||
|
down, so keep weaker spells first.
|
||||||
|
- Titles near the top of `titles.json` show up more often.
|
||||||
|
- Stats in `races.json` and `classes.json` are written as `STR`, `CON`,
|
||||||
|
`DEX`, `INT`, `WIS`, `CHA`, `HP Max` or `MP Max`.
|
||||||
|
- Existing characters keep their race and class even if you remove them from
|
||||||
|
the lists.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
+45
-4
@@ -5,12 +5,19 @@ from pathlib import Path
|
|||||||
|
|
||||||
from xdg_base_dirs import xdg_config_home
|
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.mechanic import Player
|
||||||
from pqcli.roster import Roster
|
from pqcli.roster import Roster
|
||||||
from pqcli.ui.basic import BasicUserInterface
|
from pqcli.ui.basic import BasicUserInterface
|
||||||
from pqcli.ui.curses import CursesUserInterface
|
from pqcli.ui.curses import CursesUserInterface
|
||||||
|
|
||||||
SAVE_PATH = Path(xdg_config_home()) / "pqcli" / "save.dat"
|
SAVE_PATH = Path(xdg_config_home()) / "pqcli" / "save.dat"
|
||||||
|
DATA_PATH = Path(xdg_config_home()) / "pqcli" / "data"
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
def parse_args() -> argparse.Namespace:
|
||||||
@@ -58,6 +65,22 @@ def parse_args() -> argparse.Namespace:
|
|||||||
metavar="NUM",
|
metavar="NUM",
|
||||||
help="play chosen character",
|
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()
|
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)
|
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:
|
if roster.players or args.list_saves or args.load_save:
|
||||||
return
|
return
|
||||||
|
|
||||||
print("No saved characters found. Starting first-run character creation.")
|
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:
|
while not roster.players:
|
||||||
player = ui.create_player(auto_play=False)
|
player = ui.create_player(auto_play=False)
|
||||||
if player:
|
if player:
|
||||||
@@ -83,6 +108,22 @@ def bootstrap_first_run(roster: Roster, args: argparse.Namespace) -> None:
|
|||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
args = parse_args()
|
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)
|
roster = Roster.load(SAVE_PATH)
|
||||||
|
|
||||||
if args.list_saves:
|
if args.list_saves:
|
||||||
@@ -98,10 +139,10 @@ def main() -> None:
|
|||||||
list_players(roster, file=sys.stderr)
|
list_players(roster, file=sys.stderr)
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
bootstrap_first_run(roster, args)
|
bootstrap_first_run(roster, args, data)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ui = args.ui(roster, player, args)
|
ui = args.ui(roster, player, args, data)
|
||||||
ui.run()
|
ui.run()
|
||||||
finally:
|
finally:
|
||||||
if args.use_saves:
|
if args.use_saves:
|
||||||
|
|||||||
+3
-676
@@ -39,56 +39,9 @@ PRIME_STATS = [
|
|||||||
|
|
||||||
ALL_STATS = PRIME_STATS + [StatType.hp_max, StatType.mp_max]
|
ALL_STATS = PRIME_STATS + [StatType.hp_max, StatType.mp_max]
|
||||||
|
|
||||||
|
# The word lists live in pqcli/data/*.json (see pqcli.game_data).
|
||||||
SPELLS = [
|
# Saves pickle Monster, Race and Class instances by import path, so these
|
||||||
"Slime Finger",
|
# classes must stay in this module.
|
||||||
"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",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -97,329 +50,12 @@ class Modifier:
|
|||||||
quality: int
|
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
|
@dataclass
|
||||||
class EquipmentPreset:
|
class EquipmentPreset:
|
||||||
name: str
|
name: str
|
||||||
quality: int
|
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
|
@dataclass
|
||||||
class Monster:
|
class Monster:
|
||||||
name: str
|
name: str
|
||||||
@@ -427,322 +63,13 @@ class Monster:
|
|||||||
item: T.Optional[str]
|
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
|
@dataclass
|
||||||
class Race:
|
class Race:
|
||||||
name: str
|
name: str
|
||||||
attr: T.List[StatType]
|
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
|
@dataclass
|
||||||
class Class:
|
class Class:
|
||||||
name: str
|
name: str
|
||||||
attr: T.List[StatType]
|
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 datetime
|
||||||
|
import functools
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
@@ -8,24 +9,7 @@ from dataclasses import dataclass
|
|||||||
|
|
||||||
from pqcli import random
|
from pqcli import random
|
||||||
from pqcli.config import (
|
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,
|
PRIME_STATS,
|
||||||
RACES,
|
|
||||||
SHIELDS,
|
|
||||||
SPECIALS,
|
|
||||||
SPELLS,
|
|
||||||
TITLES,
|
|
||||||
WEAPONS,
|
|
||||||
Class,
|
Class,
|
||||||
EquipmentPreset,
|
EquipmentPreset,
|
||||||
EquipmentType,
|
EquipmentType,
|
||||||
@@ -34,6 +18,7 @@ from pqcli.config import (
|
|||||||
Race,
|
Race,
|
||||||
StatType,
|
StatType,
|
||||||
)
|
)
|
||||||
|
from pqcli.game_data import TARGET_PLACEHOLDER, GameData
|
||||||
from pqcli.lingo import (
|
from pqcli.lingo import (
|
||||||
act_name,
|
act_name,
|
||||||
big,
|
big,
|
||||||
@@ -388,7 +373,7 @@ class Player(SignalMixin):
|
|||||||
def equip_price(self) -> int:
|
def equip_price(self) -> int:
|
||||||
return 5 * (self.level**2) + 10 * self.level + 20
|
return 5 * (self.level**2) + 10 * self.level + 20
|
||||||
|
|
||||||
def level_up(self) -> None:
|
def level_up(self, data: GameData) -> None:
|
||||||
self.level += 1
|
self.level += 1
|
||||||
logger.info("Leveled up to level %d!", self.level)
|
logger.info("Leveled up to level %d!", self.level)
|
||||||
self.stats.increment(
|
self.stats.increment(
|
||||||
@@ -401,7 +386,7 @@ class Player(SignalMixin):
|
|||||||
)
|
)
|
||||||
self.win_stat()
|
self.win_stat()
|
||||||
self.win_stat()
|
self.win_stat()
|
||||||
self.win_spell()
|
self.win_spell(data)
|
||||||
self.exp_bar.reset(level_up_time(self.level))
|
self.exp_bar.reset(level_up_time(self.level))
|
||||||
self.emit("level_up")
|
self.emit("level_up")
|
||||||
|
|
||||||
@@ -427,31 +412,34 @@ class Player(SignalMixin):
|
|||||||
self.inventory.set_capacity(10 + self.stats[StatType.strength])
|
self.inventory.set_capacity(10 + self.stats[StatType.strength])
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def win_spell(self) -> None:
|
def win_spell(self, data: GameData) -> None:
|
||||||
|
spells = data.spells
|
||||||
self.spell_book.add(
|
self.spell_book.add(
|
||||||
SPELLS[
|
spells[
|
||||||
random.below_low(
|
random.below_low(
|
||||||
min(self.stats[StatType.wisdom] + self.level, len(SPELLS))
|
min(self.stats[StatType.wisdom] + self.level, len(spells))
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
|
|
||||||
def win_equipment(self) -> None:
|
def win_equipment(self, data: GameData) -> None:
|
||||||
choice = random.choice(list(EquipmentType))
|
choice = random.choice(list(EquipmentType))
|
||||||
|
|
||||||
stuff: T.List[EquipmentPreset]
|
stuff: T.Sequence[EquipmentPreset]
|
||||||
better: T.List[Modifier]
|
better: T.Sequence[Modifier]
|
||||||
worse: T.List[Modifier]
|
worse: T.Sequence[Modifier]
|
||||||
|
|
||||||
if choice == EquipmentType.weapon:
|
if choice == EquipmentType.weapon:
|
||||||
stuff = WEAPONS
|
stuff = data.weapons
|
||||||
better = OFFENSE_ATTRIB
|
better = data.offense_attrib
|
||||||
worse = OFFENSE_BAD
|
worse = data.offense_bad
|
||||||
else:
|
else:
|
||||||
stuff = SHIELDS if choice == EquipmentType.shield else ARMORS
|
stuff = (
|
||||||
better = DEFENSE_ATTRIB
|
data.shields if choice == EquipmentType.shield else data.armors
|
||||||
worse = DEFENSE_BAD
|
)
|
||||||
|
better = data.defense_attrib
|
||||||
|
worse = data.defense_bad
|
||||||
|
|
||||||
equipment = pick_equipment(stuff, self.level)
|
equipment = pick_equipment(stuff, self.level)
|
||||||
name = equipment.name
|
name = equipment.name
|
||||||
@@ -479,13 +467,14 @@ class Player(SignalMixin):
|
|||||||
logger.info("Gained %s %s", name, choice.value)
|
logger.info("Gained %s %s", name, choice.value)
|
||||||
self.equipment.put(choice, name)
|
self.equipment.put(choice, name)
|
||||||
|
|
||||||
def win_item(self) -> None:
|
def win_item(self, data: GameData) -> None:
|
||||||
self.inventory.add(special_item(), 1)
|
self.inventory.add(special_item(data), 1)
|
||||||
|
|
||||||
|
|
||||||
class Simulation:
|
class Simulation:
|
||||||
def __init__(self, player: Player) -> None:
|
def __init__(self, player: Player, data: GameData) -> None:
|
||||||
self.player = player
|
self.player = player
|
||||||
|
self.data = data
|
||||||
self.last_tick = datetime.datetime.now()
|
self.last_tick = datetime.datetime.now()
|
||||||
|
|
||||||
def tick(self, elapsed: float = 100.0) -> None:
|
def tick(self, elapsed: float = 100.0) -> None:
|
||||||
@@ -526,7 +515,7 @@ class Simulation:
|
|||||||
gain = self.player.task and isinstance(self.player.task, KillTask)
|
gain = self.player.task and isinstance(self.player.task, KillTask)
|
||||||
if gain:
|
if gain:
|
||||||
if self.player.exp_bar.done:
|
if self.player.exp_bar.done:
|
||||||
self.player.level_up()
|
self.player.level_up(self.data)
|
||||||
else:
|
else:
|
||||||
self.player.exp_bar.increment(self.player.task_bar.max_ / 1000)
|
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
|
or self.player.task.monster.item is None
|
||||||
):
|
):
|
||||||
# npc
|
# npc
|
||||||
self.player.win_item()
|
self.player.win_item(self.data)
|
||||||
elif self.player.task.monster.item:
|
elif self.player.task.monster.item:
|
||||||
self.player.inventory.add(
|
self.player.inventory.add(
|
||||||
(
|
(
|
||||||
@@ -575,7 +564,7 @@ class Simulation:
|
|||||||
elif isinstance(self.player.task, BuyTask):
|
elif isinstance(self.player.task, BuyTask):
|
||||||
# buy some equipment
|
# buy some equipment
|
||||||
self.player.inventory.add_gold(-self.player.equip_price())
|
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)):
|
elif isinstance(self.player.task, (HeadingToMarketTask, SellTask)):
|
||||||
if isinstance(self.player.task, SellTask):
|
if isinstance(self.player.task, SellTask):
|
||||||
@@ -623,7 +612,9 @@ class Simulation:
|
|||||||
else:
|
else:
|
||||||
self.player.set_task(
|
self.player.set_task(
|
||||||
monster_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)
|
60 * 60 * (1 + 5 * self.player.quest_book.act)
|
||||||
)
|
)
|
||||||
if self.player.quest_book.act > 1:
|
if self.player.quest_book.act > 1:
|
||||||
self.player.win_item()
|
self.player.win_item(self.data)
|
||||||
self.player.win_equipment()
|
self.player.win_equipment(self.data)
|
||||||
|
|
||||||
def complete_quest(self) -> None:
|
def complete_quest(self) -> None:
|
||||||
self.player.quest_book.quest_bar.reset(50 + random.below_low(1000))
|
self.player.quest_book.quest_bar.reset(50 + random.below_low(1000))
|
||||||
@@ -644,10 +635,10 @@ class Simulation:
|
|||||||
)
|
)
|
||||||
random.choice(
|
random.choice(
|
||||||
[
|
[
|
||||||
self.player.win_spell,
|
functools.partial(self.player.win_spell, self.data),
|
||||||
self.player.win_equipment,
|
functools.partial(self.player.win_equipment, self.data),
|
||||||
self.player.win_stat,
|
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)
|
choice = random.below(5)
|
||||||
if choice == 0:
|
if choice == 0:
|
||||||
self.player.quest_book.monster = unnamed_monster(
|
self.player.quest_book.monster = unnamed_monster(
|
||||||
self.player.level, iterations=3
|
self.data, self.player.level, iterations=3
|
||||||
)
|
)
|
||||||
caption = "Exterminate " + definite(
|
caption = "Exterminate " + definite(
|
||||||
self.player.quest_book.monster.name, 2
|
self.player.quest_book.monster.name, 2
|
||||||
)
|
)
|
||||||
elif choice == 1:
|
elif choice == 1:
|
||||||
caption = "Seek " + definite(interesting_item(), 1)
|
caption = "Seek " + definite(interesting_item(self.data), 1)
|
||||||
elif choice == 2:
|
elif choice == 2:
|
||||||
caption = "Deliver this " + boring_item()
|
caption = "Deliver this " + boring_item(self.data)
|
||||||
elif choice == 3:
|
elif choice == 3:
|
||||||
caption = "Fetch me " + indefinite(boring_item(), 1)
|
caption = "Fetch me " + indefinite(boring_item(self.data), 1)
|
||||||
elif choice == 4:
|
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)
|
caption = "Placate " + definite(monster.name, 2)
|
||||||
else:
|
else:
|
||||||
raise AssertionError
|
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(
|
enqueue(
|
||||||
RegularTask(
|
RegularTask(
|
||||||
@@ -760,7 +753,7 @@ class Simulation:
|
|||||||
)
|
)
|
||||||
|
|
||||||
elif choice == 2:
|
elif choice == 2:
|
||||||
nemesis = impressive_guy()
|
nemesis = impressive_guy(self.data)
|
||||||
enqueue(
|
enqueue(
|
||||||
RegularTask(
|
RegularTask(
|
||||||
"Oh sweet relief! "
|
"Oh sweet relief! "
|
||||||
@@ -777,7 +770,8 @@ class Simulation:
|
|||||||
)
|
)
|
||||||
enqueue(
|
enqueue(
|
||||||
RegularTask(
|
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,
|
2000,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -807,46 +801,50 @@ class Simulation:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def special_item() -> str:
|
def special_item(data: GameData) -> str:
|
||||||
return interesting_item() + " of " + T.cast(str, random.choice(ITEM_OFS))
|
|
||||||
|
|
||||||
|
|
||||||
def interesting_item() -> str:
|
|
||||||
return (
|
return (
|
||||||
T.cast(str, random.choice(ITEM_ATTRIB))
|
interesting_item(data)
|
||||||
+ " "
|
+ " of "
|
||||||
+ T.cast(str, random.choice(SPECIALS))
|
+ T.cast(str, random.choice(data.item_ofs))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def boring_item() -> str:
|
def interesting_item(data: GameData) -> str:
|
||||||
return T.cast(str, random.choice(BORING_ITEMS))
|
return (
|
||||||
|
T.cast(str, random.choice(data.item_attrib))
|
||||||
|
+ " "
|
||||||
|
+ T.cast(str, random.choice(data.specials))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def impressive_guy() -> str:
|
def boring_item(data: GameData) -> str:
|
||||||
return T.cast(str, random.choice(IMPRESSIVE_TITLES)) + (
|
return T.cast(str, random.choice(data.boring_items))
|
||||||
" of the " + T.cast(Race, random.choice(RACES)).name
|
|
||||||
|
|
||||||
|
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)
|
if random.below(2)
|
||||||
else " of " + generate_name()
|
else " of " + generate_name()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def unnamed_monster(level: int, iterations: int) -> Monster:
|
def unnamed_monster(data: GameData, level: int, iterations: int) -> Monster:
|
||||||
result = T.cast(Monster, random.choice(MONSTERS))
|
result = T.cast(Monster, random.choice(data.monsters))
|
||||||
for _ in range(iterations):
|
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):
|
if abs(level - alternative.level) < abs(level - result.level):
|
||||||
result = alternative
|
result = alternative
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def named_monster(level: int) -> str:
|
def named_monster(data: GameData, level: int) -> str:
|
||||||
monster = unnamed_monster(level, iterations=4)
|
monster = unnamed_monster(data, level, iterations=4)
|
||||||
return generate_name() + " the " + monster.name
|
return generate_name() + " the " + monster.name
|
||||||
|
|
||||||
|
|
||||||
def pick_equipment(
|
def pick_equipment(
|
||||||
source: T.List[EquipmentPreset], goal: int
|
source: T.Sequence[EquipmentPreset], goal: int
|
||||||
) -> EquipmentPreset:
|
) -> EquipmentPreset:
|
||||||
result = T.cast(EquipmentPreset, random.choice(source))
|
result = T.cast(EquipmentPreset, random.choice(source))
|
||||||
for _ in range(5):
|
for _ in range(5):
|
||||||
@@ -857,7 +855,7 @@ def pick_equipment(
|
|||||||
|
|
||||||
|
|
||||||
def monster_task(
|
def monster_task(
|
||||||
player_level: int, quest_monster: T.Optional[Monster]
|
data: GameData, player_level: int, quest_monster: T.Optional[Monster]
|
||||||
) -> KillTask:
|
) -> KillTask:
|
||||||
level = player_level
|
level = player_level
|
||||||
for _ in range(level):
|
for _ in range(level):
|
||||||
@@ -870,12 +868,14 @@ def monster_task(
|
|||||||
monster: T.Optional[Monster] = None
|
monster: T.Optional[Monster] = None
|
||||||
if random.odds(1, 25):
|
if random.odds(1, 25):
|
||||||
# use an NPC every once in a while
|
# use an NPC every once in a while
|
||||||
race = random.choice(RACES)
|
race = random.choice(data.races)
|
||||||
if random.odds(1, 2):
|
if random.odds(1, 2):
|
||||||
result = "passing " + race.name + " " + random.choice(CLASSES).name
|
result = (
|
||||||
|
"passing " + race.name + " " + random.choice(data.classes).name
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
result = (
|
result = (
|
||||||
random.choice_low(TITLES)
|
random.choice_low(data.titles)
|
||||||
+ " "
|
+ " "
|
||||||
+ generate_name()
|
+ generate_name()
|
||||||
+ " the "
|
+ " the "
|
||||||
@@ -890,7 +890,7 @@ def monster_task(
|
|||||||
lev = monster.level
|
lev = monster.level
|
||||||
else:
|
else:
|
||||||
# pick the monster out of so many random ones closest to the level we want
|
# 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
|
result = monster.name
|
||||||
lev = monster.level
|
lev = monster.level
|
||||||
|
|
||||||
@@ -929,7 +929,14 @@ def monster_task(
|
|||||||
result = indefinite(result, qty)
|
result = indefinite(result, qty)
|
||||||
|
|
||||||
duration = (2 * 3 * level * 1000) // player_level
|
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:
|
class StatsBuilder:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import abc
|
|||||||
import argparse
|
import argparse
|
||||||
import typing as T
|
import typing as T
|
||||||
|
|
||||||
|
from pqcli.game_data import GameData
|
||||||
from pqcli.mechanic import Player
|
from pqcli.mechanic import Player
|
||||||
from pqcli.roster import Roster
|
from pqcli.roster import Roster
|
||||||
|
|
||||||
@@ -12,8 +13,10 @@ class BaseUserInterface(abc.ABC):
|
|||||||
roster: Roster,
|
roster: Roster,
|
||||||
player: T.Optional[Player],
|
player: T.Optional[Player],
|
||||||
args: argparse.Namespace,
|
args: argparse.Namespace,
|
||||||
|
data: GameData,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.roster = roster
|
self.roster = roster
|
||||||
|
self.data = data
|
||||||
self.args = args
|
self.args = args
|
||||||
self.player = player
|
self.player = player
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import typing as T
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from pqcli import lingo
|
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.mechanic import Player, Simulation, StatsBuilder, create_player
|
||||||
from pqcli.roster import Roster
|
from pqcli.roster import Roster
|
||||||
from pqcli.ui.base import BaseUserInterface
|
from pqcli.ui.base import BaseUserInterface
|
||||||
@@ -37,8 +38,9 @@ class BasicUserInterface(BaseUserInterface):
|
|||||||
roster: Roster,
|
roster: Roster,
|
||||||
player: T.Optional[Player],
|
player: T.Optional[Player],
|
||||||
args: argparse.Namespace,
|
args: argparse.Namespace,
|
||||||
|
data: GameData,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(roster, player, args)
|
super().__init__(roster, player, args, data)
|
||||||
|
|
||||||
def signal_handler(sig: T.Any, frame: T.Any) -> None:
|
def signal_handler(sig: T.Any, frame: T.Any) -> None:
|
||||||
logging.info("Quitting")
|
logging.info("Quitting")
|
||||||
@@ -107,8 +109,10 @@ class BasicUserInterface(BaseUserInterface):
|
|||||||
print("Cancelled.")
|
print("Cancelled.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
race = self.menu([(race, race.name) for race in RACES])
|
race = self.menu([(race, race.name) for race in self.data.races])
|
||||||
class_ = self.menu([(class_, class_.name) for class_ in CLASSES])
|
class_ = self.menu(
|
||||||
|
[(class_, class_.name) for class_ in self.data.classes]
|
||||||
|
)
|
||||||
|
|
||||||
stats_builder = StatsBuilder()
|
stats_builder = StatsBuilder()
|
||||||
while True:
|
while True:
|
||||||
@@ -131,7 +135,7 @@ class BasicUserInterface(BaseUserInterface):
|
|||||||
|
|
||||||
def play(self, player: Player) -> None:
|
def play(self, player: Player) -> None:
|
||||||
print(f"Playing as {player.name}")
|
print(f"Playing as {player.name}")
|
||||||
simulation = Simulation(player)
|
simulation = Simulation(player, self.data)
|
||||||
last_tick = datetime.now()
|
last_tick = datetime.now()
|
||||||
last_level = 0
|
last_level = 0
|
||||||
while True:
|
while True:
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import typing as T
|
|||||||
|
|
||||||
from pqcli import lingo
|
from pqcli import lingo
|
||||||
from pqcli.config import Class, Race
|
from pqcli.config import Class, Race
|
||||||
|
from pqcli.game_data import GameData
|
||||||
from pqcli.mechanic import Player, Stats, create_player
|
from pqcli.mechanic import Player, Stats, create_player
|
||||||
from pqcli.roster import Roster
|
from pqcli.roster import Roster
|
||||||
from pqcli.ui.base import BaseUserInterface
|
from pqcli.ui.base import BaseUserInterface
|
||||||
@@ -40,9 +41,13 @@ class StopMainLoop(Exception):
|
|||||||
|
|
||||||
class CursesUserInterface(BaseUserInterface):
|
class CursesUserInterface(BaseUserInterface):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, roster: Roster, player: Player, args: argparse.Namespace
|
self,
|
||||||
|
roster: Roster,
|
||||||
|
player: Player,
|
||||||
|
args: argparse.Namespace,
|
||||||
|
data: GameData,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(roster, player, args)
|
super().__init__(roster, player, args, data)
|
||||||
os.environ.setdefault("ESCDELAY", "25")
|
os.environ.setdefault("ESCDELAY", "25")
|
||||||
self._screen = curses.initscr()
|
self._screen = curses.initscr()
|
||||||
self._screen.nodelay(True)
|
self._screen.nodelay(True)
|
||||||
@@ -119,7 +124,7 @@ class CursesUserInterface(BaseUserInterface):
|
|||||||
def _switch_to_create_char_race_view(
|
def _switch_to_create_char_race_view(
|
||||||
self, player_name: str, race: T.Optional[Race] = None
|
self, player_name: str, race: T.Optional[Race] = None
|
||||||
) -> 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(
|
view.on_cancel += lambda: self._switch_to_create_char_name_view(
|
||||||
player_name
|
player_name
|
||||||
)
|
)
|
||||||
@@ -131,7 +136,9 @@ class CursesUserInterface(BaseUserInterface):
|
|||||||
def _switch_to_create_char_class_view(
|
def _switch_to_create_char_class_view(
|
||||||
self, player_name: str, race: Race, class_: T.Optional[Class] = None
|
self, player_name: str, race: Race, class_: T.Optional[Class] = None
|
||||||
) -> 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(
|
view.on_cancel += lambda: self._switch_to_create_char_race_view(
|
||||||
player_name, race
|
player_name, race
|
||||||
)
|
)
|
||||||
@@ -177,7 +184,9 @@ class CursesUserInterface(BaseUserInterface):
|
|||||||
self._switch_view(view)
|
self._switch_view(view)
|
||||||
|
|
||||||
def _switch_to_game_view(self, player: Player) -> None:
|
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
|
view.on_exit += self._switch_to_roster_view
|
||||||
self._switch_view(view)
|
self._switch_view(view)
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
import functools
|
import functools
|
||||||
import typing as T
|
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.util import KEYS_CANCEL, Choice
|
||||||
from pqcli.ui.curses.views.menu_view import MenuView
|
from pqcli.ui.curses.views.menu_view import MenuView
|
||||||
|
|
||||||
|
|
||||||
class ChooseCharacterClassView(MenuView):
|
class ChooseCharacterClassView(MenuView):
|
||||||
def __init__(
|
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:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
screen,
|
screen,
|
||||||
"Choose character class",
|
"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
|
key: T.Optional[str] = str(y % 10) if y <= 10 else None
|
||||||
self._choices.append(
|
self._choices.append(
|
||||||
Choice(
|
Choice(
|
||||||
|
|||||||
@@ -1,20 +1,25 @@
|
|||||||
import functools
|
import functools
|
||||||
import typing as T
|
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.util import KEYS_CANCEL, Choice
|
||||||
from pqcli.ui.curses.views.menu_view import MenuView
|
from pqcli.ui.curses.views.menu_view import MenuView
|
||||||
|
|
||||||
|
|
||||||
class ChooseCharacterRaceView(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__(
|
super().__init__(
|
||||||
screen,
|
screen,
|
||||||
"Choose character race",
|
"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
|
key: T.Optional[str] = str(y % 10) if y <= 10 else None
|
||||||
self._choices.append(
|
self._choices.append(
|
||||||
Choice(
|
Choice(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import curses
|
|||||||
import datetime
|
import datetime
|
||||||
import typing as T
|
import typing as T
|
||||||
|
|
||||||
|
from pqcli.game_data import GameData
|
||||||
from pqcli.mechanic import Player, SignalMixin, Simulation
|
from pqcli.mechanic import Player, SignalMixin, Simulation
|
||||||
from pqcli.roster import Roster
|
from pqcli.roster import Roster
|
||||||
from pqcli.ui.curses.event_handler import EventHandler
|
from pqcli.ui.curses.event_handler import EventHandler
|
||||||
@@ -33,6 +34,7 @@ class GameView(BaseView):
|
|||||||
roster: Roster,
|
roster: Roster,
|
||||||
player: Player,
|
player: Player,
|
||||||
args: argparse.Namespace,
|
args: argparse.Namespace,
|
||||||
|
data: GameData,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(screen)
|
super().__init__(screen)
|
||||||
self.on_exit = EventHandler()
|
self.on_exit = EventHandler()
|
||||||
@@ -41,7 +43,7 @@ class GameView(BaseView):
|
|||||||
self._player = player
|
self._player = player
|
||||||
self._args = args
|
self._args = args
|
||||||
|
|
||||||
self._simulation = Simulation(player)
|
self._simulation = Simulation(player, data)
|
||||||
self._last_tick = datetime.datetime.now()
|
self._last_tick = datetime.datetime.now()
|
||||||
|
|
||||||
def keypress(self, key: int) -> None:
|
def keypress(self, key: int) -> None:
|
||||||
|
|||||||
+1
-1
@@ -40,7 +40,7 @@ requires = ["hatchling"]
|
|||||||
build-backend = "hatchling.build"
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
dev-dependencies = ["pre-commit>=2.16.0"]
|
dev-dependencies = ["pre-commit>=2.16.0", "pytest>=8.0"]
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 79
|
line-length = 79
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
Vendored
+1871
File diff suppressed because it is too large.
Load diff
Vendored
+1857
File diff suppressed because it is too large.
Load diff
Vendored
+1867
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,69 @@
|
|||||||
|
import datetime
|
||||||
|
import typing as T
|
||||||
|
|
||||||
|
from pqcli import random
|
||||||
|
from pqcli.config import StatType
|
||||||
|
from pqcli.game_data import load_game_data
|
||||||
|
from pqcli.mechanic import Player, Simulation, Stats, monster_task
|
||||||
|
|
||||||
|
STATS = {
|
||||||
|
StatType.strength: 12,
|
||||||
|
StatType.condition: 11,
|
||||||
|
StatType.dexterity: 10,
|
||||||
|
StatType.intelligence: 13,
|
||||||
|
StatType.wisdom: 14,
|
||||||
|
StatType.charisma: 9,
|
||||||
|
StatType.hp_max: 6,
|
||||||
|
StatType.mp_max: 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run_trace(seed: str, ticks: int = 3000) -> T.List[str]:
|
||||||
|
"""Play a deterministic game and return everything that happened."""
|
||||||
|
data = load_game_data()
|
||||||
|
player = Player(
|
||||||
|
name="Tester",
|
||||||
|
birthday=datetime.datetime(2000, 1, 1),
|
||||||
|
race=data.races[0],
|
||||||
|
class_=data.classes[0],
|
||||||
|
stats=Stats(dict(STATS)),
|
||||||
|
)
|
||||||
|
random.seed(seed)
|
||||||
|
simulation = Simulation(player, data)
|
||||||
|
|
||||||
|
lines: T.List[str] = []
|
||||||
|
player.connect(
|
||||||
|
"new_task",
|
||||||
|
lambda: lines.append(f"task: {player.task.description}"),
|
||||||
|
)
|
||||||
|
for _ in range(ticks):
|
||||||
|
simulation.tick(100000.0)
|
||||||
|
|
||||||
|
# plot cinematics and quests are rare in a short run, so force them
|
||||||
|
for _ in range(30):
|
||||||
|
simulation.interplot_cinematic()
|
||||||
|
simulation.complete_quest()
|
||||||
|
lines.extend(f"queued: {task.description}" for task in player.queue)
|
||||||
|
player.queue.clear()
|
||||||
|
|
||||||
|
# high level monsters are unreachable in a short run
|
||||||
|
for level in range(1, 100):
|
||||||
|
task = monster_task(data, level, player.quest_book.monster)
|
||||||
|
lines.append(f"monster: {task.description} {task.duration}")
|
||||||
|
|
||||||
|
lines.append(f"level: {player.level}")
|
||||||
|
lines.append(f"act: {player.quest_book.act}")
|
||||||
|
lines.append(f"gold: {player.inventory.gold}")
|
||||||
|
for stat, value in player.stats:
|
||||||
|
lines.append(f"stat: {stat.value} {value}")
|
||||||
|
for equipment_type, item in player.equipment:
|
||||||
|
lines.append(f"equipment: {equipment_type.value} {item}")
|
||||||
|
for inventory_item in player.inventory:
|
||||||
|
lines.append(
|
||||||
|
f"inventory: {inventory_item.name} {inventory_item.quantity}"
|
||||||
|
)
|
||||||
|
for spell in player.spell_book:
|
||||||
|
lines.append(f"spell: {spell.name} {spell.level}")
|
||||||
|
for quest in player.quest_book.quests:
|
||||||
|
lines.append(f"quest: {quest}")
|
||||||
|
return lines
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import pickle
|
||||||
|
import typing as T
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from pqcli import random
|
||||||
|
from pqcli.config import Class, Monster, Race, StatType
|
||||||
|
from pqcli.game_data import (
|
||||||
|
DATA_FILES,
|
||||||
|
DataError,
|
||||||
|
GameData,
|
||||||
|
dump_builtin_data,
|
||||||
|
load_game_data,
|
||||||
|
)
|
||||||
|
from pqcli.mechanic import Player, Stats, monster_task
|
||||||
|
|
||||||
|
|
||||||
|
def write(directory: Path, name: str, value: T.Any) -> Path:
|
||||||
|
path = directory / f"{name}.json"
|
||||||
|
path.write_text(json.dumps(value), encoding="utf-8")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def test_builtin_data_loads() -> None:
|
||||||
|
data = load_game_data()
|
||||||
|
assert data.spells[0] == "Slime Finger"
|
||||||
|
assert data.monsters[0] == Monster("Anhkheg", 6, "chitin")
|
||||||
|
assert data.races[0] == Race("Half Orc", [StatType.hp_max])
|
||||||
|
assert data.classes[-1] == Class("Vermineer", [StatType.intelligence])
|
||||||
|
assert data.combat_verbs == ("Executing {target}",)
|
||||||
|
assert all(getattr(data, name) for name in DATA_FILES)
|
||||||
|
|
||||||
|
|
||||||
|
def test_builtin_data_keeps_quirks() -> None:
|
||||||
|
data = load_game_data()
|
||||||
|
names = [monster.name for monster in data.monsters]
|
||||||
|
assert names.count("Rat") == 2
|
||||||
|
assert Monster("Neo-Otyugh", 11, "organ ") in data.monsters
|
||||||
|
assert any(monster.item is None for monster in data.monsters)
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_override_dir_uses_builtin(tmp_path: Path) -> None:
|
||||||
|
assert load_game_data(tmp_path / "nope") == load_game_data()
|
||||||
|
|
||||||
|
|
||||||
|
def test_override_replaces_only_that_list(tmp_path: Path) -> None:
|
||||||
|
write(tmp_path, "spells", ["Zap", "Bonk"])
|
||||||
|
write(
|
||||||
|
tmp_path,
|
||||||
|
"races",
|
||||||
|
[{"name": "Robot", "attr": ["STR", "hp_max", "Wisdom"]}],
|
||||||
|
)
|
||||||
|
builtin = load_game_data()
|
||||||
|
data = load_game_data(tmp_path)
|
||||||
|
assert data.spells == ("Zap", "Bonk")
|
||||||
|
assert data.races == (
|
||||||
|
Race("Robot", [StatType.strength, StatType.hp_max, StatType.wisdom]),
|
||||||
|
)
|
||||||
|
assert data.monsters == builtin.monsters
|
||||||
|
assert data.classes == builtin.classes
|
||||||
|
|
||||||
|
|
||||||
|
def test_override_accepts_all_shapes(tmp_path: Path) -> None:
|
||||||
|
write(tmp_path, "monsters", [{"name": "Bob", "level": 0, "item": None}])
|
||||||
|
write(tmp_path, "weapons", [{"name": "Spoon", "quality": -1}])
|
||||||
|
write(tmp_path, "offense_bad", [{"name": "Soggy", "quality": -2}])
|
||||||
|
data = load_game_data(tmp_path)
|
||||||
|
assert data.monsters == (Monster("Bob", 0, None),)
|
||||||
|
assert data.weapons[0].name == "Spoon"
|
||||||
|
assert data.offense_bad[0].quality == -2
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_file_warns(
|
||||||
|
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||||
|
) -> None:
|
||||||
|
write(tmp_path, "monster", [])
|
||||||
|
load_game_data(tmp_path)
|
||||||
|
assert "monster.json" in capsys.readouterr().err
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"name,value,message",
|
||||||
|
[
|
||||||
|
("spells", [], "must not be empty"),
|
||||||
|
("spells", {"a": 1}, "must be a list"),
|
||||||
|
("spells", ["ok", 3], "[1]"),
|
||||||
|
("monsters", [{"name": "Imp", "level": "2", "item": "x"}], "level"),
|
||||||
|
("monsters", [{"name": "Imp", "level": True, "item": "x"}], "level"),
|
||||||
|
("monsters", [{"name": "Imp", "level": 2}], "item"),
|
||||||
|
("monsters", [{"name": "Imp", "level": 2, "item": 1}], "item"),
|
||||||
|
("weapons", [{"name": "Spoon", "quality": 1, "x": 1}], "x"),
|
||||||
|
("races", [{"name": "Elf", "attr": ["LUCK"]}], "LUCK"),
|
||||||
|
("races", [{"name": "Elf", "attr": "STR"}], "attr"),
|
||||||
|
("combat_verbs", ["Hitting"], "{target}"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_invalid_override_raises(
|
||||||
|
tmp_path: Path, name: str, value: T.Any, message: str
|
||||||
|
) -> None:
|
||||||
|
write(tmp_path, name, value)
|
||||||
|
with pytest.raises(DataError) as excinfo:
|
||||||
|
load_game_data(tmp_path)
|
||||||
|
assert f"{name}.json" in str(excinfo.value)
|
||||||
|
assert message in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_malformed_json_raises(tmp_path: Path) -> None:
|
||||||
|
(tmp_path / "spells.json").write_text("[oops", encoding="utf-8")
|
||||||
|
with pytest.raises(DataError, match="spells.json"):
|
||||||
|
load_game_data(tmp_path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_utf8_raises(tmp_path: Path) -> None:
|
||||||
|
(tmp_path / "spells.json").write_bytes(b'["\xff"]')
|
||||||
|
with pytest.raises(DataError, match="spells.json"):
|
||||||
|
load_game_data(tmp_path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_override_dir_must_be_directory(tmp_path: Path) -> None:
|
||||||
|
path = write(tmp_path, "spells", ["Zap"])
|
||||||
|
with pytest.raises(DataError, match="not a directory"):
|
||||||
|
load_game_data(path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_combat_verbs_are_used(tmp_path: Path) -> None:
|
||||||
|
verbs = [
|
||||||
|
"Fighting bravely against {target}",
|
||||||
|
"Kicking the butt of {target}",
|
||||||
|
]
|
||||||
|
write(tmp_path, "combat_verbs", verbs)
|
||||||
|
data = load_game_data(tmp_path)
|
||||||
|
random.seed("verbs")
|
||||||
|
seen = set()
|
||||||
|
for _ in range(200):
|
||||||
|
description = monster_task(data, 5, None).description
|
||||||
|
prefix = description.split(" ")[0]
|
||||||
|
assert prefix in {"Fighting", "Kicking"}
|
||||||
|
assert "{target}" not in description
|
||||||
|
seen.add(prefix)
|
||||||
|
assert seen == {"Fighting", "Kicking"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_dump_builtin_data_round_trips(tmp_path: Path) -> None:
|
||||||
|
written = dump_builtin_data(tmp_path / "out")
|
||||||
|
assert sorted(path.stem for path in written) == sorted(DATA_FILES)
|
||||||
|
assert load_game_data(tmp_path / "out") == load_game_data()
|
||||||
|
|
||||||
|
|
||||||
|
def test_dump_builtin_data_refuses_to_overwrite(tmp_path: Path) -> None:
|
||||||
|
existing = write(tmp_path, "spells", ["Mine"])
|
||||||
|
with pytest.raises(DataError, match="spells.json"):
|
||||||
|
dump_builtin_data(tmp_path)
|
||||||
|
assert json.loads(existing.read_text()) == ["Mine"]
|
||||||
|
assert list(tmp_path.iterdir()) == [existing]
|
||||||
|
|
||||||
|
|
||||||
|
def test_saved_player_survives_removed_race(tmp_path: Path) -> None:
|
||||||
|
data = load_game_data()
|
||||||
|
player = Player(
|
||||||
|
name="Saved",
|
||||||
|
birthday=datetime.datetime(2000, 1, 1),
|
||||||
|
race=data.races[3],
|
||||||
|
class_=data.classes[2],
|
||||||
|
stats=Stats({stat: 10 for stat in StatType}),
|
||||||
|
)
|
||||||
|
blob = pickle.dumps(player)
|
||||||
|
|
||||||
|
write(tmp_path, "races", [{"name": "Robot", "attr": ["STR"]}])
|
||||||
|
load_game_data(tmp_path)
|
||||||
|
restored = pickle.loads(blob)
|
||||||
|
assert restored.race == Race("Double Hobbit", [StatType.strength])
|
||||||
|
assert type(restored.race).__module__ == "pqcli.config"
|
||||||
|
|
||||||
|
|
||||||
|
def test_game_data_is_immutable() -> None:
|
||||||
|
data = load_game_data()
|
||||||
|
assert isinstance(data, GameData)
|
||||||
|
with pytest.raises(AttributeError):
|
||||||
|
data.spells = () # type: ignore[misc]
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from game_trace import run_trace
|
||||||
|
|
||||||
|
FIXTURES = Path(__file__).parent / "fixtures"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("seed", ["alpha", "beta", "gamma"])
|
||||||
|
def test_gameplay_matches_recorded_trace(seed: str) -> None:
|
||||||
|
# The fixtures were recorded before the word lists moved to data files;
|
||||||
|
# stock data must reproduce them exactly, RNG stream included.
|
||||||
|
expected = (FIXTURES / f"parity_{seed}.txt").read_text().splitlines()
|
||||||
|
assert run_trace(seed) == expected
|
||||||
@@ -11,6 +11,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
|
{ url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "distlib"
|
name = "distlib"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@@ -20,6 +29,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
|
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "exceptiongroup"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "filelock"
|
name = "filelock"
|
||||||
version = "3.20.0"
|
version = "3.20.0"
|
||||||
@@ -38,6 +59,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" },
|
{ url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960", size = 21209, upload-time = "2026-10-06T22:48:38.076Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nodeenv"
|
name = "nodeenv"
|
||||||
version = "1.9.1"
|
version = "1.9.1"
|
||||||
@@ -47,6 +77,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
|
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "26.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "platformdirs"
|
name = "platformdirs"
|
||||||
version = "4.5.1"
|
version = "4.5.1"
|
||||||
@@ -56,6 +95,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
|
{ url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.7.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/bf/db/7fc19e6f2dc92a966727031389fc2e08b558f0f25eb7403c1119ad4713cd/pluggy-1.7.0.tar.gz", hash = "sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8", size = 123304, upload-time = "2026-10-15T09:50:58.343Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/9e/2b38731e0fc536806f16490e1a12d7f0dc2a1235aa8cc07bcc75416a7daa/pluggy-1.7.0-py3-none-any.whl", hash = "sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec", size = 27082, upload-time = "2026-10-15T09:50:56.808Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pqcli"
|
name = "pqcli"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@@ -75,6 +123,7 @@ windows = [
|
|||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "pre-commit" },
|
{ name = "pre-commit" },
|
||||||
|
{ name = "pytest" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
@@ -88,7 +137,10 @@ requires-dist = [
|
|||||||
provides-extras = ["windows"]
|
provides-extras = ["windows"]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [{ name = "pre-commit", specifier = ">=2.16.0" }]
|
dev = [
|
||||||
|
{ name = "pre-commit", specifier = ">=2.16.0" },
|
||||||
|
{ name = "pytest", specifier = ">=8.0" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pre-commit"
|
name = "pre-commit"
|
||||||
@@ -106,6 +158,33 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429, upload-time = "2025-11-22T21:02:40.836Z" },
|
{ url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429, upload-time = "2025-11-22T21:02:40.836Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.21.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "9.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||||
|
{ name = "iniconfig" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyyaml"
|
name = "pyyaml"
|
||||||
version = "6.0.3"
|
version = "6.0.3"
|
||||||
@@ -170,6 +249,78 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tomli"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b0/78/9ad63712633ed3ab5cc1a648d863d7e7da371e9425e209555a0fe711b695/tomli-2.5.0.tar.gz", hash = "sha256:264507556cd8b8c8e7c6ee037cdf443a463f03f4c958e57195e3d369711b8ff6", size = 17662, upload-time = "2026-10-07T12:23:37.892Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/a6/ab99b60ee52acd949684febabc3005d0045d0f66bebd9cdebd67372d26dd/tomli-2.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c4dc1c1781f2f716de763d1e9a7b34c6a894e167e291c7c5d16c72f7a9538545", size = 163901, upload-time = "2026-10-07T12:22:15.601Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/00/ee01b7ed4579180fff07142d290257f25ba786f23f3ec6005f620933c2f5/tomli-2.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eff8babca5a7999bc137acbc7482a8b7e17ffca5075ab41f5d770ab408c7bfef", size = 163756, upload-time = "2026-10-07T12:22:16.957Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/c2/4efebf65372f6583185f79799312109dddb61102d47e5c33dcfd1a297aca/tomli-2.5.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86665cee9c4835b7a7f1e8ec2c719b5258d4dc782887aded5a8ae7352a96843b", size = 268038, upload-time = "2026-10-07T12:22:18.135Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/53/07/5850468e925d898abb36038666f9c333a94d2a223e802a8ba5b6d319d23f/tomli-2.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e369fd63331746182360977b1892bfc215476a30d61612d732425311639f56", size = 276422, upload-time = "2026-10-07T12:22:19.567Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/87/f293984cdcf83c054196d4fd3dad44fc68ae55b4b8c44bc76cef360c3150/tomli-2.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ad1ea345759240d6463efa0ed1c704402752e49aa21476620738d74d72d8aa1", size = 272616, upload-time = "2026-10-07T12:22:20.794Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/ce/db582886b3c1219d3fec93ebd669332482e5aee7a91e0f7838d84f2d1759/tomli-2.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:96243987194634bd411066ce40c952e108f86af04db533ecd8ac3ff2a85b1885", size = 276593, upload-time = "2026-10-07T12:22:22.12Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/72/7619b87dea4261fc27dd7b54c4461c129c1f7d9bb7ba3aec89c797a431b8/tomli-2.5.0-cp311-cp311-win32.whl", hash = "sha256:610b27d99f28ec5f191c7064a48f3ddb179a1fe6ca73d571483ae859f57b605e", size = 101830, upload-time = "2026-10-07T12:22:23.651Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/74/220106da34502304b6751a2a9b8a9fbca6c3fd47e737a2e2e3da7c61c9db/tomli-2.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:c804ae44fe7b4bab5da295e4f980a1ff04670bca9d23fe0a4e887e08ebd741a8", size = 112742, upload-time = "2026-10-07T12:22:24.972Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/99/7d9c8b41837a7773613e169504147375c157a290167aa59ad74a085f521f/tomli-2.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:cfac177ebd6236003846ea339981f71457cb6eb748f23381eb257e45092e3980", size = 109332, upload-time = "2026-10-07T12:22:26.117Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/ed/7baa86f87493646a594de388c7c1c40a39dd0461f7e9c0359cbeefc91fe8/tomli-2.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f4a40d03fb9f63424f0979855bdeaf44dd7696b8d59501822c10ed30ba532df", size = 164854, upload-time = "2026-10-07T12:22:27.444Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/b1/44c0341f2224397855723c7a8a39f718ea6fcbcc3dacc66e5aeca0f334e3/tomli-2.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ebf8d19b17bd0daeb7b7dec81a946a439b753942fd0210d6e96c532249eea6b", size = 164074, upload-time = "2026-10-07T12:22:28.679Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/04/e2d5b7d3fba47adedb23de616c16d428ea076c79a3d8e1d95d649ffe197e/tomli-2.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf0b5e8e0f68ebb494356e577c06c139161efd8d3b9050f93b39b7c26cc54ff0", size = 274274, upload-time = "2026-10-07T12:22:29.804Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/90/6090e706ff27a6f89f4a40578e3324b95c3cd8c4150868aabf33a8f414c3/tomli-2.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cf74416bdc94ae458b14e37286c1073081850ac8459a00d0c5efef5d44294c6", size = 286435, upload-time = "2026-10-07T12:22:31.297Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/9e/a2c40768df16c408f22430afb0a73e9d7e5f79c950884954649d1146b74d/tomli-2.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:61ea1ebe1e55a34ea8199cc8dbff398d35027b82271c8ac4802fd3a1fd5b1bcc", size = 278119, upload-time = "2026-10-07T12:22:32.601Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/25/3c0cb485b98e9cfac495629b1c93c87ccf0b72fbe9d2689fd8fe62c6d5a3/tomli-2.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed53f7e89bb04f6d9e8e7799112360b0c4d5cbff067de0814c98c37c39b920f7", size = 286177, upload-time = "2026-10-07T12:22:33.745Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/8b/0144c65f0e37e51c18d04ae15c21b19431c165002d0131fe9aa8b0b8b1e8/tomli-2.5.0-cp312-cp312-win32.whl", hash = "sha256:e7ad033e27a516a233bea839cdb77b80146facb3b4f40bf02cd0cac165cdd5c2", size = 102760, upload-time = "2026-10-07T12:22:34.887Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/32/5d6d8f42fc9a05fce69354e00ff256484192f5f2fc9a2165718fa0de61ec/tomli-2.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd05de8c1698f8413dd7d869492693a0bf2211543b787ac78cd5e7536af1a6d7", size = 112722, upload-time = "2026-10-07T12:22:36.162Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/65/df18032218db0fb9b769fb23c8039a051f15c811993995ea04c350273a32/tomli-2.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:069435bd5480429b98c5e5afb02ab21c219b6f0064680671c6dc0d46817346ea", size = 109534, upload-time = "2026-10-07T12:22:37.296Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/e5/51736d70da209350969e15aca5c5ab6e2ce1ea87a0a892a6c13aec172a86/tomli-2.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:943276cf269e0071948d9ff697159c1735e623c1151d88abb09b74659ef0cbea", size = 163328, upload-time = "2026-10-07T12:22:38.373Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/55/086f80dab4ab497602644274e6dea7ec5dd0b4e262e443a8ad3bb7edee2d/tomli-2.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:463b16086865b97facd8d0b3fb4cb7c544e3f58d2a69dc3113d6db9653fdb043", size = 162246, upload-time = "2026-10-07T12:22:39.673Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/eb/3ecc94459f3635c92321f4e7bde571323fdb2267c50e19e3188a281eae3b/tomli-2.5.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1245a6638fc4bb0a60af38a7d45413db34a13842027c77597c712c998c62fdf0", size = 272655, upload-time = "2026-10-07T12:22:41.08Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/d7/494fd1f0c37a621f1ad9975c2efadb523e8101f144ed6edb2e7fe64738f2/tomli-2.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d8bac3d603c97e6854424e5b2b5b741bdbde387e09f162fb0446812b4a8362b", size = 283595, upload-time = "2026-10-07T12:22:42.222Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/51/bb8d62b1317e6640866f6949b2d5855e5300f2c99d46de1cd245570bba65/tomli-2.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:21e4cae4114aba25aa0d4f85cdf486d290fb35c0954d7bba536248da64d43066", size = 276253, upload-time = "2026-10-07T12:22:43.625Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/f4/f46bd7f0763cd47de2db697dca9257c6a4adfd1a93b018cc75c8190ed5a8/tomli-2.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bbaefc84548d754be821bba7c4141c4787dda182f9e77f2f87b71213529efa7b", size = 283582, upload-time = "2026-10-07T12:22:44.983Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/03/70f2bcb2923a6db37818d917e124270a7f4cfd38ea576f5aa753a91c0ef5/tomli-2.5.0-cp313-cp313-win32.whl", hash = "sha256:abdbf6313b8d9efe157edeb7ab6eae4de064b1300ad31abf73755154b30abe68", size = 102628, upload-time = "2026-10-07T12:22:46.508Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/98/d52024bb5b0ff68b4f0d276d867f634c84a67319a7e9f6b7708a37742333/tomli-2.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd4dc129784e0c5335bd4e61dfcc4487499a013419e655cf2da1d091b7e0efdc", size = 113301, upload-time = "2026-10-07T12:22:47.647Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/f2/540db3a70572a8c23a28aba3e9c358ce0ffffbafc990905c1343aa265b31/tomli-2.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:69491c143d2fe063046e0301e62a810bed338fa4d1ce0fd870c27dc1e09b0d84", size = 109744, upload-time = "2026-10-07T12:22:48.925Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/49/caf6b307766eb9567664a8707e9d6be5fcc0e8903f18781c6677a60d80c7/tomli-2.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d3182ee2d887e507bd67319a0a61105d1dd33facc111329559a233b772c1a105", size = 162899, upload-time = "2026-10-07T12:22:50.088Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/c8/68cfce773a2733a49c74f99d627fb461bd990756860099eac25617889585/tomli-2.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:521345fd1f19d45b8df87657aaa38b6f2ca3800059fadf428e7ebf479a383646", size = 162080, upload-time = "2026-10-07T12:22:51.558Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/b2/e5bb8651fdad593f670501a7d718b1a7f73f064d44dea15e04c04dfef45d/tomli-2.5.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e95c7614e705bfe2b04b27aa124adec59752d15813df37e2156747cab3a006b", size = 273380, upload-time = "2026-10-07T12:22:52.918Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/d2/9e2d7f8b1dfe0e2b34c245986ebd55c4c553ea4ce6c47c443b332673253f/tomli-2.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ac2027d37c3afbdf4bdd377f2676f6f1d2122a5be1f1137b49dced590b37e75", size = 283228, upload-time = "2026-10-07T12:22:54.173Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/df/ec7b876b7b1a2718bd74a3743c076fff565b04029ba33e8f61fac262739f/tomli-2.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c414be4ed9d3cac80c42e348fa5a956117d1a48227f48026e31f59cb4a7671eb", size = 277189, upload-time = "2026-10-07T12:22:55.342Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/7b/e192d9eed0b9cb80da799f4d77052297fb9a2c3cc9b19f571f56ea88add6/tomli-2.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9b03d7dc168353b4132965bde20feceabaa470e570c6f59660dfae59b1f9eeb3", size = 283632, upload-time = "2026-10-07T12:22:56.735Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/50/ff94454e75461d75623e47401ed323d65c10aab8fe9033242c20cd2fdf32/tomli-2.5.0-cp314-cp314-win32.whl", hash = "sha256:6f041843c4d3a37245c0c056fd955b186bf8b1fb85690cbe40b81230891dc34b", size = 103535, upload-time = "2026-10-07T12:22:58.084Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/0b/bdacf05f963bd6026ebf6eeb0beda847d1d60e03e440725c64a4e08a0afd/tomli-2.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:f4b653094e18f9031102d3a1da5c729c8f222d85225b18037dac621695e46e1a", size = 114621, upload-time = "2026-10-07T12:22:59.2Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/99/53f438fa6ae4f9d4ed0ddde3e7242b3bdc34b48c8f9948b72b9e9b127676/tomli-2.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:3f89d10c1ff6a38d992c27fc8a4816af71a909e08a40ec66934240b1e74347c3", size = 111572, upload-time = "2026-10-07T12:23:00.479Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/20/1f88f19427d380a40e90a770e087489eaafe4aeee070ae88ed2bbec00acd/tomli-2.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e9e15b4a6c7dd6b85b5fbab29488a73f1f70de516942308daa266bf0e0aeb0d4", size = 171814, upload-time = "2026-10-07T12:23:01.914Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/56/cbe5079c9f9a54b9b3e27fc82f08f3cb36edee75561679f53d2380c801d6/tomli-2.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e12bbcd32897272fb05929110362ae9ff4c1b9bb26bd9e971e71dcd3275b4c3d", size = 171324, upload-time = "2026-10-07T12:23:03.18Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/30/1d53fd3b0f1cb3ba542e345ec32c26aefdddc4e829e4f3429af8a4f27782/tomli-2.5.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20aa36de8f2cf87237143bc1fa1aae8d6612c09118f4da21c6a684db5dd1f6f9", size = 297441, upload-time = "2026-10-07T12:23:04.345Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/d9/0800acb6a111686f764c1b91ef15cc42a20a66a46013bb42220f1d2c61c1/tomli-2.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22185fad8a1e622f064e78008018a0dd3323550dcb479cb7a1d296888d74024f", size = 307476, upload-time = "2026-10-07T12:23:05.671Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/63/30a8f3cd51b5bec37f04744bad0b0dc6160df84aad4f27b0e9283d66f221/tomli-2.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:984012f71908165449a951de2050d52f276bfe3aa5d5f570f63ddad814370374", size = 296113, upload-time = "2026-10-07T12:23:07.202Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ab/18/0b9ffc597e69c5a1e20a7823cb60d54b39a9f54e91edcb8574f022186758/tomli-2.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f79203b3965b4000e91808aaa7c040206093f2b8bf86f455982f2274c9ccf442", size = 307725, upload-time = "2026-10-07T12:23:08.508Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ab/c7/18f8baae0b5607a60e8e19b4a7fedee43a8ff6458e3896dcbbadeeac9c22/tomli-2.5.0-cp314-cp314t-win32.whl", hash = "sha256:91294a9fb94a75542f6e46e4a2ae709bd8d9b51134098cae5cf3bea5478b6d03", size = 108546, upload-time = "2026-10-07T12:23:09.956Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/34/4cca9739254130627bde87500b3f2b512154fe2f278efa7e2a5e10ad4bcb/tomli-2.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f15e3e0b835a6d68b10c86bf80a3149780498d6911c93c3ffd1861d19f9200f1", size = 117814, upload-time = "2026-10-07T12:23:11.486Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/fb/afa530d47dd80a78fce43beac6bc6e00f84558eafcffbc6f37b21e80d056/tomli-2.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6664b7ae7af7294256c53960a6103077f4914cec8ff98479c352f622c6f6b2f0", size = 115188, upload-time = "2026-10-07T12:23:12.728Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/98/316fdc00f8c0939e6fe50461dd343c162d3ad51d1286eb25b7db54361d50/tomli-2.5.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a525685c2f97da40762b8695eb7aa0af4c8344ca1905c73e4e29cb04d34607dc", size = 162775, upload-time = "2026-10-07T12:23:13.941Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/22/7b10fa5bb01c9539f53f69b619361b19350acc73657772ea7ac70ba309a8/tomli-2.5.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:9dbb18c1cfb2f6517942fc9314437f66aa06d94436ffb1f06102ef3572f35276", size = 161406, upload-time = "2026-10-07T12:23:15.215Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/e7/1a069d86dfd20f1f84f71c63faed9f83c1d890bc06c27d82dc7d888fb573/tomli-2.5.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:752e8b1aa6a4367ef8bf6a1a1e005540f7ed055ba36d7193796812ca5404eb52", size = 273855, upload-time = "2026-10-07T12:23:16.471Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/83/d1ef43d1687d092ab9c235455c76e6e709483b346b056f086095c7c263a5/tomli-2.5.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47300f9bf791808f77d82747691c4bb09cb14bdf3060cca99b42cdc4361d5a7", size = 284910, upload-time = "2026-10-07T12:23:18.166Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/05/f4d9cf7de61822ece0c3873f30d291e324911c71a378b8bfe5ced13fd9f5/tomli-2.5.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:19b0dd8749f4ea2f112c5fcfb3c5248390c899d7e2e173f1d91abee1fa0ff391", size = 277723, upload-time = "2026-10-07T12:23:19.355Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/28/78262493141fa543151cf005760c3cb01d09fc28a11f993c05109902cb8c/tomli-2.5.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:57b1c3b01fab802e2899bc3d168dca320e14165e2fd9fd584760fb4ca5826859", size = 285115, upload-time = "2026-10-07T12:23:20.698Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/b9/e1dab9a30bcb677b5cc5cee810609cfd64f24306a3055767dd3fda00b1e0/tomli-2.5.0-cp315-cp315-win32.whl", hash = "sha256:667e521b37a6c5ccaa044202c235b530f90177ffe2cd4a64ecc213c7dd535feb", size = 103475, upload-time = "2026-10-07T12:23:21.941Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4c/bd/31a3790c11d6ea95fcf5e6022ac0f8d0543c9b61120b730fc481bd43d3b4/tomli-2.5.0-cp315-cp315-win_amd64.whl", hash = "sha256:d747252933c8a65ef6bd8da0fbb7ce28a90eb6119d8cd00772cd528aa07b68d5", size = 114589, upload-time = "2026-10-07T12:23:23.098Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/a2/4f6310fa699364f0e3af7ee3af88dddd9af066d33e716a0265bbe2b3ea84/tomli-2.5.0-cp315-cp315-win_arm64.whl", hash = "sha256:75dbcde8751b0a960aa3de173aa5e894d590755c6d7758b7e774c06f1dc3cbdd", size = 111493, upload-time = "2026-10-07T12:23:24.233Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/14/00853f0b396d8971107ae1921bb5b322fdee1650d2f16bf06c20adb532e5/tomli-2.5.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:2419c2a189551987b59d80e63ec355671283336f41c6b9b89462df679c7d0c57", size = 171380, upload-time = "2026-10-07T12:23:25.512Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/ad/fa6949321dadee46b27363974fb197b94c911c3b0f7a5fd26d7dc18fc2a0/tomli-2.5.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:0dc598040da8d42cf20f0be588ed7004f46db12a0ac6c32e03a59dccedaaadcd", size = 170553, upload-time = "2026-10-07T12:23:26.855Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/53/aa/3056c919eb3e084df3752b2cf5f865dcc04af0b27dba2f66d7b28af4633a/tomli-2.5.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49096930c8d886c9bbdab62d2d0d17ce823ddeea522309a190b36245d5b49e01", size = 294428, upload-time = "2026-10-07T12:23:28.132Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/b2/faeeb5d8769ea3832021d73e892c8391eae7b4b4f8b55a789127bd8b18a9/tomli-2.5.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8ade5023067f99fe72b88accd30d0ea05a158e9e32a11f124e731ea9695313f", size = 304909, upload-time = "2026-10-07T12:23:29.381Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/52/f094c09e73fb654b621716d019acb5d29bdfd1be01df80c281d552bda48d/tomli-2.5.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b69564772b5c8f22ea5f498dff08cfa825045b4d4c4400529000bdf818aa3b2a", size = 293220, upload-time = "2026-10-07T12:23:30.608Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/f5/0c30541078ca4b505ce3bd76ed931facbfec524dd018535d691d1af0a6d2/tomli-2.5.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:8ff3a2ca028c7eee0c777f9a092038d0a594a9fa04e215f929a22c329e2cb142", size = 305705, upload-time = "2026-10-07T12:23:32.181Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/74/590e7d19d6a118fc5cc5704ff358e21d95b8573f6b9443b1519f29ca8825/tomli-2.5.0-cp315-cp315t-win32.whl", hash = "sha256:62fc1bc8eb03e3a9cadfca713d65614ed8e09d974a283295ffe3a831976b4dc5", size = 108432, upload-time = "2026-10-07T12:23:33.496Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1c/b8/63a75cfb27a17c38550e44025d3a6e7be64516fd8608a3b75703bf37d81b/tomli-2.5.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f3fcbc57b1791fa6cbe5d8434179d51de12be1a4811469529f47f6e7487a2571", size = 117281, upload-time = "2026-10-07T12:23:34.648Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/01/e8c1debb2173973372934c68fc8e46170ab60ef23ed4592dff4dec6e8993/tomli-2.5.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d2ba24db8a9376921b5e87b4762b9adb0f3f1deaea68f2b8b0bb2c11efb9c3e7", size = 115069, upload-time = "2026-10-07T12:23:35.77Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/3f/3e3f8fd0919249b0200c80fbc4f9a1e70be19f9883da71dfb7f8b9ab8aca/tomli-2.5.0-py3-none-any.whl", hash = "sha256:32a7b79ac57a2e83670ce329ccf675798bc5a2094783a63676866b70503f2e2b", size = 14765, upload-time = "2026-10-07T12:23:36.875Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.15.0"
|
version = "4.15.0"
|
||||||
|
|||||||
Reference in new issue
Block a user