Skip to content

Commit 779d62e

Browse files
Merge pull request #614 from open5e/610-add-extra-column-to-class-table-on-character-classes
610 add extra column to class table on character classes
2 parents af56be2 + b3a7553 commit 779d62e

15 files changed

Lines changed: 20534 additions & 1645 deletions

api_v2/admin.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ class RaceAdmin(admin.ModelAdmin):
3434
RaceTraitInline,
3535
]
3636

37+
class ClassFeatureItemInline(admin.TabularInline):
38+
model = ClassFeatureItem
39+
40+
class ClassFeatureAdmin(admin.ModelAdmin):
41+
inlines = [
42+
ClassFeatureItemInline
43+
]
44+
3745

3846
class BackgroundBenefitInline(admin.TabularInline):
3947
model = BackgroundBenefit
@@ -93,7 +101,7 @@ class LanguageAdmin(admin.ModelAdmin):
93101
admin.site.register(Condition)
94102

95103
admin.site.register(ClassFeatureItem)
96-
admin.site.register(ClassFeature)
104+
admin.site.register(ClassFeature, admin_class=ClassFeatureAdmin)
97105
admin.site.register(CharacterClass)
98106

99107
admin.site.register(Environment)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.1.2 on 2024-11-26 22:05
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('api_v2', '0017_characterclass_saving_throws'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='characterclass',
15+
name='caster_type',
16+
field=models.CharField(blank=True, choices=[('FULL', 'Full'), ('HALF', 'Half'), ('NONE', 'None')], default=None, help_text='Type of caster. Options are full, half, none.', max_length=100, null=True),
17+
),
18+
]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 5.1.2 on 2024-12-01 15:06
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('api_v2', '0018_characterclass_caster_type'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='classfeatureitem',
15+
name='column',
16+
field=models.BooleanField(default=False, help_text='Whether or not the field should be displayed as a column.'),
17+
preserve_default=False,
18+
),
19+
]
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Generated by Django 5.1.2 on 2024-12-01 15:11
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('api_v2', '0019_classfeatureitem_column'),
10+
]
11+
12+
operations = [
13+
migrations.RemoveField(
14+
model_name='classfeatureitem',
15+
name='column',
16+
),
17+
migrations.AddField(
18+
model_name='classfeatureitem',
19+
name='column_value',
20+
field=models.CharField(blank=True, help_text='The value that should be displayed in the table column (where applicable).', max_length=20, null=True),
21+
),
22+
]

api_v2/models/characterclass.py

Lines changed: 90 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@
77
from .abstracts import key_field
88
from .abilities import Ability
99
from .document import FromDocument
10-
from .enums import DIE_TYPES
10+
from .enums import DIE_TYPES, CASTER_TYPES
1111
from drf_spectacular.utils import extend_schema_field, inline_serializer
1212
from drf_spectacular.types import OpenApiTypes
1313
from rest_framework import serializers
1414

15+
16+
1517
class 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

4561
class 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

api_v2/models/enums.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@
4141
("WEAPON", "Weapon"),
4242
]
4343

44+
CASTER_TYPES = [
45+
("FULL","Full"),
46+
("HALF","Half"),
47+
("NONE","None")
48+
]
49+
4450
ACTION_TYPES = [
4551
("ACTION", "Action"),
4652
("REACTION","Reaction"),

api_v2/serializers/characterclass.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,36 @@
77
from .abstracts import GameContentSerializer
88
from .document import DocumentSerializer
99

10+
1011
class ClassFeatureItemSerializer(GameContentSerializer):
12+
1113
class Meta:
1214
model = models.ClassFeatureItem
13-
fields = ['name','desc','type']
15+
fields = ['level']
16+
17+
class ClassFeatureColumnItemSerializer(GameContentSerializer):
18+
class Meta:
19+
model = models.ClassFeatureItem
20+
fields = ['level','column_value']
1421

1522
class ClassFeatureSerializer(GameContentSerializer):
1623
key = serializers.ReadOnlyField()
24+
featureitems = ClassFeatureItemSerializer(
25+
many=True
26+
)
27+
28+
columnitems = ClassFeatureColumnItemSerializer(
29+
many=True
30+
)
1731

1832
class Meta:
1933
model = models.ClassFeature
20-
fields = ['key', 'name', 'desc']
34+
fields = ['key', 'name', 'desc','featureitems','columnitems']
2135

2236
class CharacterClassSerializer(GameContentSerializer):
2337
key = serializers.ReadOnlyField()
2438
features = ClassFeatureSerializer(
2539
many=True, context={'request': {}})
26-
levels = serializers.ReadOnlyField()
2740
hit_points = serializers.ReadOnlyField()
2841
document = DocumentSerializer()
2942

0 commit comments

Comments
 (0)