Skip to content

Commit e63f742

Browse files
calumbellcalumbell
andauthored
[V2] Weapon Properties refactor (open5e#730)
* created WeaponProperty model/serializer * added srd-2014 weapon properties to dataset * created WeaponPropertyAssignment intermediate model to link Weapons and WeaponProperties * added Versatile weapon props to srd-2014 weapons * added remaining WeaponPropertyAssignments to srd-2014 weapons * moved Item/Weapon serializer/view to use new WeaponProperties * updated tests * fixed typo in WeaponPropertyAssignment data --------- Co-authored-by: calumbell <[email protected]>
1 parent 932dd8f commit e63f742

17 files changed

+960
-684
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Generated by Django 5.1.2 on 2025-05-14 12:02
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('api_v2', '0041_merge_20250509_0736'),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name='WeaponProperty',
16+
fields=[
17+
('name', models.CharField(help_text='Name of the item.', max_length=100)),
18+
('desc', models.TextField(help_text='Description of the game content item. Markdown.')),
19+
('key', models.CharField(help_text='Unique key for the Item.', max_length=100, primary_key=True, serialize=False)),
20+
('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api_v2.document')),
21+
],
22+
options={
23+
'verbose_name_plural': 'Weapon Properties',
24+
},
25+
),
26+
]
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Generated by Django 5.1.2 on 2025-05-15 10:33
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('api_v2', '0042_weaponproperty'),
11+
]
12+
13+
operations = [
14+
migrations.AlterModelOptions(
15+
name='weaponproperty',
16+
options={'ordering': ['pk'], 'verbose_name_plural': 'Weapon Properties'},
17+
),
18+
migrations.CreateModel(
19+
name='WeaponPropertyAssignment',
20+
fields=[
21+
('key', models.CharField(help_text='Unique key for the Item.', max_length=100, primary_key=True, serialize=False)),
22+
('detail', models.CharField(blank=True, max_length=32, null=True)),
23+
('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api_v2.document')),
24+
('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='weapons', to='api_v2.weaponproperty')),
25+
('weapon', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='properties', to='api_v2.weapon')),
26+
],
27+
options={
28+
'abstract': False,
29+
},
30+
),
31+
]
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Generated by Django 5.1.2 on 2025-05-16 12:01
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('api_v2', '0043_alter_weaponproperty_options_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.RemoveField(
14+
model_name='weapon',
15+
name='is_finesse',
16+
),
17+
migrations.RemoveField(
18+
model_name='weapon',
19+
name='is_heavy',
20+
),
21+
migrations.RemoveField(
22+
model_name='weapon',
23+
name='is_lance',
24+
),
25+
migrations.RemoveField(
26+
model_name='weapon',
27+
name='is_light',
28+
),
29+
migrations.RemoveField(
30+
model_name='weapon',
31+
name='is_net',
32+
),
33+
migrations.RemoveField(
34+
model_name='weapon',
35+
name='is_thrown',
36+
),
37+
migrations.RemoveField(
38+
model_name='weapon',
39+
name='is_two_handed',
40+
),
41+
migrations.RemoveField(
42+
model_name='weapon',
43+
name='reach',
44+
),
45+
migrations.RemoveField(
46+
model_name='weapon',
47+
name='requires_ammunition',
48+
),
49+
migrations.RemoveField(
50+
model_name='weapon',
51+
name='requires_loading',
52+
),
53+
migrations.RemoveField(
54+
model_name='weapon',
55+
name='versatile_dice',
56+
),
57+
]

api_v2/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from .armor import Armor
1212

1313
from .weapon import Weapon
14+
from .weapon import WeaponProperty
15+
from .weapon import WeaponPropertyAssignment
1416

1517
from .species import SpeciesTrait
1618
from .species import Species

api_v2/models/weapon.py

Lines changed: 29 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,39 @@
33
from django.db import models
44
from django.core.validators import MinValueValidator
55

6-
from .abstracts import HasName
6+
from .abstracts import HasName, HasDescription
77
from .abstracts import distance_field, distance_unit_field
88
from .document import FromDocument
99
from drf_spectacular.utils import extend_schema_field
1010
from drf_spectacular.types import OpenApiTypes
1111
from rest_framework import serializers
1212

13+
class WeaponProperty(HasName, HasDescription, FromDocument):
14+
class Meta:
15+
verbose_name_plural = "Weapon Properties"
16+
ordering = ["pk"]
17+
18+
def __str__(self):
19+
return self.name
20+
21+
22+
class WeaponPropertyAssignment(FromDocument):
23+
"""
24+
This is an intermediate model that is used to assign WeaponProperties to
25+
Weapons while bundling in any extra contextual data in the `detail` field
26+
"""
27+
weapon = models.ForeignKey(
28+
'Weapon',
29+
related_name='properties',
30+
on_delete=models.CASCADE
31+
)
32+
property = models.ForeignKey(
33+
'WeaponProperty',
34+
related_name='weapons',
35+
on_delete=models.CASCADE,
36+
)
37+
detail = models.CharField(null=True, blank=True, max_length=32)
38+
1339
class Weapon(HasName, FromDocument):
1440
"""
1541
This model represents types of weapons.
@@ -31,21 +57,12 @@ class Weapon(HasName, FromDocument):
3157
max_length=100,
3258
help_text='The damage dice when used making an attack.')
3359

34-
versatile_dice = models.CharField(
35-
null=False,
36-
default=0,
37-
max_length=100,
38-
help_text="""The damage dice when attacking using versatile.
39-
A value of 0 means that the weapon does not have the versatile property.""")
40-
41-
reach = distance_field()
42-
4360
range = distance_field()
4461

4562
long_range = distance_field()
4663

4764
distance_unit = distance_unit_field()
48-
65+
4966
@property
5067
# or none
5168
@extend_schema_field(OpenApiTypes.STR)
@@ -54,52 +71,6 @@ def get_distance_unit(self):
5471
return self.document.distance_unit
5572
return self.distance_unit
5673

57-
58-
is_finesse = models.BooleanField(
59-
null=False,
60-
default=False,
61-
help_text='If the weapon is finesse.')
62-
63-
is_thrown = models.BooleanField(
64-
null=False,
65-
default=False,
66-
help_text='If the weapon is thrown.')
67-
68-
is_two_handed = models.BooleanField(
69-
null=False,
70-
default=False,
71-
help_text='If the weapon is two-handed.')
72-
73-
requires_ammunition = models.BooleanField(
74-
null=False,
75-
default=False,
76-
help_text='If the weapon requires ammunition.')
77-
78-
requires_loading = models.BooleanField(
79-
null=False,
80-
default=False,
81-
help_text='If the weapon requires loading.')
82-
83-
is_heavy = models.BooleanField(
84-
null=False,
85-
default=False,
86-
help_text='If the weapon is heavy.')
87-
88-
is_light = models.BooleanField(
89-
null=False,
90-
default=False,
91-
help_text='If the weapon is light.')
92-
93-
is_lance = models.BooleanField(
94-
null=False,
95-
default=False,
96-
help_text='If the weapon is a lance.')
97-
98-
is_net = models.BooleanField(
99-
null=False,
100-
default=False,
101-
help_text='If the weapon is a net.')
102-
10374
is_simple = models.BooleanField(
10475
null=False,
10576
default=False,
@@ -109,71 +80,8 @@ def get_distance_unit(self):
10980
null=False,
11081
default=False,
11182
help_text='If the weapon is improvised.')
112-
113-
@property
114-
@extend_schema_field(OpenApiTypes.BOOL)
115-
def is_versatile(self):
116-
return self.versatile_dice != str(0)
11783

11884
@property
11985
@extend_schema_field(OpenApiTypes.BOOL)
12086
def is_martial(self):
121-
return not self.is_simple
122-
123-
@property
124-
@extend_schema_field(OpenApiTypes.BOOL)
125-
def is_melee(self):
126-
# Ammunition weapons can only be used as improvised melee weapons.
127-
return not self.requires_ammunition
128-
129-
@property
130-
@extend_schema_field(OpenApiTypes.BOOL)
131-
def ranged_attack_possible(self):
132-
# Only ammunition or throw weapons can make ranged attacks.
133-
return self.requires_ammunition or self.is_thrown
134-
135-
@property
136-
# type is any
137-
@extend_schema_field(OpenApiTypes.BOOL)
138-
def range_melee(self):
139-
return self.reach
140-
141-
@property
142-
@extend_schema_field(OpenApiTypes.BOOL)
143-
def is_reach(self):
144-
# A weapon with a longer reach than the default has the reach property.
145-
return self.reach > 5
146-
147-
@property
148-
@extend_schema_field(serializers.ChoiceField(choices=['special', 'finesse', 'ammunition', 'light', 'heavy', 'thrown', 'loading', 'two-handed', 'versatile', 'reach']))
149-
def properties(self):
150-
properties = []
151-
152-
range_desc = "(range {}/{})".format(
153-
str(self.range),
154-
str(self.long_range))
155-
156-
versatile_desc = "({})".format(self.versatile_dice)
157-
158-
if self.is_net or self.is_lance:
159-
properties.append("special")
160-
if self.is_finesse:
161-
properties.append("finesse")
162-
if self.requires_ammunition:
163-
properties.append("ammuntion {}".format(range_desc))
164-
if self.is_light:
165-
properties.append("light")
166-
if self.is_heavy:
167-
properties.append("heavy")
168-
if self.is_thrown:
169-
properties.append("thrown {}".format(range_desc))
170-
if self.requires_loading:
171-
properties.append("loading")
172-
if self.is_two_handed:
173-
properties.append("two-handed")
174-
if self.is_versatile:
175-
properties.append("versatile {}".format(versatile_desc))
176-
if self.is_reach:
177-
properties.append("reach")
178-
179-
return properties
87+
return not self.is_simple

api_v2/serializers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from .item import ItemRaritySerializer
77
from .item import ItemSetSerializer
88
from .item import ItemCategorySerializer
9+
from .item import WeaponPropertySerializer
910

1011
from .background import BackgroundBenefitSerializer
1112
from .background import BackgroundSerializer

0 commit comments

Comments
 (0)