Coverage for thb/cards/equipment.py : 37%
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 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 thb.mode import THBEventHandler
20from utils.check import CheckFailed, check
21from utils.misc import classmix
24# -- code --
25class WearEquipmentAction(UserAction):
26 def apply_action(self):
27 g = self.game
28 card = self.associated_card
30 from thb.cards.definition import EquipmentCard
31 assert isinstance(card, EquipmentCard)
33 tgt = self.target
34 equips = tgt.equips
36 _s, _t, _c, rst = g.emit_event('wear_equipment', (self, tgt, card, 'default'))
37 assert _s is self
38 assert _t is tgt
39 assert _c is card
40 assert rst in ('default', 'handled')
42 if rst == 'handled':
43 return True
45 for oc in list(equips):
46 if oc.equipment_category == card.equipment_category:
47 g.process_action(DropCards(tgt, tgt, [oc]))
49 migrate_cards([card], tgt.equips)
51 return True
54class ReforgeWeapon(Reforge):
55 pass
58@register_eh
59class WeaponReforgeHandler(THBEventHandler):
60 interested = ['action_before']
62 def handle(self, evt_type, act):
63 if evt_type == 'action_before' and isinstance(act, ActionStageLaunchCard): 63 ↛ 64line 63 didn't jump to line 64, because the condition on line 63 was never true
64 c = act.card
65 tgt = act.target
67 if c.is_card(VirtualCard): return act
68 from thb.cards.definition import EquipmentCard
69 if not isinstance(c, EquipmentCard): return act
70 if c.equipment_category != 'weapon': return act
71 if tgt.tags['vitality'] <= 0: return act
73 g = self.game
75 if g.user_input([tgt], ChooseOptionInputlet(self, (False, True))):
76 tgt.tags['vitality'] -= 1
77 g.process_action(ReforgeWeapon(tgt, tgt, c))
78 act.cancelled = True
80 return act
83@register_eh
84class EquipmentTransferHandler(THBEventHandler):
85 interested = ['post_card_migration']
87 def handle(self, evt, trans):
88 if evt == 'post_card_migration':
89 from thb.cards.definition import EquipmentCard
90 assert isinstance(trans, MigrateCardsTransaction)
91 for m in trans.movements:
92 c = m.card
93 if not isinstance(c, EquipmentCard):
94 continue
96 if m.fr.type == 'equips':
97 owner = m.fr.owner
98 assert owner
100 try:
101 owner.skills.remove(c.equipment_skill)
102 except ValueError:
103 pass
105 if m.to.type == 'equips':
106 owner = m.to.owner
107 assert owner
108 if c.equipment_skill:
109 owner.skills.append(c.equipment_skill)
111 return trans
114class ShieldSkill(Skill):
115 associated_action = None
116 target = t_None()
119class OpticalCloakSkill(TreatAs, ShieldSkill): # just a tag
120 treat_as = PhysicalCard.classes['GrazeCard']
121 skill_category = ['equip', 'passive']
123 def check(self):
124 return False
127class OpticalCloak(FatetellAction):
128 # 光学迷彩
129 def __init__(self, source, target):
130 self.source = source
131 self.target = target
132 self.fatetell_target = target
134 def fatetell_action(self, ft):
135 self.fatetell_card = ft.card
136 return bool(ft.succeeded)
138 def fatetell_cond(self, card: Card):
139 return card.color == Card.RED
142@register_eh
143class OpticalCloakHandler(THBEventHandler):
144 interested = ['action_apply']
145 execute_after = ['AssistedGrazeHandler']
147 def handle(self, evt_type, act):
148 from .basic import BaseUseGraze
149 if evt_type == 'action_apply' and isinstance(act, BaseUseGraze): 149 ↛ 150line 149 didn't jump to line 150, because the condition on line 149 was never true
150 tgt = act.target
151 if not tgt.has_skill(OpticalCloakSkill): return act
152 if act.card: return act
154 g = self.game
156 if not g.user_input([tgt], ChooseOptionInputlet(self, (False, True))):
157 return act
159 if g.process_action(OpticalCloak(tgt, tgt)):
160 act.card = OpticalCloakSkill(tgt)
162 return act
165class MomijiShieldSkill(ShieldSkill):
166 skill_category = ['equip', 'passive']
169class MomijiShield(GenericAction):
170 def __init__(self, act):
171 self.action = act
172 self.source = self.target = act.target
174 def apply_action(self):
175 self.action.cancelled = True
176 return True
179@register_eh
180class MomijiShieldHandler(THBEventHandler):
181 interested = ['action_before']
182 execute_before = ['HouraiJewelHandler']
184 def handle(self, evt_type, act):
185 from .basic import BaseAttack
186 if not (evt_type == 'action_before' and isinstance(act, BaseAttack)): return act 186 ↛ 187line 186 didn't jump to line 187, because the condition on line 186 was never false
187 tgt = act.target
188 if not tgt.has_skill(MomijiShieldSkill): return act
189 if not act.associated_card.color == Card.BLACK: return act
190 g = self.game
191 g.process_action(MomijiShield(act))
193 return act
196class UFOSkill(Skill):
197 associated_action = None
198 skill_category = ['equip', 'passive']
199 target = t_None()
202class GreenUFOSkill(UFOSkill):
203 increment = 1
206class RedUFOSkill(UFOSkill):
207 increment = 1
210@register_eh
211class UFODistanceHandler(THBEventHandler):
212 interested = ['calcdistance']
214 def handle(self, evt_type, arg):
215 if not evt_type == 'calcdistance': return arg
217 src, card, dist = arg
218 for s in src.skills:
219 if not issubclass(s, RedUFOSkill): continue
220 if not src.has_skill(s): continue
221 incr = s.increment
222 incr = incr(src) if callable(incr) else incr
223 for p in dist:
224 dist[p] -= incr
226 for p in dist:
227 for s in p.skills:
228 if not issubclass(s, GreenUFOSkill): continue
229 if not p.has_skill(s): continue
230 incr = s.increment
231 dist[p] += incr(p) if callable(incr) else incr
233 return arg
236class WeaponSkill(Skill):
237 range = 1
240class RoukankenSkill(WeaponSkill):
241 associated_action = None
242 skill_category = ['equip', 'passive']
243 target = t_None()
244 range = 3
247class Roukanken(GenericAction):
249 def apply_action(self):
250 tgt = self.target
252 skills = [s for s in tgt.skills if issubclass(s, ShieldSkill)]
253 for s in skills:
254 tgt.disable_skill(s, 'roukanken')
256 return True
259@register_eh
260class RoukankenEffectHandler(THBEventHandler):
261 interested = ['action_before', 'action_done']
262 execute_before = [
263 'MomijiShieldHandler',
264 'OpticalCloakHandler',
265 'SaigyouBranchHandler',
266 'HouraiJewelHandler',
267 'SpearTheGungnirHandler',
268 'HakuroukenHandler',
269 'FreakingPowerHandler',
270 'ResonanceHandler'
271 ]
273 def handle(self, evt_type, act):
274 if evt_type == 'action_before' and isinstance(act, basic.BaseAttack): 274 ↛ 275line 274 didn't jump to line 275, because the condition on line 274 was never true
275 cls = self.__class__
276 g = self.game
278 if act.cancelled:
279 return act
281 if act._[cls] == 'roukanken-processed':
282 return act
284 act._[cls] = 'roukanken-processed'
286 src, tgt = act.source, act.target
287 if src.has_skill(RoukankenSkill):
288 g.process_action(Roukanken(src, tgt))
290 if evt_type == 'action_done' and isinstance(act, basic.BaseAttack): 290 ↛ 291line 290 didn't jump to line 291, because the condition on line 290 was never true
291 act.target.reenable_skill('roukanken')
293 return act
296class NenshaPhoneSkill(WeaponSkill):
297 associated_action = None
298 skill_category = ['equip', 'passive']
299 target = t_None()
300 range = 4
303class NenshaPhone(GenericAction):
304 def apply_action(self):
305 tgt = self.target
307 cards = list(tgt.cards)[:2]
308 g = self.game
309 g.players.exclude(tgt).reveal(cards)
310 migrate_cards(cards, tgt.showncards)
312 return True
315@register_eh
316class NenshaPhoneHandler(THBEventHandler):
317 interested = ['action_after']
319 def handle(self, evt_type, act):
320 if evt_type == 'action_after' and isinstance(act, Damage): 320 ↛ 321line 320 didn't jump to line 321, because the condition on line 320 was never true
321 if not act.succeeded: return act
322 g = self.game
323 pa = g.action_stack[-1]
324 if not isinstance(pa, basic.BaseAttack): return act
326 src = act.source
327 tgt = act.target
328 if tgt.dead: return act
329 if not tgt.cards: return act
330 if not src.has_skill(NenshaPhoneSkill): return act
331 if not g.user_input([src], ChooseOptionInputlet(self, (False, True))): return act
332 g.process_action(NenshaPhone(src, tgt))
334 return act
337class ElementalReactorSkill(WeaponSkill):
338 associated_action = None
339 skill_category = ['equip', 'passive']
340 target = t_None()
341 range = 1
344@register_eh
345class ElementalReactorHandler(THBEventHandler):
346 interested = ['action_stage_action', 'post_card_migration']
347 execute_after = ['EquipmentTransferHandler']
349 def handle(self, evt_type, arg):
350 if evt_type == 'action_stage_action':
351 tgt = arg
352 if not tgt.has_skill(ElementalReactorSkill): return arg
353 basic.AttackCardVitalityHandler.disable(tgt)
355 elif evt_type == 'post_card_migration':
356 trans = cast(MigrateCardsTransaction, arg)
357 from .definition import ElementalReactorCard
359 for m in trans.movements:
360 if m.fr.type == 'equips' and m.card.is_card(ElementalReactorCard):
361 basic.AttackCardVitalityHandler.enable(m.fr.owner)
363 return arg
366class GungnirSkill(TreatAs, WeaponSkill):
367 target = t_OtherOne()
368 skill_category = ['equip', 'active']
369 range = 3
370 treat_as = PhysicalCard.classes['AttackCard'] # arghhhhh, nasty circular references!
372 def check(self):
373 cl = self.associated_cards
374 cat = ('cards', 'showncards')
375 if not all(c.resides_in.type in cat for c in cl): return False
376 if not all(c.is_card(PhysicalCard) for c in cl): return False
377 return len(cl) == 2
380class ScarletRhapsody(ForEach):
381 action_cls = basic.Attack
384class ScarletRhapsodySkill(WeaponSkill):
385 range = 4
386 associated_action = ScarletRhapsody
387 category = ['skill', 'treat_as', 'basic']
388 skill_category = ['equip', 'active']
389 target = t_OtherLessEqThanN(3)
390 usage = 'launch'
392 def check(self):
393 try:
394 cl = self.associated_cards
395 check(len(cl) == 1)
396 card = cl[0]
397 from .definition import AttackCard
398 check(card.is_card(AttackCard))
399 tgt = card.resides_in.owner
400 check(card.is_card(PhysicalCard))
402 check(card.resides_in in (tgt.cards, tgt.showncards))
403 check(card in set(tgt.cards) or card in set(tgt.showncards))
405 check(set(tgt.cards) | set(tgt.showncards) == set([card]))
407 return True
408 except CheckFailed:
409 return False
411 def is_card(self, cls):
412 from thb.cards.definition import AttackCard
413 if issubclass(AttackCard, cls): return True
414 return isinstance(self, cls)
416 @property
417 def distance(self):
418 cl = self.associated_cards
419 if not cl:
420 return 1
421 c = cl[0]
422 return max(1, getattr(c, 'distance', 1))
425class RepentanceStickSkill(WeaponSkill):
426 range = 2
427 skill_category = ['equip', 'passive']
428 associated_action = None
429 target = t_None()
432class RepentanceStick(GenericAction):
433 def apply_action(self) -> bool:
434 src, tgt = self.source, self.target
435 g = self.game
437 catnames = ('cards', 'showncards', 'equips', 'fatetell')
438 cats = [getattr(tgt, i) for i in catnames]
440 l: List[PhysicalCard] = []
441 for i in range(2):
442 if not (tgt.cards or tgt.showncards or tgt.equips or tgt.fatetell):
443 break
445 card = g.user_input(
446 [src], ChoosePeerCardInputlet(self, tgt, catnames)
447 )
449 if not card:
450 card = random_choose_card(g, cats)
451 if card:
452 l.append(card)
453 g.players.exclude(tgt).player.reveal(card)
454 g.process_action(DropCards(src, tgt, [card]))
456 self.cards = l
457 return True
460@register_eh
461class RepentanceStickHandler(THBEventHandler):
462 interested = ['action_before']
463 execute_before = ['WineHandler']
465 def handle(self, evt_type, act):
466 if evt_type == 'action_before' and isinstance(act, Damage): 466 ↛ 467line 466 didn't jump to line 467, because the condition on line 466 was never true
467 if act.cancelled: return act
468 src, tgt = act.source, act.target
469 if src and src.has_skill(RepentanceStickSkill):
470 g = self.game
471 pa = g.action_stack[-1]
472 if not isinstance(pa, basic.BaseAttack): return act
473 if not (tgt.cards or tgt.showncards or tgt.equips or tgt.fatetell):
474 return act
476 if not g.user_input([src], ChooseOptionInputlet(self, (False, True))):
477 return act
479 g.process_action(RepentanceStick(src, tgt))
480 act.cancelled = True
482 return act
485class IbukiGourdSkill(RedUFOSkill):
486 skill_category = ['equip', 'passive']
487 increment = 0
490@register_eh
491class IbukiGourdHandler(THBEventHandler):
492 interested = ['action_apply', 'action_after', 'card_migration']
494 def handle(self, evt_type, act):
495 if evt_type == 'action_after' and isinstance(act, Damage): 495 ↛ 496line 495 didn't jump to line 496, because the condition on line 495 was never true
496 src = act.source
497 if not src: return act
498 if not src.has_skill(IbukiGourdSkill): return act
500 g = self.game
501 ttags(src)['ibukigourd_did_damage'] = True
503 elif evt_type == 'action_apply' and isinstance(act, FinalizeStage): 503 ↛ 504line 503 didn't jump to line 504, because the condition on line 503 was never true
504 tgt = act.target
505 if not tgt.has_skill(IbukiGourdSkill): return act
507 g = self.game
508 if ttags(tgt)['ibukigourd_did_damage']: return act
510 g.process_action(basic.Wine(tgt, tgt))
512 elif evt_type == 'post_card_migration': 512 ↛ 513line 512 didn't jump to line 513, because the condition on line 512 was never true
513 from .definition import IbukiGourdCard
514 trans = cast(MigrateCardsTransaction, act)
516 for m in trans.movements:
517 if m.card.is_card(IbukiGourdCard) and m.to.type == 'equips':
518 assert m.to.owner
519 tgt = m.to.owner
520 g = self.game
521 g.process_action(basic.Wine(tgt, tgt))
523 return act
526class HouraiJewelAttack(basic.BaseAttack, spellcard.InstantSpellCardAction):
527 def apply_action(self):
528 g = self.game
529 g.process_action(Damage(self.source, self.target))
530 return True
533class HouraiJewelSkill(WeaponSkill):
534 associated_action = None
535 skill_category = ['equip', 'passive']
536 target = t_None()
537 range = 1
540@register_eh
541class HouraiJewelHandler(THBEventHandler):
542 interested = ['action_before']
543 execute_before = ['RejectHandler', 'WineHandler'] # wine does not affect this.
545 def handle(self, evt_type, act):
546 if evt_type == 'action_before' and isinstance(act, basic.Attack): 546 ↛ 547line 546 didn't jump to line 547, because the condition on line 546 was never true
547 src = act.source
548 if not src.has_skill(HouraiJewelSkill): return act
549 if isinstance(act, HouraiJewelAttack): return act
550 g = self.game
551 if g.user_input([src], ChooseOptionInputlet(self, (False, True))):
552 act.__class__ = classmix(HouraiJewelAttack, act.__class__)
554 return act
557class UmbrellaSkill(ShieldSkill):
558 skill_category = ['equip', 'passive']
561class UmbrellaEffect(GenericAction):
562 def __init__(self, act, damage_act):
563 self.source = self.target = damage_act.target
564 self.action = act
565 self.damage_act = damage_act
567 def apply_action(self):
568 self.damage_act.cancelled = True
569 return True
572@register_eh
573class UmbrellaHandler(THBEventHandler):
574 # 紫的阳伞
575 interested = ['action_before']
576 execute_before = ['RejectHandler']
578 def handle(self, evt_type, act):
579 if evt_type == 'action_before' and isinstance(act, Damage): 579 ↛ 580line 579 didn't jump to line 580, because the condition on line 579 was never true
580 if not act.target.has_skill(UmbrellaSkill): return act
581 g = self.game
582 pact = g.action_stack[-1]
584 if isinstance(pact, spellcard.SpellCardAction):
585 self.game.process_action(UmbrellaEffect(pact, act))
587 return act
590class MaidenCostume(TreatAs, ShieldSkill):
591 treat_as = PhysicalCard.classes['RejectCard']
592 skill_category = ['equip', 'passive']
594 def check(self):
595 return False
598class MaidenCostumeAction(FatetellAction):
599 def __init__(self, source, act):
600 self.source = source
601 self.target = source
602 self.fatetell_target = source
603 self.act = act
605 def fatetell_action(self, ft):
606 act = self.act
607 src = self.source
609 g = self.game
610 if ft.succeeded:
611 # rej = spellcard.LaunchReject(src, act, SaigyouBranchSkill(src))
612 g.process_action(LaunchCard(
613 src, [act.target], MaidenCostume(src), spellcard.Reject(src, act)
614 ))
615 return True
616 else:
617 return False
619 def fatetell_cond(self, c: Card):
620 return 9 <= c.number <= 13
623@register_eh
624class MaidenCostumeHandler(THBEventHandler):
625 interested = ['action_before']
626 execute_before = ['RejectHandler']
627 execute_after = ['HouraiJewelHandler']
629 def handle(self, evt_type, act):
630 if evt_type == 'action_before' and isinstance(act, spellcard.SpellCardAction): 630 ↛ 631line 630 didn't jump to line 631, because the condition on line 630 was never true
631 tgt = act.target
632 if not tgt.has_skill(MaidenCostume): return act
633 if act.cancelled: return act
634 if isinstance(act, spellcard.Reject): return act # can't respond to reject
635 g = self.game
636 if not g.user_input([tgt], ChooseOptionInputlet(self, (False, True))):
637 return act
639 self.game.process_action(MaidenCostumeAction(tgt, act))
641 return act
644class HakuroukenSkill(WeaponSkill):
645 range = 2
646 skill_category = ['equip', 'passive']
647 associated_action = None
648 target = t_None()
651class Hakurouken(GenericAction):
652 card_usage = 'drop'
654 def apply_action(self):
655 src = self.source
656 tgt = self.target
658 cards = user_choose_cards(self, tgt, ('cards', 'showncards'))
659 g = self.game
660 if cards:
661 self.peer_action = 'drop'
662 g.process_action(DropCards(src, tgt, cards))
663 else:
664 self.peer_action = 'draw'
665 g.process_action(DrawCards(src, 1))
667 return True
669 def cond(self, cards):
670 return len(cards) == 1 and not cards[0].is_card(Skill)
673@register_eh
674class HakuroukenHandler(THBEventHandler):
675 interested = ['action_before']
677 # BUG WITH OUT THIS LINE:
678 # src equips [Gourd, Hakurouken], tgt drops Exinwan
679 # then src drops Gourd,
680 # but Attack.damage == 1, Wine tag preserved.
681 execute_before = ['WineHandler', 'MomijiShieldHandler']
683 def handle(self, evt_type, act):
684 if evt_type == 'action_before' and isinstance(act, basic.BaseAttack): 684 ↛ 685line 684 didn't jump to line 685, because the condition on line 684 was never true
685 if act.cancelled: return act
686 src = act.source
687 if not src.has_skill(HakuroukenSkill): return act
688 card = act.associated_card
689 if not card.suit == Card.CLUB: return act
691 g = self.game
692 if not g.user_input([src], ChooseOptionInputlet(self, (False, True))):
693 return act
695 g.process_action(Hakurouken(src, act.target))
697 return act
700class AyaRoundfan(GenericAction):
701 def apply_action(self):
702 src, tgt = self.source, self.target
703 g = self.game
705 equip = g.user_input([src], ChoosePeerCardInputlet(self, tgt, ['equips']))
706 equip = equip or random_choose_card(g, [tgt.equips])
707 g.process_action(DropCards(src, tgt, [equip]))
708 self.card = equip
710 return True
712 def is_valid(self):
713 # Proton 2015/4/2 0:26:51
714 # 小爱打幽香,打中以后,弃丸子发动团扇
715 # Proton 2015/4/2 0:26:59
716 # 然后弃了自己的装备
717 # Proton 2015/4/2 0:27:25
718 # 发动了于是又弃了幽香的装备
719 # Proton 2015/4/2 0:27:41
720 # 发动了小小军势
721 # Proton 2015/4/2 0:28:02
722 # 这时候针妙丸又来了一发,幽香卒
723 # 0:28:19
724 # 八咫乌鸦 2015/4/2 0:28:19
725 # 喜闻乐见的插入结算
726 # Proton 2015/4/2 0:28:49
727 # 嗯 然后就回到了团扇的效果
728 # Proton 2015/4/2 0:29:08
729 # 团扇弃置cost把对面弃死了啊!
730 # Proton 2015/4/2 0:29:38
731 # 然后后半段效果都假设是对面还活着
732 # Proton 2015/4/2 0:29:44
733 # 崩
735 # 所以加上这个
736 return self.target.equips
739class AyaRoundfanSkill(WeaponSkill):
740 range = 5
741 skill_category = ['equip', 'passive']
742 associated_action = None
743 target = t_None()
746@register_eh
747class AyaRoundfanHandler(THBEventHandler):
748 interested = ['action_after']
749 execute_after = ['DyingHandler']
750 card_usage = 'drop'
752 def handle(self, evt_type, act):
753 if evt_type == 'action_after' and isinstance(act, Damage): 753 ↛ 754line 753 didn't jump to line 754, because the condition on line 753 was never true
754 if not act.succeeded: return act
755 src, tgt = act.source, act.target
756 if not (src and src.has_skill(AyaRoundfanSkill) and tgt.equips): return act
758 g = self.game
759 pa = g.action_stack[-1]
760 if not isinstance(pa, basic.BaseAttack): return act
762 cards = user_choose_cards(self, src, ('cards', 'showncards'))
763 if not cards: return act
764 g.process_action(DropCards(src, src, cards))
765 g.process_action(AyaRoundfan(src, tgt))
767 return act
769 def cond(self, cards):
770 if not len(cards) == 1 or cards[0].is_card(Skill):
771 return False
773 return cards[0].resides_in.type in ('cards', 'showncards')
776class Laevatein(UserAction):
777 def apply_action(self):
778 return True # logic handled in LaevateinHandler
781class LaevateinSkill(WeaponSkill):
782 range = 3
783 skill_category = ['equip', 'passive']
784 associated_action = None
785 target = t_None()
788@register_eh
789class LaevateinHandler(THBEventHandler):
790 interested = ['attack_aftergraze']
791 card_usage = 'drop'
793 def handle(self, evt_type, arg):
794 if evt_type == 'attack_aftergraze':
795 act, succeed = arg
796 assert isinstance(act, basic.BaseAttack)
797 if succeed:
798 return arg
800 src = act.source
801 tgt = act.target
802 if not src or not src.has_skill(LaevateinSkill):
803 return arg
805 g = self.game
806 cards = user_choose_cards(self, src, ('cards', 'showncards', 'equips'))
807 if not cards:
808 return arg
810 g.process_action(DropCards(src, src, cards))
811 g.process_action(Laevatein(src, tgt))
812 return act, True
814 return arg
816 def cond(self, cards):
817 if not len(cards) == 2: return False
819 from thb.cards.definition import LaevateinCard
820 for c in cards:
821 t = c.resides_in.type
822 if t not in ('cards', 'showncards', 'equips'):
823 return False
824 elif t == 'equips' and c.is_card(LaevateinCard):
825 return False
826 elif c.is_card(Skill):
827 return False
829 return True
832class DeathSickleSkill(WeaponSkill):
833 range = 2
834 skill_category = ['equip', 'passive']
835 associated_action = None
836 target = t_None()
839class DeathSickle(GenericAction):
840 def __init__(self, act):
841 self.action = act
842 self.source, self.target = act.source, act.target
844 def apply_action(self):
845 self.action.amount += 1
846 return True
849@register_eh
850class DeathSickleHandler(THBEventHandler):
851 interested = ['action_before']
852 execute_before = ['WineHandler']
854 def handle(self, evt_type, act):
855 if evt_type == 'action_before' and isinstance(act, Damage): 855 ↛ 856line 855 didn't jump to line 856, because the condition on line 855 was never true
856 from .basic import Attack
857 g = self.game
858 pact = g.action_stack[-1]
859 if not isinstance(pact, Attack): return act
860 src = act.source
861 if not src or not src.has_skill(DeathSickleSkill): return act
862 tgt = act.target
863 if len(tgt.cards) + len(tgt.showncards) == 0:
864 g.process_action(DeathSickle(act))
866 return act
869class KeystoneSkill(GreenUFOSkill):
870 skill_category = ['equip', 'passive']
871 increment = 1
874class Keystone(GenericAction):
875 def __init__(self, act):
876 assert isinstance(act, spellcard.Sinsack)
877 self.source = self.target = act.target
878 self.action = act
880 def apply_action(self):
881 self.action.cancelled = True
882 return True
885@register_eh
886class KeystoneHandler(THBEventHandler):
887 interested = ['action_before']
888 execute_before = ['SaigyouBranchHandler', 'RejectHandler']
890 def handle(self, evt_type, act):
891 if evt_type == 'action_before' and isinstance(act, spellcard.Sinsack): 891 ↛ 892line 891 didn't jump to line 892, because the condition on line 891 was never true
892 tgt = act.target
893 if tgt.has_skill(KeystoneSkill):
894 self.game.process_action(Keystone(act))
896 return act
899class WitchBroomSkill(RedUFOSkill):
900 skill_category = ['equip', 'passive']
901 increment = 2
904class AccessoriesSkill(Skill):
905 associated_action = None
906 target = t_None()
909class YinYangOrb(GenericAction):
910 def __init__(self, ft):
911 self.ftact = ft
912 self.source = self.target = ft.target
914 def apply_action(self):
915 ft = self.ftact
916 tgt = ft.target
918 from .definition import YinYangOrbCard
919 for e in tgt.equips:
920 if e.is_card(YinYangOrbCard):
921 with MigrateCardsTransaction(self) as trans:
922 migrate_cards([ft.card], tgt.cards, unwrap=True, trans=trans)
923 migrate_cards([e], tgt.special, trans=trans)
925 detach_cards([e], trans=trans)
926 self.card = e
927 ft.set_card(e, self)
929 break
930 else:
931 raise GameError('Player has YinYangOrb skill but no equip!')
933 return True
936class YinYangOrbSkill(AccessoriesSkill):
937 skill_category = ['equip', 'passive']
940@register_eh
941class YinYangOrbHandler(THBEventHandler):
942 interested = ['fatetell']
943 execute_after = ['FatetellMalleateHandler']
945 def handle(self, evt_type, act):
946 if evt_type == 'fatetell':
947 g = self.game
948 tgt = act.target
949 if not tgt.has_skill(YinYangOrbSkill): return act
950 if not g.user_input([tgt], ChooseOptionInputlet(self, (False, True))):
951 return act
953 g.process_action(YinYangOrb(act))
955 return act
958class SuwakoHatSkill(AccessoriesSkill):
959 skill_category = ['equip', 'passive']
962class SuwakoHatEffect(UserAction):
963 def __init__(self, target, dcs):
964 self.source = self.target = target
965 self.dcs = dcs
967 def apply_action(self):
968 self.dcs.dropn = max(self.dcs.dropn - 2, 0)
969 return True
972@register_eh
973class SuwakoHatHandler(THBEventHandler):
974 interested = ['action_before']
976 def handle(self, evt_type, act):
977 if evt_type == 'action_before' and isinstance(act, DropCardStage): 977 ↛ 978line 977 didn't jump to line 978, because the condition on line 977 was never true
978 tgt = act.target
979 if tgt.has_skill(SuwakoHatSkill):
980 self.game.process_action(SuwakoHatEffect(tgt, act))
982 return act
985class YoumuPhantomSkill(AccessoriesSkill):
986 skill_category = ['equip', 'passive']
989class YoumuPhantomHeal(basic.Heal):
990 pass
993@register_eh
994class YoumuPhantomHandler(THBEventHandler):
995 interested = ['post_card_migration']
997 def handle(self, evt_type, arg):
998 if not evt_type == 'post_card_migration': return arg
1000 from .definition import YoumuPhantomCard
1001 trans = cast(MigrateCardsTransaction, arg)
1002 g = self.game
1004 for m in trans.movements:
1005 if not m.card.is_card(YoumuPhantomCard):
1006 continue
1008 if m.fr.type == 'equips':
1009 tgt = m.fr.owner
1010 assert tgt
1011 g.process_action(MaxLifeChange(tgt, tgt, -1))
1012 if not tgt.dead:
1013 g.process_action(YoumuPhantomHeal(tgt, tgt))
1015 if m.to.type == 'equips':
1016 tgt = m.to.owner
1017 assert tgt
1018 g.process_action(MaxLifeChange(tgt, tgt, 1))
1020 return arg
1023class IceWingSkill(RedUFOSkill):
1024 skill_category = ['equip', 'passive']
1025 increment = 1
1028class IceWing(GenericAction):
1029 def __init__(self, act):
1030 assert isinstance(act, (spellcard.SealingArray, spellcard.FrozenFrog))
1031 self.source = self.target = act.target
1032 self.action = act
1034 def apply_action(self):
1035 self.action.cancelled = True
1036 return True
1039@register_eh
1040class IceWingHandler(THBEventHandler):
1041 interested = ['action_before']
1042 _effect_cls = spellcard.SealingArray, spellcard.FrozenFrog
1044 execute_before = ['RejectHandler', 'SaigyouBranchHandler']
1046 def handle(self, evt_type, act):
1047 if evt_type == 'action_before' and isinstance(act, self._effect_cls): 1047 ↛ 1048line 1047 didn't jump to line 1048, because the condition on line 1047 was never true
1048 if act.target.has_skill(IceWingSkill):
1049 self.game.process_action(IceWing(act))
1051 return act
1054class GrimoireSkill(TreatAs, WeaponSkill):
1055 skill_category = ['equip', 'active']
1056 range = 1
1057 lookup_tbl = {
1058 Card.SPADE: PhysicalCard.classes['DemonParadeCard'], # again...
1059 Card.HEART: PhysicalCard.classes['FeastCard'],
1060 Card.CLUB: PhysicalCard.classes['MapCannonCard'],
1061 Card.DIAMOND: PhysicalCard.classes['HarvestCard'],
1062 }
1064 @property
1065 def treat_as(self):
1066 cl = self.associated_cards
1067 if not (cl and cl[0].suit):
1068 from .base import DummyCard
1069 return DummyCard
1070 return self.lookup_tbl[cl[0].suit]
1072 def check(self):
1073 cl = self.associated_cards
1074 if not len(cl) == 1: return False
1075 if not cl[0].resides_in.type in ('cards', 'showncards', 'equips'):
1076 return False
1077 if not cl[0].suit: return False
1078 return True
1081@register_eh
1082class GrimoireHandler(THBEventHandler):
1083 interested = ['action_after', 'action_shootdown']
1085 def handle(self, evt_type, act):
1086 if evt_type == 'action_shootdown':
1087 if not isinstance(act, LaunchCard): return act 1087 ↛ 1088line 1087 didn't jump to line 1088, because the condition on line 1087 was never false
1088 c = act.card
1089 if c.is_card(GrimoireSkill):
1090 src = act.source
1091 t = src.tags
1093 if t['turn_count'] <= t['grimoire_tag']:
1094 raise ActionLimitExceeded
1096 if t['vitality'] <= 0:
1097 raise VitalityLimitExceeded
1099 elif evt_type == 'action_after' and isinstance(act, LaunchCard): 1099 ↛ 1100line 1099 didn't jump to line 1100, because the condition on line 1099 was never true
1100 c = act.card
1101 if c.is_card(GrimoireSkill):
1102 t = act.source.tags
1103 t['vitality'] -= 1
1104 t['grimoire_tag'] = t['turn_count']
1106 return act
1109class SinsackHatAction(FatetellAction):
1110 def __init__(self, source, target, hat_card):
1111 self.source = source
1112 self.target = target
1113 self.hat_card = hat_card
1114 self.fatetell_target = target
1116 def fatetell_action(self, ft):
1117 if not ft.succeeded:
1118 return False
1120 g = self.game
1121 tgt, c = self.target, self.hat_card
1122 g.process_action(spellcard.SinsackDamage(None, tgt, amount=2))
1123 migrate_cards([c], tgt.cards, unwrap=True)
1124 return True
1126 def fatetell_cond(self, c: Card):
1127 return c.suit == Card.SPADE and 1 <= c.number <= 8
1130class SinsackHat(ShieldSkill):
1131 skill_category = ['equip', 'passive']
1134@register_eh
1135class SinsackHatHandler(THBEventHandler):
1136 interested = ['action_after']
1138 def handle(self, evt_type, act):
1139 if evt_type == 'action_after' and isinstance(act, FatetellStage): 1139 ↛ 1140line 1139 didn't jump to line 1140, because the condition on line 1139 was never true
1140 tgt = act.target
1141 g = self.game
1142 from .definition import SinsackHatCard
1143 for c in list(tgt.equips):
1144 if not c.is_card(SinsackHatCard):
1145 continue
1147 g.process_action(SinsackHatAction(tgt, tgt, c))
1148 return act
1150 return act