from django.db import models import uuid from django.contrib.auth.models import AbstractUser class CustomUser(AbstractUser): """ Custom user model that extends AbstractUser, adding a UUID field for unique identification and linking to other tables. """ uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) def __str__(self): return self.username def check_requirements(character, requirements): for requirement in requirements: char_prop = getattr(character, requirement['property']) #print(char_prop, requirement['value'], requirement['condition']) if requirement['condition'] == "==": #print("==") if requirement['value'] != char_prop: return False if requirement['condition'] == "!=": #print("!=") if requirement['value'] == char_prop: return False if requirement['condition'] == "<=": #print("<=") if requirement['value'] < char_prop: return False if requirement['condition'] == ">=": #print(">=") if requirement['value'] > char_prop: return False return True def get_operations_for_hp_max(character, attr): ops = [] for feature in character.hp_features: if check_requirements(character, feature['feature_requirements']): feature_ops = feature['feature_data'].get('operations', []) for op in feature_ops: if op['attr'] == attr: ops.append(op) return ops def get_operations_for_attr(character, attr): ops = [] for feature in character.features.all(): feature_ops = feature.feature_data.get('operations', []) if check_requirements(character, feature.feature_requirements): for op in feature_ops: if op['attr'] == attr: ops.append(op) return ops def apply_operations(base, operations, character): """Apply a list of operations (add, multiply, set, etc) in order.""" value = base for op in operations: if op['operation'] == 'add': if isinstance(op['value'], int): value += op['value'] if isinstance(op['value'], str): value += getattr(character, op['value']) elif op['operation'] == 'subtract': if isinstance(op['value'], int): value -= op['value'] if isinstance(op['value'], str): value -= getattr(character, op['value']) elif op['operation'] == 'multiply': if isinstance(op['value'], int): value *= op['value'] if isinstance(op['value'], str): value *= getattr(character, op['value']) elif op['operation'] == 'divide': if isinstance(op['value'], int): value /= op['value'] if isinstance(op['value'], str): value /= getattr(character, op['value']) elif op['operation'] == 'set': if isinstance(op['value'], int): value = op['value'] if isinstance(op['value'], str): value = getattr(character, op['value']) return value # Character model based on character_template.json class Character(models.Model): owner = models.ForeignKey('CustomUser', on_delete=models.CASCADE, related_name='characters') name = models.CharField(max_length=100) level = models.IntegerField(default=0) race = models.ForeignKey('Package', related_name='race_characters', null=True, blank=True, on_delete=models.SET_NULL) subrace = models.ForeignKey('Package', related_name='subrace_characters', null=True, blank=True, on_delete=models.SET_NULL) char_class = models.ForeignKey('Package', related_name='class_characters', null=True, blank=True, on_delete=models.SET_NULL) subclass = models.ForeignKey('Package', related_name='subclass_characters', null=True, blank=True, on_delete=models.SET_NULL) background = models.ForeignKey('Package', related_name='background_characters', null=True, blank=True, on_delete=models.SET_NULL) alignment = models.CharField(max_length=50, blank=True) size = models.CharField(max_length=20, blank=True) age = models.IntegerField(default=0) gender = models.CharField(max_length=30, blank=True) height = models.CharField(max_length=20, blank=True) weight = models.IntegerField(default=0) deity = models.CharField(max_length=100, blank=True) # Relationships to packages and features packages = models.ManyToManyField('Package', blank=True, related_name='characters') features = models.ManyToManyField('Feature', blank=True, related_name='characters') # Base and current stats strength_base = models.IntegerField(default=0) dexterity_base = models.IntegerField(default=0) constitution_base = models.IntegerField(default=0) intelligence_base = models.IntegerField(default=0) wisdom_base = models.IntegerField(default=0) charisma_base = models.IntegerField(default=0) armor_base = models.IntegerField(default=0) inspiration = models.BooleanField(default=False) experience = models.IntegerField(default=0) # Proficiencies and languages proficiencies_armor = models.JSONField(default=list, blank=True) proficiencies_weapons = models.JSONField(default=list, blank=True) proficiencies_tools = models.JSONField(default=list, blank=True) languages = models.JSONField(default=list, blank=True) # Saving throws strength_save_prof = models.BooleanField(default=False) dexterity_save_prof = models.BooleanField(default=False) constitution_save_prof = models.BooleanField(default=False) intelligence_save_prof = models.BooleanField(default=False) wisdom_save_prof = models.BooleanField(default=False) charisma_save_prof = models.BooleanField(default=False) # HP and combat hp = models.IntegerField(default=0) #hp_max = models.IntegerField(default=0) hp_temp = models.IntegerField(default=0) hit_die = models.CharField(max_length=20, blank=True) #initiative = models.IntegerField(default=0) # Death saves deathsaves_successes = models.IntegerField(default=0) deathsaves_failures = models.IntegerField(default=0) # Resistances fire_resistance = models.BooleanField(default=False) poison_resistance = models.BooleanField(default=False) psychic_resistance = models.BooleanField(default=False) cold_resistance = models.BooleanField(default=False) thunder_resistance = models.BooleanField(default=False) acid_resistance = models.BooleanField(default=False) force_resistance = models.BooleanField(default=False) radiant_resistance = models.BooleanField(default=False) necrotic_resistance = models.BooleanField(default=False) bludgeoning_resistance = models.BooleanField(default=False) piercing_resistance = models.BooleanField(default=False) slashing_resistance = models.BooleanField(default=False) immunities = models.JSONField(default=list, blank=True) vulnerabilities = models.JSONField(default=list, blank=True) # Movement & vision speed_base = models.IntegerField(default=0) speed_type = models.JSONField(default=list, blank=True) darkvision = models.IntegerField(default=0) blindsight = models.IntegerField(default=0) tremorsense = models.IntegerField(default=0) truesight = models.IntegerField(default=0) # Spellcasting spellcasting_ability = models.CharField(max_length=50, blank=True) spell_save_dc = models.IntegerField(default=0) spell_attack_bonus = models.IntegerField(default=0) known_spells = models.JSONField(default=list, blank=True) prepared_spells = models.JSONField(default=list, blank=True) spell_slots = models.JSONField(default=dict, blank=True) cantrips = models.JSONField(default=list, blank=True) spellcasting_class = models.CharField(max_length=100, blank=True) # Exhaustion, conditions, notes, and attributes exhaustion_level = models.IntegerField(default=0) conditions_active = models.JSONField(default=list, blank=True) cp = models.IntegerField(default=0) sp = models.IntegerField(default=0) ep = models.IntegerField(default=0) gp = models.IntegerField(default=0) pp = models.IntegerField(default=0) equipment = models.JSONField(default=list, blank=True) traits = models.JSONField(default=list, blank=True) personality_traits = models.JSONField(default=list, blank=True) ideals = models.JSONField(default=list, blank=True) bonds = models.JSONField(default=list, blank=True) flaws = models.JSONField(default=list, blank=True) notes = models.TextField(blank=True) hp_features = models.JSONField(default=list, blank=True) custom_attributes = models.JSONField(default=dict, blank=True) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"{self.name} (Level {self.level})" @classmethod def from_db(cls, db, field_names, values): instance = super().from_db(db, field_names, values) # Here, do your consolidation logic: instance.feature_operations = instance.build_feature_operations_dict() return instance def build_feature_operations_dict(self): feature_dict = {} #Build HP for feature in self.hp_features: feature_requirements = feature['feature_requirements'] if check_requirements(self, feature_requirements): for operation in feature['feature_data'].get('operations', []): if 'requirements' in operation: if check_requirements(self, operation['requirements']): feature_dict.setdefault(operation['attr'], []).append(operation) else: feature_dict.setdefault(operation['attr'], []).append(operation) #Build features for feature in self.features.all(): feature_requirements = feature.feature_requirements if check_requirements(self, feature_requirements): for operation in feature.feature_data.get('operations', []): if 'requirements' in operation: if check_requirements(self, operation['requirements']): feature_dict.setdefault(operation['attr'], []).append(operation) else: feature_dict.setdefault(operation['attr'], []).append(operation) print(self.packages.all()) # Build race if self.race != None: for feature in self.race.features.all(): feature_requirements = feature.feature_requirements if check_requirements(self, feature_requirements): for operation in feature.feature_data.get('operations', []): if 'requirements' in operation: if check_requirements(self, operation['requirements']): feature_dict.setdefault(operation['attr'], []).append(operation) else: feature_dict.setdefault(operation['attr'], []).append(operation) for feature in feature_dict: print(feature) return feature_dict def stat_total(self, attr): base = getattr(self, f"{attr}_base") ops = self.feature_operations.get(attr, []) return apply_operations(base, ops, self) @property def proficiency(self): lvl = getattr(self, 'level') return 2 + (lvl-1) // 4 @property def strength(self): return self.stat_total('strength') @property def strength_modifier(self): return (getattr(self, 'strength')-10) // 2 @property def strength_save(self): sav = getattr(self, 'strength_modifier') if getattr(self, 'strength_save_prof'): return getattr(self, 'proficiency') + sav return sav @property def dexterity(self): return self.stat_total('dexterity') @property def dexterity_modifier(self): return (getattr(self, 'dexterity')-10) //2 @property def dexterity_save(self): sav = getattr(self, 'dexterity_modifier') if getattr(self, 'dexterity_save_prof'): return getattr(self, 'proficiency') + sav return sav @property def constitution(self): return self.stat_total('constitution') @property def constitution_modifier(self): return (getattr(self, 'constitution')-10) //2 @property def constitution_save(self): sav = getattr(self, 'constitution_modifier') if getattr(self, 'constitution_save_prof'): return getattr(self, 'proficiency') + sav return sav @property def intelligence(self): return self.stat_total('intelligence') @property def intelligence_modifier(self): return (getattr(self, 'intelligence')-10) //2 @property def intelligence_save(self): sav = getattr(self, 'intelligence_modifier') if getattr(self, 'intelligence_save_prof'): return getattr(self, 'proficiency') + sav return sav @property def wisdom(self): return self.stat_total('wisdom') @property def wisdom_modifier(self): return (getattr(self, 'wisdom')-10) //2 @property def wisdom_save(self): sav = getattr(self, 'wisdom_modifier') if getattr(self, 'wisdom_save_prof'): return getattr(self, 'proficiency') + sav return sav @property def charisma(self): return self.stat_total('charisma') @property def charisma_modifier(self): return (getattr(self, 'charisma')-10) //2 @property def charisma_save(self): sav = getattr(self, 'charisma_modifier') if getattr(self, 'charisma_save_prof'): return getattr(self, 'proficiency') + sav return sav @property def armor_class(self): return self.stat_total('armor_class') @property def hp_max(self): con = getattr(self, 'constitution_modifier') level = getattr(self, 'level') base = con * level ops = self.feature_operations.get('hp_max', []) print(ops, base) bonus = apply_operations(base, ops, self) return bonus @property def initiative(self): return 0 @property def athletics(self): ops = get_operations_for_attr(self, 'athletics') base = getattr(self, 'strength_modifier') bonus = apply_operations(0, ops, self) return base + bonus + getattr(self, 'proficiency') @property def athletics_passive(self): return getattr(self, 'athletics') + 10 @property def acrobatics(self): ops = get_operations_for_attr(self, 'acrobatics') base = getattr(self, 'dexterity_modifier') bonus = apply_operations(0, ops, self) return base + bonus @property def acrobatics_passive(self): return getattr(self, 'acrobatics') + 10 @property def sleight_of_hand(self): ops = get_operations_for_attr(self, 'sleight_of_hand') base = getattr(self, 'dexterity_modifier') bonus = apply_operations(0, ops, self) return base + bonus @property def sleight_of_hand_passive(self): return getattr(self, 'sleight_of_hand') + 10 @property def stealth(self): ops = get_operations_for_attr(self, 'stealth') base = getattr(self, 'dexterity_modifier') bonus = apply_operations(0, ops, self) return base + bonus @property def stealth_passive(self): return getattr(self, 'stealth') + 10 @property def arcana(self): ops = get_operations_for_attr(self, 'arcana') base = getattr(self, 'intelligence_modifier') bonus = apply_operations(0, ops, self) return base + bonus @property def arcana_passive(self): return getattr(self, 'arcana') + 10 @property def history(self): ops = get_operations_for_attr(self, 'history') base = getattr(self, 'intelligence_modifier') bonus = apply_operations(0, ops, self) return base + bonus @property def history_passive(self): return getattr(self, 'history') + 10 @property def investigation(self): ops = get_operations_for_attr(self, 'investigation') base = getattr(self, 'intelligence_modifier') bonus = apply_operations(0, ops, self) return base + bonus @property def investigation_passive(self): return getattr(self, 'investigation') + 10 @property def nature(self): ops = get_operations_for_attr(self, 'nature') base = getattr(self, 'intelligence_modifier') bonus = apply_operations(0, ops, self) return base + bonus @property def nature_passive(self): return getattr(self, 'nature') + 10 @property def religion(self): ops = get_operations_for_attr(self, 'religion') base = getattr(self, 'intelligence_modifier') bonus = apply_operations(0, ops, self) return base + bonus @property def religion_passive(self): return getattr(self, 'religion') + 10 @property def animal_handling(self): ops = get_operations_for_attr(self, 'animal_handling') base = getattr(self, 'wisdom_modifier') bonus = apply_operations(0, ops, self) return base + bonus @property def animal_handling_passive(self): return getattr(self, 'animal_handling') + 10 @property def insight(self): ops = get_operations_for_attr(self, 'insight') base = getattr(self, 'wisdom_modifier') bonus = apply_operations(0, ops, self) return base + bonus @property def insight_passive(self): return getattr(self, 'insight') + 10 @property def medicine(self): ops = get_operations_for_attr(self, 'medicine') base = getattr(self, 'wisdom_modifier') bonus = apply_operations(0, ops, self) return base + bonus @property def medicine_passive(self): return getattr(self, 'medicine') + 10 @property def perception(self): ops = get_operations_for_attr(self, 'perception') base = getattr(self, 'wisdom_modifier') bonus = apply_operations(0, ops, self) return base + bonus @property def perception_passive(self): return getattr(self, 'perception') + 10 @property def survival(self): ops = get_operations_for_attr(self, 'survival') base = getattr(self, 'wisdom_modifier') bonus = apply_operations(0, ops, self) return base + bonus @property def survival_passive(self): return getattr(self, 'survival') + 10 @property def deception(self): ops = get_operations_for_attr(self, 'deception') base = getattr(self, 'charisma_modifier') bonus = apply_operations(0, ops, self) return base + bonus @property def deception_passive(self): return getattr(self, 'deception') + 10 @property def intimidation(self): ops = get_operations_for_attr(self, 'intimidation') base = getattr(self, 'charisma_modifier') bonus = apply_operations(0, ops, self) return base + bonus @property def intimidation_passive(self): return getattr(self, 'intimidation') + 10 @property def performance(self): ops = get_operations_for_attr(self, 'performance') base = getattr(self, 'charisma_modifier') bonus = apply_operations(0, ops, self) return base + bonus @property def performance_passive(self): return getattr(self, 'performance') + 10 @property def persuasion(self): ops = get_operations_for_attr(self, 'persuasion') base = getattr(self, 'charisma_modifier') bonus = apply_operations(0, ops, self) return base + bonus @property def persuasion_passive(self): return getattr(self, 'persuasion') + 10 # Feature model based on feature_template.json class Feature(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) feature_name = models.CharField(max_length=200) feature_description = models.TextField(blank=True) feature_requirements = models.JSONField(default=list, blank=True) #feature requirements requires 3 keys, {"property", "value", "condition"} feature_data = models.JSONField(default=dict, blank=True) #feature_data holds a value of {"operations", "sources"} # ----> operations is a list of dictionaries that have to have 6 values # ----> {"attr", "value", "operation", "limits", "operation_requirements", "priority"} def __str__(self): return self.feature_name # Package <-> Feature through model for priorities class PackageFeature(models.Model): package = models.ForeignKey('Package', on_delete=models.CASCADE) feature = models.ForeignKey('Feature', on_delete=models.CASCADE) priority = models.IntegerField(default=0) requirements_override = models.JSONField(blank=True, null=True) class Meta: unique_together = ('package', 'feature') ordering = ['priority'] def __str__(self): return f"{self.package} - {self.feature} (priority {self.priority})" # Package model based on package_template.json class Package(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) package_name = models.CharField(max_length=200) package_description = models.TextField(blank=True) package_type = models.CharField(max_length=100, blank=True) package_doc_md = models.TextField(blank=True) features = models.ManyToManyField('Feature', through='PackageFeature', blank=True, related_name='packages') def __str__(self): return self.package_name # Create your models here. class Pin(models.Model): label = models.CharField(max_length=100) url = models.URLField(max_length=300) x = models.FloatField(help_text="X position as percentage (0-100)") y = models.FloatField(help_text="Y position as percentage (0-100)") pin_type = models.CharField(max_length=100, default="general") def as_dict(self): return { "label": self.label, "url": self.url, "x": self.x, "y": self.y, "pin_type": self.pin_type, } def __str__(self): return f"{self.label} ({self.x:.2f}%, {self.y:.2f}%)"