import re import json from dataclasses import dataclass, asdict from items import Armor, Weapon from game import LevelingPlan, Features, Modifier, Conditions 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): def __init__(self, data=None, **kwargs): super().__init__({field: 10 for field in Features.abilities}) 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 conditions: Conditions 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("Unarmored", "light", 0) self.weapons = [Weapon("Fist", "unarmed", "brawling", 4)] self.conditions = Conditions() 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 Features.skill_ability.get(skill) def ability_modifier(self, ability): return self.conditions.modifier([ability]) + self.abilities.modifier(ability) def proficiency_bonus(self, skill): bonus = self.features.get(skill, 0) if bonus == 0: return 0 return bonus + self.level def proficiency(self, skill): ability_name = self.skill_ability(skill) res = Modifier() if ability_name: res += self.ability_modifier(ability_name) res += self.proficiency_bonus(skill) + self.conditions.modifier([skill]) return res.total() def ac(self): dex_mod = self.conditions.modifier(["dex"], exclude=["ac"]) dex_mod += self.abilities.modifier("dex") dex_bonus = min(self.armor.dex_cap, dex_mod.total()) prof = self.proficiency(self.armor.category) armor = Modifier(item=self.armor.ac+self.armor.potency) armor += self.conditions.modifier(["ac"]) return (10 + dex_bonus + prof + armor).total() def classDC(self): return 10 + self.proficiency("classDC") def update_fundamental_runes(self): try: self.armor.potency = self.features["armor potency"] self.armor.resilient = self.features["resilient"] except KeyError: pass try: for weapon in self.weapons: weapon.potency = self.features["weapon potency"] weapon.striking = self.features["striking"] except KeyError: pass def attack_bonus(self, weapon: Weapon, strike_type="melee"): dex = self.ability_modifier("dex").total() mod = self.ability_modifier("str").total() if strike_type == "melee": if weapon.finesse: mod = max(mod, dex) elif strike_type == "ranged": mod = dex prof = max(self.features.get(weapon.category, 0), self.features.get(weapon.name, 0)) return mod + prof + weapon.potency def attack_damage_bonus(self, weapon: Weapon, strike_type="melee"): str_mod = self.ability_modifier("str") dmg_mod = self.conditions.modifier("damage") if strike_type == "melee": if "thief" in self.features and weapon.finesse: return (self.ability_modifier("dex")+ dmg_mod).total() return (str_mod + dmg_mod).total() elif strike_type == "ranged": if weapon.thrown > 0: return (str_mod + dmg_mod).total() if weapon.propulsive: return (str_mod.total()//2 + dmg_mod).total() return dmg_mod.total() 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