77from .abstracts import key_field
88from .abilities import Ability
99from .document import FromDocument
10- from .enums import DIE_TYPES
10+ from .enums import DIE_TYPES , CASTER_TYPES
1111from drf_spectacular .utils import extend_schema_field , inline_serializer
1212from drf_spectacular .types import OpenApiTypes
1313from rest_framework import serializers
1414
15+
16+
1517class ClassFeatureItem (models .Model ):
1618 """This is the class for an individual class feature item, a subset of a class
17- feature. The name field is unused. """
19+ feature."""
1820
1921 key = key_field ()
2022
@@ -24,6 +26,14 @@ class ClassFeatureItem(models.Model):
2426 parent = models .ForeignKey ('ClassFeature' , on_delete = models .CASCADE )
2527 level = models .IntegerField (validators = [MinValueValidator (0 ),MaxValueValidator (20 )])
2628
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+
2737 def __str__ (self ):
2838 return "{} {} ({})" .format (
2939 self .parent .parent .name ,
@@ -38,18 +48,25 @@ class ClassFeature(HasName, HasDescription, FromDocument):
3848 parent = models .ForeignKey ('CharacterClass' ,
3949 on_delete = models .CASCADE )
4050
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+
4157 def __str__ (self ):
4258 return "{} ({})" .format (self .name ,self .parent .name )
4359
4460
4561class CharacterClass (HasName , FromDocument ):
4662 """The model for a character class or subclass."""
63+
4764 subclass_of = models .ForeignKey ('self' ,
4865 default = None ,
4966 blank = True ,
5067 null = True ,
5168 on_delete = models .CASCADE )
52-
69+
5370 hit_dice = models .CharField (
5471 max_length = 100 ,
5572 default = None ,
@@ -58,11 +75,18 @@ class CharacterClass(HasName, FromDocument):
5875 choices = DIE_TYPES ,
5976 help_text = 'Dice notation hit dice option.' )
6077
61-
6278 saving_throws = models .ManyToManyField (Ability ,
6379 related_name = "characterclass_saving_throws" ,
6480 help_text = 'Saving throw proficiencies for this class.' )
6581
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+
6690 @property
6791 @extend_schema_field (inline_serializer (
6892 name = "hit_points" ,
@@ -110,21 +134,68 @@ def features(self):
110134 }
111135 )
112136 ))
113- def levels (self ):
114- """Returns an array of level information for the given class."""
115- by_level = {}
116-
117- for classfeature in self .classfeature_set .all ():
118- for fl in classfeature .classfeatureitem_set .all ():
119- if (str (fl .level )) not in by_level .keys ():
120- by_level [str (fl .level )] = {}
121- by_level [str (fl .level )]['features' ] = []
122-
123- by_level [str (fl .level )]['features' ].append (fl .parent .key )
124- by_level [str (fl .level )]['proficiency-bonus' ] = self .proficiency_bonus (player_level = fl .level )
125- by_level [str (fl .level )]['level' ] = fl .level
126-
127- 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 []
128199
129200 def proficiency_bonus (self , player_level ):
130201 # Consider as part of enums
@@ -152,5 +223,3 @@ def search_result_extra_fields(self):
152223 "key" : self .subclass_of .key
153224 } if self .subclass_of else None
154225 }
155-
156- #TODO add verbose name plural
0 commit comments