From 0ada03ebf6a879e0a77475d525141d1090bcd634 Mon Sep 17 00:00:00 2001 From: Supremist Date: Wed, 29 Oct 2025 20:58:42 +0200 Subject: Track bonuses and penalties from conditions --- character.py | 105 +++++++++++++++++++++++++++++++-------------------- game.py | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- items.py | 2 + main.py | 5 --- 4 files changed, 186 insertions(+), 46 deletions(-) diff --git a/character.py b/character.py index 75329ed..2a306f9 100644 --- a/character.py +++ b/character.py @@ -1,36 +1,10 @@ import re import json +import copy 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" -} +from items import Armor, Weapon +from game import LevelingPlan, Features, Modifier, Conditions class FeaturesPlan(LevelingPlan, dict): @@ -58,10 +32,9 @@ class FeaturesPlan(LevelingPlan, dict): 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}) + super().__init__({field: 10 for field in Features.abilities}) if data: self.update(data) if kwargs: @@ -123,6 +96,7 @@ class Character: abilities: Abilities features: dict armor: Armor + conditions: Conditions def __init__(self, data): self.data = data @@ -132,7 +106,9 @@ class Character: self.level = 1 self.abilities = Abilities() self.features = dict() - self.armor = Armor("Leather armor", "light", 1) + 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") @@ -141,25 +117,62 @@ class Character: def skill_ability(self, skill): if skill == "classDC": return self.data["keyability"] - return _skill_ability.get(skill) + 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): - return self.features.get(skill, 0) + self.level + bonus = self.features.get(skill, 0) + if bonus == 0: + return 0 + return bonus + 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) + 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_bonus = min(self.armor.dex_cap, self.abilities.modifier("dex")) + 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) - return 10 + dex_bonus + prof + self.armor.ac + 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 toJSON(self): methods = ["classDC", "max_hp", "ac"] obj = asdict(self) @@ -168,6 +181,18 @@ class Character: return json.dumps(obj, indent=2) +class Attack: + attacker = None + target = None + previous_attacks = 0 + distance = 0 + # def map(self, previous_attacks): + # factor = max(0, min(2, previous_attacks)) + # if self.agile: + # return factor * -4 + # return factor * -5 + + class CharacterPlan(LevelingPlan): def __init__(self, data) -> None: self.data = data diff --git a/game.py b/game.py index 75162ee..7adcc8a 100644 --- a/game.py +++ b/game.py @@ -1,4 +1,5 @@ import re +import copy from enum import Enum from dataclasses import dataclass from abc import ABC, abstractmethod @@ -71,7 +72,7 @@ class RollInfo: @classmethod def parse(cls, str_data): - # match strings like 2d6+8 (15) + # match strings like '2d6+8 (15)' match = re.fullmatch(r"^(\d+)d(\d+)\s*(\+\s*\d+)?\s*(\s*\(\s*\d+\s*\))?", str_data) if not match: @@ -91,3 +92,120 @@ class LevelingPlan(ABC): @abstractmethod def build(self, level: int): pass + + +class Features: + abilities = ["str", "dex", "int", "wis", "cha", "con"] + saves = ["reflex", "will", "fortitude"] + defences = ["unarmored", "light", "medium", "heavy"] + weapons = ["unarmed", "simple", "martial", "advanced"] + skill_ability = { + "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", + } + + @staticmethod + def all(): + res = set(Features.defences) | set(Features.weapons) + res |= set(Features.saves) + res |= Features.skill_ability.keys() + return list(res) + + @staticmethod + def expand(shortcuts: list[str]): + if "all" in shortcuts: + return Features.all() + res = set() + for shortcut in shortcuts: + for skill, ability in Features.skill_ability.items(): + if shortcut == ability: + res.add(skill) + for feature in ["saves", "defences", "weapons"]: + if shortcut == feature: + res |= set(getattr(Features, feature)) + return list(res) + + +class Modifier: + names = ["circumstance", "status", "item"] + + def __init__(self, circumstance=0, status=0, item=0, untyped=0) -> None: + self.untyped = untyped + self.bonuses = dict() + self.penalties = dict() + for name in Modifier.names: + val = locals()[name] + if val > 0: + self.bonuses[name] = val + elif val < 0: + self.penalties[name] = val + + def total(self): + res = self.untyped + for val in self.bonuses.values(): + res += val + for val in self.penalties.values(): + res += val + return res + + def __iadd__(self, other): + if isinstance(other, int): + self.untyped += other + return self + if not isinstance(other, Modifier): + raise NotImplementedError + + self.untyped += other.untyped + for name in (self.bonuses.keys() & other.bonuses.keys()): + self.bonuses[name] = max(self.bonuses.get(name, 0), + other.bonuses.get(name, 0)) + for name in (self.penalties.keys() & other.penalties.keys()): + self.penalties[name] = min(self.penalties.get(name, 0), + other.penalties.get(name, 0)) + return self + + def __add__(self, other): + res = copy.deepcopy(self) + res += other + return res + + __radd__ = __add__ + + +@dataclass +class Condition: + name: str + filter: list + modifier: Modifier + + +class Conditions(list): + def modifier(self, features, exclude = None): + exclude = set(exclude) if exclude is not None else set() + features = set(features) + res = Modifier() + for condition in self: + positive = set(condition.filter) & features + negative = set(condition.filter) & exclude + if positive and not negative: + res += condition.modifier + return res + diff --git a/items.py b/items.py index a26316c..f25b718 100644 --- a/items.py +++ b/items.py @@ -16,6 +16,7 @@ class Armor: class Weapon: name: str category: str + group: str main_die: int potency: int = 0 striking: int = 0 @@ -23,4 +24,5 @@ class Weapon: fatal: int = 0 deadly: int = 0 agile: bool = False + finesse: bool = False twin: bool = False diff --git a/main.py b/main.py index 22be527..c54de8a 100644 --- a/main.py +++ b/main.py @@ -21,11 +21,6 @@ class Weapon: self.twin = twin self.crit_effect = crit_effect - def get_map(self, number): - number = max(0, min(2, number)) - if self.agile: - return number * -4 - return number * -5 def damage(self, dmg_bonus, check=Roll.Success, number = 0): persistant_damage_factor = 3 + 1/3 -- cgit v1.2.3