diff --git a/.idea/charasheet.iml b/.idea/charasheet.iml
index 03d83bd..f73b0e7 100644
--- a/.idea/charasheet.iml
+++ b/.idea/charasheet.iml
@@ -16,7 +16,7 @@
-
+
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..7502240
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/character/__init__.py b/src/character/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/character/admin.py b/src/character/admin.py
new file mode 100644
index 0000000..fa52175
--- /dev/null
+++ b/src/character/admin.py
@@ -0,0 +1,122 @@
+from django.contrib import admin
+
+from character import models
+
+
+@admin.register(models.Capability)
+class CapabilityAdmin(admin.ModelAdmin):
+ list_display = ["name", "path", "rank", "limited", "spell"]
+ list_filter = ["path", "path__profile", "path__race", "rank", "limited", "spell"]
+ search_fields = ["name", "description"]
+
+
+@admin.register(models.Path)
+class PathAdmin(admin.ModelAdmin):
+ list_display = ["name", "category", "related_to"]
+ list_filter = ["category"]
+ search_fields = ["name"]
+ fieldsets = [
+ (None, {"fields": ["name"]}),
+ ("Related to", {"fields": ["category", ("profile", "race")]}),
+ ("Notes", {"fields": ["notes"]}),
+ ]
+
+ def related_to(self, instance: models.Path) -> str:
+ category = models.Path.Category(instance.category)
+ if category == models.Path.Category.PROFILE:
+ return str(instance.profile)
+ elif category == models.Path.Category.RACE:
+ return str(instance.race)
+ else:
+ return ""
+
+
+@admin.register(models.RacialCapability)
+class RacialCapabilityAdmin(admin.ModelAdmin):
+ list_display = ["name", "race"]
+ list_filter = ["race"]
+ search_fields = ["name", "description"]
+
+
+@admin.register(models.Profile)
+class ProfileAdmin(admin.ModelAdmin):
+ list_display = ["name", "life_dice", "magical_strength"]
+ list_filter = ["life_dice", "magical_strength"]
+ search_fields = ["name"]
+
+
+class RacialCapabilityInline(admin.TabularInline):
+ model = models.RacialCapability
+ extra = 0
+
+
+@admin.register(models.Race)
+class RaceAdmin(admin.ModelAdmin):
+ list_display = ["name"]
+ search_fields = ["name"]
+ inlines = [RacialCapabilityInline]
+
+
+@admin.register(models.Character)
+class CharacterAdmin(admin.ModelAdmin):
+ list_display = ["name", "player", "race", "profile", "level"]
+ list_filter = ["race", "profile"]
+ search_fields = ["name", "notes"]
+
+ fieldsets = [
+ (
+ "Character",
+ {"fields": ["name", "player", "profile", "level", "race"]},
+ ),
+ ("Appearance", {"fields": ["gender", "age", "height", "weight"]}),
+ (
+ "Abilities",
+ {
+ "fields": [
+ ("value_strength", "modifier_strength"),
+ ("value_dexterity", "modifier_dexterity"),
+ ("value_constitution", "modifier_constitution"),
+ ("value_intelligence", "modifier_intelligence"),
+ ("value_wisdom", "modifier_wisdom"),
+ ("value_charisma", "modifier_charisma"),
+ ]
+ },
+ ),
+ (
+ "Fight",
+ {"fields": ["initiative", "attack_melee", "attack_range", "attack_magic"]},
+ ),
+ ("Health", {"fields": ["health_max", "health_remaining"]}),
+ ("Defense", {"fields": ["armor", "shield", "defense_misc", "defense"]}),
+ ("Weapons & equipment", {"fields": ["weapons", "equipment"]}),
+ ("Racial", {"fields": ["racial_capability"]}),
+ ("Capabilities", {"fields": ["capabilities"]}),
+ ("Luck", {"fields": ["luck_points_max", "luck_points_remaining"]}),
+ ("Mana", {"fields": ["mana_max", "mana_consumed", "mana_remaining"]}),
+ ("Notes", {"fields": ["notes"]}),
+ ]
+ readonly_fields = [
+ "modifier_strength",
+ "modifier_dexterity",
+ "modifier_constitution",
+ "modifier_intelligence",
+ "modifier_wisdom",
+ "modifier_charisma",
+ "initiative",
+ "attack_melee",
+ "attack_range",
+ "attack_magic",
+ "defense",
+ "mana_max",
+ "mana_remaining",
+ ]
+ filter_horizontal = [
+ "capabilities",
+ "weapons",
+ ]
+
+
+@admin.register(models.Weapon)
+class WeaponAdmin(admin.ModelAdmin):
+ list_display = ["name", "damage"]
+ search_fields = ["name", "special", "damage"]
diff --git a/src/character/apps.py b/src/character/apps.py
new file mode 100644
index 0000000..bebdfd5
--- /dev/null
+++ b/src/character/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class CharacterConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "character"
diff --git a/src/character/migrations/0001_initial.py b/src/character/migrations/0001_initial.py
new file mode 100644
index 0000000..b7995d9
--- /dev/null
+++ b/src/character/migrations/0001_initial.py
@@ -0,0 +1,376 @@
+# Generated by Django 4.1.2 on 2022-10-28 21:52
+
+import django.core.validators
+import django.db.models.deletion
+import django.db.models.functions.text
+import django_extensions.db.fields
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="Capability",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("name", models.CharField(max_length=100, unique=True)),
+ (
+ "created",
+ django_extensions.db.fields.CreationDateTimeField(
+ auto_now_add=True, verbose_name="created"
+ ),
+ ),
+ (
+ "modified",
+ django_extensions.db.fields.ModificationDateTimeField(
+ auto_now=True, verbose_name="modified"
+ ),
+ ),
+ (
+ "rank",
+ models.PositiveSmallIntegerField(
+ validators=[
+ django.core.validators.MinValueValidator(1),
+ django.core.validators.MaxValueValidator(5),
+ ]
+ ),
+ ),
+ ("limited", models.BooleanField(blank=True)),
+ ("spell", models.BooleanField(blank=True)),
+ ("description", models.TextField()),
+ ],
+ options={
+ "verbose_name_plural": "Capabilities",
+ },
+ ),
+ migrations.CreateModel(
+ name="Profile",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("name", models.CharField(max_length=100, unique=True)),
+ (
+ "created",
+ django_extensions.db.fields.CreationDateTimeField(
+ auto_now_add=True, verbose_name="created"
+ ),
+ ),
+ (
+ "modified",
+ django_extensions.db.fields.ModificationDateTimeField(
+ auto_now=True, verbose_name="modified"
+ ),
+ ),
+ (
+ "magical_strength",
+ models.CharField(
+ choices=[
+ ("NON", "None"),
+ ("INT", "Intelligence"),
+ ("WIS", "Wisdom"),
+ ("CHA", "Charisma"),
+ ],
+ default="NON",
+ max_length=3,
+ ),
+ ),
+ (
+ "life_dice",
+ models.PositiveSmallIntegerField(
+ choices=[
+ (4, "D4"),
+ (6, "D6"),
+ (8, "D8"),
+ (10, "D10"),
+ (12, "D12"),
+ (20, "D20"),
+ ]
+ ),
+ ),
+ ],
+ options={
+ "abstract": False,
+ },
+ ),
+ migrations.CreateModel(
+ name="Race",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("name", models.CharField(max_length=100, unique=True)),
+ (
+ "created",
+ django_extensions.db.fields.CreationDateTimeField(
+ auto_now_add=True, verbose_name="created"
+ ),
+ ),
+ (
+ "modified",
+ django_extensions.db.fields.ModificationDateTimeField(
+ auto_now=True, verbose_name="modified"
+ ),
+ ),
+ ],
+ options={
+ "abstract": False,
+ },
+ ),
+ migrations.CreateModel(
+ name="Weapon",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("name", models.CharField(max_length=100, unique=True)),
+ (
+ "created",
+ django_extensions.db.fields.CreationDateTimeField(
+ auto_now_add=True, verbose_name="created"
+ ),
+ ),
+ (
+ "modified",
+ django_extensions.db.fields.ModificationDateTimeField(
+ auto_now=True, verbose_name="modified"
+ ),
+ ),
+ ("damage", models.CharField(blank=True, max_length=50)),
+ ("special", models.TextField(blank=True)),
+ ],
+ options={
+ "abstract": False,
+ },
+ ),
+ migrations.CreateModel(
+ name="RacialCapability",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("name", models.CharField(max_length=100, unique=True)),
+ (
+ "created",
+ django_extensions.db.fields.CreationDateTimeField(
+ auto_now_add=True, verbose_name="created"
+ ),
+ ),
+ (
+ "modified",
+ django_extensions.db.fields.ModificationDateTimeField(
+ auto_now=True, verbose_name="modified"
+ ),
+ ),
+ ("description", models.TextField()),
+ (
+ "race",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE, to="character.race"
+ ),
+ ),
+ ],
+ options={
+ "verbose_name_plural": "Racial capabilities",
+ },
+ ),
+ migrations.CreateModel(
+ name="Path",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("name", models.CharField(max_length=100, unique=True)),
+ (
+ "created",
+ django_extensions.db.fields.CreationDateTimeField(
+ auto_now_add=True, verbose_name="created"
+ ),
+ ),
+ (
+ "modified",
+ django_extensions.db.fields.ModificationDateTimeField(
+ auto_now=True, verbose_name="modified"
+ ),
+ ),
+ (
+ "category",
+ models.CharField(
+ choices=[
+ ("profile", "Profile"),
+ ("race", "Race"),
+ ("prestige", "Prestige"),
+ ],
+ max_length=20,
+ ),
+ ),
+ ("notes", models.TextField()),
+ (
+ "profile",
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="paths",
+ to="character.profile",
+ ),
+ ),
+ (
+ "race",
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="paths",
+ to="character.race",
+ ),
+ ),
+ ],
+ options={
+ "abstract": False,
+ },
+ ),
+ migrations.CreateModel(
+ name="Character",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("name", models.CharField(max_length=100)),
+ ("level", models.PositiveSmallIntegerField()),
+ (
+ "gender",
+ models.CharField(
+ choices=[("M", "Male"), ("F", "Female"), ("O", "Other")],
+ default="O",
+ max_length=1,
+ ),
+ ),
+ ("age", models.PositiveSmallIntegerField()),
+ ("height", models.PositiveSmallIntegerField()),
+ ("weight", models.PositiveSmallIntegerField()),
+ ("value_strength", models.PositiveSmallIntegerField()),
+ ("value_dexterity", models.PositiveSmallIntegerField()),
+ ("value_constitution", models.PositiveSmallIntegerField()),
+ ("value_intelligence", models.PositiveSmallIntegerField()),
+ ("value_wisdom", models.PositiveSmallIntegerField()),
+ ("value_charisma", models.PositiveSmallIntegerField()),
+ ("health_max", models.PositiveSmallIntegerField()),
+ ("health_remaining", models.PositiveSmallIntegerField()),
+ ("armor", models.PositiveSmallIntegerField()),
+ ("shield", models.PositiveSmallIntegerField()),
+ ("defense_misc", models.SmallIntegerField()),
+ ("equipment", models.TextField()),
+ ("luck_points_max", models.PositiveSmallIntegerField()),
+ ("luck_points_remaining", models.PositiveSmallIntegerField()),
+ ("mana_max", models.PositiveSmallIntegerField()),
+ ("mana_remaining", models.PositiveSmallIntegerField()),
+ ("notes", models.TextField()),
+ ("capabilities", models.ManyToManyField(to="character.capability")),
+ (
+ "player",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="characters",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ (
+ "profile",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="characters",
+ to="character.profile",
+ ),
+ ),
+ (
+ "race",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="characters",
+ to="character.race",
+ ),
+ ),
+ (
+ "racial_capability",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="characters",
+ to="character.racialcapability",
+ ),
+ ),
+ ("weapons", models.ManyToManyField(to="character.weapon")),
+ ],
+ ),
+ migrations.AddField(
+ model_name="capability",
+ name="path",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE, to="character.path"
+ ),
+ ),
+ migrations.AddConstraint(
+ model_name="character",
+ constraint=models.UniqueConstraint(
+ django.db.models.functions.text.Lower("name"),
+ models.F("player"),
+ name="unique_character_player",
+ ),
+ ),
+ migrations.AddConstraint(
+ model_name="capability",
+ constraint=models.UniqueConstraint(
+ models.F("path"), models.F("rank"), name="unique_path_rank"
+ ),
+ ),
+ ]
diff --git a/src/character/migrations/0002_alter_character_capabilities_alter_character_weapons_and_more.py b/src/character/migrations/0002_alter_character_capabilities_alter_character_weapons_and_more.py
new file mode 100644
index 0000000..2d57df4
--- /dev/null
+++ b/src/character/migrations/0002_alter_character_capabilities_alter_character_weapons_and_more.py
@@ -0,0 +1,46 @@
+# Generated by Django 4.1.2 on 2022-10-28 21:55
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("character", "0001_initial"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="character",
+ name="capabilities",
+ field=models.ManyToManyField(blank=True, to="character.capability"),
+ ),
+ migrations.AlterField(
+ model_name="character",
+ name="weapons",
+ field=models.ManyToManyField(blank=True, to="character.weapon"),
+ ),
+ migrations.AlterField(
+ model_name="path",
+ name="profile",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="paths",
+ to="character.profile",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="path",
+ name="race",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="paths",
+ to="character.race",
+ ),
+ ),
+ ]
diff --git a/src/character/migrations/0003_alter_character_equipment_alter_character_notes_and_more.py b/src/character/migrations/0003_alter_character_equipment_alter_character_notes_and_more.py
new file mode 100644
index 0000000..804bab8
--- /dev/null
+++ b/src/character/migrations/0003_alter_character_equipment_alter_character_notes_and_more.py
@@ -0,0 +1,31 @@
+# Generated by Django 4.1.2 on 2022-10-28 21:56
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ (
+ "character",
+ "0002_alter_character_capabilities_alter_character_weapons_and_more",
+ ),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="character",
+ name="equipment",
+ field=models.TextField(blank=True),
+ ),
+ migrations.AlterField(
+ model_name="character",
+ name="notes",
+ field=models.TextField(blank=True),
+ ),
+ migrations.AlterField(
+ model_name="path",
+ name="notes",
+ field=models.TextField(blank=True),
+ ),
+ ]
diff --git a/src/character/migrations/0004_remove_character_mana_max_and_more.py b/src/character/migrations/0004_remove_character_mana_max_and_more.py
new file mode 100644
index 0000000..ba39866
--- /dev/null
+++ b/src/character/migrations/0004_remove_character_mana_max_and_more.py
@@ -0,0 +1,27 @@
+# Generated by Django 4.1.2 on 2022-10-28 21:58
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("character", "0003_alter_character_equipment_alter_character_notes_and_more"),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name="character",
+ name="mana_max",
+ ),
+ migrations.RemoveField(
+ model_name="character",
+ name="mana_remaining",
+ ),
+ migrations.AddField(
+ model_name="character",
+ name="mana_consumed",
+ field=models.PositiveSmallIntegerField(default=0),
+ preserve_default=False,
+ ),
+ ]
diff --git a/src/character/migrations/__init__.py b/src/character/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/character/migrations/max_migration.txt b/src/character/migrations/max_migration.txt
new file mode 100644
index 0000000..e063c8b
--- /dev/null
+++ b/src/character/migrations/max_migration.txt
@@ -0,0 +1 @@
+0004_remove_character_mana_max_and_more
diff --git a/src/character/models/__init__.py b/src/character/models/__init__.py
new file mode 100644
index 0000000..2dde838
--- /dev/null
+++ b/src/character/models/__init__.py
@@ -0,0 +1,13 @@
+from .capabilities import Capability, Path, RacialCapability
+from .character import Character, Profile, Race
+from .equipment import Weapon
+
+__all__ = [
+ "Capability",
+ "Path",
+ "RacialCapability",
+ "Character",
+ "Profile",
+ "Race",
+ "Weapon",
+]
diff --git a/src/character/models/capabilities.py b/src/character/models/capabilities.py
new file mode 100644
index 0000000..5174beb
--- /dev/null
+++ b/src/character/models/capabilities.py
@@ -0,0 +1,52 @@
+from django.core.validators import MaxValueValidator, MinValueValidator
+from django.db import models
+from django_extensions.db.models import TimeStampedModel
+
+from common.models import UniquelyNamedModel
+
+
+class Path(UniquelyNamedModel, TimeStampedModel, models.Model):
+ profile = models.ForeignKey(
+ "character.Profile",
+ on_delete=models.CASCADE,
+ related_name="paths",
+ blank=True,
+ null=True,
+ )
+ race = models.ForeignKey(
+ "character.Race",
+ on_delete=models.CASCADE,
+ related_name="paths",
+ blank=True,
+ null=True,
+ )
+
+ class Category(models.TextChoices):
+ PROFILE = "profile", "Profile"
+ RACE = "race", "Race"
+ PRESTIGE = "prestige", "Prestige"
+
+ category = models.CharField(max_length=20, choices=Category.choices)
+ notes = models.TextField(blank=True)
+
+
+class Capability(UniquelyNamedModel, TimeStampedModel, models.Model):
+ path = models.ForeignKey("character.Path", on_delete=models.CASCADE)
+ rank = models.PositiveSmallIntegerField(
+ validators=[MinValueValidator(1), MaxValueValidator(5)]
+ )
+ limited = models.BooleanField(blank=True, null=False)
+ spell = models.BooleanField(blank=True, null=False)
+ description = models.TextField()
+
+ class Meta:
+ constraints = [models.UniqueConstraint("path", "rank", name="unique_path_rank")]
+ verbose_name_plural = "Capabilities"
+
+
+class RacialCapability(UniquelyNamedModel, TimeStampedModel, models.Model):
+ race = models.ForeignKey("character.Race", on_delete=models.CASCADE)
+ description = models.TextField()
+
+ class Meta:
+ verbose_name_plural = "Racial capabilities"
diff --git a/src/character/models/character.py b/src/character/models/character.py
new file mode 100644
index 0000000..0f2179d
--- /dev/null
+++ b/src/character/models/character.py
@@ -0,0 +1,176 @@
+from django.db import models
+from django.db.models.functions import Lower
+from django_extensions.db.models import TimeStampedModel
+
+from character.models.dice import Dice
+from common.models import UniquelyNamedModel
+
+
+class Profile(UniquelyNamedModel, TimeStampedModel, models.Model):
+ class MagicalStrength(models.TextChoices):
+ NONE = "NON", "None"
+ INTELLIGENCE = "INT", "Intelligence"
+ WISDOM = "WIS", "Wisdom"
+ CHARISMA = "CHA", "Charisma"
+
+ magical_strength = models.CharField(
+ max_length=3, choices=MagicalStrength.choices, default=MagicalStrength.NONE
+ )
+ life_dice = models.PositiveSmallIntegerField(choices=Dice.choices)
+
+
+class Race(UniquelyNamedModel, TimeStampedModel, models.Model):
+ pass
+
+
+def modifier(value: int) -> int:
+ if 1 < value < 10:
+ value -= 1
+ value -= 10
+ return int(value / 2)
+
+
+class CharacterManager(models.Manager):
+ def get_by_natural_key(self, name: str, player_id: int):
+ return self.get(name=name, player_id=player_id)
+
+
+class Character(models.Model):
+ class Gender(models.TextChoices):
+ MALE = "M", "Male"
+ FEMALE = "F", "Female"
+ OTHER = "O", "Other"
+
+ name = models.CharField(max_length=100)
+ player = models.ForeignKey(
+ "common.User", on_delete=models.CASCADE, related_name="characters"
+ )
+
+ race = models.ForeignKey(
+ "character.Race",
+ on_delete=models.PROTECT,
+ related_name="characters",
+ )
+ profile = models.ForeignKey(
+ "character.Profile",
+ on_delete=models.PROTECT,
+ related_name="characters",
+ )
+ level = models.PositiveSmallIntegerField()
+
+ gender = models.CharField(
+ max_length=1, choices=Gender.choices, default=Gender.OTHER
+ )
+ age = models.PositiveSmallIntegerField()
+ height = models.PositiveSmallIntegerField()
+ weight = models.PositiveSmallIntegerField()
+
+ value_strength = models.PositiveSmallIntegerField()
+ value_dexterity = models.PositiveSmallIntegerField()
+ value_constitution = models.PositiveSmallIntegerField()
+ value_intelligence = models.PositiveSmallIntegerField()
+ value_wisdom = models.PositiveSmallIntegerField()
+ value_charisma = models.PositiveSmallIntegerField()
+
+ health_max = models.PositiveSmallIntegerField()
+ health_remaining = models.PositiveSmallIntegerField()
+
+ racial_capability = models.ForeignKey(
+ "character.RacialCapability",
+ on_delete=models.PROTECT,
+ related_name="characters",
+ )
+
+ weapons = models.ManyToManyField("character.Weapon", blank=True)
+
+ armor = models.PositiveSmallIntegerField()
+ shield = models.PositiveSmallIntegerField()
+ defense_misc = models.SmallIntegerField()
+
+ capabilities = models.ManyToManyField("character.Capability", blank=True)
+
+ equipment = models.TextField(blank=True)
+ luck_points_max = models.PositiveSmallIntegerField()
+ luck_points_remaining = models.PositiveSmallIntegerField()
+
+ mana_consumed = models.PositiveSmallIntegerField(default=0)
+
+ notes = models.TextField(blank=True)
+
+ objects = CharacterManager()
+
+ class Meta:
+ constraints = [
+ models.UniqueConstraint(
+ Lower("name"), "player", name="unique_character_player"
+ )
+ ]
+
+ def __str__(self):
+ return self.name
+
+ @property
+ def natural_key(self):
+ return (self.name, self.player_id)
+
+ @property
+ def modifier_strength(self) -> int:
+ return modifier(self.value_strength)
+
+ @property
+ def modifier_dexterity(self) -> int:
+ return modifier(self.value_dexterity)
+
+ @property
+ def modifier_constitution(self) -> int:
+ return modifier(self.value_constitution)
+
+ @property
+ def modifier_intelligence(self) -> int:
+ return modifier(self.value_intelligence)
+
+ @property
+ def modifier_wisdom(self) -> int:
+ return modifier(self.value_wisdom)
+
+ @property
+ def modifier_charisma(self) -> int:
+ return modifier(self.value_charisma)
+
+ @property
+ def initiative(self) -> int:
+ return self.value_dexterity
+
+ @property
+ def attack_melee(self) -> int:
+ return self.level + self.modifier_strength
+
+ @property
+ def attack_range(self) -> int:
+ return self.level + self.modifier_dexterity
+
+ @property
+ def attack_magic(self) -> int:
+ modifier_map = {
+ Profile.MagicalStrength.INTELLIGENCE: self.modifier_intelligence,
+ Profile.MagicalStrength.WISDOM: self.modifier_wisdom,
+ Profile.MagicalStrength.CHARISMA: self.modifier_charisma,
+ }
+
+ return self.level + modifier_map.get(
+ Profile.MagicalStrength(self.profile.magical_strength)
+ )
+
+ @property
+ def defense(self) -> int:
+ return (
+ 10 + self.armor + self.shield + self.modifier_dexterity + self.defense_misc
+ )
+
+ @property
+ def mana_max(self) -> int:
+ return 2 * self.level + self.modifier_intelligence
+
+ @property
+ def mana_remaining(self) -> int:
+ return self.mana_max - self.mana_consumed
diff --git a/src/character/models/dice.py b/src/character/models/dice.py
new file mode 100644
index 0000000..98bbe34
--- /dev/null
+++ b/src/character/models/dice.py
@@ -0,0 +1,10 @@
+from django.db import models
+
+
+class Dice(models.IntegerChoices):
+ D4 = 4
+ D6 = 6
+ D8 = 8
+ D10 = 10
+ D12 = 12
+ D20 = 20
diff --git a/src/character/models/equipment.py b/src/character/models/equipment.py
new file mode 100644
index 0000000..c471597
--- /dev/null
+++ b/src/character/models/equipment.py
@@ -0,0 +1,9 @@
+from django.db import models
+from django_extensions.db.models import TimeStampedModel
+
+from common.models import UniquelyNamedModel
+
+
+class Weapon(UniquelyNamedModel, TimeStampedModel, models.Model):
+ damage = models.CharField(max_length=50, blank=True)
+ special = models.TextField(blank=True)
diff --git a/src/character/tests/__init__.py b/src/character/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/character/tests/test_modifier.py b/src/character/tests/test_modifier.py
new file mode 100644
index 0000000..26b575d
--- /dev/null
+++ b/src/character/tests/test_modifier.py
@@ -0,0 +1,33 @@
+import pytest
+
+from character.models import modifier
+
+
+@pytest.mark.parametrize(
+ "value,expected",
+ [
+ (1, -4),
+ (2, -4),
+ (3, -4),
+ (4, -3),
+ (5, -3),
+ (6, -2),
+ (7, -2),
+ (8, -1),
+ (9, -1),
+ (10, 0),
+ (11, 0),
+ (12, 1),
+ (13, 1),
+ (14, 2),
+ (15, 2),
+ (16, 3),
+ (17, 3),
+ (18, 4),
+ (19, 4),
+ (20, 5),
+ (21, 5),
+ ],
+)
+def test_modifier_values(value, expected):
+ assert modifier(value) == expected
diff --git a/src/character/views.py b/src/character/views.py
new file mode 100644
index 0000000..60f00ef
--- /dev/null
+++ b/src/character/views.py
@@ -0,0 +1 @@
+# Create your views here.
diff --git a/src/charasheet/settings.py b/src/charasheet/settings.py
index 462a4be..75bc31d 100644
--- a/src/charasheet/settings.py
+++ b/src/charasheet/settings.py
@@ -60,6 +60,7 @@ if DEBUG_TOOLBAR:
CUSTOM_APPS = [
"whitenoise.runserver_nostatic", # should be first
"common",
+ "character",
]
INSTALLED_APPS = CUSTOM_APPS + DJANGO_APPS + EXTERNAL_APPS
diff --git a/src/common/models.py b/src/common/models.py
index dab8651..dc615e5 100644
--- a/src/common/models.py
+++ b/src/common/models.py
@@ -1,7 +1,28 @@
from django.contrib.auth.models import AbstractUser
+from django.db import models
class User(AbstractUser):
"""Default custom user model for My Awesome Project."""
pass
+
+
+class UniquelyNamedModelManager(models.Manager):
+ def get_by_natural_key(self, name: str):
+ return self.get(name=name)
+
+
+class UniquelyNamedModel(models.Model):
+ name = models.CharField(max_length=100, blank=False, null=False, unique=True)
+ objects = UniquelyNamedModelManager()
+
+ class Meta:
+ abstract = True
+
+ def __str__(self):
+ return self.name
+
+ @property
+ def natural_key(self):
+ return (self.name,)