From e6702871ff7aed911411c1438d9a86d14f1e774b Mon Sep 17 00:00:00 2001 From: rope Date: Sun, 25 Oct 2026 22:29:42 +0000 Subject: [PATCH] Presses count until you're really down, not forecast down Co-Authored-By: Claude Opus 5.5 --- combat.py | 6 +++--- game.py | 7 ++++--- tests/test_game.py | 18 ++++++++++++++++++ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/combat.py b/combat.py index 2ff1e9c..ea5890f 100644 --- a/combat.py +++ b/combat.py @@ -257,7 +257,7 @@ VULNERABLE, RESISTANT = 1.25, 0.75 def _member_unit(member: dict) -> dict: me = member["fighter"] return {"id": member["id"], "side": "party", "name": member["name"], "top": member["top"], "hp": member["hp_in"], - "at": member["at"], "left": member["left"], "in": False, "out": None, "role": me["role"], + "at": member["at"], "left": member["left"], "in": False, "out": None, "out_at": None, "role": me["role"], "damage": me["damage"], "interval": me["interval"], "accuracy": me["accuracy"], "evasion": me["evasion"], "crit": me["crit"], "block": me["block"], "taken": me["taken"], "base": me["base"], "abilities": list(me["abilities"]), "cds": {}, "busy": 0.0, "cast": None, "auras": [], @@ -306,7 +306,7 @@ def run(fight: dict) -> dict: unit = units[member["id"]] won = result == "won" and unit["out"] in (None, "down") and unit["in"] member.update(hp=max(0, unit["hp"]), damage=unit["hurt"], dealt=unit["dealt"], wind_used=unit["wind_used"], - drank=unit["drank"], down=unit["down"], + drank=unit["drank"], down=unit["down"], out_at=unit["out_at"], result="won" if won else unit["out"] or ("left" if not unit["in"] else result)) fight.update(events=state["events"], kills=state["kills"], result=result, end=_real(state, t), duration=(fight["rest"] + t) / fight["pace"]) @@ -906,7 +906,7 @@ def _fall(state: dict, unit: dict, t: float) -> None: def _out(state: dict, unit: dict, why: str, t: float) -> None: - unit["out"], unit["cast"], unit["auras"] = why, None, [] + unit["out"], unit["out_at"], unit["cast"], unit["auras"] = why, t, None, [] _event(state, t, unit["id"], unit["id"], why) for enemy in _enemies(state): enemy["threat"].pop(unit["id"], None) diff --git a/game.py b/game.py index 33f9559..895a424 100644 --- a/game.py +++ b/game.py @@ -2375,10 +2375,10 @@ def _my_fight(world: dict, player: dict, now: float) -> tuple[dict, dict, float] fight, start = fight_clock(world, player) if not fight or now >= start + fight["duration"] or not any(m["id"] == player["id"] for m in fight["members"]): return "You're not fighting" - member = member_of(fight, player["id"]) - if member["result"] in ("down", "left"): + member, t = member_of(fight, player["id"]), fight_time(fight, start, now) + if member.get("out_at") is not None and t >= member["out_at"]: # out by now, not just forecast to be return "You're out of this fight" - return fight, member, fight_time(fight, start, now) + return fight, member, t def watch_fight(world: dict, player: dict, now: float) -> None: @@ -6094,6 +6094,7 @@ PATCH_NOTES = [ ("new", "DPS meter under the combat log: damage per second and total for each fighter, live. Shows the last fight while resting."), ("new", "Improved raid UI"), ("fix", "Bugfix: Combats started using \"Fight once\" can now be joined."), + ("fix", "Your buttons work until you're knocked out. A fight heading for a loss no longer locks you out early with \"You're out of this fight\" while you're still standing."), ]}, {"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, " diff --git a/tests/test_game.py b/tests/test_game.py index 1934398..9dfd25f 100644 --- a/tests/test_game.py +++ b/tests/test_game.py @@ -846,6 +846,24 @@ class ActivePlayTest(unittest.TestCase): self.p["action"] = None self.assertEqual(game.press(self.world, self.p, "crushing_blow", later), "You're not fighting") + def test_a_forecast_loss_can_still_be_turned(self): + world = world_with(("ash", "miner")) + p = world["players"]["ash"] + p["xp"]["combat"] = game.XP_TABLE[20] + p["equip"].update(weapon="steel_warhammer") + p["hp"], p["last_active"] = game.max_hp(p), T0 + game.start_action(world, p, "fight_clay_golems", T0) + game.process_player(world, p, T0, random.Random(2)) + fight = p["action"]["fight"] + at = lambda t: T0 + (fight["rest"] + t) / fight["pace"] + game.set_manual(world, p, "find_the_seam", True, T0) + mine = game.member_of(fight, "ash") + self.assertEqual(mine["result"], "down") # on auto from here, ash goes down... + self.assertIsNone(game.press(world, p, "find_the_seam", at(38))) # ...but hasn't yet + self.assertEqual(game.member_of(fight, "ash")["result"], "won") + game.member_of(fight, "ash")["out_at"] = 30 # down for real (a party fights on) + self.assertEqual(game.press(world, p, "find_the_seam", at(31)), "You're out of this fight") + def test_old_worlds_get_manual(self): del self.p["manual"] self.world["version"] = 27