small update

boss prototype, auto planter, visual reworks, bug fixes
This commit is contained in:
2026-08-29 10:45:34 +02:00
parent 6840b715de
commit feb4500c6c
31 changed files with 1293 additions and 213 deletions
+105 -1
View File
@@ -155,6 +155,28 @@ class ShieldWiltDotTest(unittest.TestCase):
self.assertEqual(t.hp, 100)
self.assertNotIn(S_SHIELD, t.status)
def test_heal_random_ally_prefers_injured_ally(self):
flutter = Skill("Flutter", power=40, target=TARGET_ENEMY, mechanic="heal_random_ally")
daisy = make_unit("Daisy", TEAM_PLAYER, "radiant", atk=50, skills=[flutter])
full = make_unit("Full", TEAM_PLAYER, "radiant", hp=100)
hurt = make_unit("Hurt", TEAM_PLAYER, "radiant", hp=100)
hurt.hp = 50
enemy = make_unit("E", TEAM_ENEMY, "garden", hp=500)
b = Battle([daisy, full, hurt], [enemy])
b.start()
b.use_skill(daisy, flutter, enemy)
self.assertEqual(hurt.hp, 90) # the injured ally got healed
self.assertEqual(full.hp, 100) # full-HP ally untouched
def test_heal_random_ally_noop_when_all_full(self):
flutter = Skill("Flutter", power=40, target=TARGET_ENEMY, mechanic="heal_random_ally")
daisy = make_unit("Daisy", TEAM_PLAYER, "radiant", atk=50, skills=[flutter])
enemy = make_unit("E", TEAM_ENEMY, "garden", hp=500)
b = Battle([daisy], [enemy])
b.start()
b.use_skill(daisy, flutter, enemy)
self.assertFalse(any(isinstance(l, dict) and "recovers" in l["text"] for l in b.log)) # nothing healed
def test_wilt_ticks_over_three_turns(self):
a = make_unit("A", TEAM_PLAYER, "radiant", atk=50, skills=[Skill("Hit", 10, target=TARGET_ENEMY)])
t = make_unit("T", TEAM_ENEMY, "garden", dfn=20, hp=500, spd=5)
@@ -194,6 +216,45 @@ class ShieldWiltDotTest(unittest.TestCase):
self.assertEqual(t.hp, 410)
self.assertNotIn(S_DOT, t.status)
def test_shield_decays_after_3_turns(self):
t = make_unit("T", TEAM_ENEMY, "garden", hp=500)
b = Battle([make_unit("A", TEAM_PLAYER, "radiant")], [t])
b.start()
b._shield(t, 30)
self.assertEqual(b._shield_total(t), 30)
b._end_turn_status(t)
self.assertEqual(b._shield_total(t), 30) # 3 -> 2
b._end_turn_status(t)
self.assertEqual(b._shield_total(t), 30) # 2 -> 1
b._end_turn_status(t)
self.assertEqual(b._shield_total(t), 0) # gone after the 3rd turn
self.assertNotIn(S_SHIELD, t.status)
def test_stacked_shields_decay_independently(self):
t = make_unit("T", TEAM_ENEMY, "garden", hp=500)
b = Battle([make_unit("A", TEAM_PLAYER, "radiant")], [t])
b.start()
b._shield(t, 30)
b._end_turn_status(t) # first: 3 -> 2
b._shield(t, 40) # stack a new one (doesn't refresh the old)
b._end_turn_status(t) # first: 2 -> 1, second: 3 -> 2
b._end_turn_status(t) # first expires, second: 2 -> 1
self.assertEqual(b._shield_total(t), 40) # only the newer 40 remains
b._end_turn_status(t) # second expires too
self.assertEqual(b._shield_total(t), 0)
def test_shield_absorption_consumes_applications_in_order(self):
t = make_unit("T", TEAM_ENEMY, "garden", hp=500)
b = Battle([make_unit("A", TEAM_PLAYER, "radiant")], [t])
b.start()
b._shield(t, 30)
b._shield(t, 40)
b._absorb(None, t, 50) # 30 from the old shield, 20 from the new one
self.assertEqual(b._shield_total(t), 20)
b._absorb(None, t, 20) # finishes the new shield
self.assertEqual(b._shield_total(t), 0)
self.assertEqual(t.hp, 500)
class MechanicsTest(unittest.TestCase):
def test_sleep_skips_until_two_hits(self):
@@ -487,7 +548,7 @@ class MechanicsTest(unittest.TestCase):
enemy.status["wilt"] = (30, 3)
enemy.status["dot"] = [(10, 2)]
enemy.status["vulnerable"] = 1
zinnia.status["shield"] = 10 # one self effect only
zinnia.status["shield"] = [(10, 3)] # one self effect only
self.assertIs(b.best_skill(zinnia), glitter)
def test_zinnia_targets_most_effect_rich_enemy(self):
@@ -743,5 +804,48 @@ class TowerInjectionTest(unittest.TestCase):
self.assertNotEqual(base.name, inj["skill"]["name"])
class TurnActionLimitTest(unittest.TestCase):
"""A unit must act only once per turn (spam-clicking must not multi-cast)."""
def _battle(self, skills):
a = make_unit("A", TEAM_PLAYER, "radiant", atk=50, skills=skills)
t = make_unit("T", TEAM_ENEMY, "garden", dfn=20, hp=1000)
b = Battle([a], [t])
b.start()
return b, a, t
def test_unit_cannot_use_two_skills_in_one_turn(self):
hit1 = Skill("Hit1", power=10, target=TARGET_ENEMY)
hit2 = Skill("Hit2", power=10, target=TARGET_ENEMY)
b, a, t = self._battle([hit1, hit2])
self.assertTrue(b.use_skill(a, hit1, t))
self.assertFalse(b.use_skill(a, hit2, t)) # already acted
self.assertTrue(a.acted)
def test_same_skill_cannot_be_spammed(self):
hit = Skill("Hit", power=10, target=TARGET_ENEMY, cooldown=0)
b, a, t = self._battle([hit])
self.assertTrue(b.use_skill(a, hit, t))
self.assertFalse(b.use_skill(a, hit, t)) # acted flag blocks the re-cast
def test_extra_action_skill_still_usable_after_acting(self):
hit = Skill("Hit", power=10, target=TARGET_ENEMY)
extra = Skill("Extra", power=5, target=TARGET_ENEMY, extra_action=True)
b, a, t = self._battle([hit, extra])
self.assertTrue(b.use_skill(a, hit, t))
self.assertTrue(b.use_skill(a, extra, t)) # extra action allowed after acting
self.assertTrue(a.acted)
def test_no_extra_actions_after_act_limit_exhausted(self):
hit = Skill("Hit", power=10, target=TARGET_ENEMY)
extra = Skill("Extra", power=5, target=TARGET_ENEMY, extra_action=True)
extra2 = Skill("Extra2", power=5, target=TARGET_ENEMY, extra_action=True)
b, a, t = self._battle([hit, extra, extra2])
self.assertTrue(b.use_skill(a, hit, t))
self.assertTrue(b.use_skill(a, extra, t))
self.assertTrue(b.use_skill(a, extra2, t))
self.assertFalse(b.use_skill(a, hit, t)) # acted + cooldown: still blocked
if __name__ == "__main__":
unittest.main()