summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--actions.py15
-rw-r--r--character.py8
-rw-r--r--distribution.py8
-rw-r--r--leveling/rogue.json2
-rw-r--r--main.py75
5 files changed, 92 insertions, 16 deletions
diff --git a/actions.py b/actions.py
index 9710faa..c1c9f30 100644
--- a/actions.py
+++ b/actions.py
@@ -14,6 +14,7 @@ class Attack:
strike_type="melee"
previous_attacks: int = 0
distance: int = 0
+ sneak: bool = False
def bonus(self):
return self.attacker.attack_bonus(self.weapon, self.strike_type) + self.map()
@@ -30,10 +31,24 @@ class Attack:
def damage_bonus(self):
return self.attacker.attack_damage_bonus(self.weapon, self.strike_type)
+ def __str__(self):
+ res = ""
+ if self.sneak:
+ res += "Sneak "
+ ac = self.target.ac()
+ bonus = self.bonus()
+ res += f"{self.strike_type} attack{self.previous_attacks+1} "
+ res += f"{bonus:+} vs AC {ac}: {10+bonus-ac:+}; "
+ res += Roll.short_str(self.roll())
+ res += f" Dmg: {self.strike().expected()}"
+ return res
+
def damage(self, check=Roll.Success):
persistant_damage_factor = 3 + 1/3
wp = self.weapon
dmg_bonus = self.damage_bonus()
+ if self.sneak:
+ dmg_bonus += self.attacker.sneak_attack()
if wp.backstabber:
dmg_bonus += 1
if wp.potency >= 3:
diff --git a/character.py b/character.py
index 8614000..cab4cbb 100644
--- a/character.py
+++ b/character.py
@@ -3,7 +3,7 @@ import json
from dataclasses import dataclass, asdict
from items import Armor, Weapon
-from game import LevelingPlan, Features, Modifier, Conditions
+from game import LevelingPlan, Features, Modifier, Conditions, RollInfo
class FeaturesPlan(LevelingPlan, dict):
@@ -189,6 +189,12 @@ class Character:
return (str_mod.total()//2 + dmg_mod).total()
return dmg_mod.total()
+ def sneak_attack(self):
+ sneak = "sneak attack"
+ if sneak not in self.features:
+ return 0
+ return RollInfo.parse(self.features[sneak]).distribution()
+
def toJSON(self):
methods = ["classDC", "max_hp", "ac"]
diff --git a/distribution.py b/distribution.py
index d2503fa..47313b8 100644
--- a/distribution.py
+++ b/distribution.py
@@ -82,7 +82,7 @@ class Distribution:
ax.bar(xax, yax)
ax.set_ylabel(ylb)
ax.set_xlabel(xlb)
- ax.xaxis.set_major_locator(ticker.FixedLocator(xax))
+ # ax.xaxis.set_major_locator(ticker.FixedLocator(xax))
# ax.yaxis.set_major_locator(ticker.FixedLocator(list(set(yax))))
plt.grid(True)
plt.show()
@@ -111,9 +111,15 @@ class Distribution:
res.insert(operator(lv, rv, *args, **kwargs), lprob*rprob)
return res
+ def __iadd__(self, other):
+ self = self.apply2(lambda x, y: x + y, other)
+ return self
+
def __add__(self, other):
return self.apply2(lambda x, y: x + y, other)
+ __radd__ = __add__
+
def __neg__(self):
return self.apply(lambda x: -x)
diff --git a/leveling/rogue.json b/leveling/rogue.json
index e368f8a..87877f9 100644
--- a/leveling/rogue.json
+++ b/leveling/rogue.json
@@ -8,7 +8,7 @@
"3": {"deny advantage": true},
"5": {"sneak attack": "2d6", "unarmed":4, "simple": 4, "martial": 4, "critical specialization": "off-guard"},
"7": {"reflex": 6, "perception": 6, "weapon specialization": 1},
- "9": {"fortitude": 6, "debilitating strike": 1},
+ "9": {"fortitude": 4, "debilitating strike": 1},
"11": {"classDC": 4, "sneak attack": "3d6"},
"13": {"reflex": 8, "perception": 8, "unarmored": 4, "light": 4, "unarmed": 6, "simple": 6, "martial": 6},
"15": {"weapon specialization": 2, "debilitating strike": 2},
diff --git a/main.py b/main.py
index 65c9c45..3d4e154 100644
--- a/main.py
+++ b/main.py
@@ -1,4 +1,6 @@
import math
+
+from numpy._core import multiarray
from character import CharacterPlan
from distribution import Distribution
from game import Difficulty, Roll, d
@@ -62,22 +64,27 @@ def main():
show_stat("Treat Wounds 20 +1", treat)
class StatsTest:
- def __init__(self) -> None:
+ def __init__(self, difficulty = Difficulty.Moderate) -> None:
# self.char_js = pb2e.download_character(pfb_id=160180)
self.char_js = pb2e.open_character("quizrel2")
self.char_plan = CharacterPlan(self.char_js)
- self.creature_plan = CreaturePlan(default_difficulty=Difficulty.Moderate)
+ self.creature_plan = CreaturePlan(default_difficulty=difficulty)
# self.creature_plan["reflex"] = Difficulty.High
# self.creature_plan["ac"] = Difficulty.High
self.char = self.char_plan.build(1)
def add_weapons(self):
- falcata = Weapon("falcata", "martial", "sword", 8, fatal=12)
- sword = Weapon("pick", "martial", "pick", 6, fatal=10)
- rapier = Weapon("rapier", "martial", "sword", 6, finesse=True, deadly=8)
- urumi = Weapon("urumi", "martial", "flail", 6, deadly=10)
- sword2 = Weapon("tricky pick", "martial", "pick", 6, fatal=10, backstabber=True)
- self.char.weapons.extend([sword, falcata, rapier, sword2, urumi])
+ wps = [Weapon("falcata", "martial", "sword", 8, fatal=12),
+ Weapon("trident", "martial", "spear", 8),
+ Weapon("pick", "martial", "pick", 6, fatal=10),
+ Weapon("rapier", "martial", "sword", 6, finesse=True, deadly=8),
+ Weapon("urumi", "martial", "flail", 6, deadly=10),
+ Weapon("tricky pick", "martial", "pick", 6, fatal=10, backstabber=True),
+ Weapon("shortsword", "simple", "sword", 6, agile=True),
+ Weapon("gauntlet", "simple", "brawling", 4, agile=True),
+ Weapon("light pick", "martial", "pick", 6, agile=True, fatal=8)
+ ]
+ self.char.weapons.extend(wps)
def get_weapon(self, name):
for wp in self.char.weapons:
@@ -110,7 +117,7 @@ def make_check(name, bonus, dc):
if isinstance(bonus, Distribution):
bonus = bonus.expected()
print(f"{name} {bonus:+} vs DC {dc}:",
- Roll.short_str(roll), f"Total: {dc-bonus-10:+}")
+ Roll.short_str(roll), f"Total: {10+bonus-dc:+}")
return roll
def demoralize_test():
@@ -139,7 +146,7 @@ def demoralize_test():
frightened = roll.apply(lambda x: x if x >= Roll.Success else 0).expected()
print("Frightened:", frightened)
-def test3():
+def weapon_damage():
t = StatsTest()
def calc_damage(level):
t.set_level(level)
@@ -152,15 +159,55 @@ def test3():
res = dict()
for weapon in t.char.weapons:
- attack = Attack(t.char, t.creature, weapon)
+ attack = Attack(t.char, t.creature, weapon, sneak=False)
res[weapon.name] = attack.strike().expected()
# res["hp"] = creature["hp"]
return res
# calc_damage(20)
+ levelup_view("Weapons Comparison", "Expected Damage", calc_damage)
+
+def twin_takedown_test():
+ t = StatsTest()
+ # t.creature_plan["ac"] = Difficulty.Extreme
+ def calc_damage(level):
+ t.set_level(level)
+ res = dict()
+ wp1 = t.get_weapon("tricky pick")
+ wp2 = t.get_weapon("gauntlet")
+
+ # Twin takedown
+ attack = Attack(t.char, t.creature, wp1, sneak=True)
+ dmg = attack.strike().expected()
+ attack.previous_attacks += 1
+ attack.weapon = wp2
+ dmg += attack.strike().expected()
+ res["Twin Takedown"] = dmg
+
+ # Double slice
+ attack = Attack(t.char, t.creature, wp1, sneak=True)
+ dmg = attack.strike().expected()
+ attack.sneak = False
+ attack.weapon = wp2
+ dmg += attack.strike().expected()
+ res["Double Slice"] = dmg
+
+ return res
levelup_view("Weapons Comparison", "Expected Damage", calc_damage)
+def multiple_attacks():
+ t = StatsTest()
+ t.set_level(5)
+ wp = t.get_weapon("Shortsword")
+ wp.agile = False
+ # t.creature["ac"] -= 1
+ attack = Attack(t.char, t.creature, wp, sneak=True)
+ print(attack)
+ for _ in range(4):
+ attack.previous_attacks += 1
+ print(attack)
+
def levelup_view(title, ylabel, fn):
@@ -192,5 +239,7 @@ def levelup_view(title, ylabel, fn):
if __name__ == '__main__':
- # test4()
- demoralize_test()
+ # weapon_damage()
+ twin_takedown_test()
+ # multiple_attacks()
+ # demoralize_test()