diff options
| author | Supremist <sergkarv@gmail.com> | 2025-10-29 22:58:37 +0200 |
|---|---|---|
| committer | Supremist <sergkarv@gmail.com> | 2025-10-29 22:58:37 +0200 |
| commit | 9fa4cfbe2f948f4067ca1572bbc5dbfab678e7da (patch) | |
| tree | 590bb32b5a11dd2f3b5f9a0999d5e809ad1f3749 | |
| parent | 0ada03ebf6a879e0a77475d525141d1090bcd634 (diff) | |
| download | pf2e_calc-9fa4cfbe2f948f4067ca1572bbc5dbfab678e7da.tar.xz pf2e_calc-9fa4cfbe2f948f4067ca1572bbc5dbfab678e7da.zip | |
Add Attack action
| -rw-r--r-- | actions.py | 75 | ||||
| -rw-r--r-- | character.py | 29 | ||||
| -rw-r--r-- | creature.py | 6 | ||||
| -rw-r--r-- | items.py | 5 | ||||
| -rw-r--r-- | main.py | 99 |
5 files changed, 118 insertions, 96 deletions
diff --git a/actions.py b/actions.py new file mode 100644 index 0000000..f3cd39c --- /dev/null +++ b/actions.py @@ -0,0 +1,75 @@ +from typing import Any + +from dataclasses import dataclass +from items import Weapon +from game import d, Roll +from character import Character + + +@dataclass +class Attack: + attacker: Character + target: Any + weapon: Weapon + strike_type="melee" + previous_attacks: int = 0 + distance: int = 0 + + def bonus(self): + return self.attacker.attack_bonus(self.weapon, self.strike_type) + self.map() + + def roll(self): + ac = self.target.ac() + return d(20).apply(Roll.check, ac, self.bonus()) + + def strike(self): + def strike_dmg(check): + return self.damage(check) + return self.roll().apply(strike_dmg) + + def damage(self, check=Roll.Success): + persistant_damage_factor = 3 + 1/3 + wp = self.weapon + dmg_bonus = self.attacker.attack_damage_bonus(wp, self.strike_type) + if wp.backstabber: + dmg_bonus += 1 + if wp.potency >= 3: + dmg_bonus += 1 + + if wp.twin and self.previous_attacks > 0: + dmg_bonus += wp.dice_count() + + if check == Roll.Success: + return wp.dice_count() * d(wp.main_die) + dmg_bonus + if check == Roll.Crit: + dmg = None + if wp.fatal is not None and wp.fatal > 0: + dmg = (wp.dice_count() + 1) * d(wp.fatal) + else: + dmg = wp.dice_count() * d(wp.main_die) + dmg = 2 * (dmg+dmg_bonus) + + if wp.deadly is not None and wp.deadly > 0: + dmg += max(1, wp.dice_count()-1) * d(wp.deadly) + + if wp.group == "crossbow": + dmg += persistant_damage_factor * d(8).expected() + elif wp.group == "dart" or wp.group == "knife": + dmg += persistant_damage_factor * d(6).expected() + elif wp.group == "dagger": + dmg += persistant_damage_factor * d(6).expected() + elif wp.group == "pick": + dmg += 2 * wp.dice_count() + + return dmg + return 0 + + def range_penalty(self): + return 0 + + def map(self): + factor = max(0, min(2, self.previous_attacks)) + if self.weapon.agile: + return factor * -4 + return factor * -5 + diff --git a/character.py b/character.py index 2a306f9..14a7e42 100644 --- a/character.py +++ b/character.py @@ -1,6 +1,5 @@ import re import json -import copy from dataclasses import dataclass, asdict from items import Armor, Weapon @@ -173,6 +172,22 @@ class Character: 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) @@ -181,18 +196,6 @@ 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/creature.py b/creature.py index aa830f6..8ec883d 100644 --- a/creature.py +++ b/creature.py @@ -88,10 +88,14 @@ class CreaturePlan(LevelingPlan, dict): self.stat_range_value = stat_range_value def build(self, level: int): - res = dict() + res = Creature() for stat, difficulty in self.items(): res[stat] = stats.get(stat, level, difficulty, self.stat_range_value) return res +class Creature(dict): + def ac(self): + return self["ac"] + # if __name__ == '__main__': # main() @@ -26,3 +26,8 @@ class Weapon: agile: bool = False finesse: bool = False twin: bool = False + propulsive: bool = False + thrown: int = 0 + + def dice_count(self): + return self.striking+1 @@ -2,70 +2,11 @@ import math from character import CharacterPlan from game import Difficulty, Roll, d from creature import CreaturePlan +from actions import Attack +from items import Weapon import pb2e - - -class Weapon: - def __init__(self, crit_effect, main_die, potency, striking=1, fatal=0, - deadly=0, backstabber=False, - agile=False, twin=False) -> None: - self.main_die = main_die - self.striking = striking - self.potency = potency - self.backstabber = backstabber - self.fatal = fatal - self.deadly = deadly - self.agile = agile - self.twin = twin - self.crit_effect = crit_effect - - - def damage(self, dmg_bonus, check=Roll.Success, number = 0): - persistant_damage_factor = 3 + 1/3 - if self.backstabber: - dmg_bonus += 1 - if self.potency >= 3: - dmg_bonus += 1 - - if self.twin and number > 0: - dmg_bonus += self.striking - - if check == Roll.Success: - return self.striking * d(self.main_die) + dmg_bonus - if check == Roll.Crit: - dmg = None - if self.fatal is not None and self.fatal > 0: - dmg = (self.striking + 1) * d(self.fatal) - else: - dmg = self.striking * d(self.main_die) - dmg = 2 * (dmg+dmg_bonus) - - if self.deadly is not None and self.deadly > 0: - dmg += max(1, self.striking-1) * d(self.deadly) - - if self.crit_effect == "crossbow": - dmg += persistant_damage_factor * d(8).expected() - elif self.crit_effect == "dart" or self.crit_effect == "knife": - dmg += persistant_damage_factor * d(6).expected() - elif self.crit_effect == "dagger": - dmg += persistant_damage_factor * d(6).expected() - elif self.crit_effect == "pick": - dmg += 2 * self.striking - - return dmg - return 0 - - - def strike(self, attack_bonus, dmg_bonus, ac, number = 0): - def strike_dmg(check): - return self.damage(dmg_bonus, check, number) - bonus = attack_bonus+self.potency+self.get_map(number) - attack_roll = d(20).apply(Roll.check, ac, bonus) - return attack_roll.apply(strike_dmg) - - def treat_wounds(check_res, heal_bonus=0): if check_res <= Roll.Fumble: return -d(8) @@ -99,8 +40,6 @@ def show_stat(name, dist): assert abs(dist.accumulate_prob(lambda x: True) - 1) < math.pow(10, -10) -coin = d(2) - def main(): treat_options = [ @@ -133,29 +72,25 @@ def main2(): show_stat("rapier", total_dmg) def test3(): - falcata = Weapon("sword", 8, 0, fatal=0) - ac = 18 - bonus = +8 +3 - dmg_bonus = d(6)+4 - attack = d(20).apply(Roll.check, ac, bonus) - show_stat("attack", attack) - show_stat("falcata", falcata.strike(bonus, dmg_bonus, ac, 0)) - -def main4(): + level = 2 #char_js = pb2e.download_character(pfb_id=160180) char_js = pb2e.open_character("quizrel2") - plan = CharacterPlan(char_js) - char = plan.build(1) - print(char.toJSON()) - char = plan.build(5) - print(char.toJSON()) + char_plan = CharacterPlan(char_js) + char = char_plan.build(level) + falcata = Weapon("falcata", "martial", "sword", 8, fatal=12) + char.weapons.append(falcata) + char.update_fundamental_runes() -def main5(): creature_plan = CreaturePlan(default_difficulty=Difficulty.Moderate) - creature_plan["reflex"] = Difficulty.High - creature = creature_plan.build(10) - print(creature) + # creature_plan["reflex"] = Difficulty.High + # creature_plan["ac"] = Difficulty.High + creature = creature_plan.build(level) + attack = Attack(char, creature, falcata) + print(char.toJSON()) + print(f"Attack +{attack.bonus()} vs AC {attack.target.ac()}") + show_stat("roll", attack.roll()) + show_stat("falcata", attack.strike()) if __name__ == '__main__': - main5() + test3() |
