Coverage for thb/cards/basic.py : 98%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2from __future__ import annotations
4# -- stdlib --
5from typing import cast
7# -- third party --
8# -- own --
9from game.base import EventHandler
10from thb.actions import ActionStage, ActionStageLaunchCard, AskForCard, Damage, DropCards, ForEach
11from thb.actions import GenericAction, LaunchCard, MigrateCardsTransaction, PrepareStage, UseCard
12from thb.actions import UserAction, VitalityLimitExceeded, register_eh, user_choose_cards
15# -- code --
16class BasicAction(UserAction):
17 pass
20class BaseAttack(UserAction):
22 def __init__(self, source, target, damage=1):
23 self.source = source
24 self.target = target
25 self.damage = damage
27 def apply_action(self):
28 g = self.game
29 source, target = self.source, self.target
30 rst = g.process_action(LaunchGraze(target))
31 self1, rst = g.emit_event('attack_aftergraze', (self, not rst))
32 assert self1 is self
33 assert rst in (False, True)
34 if rst:
35 g.process_action(Damage(source, target, amount=self.damage))
36 return True
37 else:
38 return False
40 def is_valid(self):
41 return not self.target.dead
44class Attack(BaseAttack, BasicAction):
45 pass
48class InevitableAttack(Attack):
49 def apply_action(self):
50 g = self.game
51 dmg = Damage(self.source, self.target, amount=self.damage)
52 g.process_action(dmg)
53 return True
56@register_eh
57class AttackCardRangeHandler(EventHandler):
58 interested = ['calcdistance']
60 def handle(self, evt_type, act):
61 if evt_type == 'calcdistance': 61 ↛ 67line 61 didn't jump to line 67, because the condition on line 61 was never false
62 src, card, dist = act
63 from .definition import AttackCard
64 if card.is_card(AttackCard):
65 self.fix_attack_range(src, dist)
67 return act
69 @staticmethod
70 def attack_range_bonus(p):
71 from .equipment import WeaponSkill
72 l = [
73 s.range - 1 for s in p.skills
74 if issubclass(s, WeaponSkill) and p.has_skill(s)
75 ]
76 return max(0, 0, *l)
78 @classmethod
79 def fix_attack_range(cls, src, dist):
80 l = cls.attack_range_bonus(src)
81 for p in dist:
82 dist[p] -= l
85@register_eh
86class AttackCardVitalityHandler(EventHandler):
87 interested = ['action_before', 'action_shootdown']
89 @classmethod
90 def handle(cls, evt_type, act):
91 if evt_type == 'action_before' and isinstance(act, ActionStageLaunchCard):
92 from .definition import AttackCard
93 src = act.source
94 if act.card.is_card(AttackCard) and not cls.is_disabled(src):
95 act._[cls] = 'already-handled'
96 src.tags['vitality'] -= 1
98 elif evt_type == 'action_shootdown' and isinstance(act, ActionStageLaunchCard):
99 from .definition import AttackCard
100 if act.card.is_card(AttackCard):
101 src = act.source
102 if cls.is_disabled(src):
103 return act
105 if act._[cls]:
106 return act
108 if src.tags['vitality'] > 0:
109 return act
111 raise VitalityLimitExceeded
113 return act
115 @staticmethod
116 def disable(p):
117 p.tags['attack_card_vitality'] = p.tags['turn_count']
119 @staticmethod
120 def enable(p):
121 p.tags['attack_card_vitality'] = 0
123 @staticmethod
124 def is_disabled(p):
125 return p.tags['attack_card_vitality'] >= p.tags['turn_count']
128@register_eh
129class VitalityHandler(EventHandler):
130 interested = ['action_before']
132 def handle(self, evt_type, act):
133 if evt_type == 'action_before' and isinstance(act, ActionStage):
134 act.source.tags['vitality'] = 1
136 return act
139class Heal(BasicAction):
141 def __init__(self, source, target, amount=1):
142 self.source = source
143 self.target = target
144 self.amount = amount
146 def apply_action(self):
147 target = self.target
148 if target.life < target.maxlife: 148 ↛ 152line 148 didn't jump to line 152, because the condition on line 148 was never false
149 target.life = min(target.life + self.amount, target.maxlife)
150 return True
151 else:
152 return False
154 def is_valid(self):
155 tgt = self.target
156 return not tgt.dead and tgt.life < tgt.maxlife
159class GrazeAction(BasicAction):
161 def apply_action(self):
162 return True
165class UseAttack(AskForCard):
166 card_usage = 'use'
168 def __init__(self, target):
169 from thb.cards.definition import AttackCard
170 AskForCard.__init__(self, target, target, AttackCard)
172 def process_card(self, card):
173 g = self.game
174 return g.process_action(UseCard(self.target, card))
176 def ask_for_action_verify(self, p, cl, tl):
177 return UseCard(p, cl[0]).can_fire()
180class BaseUseGraze(AskForCard):
181 def __init__(self, target):
182 from thb.cards.definition import GrazeCard
183 AskForCard.__init__(self, target, target, GrazeCard)
186class UseGraze(BaseUseGraze):
187 card_usage = 'use'
189 def process_card(self, card):
190 g = self.game
191 return g.process_action(UseCard(self.target, card))
193 def ask_for_action_verify(self, p, cl, tl):
194 return UseCard(p, cl[0]).can_fire()
197class LaunchGraze(BaseUseGraze):
198 card_usage = 'launch'
200 def process_card(self, card):
201 g = self.game
202 tgt = self.target
203 return g.process_action(LaunchCard(tgt, [tgt], card, GrazeAction(tgt, tgt)))
205 def ask_for_action_verify(self, p, cl, tl):
206 tgt = self.target
207 return LaunchCard(tgt, [tgt], cl[0], GrazeAction(tgt, tgt)).can_fire()
210class AskForHeal(AskForCard):
211 card_usage = 'launch'
213 def __init__(self, source, target):
214 from thb import cards
215 AskForCard.__init__(self, source, target, cards.definition.HealCard)
217 def process_card(self, card):
218 g = self.game
219 src, tgt = self.source, self.target
220 heal_cls = card.associated_action or Heal
221 return g.process_action(LaunchCard(tgt, [src], card, heal_cls(tgt, src)))
223 def ask_for_action_verify(self, p, cl, tl):
224 src, tgt = self.source, self.target
225 card = cl[0]
226 heal_cls = card.associated_action or Heal
227 return LaunchCard(tgt, [src], card, heal_cls(tgt, src)).can_fire()
230class Wine(BasicAction):
231 def apply_action(self):
232 self.target.tags['wine'] = True
233 return True
235 def is_valid(self):
236 return not self.target.dead
239class SoberUp(GenericAction):
240 def apply_action(self):
241 self.target.tags['wine'] = False
242 return True
245class WineRevive(GenericAction):
246 def __init__(self, act):
247 self.act = act
248 self.source = act.target
249 self.target = act.target
251 def apply_action(self):
252 self.act.amount -= 1
253 tgt = self.target
254 self.game.process_action(SoberUp(tgt, tgt))
255 return True
258@register_eh
259class WineHandler(EventHandler):
260 interested = ['action_apply', 'action_before', 'post_choose_target']
262 def handle(self, evt_type, act):
263 if evt_type == 'action_before' and isinstance(act, BaseAttack):
264 pact = ForEach.get_actual_action(act) or act
265 if getattr(pact, 'in_wine', False):
266 act.damage += 1
268 elif evt_type == 'post_choose_target':
269 act, tl = arg = act
271 from thb.cards.definition import AttackCard
272 if act.card.is_card(AttackCard):
273 src = act.source
274 if src.tags['wine']:
275 self.game.process_action(SoberUp(src, src))
276 act.card_action.in_wine = True
278 return arg
280 elif evt_type == 'action_apply' and isinstance(act, PrepareStage):
281 src = act.target
282 if src.tags['wine']:
283 self.game.process_action(SoberUp(src, src))
285 elif evt_type == 'action_before' and isinstance(act, Damage):
286 if act.cancelled: return act 286 ↛ exitline 286 didn't return from function 'handle', because the return on line 286 wasn't executed
287 if act.amount < 1: return act
288 tgt = act.target
289 if act.amount >= tgt.life and tgt.tags['wine']:
290 g = self.game
291 g.process_action(WineRevive(act))
293 return act
296class Exinwan(BasicAction):
297 # 恶心丸
298 def apply_action(self):
299 return True
302class ExinwanEffect(GenericAction):
303 # 恶心丸
304 card_usage = 'drop'
306 def apply_action(self):
307 g = self.game
308 tgt = self.target
309 if tgt.dead: 309 ↛ 310line 309 didn't jump to line 310, because the condition on line 309 was never true
310 return False
312 cards = user_choose_cards(self, tgt, ('cards', 'showncards', 'equips'))
314 if cards:
315 g.process_action(DropCards(tgt, tgt, cards))
316 else:
317 g.process_action(Damage(None, tgt))
319 return True
321 def cond(self, cards):
322 if len(cards) != 2: return False
323 from thb.cards.base import Skill
324 if any(isinstance(c, Skill) for c in cards): return False 324 ↛ exitline 324 didn't return from function 'cond', because the return on line 324 wasn't executed
325 return True
327 def is_valid(self):
328 return not self.target.dead
331@register_eh
332class ExinwanHandler(EventHandler):
333 # 恶心丸
335 interested = ['post_card_migration']
337 def handle(self, evt_type, arg) -> None:
338 from thb.cards.base import VirtualCard, HiddenCard
339 from thb.cards.definition import ExinwanCard
341 if evt_type == 'post_card_migration': 341 ↛ 363line 341 didn't jump to line 363, because the condition on line 341 was never false
342 moves = cast(MigrateCardsTransaction, arg).movements
343 moves = [m for m in moves if m.to.type == 'droppedcard']
345 # cards to dropped area should all unwrapped
346 assert not any(
347 m.card.is_card(VirtualCard) or m.card.is_card(HiddenCard)
348 for m in moves
349 )
351 moves = [m for m in moves if m.card.is_card(ExinwanCard)]
353 # no same card dropped twice in the same transaction
354 assert len(moves) == len(set(m.card for m in moves))
356 for m in moves:
357 tgt = m.trans.action.source
358 if tgt and not tgt.dead:
359 act = ExinwanEffect(tgt, tgt)
360 act.associated_card = m.card
361 self.game.process_action(act)
363 return arg