Rapid Heal mends during fights

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-20 20:44:20 +01:00
parent 8425989df3
commit 636123e454
2 changed files with 24 additions and 8 deletions

20
game.py
View file

@ -1567,11 +1567,11 @@ def max_hp(player: dict) -> int:
return 40 + 4 * skill_level(player, "combat") + int(gear_bonus(player, "hp"))
def regen(world: dict, player: dict, now: float, boost: float = 1) -> None:
def regen(world: dict, player: dict, now: float) -> None:
"""Heal for the time since HP was last settled."""
if now > player["hp_at"]:
top = max_hp(player)
boost *= 2 if fitted(world, "enchanted_hearths") else 1
boost = 2 if fitted(world, "enchanted_hearths") else 1
boost *= 1 + gear_bonus(player, "regen") / 100
per_second = top * REGEN_PER_HOUR * boost * speed(world) / HOUR
player["hp"] = min(top, player["hp"] + (now - player["hp_at"]) * per_second)
@ -1627,6 +1627,7 @@ def _variant_odds(player: dict, foe: dict, blessed: dict, brew: dict) -> dict:
kill = foe["hp"] / (hit * _average_hit(me["max_hit"], me["crit"]) / me["interval"])
taken = sum(_taken(me, kind) for kind in foe["attack"]) / len(foe["attack"])
hurt = their_hit * (1 - me["block"]) * _average_hit(foe["max_hit"]) * taken / foe["interval"] * kill
hurt = max(0, hurt - blessed["mend"] * max_hp(player) * kill)
return {"hit": hit, "their_hit": their_hit, "max_hit": me["max_hit"], "affinity": me["affinity"],
"their_max": max(1, round(foe["max_hit"] * max(_taken(me, kind) for kind in foe["attack"]))),
"kill_time": kill, "damage": hurt}
@ -1669,6 +1670,7 @@ def simulate_fight(world: dict, player: dict, action: dict, rng: random.Random)
break
if mine <= theirs:
mine += me["interval"]
hp = min(top, hp + blessed["mend"] * top * me["interval"]) # Rapid Heal mends a little each swing
crit = rng.random() < me["crit"]
landed = rng.random() < hit
dealt = max(1, round(me["max_hit"] * CRIT_MULT if crit else rng.randint(1, me["max_hit"]))) if landed else 0
@ -1720,10 +1722,12 @@ def simulate_fight(world: dict, player: dict, action: dict, rng: random.Random)
# Melvor-style: bones become prayer points, active prayers cost points each
# fight and give boosts while they last, and praying in a fight trains Prayer.
MEND = 0.0004 # Rapid Heal: share of max HP mended per second of fighting
PRAYERS = [
{"id": "thick_skin", "name": "Thick Skin", "icon": "🪨", "level": 1, "cost": 1, "reduce": 0.10},
{"id": "burst_of_strength", "name": "Burst of Strength", "icon": "💪", "level": 5, "cost": 1, "power": 5},
{"id": "rapid_heal", "name": "Rapid Heal", "icon": "💗", "level": 10, "cost": 1, "regen": 2},
{"id": "rapid_heal", "name": "Rapid Heal", "icon": "💗", "level": 10, "cost": 1, "mend": MEND},
{"id": "steel_skin", "name": "Steel Skin", "icon": "🛡️", "level": 20, "cost": 2, "reduce": 0.20},
{"id": "superhuman_strength", "name": "Superhuman Strength", "icon": "🦾", "level": 30, "cost": 2, "power": 12},
{"id": "second_wind", "name": "Second Wind", "icon": "🌬️", "level": 40, "cost": 1, "wind": True},
@ -1733,7 +1737,7 @@ PRAYERS = [
PRAYER_BY_ID = {prayer["id"]: prayer for prayer in PRAYERS}
MAX_PRAYERS = 2
PRAYER_XP_PER_POINT = 5
NO_BLESSING = {"cost": 0, "power": 0, "taken": 1.0, "regen": 1, "wind": False}
NO_BLESSING = {"cost": 0, "power": 0, "taken": 1.0, "mend": 0, "wind": False}
def describe_prayer(prayer: dict) -> str:
@ -1742,8 +1746,8 @@ def describe_prayer(prayer: dict) -> str:
parts.append(f"-{round(prayer['reduce'] * 100)}% damage taken")
if prayer.get("power"):
parts.append(f"+{prayer['power']} power")
if prayer.get("regen"):
parts.append("HP recovers twice as fast")
if prayer.get("mend"):
parts.append(f"+{prayer['mend'] * 6000:g}% HP / min in fights")
if prayer.get("wind"):
parts.append("once per outing, revive at half HP")
return ", ".join(parts)
@ -1759,7 +1763,7 @@ def blessing(player: dict) -> dict:
for prayer in active:
taken *= 1 - prayer.get("reduce", 0)
return {"cost": cost, "power": sum(p.get("power", 0) for p in active), "taken": taken,
"regen": max([p.get("regen", 1) for p in active]), "wind": any(p.get("wind") for p in active)}
"mend": sum(p.get("mend", 0) for p in active), "wind": any(p.get("wind") for p in active)}
def set_prayers(player: dict, ids: list) -> str | None:
@ -2132,7 +2136,7 @@ def process_player(world: dict, player: dict, now: float, rng: random.Random | N
action = growing(player)
if "enemy" in action and not can_start(world, player, action):
if not current.get("fight"):
regen(world, player, current["start"], blessing(player)["regen"])
regen(world, player, current["start"])
current["fight"] = simulate_fight(world, player, action, rng)
duration = current["fight"]["duration"]
else:

View file

@ -837,6 +837,18 @@ class PrayerTest(unittest.TestCase):
self.assertEqual(summary["second_wind"], 1)
self.assertTrue(summary["stopped"].startswith("Retreated"))
def test_rapid_heal_mends_during_fights(self):
world, p = self.priest(level=10, combat=10)
scouts = game.ACTION_BY_ID["fight_scouts"]
p["pp"], p["hp"] = 1000, 50
plain = game.simulate_fight(world, p, scouts, random.Random(3))
game.set_prayers(p, ["rapid_heal"])
mended = game.simulate_fight(world, p, scouts, random.Random(3))
swings = sum(1 for e in plain["events"] if e[1] == "you")
top = game.max_hp(p)
self.assertAlmostEqual(mended["hp"] - plain["hp"], swings * game.MEND * top * plain["interval"], places=6)
self.assertLess(game.fight_odds(p, scouts)["damage"], game.fight_odds({**p, "prayers": []}, scouts)["damage"])
def test_offerings_ward_the_city_until_the_raid(self):
world, p = self.priest(level=15)
world["city"]["buildings"]["temple"]["level"] = 1