Combat: each enemy starts with fresh swing timers; show the pause after a kill
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
60b7b23a05
commit
5898e2006f
4 changed files with 23 additions and 33 deletions
22
game.py
22
game.py
|
|
@ -1195,12 +1195,9 @@ def fight_odds(player: dict, action: dict) -> dict:
|
|||
"kill_time": round(kill, 1), "damage": round(hurt, 1)}
|
||||
|
||||
|
||||
def simulate_fight(world: dict, player: dict, action: dict, rng: random.Random, carry: float = 0) -> dict:
|
||||
"""Play out one fight swing by swing. Nothing changes until it's over: the result is applied then.
|
||||
|
||||
`carry` is how long ago (in game seconds) you last swung: your swing timer runs on from one
|
||||
enemy to the next, so outclassing an enemy means killing as fast as you can swing.
|
||||
"""
|
||||
def simulate_fight(world: dict, player: dict, action: dict, rng: random.Random) -> dict:
|
||||
"""Play out one fight swing by swing, both swing timers starting empty. Nothing changes
|
||||
until it's over: the result is applied then."""
|
||||
foe, blessed, brew = action["enemy"], blessing(player), potion_effect(player, "combat")
|
||||
me = fighter(player, blessed, brew, foe)
|
||||
shrug = 0 if me["pierce"] else foe["reduction"]
|
||||
|
|
@ -1211,16 +1208,13 @@ def simulate_fight(world: dict, player: dict, action: dict, rng: random.Random,
|
|||
wind = blessed["wind"] and not (player["action"] or {}).get("wind_used")
|
||||
pace = speed(world)
|
||||
# A bow looses its first arrow before the enemy closes in.
|
||||
ready = max(0.0, me["interval"] - carry)
|
||||
mine, theirs, result, t = (min(ready, 0.5) if me["first"] else ready), foe["interval"], None, 0.0
|
||||
swung = -carry
|
||||
mine, theirs, result, t = (0.5 if me["first"] else me["interval"]), foe["interval"], None, 0.0
|
||||
while result is None:
|
||||
t = min(mine, theirs)
|
||||
if t > MAX_FIGHT:
|
||||
result = "fled"
|
||||
break
|
||||
if mine <= theirs:
|
||||
swung = mine
|
||||
mine += me["interval"]
|
||||
crit = rng.random() < me["crit"]
|
||||
landed = rng.random() < hit
|
||||
|
|
@ -1258,7 +1252,7 @@ def simulate_fight(world: dict, player: dict, action: dict, rng: random.Random,
|
|||
elif starving:
|
||||
result = "fled"
|
||||
return {"enemy": foe["name"], "enemy_hp": foe["hp"], "top": top, "start_hp": round(player["hp"]),
|
||||
"hp": hp, "result": result, "end": round(t / pace, 2), "duration": t / pace, "carry": t - swung,
|
||||
"hp": hp, "result": result, "end": round(t / pace, 2), "duration": t / pace,
|
||||
"interval": me["interval"] / pace, "enemy_interval": foe["interval"] / pace, "events": events, "style": me["style"],
|
||||
"eaten": eaten, "damage": hurt, "pp": blessed["cost"],
|
||||
"wind_used": blessed["wind"] and not wind and not (player["action"] or {}).get("wind_used")}
|
||||
|
|
@ -1519,8 +1513,8 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R
|
|||
|
||||
if "enemy" in action:
|
||||
outing = player["action"]
|
||||
fight = outing.pop("fight", None) or simulate_fight(world, player, action, rng, outing.get("carry", 0))
|
||||
outing["carry"] = fight.get("carry", 0) + RESPAWN # fights from before the carry-over have none
|
||||
fight = outing.pop("fight", None) or simulate_fight(world, player, action, rng)
|
||||
outing.pop("carry", None) # from a short-lived version that ran the swing timer on
|
||||
if player["prayers"] and not fight["pp"]:
|
||||
summary["prayer_out"] = True
|
||||
paid = min(player["pp"], fight["pp"])
|
||||
|
|
@ -1647,7 +1641,7 @@ def process_player(world: dict, player: dict, now: float, rng: random.Random | N
|
|||
if "enemy" in action and not can_start(world, player, action):
|
||||
if not current.get("fight"):
|
||||
regen(world, player, current["start"], blessing(player)["regen"])
|
||||
current["fight"] = simulate_fight(world, player, action, rng, current.get("carry", 0))
|
||||
current["fight"] = simulate_fight(world, player, action, rng)
|
||||
duration = current["fight"]["duration"]
|
||||
else:
|
||||
duration = action_time(world, player, action)
|
||||
|
|
|
|||
|
|
@ -489,6 +489,8 @@ function animateFight(now) {
|
|||
}
|
||||
const panel = $("[data-fight]");
|
||||
if (!panel) return;
|
||||
// Between kills: the next enemy is on its way, both swing timers empty.
|
||||
panel.classList.toggle("waiting", t < 0);
|
||||
let shown = -1;
|
||||
while (shown + 1 < f.events.length && f.events[shown + 1][0] <= t) shown++;
|
||||
const last = f.events[shown];
|
||||
|
|
@ -505,16 +507,17 @@ function animateFight(now) {
|
|||
for (const [who, every] of [["you", f.interval], ["foe", f.enemy_interval]]) {
|
||||
let swung = 0;
|
||||
for (let i = shown; i >= 0; i--) if (f.events[i][1] === who && f.events[i][2] !== "eat" && f.events[i][2] !== "wind") { swung = f.events[i][0]; break; }
|
||||
set(`${who}-swing`, (el) => { el.style.width = over ? "0%" : `${Math.min(100, (t - swung) / every * 100)}%`; });
|
||||
set(`${who}-swing`, (el) => { el.style.width = over || t < 0 ? "0%" : `${Math.min(100, (t - swung) / every * 100)}%`; });
|
||||
}
|
||||
set("log", (el) => {
|
||||
const key = `${a.start}:${shown}:${over}`;
|
||||
const key = `${a.start}:${shown}:${over}:${t < 0 ? Math.ceil(-t * 10) : ""}`;
|
||||
if (el.dataset.key === key) return;
|
||||
el.dataset.key = key;
|
||||
const lines = f.events.slice(Math.max(0, shown - 5), shown + 1).reverse().map((e) => fightLine(e, f.enemy));
|
||||
if (over) lines.unshift(f.result === "won" ? `<li class="good"><b>${esc(f.enemy)} is beaten.</b> The next one is coming…</li>`
|
||||
: `<li class="bad"><b>You fall back.</b></li>`);
|
||||
el.innerHTML = lines.join("") || `<li class="muted">${t < 0 ? "The next one is coming…" : "Squaring up…"}</li>`;
|
||||
el.innerHTML = t < 0 ? `<li class="good">Victory! Next enemy in ${(-t).toFixed(1)}s</li>`
|
||||
: lines.join("") || `<li class="muted">Squaring up…</li>`;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -231,6 +231,7 @@ select { background: var(--bg); border: 1px solid var(--line); border-radius: 8p
|
|||
.fund { display: flex; flex-direction: column; gap: 6px; }
|
||||
.fight .fighters { display: grid; grid-template-columns: 1fr auto 1fr; gap: 16px; align-items: center; }
|
||||
.fight .vs { font-size: 22px; }
|
||||
.fight.waiting .fighter:last-child { opacity: .45; }
|
||||
.fighter { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
|
||||
.fighter-name { display: flex; justify-content: space-between; gap: 8px; }
|
||||
.fighter-name b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
|
|
|||
|
|
@ -536,14 +536,6 @@ class CombatTest(unittest.TestCase):
|
|||
self.assertAlmostEqual(p["hp"], fight["hp"], delta=0.1)
|
||||
self.assertAlmostEqual(p["action"]["start"], T0 + fight["duration"] + game.RESPAWN) # then a short pause
|
||||
|
||||
def test_a_fight_from_before_the_carry_over_still_finishes(self):
|
||||
world, p = self.fighter(level=30, weapon="iron_sword")
|
||||
game.start_action(world, p, "fight_wolves", T0)
|
||||
game.process_player(world, p, T0 + 0.1, random.Random(1))
|
||||
del p["action"]["fight"]["carry"]
|
||||
summary = game.process_player(world, p, T0 + 60, random.Random(1))
|
||||
self.assertGreater(summary["wins"], 0)
|
||||
|
||||
def test_outclassing_an_enemy_kills_faster(self):
|
||||
kills = {}
|
||||
for level in (20, 90):
|
||||
|
|
@ -552,15 +544,15 @@ class CombatTest(unittest.TestCase):
|
|||
kills[level] = game.process_player(world, p, T0 + 600, random.Random(1))["wins"]
|
||||
self.assertGreater(kills[90], kills[20] * 1.3)
|
||||
|
||||
def test_the_swing_timer_runs_on_between_enemies(self):
|
||||
def test_each_enemy_starts_with_both_swing_timers_empty(self):
|
||||
world, p = self.fighter(level=60, weapon="rune_sword")
|
||||
wolves = game.ACTION_BY_ID["fight_wolves"]
|
||||
fresh = game.simulate_fight(world, p, wolves, random.Random(1))
|
||||
carried = game.simulate_fight(world, p, wolves, random.Random(1), carry=1.0)
|
||||
first = lambda fight: next(e[0] for e in fight["events"] if e[1] == "you")
|
||||
self.assertEqual(first(fresh), 2.4)
|
||||
self.assertEqual(first(carried), 1.4) # a second already gone in the pause between wolves
|
||||
|
||||
game.start_action(world, p, "fight_wolves", T0)
|
||||
game.process_player(world, p, T0 + 0.1, random.Random(1))
|
||||
first = p["action"]["fight"]
|
||||
game.process_player(world, p, T0 + first["duration"] + 0.01, random.Random(1))
|
||||
self.assertAlmostEqual(p["action"]["start"], T0 + first["duration"] + game.RESPAWN) # the pause starts at the kill
|
||||
second = p["action"]["fight"]
|
||||
self.assertEqual(next(e[0] for e in second["events"] if e[1] == "you"), 2.4) # a full swing, from scratch
|
||||
def test_preferred_food_is_eaten_first(self):
|
||||
world, p = self.fighter()
|
||||
p["bank"] = {"bread": 5, "cooked_salmon": 5}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue