Add models

This commit is contained in:
Gabriel Augendre 2022-10-29 00:32:18 +02:00
parent 6ea9e6a997
commit e61e70e465
21 changed files with 930 additions and 1 deletions

View file

@ -16,7 +16,7 @@
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="jdk" jdkName="Python 3.10 (charasheet)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">

4
.idea/misc.xml Normal file
View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (charasheet)" project-jdk-type="Python SDK" />
</project>

View file

122
src/character/admin.py Normal file
View file

@ -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"]

6
src/character/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class CharacterConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "character"

View file

@ -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"
),
),
]

View file

@ -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",
),
),
]

View file

@ -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),
),
]

View file

@ -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,
),
]

View file

View file

@ -0,0 +1 @@
0004_remove_character_mana_max_and_more

View file

@ -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",
]

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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)

View file

View file

@ -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

1
src/character/views.py Normal file
View file

@ -0,0 +1 @@
# Create your views here.

View file

@ -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

View file

@ -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,)