Active play and the Watchtower Depths dungeon
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
db1a00e8aa
commit
9d61de2012
9 changed files with 790 additions and 56 deletions
|
|
@ -736,6 +736,178 @@ class AbilityTest(unittest.TestCase):
|
|||
self.assertIsNone(world["parties"]["1"]["fight"])
|
||||
|
||||
|
||||
class ActivePlayTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.world = world_with(("ash", "miner"))
|
||||
self.p = self.world["players"]["ash"]
|
||||
self.p["xp"]["combat"] = game.XP_TABLE[30]
|
||||
self.p["equip"].update(weapon="steel_warhammer", helmet="steel_helmet", body="steel_body", legs="steel_legs")
|
||||
self.p["hp"] = game.max_hp(self.p)
|
||||
self.p["last_active"] = T0
|
||||
game.start_action(self.world, self.p, "fight_stone_guardians", T0)
|
||||
game.process_player(self.world, self.p, T0, random.Random(3)) # two stone guardians
|
||||
self.fight = self.p["action"]["fight"]
|
||||
|
||||
def at(self, t):
|
||||
"""The real moment `t` seconds into the fight."""
|
||||
return T0 + (self.fight["rest"] + t) / self.fight["pace"]
|
||||
|
||||
def mine(self, label, kinds=("cast", "use", "hit", "crit")):
|
||||
return [e for e in self.fight["events"] if e[1] == "ash" and e[6] == label and e[3] in kinds]
|
||||
|
||||
def test_switching_and_pressing(self):
|
||||
self.assertEqual(game.press(self.world, self.p, "crushing_blow", self.at(5)), "That one's on auto")
|
||||
self.assertIsNone(game.set_manual(self.world, self.p, "crushing_blow", True, T0)) # yours from the start
|
||||
self.assertEqual(self.p["manual"], ["crushing_blow"])
|
||||
watch = (self.fight["rest"] + game.member_of(self.fight, "ash")["watch"]) / self.fight["pace"]
|
||||
self.assertTrue(all(e[0] >= watch for e in self.mine("crushing_blow", ("cast",)))) # back on auto once you look away
|
||||
before = [e for e in self.fight["events"] if e[0] < self.at(5) - T0]
|
||||
self.assertIsNone(game.press(self.world, self.p, "crushing_blow", self.at(5)))
|
||||
self.assertEqual([e for e in self.fight["events"] if e[0] < self.at(5) - T0], before) # the past stands
|
||||
casts = [e[0] for e in self.mine("crushing_blow", ("cast",)) if e[0] >= self.at(5) - T0]
|
||||
self.assertTrue(casts)
|
||||
self.assertLessEqual(casts[0], self.at(5 + combat_queue()) - T0) # as soon as it's ready
|
||||
|
||||
def test_while_watching_auto_leaves_your_abilities_alone(self):
|
||||
game.set_manual(self.world, self.p, "crushing_blow", True, self.at(1))
|
||||
watch = game.member_of(self.fight, "ash")["watch"]
|
||||
uses = [e[0] for e in self.mine("crushing_blow", ("cast",))]
|
||||
self.assertTrue(all(u >= (self.fight["rest"] + watch) / self.fight["pace"] for u in uses if u > self.at(1) - T0))
|
||||
# Look again and again: still nothing on auto until you look away.
|
||||
for t in (5, 10, 15):
|
||||
game.watch_fight(self.world, self.p, self.at(t))
|
||||
self.assertGreater(game.member_of(self.fight, "ash")["watch"], 25)
|
||||
|
||||
def test_tapping_an_enemy_turns_you_on_it(self):
|
||||
self.assertIsNone(game.set_fight_target(self.world, self.p, "e1", self.at(2)))
|
||||
swings = [e for e in self.fight["events"] if e[1] == "ash" and e[3] in ("hit", "crit", "miss")
|
||||
and e[6] in game.DAMAGE_TYPES and e[0] > self.at(2.5) - T0]
|
||||
dead = {e[2]: e[0] for e in self.fight["events"] if e[3] == "die"}
|
||||
self.assertTrue(all(e[2] == "e1" for e in swings if e[0] < dead.get("e1", 1e9)))
|
||||
|
||||
def test_no_presses_while_resting_or_out_of_a_fight(self):
|
||||
game.set_manual(self.world, self.p, "crushing_blow", True, T0)
|
||||
self.assertEqual(game.press(self.world, self.p, "crushing_blow", T0), "Still resting")
|
||||
later = T0 + self.fight["duration"] + 100
|
||||
self.p["action"] = None
|
||||
self.assertEqual(game.press(self.world, self.p, "crushing_blow", later), "You're not fighting")
|
||||
|
||||
def test_old_worlds_get_manual(self):
|
||||
del self.p["manual"]
|
||||
self.world["version"] = 27
|
||||
game.migrate(self.world, T0)
|
||||
self.assertEqual(self.p["manual"], [])
|
||||
|
||||
|
||||
def combat_queue():
|
||||
return game.combat.QUEUE + game.combat.GCD
|
||||
|
||||
|
||||
class DungeonTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.world = world_with(("wil", "builder"), ("kay", "homesteader"), ("ann", "miner"))
|
||||
gear = {"builder": dict(weapon="rune_sword", shield="rune_shield", helmet="rune_helmet", body="rune_body",
|
||||
legs="rune_legs"),
|
||||
"homesteader": dict(weapon="magic_staff", helmet="rune_helmet", body="rune_body", legs="rune_legs"),
|
||||
"miner": dict(weapon="rune_warhammer", helmet="rune_helmet", body="rune_body", legs="rune_legs")}
|
||||
for p in self.world["players"].values():
|
||||
p["xp"]["combat"] = game.XP_TABLE[60] # well past it: these are about the run, not the fights
|
||||
p["equip"].update(gear[p["cls"]])
|
||||
p["hp"] = game.max_hp(p)
|
||||
p["last_active"] = T0
|
||||
self.wil, self.kay, self.ann = (self.world["players"][uid] for uid in ("wil", "kay", "ann"))
|
||||
|
||||
def online(self, now, players=None):
|
||||
for p in players or self.world["players"].values():
|
||||
p["last_active"] = now
|
||||
game.catch_up(self.world, now, random.Random(1))
|
||||
|
||||
def party(self):
|
||||
return game.party_of(self.world, self.wil)
|
||||
|
||||
def test_a_run_goes_pull_by_pull_and_pays_on_the_way_out(self):
|
||||
self.assertIsNone(game.start_action(self.world, self.wil, "dungeon_watchtower", T0))
|
||||
self.online(T0 + 600)
|
||||
self.assertNotIn("fight", self.wil["action"]) # alone, it waits
|
||||
self.assertIsNone(game.join_fight(self.world, self.kay, "wil", T0 + 600))
|
||||
names = []
|
||||
now = T0 + 600
|
||||
while game.party_of(self.world, self.wil):
|
||||
fight = self.party()["fight"] or {}
|
||||
if fight.get("stage"):
|
||||
names.append(fight["enemy"])
|
||||
now += 2
|
||||
self.online(now)
|
||||
self.assertLess(now, T0 + 6 * 3600)
|
||||
pulls = game.ACTION_BY_ID["dungeon_watchtower"]["pulls"]
|
||||
self.assertEqual(list(dict.fromkeys(names)), pulls)
|
||||
for p in (self.wil, self.kay):
|
||||
self.assertIsNone(p["action"])
|
||||
self.assertEqual(p["stopped"], "Cleared Watchtower Depths")
|
||||
self.assertGreaterEqual(p["gold"], game.DUNGEONS["dungeon_watchtower"]["bonus_gold"])
|
||||
self.assertIn("cleared Watchtower Depths", self.world["ledger"][-1]["text"])
|
||||
|
||||
def test_joining_only_before_the_first_pull_and_up_to_four(self):
|
||||
game.start_action(self.world, self.wil, "dungeon_watchtower", T0)
|
||||
self.assertIsNone(game.join_fight(self.world, self.kay, "wil", T0))
|
||||
self.online(T0 + 1)
|
||||
self.assertIsNone(game.join_fight(self.world, self.ann, "wil", T0 + 1)) # still resting: in time
|
||||
self.party()["run"]["pull"] = 1
|
||||
extra = game.new_player("bo", "Bo", "ranger", T0)
|
||||
extra["xp"]["combat"] = game.XP_TABLE[60]
|
||||
self.world["players"]["bo"] = extra
|
||||
self.assertEqual(game.join_fight(self.world, extra, "wil", T0 + 2), "The run has started")
|
||||
self.assertFalse(game.joinable(self.world, self.wil))
|
||||
|
||||
def test_a_run_waits_for_whoever_looked_away(self):
|
||||
game.start_action(self.world, self.wil, "dungeon_watchtower", T0)
|
||||
game.join_fight(self.world, self.kay, "wil", T0)
|
||||
self.online(T0 + 300, [self.wil]) # kay went to make tea
|
||||
self.online(T0 + 1200, [self.wil])
|
||||
self.assertIsNone(self.party()["fight"])
|
||||
self.assertEqual(game.run_of(self.world, self.wil)["away"], ["Kay"])
|
||||
self.online(T0 + 1210)
|
||||
self.assertIsNotNone(self.party()["fight"])
|
||||
|
||||
def test_three_wipes_on_a_pull_end_the_run(self):
|
||||
game.start_action(self.world, self.wil, "dungeon_watchtower", T0)
|
||||
game.join_fight(self.world, self.kay, "wil", T0)
|
||||
for p in (self.wil, self.kay):
|
||||
p["equip"] = {slot: None for slot in p["equip"]}
|
||||
p["xp"]["combat"] = game.XP_TABLE[20]
|
||||
self.party()["run"] = {"pull": 7, "wipes": 0} # the ogre, naked
|
||||
now = T0
|
||||
while game.party_of(self.world, self.wil):
|
||||
now += 30
|
||||
self.online(now)
|
||||
self.assertLess(now, T0 + 3 * 3600)
|
||||
self.assertEqual(self.wil["stopped"], "Wiped 3 times at Watchtower ogre")
|
||||
|
||||
def test_the_leader_can_pull_early(self):
|
||||
game.start_action(self.world, self.wil, "dungeon_watchtower", T0)
|
||||
game.join_fight(self.world, self.kay, "wil", T0)
|
||||
self.kay["hp"] = 1
|
||||
self.online(T0 + 0.5)
|
||||
fight = self.party()["fight"]
|
||||
self.assertGreater(fight["rest"], 10)
|
||||
self.assertEqual(game.pull_now(self.world, self.kay, T0 + 3), "Only the leader pulls")
|
||||
self.assertIsNone(game.pull_now(self.world, self.wil, T0 + 3))
|
||||
self.assertLessEqual(fight["rest"], 3)
|
||||
self.assertLess(game.member_of(fight, "kay")["hp_in"], game.max_hp(self.kay) / 2)
|
||||
|
||||
def test_adds_join_halfway_and_never_count_if_they_dont(self):
|
||||
foes = game.DUNGEONS["dungeon_watchtower"]["pulls"][7][1]
|
||||
self.assertTrue(foes[1]["dormant"] and foes[2]["dormant"])
|
||||
players = [self.wil, self.kay]
|
||||
fight = game.draw_pull(self.world, players, game.ACTION_BY_ID["dungeon_watchtower"], {"pull": 7, "wipes": 0},
|
||||
random.Random(2), T0)
|
||||
joined = [e for e in fight["events"] if e[3] == "join"]
|
||||
yell = next(e for e in fight["events"] if e[3] == "yell")
|
||||
self.assertEqual(yell[6], "Crush them!")
|
||||
self.assertEqual({e[1] for e in joined}, {"e1", "e2"})
|
||||
self.assertGreaterEqual(joined[0][0], yell[0])
|
||||
|
||||
|
||||
class NewSkillsTest(unittest.TestCase):
|
||||
def test_tools_speed_up_gathering(self):
|
||||
world = world_with(("pia", "commoner"))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue