Coverage for thb/cards/basic.py : 34%
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
11from thb.actions import FinalizeStage, ForEach, GenericAction, LaunchCard, MigrateCardsTransaction
12from thb.actions import PrepareStage, UseCard, UserAction, VitalityLimitExceeded, register_eh
13from thb.actions import user_choose_cards
16# -- code --
17class BasicAction(UserAction):
18 pass
21class BaseAttack(UserAction):
23 def __init__(self, source, target, damage=1):
24 self.source = source
25 self.target = target
26 self.damage = damage
28 def apply_action(self):
29 g = self.game
30 source, target = self.source, self.target
31 rst = g.process_action(LaunchGraze(target))
32 self1, rst = g.emit_event('attack_aftergraze', (self, not rst))
33 assert self1 is self
34 assert rst in (False, True)
35 if rst:
36 g.process_action(Damage(source, target, amount=self.damage))
37 return True
38 else:
39 return False
41 def is_valid(self):
42 return not self.target.dead
45class Attack(BaseAttack, BasicAction):
46 pass
49class InevitableAttack(Attack):
50 def apply_action(self):
51 g = self.game
52 dmg = Damage(self.source, self.target, amount=self.damage)
53 g.process_action(dmg)
54 return True
57@register_eh
58class AttackCardRangeHandler(EventHandler):
59 interested = ['calcdistance']
61 def handle(self, evt_type, act):
62 if evt_type == 'calcdistance':
63 src, card, dist = act
64 from .definition import AttackCard
65 if card.is_card(AttackCard):
66 self.fix_attack_range(src, dist)
68 return act
70 @staticmethod
71 def attack_range_bonus(p):
72 from .equipment import WeaponSkill
73 l = [
74 s.range - 1 for s in p.skills
75 if issubclass(s, WeaponSkill) and p.has_skill(s)
76 ]
77 return max(0, 0, *l)
79 @classmethod
80 def fix_attack_range(cls, src, dist):
81 l = cls.attack_range_bonus(src)
82 for p in dist:
83 dist[p] -= l
86@register_eh
87class AttackCardVitalityHandler(EventHandler):
88 interested = ['action_before', 'action_shootdown']
90 def handle(self, evt_type, act):
91 if evt_type == 'action_before' and isinstance(act, ActionStageLaunchCard): 91 ↛ 92line 91 didn't jump to line 92, because the condition on line 91 was never true
92 from .definition import AttackCard
93 src = act.source
94 if act.card.is_card(AttackCard) and not self.is_disabled(src):
95 act._['vitality-handler'] = 'already-handled'
96 src.tags['vitality'] -= 1
98 elif evt_type == 'action_shootdown' and isinstance(act, ActionStageLaunchCard): 98 ↛ 99line 98 didn't jump to line 99, because the condition on line 98 was never true
99 from .definition import AttackCard
100 if act.card.is_card(AttackCard):
101 src = act.source
102 if self.is_disabled(src):
103 return act
105 if act._['vitality-handler']:
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): 133 ↛ 134line 133 didn't jump to line 134, because the condition on line 133 was never true
134 act.source.tags['vitality'] = 1
135 elif evt_type == 'action_before' and isinstance(act, FinalizeStage): 135 ↛ 136line 135 didn't jump to line 136, because the condition on line 135 was never true
136 act.source.tags['vitality'] = 0
138 return act
141class Heal(BasicAction):
143 def __init__(self, source, target, amount=1):
144 self.source = source
145 self.target = target
146 self.amount = amount
148 def apply_action(self):
149 target = self.target
150 if target.life < target.maxlife:
151 target.life = min(target.life + self.amount, target.maxlife)
152 return True
153 else:
154 return False
156 def is_valid(self):
157 tgt = self.target
158 return not tgt.dead and tgt.life < tgt.maxlife
161class GrazeAction(BasicAction):
163 def apply_action(self):
164 return True
167class UseAttack(AskForCard):
168 card_usage = 'use'
170 def __init__(self, target):
171 from thb.cards.definition import AttackCard
172 AskForCard.__init__(self, target, target, AttackCard)
174 def process_card(self, card):
175 g = self.game
176 return g.process_action(UseCard(self.target, card))
178 def ask_for_action_verify(self, p, cl, tl):
179 return UseCard(p, cl[0]).can_fire()
182class BaseUseGraze(AskForCard):
183 def __init__(self, target):
184 from thb.cards.definition import GrazeCard
185 AskForCard.__init__(self, target, target, GrazeCard)
188class UseGraze(BaseUseGraze):
189 card_usage = 'use'
191 def process_card(self, card):
192 g = self.game
193 return g.process_action(UseCard(self.target, card))
195 def ask_for_action_verify(self, p, cl, tl):
196 return UseCard(p, cl[0]).can_fire()
199class LaunchGraze(BaseUseGraze):
200 card_usage = 'launch'
202 def process_card(self, card):
203 g = self.game
204 tgt = self.target
205 return g.process_action(LaunchCard(tgt, [tgt], card, GrazeAction(tgt, tgt)))
207 def ask_for_action_verify(self, p, cl, tl):
208 tgt = self.target
209 return LaunchCard(tgt, [tgt], cl[0], GrazeAction(tgt, tgt)).can_fire()
212class AskForHeal(AskForCard):
213 card_usage = 'launch'
215 def __init__(self, source, target):
216 from thb import cards
217 AskForCard.__init__(self, source, target, cards.definition.HealCard)
219 def process_card(self, card):
220 g = self.game
221 src, tgt = self.source, self.target
222 heal_cls = card.associated_action or Heal
223 return g.process_action(LaunchCard(tgt, [src], card, heal_cls(tgt, src)))
225 def ask_for_action_verify(self, p, cl, tl):
226 src, tgt = self.source, self.target
227 card = cl[0]
228 heal_cls = card.associated_action or Heal
229 return LaunchCard(tgt, [src], card, heal_cls(tgt, src)).can_fire()
232class Wine(BasicAction):
233 def apply_action(self):
234 self.target.tags['wine'] = True
235 return True
237 def is_valid(self):
238 return not self.target.dead
241class SoberUp(GenericAction):
242 def apply_action(self):
243 self.target.tags['wine'] = False
244 return True
247class WineRevive(GenericAction):
248 def __init__(self, act):
249 self.act = act
250 self.source = act.target
251 self.target = act.target
253 def apply_action(self):
254 self.act.amount -= 1
255 tgt = self.target
256 self.game.process_action(SoberUp(tgt, tgt))
257 return True
260@register_eh
261class WineHandler(EventHandler):
262 interested = ['action_apply', 'action_before', 'post_choose_target']
264 def handle(self, evt_type, act):
265 if evt_type == 'action_before' and isinstance(act, BaseAttack): 265 ↛ 266line 265 didn't jump to line 266, because the condition on line 265 was never true
266 pact = ForEach.get_actual_action(act) or act
267 if getattr(pact, 'in_wine', False):
268 act.damage += 1
270 elif evt_type == 'post_choose_target': 270 ↛ 271line 270 didn't jump to line 271, because the condition on line 270 was never true
271 act, tl = arg = act
273 from thb.cards.definition import AttackCard
274 if act.card.is_card(AttackCard):
275 src = act.source
276 if src.tags['wine']:
277 self.game.process_action(SoberUp(src, src))
278 act.card_action.in_wine = True
280 return arg
282 elif evt_type == 'action_apply' and isinstance(act, PrepareStage): 282 ↛ 283line 282 didn't jump to line 283, because the condition on line 282 was never true
283 src = act.target
284 if src.tags['wine']:
285 self.game.process_action(SoberUp(src, src))
287 elif evt_type == 'action_before' and isinstance(act, Damage): 287 ↛ 288line 287 didn't jump to line 288, because the condition on line 287 was never true
288 if act.cancelled: return act
289 if act.amount < 1: return act
290 tgt = act.target
291 if act.amount >= tgt.life and tgt.tags['wine']:
292 g = self.game
293 g.process_action(WineRevive(act))
295 return act
298class Exinwan(BasicAction):
299 # 恶心丸
300 def apply_action(self):
301 return True
304class ExinwanEffect(GenericAction):
305 # 恶心丸
306 card_usage = 'drop'
308 def apply_action(self):
309 g = self.game
310 tgt = self.target
311 if tgt.dead:
312 return False
314 cards = user_choose_cards(self, tgt, ('cards', 'showncards', 'equips'))
316 if cards:
317 g.process_action(DropCards(tgt, tgt, cards))
318 else:
319 g.process_action(Damage(None, tgt))
321 return True
323 def cond(self, cards):
324 if len(cards) != 2: return False
325 from thb.cards.base import Skill
326 if any(isinstance(c, Skill) for c in cards): return False
327 return True
329 def is_valid(self):
330 return not self.target.dead
333@register_eh
334class ExinwanHandler(EventHandler):
335 # 恶心丸
337 interested = ['post_card_migration']
339 def handle(self, evt_type, arg) -> None:
340 from thb.cards.base import VirtualCard, HiddenCard
341 from thb.cards.definition import ExinwanCard
343 if evt_type == 'post_card_migration':
344 moves = cast(MigrateCardsTransaction, arg).movements
345 moves = [m for m in moves if m.to.type == 'droppedcard']
347 # cards to dropped area should all unwrapped
348 assert not any(
349 m.card.is_card(VirtualCard) or m.card.is_card(HiddenCard)
350 for m in moves
351 )
353 moves = [m for m in moves if m.card.is_card(ExinwanCard)]
355 # no same card dropped twice in the same transaction
356 assert len(moves) == len(set(m.card for m in moves))
358 for m in moves:
359 tgt = m.trans.action.source
360 if tgt and not tgt.dead:
361 act = ExinwanEffect(tgt, tgt)
362 act.associated_card = m.card
363 self.game.process_action(act)
365 return arg