# Coverage for thb/cards/equipment.py : 37%

## 786 statements 346 run 440 missing 0 excluded 22 partial

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 -*-

2 from __future__ import annotations

3

4# -- stdlib --

5 from typing import List, cast

6

7# -- third party --

8# -- own --

9 from game.base import GameError

10 from thb.actions import ActionLimitExceeded, ActionStageLaunchCard, Damage, DrawCards, DropCardStage

11 from thb.actions import DropCards, FatetellAction, FatetellStage, FinalizeStage, ForEach

12 from thb.actions import GenericAction, LaunchCard, MaxLifeChange, MigrateCardsTransaction, Reforge

13 from thb.actions import UserAction, VitalityLimitExceeded, detach_cards, migrate_cards

14 from thb.actions import random_choose_card, register_eh, ttags, user_choose_cards

15 from thb.cards import basic, spellcard

16 from thb.cards.base import Card, PhysicalCard, Skill, TreatAs, VirtualCard, t_None

17 from thb.cards.base import t_OtherLessEqThanN, t_OtherOne

18 from thb.inputlets import ChooseOptionInputlet, ChoosePeerCardInputlet

19 from thb.mode import THBEventHandler

20 from utils.check import CheckFailed, check

21 from utils.misc import classmix

22

23

24# -- code --

25 class WearEquipmentAction(UserAction):

26 def apply_action(self):

27 g = self.game

28 card = self.associated_card

29

30 from thb.cards.definition import EquipmentCard

31 assert isinstance(card, EquipmentCard)

32

33 tgt = self.target

34 equips = tgt.equips

35

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')

41

42 if rst == 'handled':

43 return True

44

45 for oc in list(equips):

46 if oc.equipment_category == card.equipment_category:

47 g.process_action(DropCards(tgt, tgt, [oc]))

48

49 migrate_cards([card], tgt.equips)

50

51 return True

52

53

54 class ReforgeWeapon(Reforge):

55 pass

56

57

58@register_eh

59 class WeaponReforgeHandler(THBEventHandler):

60 interested = ['action_before']

61

62 def handle(self, evt_type, act):

63 if evt_type == 'action_before' and isinstance(act, ActionStageLaunchCard): 63 ↛ 64 line 63 didn't jump to line 64, because the condition on line 63 was never true

64 c = act.card

65 tgt = act.target

66

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

72

73 g = self.game

74

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

79

80 return act

81

82

83@register_eh

84 class EquipmentTransferHandler(THBEventHandler):

85 interested = ['post_card_migration']

86

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

95

96 if m.fr.type == 'equips':

97 owner = m.fr.owner

98 assert owner

99

100 try:

101 owner.skills.remove(c.equipment_skill)

102 except ValueError:

103 pass

104

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)

110

111 return trans

112

113

114 class ShieldSkill(Skill):

115 associated_action = None

116 target = t_None()

117

118

119 class OpticalCloakSkill(TreatAs, ShieldSkill): # just a tag

120 treat_as = PhysicalCard.classes['GrazeCard']

121 skill_category = ['equip', 'passive']

122

123 def check(self):

124 return False

125

126

127 class OpticalCloak(FatetellAction):

128 # 光学迷彩

129 def __init__(self, source, target):

130 self.source = source

131 self.target = target

132 self.fatetell_target = target

133

134 def fatetell_action(self, ft):

135 self.fatetell_card = ft.card

136 return bool(ft.succeeded)

137

138 def fatetell_cond(self, card: Card):

139 return card.color == Card.RED

140

141

142@register_eh

143 class OpticalCloakHandler(THBEventHandler):

144 interested = ['action_apply']

145 execute_after = ['AssistedGrazeHandler']

146

147 def handle(self, evt_type, act):

148 from .basic import BaseUseGraze

149 if evt_type == 'action_apply' and isinstance(act, BaseUseGraze): 149 ↛ 150 line 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

153

154 g = self.game

155

156 if not g.user_input([tgt], ChooseOptionInputlet(self, (False, True))):

157 return act

158

159 if g.process_action(OpticalCloak(tgt, tgt)):

160 act.card = OpticalCloakSkill(tgt)

161

162 return act

163

164

165 class MomijiShieldSkill(ShieldSkill):

166 skill_category = ['equip', 'passive']

167

168

169 class MomijiShield(GenericAction):

170 def __init__(self, act):

171 self.action = act

172 self.source = self.target = act.target

173

174 def apply_action(self):

175 self.action.cancelled = True

176 return True

177

178

179@register_eh

180 class MomijiShieldHandler(THBEventHandler):

181 interested = ['action_before']

182 execute_before = ['HouraiJewelHandler']

183

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 ↛ 187 line 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))

192

193 return act

194

195

196 class UFOSkill(Skill):

197 associated_action = None

198 skill_category = ['equip', 'passive']

199 target = t_None()

200

201

202 class GreenUFOSkill(UFOSkill):

203 increment = 1

204

205

206 class RedUFOSkill(UFOSkill):

207 increment = 1

208

209

210@register_eh

211 class UFODistanceHandler(THBEventHandler):

212 interested = ['calcdistance']

213

214 def handle(self, evt_type, arg):

215 if not evt_type == 'calcdistance': return arg

216

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

225

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

232

233 return arg

234

235

236 class WeaponSkill(Skill):

237 range = 1

238

239

240 class RoukankenSkill(WeaponSkill):

241 associated_action = None

242 skill_category = ['equip', 'passive']

243 target = t_None()

244 range = 3

245

246

247 class Roukanken(GenericAction):

248

249 def apply_action(self):

250 tgt = self.target

251

252 skills = [s for s in tgt.skills if issubclass(s, ShieldSkill)]

253 for s in skills:

254 tgt.disable_skill(s, 'roukanken')

255

256 return True

257

258

259@register_eh

260 class 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 ]

272

273 def handle(self, evt_type, act):

274 if evt_type == 'action_before' and isinstance(act, basic.BaseAttack): 274 ↛ 275 line 274 didn't jump to line 275, because the condition on line 274 was never true

275 cls = self.__class__

276 g = self.game

277

278 if act.cancelled:

279 return act

280

281 if act._[cls] == 'roukanken-processed':

282 return act

283

284 act._[cls] = 'roukanken-processed'

285

286 src, tgt = act.source, act.target

287 if src.has_skill(RoukankenSkill):

288 g.process_action(Roukanken(src, tgt))

289

290 if evt_type == 'action_done' and isinstance(act, basic.BaseAttack): 290 ↛ 291 line 290 didn't jump to line 291, because the condition on line 290 was never true

291 act.target.reenable_skill('roukanken')

292

293 return act

294

295

296 class NenshaPhoneSkill(WeaponSkill):

297 associated_action = None

298 skill_category = ['equip', 'passive']

299 target = t_None()

300 range = 4

301

302

303 class NenshaPhone(GenericAction):

304 def apply_action(self):

305 tgt = self.target

306

307 cards = list(tgt.cards)[:2]

308 g = self.game

309 g.players.exclude(tgt).reveal(cards)

310 migrate_cards(cards, tgt.showncards)

311

312 return True

313

314

315@register_eh

316 class NenshaPhoneHandler(THBEventHandler):

317 interested = ['action_after']

318

319 def handle(self, evt_type, act):

320 if evt_type == 'action_after' and isinstance(act, Damage): 320 ↛ 321 line 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

325

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))

333

334 return act

335

336

337 class ElementalReactorSkill(WeaponSkill):

338 associated_action = None

339 skill_category = ['equip', 'passive']

340 target = t_None()

341 range = 1

342

343

344@register_eh

345 class ElementalReactorHandler(THBEventHandler):

346 interested = ['action_stage_action', 'post_card_migration']

347 execute_after = ['EquipmentTransferHandler']

348

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)

354

355 elif evt_type == 'post_card_migration':

356 trans = cast(MigrateCardsTransaction, arg)

357 from .definition import ElementalReactorCard

358

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)

362

363 return arg

364

365

366 class 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!

371

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

378

379

380 class ScarletRhapsody(ForEach):

381 action_cls = basic.Attack

382

383

384 class 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'

391

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))

401

402 check(card.resides_in in (tgt.cards, tgt.showncards))

403 check(card in set(tgt.cards) or card in set(tgt.showncards))

404

405 check(set(tgt.cards) | set(tgt.showncards) == set([card]))

406

407 return True

408 except CheckFailed:

409 return False

410

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)

415

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))

423

424

425 class RepentanceStickSkill(WeaponSkill):

426 range = 2

427 skill_category = ['equip', 'passive']

428 associated_action = None

429 target = t_None()

430

431

432 class RepentanceStick(GenericAction):

433 def apply_action(self) -> bool:

434 src, tgt = self.source, self.target

435 g = self.game

436

437 catnames = ('cards', 'showncards', 'equips', 'fatetell')

438 cats = [getattr(tgt, i) for i in catnames]

439

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

444

445 card = g.user_input(

446 [src], ChoosePeerCardInputlet(self, tgt, catnames)

447 )

448

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]))

455

456 self.cards = l

457 return True

458

459

460@register_eh

461 class RepentanceStickHandler(THBEventHandler):

462 interested = ['action_before']

463 execute_before = ['WineHandler']

464

465 def handle(self, evt_type, act):

466 if evt_type == 'action_before' and isinstance(act, Damage): 466 ↛ 467 line 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

475

476 if not g.user_input([src], ChooseOptionInputlet(self, (False, True))):

477 return act

478

479 g.process_action(RepentanceStick(src, tgt))

480 act.cancelled = True

481

482 return act

483

484

485 class IbukiGourdSkill(RedUFOSkill):

486 skill_category = ['equip', 'passive']

487 increment = 0

488

489

490@register_eh

491 class IbukiGourdHandler(THBEventHandler):

492 interested = ['action_apply', 'action_after', 'card_migration']

493

494 def handle(self, evt_type, act):

495 if evt_type == 'action_after' and isinstance(act, Damage): 495 ↛ 496 line 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

499

500 g = self.game

501 ttags(src)['ibukigourd_did_damage'] = True

502

503 elif evt_type == 'action_apply' and isinstance(act, FinalizeStage): 503 ↛ 504 line 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

506

507 g = self.game

508 if ttags(tgt)['ibukigourd_did_damage']: return act

509

510 g.process_action(basic.Wine(tgt, tgt))

511

512 elif evt_type == 'post_card_migration': 512 ↛ 513 line 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)

515

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))

522

523 return act

524

525

526 class 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

531

532

533 class HouraiJewelSkill(WeaponSkill):

534 associated_action = None

535 skill_category = ['equip', 'passive']

536 target = t_None()

537 range = 1

538

539

540@register_eh

541 class HouraiJewelHandler(THBEventHandler):

542 interested = ['action_before']

543 execute_before = ['RejectHandler', 'WineHandler'] # wine does not affect this.

544

545 def handle(self, evt_type, act):

546 if evt_type == 'action_before' and isinstance(act, basic.Attack): 546 ↛ 547 line 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__)

553

554 return act

555

556

557 class UmbrellaSkill(ShieldSkill):

558 skill_category = ['equip', 'passive']

559

560

561 class 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

566

567 def apply_action(self):

568 self.damage_act.cancelled = True

569 return True

570

571

572@register_eh

573 class UmbrellaHandler(THBEventHandler):

574 # 紫的阳伞

575 interested = ['action_before']

576 execute_before = ['RejectHandler']

577

578 def handle(self, evt_type, act):

579 if evt_type == 'action_before' and isinstance(act, Damage): 579 ↛ 580 line 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]

583

584 if isinstance(pact, spellcard.SpellCardAction):

585 self.game.process_action(UmbrellaEffect(pact, act))

586

587 return act

588

589

590 class MaidenCostume(TreatAs, ShieldSkill):

591 treat_as = PhysicalCard.classes['RejectCard']

592 skill_category = ['equip', 'passive']

593

594 def check(self):

595 return False

596

597

598 class 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

604

605 def fatetell_action(self, ft):

606 act = self.act

607 src = self.source

608

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

618

619 def fatetell_cond(self, c: Card):

620 return 9 <= c.number <= 13

621

622

623@register_eh

624 class MaidenCostumeHandler(THBEventHandler):

625 interested = ['action_before']

626 execute_before = ['RejectHandler']

627 execute_after = ['HouraiJewelHandler']

628

629 def handle(self, evt_type, act):

630 if evt_type == 'action_before' and isinstance(act, spellcard.SpellCardAction): 630 ↛ 631 line 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

638

639 self.game.process_action(MaidenCostumeAction(tgt, act))

640

641 return act

642

643

644 class HakuroukenSkill(WeaponSkill):

645 range = 2

646 skill_category = ['equip', 'passive']

647 associated_action = None

648 target = t_None()

649

650

651 class Hakurouken(GenericAction):

652 card_usage = 'drop'

653

654 def apply_action(self):

655 src = self.source

656 tgt = self.target

657

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))

666

667 return True

668

669 def cond(self, cards):

670 return len(cards) == 1 and not cards[0].is_card(Skill)

671

672

673@register_eh

674 class HakuroukenHandler(THBEventHandler):

675 interested = ['action_before']

676

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']

682

683 def handle(self, evt_type, act):

684 if evt_type == 'action_before' and isinstance(act, basic.BaseAttack): 684 ↛ 685 line 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

690

691 g = self.game

692 if not g.user_input([src], ChooseOptionInputlet(self, (False, True))):

693 return act

694

695 g.process_action(Hakurouken(src, act.target))

696

697 return act

698

699

700 class AyaRoundfan(GenericAction):

701 def apply_action(self):

702 src, tgt = self.source, self.target

703 g = self.game

704

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

709

710 return True

711

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 # 崩

734

735 # 所以加上这个

736 return self.target.equips

737

738

739 class AyaRoundfanSkill(WeaponSkill):

740 range = 5

741 skill_category = ['equip', 'passive']

742 associated_action = None

743 target = t_None()

744

745

746@register_eh

747 class AyaRoundfanHandler(THBEventHandler):

748 interested = ['action_after']

749 execute_after = ['DyingHandler']

750 card_usage = 'drop'

751

752 def handle(self, evt_type, act):

753 if evt_type == 'action_after' and isinstance(act, Damage): 753 ↛ 754 line 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

757

758 g = self.game

759 pa = g.action_stack[-1]

760 if not isinstance(pa, basic.BaseAttack): return act

761

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))

766

767 return act

768

769 def cond(self, cards):

770 if not len(cards) == 1 or cards[0].is_card(Skill):

771 return False

772

773 return cards[0].resides_in.type in ('cards', 'showncards')

774

775

776 class Laevatein(UserAction):

777 def apply_action(self):

778 return True # logic handled in LaevateinHandler

779

780

781 class LaevateinSkill(WeaponSkill):

782 range = 3

783 skill_category = ['equip', 'passive']

784 associated_action = None

785 target = t_None()

786

787

788@register_eh

789 class LaevateinHandler(THBEventHandler):

790 interested = ['attack_aftergraze']

791 card_usage = 'drop'

792

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

799

800 src = act.source

801 tgt = act.target

802 if not src or not src.has_skill(LaevateinSkill):

803 return arg

804

805 g = self.game

806 cards = user_choose_cards(self, src, ('cards', 'showncards', 'equips'))

807 if not cards:

808 return arg

809

810 g.process_action(DropCards(src, src, cards))

811 g.process_action(Laevatein(src, tgt))

812 return act, True

813

814 return arg

815

816 def cond(self, cards):

817 if not len(cards) == 2: return False

818

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

828

829 return True

830

831

832 class DeathSickleSkill(WeaponSkill):

833 range = 2

834 skill_category = ['equip', 'passive']

835 associated_action = None

836 target = t_None()

837

838

839 class DeathSickle(GenericAction):

840 def __init__(self, act):

841 self.action = act

842 self.source, self.target = act.source, act.target

843

844 def apply_action(self):

845 self.action.amount += 1

846 return True

847

848

849@register_eh

850 class DeathSickleHandler(THBEventHandler):

851 interested = ['action_before']

852 execute_before = ['WineHandler']

853

854 def handle(self, evt_type, act):

855 if evt_type == 'action_before' and isinstance(act, Damage): 855 ↛ 856 line 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))

865

866 return act

867

868

869 class KeystoneSkill(GreenUFOSkill):

870 skill_category = ['equip', 'passive']

871 increment = 1

872

873

874 class Keystone(GenericAction):

875 def __init__(self, act):

876 assert isinstance(act, spellcard.Sinsack)

877 self.source = self.target = act.target

878 self.action = act

879

880 def apply_action(self):

881 self.action.cancelled = True

882 return True

883

884

885@register_eh

886 class KeystoneHandler(THBEventHandler):

887 interested = ['action_before']

888 execute_before = ['SaigyouBranchHandler', 'RejectHandler']

889

890 def handle(self, evt_type, act):

891 if evt_type == 'action_before' and isinstance(act, spellcard.Sinsack): 891 ↛ 892 line 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))

895

896 return act

897

898

899 class WitchBroomSkill(RedUFOSkill):

900 skill_category = ['equip', 'passive']

901 increment = 2

902

903

904 class AccessoriesSkill(Skill):

905 associated_action = None

906 target = t_None()

907

908

909 class YinYangOrb(GenericAction):

910 def __init__(self, ft):

911 self.ftact = ft

912 self.source = self.target = ft.target

913

914 def apply_action(self):

915 ft = self.ftact

916 tgt = ft.target

917

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)

924

925 detach_cards([e], trans=trans)

926 self.card = e

927 ft.set_card(e, self)

928

929 break

930 else:

931 raise GameError('Player has YinYangOrb skill but no equip!')

932

933 return True

934

935

936 class YinYangOrbSkill(AccessoriesSkill):

937 skill_category = ['equip', 'passive']

938

939

940@register_eh

941 class YinYangOrbHandler(THBEventHandler):

942 interested = ['fatetell']

943 execute_after = ['FatetellMalleateHandler']

944

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

952

953 g.process_action(YinYangOrb(act))

954

955 return act

956

957

958 class SuwakoHatSkill(AccessoriesSkill):

959 skill_category = ['equip', 'passive']

960

961

962 class SuwakoHatEffect(UserAction):

963 def __init__(self, target, dcs):

964 self.source = self.target = target

965 self.dcs = dcs

966

967 def apply_action(self):

968 self.dcs.dropn = max(self.dcs.dropn - 2, 0)

969 return True

970

971

972@register_eh

973 class SuwakoHatHandler(THBEventHandler):

974 interested = ['action_before']

975

976 def handle(self, evt_type, act):

977 if evt_type == 'action_before' and isinstance(act, DropCardStage): 977 ↛ 978 line 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))

981

982 return act

983

984

985 class YoumuPhantomSkill(AccessoriesSkill):

986 skill_category = ['equip', 'passive']

987

988

989 class YoumuPhantomHeal(basic.Heal):

990 pass

991

992

993@register_eh

994 class YoumuPhantomHandler(THBEventHandler):

995 interested = ['post_card_migration']

996

997 def handle(self, evt_type, arg):

998 if not evt_type == 'post_card_migration': return arg

999

1000 from .definition import YoumuPhantomCard

1001 trans = cast(MigrateCardsTransaction, arg)

1002 g = self.game

1003

1004 for m in trans.movements:

1005 if not m.card.is_card(YoumuPhantomCard):

1006 continue

1007

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))

1014

1015 if m.to.type == 'equips':

1016 tgt = m.to.owner

1017 assert tgt

1018 g.process_action(MaxLifeChange(tgt, tgt, 1))

1019

1020 return arg

1021

1022

1023 class IceWingSkill(RedUFOSkill):

1024 skill_category = ['equip', 'passive']

1025 increment = 1

1026

1027

1028 class 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

1033

1034 def apply_action(self):

1035 self.action.cancelled = True

1036 return True

1037

1038

1039@register_eh

1040 class IceWingHandler(THBEventHandler):

1041 interested = ['action_before']

1042 _effect_cls = spellcard.SealingArray, spellcard.FrozenFrog

1043

1044 execute_before = ['RejectHandler', 'SaigyouBranchHandler']

1045

1046 def handle(self, evt_type, act):

1047 if evt_type == 'action_before' and isinstance(act, self._effect_cls): 1047 ↛ 1048 line 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))

1050

1051 return act

1052

1053

1054 class 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 }

1063

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]

1071

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

1079

1080

1081@register_eh

1082 class GrimoireHandler(THBEventHandler):

1083 interested = ['action_after', 'action_shootdown']

1084

1085 def handle(self, evt_type, act):

1086 if evt_type == 'action_shootdown':

1087 if not isinstance(act, LaunchCard): return act 1087 ↛ 1088 line 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

1092

1093 if t['turn_count'] <= t['grimoire_tag']:

1094 raise ActionLimitExceeded

1095

1096 if t['vitality'] <= 0:

1097 raise VitalityLimitExceeded

1098

1099 elif evt_type == 'action_after' and isinstance(act, LaunchCard): 1099 ↛ 1100 line 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']

1105

1106 return act

1107

1108

1109 class 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

1115

1116 def fatetell_action(self, ft):

1117 if not ft.succeeded:

1118 return False

1119

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

1125

1126 def fatetell_cond(self, c: Card):

1127 return c.suit == Card.SPADE and 1 <= c.number <= 8

1128

1129

1130 class SinsackHat(ShieldSkill):

1131 skill_category = ['equip', 'passive']

1132

1133

1134@register_eh

1135 class SinsackHatHandler(THBEventHandler):

1136 interested = ['action_after']

1137

1138 def handle(self, evt_type, act):

1139 if evt_type == 'action_after' and isinstance(act, FatetellStage): 1139 ↛ 1140 line 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

1146

1147 g.process_action(SinsackHatAction(tgt, tgt, c))

1148 return act

1149

1150 return act