add spell inventory ui, refined level up ui, fixed bugs
This commit is contained in:
+34
-12
@@ -91,28 +91,50 @@ def bootstrap(db):
|
||||
assert acrobatics in fighter.skills
|
||||
assert athletics in fighter.skills
|
||||
|
||||
wizard = schema.CharacterClass(
|
||||
"wizard",
|
||||
hit_die_name="1d6",
|
||||
hit_die_stat_name="_intelligence",
|
||||
)
|
||||
db.add_or_update(wizard)
|
||||
wizard.spell_slots = [
|
||||
schema.ClassSpellSlotMap(wizard.id, class_level=1, spell_level=1),
|
||||
schema.ClassSpellSlotMap(wizard.id, class_level=1, spell_level=1),
|
||||
schema.ClassSpellSlotMap(wizard.id, class_level=2, spell_level=1),
|
||||
schema.ClassSpellSlotMap(wizard.id, class_level=3, spell_level=1),
|
||||
schema.ClassSpellSlotMap(wizard.id, class_level=3, spell_level=2),
|
||||
schema.ClassSpellSlotMap(wizard.id, class_level=3, spell_level=2),
|
||||
schema.ClassSpellSlotMap(wizard.id, class_level=4, spell_level=2),
|
||||
schema.ClassSpellSlotMap(wizard.id, class_level=5, spell_level=3),
|
||||
schema.ClassSpellSlotMap(wizard.id, class_level=5, spell_level=3),
|
||||
schema.ClassSpellSlotMap(wizard.id, class_level=6, spell_level=3),
|
||||
schema.ClassSpellSlotMap(wizard.id, class_level=7, spell_level=4),
|
||||
]
|
||||
|
||||
rogue = schema.CharacterClass("rogue", hit_die_name="1d8", hit_die_stat_name="_dexterity")
|
||||
db.add_or_update([rogue, fighter])
|
||||
db.add_or_update([rogue, fighter, wizard])
|
||||
|
||||
# characters
|
||||
foo = schema.Character("Foo", ancestry=tiefling, _intelligence=14)
|
||||
db.add_or_update(foo)
|
||||
foo.add_class(fighter, level=2)
|
||||
foo.add_class(rogue, level=3)
|
||||
# create a character: Carl the Wizard
|
||||
carl = schema.Character("Carl", ancestry=tiefling, _intelligence=14)
|
||||
carl.add_class(wizard)
|
||||
db.add_or_update(carl)
|
||||
|
||||
bar = schema.Character("Bar", ancestry=human)
|
||||
|
||||
# persist all the records we've created
|
||||
db.add_or_update([foo, bar])
|
||||
|
||||
@pytest.fixture
|
||||
def carl(db, bootstrap, tiefling):
|
||||
return schema.Character(name="Carl", ancestry=tiefling)
|
||||
def carl(db, bootstrap):
|
||||
return db.Character.filter_by(name="Carl").one()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def wizard(db, bootstrap):
|
||||
return db.CharacterClass.filter_by(name="wizard").one()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tiefling(db, bootstrap):
|
||||
return db.Ancestry.filter_by(name="tiefling").one()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def human(db, bootstrap):
|
||||
return db.Ancestry.filter_by(name="human").one()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from ttfrog.db.schema.item import Item, Spell, ItemType
|
||||
from ttfrog.db.schema.item import Item, ItemType, Spell
|
||||
|
||||
|
||||
def test_equipment_inventory(db, carl):
|
||||
@@ -83,3 +83,65 @@ def test_inventory_bundles(db, carl):
|
||||
# consume all remaining arrows
|
||||
assert quiver.use(19) == 0
|
||||
assert arrows not in carl.equipment
|
||||
|
||||
|
||||
def test_spell_slots(db, carl, wizard):
|
||||
with db.transaction():
|
||||
prestidigitation = Spell(name="Prestidigitation", level=0, concentration=False)
|
||||
fireball = Spell(name="Fireball", level=3, concentration=False)
|
||||
db.add_or_update([carl, prestidigitation, fireball])
|
||||
|
||||
carl.spells.add(prestidigitation)
|
||||
carl.spells.add(fireball)
|
||||
db.add_or_update(carl)
|
||||
|
||||
# verify carl has the spell slots granted by wizard at 1st level
|
||||
print(carl.levels)
|
||||
print(carl.spell_slots)
|
||||
assert len(carl.spell_slots) == 2
|
||||
assert carl.spell_slots[0].spell_level == 1
|
||||
assert carl.spell_slots[1].spell_level == 1
|
||||
|
||||
# carl knows the spells but hasn't prepared them
|
||||
assert prestidigitation in carl.spells
|
||||
assert fireball in carl.spells
|
||||
assert prestidigitation not in carl.prepared_spells
|
||||
assert fireball not in carl.prepared_spells
|
||||
|
||||
# prepare the cantrip
|
||||
carls_prestidigitation = carl.spells.get(prestidigitation)[0]
|
||||
assert carl.prepare(carls_prestidigitation)
|
||||
assert carl.cast(carls_prestidigitation)
|
||||
|
||||
# prepare() and cast() require a spell from the spell inventory
|
||||
carls_fireball = carl.spells.get(fireball)[0]
|
||||
|
||||
# can't prepare a 3rd level spell if you don't have 3rd level slots
|
||||
assert carl.spellcaster_level == 1
|
||||
assert not carl.prepare(carls_fireball)
|
||||
|
||||
# make carl a 5th level wizard so he gets a 3rd level spell slot
|
||||
carl.level_up(wizard, num_levels=4)
|
||||
assert carl.level == 5
|
||||
assert carl.spellcaster_level == 3
|
||||
|
||||
# cast fireball until he's out of 3rd level slots
|
||||
assert carl.prepare(carls_fireball)
|
||||
assert carl.cast(carls_fireball)
|
||||
assert carl.cast(carls_fireball)
|
||||
assert not carl.cast(carls_fireball)
|
||||
|
||||
# level up to 7th level, gaining 1 4th level slot and 1 more 3rd level slot
|
||||
carl.add_class(wizard)
|
||||
carl.level_up(wizard, num_levels=2)
|
||||
assert carl.spellcaster_level == 4
|
||||
assert len(carl.spell_slots_available[4]) == 1
|
||||
assert len(carl.spell_slots_available[3]) == 1
|
||||
|
||||
# cast at 4th level
|
||||
assert carl.cast(carls_fireball, 4)
|
||||
assert not carl.cast(carls_fireball, 4)
|
||||
|
||||
# use the last 3rd level slot
|
||||
assert carl.cast(carls_fireball)
|
||||
assert not carl.cast(carls_fireball)
|
||||
|
||||
+12
-13
@@ -12,7 +12,7 @@ def test_manage_character(db, bootstrap):
|
||||
# create a human character (the default)
|
||||
char = schema.Character(name="Test Character", ancestry=human)
|
||||
db.add_or_update(char)
|
||||
assert char.id == 3
|
||||
assert char.id
|
||||
assert char.name == "Test Character"
|
||||
assert char.ancestry.name == "human"
|
||||
assert char.armor_class == 10
|
||||
@@ -67,7 +67,7 @@ def test_manage_character(db, bootstrap):
|
||||
rogue = db.CharacterClass.filter_by(name="rogue").one()
|
||||
|
||||
# assign a class and level
|
||||
char.add_class(fighter, level=1)
|
||||
assert char.add_class(fighter)
|
||||
db.add_or_update(char)
|
||||
assert char.levels == {"fighter": 1}
|
||||
assert char.level == 1
|
||||
@@ -81,7 +81,7 @@ def test_manage_character(db, bootstrap):
|
||||
assert char.class_features == {}
|
||||
|
||||
# level up
|
||||
char.add_class(fighter, level=7)
|
||||
char.level_up(fighter, num_levels=6)
|
||||
db.add_or_update(char)
|
||||
assert char.levels == {"fighter": 7}
|
||||
assert char.level == 7
|
||||
@@ -109,7 +109,7 @@ def test_manage_character(db, bootstrap):
|
||||
char._dexterity = 10
|
||||
|
||||
# multiclass
|
||||
char.add_class(rogue, level=1)
|
||||
char.add_class(rogue)
|
||||
db.add_or_update(char)
|
||||
assert char.level == 8
|
||||
assert char.levels == {"fighter": 7, "rogue": 1}
|
||||
@@ -132,8 +132,7 @@ def test_manage_character(db, bootstrap):
|
||||
assert char.hit_dice["fighter"][0].name == fighter.hit_die_name == "1d10"
|
||||
assert char.hit_dice["fighter"][0].stat == fighter.hit_die_stat_name == "_constitution"
|
||||
|
||||
# remove remaining class by setting level to zero
|
||||
char.add_class(fighter, level=0)
|
||||
char.remove_class(fighter)
|
||||
db.add_or_update(char)
|
||||
assert char.levels == {}
|
||||
assert char.class_features == {}
|
||||
@@ -205,9 +204,8 @@ def test_ancestries(db, bootstrap):
|
||||
assert grognak.check_modifier(strength, save=True) == 1
|
||||
|
||||
|
||||
def test_modifiers(db, bootstrap, carl, tiefling, human):
|
||||
def test_modifiers(db, bootstrap, carl, tiefling, human, wizard):
|
||||
with db.transaction():
|
||||
|
||||
# no modifiers; speed is ancestry speed
|
||||
marx = schema.Character(name="Marx", ancestry=human)
|
||||
db.add_or_update([carl, marx])
|
||||
@@ -301,32 +299,33 @@ def test_modifiers(db, bootstrap, carl, tiefling, human):
|
||||
assert carl.add_modifier(temp_proficiency)
|
||||
assert carl.check_modifier(athletics) == carl.expertise_bonus + carl.strength.bonus == 2
|
||||
assert carl.remove_modifier(temp_proficiency)
|
||||
assert carl.check_modifier(athletics) == 0
|
||||
|
||||
# fighters get proficiency in athletics by default
|
||||
fighter = db.CharacterClass.filter_by(name="fighter").one()
|
||||
carl.add_class(fighter)
|
||||
db.add_or_update(carl)
|
||||
assert carl.check_modifier(athletics) == 1
|
||||
assert carl.check_modifier(athletics) == carl.proficiency_bonus
|
||||
|
||||
# add the skill directly, which will grant proficiency but will not stack with proficiency from the class
|
||||
carl.add_skill(athletics, proficient=True)
|
||||
db.add_or_update(carl)
|
||||
assert len([s for s in carl.skills if s == athletics]) == 2
|
||||
assert carl.check_modifier(athletics) == 1
|
||||
assert carl.check_modifier(athletics) == 2
|
||||
|
||||
# manually override proficiency with expertise
|
||||
carl.add_skill(athletics, expert=True)
|
||||
assert carl.check_modifier(athletics) == 2
|
||||
assert carl.check_modifier(athletics) == 2 * carl.proficiency_bonus
|
||||
assert len([s for s in carl.skills if s == athletics]) == 2
|
||||
|
||||
# remove expertise
|
||||
carl.add_skill(athletics, proficient=True, expert=False)
|
||||
assert carl.check_modifier(athletics) == 1
|
||||
assert carl.check_modifier(athletics) == carl.proficiency_bonus
|
||||
|
||||
# remove the extra skill entirely, but the fighter proficiency remains
|
||||
carl.remove_skill(athletics, proficient=True, expert=False, character_class=None)
|
||||
assert len([s for s in carl.skills if s == athletics]) == 1
|
||||
assert carl.check_modifier(athletics) == 1
|
||||
assert carl.check_modifier(athletics) == carl.proficiency_bonus
|
||||
|
||||
# ensure you can't remove a skill already removed
|
||||
assert not carl.remove_skill(athletics, proficient=True, expert=False, character_class=None)
|
||||
|
||||
Reference in New Issue
Block a user