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 --- game.py | 120 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 119 insertions(+), 1 deletion(-) (limited to 'game.py') 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 + -- cgit v1.2.3