Coverage for thb/cards/equipment.py : 91%
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 List, cast
7# -- third party --
8# -- own --
9from game.base import EventHandler, GameError
10from thb.actions import ActionLimitExceeded, ActionStageLaunchCard, Damage, DrawCards, DropCardStage
11from thb.actions import DropCards, FatetellAction, FatetellStage, FinalizeStage, ForEach
12from thb.actions import GenericAction, LaunchCard, MaxLifeChange, MigrateCardsTransaction, Reforge
13from thb.actions import UserAction, VitalityLimitExceeded, detach_cards, migrate_cards
14from thb.actions import random_choose_card, register_eh, ttags, user_choose_cards
15from thb.cards import basic, spellcard
16from thb.cards.base import Card, PhysicalCard, Skill, TreatAs, VirtualCard, t_None
17from thb.cards.base import t_OtherLessEqThanN, t_OtherOne
18from thb.inputlets import ChooseOptionInputlet, ChoosePeerCardInputlet
19from utils.check import CheckFailed, check
20from utils.misc import classmix
23# -- code --
24class WearEquipmentAction(UserAction):
25 def apply_action(self):
26 g = self.game
27 card = self.associated_card
28 tgt = self.target
29 equips = tgt.equips
31 _s, _t, _c, rst = g.emit_event('wear_equipment', (self, tgt, card, 'default'))
32 assert _s is self
33 assert _t is tgt
34 assert _c is card
35 assert rst in ('default', 'handled')
37 if rst == 'handled': 37 ↛ 38line 37 didn't jump to line 38, because the condition on line 37 was never true
38 return True
40 for oc in list(equips):
41 if oc.equipment_category == card.equipment_category:
42 g.process_action(DropCards(tgt, tgt, [oc]))
44 migrate_cards([card], tgt.equips)
46 return True
49class ReforgeWeapon(Reforge):
50 pass
53@register_eh
54class WeaponReforgeHandler(EventHandler):
55 interested = ['action_before']
57 def handle(self, evt_type, act):
58 if evt_type == 'action_before' and isinstance(act, ActionStageLaunchCard):
59 c = act.card
60 tgt = act.target
62 if c.is_card(VirtualCard): return act
63 from thb.cards.definition import EquipmentCard
64 if not isinstance(c, EquipmentCard): return act
65 if c.equipment_category != 'weapon': return act
66 if tgt.tags['vitality'] <= 0: return act
68 g = self.game
70 if g.user_input([tgt], ChooseOptionInputlet(self, (False, True))):
71 tgt.tags['vitality'] -= 1
72 g.process_action(ReforgeWeapon(tgt, tgt, c))
73 act.cancelled = True
75 return act
78@register_eh
79class EquipmentTransferHandler(EventHandler):
80 interested = ['post_card_migration']
82 def handle(self, evt, trans):
83 if evt == 'post_card_migration': 83 ↛ 96line 83 didn't jump to line 96, because the condition on line 83 was never false
84 assert isinstance(trans, MigrateCardsTransaction)
85 for m in trans.movements:
86 if m.fr.type == 'equips':
87 try:
88 m.fr.owner.skills.remove(m.card.equipment_skill)
89 except ValueError:
90 pass
92 if m.to.type == 'equips':
93 if m.card.equipment_skill: 93 ↛ 85line 93 didn't jump to line 85, because the condition on line 93 was never false
94 m.to.owner.skills.append(m.card.equipment_skill)
96 return trans
99class ShieldSkill(Skill):
100 associated_action = None
101 target = t_None()
104class OpticalCloakSkill(TreatAs, ShieldSkill): # just a tag
105 treat_as = PhysicalCard.classes['GrazeCard']
106 skill_category = ['equip', 'passive']
108 def check(self):
109 return False
112class OpticalCloak(FatetellAction):
113 # 光学迷彩
114 def __init__(self, source, target):
115 self.source = source
116 self.target = target
117 self.fatetell_target = target
118 self.fatetell_cond = lambda card: card.color == Card.RED
120 def fatetell_action(self, ft):
121 self.fatetell_card = ft.card
122 return bool(ft.succeeded)
125@register_eh
126class OpticalCloakHandler(EventHandler):
127 interested = ['action_apply']
128 execute_after = ['AssistedGrazeHandler']
130 def handle(self, evt_type, act):
131 from .basic import BaseUseGraze
132 if evt_type == 'action_apply' and isinstance(act, BaseUseGraze):
133 tgt = act.target
134 if not tgt.has_skill(OpticalCloakSkill): return act
135 if act.card: return act 135 ↛ exitline 135 didn't return from function 'handle', because the return on line 135 wasn't executed
137 g = self.game
139 if not g.user_input([tgt], ChooseOptionInputlet(self, (False, True))):
140 return act
142 if g.process_action(OpticalCloak(tgt, tgt)):
143 act.card = OpticalCloakSkill(tgt)
145 return act
148class MomijiShieldSkill(ShieldSkill):
149 skill_category = ['equip', 'passive']
152class MomijiShield(GenericAction):
153 def __init__(self, act):
154 self.action = act
155 self.source = self.target = act.target
157 def apply_action(self):
158 self.action.cancelled = True
159 return True
162@register_eh
163class MomijiShieldHandler(EventHandler):
164 interested = ['action_before']
165 execute_before = ['HouraiJewelHandler']
167 def handle(self, evt_type, act):
168 from .basic import BaseAttack
169 if not (evt_type == 'action_before' and isinstance(act, BaseAttack)): return act
170 tgt = act.target
171 if not tgt.has_skill(MomijiShieldSkill): return act
172 if not act.associated_card.color == Card.BLACK: return act
173 g = self.game
174 g.process_action(MomijiShield(act))
176 return act
179class UFOSkill(Skill):
180 associated_action = None
181 skill_category = ['equip', 'passive']
182 target = t_None()
185class GreenUFOSkill(UFOSkill):
186 increment = 1
189class RedUFOSkill(UFOSkill):
190 increment = 1
193@register_eh
194class UFODistanceHandler(EventHandler):
195 interested = ['calcdistance']
197 def handle(self, evt_type, arg):
198 if not evt_type == 'calcdistance': return arg 198 ↛ exitline 198 didn't return from function 'handle', because the return on line 198 wasn't executed
200 src, card, dist = arg
201 for s in src.skills:
202 if not issubclass(s, RedUFOSkill): continue
203 if not src.has_skill(s): continue 203 ↛ 201line 203 didn't jump to line 201, because the continue on line 203 wasn't executed
204 incr = s.increment
205 incr = incr(src) if callable(incr) else incr
206 for p in dist:
207 dist[p] -= incr
209 for p in dist:
210 for s in p.skills:
211 if not issubclass(s, GreenUFOSkill): continue
212 if not p.has_skill(s): continue 212 ↛ 210line 212 didn't jump to line 210, because the continue on line 212 wasn't executed
213 incr = s.increment
214 dist[p] += incr(p) if callable(incr) else incr
216 return arg
219class WeaponSkill(Skill):
220 range = 1
223class RoukankenSkill(WeaponSkill):
224 associated_action = None
225 skill_category = ['equip', 'passive']
226 target = t_None()
227 range = 3
230class Roukanken(GenericAction):
231 def __init__(self, act):
232 assert isinstance(act, basic.BaseAttack)
233 self.action = act
234 self.source = act.source
235 self.target = act.target
237 def apply_action(self):
238 act = self.action
239 tgt = act.target
241 skills = [s for s in tgt.skills if issubclass(s, ShieldSkill)]
242 for s in skills:
243 tgt.disable_skill(s, 'roukanken')
245 act.roukanken_disabled_skills = skills
246 return self.game.process_action(act)
249@register_eh
250class RoukankenEffectHandler(EventHandler):
251 interested = ('action_before', 'action_done',)
252 execute_before = (
253 'MomijiShieldHandler',
254 'OpticalCloakHandler',
255 'SaigyouBranchHandler',
256 'HouraiJewelHandler',
257 'SpearTheGungnirHandler',
258 'HakuroukenHandler',
259 'FreakingPowerHandler',
260 'ResonanceHandler'
261 )
263 @classmethod
264 def handle(cls, evt_type, act):
265 if evt_type == 'action_before' and isinstance(act, basic.BaseAttack):
266 if act.cancelled: 266 ↛ 267line 266 didn't jump to line 267, because the condition on line 266 was never true
267 return act
269 if act._[cls]:
270 return act
272 act._[cls] = 'roukanken-processed'
274 source = act.source
275 if source.has_skill(RoukankenSkill):
276 act = Roukanken(act)
277 return act
279 if evt_type == 'action_done' and isinstance(act, basic.BaseAttack):
280 if hasattr(act, 'roukanken_tag'): 280 ↛ 281line 280 didn't jump to line 281, because the condition on line 280 was never true
281 for s in getattr(act, 'roukanken_disabled_skills', []):
282 act.target.reenable_skill('roukanken')
284 return act
287class NenshaPhoneSkill(WeaponSkill):
288 associated_action = None
289 skill_category = ['equip', 'passive']
290 target = t_None()
291 range = 4
294class NenshaPhone(GenericAction):
295 def apply_action(self):
296 tgt = self.target
298 cards = list(tgt.cards)[:2]
299 g = self.game
300 g.players.exclude(tgt).reveal(cards)
301 migrate_cards(cards, tgt.showncards)
303 return True
306@register_eh
307class NenshaPhoneHandler(EventHandler):
308 interested = ['action_after']
310 def handle(self, evt_type, act):
311 if evt_type == 'action_after' and isinstance(act, Damage):
312 if not act.succeeded: return act 312 ↛ exitline 312 didn't return from function 'handle', because the return on line 312 wasn't executed
313 g = self.game
314 pa = g.action_stack[-1]
315 if not isinstance(pa, basic.BaseAttack): return act
317 src = act.source
318 tgt = act.target
319 if tgt.dead: return act
320 if not tgt.cards: return act
321 if not src.has_skill(NenshaPhoneSkill): return act
322 if not g.user_input([src], ChooseOptionInputlet(self, (False, True))): return act
323 g.process_action(NenshaPhone(src, tgt))
325 return act
328class ElementalReactorSkill(WeaponSkill):
329 associated_action = None
330 skill_category = ['equip', 'passive']
331 target = t_None()
332 range = 1
335@register_eh
336class ElementalReactorHandler(EventHandler):
337 interested = ['action_stage_action', 'post_card_migration']
338 execute_after = ['EquipmentTransferHandler']
340 def handle(self, evt_type, arg):
341 if evt_type == 'action_stage_action':
342 tgt = arg
343 if not tgt.has_skill(ElementalReactorSkill): return arg
344 basic.AttackCardVitalityHandler.disable(tgt)
346 elif evt_type == 'post_card_migration': 346 ↛ 354line 346 didn't jump to line 354, because the condition on line 346 was never false
347 trans = cast(MigrateCardsTransaction, arg)
348 from .definition import ElementalReactorCard
350 for m in trans.movements:
351 if m.fr.type == 'equips' and m.card.is_card(ElementalReactorCard):
352 basic.AttackCardVitalityHandler.enable(m.fr.owner)
354 return arg
357class GungnirSkill(TreatAs, WeaponSkill):
358 target = t_OtherOne()
359 skill_category = ['equip', 'active']
360 range = 3
361 treat_as = PhysicalCard.classes['AttackCard'] # arghhhhh, nasty circular references!
363 def check(self):
364 cl = self.associated_cards
365 cat = ('cards', 'showncards')
366 if not all(c.resides_in.type in cat for c in cl): return False
367 if not all(c.is_card(PhysicalCard) for c in cl): return False 367 ↛ exitline 367 didn't return from function 'check', because the return on line 367 wasn't executed
368 return len(cl) == 2
371class ScarletRhapsody(ForEach):
372 action_cls = basic.Attack
375class ScarletRhapsodySkill(WeaponSkill):
376 range = 4
377 associated_action = ScarletRhapsody
378 category = ['skill', 'treat_as', 'basic']
379 skill_category = ['equip', 'active']
380 target = t_OtherLessEqThanN(3)
381 usage = 'launch'
383 def check(self):
384 try:
385 cl = self.associated_cards
386 check(len(cl) == 1)
387 card = cl[0]
388 from .definition import AttackCard
389 check(card.is_card(AttackCard))
390 tgt = card.resides_in.owner
391 check(card.is_card(PhysicalCard))
393 check(card.resides_in in (tgt.cards, tgt.showncards))
394 check(card in set(tgt.cards) or card in set(tgt.showncards))
396 check(set(tgt.cards) | set(tgt.showncards) == set([card]))
398 return True
399 except CheckFailed:
400 return False
402 def is_card(self, cls):
403 from thb.cards.definition import AttackCard
404 if issubclass(AttackCard, cls): return True
405 return isinstance(self, cls)
407 @property
408 def distance(self):
409 try:
410 return max(1, self.associated_cards[0].distance)
411 except Exception:
412 return 1
415class RepentanceStickSkill(WeaponSkill):
416 range = 2
417 skill_category = ['equip', 'passive']
418 associated_action = None
419 target = t_None()
422class RepentanceStick(GenericAction):
423 def apply_action(self) -> bool:
424 src, tgt = self.source, self.target
425 g = self.game
427 catnames = ('cards', 'showncards', 'equips', 'fatetell')
428 cats = [getattr(tgt, i) for i in catnames]
430 l: List[PhysicalCard] = []
431 for i in range(2):
432 if not (tgt.cards or tgt.showncards or tgt.equips or tgt.fatetell):
433 break
435 card = g.user_input(
436 [src], ChoosePeerCardInputlet(self, tgt, catnames)
437 )
439 if not card:
440 card = random_choose_card(g, cats)
441 if card: 441 ↛ 431line 441 didn't jump to line 431, because the condition on line 441 was never false
442 l.append(card)
443 g.players.exclude(tgt).player.reveal(card)
444 g.process_action(DropCards(src, tgt, [card]))
446 self.cards = l
447 return True
450@register_eh
451class RepentanceStickHandler(EventHandler):
452 interested = ['action_before']
453 execute_before = ['WineHandler']
455 def handle(self, evt_type, act):
456 if evt_type == 'action_before' and isinstance(act, Damage):
457 if act.cancelled: return act 457 ↛ exitline 457 didn't return from function 'handle', because the return on line 457 wasn't executed
458 src, tgt = act.source, act.target
459 if src and src.has_skill(RepentanceStickSkill):
460 g = self.game
461 pa = g.action_stack[-1]
462 if not isinstance(pa, basic.BaseAttack): return act
463 if not (tgt.cards or tgt.showncards or tgt.equips or tgt.fatetell):
464 return act
466 if not g.user_input([src], ChooseOptionInputlet(self, (False, True))):
467 return act
469 g.process_action(RepentanceStick(src, tgt))
470 act.cancelled = True
472 return act
475class IbukiGourdSkill(RedUFOSkill):
476 skill_category = ['equip', 'passive']
477 increment = 0
480@register_eh
481class IbukiGourdHandler(EventHandler):
482 interested = ('action_apply', 'action_after', 'card_migration')
484 def handle(self, evt_type, act):
485 if evt_type == 'action_after' and isinstance(act, Damage):
486 src = act.source
487 if not src: return act
488 if not src.has_skill(IbukiGourdSkill): return act
490 g = self.game
491 ttags(src)['ibukigourd_did_damage'] = True
493 elif evt_type == 'action_apply' and isinstance(act, FinalizeStage):
494 tgt = act.target
495 if not tgt.has_skill(IbukiGourdSkill): return act
497 g = self.game
498 if ttags(tgt)['ibukigourd_did_damage']: return act
500 g.process_action(basic.Wine(tgt, tgt))
502 elif evt_type == 'post_card_migration': 502 ↛ 503line 502 didn't jump to line 503, because the condition on line 502 was never true
503 from .definition import IbukiGourdCard
504 trans = cast(MigrateCardsTransaction, act)
506 for m in trans.movements:
507 if m.card.is_card(IbukiGourdCard) and m.to.type == 'equips':
508 tgt = m.to.owner
509 g = self.game
510 g.process_action(basic.Wine(tgt, tgt))
512 return act
515class HouraiJewelAttack(basic.BaseAttack, spellcard.InstantSpellCardAction):
516 def apply_action(self):
517 g = self.game
518 g.process_action(Damage(self.source, self.target))
519 return True
522class HouraiJewelSkill(WeaponSkill):
523 associated_action = None
524 skill_category = ['equip', 'passive']
525 target = t_None()
526 range = 1
529@register_eh
530class HouraiJewelHandler(EventHandler):
531 interested = ['action_before']
532 execute_before = ['RejectHandler', 'WineHandler'] # wine does not affect this.
534 def handle(self, evt_type, act):
535 if evt_type == 'action_before' and isinstance(act, basic.Attack):
536 src = act.source
537 if not src.has_skill(HouraiJewelSkill): return act
538 if isinstance(act, HouraiJewelAttack): return act 538 ↛ exitline 538 didn't return from function 'handle', because the return on line 538 wasn't executed
539 g = self.game
540 if g.user_input([src], ChooseOptionInputlet(self, (False, True))):
541 act.__class__ = classmix(HouraiJewelAttack, act.__class__)
543 return act
546class UmbrellaSkill(ShieldSkill):
547 skill_category = ['equip', 'passive']
550class UmbrellaEffect(GenericAction):
551 def __init__(self, act, damage_act):
552 self.source = self.target = damage_act.target
553 self.action = act
554 self.damage_act = damage_act
556 def apply_action(self):
557 self.damage_act.cancelled = True
558 return True
561@register_eh
562class UmbrellaHandler(EventHandler):
563 # 紫的阳伞
564 interested = ['action_before']
565 execute_before = ['RejectHandler']
567 def handle(self, evt_type, act):
568 if evt_type == 'action_before' and isinstance(act, Damage):
569 if not act.target.has_skill(UmbrellaSkill): return act
570 g = self.game
571 pact = g.action_stack[-1]
573 if isinstance(pact, spellcard.SpellCardAction):
574 self.game.process_action(UmbrellaEffect(pact, act))
576 return act
579class MaidenCostume(TreatAs, ShieldSkill):
580 treat_as = PhysicalCard.classes['RejectCard']
581 skill_category = ['equip', 'passive']
583 def check(self):
584 return False
587class MaidenCostumeAction(FatetellAction):
588 def __init__(self, source, act):
589 self.source = source
590 self.target = source
591 self.fatetell_target = source
592 self.fatetell_cond = lambda c: 9 <= c.number <= 13
593 self.act = act
595 def fatetell_action(self, ft):
596 act = self.act
597 src = self.source
599 g = self.game
600 if ft.succeeded:
601 # rej = spellcard.LaunchReject(src, act, SaigyouBranchSkill(src))
602 g.process_action(LaunchCard(
603 src, [act.target], MaidenCostume(src), spellcard.Reject(src, act)
604 ))
605 return True
606 else:
607 return False
610@register_eh
611class MaidenCostumeHandler(EventHandler):
612 interested = ['action_before']
613 execute_before = ['RejectHandler']
614 execute_after = ['HouraiJewelHandler']
616 def handle(self, evt_type, act):
617 if evt_type == 'action_before' and isinstance(act, spellcard.SpellCardAction):
618 tgt = act.target
619 if not tgt.has_skill(MaidenCostume): return act
620 if act.cancelled: return act 620 ↛ exitline 620 didn't return from function 'handle', because the return on line 620 wasn't executed
621 if isinstance(act, spellcard.Reject): return act # can't respond to reject
622 g = self.game
623 if not g.user_input([tgt], ChooseOptionInputlet(self, (False, True))):
624 return act
626 self.game.process_action(MaidenCostumeAction(tgt, act))
628 return act
631class HakuroukenSkill(WeaponSkill):
632 range = 2
633 skill_category = ['equip', 'passive']
634 associated_action = None
635 target = t_None()
638class Hakurouken(GenericAction):
639 card_usage = 'drop'
641 def apply_action(self):
642 src = self.source
643 tgt = self.target
645 cards = user_choose_cards(self, tgt, ('cards', 'showncards'))
646 g = self.game
647 if cards: 647 ↛ 648line 647 didn't jump to line 648, because the condition on line 647 was never true
648 self.peer_action = 'drop'
649 g.process_action(DropCards(src, tgt, cards))
650 else:
651 self.peer_action = 'draw'
652 g.process_action(DrawCards(src, 1))
654 return True
656 def cond(self, cards):
657 return len(cards) == 1 and not cards[0].is_card(Skill)
660@register_eh
661class HakuroukenHandler(EventHandler):
662 interested = ['action_before']
664 # BUG WITH OUT THIS LINE:
665 # src equips [Gourd, Hakurouken], tgt drops Exinwan
666 # then src drops Gourd,
667 # but Attack.damage == 1, Wine tag preserved.
668 execute_before = ['WineHandler', 'MomijiShieldHandler']
670 def handle(self, evt_type, act):
671 if evt_type == 'action_before' and isinstance(act, basic.BaseAttack):
672 if act.cancelled: return act 672 ↛ exitline 672 didn't return from function 'handle', because the return on line 672 wasn't executed
673 src = act.source
674 if not src.has_skill(HakuroukenSkill): return act
675 card = act.associated_card
676 if not card.suit == Card.CLUB: return act
678 g = self.game
679 if not g.user_input([src], ChooseOptionInputlet(self, (False, True))):
680 return act
682 g.process_action(Hakurouken(src, act.target))
684 return act
687class AyaRoundfan(GenericAction):
688 def apply_action(self):
689 src, tgt = self.source, self.target
690 g = self.game
692 equip = g.user_input([src], ChoosePeerCardInputlet(self, tgt, ['equips']))
693 equip = equip or random_choose_card(g, [tgt.equips])
694 g.process_action(DropCards(src, tgt, [equip]))
695 self.card = equip
697 return True
699 def is_valid(self):
700 # Proton 2015/4/2 0:26:51
701 # 小爱打幽香,打中以后,弃丸子发动团扇
702 # Proton 2015/4/2 0:26:59
703 # 然后弃了自己的装备
704 # Proton 2015/4/2 0:27:25
705 # 发动了于是又弃了幽香的装备
706 # Proton 2015/4/2 0:27:41
707 # 发动了小小军势
708 # Proton 2015/4/2 0:28:02
709 # 这时候针妙丸又来了一发,幽香卒
710 # 0:28:19
711 # 八咫乌鸦 2015/4/2 0:28:19
712 # 喜闻乐见的插入结算
713 # Proton 2015/4/2 0:28:49
714 # 嗯 然后就回到了团扇的效果
715 # Proton 2015/4/2 0:29:08
716 # 团扇弃置cost把对面弃死了啊!
717 # Proton 2015/4/2 0:29:38
718 # 然后后半段效果都假设是对面还活着
719 # Proton 2015/4/2 0:29:44
720 # 崩
722 # 所以加上这个
723 return self.target.equips
726class AyaRoundfanSkill(WeaponSkill):
727 range = 5
728 skill_category = ['equip', 'passive']
729 associated_action = None
730 target = t_None()
733@register_eh
734class AyaRoundfanHandler(EventHandler):
735 interested = ('action_after',)
736 execute_after = ('DyingHandler', )
737 card_usage = 'drop'
739 def handle(self, evt_type, act):
740 if evt_type == 'action_after' and isinstance(act, Damage):
741 if not act.succeeded: return act 741 ↛ exitline 741 didn't return from function 'handle', because the return on line 741 wasn't executed
742 src, tgt = act.source, act.target
743 if not (src and src.has_skill(AyaRoundfanSkill) and tgt.equips): return act
745 g = self.game
746 pa = g.action_stack[-1]
747 if not isinstance(pa, basic.BaseAttack): return act
749 cards = user_choose_cards(self, src, ('cards', 'showncards'))
750 if not cards: return act 750 ↛ 751line 750 didn't jump to line 751, because the condition on line 750 was never false
751 g.process_action(DropCards(src, src, cards))
752 g.process_action(AyaRoundfan(src, tgt))
754 return act
756 def cond(self, cards):
757 if not len(cards) == 1 or cards[0].is_card(Skill): 757 ↛ 760line 757 didn't jump to line 760, because the condition on line 757 was never false
758 return False
760 return cards[0].resides_in.type in ('cards', 'showncards')
763class Laevatein(UserAction):
764 def apply_action(self):
765 return True # logic handled in LaevateinHandler
768class LaevateinSkill(WeaponSkill):
769 range = 3
770 skill_category = ['equip', 'passive']
771 associated_action = None
772 target = t_None()
775@register_eh
776class LaevateinHandler(EventHandler):
777 interested = ['attack_aftergraze']
778 card_usage = 'drop'
780 def handle(self, evt_type, arg):
781 if evt_type == 'attack_aftergraze': 781 ↛ 801line 781 didn't jump to line 801, because the condition on line 781 was never false
782 act, succeed = arg
783 assert isinstance(act, basic.BaseAttack)
784 if succeed:
785 return arg
787 src = act.source
788 tgt = act.target
789 if not src or not src.has_skill(LaevateinSkill): 789 ↛ 792line 789 didn't jump to line 792, because the condition on line 789 was never false
790 return arg
792 g = self.game
793 cards = user_choose_cards(self, src, ('cards', 'showncards', 'equips'))
794 if not cards:
795 return arg
797 g.process_action(DropCards(src, src, cards))
798 g.process_action(Laevatein(src, tgt))
799 return act, True
801 return arg
803 def cond(self, cards):
804 if not len(cards) == 2: return False
806 from thb.cards.definition import LaevateinCard
807 for c in cards:
808 t = c.resides_in.type
809 if t not in ('cards', 'showncards', 'equips'):
810 return False
811 elif t == 'equips' and c.is_card(LaevateinCard):
812 return False
813 elif c.is_card(Skill):
814 return False
816 return True
819class DeathSickleSkill(WeaponSkill):
820 range = 2
821 skill_category = ['equip', 'passive']
822 associated_action = None
823 target = t_None()
826class DeathSickle(GenericAction):
827 def __init__(self, act):
828 self.action = act
829 self.source, self.target = act.source, act.target
831 def apply_action(self):
832 self.action.amount += 1
833 return True
836@register_eh
837class DeathSickleHandler(EventHandler):
838 interested = ['action_before']
839 execute_before = ['WineHandler']
841 def handle(self, evt_type, act):
842 if evt_type == 'action_before' and isinstance(act, Damage):
843 from .basic import Attack
844 g = self.game
845 pact = g.action_stack[-1]
846 if not isinstance(pact, Attack): return act
847 src = act.source
848 if not src or not src.has_skill(DeathSickleSkill): return act
849 tgt = act.target
850 if len(tgt.cards) + len(tgt.showncards) == 0:
851 g.process_action(DeathSickle(act))
853 return act
856class KeystoneSkill(GreenUFOSkill):
857 skill_category = ['equip', 'passive']
858 increment = 1
861class Keystone(GenericAction):
862 def __init__(self, act):
863 assert isinstance(act, spellcard.Sinsack)
864 self.source = self.target = act.target
865 self.action = act
867 def apply_action(self):
868 self.action.cancelled = True
869 return True
872@register_eh
873class KeystoneHandler(EventHandler):
874 interested = ['action_before']
875 execute_before = ['SaigyouBranchHandler', 'RejectHandler']
877 def handle(self, evt_type, act):
878 if evt_type == 'action_before' and isinstance(act, spellcard.Sinsack):
879 tgt = act.target
880 if tgt.has_skill(KeystoneSkill):
881 self.game.process_action(Keystone(act))
883 return act
886class WitchBroomSkill(RedUFOSkill):
887 skill_category = ['equip', 'passive']
888 increment = 2
891class AccessoriesSkill(Skill):
892 associated_action = None
893 target = t_None()
896class YinYangOrb(GenericAction):
897 def __init__(self, ft):
898 self.ftact = ft
899 self.source = self.target = ft.target
901 def apply_action(self):
902 ft = self.ftact
903 tgt = ft.target
905 from .definition import YinYangOrbCard
906 for e in tgt.equips: 906 ↛ 916line 906 didn't jump to line 916, because the loop on line 906 didn't complete
907 if e.is_card(YinYangOrbCard):
908 with MigrateCardsTransaction(self) as trans:
909 migrate_cards([ft.card], tgt.cards, unwrap=True, trans=trans)
910 detach_cards([e], trans=trans)
911 self.card = e
912 ft.set_card(e, self)
914 break
915 else:
916 raise GameError('Player has YinYangOrb skill but no equip!')
918 return True
921class YinYangOrbSkill(AccessoriesSkill):
922 skill_category = ['equip', 'passive']
925@register_eh
926class YinYangOrbHandler(EventHandler):
927 interested = ['fatetell']
928 execute_after = ['FatetellMalleateHandler']
930 def handle(self, evt_type, act):
931 if evt_type == 'fatetell': 931 ↛ 940line 931 didn't jump to line 940, because the condition on line 931 was never false
932 g = self.game
933 tgt = act.target
934 if not tgt.has_skill(YinYangOrbSkill): return act
935 if not g.user_input([tgt], ChooseOptionInputlet(self, (False, True))):
936 return act
938 g.process_action(YinYangOrb(act))
940 return act
943class SuwakoHatSkill(AccessoriesSkill):
944 skill_category = ['equip', 'passive']
947class SuwakoHatEffect(UserAction):
948 def __init__(self, target, dcs):
949 self.source = self.target = target
950 self.dcs = dcs
952 def apply_action(self):
953 self.dcs.dropn = max(self.dcs.dropn - 2, 0)
954 return True
957@register_eh
958class SuwakoHatHandler(EventHandler):
959 interested = ['action_before']
961 def handle(self, evt_type, act):
962 if evt_type == 'action_before' and isinstance(act, DropCardStage):
963 tgt = act.target
964 if tgt.has_skill(SuwakoHatSkill):
965 self.game.process_action(SuwakoHatEffect(tgt, act))
967 return act
970class YoumuPhantomSkill(AccessoriesSkill):
971 skill_category = ['equip', 'passive']
974class YoumuPhantomHeal(basic.Heal):
975 pass
978@register_eh
979class YoumuPhantomHandler(EventHandler):
980 interested = ['post_card_migration']
982 def handle(self, evt_type, arg):
983 if not evt_type == 'post_card_migration': return arg 983 ↛ exitline 983 didn't return from function 'handle', because the return on line 983 wasn't executed
985 from .definition import YoumuPhantomCard
986 trans = cast(MigrateCardsTransaction, arg)
987 g = self.game
989 for m in trans.movements:
990 if not m.card.is_card(YoumuPhantomCard):
991 continue
993 if m.fr.type == 'equips':
994 tgt = m.fr.owner
995 g.process_action(MaxLifeChange(tgt, tgt, -1))
996 if not tgt.dead:
997 g.process_action(YoumuPhantomHeal(tgt, tgt))
999 if m.to.type == 'equips':
1000 tgt = m.to.owner
1001 g.process_action(MaxLifeChange(tgt, tgt, 1))
1003 return arg
1006class IceWingSkill(RedUFOSkill):
1007 skill_category = ['equip', 'passive']
1008 increment = 1
1011class IceWing(GenericAction):
1012 def __init__(self, act):
1013 assert isinstance(act, (spellcard.SealingArray, spellcard.FrozenFrog))
1014 self.source = self.target = act.target
1015 self.action = act
1017 def apply_action(self):
1018 self.action.cancelled = True
1019 return True
1022@register_eh
1023class IceWingHandler(EventHandler):
1024 interested = ['action_before']
1025 _effect_cls = spellcard.SealingArray, spellcard.FrozenFrog
1027 execute_before = ['RejectHandler', 'SaigyouBranchHandler']
1029 def handle(self, evt_type, act):
1030 if evt_type == 'action_before' and isinstance(act, self._effect_cls):
1031 if act.target.has_skill(IceWingSkill):
1032 self.game.process_action(IceWing(act))
1034 return act
1037class GrimoireSkill(TreatAs, WeaponSkill):
1038 skill_category = ['equip', 'active']
1039 range = 1
1040 lookup_tbl = {
1041 Card.SPADE: PhysicalCard.classes['DemonParadeCard'], # again...
1042 Card.HEART: PhysicalCard.classes['FeastCard'],
1043 Card.CLUB: PhysicalCard.classes['MapCannonCard'],
1044 Card.DIAMOND: PhysicalCard.classes['HarvestCard'],
1045 }
1047 @property
1048 def treat_as(self):
1049 cl = self.associated_cards
1050 if not (cl and cl[0].suit):
1051 from .base import DummyCard
1052 return DummyCard
1053 return self.lookup_tbl[cl[0].suit]
1055 def check(self):
1056 cl = self.associated_cards
1057 if not len(cl) == 1: return False
1058 if not cl[0].resides_in.type in ('cards', 'showncards', 'equips'): 1058 ↛ 1059line 1058 didn't jump to line 1059, because the condition on line 1058 was never true
1059 return False
1060 if not cl[0].suit: return False 1060 ↛ exitline 1060 didn't return from function 'check', because the return on line 1060 wasn't executed
1061 return True
1064@register_eh
1065class GrimoireHandler(EventHandler):
1066 interested = ['action_after', 'action_shootdown']
1068 def handle(self, evt_type, act):
1069 if evt_type == 'action_shootdown':
1070 if not isinstance(act, LaunchCard): return act
1071 c = act.card
1072 if c.is_card(GrimoireSkill):
1073 src = act.source
1074 t = src.tags
1076 if t['turn_count'] <= t['grimoire_tag']: 1076 ↛ 1077line 1076 didn't jump to line 1077, because the condition on line 1076 was never true
1077 raise ActionLimitExceeded
1079 if t['vitality'] <= 0:
1080 raise VitalityLimitExceeded
1082 elif evt_type == 'action_after' and isinstance(act, LaunchCard):
1083 c = act.card
1084 if c.is_card(GrimoireSkill):
1085 t = act.source.tags
1086 t['vitality'] -= 1
1087 t['grimoire_tag'] = t['turn_count']
1089 return act
1092class SinsackHatAction(FatetellAction):
1093 def __init__(self, source, target, hat_card):
1094 self.source = source
1095 self.target = target
1096 self.hat_card = hat_card
1097 self.fatetell_target = target
1099 self.fatetell_cond = lambda c: c.suit == Card.SPADE and 1 <= c.number <= 8
1101 def fatetell_action(self, ft):
1102 if not ft.succeeded:
1103 return False
1105 g = self.game
1106 tgt, c = self.target, self.hat_card
1107 g.process_action(spellcard.SinsackDamage(None, tgt, amount=2))
1108 migrate_cards([c], tgt.cards, unwrap=True)
1109 return True
1112class SinsackHat(ShieldSkill):
1113 skill_category = ['equip', 'passive']
1116@register_eh
1117class SinsackHatHandler(EventHandler):
1118 interested = ['action_after']
1120 def handle(self, evt_type, act):
1121 if evt_type == 'action_after' and isinstance(act, FatetellStage):
1122 tgt = act.target
1123 g = self.game
1124 from .definition import SinsackHatCard
1125 for c in list(tgt.equips):
1126 if not c.is_card(SinsackHatCard):
1127 continue
1129 g.process_action(SinsackHatAction(tgt, tgt, c))
1130 return act
1132 return act