Group rest: everyone heals at their own pace

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-25 20:21:08 +00:00
parent 67a1ab876a
commit 1bf3f3b62f
3 changed files with 20 additions and 5 deletions

View file

@ -2343,7 +2343,7 @@ def draw_fight(world: dict, players: list, action: dict, rng: random.Random, sta
for player in players: for player in players:
seconds, eaten = rest_plan(world, player) seconds, eaten = rest_plan(world, player)
member = fight_member(world, player) member = fight_member(world, player)
member.update(eaten=eaten, hp_in=member["top"]) member.update(eaten=eaten, hp_in=member["top"], rest=seconds) # full after your own rest, then wait
rest = max(rest, seconds) rest = max(rest, seconds)
members.append(member) members.append(member)
fight = {"seed": rng.random(), "pace": speed(world), "rest": rest, "enemy": action["enemy"]["name"], fight = {"seed": rng.random(), "pace": speed(world), "rest": rest, "enemy": action["enemy"]["name"],
@ -3349,7 +3349,7 @@ def draw_pull(world: dict, players: list, action: dict, run: dict, rng: random.R
for player in players: for player in players:
seconds, eaten = rest_plan(world, player) seconds, eaten = rest_plan(world, player)
member = fight_member(world, player) member = fight_member(world, player)
member.update(eaten=eaten, hp_in=member["top"]) member.update(eaten=eaten, hp_in=member["top"], rest=seconds) # full after your own rest, then wait
rest = max(rest, seconds) rest = max(rest, seconds)
members.append(member) members.append(member)
fight = {"seed": rng.random(), "pace": speed(world), "rest": rest, "enemy": name, "members": members, fight = {"seed": rng.random(), "pace": speed(world), "rest": rest, "enemy": name, "members": members,
@ -3447,8 +3447,8 @@ def pull_now(world: dict, player: dict, now: float) -> str | None:
if rested >= fight["rest"]: if rested >= fight["rest"]:
return "Already fighting" return "Already fighting"
rested = math.ceil(rested / combat.STEP) * combat.STEP rested = math.ceil(rested / combat.STEP) * combat.STEP
share = rested / fight["rest"] if fight["rest"] else 1
for member in fight["members"]: for member in fight["members"]:
share = min(1, rested / member["rest"]) if member.get("rest") else 1
member["hp_in"] = member["start_hp"] + (member["hp_in"] - member["start_hp"]) * share member["hp_in"] = member["start_hp"] + (member["hp_in"] - member["start_hp"]) * share
member["eaten"] = {food: int(count * share) for food, count in member["eaten"].items() if int(count * share)} member["eaten"] = {food: int(count * share) for food, count in member["eaten"].items() if int(count * share)}
member["watch"] += fight["rest"] - rested member["watch"] += fight["rest"] - rested
@ -6071,6 +6071,8 @@ PATCH_NOTES = [
("fix", "Combat page shows your health potion: drunk on its own, no need to equip. The other card now reads \"No combat potion\"."), ("fix", "Combat page shows your health potion: drunk on its own, no need to equip. The other card now reads \"No combat potion\"."),
("fix", "Combat page no longer jumps around: cast bars hold their space and the fight log stays 7 lines tall."), ("fix", "Combat page no longer jumps around: cast bars hold their space and the fight log stays 7 lines tall."),
("new", "Enemy casts that can't be interrupted show a 🔒 and a grey bar. Stuns still stop them, except on bosses."), ("new", "Enemy casts that can't be interrupted show a 🔒 and a grey bar. Stuns still stop them, except on bosses."),
("fix", "Resting in a group: everyone heals at their own pace, so good food tops you up first. The pull still "
"waits for the slowest, and Pull now sends each in as healed as they really are."),
]}, ]},
{"date": "2026-10-24", "title": "Dungeon loot", "notes": [ {"date": "2026-10-24", "title": "Dungeon loot", "notes": [
("new", "Masterwork patterns: every dungeon boss can drop one of 7 (15% from a mini-boss, 35% from the last boss, " ("new", "Masterwork patterns: every dungeon boss can drop one of 7 (15% from a mini-boss, 35% from the last boss, "

View file

@ -1081,8 +1081,8 @@ function animateFight(now) {
const before = fightState(f, t - LAG).st; // the HP a moment ago, trailing the bar const before = fightState(f, t - LAG).st; // the HP a moment ago, trailing the bar
const mine = f.members.find((m) => m.id === S.uid) || f.members[0]; const mine = f.members.find((m) => m.id === S.uid) || f.members[0];
const me = st[mine.id]; const me = st[mine.id];
// Resting: everyone's HP climbs back to full while they eat. // Resting: everyone's HP climbs back to full at their own pace, then waits for the slowest.
if (resting) for (const m of f.members) st[m.id].hp = m.start_hp + (m.hp_in - m.start_hp) * Math.min(1, t / rest); if (resting) for (const m of f.members) st[m.id].hp = m.start_hp + (m.hp_in - m.start_hp) * Math.min(1, t * f.pace / (m.rest || f.rest));
const enemies = f.pack.map((_, i) => st[`e${i}`]); const enemies = f.pack.map((_, i) => st[`e${i}`]);
const left = enemies.reduce((sum, e) => sum + e.hp, 0), total = enemies.reduce((sum, e) => sum + e.top, 0); const left = enemies.reduce((sum, e) => sum + e.hp, 0), total = enemies.reduce((sum, e) => sum + e.top, 0);
for (const el of document.querySelectorAll("[data-progress]")) el.style.width = `${(resting ? 1 : left / total) * 100}%`; for (const el of document.querySelectorAll("[data-progress]")) el.style.width = `${(resting ? 1 : left / total) * 100}%`;

View file

@ -966,6 +966,19 @@ class DungeonTest(unittest.TestCase):
self.assertLessEqual(fight["rest"], 3) self.assertLessEqual(fight["rest"], 3)
self.assertLess(game.member_of(fight, "kay")["hp_in"], game.max_hp(self.kay) / 2) self.assertLess(game.member_of(fight, "kay")["hp_in"], game.max_hp(self.kay) / 2)
def test_a_pull_early_heals_each_at_their_own_pace(self):
game.start_action(self.world, self.wil, "dungeon_watchtower", T0)
game.join_fight(self.world, self.kay, "wil", T0)
game.start_run(self.world, self.wil, T0)
self.wil["hp"], self.kay["hp"] = game.max_hp(self.wil) - 5, 1
self.online(T0 + 0.5)
fight = self.party()["fight"]
wil = game.member_of(fight, "wil")
self.assertLess(wil["rest"], fight["rest"]) # done resting first, waiting on Kay
self.assertIsNone(game.pull_now(self.world, self.wil, T0 + wil["rest"] / fight["pace"] + 1))
self.assertEqual(wil["hp_in"], game.max_hp(self.wil))
self.assertLess(game.member_of(fight, "kay")["hp_in"], game.max_hp(self.kay))
def test_adds_join_halfway_and_never_count_if_they_dont(self): def test_adds_join_halfway_and_never_count_if_they_dont(self):
foes = game.DUNGEONS["dungeon_watchtower"]["pulls"][7][1] foes = game.DUNGEONS["dungeon_watchtower"]["pulls"][7][1]
self.assertTrue(foes[1]["dormant"] and foes[2]["dormant"]) self.assertTrue(foes[1]["dormant"] and foes[2]["dormant"])