import re import json from dataclasses import dataclass, asdict from items import Armor from game import LevelingPlan _skill_ability = { # "classDC" "perception": "wis", "fortitude": "con", "reflex": "dex", "will": "wis", "acrobatics": "dex", "arcana": "int", "athletics": "str", "crafting": "int", "deception": "cha", "diplomacy": "cha", "intimidation": "cha", "medicine": "wis", "nature": "wis", "occultism": "int", "performance": "cha", "religion": "wis", "society": "int", "stealth": "dex", "survival": "wis", "thievery": "dex", "lore1": "int", "lore2": "int" } class FeaturesPlan(LevelingPlan, dict): @classmethod def load(cls, name): with open(f"leveling/{name}.json", "r", encoding="utf8") as js_file: return cls({int(k):v for k,v in json.load(js_file).items()}) def build(self, level: int): res = dict() # should be 0, but just in case of negative levels for lvl in range(-5, level+1): res.update(self.get(lvl, dict())) return res def merge(self, other): for lvl, other_val in other.items(): if lvl in self: val = self[lvl] for key in (val.keys() & other_val.keys()): assert other_val[key] == val[key] val.update(other_val) else: self[lvl] = other_val class Abilities(dict): field_names = ["str", "dex", "int", "wis", "cha", "con"] def __init__(self, data=None, **kwargs): super().__init__({field: 10 for field in Abilities.field_names}) if data: self.update(data) if kwargs: self.update(kwargs) @staticmethod def ability_boost_factor(value): if value < 18: return 2 return 1 @staticmethod def ability_modifier(value): return (value - 10) // 2 def apply_change(self, ability_name, step_direction=1): ability = self[ability_name] ability += step_direction*Abilities.ability_boost_factor(ability) self[ability_name] = ability def apply_changes(self, changes, step_direction=1): for change in changes: self.apply_change(change.lower(), step_direction) def modifier(self, ability_name): return Abilities.ability_modifier(self[ability_name]) class AbilitiesPlan(LevelingPlan): def __init__(self, data) -> None: super().__init__() boosts = data.pop("mapLevelledBoosts") self.levelBoosts = { int(level): values for level, values in boosts.items() } self.initialFlaws = dict() for flaw in ["ancestryFlaws"]: self.initialFlaws[flaw] = data.pop(flaw) self.initialBoosts = data def build(self, level: int): res = Abilities() for flaw in self.initialFlaws.values(): res.apply_changes(flaw, -1) if level <= 0: return res for boost in self.initialBoosts.values(): res.apply_changes(boost) for lvl, boost in self.levelBoosts.items(): if lvl <= level: res.apply_changes(boost) return res @dataclass class Character: data: dict hp_bonus: int hp_per_level: int speed: int level: int abilities: Abilities features: dict armor: Armor def __init__(self, data): self.data = data self.hp_bonus = 10 self.hp_per_level = 8 self.speed = 25 self.level = 1 self.abilities = Abilities() self.features = dict() self.armor = Armor("Leather armor", "light", 1) def max_hp(self): per_level = self.hp_per_level + self.abilities.modifier("con") return self.hp_bonus + per_level * self.level def skill_ability(self, skill): if skill == "classDC": return self.data["keyability"] return _skill_ability.get(skill) def proficiency_bonus(self, skill): return self.features.get(skill, 0) + self.level def proficiency(self, skill): # TODO + item bonuses ability = self.skill_ability(skill) ability = self.abilities.modifier(ability) if ability else 0 return ability + self.proficiency_bonus(skill) def ac(self): dex_bonus = min(self.armor.dex_cap, self.abilities.modifier("dex")) prof = self.proficiency(self.armor.category) return 10 + dex_bonus + prof + self.armor.ac def classDC(self): return 10 + self.proficiency("classDC") def toJSON(self): methods = ["classDC", "max_hp", "ac"] obj = asdict(self) for method in methods: obj[method] = getattr(self, method)() return json.dumps(obj, indent=2) class CharacterPlan(LevelingPlan): def __init__(self, data) -> None: self.data = data (self._class, self.subclass) = self.parse_subclass(data["class"]) data["class"] = self._class data["subclass"] = self.subclass abilities = data["abilities"]["breakdown"] self.attributes = data["attributes"] self.cleanup_build_data(data) self.abilities = AbilitiesPlan(abilities) self.features = FeaturesPlan() self.features.merge(FeaturesPlan.load(self._class)) self.features.merge(FeaturesPlan.load("fundamental_runes")) # TODO add skill increases to calculate proficiencies @staticmethod def cleanup_build_data(data): duplicates = ["acTotal", "level", "xp", "abilities", "attributes", "proficiencies"] spells = ["rituals", "spellCasters", "focus", "focusPoints"] equipment = ["money", "armor", "weapons", "equipment", "equipmentContainers"] blacklist = duplicates blacklist.extend(spells) blacklist.extend(equipment) for key in blacklist: del data[key] @staticmethod def parse_subclass(s: str): match = re.fullmatch(r'\s*([^(]+)\s*\(\s*([^)]+)\s*\)\s*', s) if match: return match.group(1).strip().lower(), match.group(2).strip().lower() return s, None def build(self, level: int): res = Character(self.data) res.level = level res.speed = self.attributes["speed"] + self.attributes["speedBonus"] res.hp_bonus = self.attributes["bonushp"] + self.attributes["ancestryhp"] res.hp_per_level = \ self.attributes["classhp"] + self.attributes["bonushpPerLevel"] res.abilities = self.abilities.build(level) res.features = self.features.build(level) return res