Compare commits
2 Commits
a8bb6de008
...
551140b5bc
Author | SHA1 | Date | |
---|---|---|---|
|
551140b5bc | ||
|
68251ff4e9 |
|
@ -9,7 +9,7 @@ packages = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.10"
|
python = "^3.11"
|
||||||
python-dotenv = "^0.21.0"
|
python-dotenv = "^0.21.0"
|
||||||
typer = "^0.9.0"
|
typer = "^0.9.0"
|
||||||
rich = "^13.7.0"
|
rich = "^13.7.0"
|
||||||
|
|
|
@ -80,11 +80,11 @@ def serve(
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# delay loading the app until we have configured our environment
|
# delay loading the app until we have configured our environment
|
||||||
from ttfrog.db.bootstrap import bootstrap
|
from ttfrog.db.bootstrap import loader
|
||||||
from ttfrog.webserver import application
|
from ttfrog.webserver import application
|
||||||
|
|
||||||
print("Starting TableTop Frog server...")
|
print("Starting TableTop Frog server...")
|
||||||
bootstrap()
|
loader.load()
|
||||||
application.start(host=host, port=port, debug=debug)
|
application.start(host=host, port=port, debug=debug)
|
||||||
|
|
||||||
|
|
||||||
|
@ -93,13 +93,13 @@ def setup(context: typer.Context):
|
||||||
"""
|
"""
|
||||||
(Re)Initialize TableTop Frog. Idempotent; will preserve any existing configuration.
|
(Re)Initialize TableTop Frog. Idempotent; will preserve any existing configuration.
|
||||||
"""
|
"""
|
||||||
from ttfrog.db.bootstrap import bootstrap
|
from ttfrog.db.bootstrap import loader
|
||||||
|
|
||||||
if not os.path.exists(app_state["env"]):
|
if not os.path.exists(app_state["env"]):
|
||||||
app_state["env"].parent.mkdir(parents=True, exist_ok=True)
|
app_state["env"].parent.mkdir(parents=True, exist_ok=True)
|
||||||
app_state["env"].write_text(dedent(SETUP_HELP))
|
app_state["env"].write_text(dedent(SETUP_HELP))
|
||||||
print(f"Wrote defaults file {app_state['env']}.")
|
print(f"Wrote defaults file {app_state['env']}.")
|
||||||
bootstrap()
|
loader.load()
|
||||||
|
|
||||||
|
|
||||||
@db_app.command()
|
@db_app.command()
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
from ttfrog.db import schema
|
|
||||||
from ttfrog.db.manager import db
|
|
||||||
|
|
||||||
|
|
||||||
def bootstrap():
|
|
||||||
db.metadata.drop_all(bind=db.engine)
|
|
||||||
db.init()
|
|
||||||
with db.transaction():
|
|
||||||
# ancestries
|
|
||||||
human = schema.Ancestry("human")
|
|
||||||
tiefling = schema.Ancestry("tiefling")
|
|
||||||
tiefling.add_modifier(schema.Modifier("Ability Score Increase", target="intelligence", relative_value=1))
|
|
||||||
tiefling.add_modifier(schema.Modifier("Ability Score Increase", target="charisma", relative_value=2))
|
|
||||||
darkvision = schema.AncestryTrait(
|
|
||||||
"Darkvision",
|
|
||||||
description=(
|
|
||||||
"You can see in dim light within 60 feet of you as if it were bright light, and in darkness as if it "
|
|
||||||
"were dim light. You can’t discern color in darkness, only shades of gray."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
darkvision.add_modifier(schema.Modifier("Darkvision", target="vision_in_darkness", absolute_value=120))
|
|
||||||
tiefling.add_trait(darkvision)
|
|
||||||
|
|
||||||
# classes
|
|
||||||
fighter = schema.CharacterClass("fighter", hit_dice="1d10", hit_dice_stat="CON")
|
|
||||||
rogue = schema.CharacterClass("rogue", hit_dice="1d8", hit_dice_stat="DEX")
|
|
||||||
|
|
||||||
# characters
|
|
||||||
sabetha = schema.Character("Sabetha", ancestry=tiefling, _intelligence=14)
|
|
||||||
sabetha.add_class(fighter, level=2)
|
|
||||||
sabetha.add_class(rogue, level=3)
|
|
||||||
|
|
||||||
bob = schema.Character("Bob", ancestry=human)
|
|
||||||
|
|
||||||
# persist all the records we've created
|
|
||||||
db.add_or_update([sabetha, bob])
|
|
0
src/ttfrog/db/bootstrap/__init__.py
Normal file
0
src/ttfrog/db/bootstrap/__init__.py
Normal file
208
src/ttfrog/db/bootstrap/bootstrap.json
Normal file
208
src/ttfrog/db/bootstrap/bootstrap.json
Normal file
|
@ -0,0 +1,208 @@
|
||||||
|
{
|
||||||
|
"ancestry": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "human",
|
||||||
|
"creature_type": "humanoid",
|
||||||
|
"size": "medium",
|
||||||
|
"speed": 30,
|
||||||
|
"_fly_speed": null,
|
||||||
|
"_climb_speed": null,
|
||||||
|
"_swim_speed": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "tiefling",
|
||||||
|
"creature_type": "humanoid",
|
||||||
|
"size": "medium",
|
||||||
|
"speed": 30,
|
||||||
|
"_fly_speed": null,
|
||||||
|
"_climb_speed": null,
|
||||||
|
"_swim_speed": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ancestry_trait": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Darkvision",
|
||||||
|
"description": "You can see in dim light within 60 feet of you as if it were bright light, and in darkness as if it were dim light. You can\u2019t discern color in darkness, only shades of gray."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"character_class": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "fighter",
|
||||||
|
"hit_die_name": "1d10",
|
||||||
|
"hit_die_stat_name": "constitution",
|
||||||
|
"starting_skills": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "rogue",
|
||||||
|
"hit_die_name": "1d8",
|
||||||
|
"hit_die_stat_name": "dexterity",
|
||||||
|
"starting_skills": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"class_attribute": [],
|
||||||
|
"modifier": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Darkvision",
|
||||||
|
"target": "vision_in_darkness",
|
||||||
|
"stacks": false,
|
||||||
|
"absolute_value": 120,
|
||||||
|
"multiply_value": null,
|
||||||
|
"multiply_attribute": null,
|
||||||
|
"relative_value": null,
|
||||||
|
"relative_attribute": null,
|
||||||
|
"new_value": null,
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "Ability Score Increase",
|
||||||
|
"target": "intelligence",
|
||||||
|
"stacks": false,
|
||||||
|
"absolute_value": null,
|
||||||
|
"multiply_value": null,
|
||||||
|
"multiply_attribute": null,
|
||||||
|
"relative_value": 1,
|
||||||
|
"relative_attribute": null,
|
||||||
|
"new_value": null,
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"name": "Ability Score Increase",
|
||||||
|
"target": "charisma",
|
||||||
|
"stacks": false,
|
||||||
|
"absolute_value": null,
|
||||||
|
"multiply_value": null,
|
||||||
|
"multiply_attribute": null,
|
||||||
|
"relative_value": 2,
|
||||||
|
"relative_attribute": null,
|
||||||
|
"new_value": null,
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"skill": [],
|
||||||
|
"transaction_log": [],
|
||||||
|
"character": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Sabetha",
|
||||||
|
"hit_points": 10,
|
||||||
|
"temp_hit_points": 0,
|
||||||
|
"_max_hit_points": 10,
|
||||||
|
"_armor_class": 10,
|
||||||
|
"_strength": 10,
|
||||||
|
"_dexterity": 10,
|
||||||
|
"_constitution": 10,
|
||||||
|
"_intelligence": 14,
|
||||||
|
"_wisdom": 10,
|
||||||
|
"_charisma": 10,
|
||||||
|
"_vision": null,
|
||||||
|
"exhaustion": 0,
|
||||||
|
"ancestry_id": 2,
|
||||||
|
"slug": "nMnWu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "Bob",
|
||||||
|
"hit_points": 10,
|
||||||
|
"temp_hit_points": 0,
|
||||||
|
"_max_hit_points": 10,
|
||||||
|
"_armor_class": 10,
|
||||||
|
"_strength": 10,
|
||||||
|
"_dexterity": 10,
|
||||||
|
"_constitution": 10,
|
||||||
|
"_intelligence": 10,
|
||||||
|
"_wisdom": 10,
|
||||||
|
"_charisma": 10,
|
||||||
|
"_vision": null,
|
||||||
|
"exhaustion": 0,
|
||||||
|
"ancestry_id": 1,
|
||||||
|
"slug": "PjPdM"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"class_attribute_map": [],
|
||||||
|
"class_attribute_option": [],
|
||||||
|
"class_skill_map": [],
|
||||||
|
"modifier_map": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"modifier_id": 1,
|
||||||
|
"primary_table_name": "ancestry_trait",
|
||||||
|
"primary_table_id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"modifier_id": 2,
|
||||||
|
"primary_table_name": "ancestry",
|
||||||
|
"primary_table_id": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"modifier_id": 3,
|
||||||
|
"primary_table_name": "ancestry",
|
||||||
|
"primary_table_id": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trait_map": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"ancestry_id": 2,
|
||||||
|
"ancestry_trait_id": 1,
|
||||||
|
"level": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"character_class_attribute_map": [],
|
||||||
|
"character_skill_map": [],
|
||||||
|
"class_map": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"character_id": 1,
|
||||||
|
"character_class_id": 1,
|
||||||
|
"level": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"character_id": 1,
|
||||||
|
"character_class_id": 2,
|
||||||
|
"level": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hit_die": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"character_id": 1,
|
||||||
|
"character_class_id": 1,
|
||||||
|
"spent": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"character_id": 1,
|
||||||
|
"character_class_id": 1,
|
||||||
|
"spent": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"character_id": 1,
|
||||||
|
"character_class_id": 2,
|
||||||
|
"spent": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"character_id": 1,
|
||||||
|
"character_class_id": 2,
|
||||||
|
"spent": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"character_id": 1,
|
||||||
|
"character_class_id": 2,
|
||||||
|
"spent": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
29
src/ttfrog/db/bootstrap/loader.py
Normal file
29
src/ttfrog/db/bootstrap/loader.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ttfrog.db import schema
|
||||||
|
from ttfrog.db.manager import db
|
||||||
|
|
||||||
|
DATA_PATH = Path(__file__).parent
|
||||||
|
|
||||||
|
|
||||||
|
def load(data: str = ""):
|
||||||
|
db.metadata.drop_all(bind=db.engine)
|
||||||
|
db.init()
|
||||||
|
with db.transaction():
|
||||||
|
if not data:
|
||||||
|
data = (DATA_PATH / "bootstrap.json").read_text()
|
||||||
|
db.load(json.loads(data))
|
||||||
|
tiefling = db.Ancestry.filter_by(name="tiefling").one()
|
||||||
|
human = db.Ancestry.filter_by(name="human").one()
|
||||||
|
fighter = db.CharacterClass.filter_by(name="fighter").one()
|
||||||
|
rogue = db.CharacterClass.filter_by(name="rogue").one()
|
||||||
|
|
||||||
|
sabetha = schema.Character("Sabetha", ancestry=tiefling, _intelligence=14)
|
||||||
|
sabetha.add_class(fighter, level=2)
|
||||||
|
sabetha.add_class(rogue, level=3)
|
||||||
|
|
||||||
|
bob = schema.Character("Bob", ancestry=human)
|
||||||
|
|
||||||
|
# persist all the records we've created
|
||||||
|
db.add_or_update([sabetha, bob])
|
|
@ -4,16 +4,15 @@ import json
|
||||||
import os
|
import os
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
from sqlite3 import IntegrityError
|
||||||
|
|
||||||
import transaction
|
import transaction
|
||||||
from pyramid_sqlalchemy.meta import Session
|
from pyramid_sqlalchemy.meta import Session
|
||||||
from sqlalchemy import create_engine, event
|
from sqlalchemy import create_engine, event, insert
|
||||||
|
|
||||||
import ttfrog.db.schema
|
from ttfrog.db import schema
|
||||||
from ttfrog.path import database
|
from ttfrog.path import database
|
||||||
|
|
||||||
assert ttfrog.db.schema
|
|
||||||
|
|
||||||
|
|
||||||
class AlchemyEncoder(json.JSONEncoder):
|
class AlchemyEncoder(json.JSONEncoder):
|
||||||
def default(self, obj):
|
def default(self, obj):
|
||||||
|
@ -42,7 +41,7 @@ class SQLDatabaseManager:
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def metadata(self):
|
def metadata(self):
|
||||||
return ttfrog.db.schema.BaseObject.metadata
|
return schema.BaseObject.metadata
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def tables(self):
|
def tables(self):
|
||||||
|
@ -87,8 +86,18 @@ class SQLDatabaseManager:
|
||||||
results[table_name] = [dict(row._mapping) for row in self.query(table).all()]
|
results[table_name] = [dict(row._mapping) for row in self.query(table).all()]
|
||||||
return json.dumps(results, indent=2, cls=AlchemyEncoder)
|
return json.dumps(results, indent=2, cls=AlchemyEncoder)
|
||||||
|
|
||||||
|
def load(self, data: dict):
|
||||||
|
for table_name, rows in data.items():
|
||||||
|
table = self.tables.get(table_name, None)
|
||||||
|
if table is None:
|
||||||
|
raise IntegrityError(f"Table {table_name} not found in database.")
|
||||||
|
if not rows:
|
||||||
|
continue
|
||||||
|
query = insert(table), rows
|
||||||
|
self.session.execute(*query)
|
||||||
|
|
||||||
def __getattr__(self, name: str):
|
def __getattr__(self, name: str):
|
||||||
return self.query(getattr(ttfrog.db.schema, name))
|
return self.query(getattr(schema, name))
|
||||||
|
|
||||||
|
|
||||||
db = SQLDatabaseManager()
|
db = SQLDatabaseManager()
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from .character import *
|
from .character import *
|
||||||
from .classes import *
|
from .classes import *
|
||||||
|
from .constants import *
|
||||||
from .log import *
|
from .log import *
|
||||||
from .modifiers import *
|
from .modifiers import *
|
||||||
from .skill import *
|
from .skill import *
|
||||||
|
|
|
@ -6,6 +6,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from ttfrog.db.base import BaseObject, SlugMixin
|
from ttfrog.db.base import BaseObject, SlugMixin
|
||||||
from ttfrog.db.schema.classes import CharacterClass, ClassAttribute
|
from ttfrog.db.schema.classes import CharacterClass, ClassAttribute
|
||||||
|
from ttfrog.db.schema.constants import Conditions, DamageType, Defenses
|
||||||
from ttfrog.db.schema.modifiers import Modifier, ModifierMixin, Stat
|
from ttfrog.db.schema.modifiers import Modifier, ModifierMixin, Stat
|
||||||
from ttfrog.db.schema.skill import Skill
|
from ttfrog.db.schema.skill import Skill
|
||||||
|
|
||||||
|
@ -212,6 +213,7 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
|
||||||
)
|
)
|
||||||
|
|
||||||
_vision: Mapped[int] = mapped_column(default=None, nullable=True, info={"min": 0, "modifiable": True})
|
_vision: Mapped[int] = mapped_column(default=None, nullable=True, info={"min": 0, "modifiable": True})
|
||||||
|
exhaustion: Mapped[int] = mapped_column(nullable=False, default=0, info={"min": 0, "max": 5})
|
||||||
|
|
||||||
class_map = relationship("CharacterClassMap", cascade="all,delete,delete-orphan")
|
class_map = relationship("CharacterClassMap", cascade="all,delete,delete-orphan")
|
||||||
class_list = association_proxy("class_map", "id", creator=class_map_creator)
|
class_list = association_proxy("class_map", "id", creator=class_map_creator)
|
||||||
|
@ -272,6 +274,10 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
|
||||||
def traits(self):
|
def traits(self):
|
||||||
return self.ancestry.traits
|
return self.ancestry.traits
|
||||||
|
|
||||||
|
@property
|
||||||
|
def initiative(self):
|
||||||
|
return self._apply_modifiers("initiative", self.dexterity.bonus)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def speed(self):
|
def speed(self):
|
||||||
return self._apply_modifiers("speed", self.ancestry.speed)
|
return self._apply_modifiers("speed", self.ancestry.speed)
|
||||||
|
@ -294,7 +300,7 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def vision_in_darkness(self):
|
def vision_in_darkness(self):
|
||||||
return self.apply_modifiers("vision_in_darkness", self.vision if self.vision is not None else 0)
|
return self._apply_modifiers("vision_in_darkness", self.vision if self.vision is not None else 0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def level(self):
|
def level(self):
|
||||||
|
@ -314,23 +320,26 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
|
||||||
return None
|
return None
|
||||||
return mapping[0]
|
return mapping[0]
|
||||||
|
|
||||||
def immune(self, damage_type: str, magical: bool = False):
|
def immune(self, damage_type: DamageType):
|
||||||
return self.defense(damage_type, magical) == 'immune'
|
return self.defense(damage_type) == Defenses.immune
|
||||||
|
|
||||||
def resistant(self, damage_type: str, magical: bool = False):
|
def resistant(self, damage_type: DamageType):
|
||||||
return self.defense(damage_type, magical) == 'resistant'
|
return self.defense(damage_type) == Defenses.resistant.value
|
||||||
|
|
||||||
def vulnerable(self, damage_type: str, magical: bool = False):
|
def vulnerable(self, damage_type: DamageType):
|
||||||
return self.defense(damage_type, magical) == 'vulnerable'
|
return self.defense(damage_type) == Defenses.vulnerable
|
||||||
|
|
||||||
def absorbs(self, damage_type: str, magical: bool = False):
|
def absorbs(self, damage_type: DamageType):
|
||||||
return self.defense(damage_type, magical) == 'absorbs'
|
return self.defense(damage_type) == Defenses.absorbs
|
||||||
|
|
||||||
def defense(self, damage_type: str, magical: bool = False):
|
def conditions(self):
|
||||||
attr_name = damage_type
|
return [self._apply_modifiers(f"conditions.{name}") for name in Conditions]
|
||||||
if magical:
|
|
||||||
attr_name = f"magical_{attr_name}"
|
def condition(self, condition_name: str):
|
||||||
return self._apply_modifiers(f"defenses.{attr_name}", None)
|
return self._apply_modifiers(f"conditions.{condition_name}", False)
|
||||||
|
|
||||||
|
def defense(self, damage_type: DamageType):
|
||||||
|
return self._apply_modifiers(damage_type, None)
|
||||||
|
|
||||||
def check_modifier(self, skill: Skill, save: bool = False):
|
def check_modifier(self, skill: Skill, save: bool = False):
|
||||||
# if the skill is not assigned, but we have modifiers, apply them to zero.
|
# if the skill is not assigned, but we have modifiers, apply them to zero.
|
||||||
|
@ -382,18 +391,18 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
|
||||||
self.add_skill(skill, proficient=True, character_class=newclass)
|
self.add_skill(skill, proficient=True, character_class=newclass)
|
||||||
|
|
||||||
# add hit dice
|
# add hit dice
|
||||||
existing = len(self.hit_dice[newclass.name])
|
existing = len([die for die in self._hit_dice if die.character_class_id == newclass.id])
|
||||||
for lvl in range(level - existing):
|
for lvl in range(level - existing):
|
||||||
self._hit_dice.append(HitDie(character_id=self.id, character_class_id=newclass.id))
|
self._hit_dice.append(HitDie(character_id=self.id, character_class_id=newclass.id))
|
||||||
|
|
||||||
def remove_class(self, target):
|
def remove_class(self, target):
|
||||||
|
self.class_map = [m for m in self.class_map if m.character_class != target]
|
||||||
for mapping in self.character_class_attribute_map:
|
for mapping in self.character_class_attribute_map:
|
||||||
if mapping.character_class == target:
|
if mapping.character_class == target:
|
||||||
self.remove_class_attribute(mapping.class_attribute)
|
self.remove_class_attribute(mapping.class_attribute)
|
||||||
for skill in target.skills:
|
for skill in target.skills:
|
||||||
self.remove_skill(skill, proficient=True, expert=False, character_class=target)
|
self.remove_skill(skill, proficient=True, expert=False, character_class=target)
|
||||||
self._hit_dice = [die for die in self._hit_dice if die.character_class != target]
|
self._hit_dice = [die for die in self._hit_dice if die.character_class != target]
|
||||||
self.class_map = [m for m in self.class_map if m.character_class != target]
|
|
||||||
|
|
||||||
def remove_class_attribute(self, attribute):
|
def remove_class_attribute(self, attribute):
|
||||||
self.character_class_attribute_map = [
|
self.character_class_attribute_map = [
|
||||||
|
@ -422,8 +431,8 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def add_skill(self, skill, proficient=False, expert=False, character_class=None):
|
def add_skill(self, skill, proficient=False, expert=False, character_class=None):
|
||||||
if not self.id:
|
# if not self.id:
|
||||||
raise Exception("Cannot add a skill before the character has been persisted.")
|
# raise Exception("Cannot add a skill before the character has been persisted.")
|
||||||
skillmap = None
|
skillmap = None
|
||||||
exists = False
|
exists = False
|
||||||
if skill in self.skills:
|
if skill in self.skills:
|
||||||
|
@ -474,15 +483,15 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
|
||||||
def apply_healing(self, value: int):
|
def apply_healing(self, value: int):
|
||||||
self.hit_points = min(self.hit_points + value, self._max_hit_points)
|
self.hit_points = min(self.hit_points + value, self._max_hit_points)
|
||||||
|
|
||||||
def apply_damage(self, value: int, damage_type: str, magical=False):
|
def apply_damage(self, value: int, damage_type: DamageType):
|
||||||
total = value
|
total = value
|
||||||
if self.absorbs(damage_type, magical):
|
if self.absorbs(damage_type):
|
||||||
return self.apply_healing(total)
|
return self.apply_healing(total)
|
||||||
if self.immune(damage_type, magical):
|
if self.immune(damage_type):
|
||||||
return
|
return
|
||||||
if self.resistant(damage_type, magical):
|
if self.resistant(damage_type):
|
||||||
total = int(value / 2)
|
total = int(value / 2)
|
||||||
elif self.vulnerable(damage_type, magical):
|
elif self.vulnerable(damage_type):
|
||||||
total = value * 2
|
total = value * 2
|
||||||
|
|
||||||
if total <= self.temp_hit_points:
|
if total <= self.temp_hit_points:
|
||||||
|
|
52
src/ttfrog/db/schema/constants.py
Normal file
52
src/ttfrog/db/schema/constants.py
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
from enum import StrEnum, auto
|
||||||
|
|
||||||
|
|
||||||
|
class Conditions(StrEnum):
|
||||||
|
blinded = auto()
|
||||||
|
charmed = auto()
|
||||||
|
deafened = auto()
|
||||||
|
frightened = auto()
|
||||||
|
grappled = auto()
|
||||||
|
incapacitated = auto()
|
||||||
|
invisible = auto()
|
||||||
|
paralyzed = auto()
|
||||||
|
petrified = auto()
|
||||||
|
poisoned = auto()
|
||||||
|
prone = auto()
|
||||||
|
restrained = auto()
|
||||||
|
stunned = auto()
|
||||||
|
unconscious = auto()
|
||||||
|
dead = auto()
|
||||||
|
|
||||||
|
|
||||||
|
class DamageType(StrEnum):
|
||||||
|
piercing = auto()
|
||||||
|
slashing = auto()
|
||||||
|
bludgeoning = auto()
|
||||||
|
fire = auto()
|
||||||
|
cold = auto()
|
||||||
|
lightning = auto()
|
||||||
|
thunder = auto()
|
||||||
|
acid = auto()
|
||||||
|
poison = auto()
|
||||||
|
radiant = auto()
|
||||||
|
necrotic = auto()
|
||||||
|
psychic = auto()
|
||||||
|
force = auto()
|
||||||
|
magical = auto()
|
||||||
|
magical_piercing = auto()
|
||||||
|
magical_slashing = auto()
|
||||||
|
magical_bludgeoning = auto()
|
||||||
|
silvered_piercing = auto()
|
||||||
|
silvered_slashing = auto()
|
||||||
|
silvered_bludgeoning = auto()
|
||||||
|
adamantium_piercing = auto()
|
||||||
|
adamantium_slashing = auto()
|
||||||
|
adamantium_bludgeoning = auto()
|
||||||
|
|
||||||
|
|
||||||
|
class Defenses(StrEnum):
|
||||||
|
vulnerable = auto()
|
||||||
|
resistant = auto()
|
||||||
|
immune = auto()
|
||||||
|
absorbs = auto()
|
|
@ -6,6 +6,7 @@ import pytest
|
||||||
|
|
||||||
from ttfrog.db import schema
|
from ttfrog.db import schema
|
||||||
from ttfrog.db.manager import db as _db
|
from ttfrog.db.manager import db as _db
|
||||||
|
from ttfrog.db.schema.constants import DamageType, Defenses
|
||||||
|
|
||||||
FIXTURE_PATH = Path(__file__).parent / "fixtures"
|
FIXTURE_PATH = Path(__file__).parent / "fixtures"
|
||||||
|
|
||||||
|
@ -49,14 +50,12 @@ def bootstrap(db):
|
||||||
darkvision.add_modifier(schema.Modifier("Darkvision", target="vision_in_darkness", absolute_value=120))
|
darkvision.add_modifier(schema.Modifier("Darkvision", target="vision_in_darkness", absolute_value=120))
|
||||||
tiefling.add_trait(darkvision)
|
tiefling.add_trait(darkvision)
|
||||||
|
|
||||||
# resistant to both magical and non-magical sources of fire
|
# resistant to fire
|
||||||
infernal_origin = schema.AncestryTrait("Infernal Origin")
|
infernal_origin = schema.AncestryTrait("Infernal Origin")
|
||||||
infernal_origin.add_modifier(schema.Modifier("Infernal Origin", target="defenses.fire", new_value="resistant"))
|
|
||||||
infernal_origin.add_modifier(
|
infernal_origin.add_modifier(
|
||||||
schema.Modifier("Infernal Origin", target="defenses.magical_fire", new_value="resistant")
|
schema.Modifier("Infernal Origin", target=DamageType.fire, new_value=Defenses.resistant)
|
||||||
)
|
)
|
||||||
tiefling.add_trait(infernal_origin)
|
tiefling.add_trait(infernal_origin)
|
||||||
db.add_or_update(tiefling)
|
|
||||||
|
|
||||||
dragonborn = schema.Ancestry("dragonborn")
|
dragonborn = schema.Ancestry("dragonborn")
|
||||||
dragonborn.add_trait(darkvision)
|
dragonborn.add_trait(darkvision)
|
||||||
|
|
23
test/test_bootstrap.py
Normal file
23
test/test_bootstrap.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
from ttfrog.db.bootstrap import loader
|
||||||
|
|
||||||
|
|
||||||
|
def test_dump_load(db, bootstrap):
|
||||||
|
# dump the bootstrapped data
|
||||||
|
data = json.loads(db.dump())
|
||||||
|
|
||||||
|
# clear the database and reinitialize
|
||||||
|
db.metadata.drop_all(bind=db.engine)
|
||||||
|
db.init()
|
||||||
|
|
||||||
|
# load the dump
|
||||||
|
db.load(data)
|
||||||
|
|
||||||
|
# dump again and compare to the initial data
|
||||||
|
assert data == json.loads(db.dump())
|
||||||
|
|
||||||
|
|
||||||
|
def test_loader(db, bootstrap):
|
||||||
|
loader.load(db.dump())
|
||||||
|
assert len(db.Ancestry.all()) > 0
|
|
@ -1,6 +1,7 @@
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from ttfrog.db import schema
|
from ttfrog.db import schema
|
||||||
|
from ttfrog.db.schema.constants import DamageType, Defenses
|
||||||
|
|
||||||
|
|
||||||
def test_manage_character(db, bootstrap):
|
def test_manage_character(db, bootstrap):
|
||||||
|
@ -96,6 +97,14 @@ def test_manage_character(db, bootstrap):
|
||||||
assert char.check_modifier(athletics) == char.proficiency_bonus + char.strength.bonus == 3
|
assert char.check_modifier(athletics) == char.proficiency_bonus + char.strength.bonus == 3
|
||||||
assert char.check_modifier(acrobatics) == char.proficiency_bonus + char.dexterity.bonus == 3
|
assert char.check_modifier(acrobatics) == char.proficiency_bonus + char.dexterity.bonus == 3
|
||||||
|
|
||||||
|
# assert dexterity bonus apply to initiative
|
||||||
|
char._dexterity = 17
|
||||||
|
assert char.dexterity.bonus == 3
|
||||||
|
assert char.initiative == char.dexterity.bonus == 3
|
||||||
|
char.add_modifier(schema.Modifier("+1 initiative", target="initiative", relative_value=1))
|
||||||
|
assert char.initiative == 4
|
||||||
|
char._dexterity = 10
|
||||||
|
|
||||||
# multiclass
|
# multiclass
|
||||||
char.add_class(rogue, level=1)
|
char.add_class(rogue, level=1)
|
||||||
db.add_or_update(char)
|
db.add_or_update(char)
|
||||||
|
@ -279,45 +288,39 @@ def test_defenses(db, bootstrap):
|
||||||
with db.transaction():
|
with db.transaction():
|
||||||
tiefling = db.Ancestry.filter_by(name="tiefling").one()
|
tiefling = db.Ancestry.filter_by(name="tiefling").one()
|
||||||
carl = schema.Character(name="Carl", ancestry=tiefling)
|
carl = schema.Character(name="Carl", ancestry=tiefling)
|
||||||
assert carl.resistant("fire", magical=False)
|
assert carl.resistant(DamageType.fire)
|
||||||
assert carl.resistant("fire", magical=True)
|
carl.apply_damage(5, DamageType.fire)
|
||||||
carl.apply_damage(5, "fire", magical=True)
|
|
||||||
assert carl.hit_points == 8 # half damage
|
assert carl.hit_points == 8 # half damage
|
||||||
|
|
||||||
immunity = [
|
immunity = [
|
||||||
schema.Modifier("Fire Immunity", target="defenses.fire", new_value="immune"),
|
schema.Modifier("Fire Immunity", target=DamageType.fire, new_value=Defenses.immune),
|
||||||
schema.Modifier("Fire Immunity", target="defenses.magical_fire", new_value="immune")
|
|
||||||
]
|
]
|
||||||
for i in immunity:
|
for i in immunity:
|
||||||
carl.add_modifier(i)
|
carl.add_modifier(i)
|
||||||
assert carl.immune("fire")
|
assert carl.immune(DamageType.fire)
|
||||||
carl.apply_damage(5, "fire", magical=True)
|
carl.apply_damage(5, DamageType.fire)
|
||||||
carl.apply_damage(5, "fire", magical=False)
|
|
||||||
assert carl.hit_points == 8 # no damage
|
assert carl.hit_points == 8 # no damage
|
||||||
|
|
||||||
vulnerability = [
|
vulnerability = [
|
||||||
schema.Modifier("Fire Vulnerability", target="defenses.fire", new_value="vulnerable"),
|
schema.Modifier("Fire Vulnerability", target=DamageType.fire, new_value=Defenses.vulnerable),
|
||||||
schema.Modifier("Fire Vulnerability", target="defenses.magical_fire", new_value="vulnerable")
|
|
||||||
]
|
]
|
||||||
for i in vulnerability:
|
for i in vulnerability:
|
||||||
carl.add_modifier(i)
|
carl.add_modifier(i)
|
||||||
assert carl.vulnerable("fire")
|
assert carl.vulnerable(DamageType.fire)
|
||||||
assert not carl.immune("fire")
|
assert not carl.immune(DamageType.fire)
|
||||||
carl.apply_damage(2, "fire", magical=True)
|
carl.apply_damage(2, DamageType.fire)
|
||||||
assert carl.hit_points == 4 # double damage
|
assert carl.hit_points == 4 # double damage
|
||||||
|
|
||||||
absorbs = [
|
absorbs = [schema.Modifier("Absorbs Non-Magical Fire", target=DamageType.fire, new_value=Defenses.absorbs)]
|
||||||
schema.Modifier("Absorbs Non-Magical Fire", target="defenses.fire", new_value="absorbs"),
|
|
||||||
]
|
|
||||||
carl.add_modifier(absorbs[0])
|
carl.add_modifier(absorbs[0])
|
||||||
carl.apply_damage(20, "fire", magical=False)
|
carl.apply_damage(20, DamageType.fire)
|
||||||
assert carl.hit_points == carl._max_hit_points == 10
|
assert carl.hit_points == carl._max_hit_points == 10
|
||||||
|
|
||||||
for i in immunity + vulnerability + absorbs:
|
for i in immunity + vulnerability + absorbs:
|
||||||
carl.remove_modifier(i)
|
carl.remove_modifier(i)
|
||||||
carl.apply_damage(5, "fire", magical=True)
|
carl.apply_damage(5, DamageType.fire)
|
||||||
assert carl.resistant("fire")
|
assert carl.resistant(DamageType.fire)
|
||||||
assert not carl.immune("fire")
|
assert not carl.immune(DamageType.fire)
|
||||||
assert not carl.vulnerable("fire")
|
assert not carl.vulnerable(DamageType.fire)
|
||||||
assert not carl.absorbs("fire")
|
assert not carl.absorbs(DamageType.fire)
|
||||||
assert carl.hit_points == 8 # half damage
|
assert carl.hit_points == 8 # half damage
|
||||||
|
|
Loading…
Reference in New Issue
Block a user