Coverage for thb/cards/equipment.py : 95%
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:
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
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):
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):
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'): 809 ↛ 810line 809 didn't jump to line 810, because the condition on line 809 was never true
810 return False
811 elif t == 'equips' and c.is_card(LaevateinCard): 811 ↛ 812line 811 didn't jump to line 812, because the condition on line 811 was never true
812 return False
813 elif c.is_card(Skill): 813 ↛ 814line 813 didn't jump to line 814, because the condition on line 813 was never true
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 ↛ 918line 906 didn't jump to line 918, 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 migrate_cards([e], tgt.special, trans=trans)
912 detach_cards([e], trans=trans)
913 self.card = e
914 ft.set_card(e, self)
916 break
917 else:
918 raise GameError('Player has YinYangOrb skill but no equip!')
920 return True
923class YinYangOrbSkill(AccessoriesSkill):
924 skill_category = ['equip', 'passive']
927@register_eh
928class YinYangOrbHandler(EventHandler):
929 interested = ['fatetell']
930 execute_after = ['FatetellMalleateHandler']
932 def handle(self, evt_type, act):
933 if evt_type == 'fatetell': 933 ↛ 942line 933 didn't jump to line 942, because the condition on line 933 was never false
934 g = self.game
935 tgt = act.target
936 if not tgt.has_skill(YinYangOrbSkill): return act
937 if not g.user_input([tgt], ChooseOptionInputlet(self, (False, True))):
938 return act
940 g.process_action(YinYangOrb(act))
942 return act
945class SuwakoHatSkill(AccessoriesSkill):
946 skill_category = ['equip', 'passive']
949class SuwakoHatEffect(UserAction):
950 def __init__(self, target, dcs):
951 self.source = self.target = target
952 self.dcs = dcs
954 def apply_action(self):
955 self.dcs.dropn = max(self.dcs.dropn - 2, 0)
956 return True
959@register_eh
960class SuwakoHatHandler(EventHandler):
961 interested = ['action_before']
963 def handle(self, evt_type, act):
964 if evt_type == 'action_before' and isinstance(act, DropCardStage):
965 tgt = act.target
966 if tgt.has_skill(SuwakoHatSkill):
967 self.game.process_action(SuwakoHatEffect(tgt, act))
969 return act
972class YoumuPhantomSkill(AccessoriesSkill):
973 skill_category = ['equip', 'passive']
976class YoumuPhantomHeal(basic.Heal):
977 pass
980@register_eh
981class YoumuPhantomHandler(EventHandler):
982 interested = ['post_card_migration']
984 def handle(self, evt_type, arg):
985 if not evt_type == 'post_card_migration': return arg 985 ↛ exitline 985 didn't return from function 'handle', because the return on line 985 wasn't executed
987 from .definition import YoumuPhantomCard
988 trans = cast(MigrateCardsTransaction, arg)
989 g = self.game
991 for m in trans.movements:
992 if not m.card.is_card(YoumuPhantomCard):
993 continue
995 if m.fr.type == 'equips':
996 tgt = m.fr.owner
997 g.process_action(MaxLifeChange(tgt, tgt, -1))
998 if not tgt.dead:
999 g.process_action(YoumuPhantomHeal(tgt, tgt))
1001 if m.to.type == 'equips':
1002 tgt = m.to.owner
1003 g.process_action(MaxLifeChange(tgt, tgt, 1))
1005 return arg
1008class IceWingSkill(RedUFOSkill):
1009 skill_category = ['equip', 'passive']
1010 increment = 1
1013class IceWing(GenericAction):
1014 def __init__(self, act):
1015 assert isinstance(act, (spellcard.SealingArray, spellcard.FrozenFrog))
1016 self.source = self.target = act.target
1017 self.action = act
1019 def apply_action(self):
1020 self.action.cancelled = True
1021 return True
1024@register_eh
1025class IceWingHandler(EventHandler):
1026 interested = ['action_before']
1027 _effect_cls = spellcard.SealingArray, spellcard.FrozenFrog
1029 execute_before = ['RejectHandler', 'SaigyouBranchHandler']
1031 def handle(self, evt_type, act):
1032 if evt_type == 'action_before' and isinstance(act, self._effect_cls):
1033 if act.target.has_skill(IceWingSkill):
1034 self.game.process_action(IceWing(act))
1036 return act
1039class GrimoireSkill(TreatAs, WeaponSkill):
1040 skill_category = ['equip', 'active']
1041 range = 1
1042 lookup_tbl = {
1043 Card.SPADE: PhysicalCard.classes['DemonParadeCard'], # again...
1044 Card.HEART: PhysicalCard.classes['FeastCard'],
1045 Card.CLUB: PhysicalCard.classes['MapCannonCard'],
1046 Card.DIAMOND: PhysicalCard.classes['HarvestCard'],
1047 }
1049 @property
1050 def treat_as(self):
1051 cl = self.associated_cards
1052 if not (cl and cl[0].suit):
1053 from .base import DummyCard
1054 return DummyCard
1055 return self.lookup_tbl[cl[0].suit]
1057 def check(self):
1058 cl = self.associated_cards
1059 if not len(cl) == 1: return False
1060 if not cl[0].resides_in.type in ('cards', 'showncards', 'equips'): 1060 ↛ 1061line 1060 didn't jump to line 1061, because the condition on line 1060 was never true
1061 return False
1062 if not cl[0].suit: return False 1062 ↛ exitline 1062 didn't return from function 'check', because the return on line 1062 wasn't executed
1063 return True
1066@register_eh
1067class GrimoireHandler(EventHandler):
1068 interested = ['action_after', 'action_shootdown']
1070 def handle(self, evt_type, act):
1071 if evt_type == 'action_shootdown':
1072 if not isinstance(act, LaunchCard): return act
1073 c = act.card
1074 if c.is_card(GrimoireSkill):
1075 src = act.source
1076 t = src.tags
1078 if t['turn_count'] <= t['grimoire_tag']:
1079 raise ActionLimitExceeded
1081 if t['vitality'] <= 0:
1082 raise VitalityLimitExceeded
1084 elif evt_type == 'action_after' and isinstance(act, LaunchCard):
1085 c = act.card
1086 if c.is_card(GrimoireSkill):
1087 t = act.source.tags
1088 t['vitality'] -= 1
1089 t['grimoire_tag'] = t['turn_count']
1091 return act
1094class SinsackHatAction(FatetellAction):
1095 def __init__(self, source, target, hat_card):
1096 self.source = source
1097 self.target = target
1098 self.hat_card = hat_card
1099 self.fatetell_target = target
1101 self.fatetell_cond = lambda c: c.suit == Card.SPADE and 1 <= c.number <= 8
1103 def fatetell_action(self, ft):
1104 if not ft.succeeded:
1105 return False
1107 g = self.game
1108 tgt, c = self.target, self.hat_card
1109 g.process_action(spellcard.SinsackDamage(None, tgt, amount=2))
1110 migrate_cards([c], tgt.cards, unwrap=True)
1111 return True
1114class SinsackHat(ShieldSkill):
1115 skill_category = ['equip', 'passive']
1118@register_eh
1119class SinsackHatHandler(EventHandler):
1120 interested = ['action_after']
1122 def handle(self, evt_type, act):
1123 if evt_type == 'action_after' and isinstance(act, FatetellStage):
1124 tgt = act.target
1125 g = self.game
1126 from .definition import SinsackHatCard
1127 for c in list(tgt.equips):
1128 if not c.is_card(SinsackHatCard):
1129 continue
1131 g.process_action(SinsackHatAction(tgt, tgt, c))
1132 return act
1134 return act