55
66from .abstracts import HasName , HasDescription , Modification
77from .abstracts import key_field
8+ from .abilities import Ability
89from .document import FromDocument
9- from .enums import DIE_TYPES
10+ from .enums import DIE_TYPES , CASTER_TYPES
1011from drf_spectacular .utils import extend_schema_field , inline_serializer
1112from drf_spectacular .types import OpenApiTypes
1213from rest_framework import serializers
1314
15+
16+
1417class ClassFeatureItem (models .Model ):
1518 """This is the class for an individual class feature item, a subset of a class
16- feature. The name field is unused. """
19+ feature."""
1720
1821 key = key_field ()
1922
@@ -23,6 +26,14 @@ class ClassFeatureItem(models.Model):
2326 parent = models .ForeignKey ('ClassFeature' , on_delete = models .CASCADE )
2427 level = models .IntegerField (validators = [MinValueValidator (0 ),MaxValueValidator (20 )])
2528
29+ column_value = models .CharField (
30+ # The value displayed in a column, or null if no value.
31+ null = True ,
32+ blank = True ,
33+ max_length = 20 ,
34+ help_text = 'The value that should be displayed in the table column (where applicable).'
35+ )
36+
2637 def __str__ (self ):
2738 return "{} {} ({})" .format (
2839 self .parent .parent .name ,
@@ -37,18 +48,25 @@ class ClassFeature(HasName, HasDescription, FromDocument):
3748 parent = models .ForeignKey ('CharacterClass' ,
3849 on_delete = models .CASCADE )
3950
51+ def featureitems (self ):
52+ return self .classfeatureitem_set .exclude (column_value__isnull = False )
53+
54+ def columnitems (self ):
55+ return self .classfeatureitem_set .exclude (column_value__isnull = True )
56+
4057 def __str__ (self ):
4158 return "{} ({})" .format (self .name ,self .parent .name )
4259
4360
4461class CharacterClass (HasName , FromDocument ):
4562 """The model for a character class or subclass."""
63+
4664 subclass_of = models .ForeignKey ('self' ,
4765 default = None ,
4866 blank = True ,
4967 null = True ,
5068 on_delete = models .CASCADE )
51-
69+
5270 hit_dice = models .CharField (
5371 max_length = 100 ,
5472 default = None ,
@@ -57,6 +75,18 @@ class CharacterClass(HasName, FromDocument):
5775 choices = DIE_TYPES ,
5876 help_text = 'Dice notation hit dice option.' )
5977
78+ saving_throws = models .ManyToManyField (Ability ,
79+ related_name = "characterclass_saving_throws" ,
80+ help_text = 'Saving throw proficiencies for this class.' )
81+
82+ caster_type = models .CharField (
83+ max_length = 100 ,
84+ default = None ,
85+ blank = True ,
86+ null = True ,
87+ choices = CASTER_TYPES ,
88+ help_text = 'Type of caster. Options are full, half, none.' )
89+
6090 @property
6191 @extend_schema_field (inline_serializer (
6292 name = "hit_points" ,
@@ -104,21 +134,68 @@ def features(self):
104134 }
105135 )
106136 ))
107- def levels (self ):
108- """Returns an array of level information for the given class."""
109- by_level = {}
110-
111- for classfeature in self .classfeature_set .all ():
112- for fl in classfeature .classfeatureitem_set .all ():
113- if (str (fl .level )) not in by_level .keys ():
114- by_level [str (fl .level )] = {}
115- by_level [str (fl .level )]['features' ] = []
116-
117- by_level [str (fl .level )]['features' ].append (fl .parent .key )
118- by_level [str (fl .level )]['proficiency-bonus' ] = self .proficiency_bonus (player_level = fl .level )
119- by_level [str (fl .level )]['level' ] = fl .level
120-
121- return by_level
137+
138+
139+ def get_slots_by_player_level (self ,level ,caster_type ):
140+ # full is for a full caster, not including cantrips.
141+ # full=False is for a half caster.
142+ if level < 0 : # Invalid player level.
143+ return None
144+ if level > 20 : # Invalid player level.
145+ return None
146+
147+ full = [[],
148+ [0 ,2 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ],
149+ [0 ,3 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ],
150+ [0 ,4 ,2 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ],
151+ [0 ,4 ,3 ,0 ,0 ,0 ,0 ,0 ,0 ,0 ],
152+ [0 ,4 ,3 ,2 ,0 ,0 ,0 ,0 ,0 ,0 ],
153+ [0 ,4 ,3 ,3 ,0 ,0 ,0 ,0 ,0 ,0 ],
154+ [0 ,4 ,3 ,3 ,1 ,0 ,0 ,0 ,0 ,0 ],
155+ [0 ,4 ,3 ,3 ,2 ,0 ,0 ,0 ,0 ,0 ],
156+ [0 ,4 ,3 ,3 ,3 ,1 ,0 ,0 ,0 ,0 ],
157+ [0 ,4 ,3 ,3 ,3 ,2 ,0 ,0 ,0 ,0 ],
158+ [0 ,4 ,3 ,3 ,3 ,2 ,1 ,0 ,0 ,0 ],
159+ [0 ,4 ,3 ,3 ,3 ,2 ,1 ,0 ,0 ,0 ],
160+ [0 ,4 ,3 ,3 ,3 ,2 ,1 ,1 ,0 ,0 ],
161+ [0 ,4 ,3 ,3 ,3 ,2 ,1 ,1 ,0 ,0 ],
162+ [0 ,4 ,3 ,3 ,3 ,2 ,1 ,1 ,1 ,0 ],
163+ [0 ,4 ,3 ,3 ,3 ,2 ,1 ,1 ,1 ,0 ],
164+ [0 ,4 ,3 ,3 ,3 ,2 ,1 ,1 ,1 ,1 ],
165+ [0 ,4 ,3 ,3 ,3 ,3 ,1 ,1 ,1 ,1 ],
166+ [0 ,4 ,3 ,3 ,3 ,3 ,2 ,1 ,1 ,1 ],
167+ [0 ,4 ,3 ,3 ,3 ,3 ,2 ,2 ,1 ,1 ]
168+ ]
169+
170+ half = [[],
171+ [0 ,0 ,0 ,0 ,0 ,0 ],
172+ [0 ,2 ,0 ,0 ,0 ,0 ],
173+ [0 ,3 ,0 ,0 ,0 ,0 ],
174+ [0 ,3 ,0 ,0 ,0 ,0 ],
175+ [0 ,4 ,2 ,0 ,0 ,0 ],
176+ [0 ,4 ,2 ,0 ,0 ,0 ],
177+ [0 ,4 ,3 ,0 ,0 ,0 ],
178+ [0 ,4 ,3 ,0 ,0 ,0 ],
179+ [0 ,4 ,3 ,2 ,0 ,0 ],
180+ [0 ,4 ,3 ,2 ,0 ,0 ],
181+ [0 ,4 ,3 ,3 ,0 ,0 ],
182+ [0 ,4 ,3 ,3 ,0 ,0 ],
183+ [0 ,4 ,3 ,3 ,1 ,0 ],
184+ [0 ,4 ,3 ,3 ,1 ,0 ],
185+ [0 ,4 ,3 ,3 ,2 ,0 ],
186+ [0 ,4 ,3 ,3 ,2 ,0 ],
187+ [0 ,4 ,3 ,3 ,3 ,1 ],
188+ [0 ,4 ,3 ,3 ,3 ,1 ],
189+ [0 ,4 ,3 ,3 ,3 ,2 ],
190+ [0 ,4 ,3 ,3 ,3 ,2 ]
191+ ]
192+
193+ if caster_type == 'FULL' :
194+ return full [level ]
195+ if caster_type == 'HALF' :
196+ return half [level ]
197+ else :
198+ return []
122199
123200 def proficiency_bonus (self , player_level ):
124201 # Consider as part of enums
@@ -146,5 +223,3 @@ def search_result_extra_fields(self):
146223 "key" : self .subclass_of .key
147224 } if self .subclass_of else None
148225 }
149-
150- #TODO add verbose name plural
0 commit comments