summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--character.py105
-rw-r--r--game.py120
-rw-r--r--items.py2
-rw-r--r--main.py5
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