Compare commits
2 Commits
a8bb6de008
...
551140b5bc
Author | SHA1 | Date | |
---|---|---|---|
|
551140b5bc | ||
|
68251ff4e9 |
|
@ -9,7 +9,7 @@ packages = [
|
|||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
python = "^3.11"
|
||||
python-dotenv = "^0.21.0"
|
||||
typer = "^0.9.0"
|
||||
rich = "^13.7.0"
|
||||
|
|
|
@ -80,11 +80,11 @@ def serve(
|
|||
"""
|
||||
|
||||
# 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
|
||||
|
||||
print("Starting TableTop Frog server...")
|
||||
bootstrap()
|
||||
loader.load()
|
||||
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.
|
||||
"""
|
||||
from ttfrog.db.bootstrap import bootstrap
|
||||
from ttfrog.db.bootstrap import loader
|
||||
|
||||
if not os.path.exists(app_state["env"]):
|
||||
app_state["env"].parent.mkdir(parents=True, exist_ok=True)
|
||||
app_state["env"].write_text(dedent(SETUP_HELP))
|
||||
print(f"Wrote defaults file {app_state['env']}.")
|
||||
bootstrap()
|
||||
loader.load()
|
||||
|
||||
|
||||
@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
|
||||
from contextlib import contextmanager
|
||||
from functools import cached_property
|
||||
from sqlite3 import IntegrityError
|
||||
|
||||
import transaction
|
||||
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
|
||||
|
||||
assert ttfrog.db.schema
|
||||
|
||||
|
||||
class AlchemyEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
|
@ -42,7 +41,7 @@ class SQLDatabaseManager:
|
|||
|
||||
@cached_property
|
||||
def metadata(self):
|
||||
return ttfrog.db.schema.BaseObject.metadata
|
||||
return schema.BaseObject.metadata
|
||||
|
||||
@cached_property
|
||||
def tables(self):
|
||||
|
@ -87,8 +86,18 @@ class SQLDatabaseManager:
|
|||
results[table_name] = [dict(row._mapping) for row in self.query(table).all()]
|
||||
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):
|
||||
return self.query(getattr(ttfrog.db.schema, name))
|
||||
return self.query(getattr(schema, name))
|
||||
|
||||
|
||||
db = SQLDatabaseManager()
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from .character import *
|
||||
from .classes import *
|
||||
from .constants import *
|
||||
from .log import *
|
||||
from .modifiers 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.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.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})
|
||||
exhaustion: Mapped[int] = mapped_column(nullable=False, default=0, info={"min": 0, "max": 5})
|
||||
|
||||
class_map = relationship("CharacterClassMap", cascade="all,delete,delete-orphan")
|
||||
class_list = association_proxy("class_map", "id", creator=class_map_creator)
|
||||
|
@ -272,6 +274,10 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
|
|||
def traits(self):
|
||||
return self.ancestry.traits
|
||||
|
||||
@property
|
||||
def initiative(self):
|
||||
return self._apply_modifiers("initiative", self.dexterity.bonus)
|
||||
|
||||
@property
|
||||
def speed(self):
|
||||
return self._apply_modifiers("speed", self.ancestry.speed)
|
||||
|
@ -294,7 +300,7 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
|
|||
|
||||
@property
|
||||
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
|
||||
def level(self):
|
||||
|
@ -314,23 +320,26 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
|
|||
return None
|
||||
return mapping[0]
|
||||
|
||||
def immune(self, damage_type: str, magical: bool = False):
|
||||
return self.defense(damage_type, magical) == 'immune'
|
||||
def immune(self, damage_type: DamageType):
|
||||
return self.defense(damage_type) == Defenses.immune
|
||||
|
||||
def resistant(self, damage_type: str, magical: bool = False):
|
||||
return self.defense(damage_type, magical) == 'resistant'
|
||||
def resistant(self, damage_type: DamageType):
|
||||
return self.defense(damage_type) == Defenses.resistant.value
|
||||
|
||||
def vulnerable(self, damage_type: str, magical: bool = False):
|
||||
return self.defense(damage_type, magical) == 'vulnerable'
|
||||
def vulnerable(self, damage_type: DamageType):
|
||||
return self.defense(damage_type) == Defenses.vulnerable
|
||||
|
||||
def absorbs(self, damage_type: str, magical: bool = False):
|
||||
return self.defense(damage_type, magical) == 'absorbs'
|
||||
def absorbs(self, damage_type: DamageType):
|
||||
return self.defense(damage_type) == Defenses.absorbs
|
||||
|
||||
def defense(self, damage_type: str, magical: bool = False):
|
||||
attr_name = damage_type
|
||||
if magical:
|
||||
attr_name = f"magical_{attr_name}"
|
||||
return self._apply_modifiers(f"defenses.{attr_name}", None)
|
||||
def conditions(self):
|
||||
return [self._apply_modifiers(f"conditions.{name}") for name in Conditions]
|
||||
|
||||
def condition(self, condition_name: str):
|
||||
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):
|
||||
# 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)
|
||||
|
||||
# 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):
|
||||
self._hit_dice.append(HitDie(character_id=self.id, character_class_id=newclass.id))
|
||||
|
||||
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:
|
||||
if mapping.character_class == target:
|
||||
self.remove_class_attribute(mapping.class_attribute)
|
||||
for skill in target.skills:
|
||||
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.class_map = [m for m in self.class_map if m.character_class != target]
|
||||
|
||||
def remove_class_attribute(self, attribute):
|
||||
self.character_class_attribute_map = [
|
||||
|
@ -422,8 +431,8 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
|
|||
return True
|
||||
|
||||
def add_skill(self, skill, proficient=False, expert=False, character_class=None):
|
||||
if not self.id:
|
||||
raise Exception("Cannot add a skill before the character has been persisted.")
|
||||
# if not self.id:
|
||||
# raise Exception("Cannot add a skill before the character has been persisted.")
|
||||
skillmap = None
|
||||
exists = False
|
||||
if skill in self.skills:
|
||||
|
@ -474,15 +483,15 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
|
|||
def apply_healing(self, value: int):
|
||||
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
|
||||
if self.absorbs(damage_type, magical):
|
||||
if self.absorbs(damage_type):
|
||||
return self.apply_healing(total)
|
||||
if self.immune(damage_type, magical):
|
||||
if self.immune(damage_type):
|
||||
return
|
||||
if self.resistant(damage_type, magical):
|
||||
if self.resistant(damage_type):
|
||||
total = int(value / 2)
|
||||
elif self.vulnerable(damage_type, magical):
|
||||
elif self.vulnerable(damage_type):
|
||||
total = value * 2
|
||||
|
||||
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.manager import db as _db
|
||||
from ttfrog.db.schema.constants import DamageType, Defenses
|
||||
|
||||
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))
|
||||
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.add_modifier(schema.Modifier("Infernal Origin", target="defenses.fire", new_value="resistant"))
|
||||
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)
|
||||
db.add_or_update(tiefling)
|
||||
|
||||
dragonborn = schema.Ancestry("dragonborn")
|
||||
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
|
||||
|
||||
from ttfrog.db import schema
|
||||
from ttfrog.db.schema.constants import DamageType, Defenses
|
||||
|
||||
|
||||
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(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
|
||||
char.add_class(rogue, level=1)
|
||||
db.add_or_update(char)
|
||||
|
@ -279,45 +288,39 @@ def test_defenses(db, bootstrap):
|
|||
with db.transaction():
|
||||
tiefling = db.Ancestry.filter_by(name="tiefling").one()
|
||||
carl = schema.Character(name="Carl", ancestry=tiefling)
|
||||
assert carl.resistant("fire", magical=False)
|
||||
assert carl.resistant("fire", magical=True)
|
||||
carl.apply_damage(5, "fire", magical=True)
|
||||
assert carl.resistant(DamageType.fire)
|
||||
carl.apply_damage(5, DamageType.fire)
|
||||
assert carl.hit_points == 8 # half damage
|
||||
|
||||
immunity = [
|
||||
schema.Modifier("Fire Immunity", target="defenses.fire", new_value="immune"),
|
||||
schema.Modifier("Fire Immunity", target="defenses.magical_fire", new_value="immune")
|
||||
schema.Modifier("Fire Immunity", target=DamageType.fire, new_value=Defenses.immune),
|
||||
]
|
||||
for i in immunity:
|
||||
carl.add_modifier(i)
|
||||
assert carl.immune("fire")
|
||||
carl.apply_damage(5, "fire", magical=True)
|
||||
carl.apply_damage(5, "fire", magical=False)
|
||||
assert carl.immune(DamageType.fire)
|
||||
carl.apply_damage(5, DamageType.fire)
|
||||
assert carl.hit_points == 8 # no damage
|
||||
|
||||
vulnerability = [
|
||||
schema.Modifier("Fire Vulnerability", target="defenses.fire", new_value="vulnerable"),
|
||||
schema.Modifier("Fire Vulnerability", target="defenses.magical_fire", new_value="vulnerable")
|
||||
schema.Modifier("Fire Vulnerability", target=DamageType.fire, new_value=Defenses.vulnerable),
|
||||
]
|
||||
for i in vulnerability:
|
||||
carl.add_modifier(i)
|
||||
assert carl.vulnerable("fire")
|
||||
assert not carl.immune("fire")
|
||||
carl.apply_damage(2, "fire", magical=True)
|
||||
assert carl.vulnerable(DamageType.fire)
|
||||
assert not carl.immune(DamageType.fire)
|
||||
carl.apply_damage(2, DamageType.fire)
|
||||
assert carl.hit_points == 4 # double damage
|
||||
|
||||
absorbs = [
|
||||
schema.Modifier("Absorbs Non-Magical Fire", target="defenses.fire", new_value="absorbs"),
|
||||
]
|
||||
absorbs = [schema.Modifier("Absorbs Non-Magical Fire", target=DamageType.fire, new_value=Defenses.absorbs)]
|
||||
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
|
||||
|
||||
for i in immunity + vulnerability + absorbs:
|
||||
carl.remove_modifier(i)
|
||||
carl.apply_damage(5, "fire", magical=True)
|
||||
assert carl.resistant("fire")
|
||||
assert not carl.immune("fire")
|
||||
assert not carl.vulnerable("fire")
|
||||
assert not carl.absorbs("fire")
|
||||
carl.apply_damage(5, DamageType.fire)
|
||||
assert carl.resistant(DamageType.fire)
|
||||
assert not carl.immune(DamageType.fire)
|
||||
assert not carl.vulnerable(DamageType.fire)
|
||||
assert not carl.absorbs(DamageType.fire)
|
||||
assert carl.hit_points == 8 # half damage
|
||||
|
|
Loading…
Reference in New Issue
Block a user