Add Container class with lock/trap detection, corpse drops, and numbered duplicate corpses
This commit is contained in:
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
8
.idea/Kanjin-Text-RPG.iml
generated
Normal file
8
.idea/Kanjin-Text-RPG.iml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="jdk" jdkName="Python 3.12" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
7
.idea/dictionaries/project.xml
generated
Normal file
7
.idea/dictionaries/project.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<component name="ProjectDictionaryState">
|
||||||
|
<dictionary name="project">
|
||||||
|
<words>
|
||||||
|
<w>darkvision</w>
|
||||||
|
</words>
|
||||||
|
</dictionary>
|
||||||
|
</component>
|
||||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
7
.idea/misc.xml
generated
Normal file
7
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Black">
|
||||||
|
<option name="sdkName" value="Python 3.12" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/Kanjin-Text-RPG.iml" filepath="$PROJECT_DIR$/.idea/Kanjin-Text-RPG.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
7
.idea/vcs.xml
generated
Normal file
7
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
33
AGENTS.md
Normal file
33
AGENTS.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Pure stdlib. No dependencies, no build, no test/lint tooling.
|
||||||
|
|
||||||
|
## Entrypoints
|
||||||
|
|
||||||
|
- `main.py:main_game_loop()` — game start
|
||||||
|
- `function_list.py:startGame()` — character creation (returns `name, gender, age, race, class_, pre_allocated_stats`)
|
||||||
|
|
||||||
|
## Play vs. debug
|
||||||
|
|
||||||
|
To skip character creation during playtesting, replace `startGame()` with a pre-generated character and assign stats directly (see `main.py:274-290` and `function_list.py:14-94` for the 3 pre-gens).
|
||||||
|
|
||||||
|
## Quirks
|
||||||
|
|
||||||
|
- `sys.path.append('.')` is required at the top of `main.py`, `scene.py`, `function_list.py` — sibling imports break without it.
|
||||||
|
- `class` is a reserved keyword, so the project uses `class_` everywhere (parameter/attribute names, e.g. `Player.__init__(self, ..., class_)`).
|
||||||
|
- Race/class modules export `RACES` and `CLASSES` dicts (e.g. `RACES["Elf"]`, `CLASSES["Wizard"]`). The underlying classes (`Race`, `CharacterClass`) are preserved for instantiation if needed.
|
||||||
|
- `CharacterClass.__init__` receives `class_` as a parameter — do not confuse with Python's `class` statement.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- `scene.py` — a directed graph of `Scene` objects linked via `add_exit(direction, scene)`. The `Scene` class also holds `available_items` (free pickups) and `lootable_items` (container contents). Containers use the `Container` class which has a `description`, `lock_dc` / `trap_dc` (DC for detecting locks/traps via an Intelligence check), and visible `contents`. Locked containers can't be opened (contents hidden); trapped containers trigger if opened without detection/disarming. Use `scene.add_corpse(enemy_name, items_dict)` to drop a lootable corpse when an enemy dies.
|
||||||
|
- `player.py` — `Player`, `Inventory` (equipped slots, carried items, weight/encumbrance, currency), item hierarchy (`Item` → `Weapon`, `Armor`, `Money`).
|
||||||
|
- `spell_list.py` — `Spell` objects grouped into `SPELL_LISTS[class_name][spell_level]`. Casting uses an SRD-style slot system with upcasting.
|
||||||
|
- `enum_list.py` — shared enums (`Slots`, `DamageType`, `ItemType`, `SaveType`, etc.).
|
||||||
|
- Modifier formula: `mod = -5 + stat // 2` (standard D&D 5e).
|
||||||
BIN
__pycache__/class_list.cpython-311.pyc
Normal file
BIN
__pycache__/class_list.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/class_list.cpython-312.pyc
Normal file
BIN
__pycache__/class_list.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/enum_list.cpython-311.pyc
Normal file
BIN
__pycache__/enum_list.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/enum_list.cpython-312.pyc
Normal file
BIN
__pycache__/enum_list.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/function_list.cpython-311.pyc
Normal file
BIN
__pycache__/function_list.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/function_list.cpython-312.pyc
Normal file
BIN
__pycache__/function_list.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/job_list.cpython-312.pyc
Normal file
BIN
__pycache__/job_list.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/main.cpython-311.pyc
Normal file
BIN
__pycache__/main.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/player.cpython-311.pyc
Normal file
BIN
__pycache__/player.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/player.cpython-312.pyc
Normal file
BIN
__pycache__/player.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/race_list.cpython-311.pyc
Normal file
BIN
__pycache__/race_list.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/race_list.cpython-312.pyc
Normal file
BIN
__pycache__/race_list.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/scene.cpython-311.pyc
Normal file
BIN
__pycache__/scene.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/scene.cpython-312.pyc
Normal file
BIN
__pycache__/scene.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/spell_list.cpython-311.pyc
Normal file
BIN
__pycache__/spell_list.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/spell_list.cpython-312.pyc
Normal file
BIN
__pycache__/spell_list.cpython-312.pyc
Normal file
Binary file not shown.
@@ -1,5 +1,14 @@
|
|||||||
class Job:
|
# NOTE ON NAMING: this represents a D&D "class" (Barbarian, Cleric, Wizard...), but `class`
|
||||||
def __init__(self, name, description, hitdie, proficiency, savingthrow, skill, gold):
|
# is a reserved Python keyword and can't be used as a variable/parameter/attribute name.
|
||||||
|
# The convention used throughout this project (per PEP 8) is:
|
||||||
|
# - The type itself is named `CharacterClass` (capitalised type names don't collide with
|
||||||
|
# the keyword, but "Class" alone reads ambiguously next to Python's own `class` statement).
|
||||||
|
# - Any variable, parameter, or attribute that holds one of these is named `class_`
|
||||||
|
# (trailing underscore), e.g. `self.class_` on Player, or `class_=Wizard`.
|
||||||
|
class CharacterClass:
|
||||||
|
def __init__(self, name, description, hitdie, proficiency, savingthrow, skill, gold,
|
||||||
|
spellcasting_ability=None, cantrips_known=0, spell_slots=None,
|
||||||
|
spellbook_style=None):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.description = description
|
self.description = description
|
||||||
self.hitdie = hitdie
|
self.hitdie = hitdie
|
||||||
@@ -8,6 +17,20 @@ class Job:
|
|||||||
self.skill = skill
|
self.skill = skill
|
||||||
self.gold = gold
|
self.gold = gold
|
||||||
|
|
||||||
|
# --- Spellcasting (SRD) ---
|
||||||
|
# spellcasting_ability: e.g. "Intelligence" for Wizard, "Wisdom" for Cleric. None = non-caster.
|
||||||
|
self.spellcasting_ability = spellcasting_ability
|
||||||
|
self.cantrips_known = cantrips_known
|
||||||
|
# spell_slots: dict of {spell_level: slot_count} at character level 1.
|
||||||
|
self.spell_slots = spell_slots or {}
|
||||||
|
# spellbook_style: "spellbook" (Wizard - learns a fixed list, prepares a subset)
|
||||||
|
# "all_known" (Cleric - knows/can prepare from their entire class list)
|
||||||
|
self.spellbook_style = spellbook_style
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_caster(self):
|
||||||
|
return self.spellcasting_ability is not None
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
profs = ""
|
profs = ""
|
||||||
for key, value in self.proficiency.items():
|
for key, value in self.proficiency.items():
|
||||||
@@ -24,7 +47,7 @@ class Job:
|
|||||||
+ '\n'.join(self.skill[1:]).title() + '\n'
|
+ '\n'.join(self.skill[1:]).title() + '\n'
|
||||||
|
|
||||||
|
|
||||||
class Barbarian(Job):
|
class Barbarian(CharacterClass):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
name = "Barbarian"
|
name = "Barbarian"
|
||||||
description = "A tall human tribesman strides through a blizzard, draped in fur and hefting his axe. " \
|
description = "A tall human tribesman strides through a blizzard, draped in fur and hefting his axe. " \
|
||||||
@@ -72,7 +95,7 @@ class Barbarian(Job):
|
|||||||
super().__init__(name, description, hitdie, proficiency, savingthrow, skills, gold)
|
super().__init__(name, description, hitdie, proficiency, savingthrow, skills, gold)
|
||||||
|
|
||||||
|
|
||||||
class Cleric(Job):
|
class Cleric(CharacterClass):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
name = "Cleric"
|
name = "Cleric"
|
||||||
description = "Arms and eyes upraised toward the sun and a prayer on his lips, an elf begins to glow with " \
|
description = "Arms and eyes upraised toward the sun and a prayer on his lips, an elf begins to glow with " \
|
||||||
@@ -111,10 +134,12 @@ class Cleric(Job):
|
|||||||
"religion"
|
"religion"
|
||||||
]
|
]
|
||||||
gold = [2, 10]
|
gold = [2, 10]
|
||||||
super().__init__(name, description, hitdie, proficiency, savingthrow, skill, gold)
|
super().__init__(name, description, hitdie, proficiency, savingthrow, skill, gold,
|
||||||
|
spellcasting_ability="Wisdom", cantrips_known=3,
|
||||||
|
spell_slots={1: 2}, spellbook_style="all_known")
|
||||||
|
|
||||||
|
|
||||||
class Wizard(Job):
|
class Wizard(CharacterClass):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
name = "Wizard"
|
name = "Wizard"
|
||||||
description = "Clad in the silver robes that denote her station, an elf closes her eyes to shut out the " \
|
description = "Clad in the silver robes that denote her station, an elf closes her eyes to shut out the " \
|
||||||
@@ -165,9 +190,13 @@ class Wizard(Job):
|
|||||||
"religion"
|
"religion"
|
||||||
]
|
]
|
||||||
gold = [2, 10]
|
gold = [2, 10]
|
||||||
super().__init__(name, description, hitdie, proficiency, savingthrow, skills, gold)
|
super().__init__(name, description, hitdie, proficiency, savingthrow, skills, gold,
|
||||||
|
spellcasting_ability="Intelligence", cantrips_known=3,
|
||||||
|
spell_slots={1: 2}, spellbook_style="spellbook")
|
||||||
|
|
||||||
|
|
||||||
Barbarian = Barbarian()
|
CLASSES = {
|
||||||
Cleric = Cleric()
|
"Barbarian": Barbarian(),
|
||||||
Wizard = Wizard()
|
"Cleric": Cleric(),
|
||||||
|
"Wizard": Wizard(),
|
||||||
|
}
|
||||||
22
enum_list.py
22
enum_list.py
@@ -48,3 +48,25 @@ class Slots(Enum):
|
|||||||
RightRing = auto()
|
RightRing = auto()
|
||||||
Other = auto()
|
Other = auto()
|
||||||
Attunement = auto()
|
Attunement = auto()
|
||||||
|
|
||||||
|
|
||||||
|
class SpellSchool(Enum):
|
||||||
|
Abjuration = auto()
|
||||||
|
Conjuration = auto()
|
||||||
|
Divination = auto()
|
||||||
|
Enchantment = auto()
|
||||||
|
Evocation = auto()
|
||||||
|
Illusion = auto()
|
||||||
|
Necromancy = auto()
|
||||||
|
Transmutation = auto()
|
||||||
|
|
||||||
|
|
||||||
|
class SaveType(Enum):
|
||||||
|
"""Which stat a target rolls to resist a spell's effect."""
|
||||||
|
Strength = auto()
|
||||||
|
Dexterity = auto()
|
||||||
|
Constitution = auto()
|
||||||
|
Intelligence = auto()
|
||||||
|
Wisdom = auto()
|
||||||
|
Charisma = auto()
|
||||||
|
NONE = auto() # No saving throw (e.g. Magic Missile)
|
||||||
142
function_list.py
142
function_list.py
@@ -4,8 +4,8 @@ sys.path.append('.')
|
|||||||
|
|
||||||
from player import Player # Only import what's necessary
|
from player import Player # Only import what's necessary
|
||||||
from enum_list import ItemType, Slots
|
from enum_list import ItemType, Slots
|
||||||
from race_list import Elf, Dwarf, Human
|
from race_list import RACES
|
||||||
from job_list import Barbarian, Cleric, Wizard
|
from class_list import CLASSES
|
||||||
|
|
||||||
|
|
||||||
def wait():
|
def wait():
|
||||||
@@ -16,8 +16,8 @@ pre_generated_characters = {
|
|||||||
"name": "Arion", # Example Name
|
"name": "Arion", # Example Name
|
||||||
"gender": "Male",
|
"gender": "Male",
|
||||||
"age": 25,
|
"age": 25,
|
||||||
"race": Elf,
|
"race": RACES["Elf"],
|
||||||
"job": Wizard, # Based on the High Elf sheet
|
"class_": CLASSES["Wizard"], # Based on the High Elf sheet
|
||||||
"alignment": "lawful good",
|
"alignment": "lawful good",
|
||||||
"description": "A scholarly High Elf skilled in the arcane arts.",
|
"description": "A scholarly High Elf skilled in the arcane arts.",
|
||||||
"stats": {
|
"stats": {
|
||||||
@@ -41,8 +41,8 @@ pre_generated_characters = {
|
|||||||
"name": "Brundle", # Example Name
|
"name": "Brundle", # Example Name
|
||||||
"gender": "Female",
|
"gender": "Female",
|
||||||
"age": 35,
|
"age": 35,
|
||||||
"race": Human,
|
"race": RACES["Human"],
|
||||||
"job": Barbarian, # Based on the Human sheet
|
"class_": CLASSES["Barbarian"], # Based on the Human sheet
|
||||||
"alignment": "chaotic good",
|
"alignment": "chaotic good",
|
||||||
"description": "A robust Human warrior, charging into battle.",
|
"description": "A robust Human warrior, charging into battle.",
|
||||||
"stats": {
|
"stats": {
|
||||||
@@ -67,8 +67,8 @@ pre_generated_characters = {
|
|||||||
"name": "Drok", # Example Name
|
"name": "Drok", # Example Name
|
||||||
"gender": "Other",
|
"gender": "Other",
|
||||||
"age": 75, # Dwarves live longer
|
"age": 75, # Dwarves live longer
|
||||||
"race": Dwarf,
|
"race": RACES["Dwarf"],
|
||||||
"job": Cleric, # Based on the Hill Dwarf sheet (Life Domain implied by "healing" focus)
|
"class_": CLASSES["Cleric"], # Based on the Hill Dwarf sheet (Life Domain implied by "healing" focus)
|
||||||
"alignment": "lawful good",
|
"alignment": "lawful good",
|
||||||
"description": "A stout Hill Dwarf cleric, a pillar of his community.",
|
"description": "A stout Hill Dwarf cleric, a pillar of his community.",
|
||||||
"stats": {
|
"stats": {
|
||||||
@@ -142,10 +142,10 @@ def startGame():
|
|||||||
print("Sorry, I didn't catch that. Please try again.\n")
|
print("Sorry, I didn't catch that. Please try again.\n")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# --- Race and Job Selection ---
|
# --- Race and Class Selection ---
|
||||||
race = None
|
race = None
|
||||||
job = None
|
class_ = None
|
||||||
# Loop until Race and Job are confirmed
|
# Loop until Race and Class are confirmed
|
||||||
while True:
|
while True:
|
||||||
# Race Selection
|
# Race Selection
|
||||||
selected_race_obj = None
|
selected_race_obj = None
|
||||||
@@ -154,11 +154,11 @@ def startGame():
|
|||||||
race_choice = input("Please select a race to learn more about it:\n"
|
race_choice = input("Please select a race to learn more about it:\n"
|
||||||
"1) Elf\n2) Dwarf\n3) Human\n >> ").title()
|
"1) Elf\n2) Dwarf\n3) Human\n >> ").title()
|
||||||
if race_choice in ['Elf', '1']:
|
if race_choice in ['Elf', '1']:
|
||||||
selected_race_obj = Elf
|
selected_race_obj = RACES["Elf"]
|
||||||
elif race_choice in ['Dwarf', '2']:
|
elif race_choice in ['Dwarf', '2']:
|
||||||
selected_race_obj = Dwarf
|
selected_race_obj = RACES["Dwarf"]
|
||||||
elif race_choice in ['Human', '3']:
|
elif race_choice in ['Human', '3']:
|
||||||
selected_race_obj = Human
|
selected_race_obj = RACES["Human"]
|
||||||
else:
|
else:
|
||||||
print("Sorry I didn't recognise that race. Please select 'Elf', 'Dwarf', or 'Human'.\n")
|
print("Sorry I didn't recognise that race. Please select 'Elf', 'Dwarf', or 'Human'.\n")
|
||||||
continue
|
continue
|
||||||
@@ -177,48 +177,48 @@ def startGame():
|
|||||||
print("Sorry, I didn't catch that. Please try again.\n")
|
print("Sorry, I didn't catch that. Please try again.\n")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Job Selection
|
# Class Selection
|
||||||
selected_job_obj = None
|
selected_class_obj = None
|
||||||
# Loop for selecting and viewing job
|
# Loop for selecting and viewing class
|
||||||
while True:
|
while True:
|
||||||
job_choice = input("Please select a job to learn more about it:\n"
|
class_choice = input("Please select a class to learn more about it:\n"
|
||||||
"1) Barbarian\n2) Cleric\n3) Wizard\n >> ").title()
|
"1) Barbarian\n2) Cleric\n3) Wizard\n >> ").title()
|
||||||
if job_choice in ['Barbarian', '1']:
|
if class_choice in ['Barbarian', '1']:
|
||||||
selected_job_obj = Barbarian
|
selected_class_obj = CLASSES["Barbarian"]
|
||||||
elif job_choice in ['Cleric', '2']:
|
elif class_choice in ['Cleric', '2']:
|
||||||
selected_job_obj = Cleric
|
selected_class_obj = CLASSES["Cleric"]
|
||||||
elif job_choice in ['Wizard', '3']:
|
elif class_choice in ['Wizard', '3']:
|
||||||
selected_job_obj = Wizard
|
selected_class_obj = CLASSES["Wizard"]
|
||||||
else:
|
else:
|
||||||
print("Sorry I didn't recognise that job. Please select 'Barbarian', 'Cleric', or 'Wizard'.\n")
|
print("Sorry I didn't recognise that class. Please select 'Barbarian', 'Cleric', or 'Wizard'.\n")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if selected_job_obj:
|
if selected_class_obj:
|
||||||
print(f"\n--- {selected_job_obj.name} ---")
|
print(f"\n--- {selected_class_obj.name} ---")
|
||||||
print(selected_job_obj.__repr__())
|
print(selected_class_obj.__repr__())
|
||||||
|
|
||||||
print('Would you like to proceed with this job or view another?')
|
print('Would you like to proceed with this class or view another?')
|
||||||
proceed = input('1) Proceed\n2) View another job\n >> ')
|
proceed = input('1) Proceed\n2) View another class\n >> ')
|
||||||
if proceed.lower() in ('1', 'proceed'):
|
if proceed.lower() in ('1', 'proceed'):
|
||||||
job = selected_job_obj # Assign job object
|
class_ = selected_class_obj # Assign class object
|
||||||
break # Exit job selection loop
|
break # Exit class selection loop
|
||||||
elif proceed.lower() in ('2', 'view', 'view another', 'view another job'):
|
elif proceed.lower() in ('2', 'view', 'view another', 'view another class'):
|
||||||
continue # Restart job selection
|
continue # Restart class selection
|
||||||
else:
|
else:
|
||||||
print("Sorry, I didn't catch that. Please try again.\n")
|
print("Sorry, I didn't catch that. Please try again.\n")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Final confirmation for both Race and Job
|
# Final confirmation for both Race and Class
|
||||||
while True:
|
while True:
|
||||||
# This safeguard should ideally not be hit if inner loops work correctly
|
# This safeguard should ideally not be hit if inner loops work correctly
|
||||||
if race is None or job is None:
|
if race is None or class_ is None:
|
||||||
print("Error: Race or Job not selected. Restarting Race/Job selection.")
|
print("Error: Race or Class not selected. Restarting Race/Class selection.")
|
||||||
break # Break out of this inner loop to restart the outer race/job loop
|
break # Break out of this inner loop to restart the outer race/class loop
|
||||||
correct = input(f"\n{name}, you are a {race.name_adjective} {job.name}.\nIs this correct? Y/N\n >> ")
|
correct = input(f"\n{name}, you are a {race.name_adjective} {class_.name}.\nIs this correct? Y/N\n >> ")
|
||||||
if correct.lower() in ['y', 'yes']:
|
if correct.lower() in ['y', 'yes']:
|
||||||
return name, gender, age, race, job, None# All confirmed, return values
|
return name, gender, age, race, class_, None# All confirmed, return values
|
||||||
elif correct.lower() in ['n', 'no']:
|
elif correct.lower() in ['n', 'no']:
|
||||||
# If not correct, break this loop to re-enter race/job selection
|
# If not correct, break this loop to re-enter race/class selection
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
print("Sorry, I didn't catch that. Please try again.\n")
|
print("Sorry, I didn't catch that. Please try again.\n")
|
||||||
@@ -233,19 +233,19 @@ def startGame():
|
|||||||
gender = input("Gender (Male, Female, Other): ")
|
gender = input("Gender (Male, Female, Other): ")
|
||||||
age = int(input("Age: "))
|
age = int(input("Age: "))
|
||||||
race_str = input("Race (Elf, Dwarf, Human): ")
|
race_str = input("Race (Elf, Dwarf, Human): ")
|
||||||
job_str = input("Job (Barbarian, Cleric, Wizard): ")
|
class_str = input("Class (Barbarian, Cleric, Wizard): ")
|
||||||
|
|
||||||
# Map string inputs to actual Race/Job objects for consistency
|
# Map string inputs to actual Race/Class objects for consistency
|
||||||
race_map = {'elf': Elf, 'dwarf': Dwarf, 'human': Human}
|
race_map = {'elf': RACES["Elf"], 'dwarf': RACES["Dwarf"], 'human': RACES["Human"]}
|
||||||
job_map = {'barbarian': Barbarian, 'cleric': Cleric, 'wizard': Wizard}
|
class_map = {'barbarian': CLASSES["Barbarian"], 'cleric': CLASSES["Cleric"], 'wizard': CLASSES["Wizard"]}
|
||||||
|
|
||||||
actual_race = race_map.get(race_str.lower())
|
actual_race = race_map.get(race_str.lower())
|
||||||
actual_job = job_map.get(job_str.lower())
|
actual_class = class_map.get(class_str.lower())
|
||||||
|
|
||||||
if actual_race and actual_job:
|
if actual_race and actual_class:
|
||||||
return name, gender, age, actual_race, actual_job, None
|
return name, gender, age, actual_race, actual_class, None
|
||||||
else:
|
else:
|
||||||
print("Invalid race or job entered for pre-generated character. Please try again.")
|
print("Invalid race or class entered for pre-generated character. Please try again.")
|
||||||
start = "1"
|
start = "1"
|
||||||
continue
|
continue
|
||||||
return None
|
return None
|
||||||
@@ -267,7 +267,7 @@ def startGame():
|
|||||||
print("Who would you like to to play?")
|
print("Who would you like to to play?")
|
||||||
for key, char_data in pre_generated_characters.items():
|
for key, char_data in pre_generated_characters.items():
|
||||||
print(
|
print(
|
||||||
f"{key}) {char_data['name']} ({char_data['race'].name_adjective} {char_data['job'].name}, {char_data['alignment']})")
|
f"{key}) {char_data['name']} ({char_data['race'].name_adjective} {char_data['class_'].name}, {char_data['alignment']})")
|
||||||
print("Type 'back' to return to character creation options.")
|
print("Type 'back' to return to character creation options.")
|
||||||
|
|
||||||
char_choice = input(">> ").strip().lower()
|
char_choice = input(">> ").strip().lower()
|
||||||
@@ -284,7 +284,7 @@ def startGame():
|
|||||||
print(f"Gender: {selected_char_data['gender']}")
|
print(f"Gender: {selected_char_data['gender']}")
|
||||||
print(f"Age: {selected_char_data['age']}")
|
print(f"Age: {selected_char_data['age']}")
|
||||||
print(f"Race: {selected_char_data['race'].name_adjective}")
|
print(f"Race: {selected_char_data['race'].name_adjective}")
|
||||||
print(f"Class: {selected_char_data['job'].name}")
|
print(f"Class: {selected_char_data['class_'].name}")
|
||||||
print(f"Alignment: {selected_char_data['alignment']}")
|
print(f"Alignment: {selected_char_data['alignment']}")
|
||||||
print(f"Description: {selected_char_data['description']}")
|
print(f"Description: {selected_char_data['description']}")
|
||||||
print(f"Age: {selected_char_data['age']}")
|
print(f"Age: {selected_char_data['age']}")
|
||||||
@@ -340,11 +340,11 @@ def startGame():
|
|||||||
gender = selected_char_data['gender']
|
gender = selected_char_data['gender']
|
||||||
age = selected_char_data['age']
|
age = selected_char_data['age']
|
||||||
race = selected_char_data['race']
|
race = selected_char_data['race']
|
||||||
job = selected_char_data['job']
|
class_ = selected_char_data['class_']
|
||||||
pre_allocated_stats = selected_char_data['stats']
|
pre_allocated_stats = selected_char_data['stats']
|
||||||
|
|
||||||
print(f"\nYou have selected: {name}, a {age} year old {race.name_adjective} {job.name}.")
|
print(f"\nYou have selected: {name}, a {age} year old {race.name_adjective} {class_.name}.")
|
||||||
return name, gender, age, race, job, pre_allocated_stats # Exit all loops and function
|
return name, gender, age, race, class_, pre_allocated_stats # Exit all loops and function
|
||||||
|
|
||||||
elif confirm_choice in ("2", "no"):
|
elif confirm_choice in ("2", "no"):
|
||||||
print("\nReturning to pre-generated character selection.")
|
print("\nReturning to pre-generated character selection.")
|
||||||
@@ -532,7 +532,13 @@ def get_instructions():
|
|||||||
"Enter cave | house | room - maybe... TBC\n"
|
"Enter cave | house | room - maybe... TBC\n"
|
||||||
"Scene or Location - Replays the current area's details\n"
|
"Scene or Location - Replays the current area's details\n"
|
||||||
"Look around - lists the available items in your scene without displaying the scene description.\n"
|
"Look around - lists the available items in your scene without displaying the scene description.\n"
|
||||||
"Examine | Open | Loot *object* - Provides details of an item, opens a container, or takes an object.")
|
"Examine | Open | Loot *object* - Provides details of an item, opens a container, or takes an object.\n"
|
||||||
|
"Spells | Spellbook - Lists your cantrips, known/preparable spells, prepared spells, and slots.\n"
|
||||||
|
"Cast *spell name* - Casts a cantrip or prepared spell.\n"
|
||||||
|
"Prepare | Unprepare *spell name* - Moves a spell in/out of your prepared list.\n"
|
||||||
|
"Rest - Long rest: restores HP and spell slots.\n"
|
||||||
|
"Check weight | carry | encumbrance - Shows carrying weight vs capacity.\n"
|
||||||
|
"Check currency | coins | gold - Shows coinage held and total gp value.")
|
||||||
print(descrip1)
|
print(descrip1)
|
||||||
time.sleep(3) # Reduced sleep for faster testing
|
time.sleep(3) # Reduced sleep for faster testing
|
||||||
print(descrip2)
|
print(descrip2)
|
||||||
@@ -580,6 +586,24 @@ def parse(input_text):
|
|||||||
command = "go"
|
command = "go"
|
||||||
object1 = " ".join(words[1:])
|
object1 = " ".join(words[1:])
|
||||||
return command, object1
|
return command, object1
|
||||||
|
elif words[0] == "cast":
|
||||||
|
command = "cast"
|
||||||
|
object1 = " ".join(words[1:])
|
||||||
|
return command, object1
|
||||||
|
elif words[0] == "prepare":
|
||||||
|
command = "prepare"
|
||||||
|
object1 = " ".join(words[1:])
|
||||||
|
return command, object1
|
||||||
|
elif words[0] == "unprepare":
|
||||||
|
command = "unprepare"
|
||||||
|
object1 = " ".join(words[1:])
|
||||||
|
return command, object1
|
||||||
|
elif words[0] == "check" and words[1] in ("weight", "carry", "encumbrance"):
|
||||||
|
command = "weight"
|
||||||
|
return command, object1
|
||||||
|
elif words[0] == "check" and words[1] in ("currency", "coins", "gold", "money"):
|
||||||
|
command = "currency"
|
||||||
|
return command, object1
|
||||||
|
|
||||||
# Single-word commands
|
# Single-word commands
|
||||||
if words[0] == "help":
|
if words[0] == "help":
|
||||||
@@ -630,6 +654,14 @@ def parse(input_text):
|
|||||||
command = "drop" # User needs to specify what to drop
|
command = "drop" # User needs to specify what to drop
|
||||||
object1 = None
|
object1 = None
|
||||||
|
|
||||||
|
elif words[0] in ("spells", "spellbook"):
|
||||||
|
command = "spells"
|
||||||
|
elif words[0] == "rest":
|
||||||
|
command = "rest"
|
||||||
|
elif words[0] in ("weight", "carry", "encumbrance"):
|
||||||
|
command = "weight"
|
||||||
|
elif words[0] in ("currency", "coins", "gold", "money"):
|
||||||
|
command = "currency"
|
||||||
elif words[0] == "quit":
|
elif words[0] == "quit":
|
||||||
command = "quit"
|
command = "quit"
|
||||||
else:
|
else:
|
||||||
|
|||||||
139
main.py
139
main.py
@@ -1,4 +1,5 @@
|
|||||||
import sys
|
import sys
|
||||||
|
from random import randint
|
||||||
# Add the current directory to sys.path to allow imports from sibling files
|
# Add the current directory to sys.path to allow imports from sibling files
|
||||||
sys.path.append('.')
|
sys.path.append('.')
|
||||||
from player import Player
|
from player import Player
|
||||||
@@ -36,8 +37,8 @@ class Engine:
|
|||||||
# Display lootable containers
|
# Display lootable containers
|
||||||
if self.current_scene.lootable_items:
|
if self.current_scene.lootable_items:
|
||||||
print("\nYou also notice:")
|
print("\nYou also notice:")
|
||||||
for container_name, items_in_container in self.current_scene.lootable_items.items():
|
for container_name, container in self.current_scene.lootable_items.items():
|
||||||
if any(count > 0 for count in items_in_container.values()): # Check if container has any items left
|
if container.has_items():
|
||||||
print(f" - {container_name.title()}")
|
print(f" - {container_name.title()}")
|
||||||
|
|
||||||
def look_around(self):
|
def look_around(self):
|
||||||
@@ -53,8 +54,8 @@ class Engine:
|
|||||||
# Display lootable containers
|
# Display lootable containers
|
||||||
if self.current_scene.lootable_items:
|
if self.current_scene.lootable_items:
|
||||||
print("\nYou also notice:")
|
print("\nYou also notice:")
|
||||||
for container_name, items_in_container in self.current_scene.lootable_items.items():
|
for container_name, container in self.current_scene.lootable_items.items():
|
||||||
if any(count > 0 for count in items_in_container.values()): # Check if container has any items left
|
if container.has_items():
|
||||||
print(f" - {container_name.title()}")
|
print(f" - {container_name.title()}")
|
||||||
|
|
||||||
|
|
||||||
@@ -116,19 +117,45 @@ class Engine:
|
|||||||
if not found_in_scene:
|
if not found_in_scene:
|
||||||
# Check lootable containers in the current scene
|
# Check lootable containers in the current scene
|
||||||
found_in_loot = False
|
found_in_loot = False
|
||||||
for container_name, items_in_container in self.current_scene.lootable_items.items():
|
for container_name, container in self.current_scene.lootable_items.items():
|
||||||
if object_name.lower() in container_name.lower(): # If user tries to examine the container itself
|
if object_name.lower() in container_name.lower():
|
||||||
print(f"\nYou examine the {container_name}. Inside, you see:")
|
print(f"\n--- {container.name.title()} ---")
|
||||||
if items_in_container:
|
print(container.description)
|
||||||
for item_obj, count in items_in_container.items():
|
|
||||||
if count > 0:
|
|
||||||
print(f" - {item_obj.name} (x{count})")
|
|
||||||
else:
|
|
||||||
print("It appears to be empty.")
|
|
||||||
found_in_loot = True
|
|
||||||
break # Exit after examining the container
|
|
||||||
|
|
||||||
for item_obj, count in items_in_container.items():
|
int_mod = self.player.mods["Intelligence"]
|
||||||
|
noticed_something = False
|
||||||
|
|
||||||
|
if container.lock_dc:
|
||||||
|
roll = randint(1, 20) + int_mod
|
||||||
|
print(f"\nInvestigation check: 1d20{int_mod:+} = {roll} vs DC {container.lock_dc}")
|
||||||
|
if roll >= container.lock_dc:
|
||||||
|
print("It appears to be locked.")
|
||||||
|
noticed_something = True
|
||||||
|
elif not container.trap_dc:
|
||||||
|
print("You don't notice anything unusual about it.")
|
||||||
|
|
||||||
|
if container.trap_dc:
|
||||||
|
roll = randint(1, 20) + int_mod
|
||||||
|
print(f"\nInvestigation check: 1d20{int_mod:+} = {roll} vs DC {container.trap_dc}")
|
||||||
|
if roll >= container.trap_dc:
|
||||||
|
print("You notice a trap mechanism on it.")
|
||||||
|
container.trap_detected = True
|
||||||
|
noticed_something = True
|
||||||
|
elif not container.lock_dc and not noticed_something:
|
||||||
|
print("You don't notice anything unusual about it.")
|
||||||
|
|
||||||
|
if not container.is_locked:
|
||||||
|
if container.contents:
|
||||||
|
print("\nInside, you see:")
|
||||||
|
for item_obj, count in container.contents.items():
|
||||||
|
if count > 0:
|
||||||
|
print(f" - {item_obj.name} (x{count})")
|
||||||
|
else:
|
||||||
|
print("It appears to be empty.")
|
||||||
|
found_in_loot = True
|
||||||
|
break
|
||||||
|
|
||||||
|
for item_obj, count in container.contents.items():
|
||||||
if object_name.lower() in item_obj.name.lower() and count > 0:
|
if object_name.lower() in item_obj.name.lower() and count > 0:
|
||||||
print(f"\n--- {item_obj.name} ---")
|
print(f"\n--- {item_obj.name} ---")
|
||||||
print(item_obj.description)
|
print(item_obj.description)
|
||||||
@@ -185,22 +212,32 @@ class Engine:
|
|||||||
|
|
||||||
def loot_container(self, container_name):
|
def loot_container(self, container_name):
|
||||||
"""Allows the player to loot items from a specified container."""
|
"""Allows the player to loot items from a specified container."""
|
||||||
container_found = False
|
for current_name, container in list(self.current_scene.lootable_items.items()):
|
||||||
for current_container_name, items_in_container in list(self.current_scene.lootable_items.items()):
|
if current_name.lower() == container_name.lower():
|
||||||
if current_container_name.lower() == container_name.lower():
|
if container.is_locked:
|
||||||
container_found = True
|
print(f"The {container_name} is locked.")
|
||||||
if not items_in_container:
|
return
|
||||||
|
|
||||||
|
if container.is_trapped and not container.trap_disarmed:
|
||||||
|
if container.trap_detected:
|
||||||
|
print(f"The {container_name} is trapped. You'll need to disable the trap first.")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
print(f"You open the {container_name}. A trap is triggered!")
|
||||||
|
# TODO: trap damage/effects
|
||||||
|
return
|
||||||
|
|
||||||
|
if not container.contents:
|
||||||
print(f"The {container_name} is empty.")
|
print(f"The {container_name} is empty.")
|
||||||
return
|
return
|
||||||
|
|
||||||
print(f"You look inside the {container_name}. You see:")
|
print(f"You look inside the {container_name}. You see:")
|
||||||
loot_list = list(items_in_container.items())
|
loot_list = list(container.contents.items())
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
for i, (item_obj, count) in enumerate(loot_list, 1):
|
for i, (item_obj, count) in enumerate(loot_list, 1):
|
||||||
print(f"{i}) {item_obj.name} (x{count})")
|
print(f"{i}) {item_obj.name} (x{count})")
|
||||||
|
|
||||||
|
|
||||||
take_all_option = len(loot_list) + 1
|
take_all_option = len(loot_list) + 1
|
||||||
leave_option = len(loot_list) + 2
|
leave_option = len(loot_list) + 2
|
||||||
|
|
||||||
@@ -209,13 +246,14 @@ class Engine:
|
|||||||
|
|
||||||
choice = input("What would you like to take? (number or 'take all'/'leave')\n>> ").lower().strip()
|
choice = input("What would you like to take? (number or 'take all'/'leave')\n>> ").lower().strip()
|
||||||
|
|
||||||
|
|
||||||
if choice == "take all" or (choice.isdigit() and int(choice) == take_all_option):
|
if choice == "take all" or (choice.isdigit() and int(choice) == take_all_option):
|
||||||
for item_obj, count in list(items_in_container.items()):
|
for item_obj, count in list(container.contents.items()):
|
||||||
self.player.inventory.add_item(item_obj, count)
|
self.player.inventory.add_item(item_obj, count)
|
||||||
print(f"- Took {count} {item_obj.name}")
|
print(f"- Took {count} {item_obj.name}")
|
||||||
del items_in_container[item_obj]
|
container.remove_item(item_obj, count)
|
||||||
print(f"The {container_name} is now empty.")
|
print(f"The {container_name} is now empty.")
|
||||||
|
if not container.has_items():
|
||||||
|
del self.current_scene.lootable_items[current_name]
|
||||||
return
|
return
|
||||||
elif choice == "leave" or (choice.isdigit() and int(choice) == leave_option):
|
elif choice == "leave" or (choice.isdigit() and int(choice) == leave_option):
|
||||||
print(f"You leave the {container_name} untouched.")
|
print(f"You leave the {container_name} untouched.")
|
||||||
@@ -239,18 +277,16 @@ class Engine:
|
|||||||
|
|
||||||
if 0 < take_count <= current_count:
|
if 0 < take_count <= current_count:
|
||||||
self.player.inventory.add_item(item_obj, take_count)
|
self.player.inventory.add_item(item_obj, take_count)
|
||||||
items_in_container[item_obj] -= take_count
|
container.remove_item(item_obj, take_count)
|
||||||
print(f"You took {take_count} {item_obj.name}.")
|
print(f"You took {take_count} {item_obj.name}.")
|
||||||
|
|
||||||
if items_in_container[item_obj] <= 0:
|
if item_obj not in container.contents:
|
||||||
del items_in_container[item_obj]
|
loot_list = list(container.contents.items())
|
||||||
loot_list = list(items_in_container.items())
|
|
||||||
|
|
||||||
if not items_in_container:
|
if not container.contents:
|
||||||
print(f"The {container_name} is now empty.")
|
|
||||||
return
|
|
||||||
elif not loot_list:
|
|
||||||
print(f"The {container_name} is now empty.")
|
print(f"The {container_name} is now empty.")
|
||||||
|
if not container.has_items():
|
||||||
|
del self.current_scene.lootable_items[current_name]
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
print("Invalid amount or not enough items.")
|
print("Invalid amount or not enough items.")
|
||||||
@@ -260,8 +296,7 @@ class Engine:
|
|||||||
print("Invalid selection. Please enter a number, 'take all', or 'leave'.")
|
print("Invalid selection. Please enter a number, 'take all', or 'leave'.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not container_found:
|
print(f"You don't see a '{container_name}' here to loot.")
|
||||||
print(f"You don't see a '{container_name}' here to loot.")
|
|
||||||
|
|
||||||
|
|
||||||
# Initialize the game engine
|
# Initialize the game engine
|
||||||
@@ -272,10 +307,10 @@ def main_game_loop():
|
|||||||
The main loop for the game, handling character creation and continuous gameplay.
|
The main loop for the game, handling character creation and continuous gameplay.
|
||||||
"""
|
"""
|
||||||
# Character Creation
|
# Character Creation
|
||||||
name, gender, age, race, job, pre_allocated_stats = startGame()
|
name, gender, age, race, class_, pre_allocated_stats = startGame()
|
||||||
|
|
||||||
# Create player instance
|
# Create player instance
|
||||||
player = Player(name, gender, age, race, job)
|
player = Player(name, gender, age, race, class_)
|
||||||
game_engine.set_player(player) # Set the player in the game engine
|
game_engine.set_player(player) # Set the player in the game engine
|
||||||
|
|
||||||
# Display initial character summary
|
# Display initial character summary
|
||||||
@@ -286,13 +321,16 @@ def main_game_loop():
|
|||||||
# For 'Pre-generated Character'
|
# For 'Pre-generated Character'
|
||||||
game_engine.player.stats = pre_allocated_stats
|
game_engine.player.stats = pre_allocated_stats
|
||||||
game_engine.player.getModifier()
|
game_engine.player.getModifier()
|
||||||
game_engine.player.set_stats_job() # Still need to set HP based on job and Con modifier
|
game_engine.player.set_stats_class() # Still need to set HP based on class and Con modifier
|
||||||
game_engine.player.current_stats() # Display stats after setting them
|
game_engine.player.current_stats() # Display stats after setting them
|
||||||
print(' ')
|
print(' ')
|
||||||
game_engine.player.inventory.current_equipment()
|
game_engine.player.inventory.current_equipment()
|
||||||
print(' ')
|
print(' ')
|
||||||
game_engine.player.inventory.current_inventory()
|
game_engine.player.inventory.current_inventory()
|
||||||
print(' ')
|
print(' ')
|
||||||
|
if game_engine.player.class_.is_caster:
|
||||||
|
game_engine.player.print_spellbook()
|
||||||
|
print(' ')
|
||||||
|
|
||||||
wait()
|
wait()
|
||||||
print("\nYour adventure begins...")
|
print("\nYour adventure begins...")
|
||||||
@@ -340,6 +378,29 @@ def main_game_loop():
|
|||||||
elif command == "go":
|
elif command == "go":
|
||||||
direction = obj1
|
direction = obj1
|
||||||
game_engine.move_to_scene(direction)
|
game_engine.move_to_scene(direction)
|
||||||
|
elif command == "spells":
|
||||||
|
game_engine.player.print_spellbook()
|
||||||
|
elif command == "cast":
|
||||||
|
if obj1:
|
||||||
|
game_engine.player.cast_spell(obj1)
|
||||||
|
else:
|
||||||
|
print("What would you like to cast?")
|
||||||
|
elif command == "prepare":
|
||||||
|
if obj1:
|
||||||
|
game_engine.player.prepare_spell(obj1)
|
||||||
|
else:
|
||||||
|
print("What would you like to prepare?")
|
||||||
|
elif command == "unprepare":
|
||||||
|
if obj1:
|
||||||
|
game_engine.player.unprepare_spell(obj1)
|
||||||
|
else:
|
||||||
|
print("What would you like to unprepare?")
|
||||||
|
elif command == "rest":
|
||||||
|
game_engine.player.long_rest()
|
||||||
|
elif command == "weight":
|
||||||
|
game_engine.player.carry_report()
|
||||||
|
elif command == "currency":
|
||||||
|
game_engine.player.currency_report()
|
||||||
elif command == "quit":
|
elif command == "quit":
|
||||||
print("Thanks for playing!")
|
print("Thanks for playing!")
|
||||||
break
|
break
|
||||||
|
|||||||
339
player.py
339
player.py
@@ -1,16 +1,33 @@
|
|||||||
|
import re
|
||||||
from random import randint
|
from random import randint
|
||||||
from enum_list import DamageType, DamageMod, ItemType, Slots
|
from enum_list import DamageType, DamageMod, ItemType, Slots, SaveType
|
||||||
|
from spell_list import SPELL_LISTS, get_spell_by_name
|
||||||
|
|
||||||
|
|
||||||
|
def roll_dice(dice_str):
|
||||||
|
"""Rolls a dice string like '1d6', '3d4+3', '2d10-1' and returns the total."""
|
||||||
|
if not dice_str:
|
||||||
|
return 0
|
||||||
|
match = re.match(r"(\d+)d(\d+)([+-]\d+)?", dice_str.strip())
|
||||||
|
if not match:
|
||||||
|
return 0
|
||||||
|
num, size, modifier = match.groups()
|
||||||
|
total = sum(randint(1, int(size)) for _ in range(int(num)))
|
||||||
|
if modifier:
|
||||||
|
total += int(modifier)
|
||||||
|
return total
|
||||||
|
|
||||||
# Define Item classes here, as they are fundamental building blocks
|
# Define Item classes here, as they are fundamental building blocks
|
||||||
class Item:
|
class Item:
|
||||||
"""The base class for all items"""
|
"""The base class for all items"""
|
||||||
def __init__(self, name, description, value, magical, itemType, attunement):
|
def __init__(self, name, description, value, magical, itemType, attunement, weight=0):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.description = description
|
self.description = description
|
||||||
self.value = value
|
self.value = value
|
||||||
self.magical = magical
|
self.magical = magical
|
||||||
self.itemType = itemType
|
self.itemType = itemType
|
||||||
self.attunement = attunement
|
self.attunement = attunement
|
||||||
|
self.weight = weight # in pounds, per SRD carrying capacity rules
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"{self.name}\n=====\n{self.description}\nValue: {self.value}\n"
|
return f"{self.name}\n=====\n{self.description}\nValue: {self.value}\n"
|
||||||
@@ -29,13 +46,14 @@ class Money(Item):
|
|||||||
value=self.amt,
|
value=self.amt,
|
||||||
magical=self.magical,
|
magical=self.magical,
|
||||||
itemType=ItemType.Money,
|
itemType=ItemType.Money,
|
||||||
attunement=False)
|
attunement=False,
|
||||||
|
weight=0.02) # SRD: 50 coins weigh 1 lb, regardless of denomination
|
||||||
|
|
||||||
|
|
||||||
class Weapon(Item):
|
class Weapon(Item):
|
||||||
"""The base class for all weapons"""
|
"""The base class for all weapons"""
|
||||||
def __init__(self, name, description, value, slot, damage1H, damage2H, versatiledmg, light, finesse,
|
def __init__(self, name, description, value, slot, damage1H, damage2H, versatiledmg, light, finesse,
|
||||||
dmgType, dmgMod, versatile, thrown, ItemType, magical, attunement):
|
dmgType, dmgMod, versatile, thrown, ItemType, magical, attunement, weight=0):
|
||||||
self.slot = slot
|
self.slot = slot
|
||||||
self.damage1H = damage1H
|
self.damage1H = damage1H
|
||||||
self.damage2H = damage2H
|
self.damage2H = damage2H
|
||||||
@@ -47,7 +65,7 @@ class Weapon(Item):
|
|||||||
self.versatile = versatile
|
self.versatile = versatile
|
||||||
self.thrown = thrown
|
self.thrown = thrown
|
||||||
self.itemType = ItemType
|
self.itemType = ItemType
|
||||||
super().__init__(name, description, value, magical, ItemType, attunement)
|
super().__init__(name, description, value, magical, ItemType, attunement, weight)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.damage2H is None and self.damage1H is not None:
|
if self.damage2H is None and self.damage1H is not None:
|
||||||
@@ -84,14 +102,15 @@ class Weapon(Item):
|
|||||||
|
|
||||||
class Armor(Item):
|
class Armor(Item):
|
||||||
"""The base class for all armor"""
|
"""The base class for all armor"""
|
||||||
def __init__(self, name, description, value, slot, grade, ac, disadvantage, dmgRes, ItemType, magical, attunement):
|
def __init__(self, name, description, value, slot, grade, ac, disadvantage, dmgRes, ItemType, magical,
|
||||||
|
attunement, weight=0):
|
||||||
self.slot = slot
|
self.slot = slot
|
||||||
self.grade = grade
|
self.grade = grade
|
||||||
self.ac = ac
|
self.ac = ac
|
||||||
self.stealthDis = disadvantage
|
self.stealthDis = disadvantage
|
||||||
self.dmgRes = dmgRes
|
self.dmgRes = dmgRes
|
||||||
self.itemType = ItemType
|
self.itemType = ItemType
|
||||||
super().__init__(name, description, value, magical, ItemType, attunement)
|
super().__init__(name, description, value, magical, ItemType, attunement, weight)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if self.stealthDis:
|
if self.stealthDis:
|
||||||
@@ -299,6 +318,65 @@ class Inventory:
|
|||||||
attune_count += 1
|
attune_count += 1
|
||||||
self.equipped_items[Slots.Attunement] = attune_count
|
self.equipped_items[Slots.Attunement] = attune_count
|
||||||
|
|
||||||
|
# ----- Weight / Encumbrance (SRD carrying capacity) -----
|
||||||
|
def total_weight(self):
|
||||||
|
"""Sums the weight of everything carried, including equipped gear."""
|
||||||
|
return sum(item.weight * data["Count"] for item, data in self.items.items())
|
||||||
|
|
||||||
|
def carrying_capacity(self, strength_score):
|
||||||
|
"""SRD: your carrying capacity is your Strength score multiplied by 15."""
|
||||||
|
return strength_score * 15
|
||||||
|
|
||||||
|
def encumbrance_status(self, strength_score):
|
||||||
|
"""Returns (status_str, weight, capacity) using the SRD variant encumbrance thresholds."""
|
||||||
|
weight = self.total_weight()
|
||||||
|
capacity = self.carrying_capacity(strength_score)
|
||||||
|
heavily_encumbered_at = strength_score * 10
|
||||||
|
encumbered_at = strength_score * 5
|
||||||
|
|
||||||
|
if weight > capacity:
|
||||||
|
status = "Over Capacity! You cannot carry this much."
|
||||||
|
elif weight > heavily_encumbered_at:
|
||||||
|
status = "Heavily Encumbered (speed -20 ft, disadvantage on Strength/Dexterity/Constitution checks, attacks, and saves)"
|
||||||
|
elif weight > encumbered_at:
|
||||||
|
status = "Encumbered (speed -10 ft)"
|
||||||
|
else:
|
||||||
|
status = "Unencumbered"
|
||||||
|
return status, weight, capacity
|
||||||
|
|
||||||
|
def print_carry_report(self, strength_score):
|
||||||
|
"""Prints a human-readable summary of current carrying weight/capacity."""
|
||||||
|
status, weight, capacity = self.encumbrance_status(strength_score)
|
||||||
|
print(f"Carrying {weight:.2f} lb / {capacity} lb capacity.")
|
||||||
|
print(f"Status: {status}")
|
||||||
|
|
||||||
|
# ----- Currency -----
|
||||||
|
def _coin_counts(self):
|
||||||
|
"""Returns a dict of coin name -> count currently held, e.g. {'Gold': 12}."""
|
||||||
|
counts = {}
|
||||||
|
for item, data in self.items.items():
|
||||||
|
if item.itemType == ItemType.Money:
|
||||||
|
counts[item.name] = data["Count"]
|
||||||
|
return counts
|
||||||
|
|
||||||
|
def total_currency_in_gp(self):
|
||||||
|
"""Converts all held coinage to a single gold-piece value (SRD: 1gp = 10sp = 100cp)."""
|
||||||
|
coins = self._coin_counts()
|
||||||
|
gp = coins.get("Gold", 0)
|
||||||
|
sp = coins.get("Silver", 0)
|
||||||
|
cp = coins.get("Copper", 0)
|
||||||
|
return gp + (sp / 10) + (cp / 100)
|
||||||
|
|
||||||
|
def print_currency(self):
|
||||||
|
"""Prints a breakdown of held coinage and its total gold-piece value."""
|
||||||
|
coins = self._coin_counts()
|
||||||
|
if not coins:
|
||||||
|
print("You have no coins.")
|
||||||
|
return
|
||||||
|
parts = [f"{count} {name}" for name, count in coins.items() if count > 0]
|
||||||
|
print(f"You are carrying: {', '.join(parts) if parts else 'no coins'}.")
|
||||||
|
print(f"Total value: {self.total_currency_in_gp():.2f} gp")
|
||||||
|
|
||||||
|
|
||||||
# --- Item Instances (Moved here for clarity, but still defined once) ---
|
# --- Item Instances (Moved here for clarity, but still defined once) ---
|
||||||
# Different types of coins
|
# Different types of coins
|
||||||
@@ -323,7 +401,8 @@ rock = Weapon(
|
|||||||
thrown=True,
|
thrown=True,
|
||||||
magical=False,
|
magical=False,
|
||||||
ItemType=ItemType.Weapon,
|
ItemType=ItemType.Weapon,
|
||||||
attunement=False
|
attunement=False,
|
||||||
|
weight=2
|
||||||
)
|
)
|
||||||
|
|
||||||
dagger = Weapon(
|
dagger = Weapon(
|
||||||
@@ -342,7 +421,8 @@ dagger = Weapon(
|
|||||||
thrown=True,
|
thrown=True,
|
||||||
magical=False,
|
magical=False,
|
||||||
ItemType=ItemType.Weapon,
|
ItemType=ItemType.Weapon,
|
||||||
attunement=False
|
attunement=False,
|
||||||
|
weight=1
|
||||||
)
|
)
|
||||||
|
|
||||||
polearm = Weapon(
|
polearm = Weapon(
|
||||||
@@ -361,7 +441,8 @@ polearm = Weapon(
|
|||||||
thrown=False, # Polearms are not typically thrown
|
thrown=False, # Polearms are not typically thrown
|
||||||
magical=False,
|
magical=False,
|
||||||
ItemType=ItemType.Weapon,
|
ItemType=ItemType.Weapon,
|
||||||
attunement=False
|
attunement=False,
|
||||||
|
weight=6
|
||||||
)
|
)
|
||||||
|
|
||||||
tornRags = Armor(
|
tornRags = Armor(
|
||||||
@@ -375,7 +456,8 @@ tornRags = Armor(
|
|||||||
dmgRes="None",
|
dmgRes="None",
|
||||||
magical=False,
|
magical=False,
|
||||||
ItemType=ItemType.Armor,
|
ItemType=ItemType.Armor,
|
||||||
attunement=False
|
attunement=False,
|
||||||
|
weight=2
|
||||||
)
|
)
|
||||||
|
|
||||||
paper = Item(
|
paper = Item(
|
||||||
@@ -384,19 +466,22 @@ paper = Item(
|
|||||||
value=None,
|
value=None,
|
||||||
magical=False,
|
magical=False,
|
||||||
itemType=ItemType.Item,
|
itemType=ItemType.Item,
|
||||||
attunement=False
|
attunement=False,
|
||||||
|
weight=0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Player:
|
class Player:
|
||||||
"""Character Creation"""
|
"""Character Creation"""
|
||||||
def __init__(self, name, gender, age, race, job):
|
def __init__(self, name, gender, age, race, class_):
|
||||||
# Identity
|
# Identity
|
||||||
self.name = name
|
self.name = name
|
||||||
self.gender = gender
|
self.gender = gender
|
||||||
self.age = age
|
self.age = age
|
||||||
self.race = race
|
self.race = race
|
||||||
self.job = job
|
# `class_` (not `class`) because `class` is a reserved Python keyword.
|
||||||
|
# See the NOTE ON NAMING comment at the top of class_list.py for the full convention.
|
||||||
|
self.class_ = class_
|
||||||
|
|
||||||
# Defence
|
# Defence
|
||||||
self.ac = 0 # This will be calculated based on equipped armor
|
self.ac = 0 # This will be calculated based on equipped armor
|
||||||
@@ -423,6 +508,19 @@ class Player:
|
|||||||
# Equipment and Inventory
|
# Equipment and Inventory
|
||||||
self.inventory = Inventory() # Each player gets their own inventory instance
|
self.inventory = Inventory() # Each player gets their own inventory instance
|
||||||
|
|
||||||
|
# Proficiency bonus (SRD: +2 at levels 1-4)
|
||||||
|
self.proficiency_bonus = 2
|
||||||
|
|
||||||
|
# --- Spellcasting (SRD) ---
|
||||||
|
# None of these are populated until initialize_spellcasting() runs, since they
|
||||||
|
# depend on ability modifiers, which are calculated later during character creation.
|
||||||
|
self.spellcasting_ability = class_.spellcasting_ability # e.g. "Intelligence", or None
|
||||||
|
self.cantrips_known = [] # list of Spell objects, always available, no slot cost
|
||||||
|
self.spells_known = [] # Wizard: spellbook contents. Cleric: full accessible list.
|
||||||
|
self.spells_prepared = [] # subset of spells_known currently prepared/castable
|
||||||
|
self.spell_slots_max = dict(class_.spell_slots) # {level: max_slots}
|
||||||
|
self.spell_slots_current = dict(class_.spell_slots) # {level: slots_remaining}
|
||||||
|
|
||||||
# Ability Scores
|
# Ability Scores
|
||||||
self.stats = {"Strength": 0,
|
self.stats = {"Strength": 0,
|
||||||
"Dexterity": 0,
|
"Dexterity": 0,
|
||||||
@@ -540,22 +638,198 @@ class Player:
|
|||||||
print("As a Dwarf, your constitution increases by 2.")
|
print("As a Dwarf, your constitution increases by 2.")
|
||||||
self.stats["Constitution"] += 2
|
self.stats["Constitution"] += 2
|
||||||
|
|
||||||
def set_stats_job(self):
|
def set_stats_class(self):
|
||||||
"""Applies job-specific stats like hit points."""
|
"""Applies class-specific stats like hit points."""
|
||||||
# Using self.job.name to access the job name from the Job object
|
# Using self.class_.name to access the class name from the CharacterClass object
|
||||||
if self.job.name.lower() == "barbarian":
|
if self.class_.name.lower() == "barbarian":
|
||||||
self.hitDie = randint(self.job.hitdie[0], self.job.hitdie[1]) # Use job's hitdie range
|
self.hitDie = randint(self.class_.hitdie[0], self.class_.hitdie[1]) # Use class's hitdie range
|
||||||
self.maxHP = (self.job.hitdie[1] + self.mods["Constitution"]) # Use max hitdie value for initial HP
|
self.maxHP = (self.class_.hitdie[1] + self.mods["Constitution"]) # Use max hitdie value for initial HP
|
||||||
self.currentHP = self.maxHP
|
self.currentHP = self.maxHP
|
||||||
elif self.job.name.lower() == "cleric":
|
elif self.class_.name.lower() == "cleric":
|
||||||
self.hitDie = randint(self.job.hitdie[0], self.job.hitdie[1])
|
self.hitDie = randint(self.class_.hitdie[0], self.class_.hitdie[1])
|
||||||
self.maxHP = (self.job.hitdie[1] + self.mods["Constitution"])
|
self.maxHP = (self.class_.hitdie[1] + self.mods["Constitution"])
|
||||||
self.currentHP = self.maxHP
|
self.currentHP = self.maxHP
|
||||||
elif self.job.name.lower() == "wizard":
|
elif self.class_.name.lower() == "wizard":
|
||||||
self.hitDie = randint(self.job.hitdie[0], self.job.hitdie[1])
|
self.hitDie = randint(self.class_.hitdie[0], self.class_.hitdie[1])
|
||||||
self.maxHP = (self.job.hitdie[1] + self.mods["Constitution"])
|
self.maxHP = (self.class_.hitdie[1] + self.mods["Constitution"])
|
||||||
self.currentHP = self.maxHP
|
self.currentHP = self.maxHP
|
||||||
|
|
||||||
|
self.initialize_spellcasting()
|
||||||
|
|
||||||
|
# ----- Magic system (SRD) -----
|
||||||
|
def initialize_spellcasting(self):
|
||||||
|
"""Grants starting cantrips and known/preparable spells for casters. Non-casters no-op."""
|
||||||
|
if not self.class_.is_caster:
|
||||||
|
return
|
||||||
|
|
||||||
|
class_name = self.class_.name
|
||||||
|
class_spells = SPELL_LISTS.get(class_name, {})
|
||||||
|
|
||||||
|
# Cantrips: granted automatically up to the class's cantrips_known count.
|
||||||
|
available_cantrips = class_spells.get(0, [])
|
||||||
|
self.cantrips_known = list(available_cantrips[:self.class_.cantrips_known])
|
||||||
|
|
||||||
|
# Known/preparable leveled spells depend on the class's spellbook style.
|
||||||
|
available_level1 = class_spells.get(1, [])
|
||||||
|
if self.class_.spellbook_style == "spellbook":
|
||||||
|
# Wizard: the whole curated list forms your starting spellbook.
|
||||||
|
self.spells_known = list(available_level1)
|
||||||
|
elif self.class_.spellbook_style == "all_known":
|
||||||
|
# Cleric: you have access to your entire class list, and choose what to prepare.
|
||||||
|
self.spells_known = list(available_level1)
|
||||||
|
|
||||||
|
# Auto-prepare up to your max_prepared_spells() limit so the character is playable
|
||||||
|
# immediately after creation; the player can re-prepare later with 'prepare <spell>'.
|
||||||
|
max_prepared = self.max_prepared_spells()
|
||||||
|
self.spells_prepared = list(self.spells_known[:max_prepared])
|
||||||
|
|
||||||
|
def max_prepared_spells(self):
|
||||||
|
"""SRD: ability modifier + character level (minimum 1)."""
|
||||||
|
if not self.spellcasting_ability:
|
||||||
|
return 0
|
||||||
|
return max(1, self.mods[self.spellcasting_ability] + self.level)
|
||||||
|
|
||||||
|
def spell_save_dc(self):
|
||||||
|
"""SRD: 8 + proficiency bonus + spellcasting ability modifier."""
|
||||||
|
if not self.spellcasting_ability:
|
||||||
|
return None
|
||||||
|
return 8 + self.proficiency_bonus + self.mods[self.spellcasting_ability]
|
||||||
|
|
||||||
|
def spell_attack_bonus(self):
|
||||||
|
"""SRD: proficiency bonus + spellcasting ability modifier."""
|
||||||
|
if not self.spellcasting_ability:
|
||||||
|
return None
|
||||||
|
return self.proficiency_bonus + self.mods[self.spellcasting_ability]
|
||||||
|
|
||||||
|
def print_spellbook(self):
|
||||||
|
"""Prints known cantrips, known/preparable spells, prepared spells, and slots."""
|
||||||
|
if not self.class_.is_caster:
|
||||||
|
print(f"{self.class_.name}s don't cast spells.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Spellcasting Ability: {self.spellcasting_ability}")
|
||||||
|
print(f"Spell Save DC: {self.spell_save_dc()}")
|
||||||
|
print(f"Spell Attack Bonus: {self.spell_attack_bonus():+}")
|
||||||
|
print(f"\nCantrips Known ({len(self.cantrips_known)}):")
|
||||||
|
for spell in self.cantrips_known:
|
||||||
|
print(f" - {spell.name}")
|
||||||
|
|
||||||
|
style_label = "Spellbook" if self.class_.spellbook_style == "spellbook" else "Class Spell List"
|
||||||
|
print(f"\n{style_label} ({len(self.spells_known)}):")
|
||||||
|
for spell in self.spells_known:
|
||||||
|
prepared_tag = " [Prepared]" if spell in self.spells_prepared else ""
|
||||||
|
print(f" - {spell.name} (Level {spell.level}){prepared_tag}")
|
||||||
|
|
||||||
|
print(f"\nPrepared Spells: {len(self.spells_prepared)}/{self.max_prepared_spells()}")
|
||||||
|
print("Spell Slots:")
|
||||||
|
for level in sorted(self.spell_slots_max):
|
||||||
|
print(f" Level {level}: {self.spell_slots_current.get(level, 0)}/{self.spell_slots_max[level]}")
|
||||||
|
|
||||||
|
def prepare_spell(self, spell_name):
|
||||||
|
"""Moves a spell from your known list into your prepared list, if you have room."""
|
||||||
|
spell = next((s for s in self.spells_known if s.name.lower() == spell_name.lower()), None)
|
||||||
|
if not spell:
|
||||||
|
print(f"'{spell_name}' isn't in your {'spellbook' if self.class_.spellbook_style == 'spellbook' else 'class spell list'}.")
|
||||||
|
return
|
||||||
|
if spell in self.spells_prepared:
|
||||||
|
print(f"{spell.name} is already prepared.")
|
||||||
|
return
|
||||||
|
if len(self.spells_prepared) >= self.max_prepared_spells():
|
||||||
|
print(f"You can't prepare any more spells ({self.max_prepared_spells()} max). "
|
||||||
|
f"Unprepare something first.")
|
||||||
|
return
|
||||||
|
self.spells_prepared.append(spell)
|
||||||
|
print(f"Prepared {spell.name}.")
|
||||||
|
|
||||||
|
def unprepare_spell(self, spell_name):
|
||||||
|
"""Removes a spell from your prepared list."""
|
||||||
|
spell = next((s for s in self.spells_prepared if s.name.lower() == spell_name.lower()), None)
|
||||||
|
if not spell:
|
||||||
|
print(f"'{spell_name}' isn't currently prepared.")
|
||||||
|
return
|
||||||
|
self.spells_prepared.remove(spell)
|
||||||
|
print(f"Unprepared {spell.name}.")
|
||||||
|
|
||||||
|
def cast_spell(self, spell_name, slot_level=None):
|
||||||
|
"""
|
||||||
|
Casts a cantrip (free) or a prepared leveled spell (consumes a slot).
|
||||||
|
Returns a result dict describing what happened, or None if the cast failed.
|
||||||
|
"""
|
||||||
|
# Cantrips are always available and never cost a slot.
|
||||||
|
cantrip = next((s for s in self.cantrips_known if s.name.lower() == spell_name.lower()), None)
|
||||||
|
if cantrip:
|
||||||
|
return self._resolve_spell_effect(cantrip, slot_level=0)
|
||||||
|
|
||||||
|
spell = next((s for s in self.spells_prepared if s.name.lower() == spell_name.lower()), None)
|
||||||
|
if not spell:
|
||||||
|
print(f"You don't have '{spell_name}' prepared, and it isn't a cantrip you know.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Default to casting at its base level if no upcast level is specified.
|
||||||
|
cast_level = slot_level or spell.level
|
||||||
|
if cast_level < spell.level:
|
||||||
|
print(f"{spell.name} requires at least a level {spell.level} slot.")
|
||||||
|
return None
|
||||||
|
if self.spell_slots_current.get(cast_level, 0) <= 0:
|
||||||
|
print(f"You have no level {cast_level} spell slots remaining.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.spell_slots_current[cast_level] -= 1
|
||||||
|
return self._resolve_spell_effect(spell, slot_level=cast_level)
|
||||||
|
|
||||||
|
def _resolve_spell_effect(self, spell, slot_level):
|
||||||
|
"""Rolls damage/healing for a spell and prints/returns the outcome."""
|
||||||
|
result = {"spell": spell.name, "damage": 0, "heal": 0, "dmg_type": spell.dmg_type,
|
||||||
|
"save": spell.save, "save_dc": self.spell_save_dc()}
|
||||||
|
|
||||||
|
# Determine upcast bonus dice, if any.
|
||||||
|
extra_levels = max(0, slot_level - spell.level) if spell.level > 0 else 0
|
||||||
|
|
||||||
|
if spell.damage:
|
||||||
|
damage = roll_dice(spell.damage)
|
||||||
|
if spell.scaling_damage:
|
||||||
|
if "per_slot_level" in spell.scaling_damage and extra_levels:
|
||||||
|
for _ in range(extra_levels):
|
||||||
|
damage += roll_dice(spell.scaling_damage["per_slot_level"])
|
||||||
|
elif self.level in spell.scaling_damage:
|
||||||
|
# Cantrip scaling by character level (e.g. Fire Bolt at level 5/11/17)
|
||||||
|
damage = roll_dice(spell.scaling_damage.get(self.level, spell.damage))
|
||||||
|
else:
|
||||||
|
for threshold in sorted([t for t in spell.scaling_damage if isinstance(t, int)], reverse=True):
|
||||||
|
if self.level >= threshold:
|
||||||
|
damage = roll_dice(spell.scaling_damage[threshold])
|
||||||
|
break
|
||||||
|
result["damage"] = damage
|
||||||
|
|
||||||
|
if spell.heal:
|
||||||
|
heal = roll_dice(spell.heal)
|
||||||
|
if spell.scaling_damage and "per_slot_level" in spell.scaling_damage and extra_levels:
|
||||||
|
for _ in range(extra_levels):
|
||||||
|
heal += roll_dice(spell.scaling_damage["per_slot_level"])
|
||||||
|
result["heal"] = heal
|
||||||
|
|
||||||
|
# Print a readable summary.
|
||||||
|
print(f"\nYou cast {spell.name}!")
|
||||||
|
if spell.save != SaveType.NONE:
|
||||||
|
print(f"Target must make a DC {result['save_dc']} {spell.save.name} saving throw.")
|
||||||
|
elif spell.damage:
|
||||||
|
atk_bonus = self.spell_attack_bonus()
|
||||||
|
print(f"Spell attack roll: 1d20{atk_bonus:+} vs target AC.")
|
||||||
|
if result["damage"]:
|
||||||
|
print(f"Damage: {result['damage']} {spell.dmg_type.name if spell.dmg_type else ''}".rstrip())
|
||||||
|
if result["heal"]:
|
||||||
|
print(f"Healing: {result['heal']} HP")
|
||||||
|
if not spell.damage and not spell.heal:
|
||||||
|
print(spell.description)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def long_rest(self):
|
||||||
|
"""Restores HP to max and refills all spell slots."""
|
||||||
|
self.currentHP = self.maxHP
|
||||||
|
self.spell_slots_current = dict(self.spell_slots_max)
|
||||||
|
print("You take a long rest. HP and spell slots are fully restored.")
|
||||||
|
|
||||||
def current_stats(self):
|
def current_stats(self):
|
||||||
"""Prints a display of the user's current statistics."""
|
"""Prints a display of the user's current statistics."""
|
||||||
print(f'\nYour current stats are:')
|
print(f'\nYour current stats are:')
|
||||||
@@ -584,18 +858,29 @@ class Player:
|
|||||||
self.allocation() # 1. Sets base self.stats from rolls.
|
self.allocation() # 1. Sets base self.stats from rolls.
|
||||||
self.set_stats_race() # 2. Applies racial bonuses to self.stats.
|
self.set_stats_race() # 2. Applies racial bonuses to self.stats.
|
||||||
self.getModifier() # 3. Calculates ALL self.mods based on the FINAL self.stats (base + racial).
|
self.getModifier() # 3. Calculates ALL self.mods based on the FINAL self.stats (base + racial).
|
||||||
self.set_stats_job() # 4. Calculates maxHP using the now-accurate self.mods["Constitution"].
|
self.set_stats_class() # 4. Calculates maxHP using the now-accurate self.mods["Constitution"].
|
||||||
self.current_stats() # 5. Displays the final stats.
|
self.current_stats() # 5. Displays the final stats.
|
||||||
print(' ')
|
print(' ')
|
||||||
self.inventory.current_equipment() # Corrected to call instance method
|
self.inventory.current_equipment() # Corrected to call instance method
|
||||||
print(' ')
|
print(' ')
|
||||||
self.inventory.current_inventory()
|
self.inventory.current_inventory()
|
||||||
print(' ')
|
print(' ')
|
||||||
|
if self.class_.is_caster:
|
||||||
|
self.print_spellbook()
|
||||||
|
print(' ')
|
||||||
|
|
||||||
def health_check(self):
|
def health_check(self):
|
||||||
"""Print out current/max HP"""
|
"""Print out current/max HP"""
|
||||||
print(f'You have {self.currentHP}/{self.maxHP} HP.')
|
print(f'You have {self.currentHP}/{self.maxHP} HP.')
|
||||||
|
|
||||||
|
def carry_report(self):
|
||||||
|
"""Print current weight carried vs. carrying capacity, and encumbrance status."""
|
||||||
|
self.inventory.print_carry_report(self.stats["Strength"])
|
||||||
|
|
||||||
|
def currency_report(self):
|
||||||
|
"""Print current coinage and its total gold-piece value."""
|
||||||
|
self.inventory.print_currency()
|
||||||
|
|
||||||
def take_damage(self, DMGtype, size):
|
def take_damage(self, DMGtype, size):
|
||||||
"""Define the damage type and dice size"""
|
"""Define the damage type and dice size"""
|
||||||
damage = randint(1, size)
|
damage = randint(1, size)
|
||||||
|
|||||||
10
race_list.py
10
race_list.py
@@ -39,7 +39,7 @@ class Elf(Race):
|
|||||||
speed = {
|
speed = {
|
||||||
'Walking': 30
|
'Walking': 30
|
||||||
}
|
}
|
||||||
darkvision = [30, 60]
|
darkvision = 60
|
||||||
language = ["Common", "Elvish"]
|
language = ["Common", "Elvish"]
|
||||||
advantage = []
|
advantage = []
|
||||||
resistance = []
|
resistance = []
|
||||||
@@ -155,6 +155,8 @@ class Human(Race):
|
|||||||
language, advantage, resistance, proficiency, traits)
|
language, advantage, resistance, proficiency, traits)
|
||||||
|
|
||||||
|
|
||||||
Elf = Elf()
|
RACES = {
|
||||||
Dwarf = Dwarf()
|
"Elf": Elf(),
|
||||||
Human = Human()
|
"Dwarf": Dwarf(),
|
||||||
|
"Human": Human(),
|
||||||
|
}
|
||||||
|
|||||||
90
scene.py
90
scene.py
@@ -5,6 +5,36 @@ sys.path.append('.')
|
|||||||
from player import Item, Weapon, Armor, rock, dagger, polearm, tornRags, paper, GP1, SP1, CP1
|
from player import Item, Weapon, Armor, rock, dagger, polearm, tornRags, paper, GP1, SP1, CP1
|
||||||
from enum_list import *
|
from enum_list import *
|
||||||
|
|
||||||
|
class Container:
|
||||||
|
def __init__(self, name, description, lock_dc=None, trap_dc=None):
|
||||||
|
self.name = name
|
||||||
|
self.description = description
|
||||||
|
self.lock_dc = lock_dc
|
||||||
|
self.trap_dc = trap_dc
|
||||||
|
self.trap_disarmed = False
|
||||||
|
self.contents = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_locked(self):
|
||||||
|
return self.lock_dc is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_trapped(self):
|
||||||
|
return self.trap_dc is not None
|
||||||
|
|
||||||
|
def add_item(self, item_obj, count=1):
|
||||||
|
self.contents[item_obj] = self.contents.get(item_obj, 0) + count
|
||||||
|
|
||||||
|
def remove_item(self, item_obj, count=1):
|
||||||
|
if item_obj in self.contents:
|
||||||
|
self.contents[item_obj] -= count
|
||||||
|
if self.contents[item_obj] <= 0:
|
||||||
|
del self.contents[item_obj]
|
||||||
|
|
||||||
|
def has_items(self):
|
||||||
|
return bool(self.contents)
|
||||||
|
|
||||||
|
|
||||||
class Scene:
|
class Scene:
|
||||||
"""
|
"""
|
||||||
Represents a single location or area in the game.
|
Represents a single location or area in the game.
|
||||||
@@ -45,15 +75,12 @@ class Scene:
|
|||||||
return self.exits.get(direction.lower())
|
return self.exits.get(direction.lower())
|
||||||
|
|
||||||
def add_lootable_item(self, container_name, item_obj, count=1):
|
def add_lootable_item(self, container_name, item_obj, count=1):
|
||||||
"""
|
|
||||||
Adds an item to a lootable container within the scene.
|
|
||||||
:param container_name: The name of the container (e.g., "chest", "goblin corpse").
|
|
||||||
:param item_obj: The Item object to add.
|
|
||||||
:param count: The quantity of the item.
|
|
||||||
"""
|
|
||||||
if container_name not in self.lootable_items:
|
if container_name not in self.lootable_items:
|
||||||
self.lootable_items[container_name] = {}
|
self.lootable_items[container_name] = Container(container_name, "")
|
||||||
self.lootable_items[container_name][item_obj] = self.lootable_items[container_name].get(item_obj, 0) + count
|
self.lootable_items[container_name].add_item(item_obj, count)
|
||||||
|
|
||||||
|
def add_container(self, container):
|
||||||
|
self.lootable_items[container.name] = container
|
||||||
|
|
||||||
def add_available_item(self, item_obj, count=1):
|
def add_available_item(self, item_obj, count=1):
|
||||||
"""
|
"""
|
||||||
@@ -75,18 +102,31 @@ class Scene:
|
|||||||
del self.available_items[item_obj]
|
del self.available_items[item_obj]
|
||||||
|
|
||||||
def remove_lootable_item(self, container_name, item_obj, count=1):
|
def remove_lootable_item(self, container_name, item_obj, count=1):
|
||||||
|
if container_name in self.lootable_items:
|
||||||
|
self.lootable_items[container_name].remove_item(item_obj, count)
|
||||||
|
if not self.lootable_items[container_name].has_items():
|
||||||
|
del self.lootable_items[container_name]
|
||||||
|
|
||||||
|
def add_corpse(self, enemy_name, items):
|
||||||
|
"""Creates a lootable corpse from a defeated enemy.
|
||||||
|
Multiple corpses of the same enemy are numbered: 'spider corpse', 'spider corpse 2', etc.
|
||||||
|
items: dict of {item_obj: count} or iterable of (item_obj, count) pairs.
|
||||||
"""
|
"""
|
||||||
Removes a specified count of an item from a lootable container in the scene.
|
base_name = f"{enemy_name.lower()} corpse"
|
||||||
:param container_name: The name of the container.
|
name = base_name
|
||||||
:param item_obj: The Item object to remove.
|
counter = 1
|
||||||
:param count: The quantity to remove.
|
while name in self.lootable_items:
|
||||||
"""
|
counter += 1
|
||||||
if container_name in self.lootable_items and item_obj in self.lootable_items[container_name]:
|
name = f"{base_name} {counter}"
|
||||||
self.lootable_items[container_name][item_obj] -= count
|
|
||||||
if self.lootable_items[container_name][item_obj] <= 0:
|
corpse = Container(name, f"The lifeless body of a {enemy_name}.")
|
||||||
del self.lootable_items[container_name][item_obj]
|
if isinstance(items, dict):
|
||||||
if not self.lootable_items[container_name]: # If container is empty, remove it
|
for item_obj, count in items.items():
|
||||||
del self.lootable_items[container_name]
|
corpse.add_item(item_obj, count)
|
||||||
|
else:
|
||||||
|
for item_obj, count in items:
|
||||||
|
corpse.add_item(item_obj, count)
|
||||||
|
self.lootable_items[name] = corpse
|
||||||
|
|
||||||
|
|
||||||
# --- Default Scenes ---
|
# --- Default Scenes ---
|
||||||
@@ -112,9 +152,9 @@ tutorial = Scene(
|
|||||||
)
|
)
|
||||||
tutorial.add_available_item(rock, 1)
|
tutorial.add_available_item(rock, 1)
|
||||||
tutorial.add_available_item(paper, 2)
|
tutorial.add_available_item(paper, 2)
|
||||||
tutorial.add_lootable_item("suspicious tree trunk", Item("Test Item",
|
tutorial_trunk = Container("suspicious tree trunk", "A hollow tree trunk with a small gap in the bark.")
|
||||||
"This is for testing purposes", None,
|
tutorial_trunk.add_item(Item("Test Item", "This is for testing purposes", None, False, ItemType.Item, False), 1)
|
||||||
False, ItemType.Item, False), 1)
|
tutorial.add_container(tutorial_trunk)
|
||||||
|
|
||||||
# Scene 1: Starting Clearing
|
# Scene 1: Starting Clearing
|
||||||
starting_clearing = Scene(
|
starting_clearing = Scene(
|
||||||
@@ -125,8 +165,10 @@ starting_clearing = Scene(
|
|||||||
"You notice a small, weathered wooden chest near a fallen log."
|
"You notice a small, weathered wooden chest near a fallen log."
|
||||||
)
|
)
|
||||||
starting_clearing.add_available_item(rock, 3) # Rocks lying on the ground
|
starting_clearing.add_available_item(rock, 3) # Rocks lying on the ground
|
||||||
starting_clearing.add_lootable_item("small wooden chest", GP1, 15)
|
starting_clearing_chest = Container("small wooden chest", "A small, weathered wooden chest with rusty iron bands.")
|
||||||
starting_clearing.add_lootable_item("small wooden chest", SP1, 20)
|
starting_clearing_chest.add_item(GP1, 15)
|
||||||
|
starting_clearing_chest.add_item(SP1, 20)
|
||||||
|
starting_clearing.add_container(starting_clearing_chest)
|
||||||
|
|
||||||
|
|
||||||
# Scene 2: Forest Path
|
# Scene 2: Forest Path
|
||||||
|
|||||||
221
spell_list.py
Normal file
221
spell_list.py
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
from enum_list import SpellSchool, SaveType, DamageType
|
||||||
|
|
||||||
|
|
||||||
|
class Spell:
|
||||||
|
"""Base class for all spells, built from the D&D 5e SRD."""
|
||||||
|
def __init__(self, name, level, school, casting_time, range_, components,
|
||||||
|
duration, description, classes, damage=None, dmg_type=None,
|
||||||
|
scaling_damage=None, save=SaveType.NONE, heal=None,
|
||||||
|
ritual=False, concentration=False):
|
||||||
|
self.name = name
|
||||||
|
self.level = level # 0 = cantrip
|
||||||
|
self.school = school
|
||||||
|
self.casting_time = casting_time
|
||||||
|
self.range = range_
|
||||||
|
self.components = components
|
||||||
|
self.duration = duration
|
||||||
|
self.description = description
|
||||||
|
self.classes = classes # list of class names that can cast this, e.g. ["Wizard"]
|
||||||
|
self.damage = damage # base damage dice string e.g. "1d10"
|
||||||
|
self.dmg_type = dmg_type
|
||||||
|
self.scaling_damage = scaling_damage # dice added per extra slot level/character level, if any
|
||||||
|
self.save = save
|
||||||
|
self.heal = heal # healing dice string, e.g. "1d8"
|
||||||
|
self.ritual = ritual
|
||||||
|
self.concentration = concentration
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
level_str = "Cantrip" if self.level == 0 else f"Level {self.level}"
|
||||||
|
tags = []
|
||||||
|
if self.ritual:
|
||||||
|
tags.append("Ritual")
|
||||||
|
if self.concentration:
|
||||||
|
tags.append("Concentration")
|
||||||
|
tag_str = f" ({', '.join(tags)})" if tags else ""
|
||||||
|
return (f"{self.name} - {level_str} {self.school.name}{tag_str}\n"
|
||||||
|
f"=====\n"
|
||||||
|
f"Casting Time: {self.casting_time}\n"
|
||||||
|
f"Range: {self.range}\n"
|
||||||
|
f"Components: {self.components}\n"
|
||||||
|
f"Duration: {self.duration}\n\n"
|
||||||
|
f"{self.description}")
|
||||||
|
|
||||||
|
|
||||||
|
# ----- Wizard Cantrips -----
|
||||||
|
fire_bolt = Spell(
|
||||||
|
name="Fire Bolt", level=0, school=SpellSchool.Evocation,
|
||||||
|
casting_time="1 action", range_="120 feet", components="V, S",
|
||||||
|
duration="Instantaneous",
|
||||||
|
description="You hurl a mote of fire at a creature or object within range. Make a ranged "
|
||||||
|
"spell attack against the target. On a hit, the target takes fire damage. "
|
||||||
|
"A flammable object hit by this spell ignites if it isn't being worn or carried.",
|
||||||
|
classes=["Wizard"], damage="1d10", dmg_type=DamageType.Fire,
|
||||||
|
scaling_damage={5: "2d10", 11: "3d10", 17: "4d10"}
|
||||||
|
)
|
||||||
|
|
||||||
|
ray_of_frost = Spell(
|
||||||
|
name="Ray of Frost", level=0, school=SpellSchool.Evocation,
|
||||||
|
casting_time="1 action", range_="60 feet", components="V, S",
|
||||||
|
duration="Instantaneous",
|
||||||
|
description="A frigid beam of blue-white light streaks toward a creature within range. "
|
||||||
|
"Make a ranged spell attack against the target. On a hit, it takes cold "
|
||||||
|
"damage and its speed is reduced by 10 feet until the start of your next turn.",
|
||||||
|
classes=["Wizard"], damage="1d8", dmg_type=DamageType.Cold,
|
||||||
|
scaling_damage={5: "2d8", 11: "3d8", 17: "4d8"}
|
||||||
|
)
|
||||||
|
|
||||||
|
mage_hand = Spell(
|
||||||
|
name="Mage Hand", level=0, school=SpellSchool.Conjuration,
|
||||||
|
casting_time="1 action", range_="30 feet", components="V, S",
|
||||||
|
duration="1 minute",
|
||||||
|
description="A spectral, floating hand appears at a point you choose within range. "
|
||||||
|
"The hand lasts for the duration and can manipulate objects, open unlocked "
|
||||||
|
"containers, and carry up to 10 pounds.",
|
||||||
|
classes=["Wizard"]
|
||||||
|
)
|
||||||
|
|
||||||
|
prestidigitation = Spell(
|
||||||
|
name="Prestidigitation", level=0, school=SpellSchool.Transmutation,
|
||||||
|
casting_time="1 action", range_="10 feet", components="V, S",
|
||||||
|
duration="Up to 1 hour",
|
||||||
|
description="This spell is a minor magical trick: create a harmless sensory effect, "
|
||||||
|
"light or snuff a small flame, clean or soil an object, chill/warm/flavor "
|
||||||
|
"a small amount of nonliving material, or make a small mark or symbol appear.",
|
||||||
|
classes=["Wizard"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# ----- Wizard Level 1 Spells -----
|
||||||
|
magic_missile = Spell(
|
||||||
|
name="Magic Missile", level=1, school=SpellSchool.Evocation,
|
||||||
|
casting_time="1 action", range_="120 feet", components="V, S",
|
||||||
|
duration="Instantaneous",
|
||||||
|
description="You create three glowing darts of magical force. Each dart hits a creature "
|
||||||
|
"of your choice that you can see within range, dealing force damage. The "
|
||||||
|
"darts all strike simultaneously and automatically hit - no attack roll needed.",
|
||||||
|
classes=["Wizard"], damage="3d4+3", dmg_type=DamageType.Force,
|
||||||
|
scaling_damage={"per_slot_level": "1d4+1"}
|
||||||
|
)
|
||||||
|
|
||||||
|
shield = Spell(
|
||||||
|
name="Shield", level=1, school=SpellSchool.Abjuration,
|
||||||
|
casting_time="1 reaction", range_="Self", components="V, S",
|
||||||
|
duration="1 round",
|
||||||
|
description="An invisible barrier of magical force appears and protects you. Until the "
|
||||||
|
"start of your next turn, you have a +5 bonus to AC and you take no damage "
|
||||||
|
"from Magic Missile.",
|
||||||
|
classes=["Wizard"]
|
||||||
|
)
|
||||||
|
|
||||||
|
burning_hands = Spell(
|
||||||
|
name="Burning Hands", level=1, school=SpellSchool.Evocation,
|
||||||
|
casting_time="1 action", range_="Self (15-foot cone)", components="V, S",
|
||||||
|
duration="Instantaneous",
|
||||||
|
description="A thin sheet of flames shoots forth from your outstretched fingertips. Each "
|
||||||
|
"creature in a 15-foot cone must make a Dexterity saving throw. A creature "
|
||||||
|
"takes fire damage on a failed save, or half as much on a successful one.",
|
||||||
|
classes=["Wizard"], damage="3d6", dmg_type=DamageType.Fire, save=SaveType.Dexterity,
|
||||||
|
scaling_damage={"per_slot_level": "1d6"}
|
||||||
|
)
|
||||||
|
|
||||||
|
detect_magic = Spell(
|
||||||
|
name="Detect Magic", level=1, school=SpellSchool.Divination,
|
||||||
|
casting_time="1 action", range_="Self", components="V, S",
|
||||||
|
duration="Concentration, up to 10 minutes",
|
||||||
|
description="For the duration, you sense the presence of magic within 30 feet of you. "
|
||||||
|
"If you sense magic in this way, you can use your action to see a faint "
|
||||||
|
"aura around any visible creature or object that bears magic.",
|
||||||
|
classes=["Wizard", "Cleric"], ritual=True, concentration=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# ----- Cleric Cantrips -----
|
||||||
|
sacred_flame = Spell(
|
||||||
|
name="Sacred Flame", level=0, school=SpellSchool.Evocation,
|
||||||
|
casting_time="1 action", range_="60 feet", components="V, S",
|
||||||
|
duration="Instantaneous",
|
||||||
|
description="Flame-like radiance descends on a creature that you can see within range. "
|
||||||
|
"The target must succeed on a Dexterity saving throw or take radiant damage. "
|
||||||
|
"The target gains no benefit from cover for this saving throw.",
|
||||||
|
classes=["Cleric"], damage="1d8", dmg_type=DamageType.Radiant, save=SaveType.Dexterity,
|
||||||
|
scaling_damage={5: "2d8", 11: "3d8", 17: "4d8"}
|
||||||
|
)
|
||||||
|
|
||||||
|
guidance = Spell(
|
||||||
|
name="Guidance", level=0, school=SpellSchool.Divination,
|
||||||
|
casting_time="1 action", range_="Touch", components="V, S",
|
||||||
|
duration="Concentration, up to 1 minute",
|
||||||
|
description="You touch one willing creature. Once before the spell ends, the target "
|
||||||
|
"can roll a d4 and add the number rolled to one ability check of its choice.",
|
||||||
|
classes=["Cleric"], concentration=True
|
||||||
|
)
|
||||||
|
|
||||||
|
spare_the_dying = Spell(
|
||||||
|
name="Spare the Dying", level=0, school=SpellSchool.Necromancy,
|
||||||
|
casting_time="1 action", range_="Touch", components="V, S",
|
||||||
|
duration="Instantaneous",
|
||||||
|
description="You touch a living creature that has 0 hit points. The creature becomes "
|
||||||
|
"stable. This spell has no effect on undead or constructs.",
|
||||||
|
classes=["Cleric"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# ----- Cleric Level 1 Spells -----
|
||||||
|
cure_wounds = Spell(
|
||||||
|
name="Cure Wounds", level=1, school=SpellSchool.Evocation,
|
||||||
|
casting_time="1 action", range_="Touch", components="V, S",
|
||||||
|
duration="Instantaneous",
|
||||||
|
description="A creature you touch regains a number of hit points.",
|
||||||
|
classes=["Cleric"], heal="1d8", scaling_damage={"per_slot_level": "1d8"}
|
||||||
|
)
|
||||||
|
|
||||||
|
bless = Spell(
|
||||||
|
name="Bless", level=1, school=SpellSchool.Enchantment,
|
||||||
|
casting_time="1 action", range_="30 feet", components="V, S, M",
|
||||||
|
duration="Concentration, up to 1 minute",
|
||||||
|
description="You bless up to three creatures of your choice within range. Whenever a "
|
||||||
|
"target makes an attack roll or a saving throw before the spell ends, the "
|
||||||
|
"target can add 1d4 to the attack roll or saving throw.",
|
||||||
|
classes=["Cleric"], concentration=True
|
||||||
|
)
|
||||||
|
|
||||||
|
guiding_bolt = Spell(
|
||||||
|
name="Guiding Bolt", level=1, school=SpellSchool.Evocation,
|
||||||
|
casting_time="1 action", range_="120 feet", components="V, S",
|
||||||
|
duration="1 round",
|
||||||
|
description="A flash of light streaks toward a creature of your choice within range. "
|
||||||
|
"Make a ranged spell attack against the target. On a hit, the target takes "
|
||||||
|
"radiant damage, and the next attack roll made against this target before "
|
||||||
|
"the end of your next turn has advantage.",
|
||||||
|
classes=["Cleric"], damage="4d6", dmg_type=DamageType.Radiant,
|
||||||
|
scaling_damage={"per_slot_level": "1d6"}
|
||||||
|
)
|
||||||
|
|
||||||
|
healing_word = Spell(
|
||||||
|
name="Healing Word", level=1, school=SpellSchool.Evocation,
|
||||||
|
casting_time="1 bonus action", range_="60 feet", components="V",
|
||||||
|
duration="Instantaneous",
|
||||||
|
description="A creature of your choice that you can see within range regains hit points.",
|
||||||
|
classes=["Cleric"], heal="1d4", scaling_damage={"per_slot_level": "1d4"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Master lookup: {class_name: {spell_level: [Spell, ...]}}
|
||||||
|
SPELL_LISTS = {
|
||||||
|
"Wizard": {
|
||||||
|
0: [fire_bolt, ray_of_frost, mage_hand, prestidigitation],
|
||||||
|
1: [magic_missile, shield, burning_hands, detect_magic],
|
||||||
|
},
|
||||||
|
"Cleric": {
|
||||||
|
0: [sacred_flame, guidance, spare_the_dying],
|
||||||
|
1: [cure_wounds, bless, guiding_bolt, healing_word, detect_magic],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_spell_by_name(name):
|
||||||
|
"""Looks up a Spell object across all class lists by (case-insensitive) name."""
|
||||||
|
name = name.lower().strip()
|
||||||
|
for class_spells in SPELL_LISTS.values():
|
||||||
|
for level_spells in class_spells.values():
|
||||||
|
for spell in level_spells:
|
||||||
|
if spell.name.lower() == name:
|
||||||
|
return spell
|
||||||
|
return None
|
||||||
Reference in New Issue
Block a user