summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSupremist <sergkarv@gmail.com>2025-10-29 22:58:37 +0200
committerSupremist <sergkarv@gmail.com>2025-10-29 22:58:37 +0200
commit9fa4cfbe2f948f4067ca1572bbc5dbfab678e7da (patch)
tree590bb32b5a11dd2f3b5f9a0999d5e809ad1f3749
parent0ada03ebf6a879e0a77475d525141d1090bcd634 (diff)
downloadpf2e_calc-9fa4cfbe2f948f4067ca1572bbc5dbfab678e7da.tar.xz
pf2e_calc-9fa4cfbe2f948f4067ca1572bbc5dbfab678e7da.zip
Add Attack action
-rw-r--r--actions.py75
-rw-r--r--character.py29
-rw-r--r--creature.py6
-rw-r--r--items.py5
-rw-r--r--main.py99
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()
diff --git a/items.py b/items.py
index f25b718..cd14c69 100644
--- a/items.py
+++ b/items.py
@@ -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
diff --git a/main.py b/main.py
index c54de8a..53993f1 100644
--- a/main.py
+++ b/main.py
@@ -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()