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

## 776 statements 757 run 19 missing 0 excluded 28 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 EventHandler, 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 utils.check import CheckFailed, check

20 from utils.misc import classmix

21

22

23# -- code --

24 class WearEquipmentAction(UserAction):

25 def apply_action(self):

26 g = self.game

27 card = self.associated_card

28 tgt = self.target

29 equips = tgt.equips

30

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

36

37 if rst == 'handled': 37 ↛ 38 line 37 didn't jump to line 38, because the condition on line 37 was never true

38 return True

39

40 for oc in list(equips):

41 if oc.equipment_category == card.equipment_category:

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

43

44 migrate_cards([card], tgt.equips)

45

46 return True

47

48

49 class ReforgeWeapon(Reforge):

50 pass

51

52

53@register_eh

54 class WeaponReforgeHandler(EventHandler):

55 interested = ['action_before']

56

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

61

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

67

68 g = self.game

69

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

74

75 return act

76

77

78@register_eh

79 class EquipmentTransferHandler(EventHandler):

80 interested = ['post_card_migration']

81

82 def handle(self, evt, trans):

83 if evt == 'post_card_migration': 83 ↛ 96 line 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

91

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

93 if m.card.equipment_skill: 93 ↛ 85 line 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)

95

96 return trans

97

98

99 class ShieldSkill(Skill):

100 associated_action = None

101 target = t_None()

102

103

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

105 treat_as = PhysicalCard.classes['GrazeCard']

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

107

108 def check(self):

109 return False

110

111

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

119

120 def fatetell_action(self, ft):

121 self.fatetell_card = ft.card

122 return bool(ft.succeeded)

123

124

125@register_eh

126 class OpticalCloakHandler(EventHandler):

127 interested = ['action_apply']

128 execute_after = ['AssistedGrazeHandler']

129

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 ↛ exit line 135 didn't return from function 'handle', because the return on line 135 wasn't executed

136

137 g = self.game

138

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

140 return act

141

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

143 act.card = OpticalCloakSkill(tgt)

144

145 return act

146

147

148 class MomijiShieldSkill(ShieldSkill):

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

150

151

152 class MomijiShield(GenericAction):

153 def __init__(self, act):

154 self.action = act

155 self.source = self.target = act.target

156

157 def apply_action(self):

158 self.action.cancelled = True

159 return True

160

161

162@register_eh

163 class MomijiShieldHandler(EventHandler):

164 interested = ['action_before']

165 execute_before = ['HouraiJewelHandler']

166

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

175

176 return act

177

178

179 class UFOSkill(Skill):

180 associated_action = None

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

182 target = t_None()

183

184

185 class GreenUFOSkill(UFOSkill):

186 increment = 1

187

188

189 class RedUFOSkill(UFOSkill):

190 increment = 1

191

192

193@register_eh

194 class UFODistanceHandler(EventHandler):

195 interested = ['calcdistance']

196

197 def handle(self, evt_type, arg):

198 if not evt_type == 'calcdistance': return arg 198 ↛ exit line 198 didn't return from function 'handle', because the return on line 198 wasn't executed

199

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 ↛ 201 line 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

208

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 ↛ 210 line 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

215

216 return arg

217

218

219 class WeaponSkill(Skill):

220 range = 1

221

222

223 class RoukankenSkill(WeaponSkill):

224 associated_action = None

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

226 target = t_None()

227 range = 3

228

229

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

236

237 def apply_action(self):

238 act = self.action

239 tgt = act.target

240

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

242 for s in skills:

243 tgt.disable_skill(s, 'roukanken')

244

245 act.roukanken_disabled_skills = skills

246 return self.game.process_action(act)

247

248

249@register_eh

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

262

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 ↛ 267 line 266 didn't jump to line 267, because the condition on line 266 was never true

267 return act

268

269 if act._[cls]:

270 return act

271

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

273

274 source = act.source

275 if source.has_skill(RoukankenSkill):

276 act = Roukanken(act)

277 return act

278

279 if evt_type == 'action_done' and isinstance(act, basic.BaseAttack):

280 if hasattr(act, 'roukanken_tag'): 280 ↛ 281 line 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')

283

284 return act

285

286

287 class NenshaPhoneSkill(WeaponSkill):

288 associated_action = None

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

290 target = t_None()

291 range = 4

292

293

294 class NenshaPhone(GenericAction):

295 def apply_action(self):

296 tgt = self.target

297

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

299 g = self.game

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

301 migrate_cards(cards, tgt.showncards)

302

303 return True

304

305

306@register_eh

307 class NenshaPhoneHandler(EventHandler):

308 interested = ['action_after']

309

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 ↛ exit line 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

316

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

324

325 return act

326

327

328 class ElementalReactorSkill(WeaponSkill):

329 associated_action = None

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

331 target = t_None()

332 range = 1

333

334

335@register_eh

336 class ElementalReactorHandler(EventHandler):

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

338 execute_after = ['EquipmentTransferHandler']

339

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)

345

346 elif evt_type == 'post_card_migration': 346 ↛ 354 line 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

349

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)

353

354 return arg

355

356

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

362

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 ↛ exit line 367 didn't return from function 'check', because the return on line 367 wasn't executed

368 return len(cl) == 2

369

370

371 class ScarletRhapsody(ForEach):

372 action_cls = basic.Attack

373

374

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

382

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

392

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

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

395

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

397

398 return True

399 except CheckFailed:

400 return False

401

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)

406

407 @property

408 def distance(self):

409 try:

410 return max(1, self.associated_cards[0].distance)

411 except Exception:

412 return 1

413

414

415 class RepentanceStickSkill(WeaponSkill):

416 range = 2

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

418 associated_action = None

419 target = t_None()

420

421

422 class RepentanceStick(GenericAction):

423 def apply_action(self) -> bool:

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

425 g = self.game

426

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

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

429

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

434

435 card = g.user_input(

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

437 )

438

439 if not card:

440 card = random_choose_card(g, cats)

441 if card: 441 ↛ 431 line 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]))

445

446 self.cards = l

447 return True

448

449

450@register_eh

451 class RepentanceStickHandler(EventHandler):

452 interested = ['action_before']

453 execute_before = ['WineHandler']

454

455 def handle(self, evt_type, act):

456 if evt_type == 'action_before' and isinstance(act, Damage):

457 if act.cancelled: return act 457 ↛ exit line 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

465

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

467 return act

468

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

470 act.cancelled = True

471

472 return act

473

474

475 class IbukiGourdSkill(RedUFOSkill):

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

477 increment = 0

478

479

480@register_eh

481 class IbukiGourdHandler(EventHandler):

482 interested = ('action_apply', 'action_after', 'card_migration')

483

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

489

490 g = self.game

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

492

493 elif evt_type == 'action_apply' and isinstance(act, FinalizeStage):

494 tgt = act.target

495 if not tgt.has_skill(IbukiGourdSkill): return act

496

497 g = self.game

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

499

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

501

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

505

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

511

512 return act

513

514

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

520

521

522 class HouraiJewelSkill(WeaponSkill):

523 associated_action = None

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

525 target = t_None()

526 range = 1

527

528

529@register_eh

530 class HouraiJewelHandler(EventHandler):

531 interested = ['action_before']

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

533

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 ↛ exit line 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__)

542

543 return act

544

545

546 class UmbrellaSkill(ShieldSkill):

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

548

549

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

555

556 def apply_action(self):

557 self.damage_act.cancelled = True

558 return True

559

560

561@register_eh

562 class UmbrellaHandler(EventHandler):

563 # 紫的阳伞

564 interested = ['action_before']

565 execute_before = ['RejectHandler']

566

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]

572

573 if isinstance(pact, spellcard.SpellCardAction):

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

575

576 return act

577

578

579 class MaidenCostume(TreatAs, ShieldSkill):

580 treat_as = PhysicalCard.classes['RejectCard']

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

582

583 def check(self):

584 return False

585

586

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

594

595 def fatetell_action(self, ft):

596 act = self.act

597 src = self.source

598

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

608

609

610@register_eh

611 class MaidenCostumeHandler(EventHandler):

612 interested = ['action_before']

613 execute_before = ['RejectHandler']

614 execute_after = ['HouraiJewelHandler']

615

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 ↛ exit line 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

625

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

627

628 return act

629

630

631 class HakuroukenSkill(WeaponSkill):

632 range = 2

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

634 associated_action = None

635 target = t_None()

636

637

638 class Hakurouken(GenericAction):

639 card_usage = 'drop'

640

641 def apply_action(self):

642 src = self.source

643 tgt = self.target

644

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

653

654 return True

655

656 def cond(self, cards):

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

658

659

660@register_eh

661 class HakuroukenHandler(EventHandler):

662 interested = ['action_before']

663

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

669

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 ↛ exit line 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

677

678 g = self.game

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

680 return act

681

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

683

684 return act

685

686

687 class AyaRoundfan(GenericAction):

688 def apply_action(self):

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

690 g = self.game

691

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

696

697 return True

698

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

721

722 # 所以加上这个

723 return self.target.equips

724

725

726 class AyaRoundfanSkill(WeaponSkill):

727 range = 5

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

729 associated_action = None

730 target = t_None()

731

732

733@register_eh

734 class AyaRoundfanHandler(EventHandler):

735 interested = ('action_after',)

736 execute_after = ('DyingHandler', )

737 card_usage = 'drop'

738

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 ↛ exit line 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

744

745 g = self.game

746 pa = g.action_stack[-1]

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

748

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

753

754 return act

755

756 def cond(self, cards):

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

758 return False

759

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

761

762

763 class Laevatein(UserAction):

764 def apply_action(self):

765 return True # logic handled in LaevateinHandler

766

767

768 class LaevateinSkill(WeaponSkill):

769 range = 3

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

771 associated_action = None

772 target = t_None()

773

774

775@register_eh

776 class LaevateinHandler(EventHandler):

777 interested = ['attack_aftergraze']

778 card_usage = 'drop'

779

780 def handle(self, evt_type, arg):

781 if evt_type == 'attack_aftergraze': 781 ↛ 801 line 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

786

787 src = act.source

788 tgt = act.target

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

790 return arg

791

792 g = self.game

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

794 if not cards:

795 return arg

796

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

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

799 return act, True

800

801 return arg

802

803 def cond(self, cards):

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

805

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 ↛ 810 line 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 ↛ 812 line 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 ↛ 814 line 813 didn't jump to line 814, because the condition on line 813 was never true

814 return False

815

816 return True

817

818

819 class DeathSickleSkill(WeaponSkill):

820 range = 2

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

822 associated_action = None

823 target = t_None()

824

825

826 class DeathSickle(GenericAction):

827 def __init__(self, act):

828 self.action = act

829 self.source, self.target = act.source, act.target

830

831 def apply_action(self):

832 self.action.amount += 1

833 return True

834

835

836@register_eh

837 class DeathSickleHandler(EventHandler):

838 interested = ['action_before']

839 execute_before = ['WineHandler']

840

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

852

853 return act

854

855

856 class KeystoneSkill(GreenUFOSkill):

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

858 increment = 1

859

860

861 class Keystone(GenericAction):

862 def __init__(self, act):

863 assert isinstance(act, spellcard.Sinsack)

864 self.source = self.target = act.target

865 self.action = act

866

867 def apply_action(self):

868 self.action.cancelled = True

869 return True

870

871

872@register_eh

873 class KeystoneHandler(EventHandler):

874 interested = ['action_before']

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

876

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

882

883 return act

884

885

886 class WitchBroomSkill(RedUFOSkill):

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

888 increment = 2

889

890

891 class AccessoriesSkill(Skill):

892 associated_action = None

893 target = t_None()

894

895

896 class YinYangOrb(GenericAction):

897 def __init__(self, ft):

898 self.ftact = ft

899 self.source = self.target = ft.target

900

901 def apply_action(self):

902 ft = self.ftact

903 tgt = ft.target

904

905 from .definition import YinYangOrbCard

906 for e in tgt.equips: 906 ↛ 918 line 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)

911

912 detach_cards([e], trans=trans)

913 self.card = e

914 ft.set_card(e, self)

915

916 break

917 else:

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

919

920 return True

921

922

923 class YinYangOrbSkill(AccessoriesSkill):

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

925

926

927@register_eh

928 class YinYangOrbHandler(EventHandler):

929 interested = ['fatetell']

930 execute_after = ['FatetellMalleateHandler']

931

932 def handle(self, evt_type, act):

933 if evt_type == 'fatetell': 933 ↛ 942 line 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

939

940 g.process_action(YinYangOrb(act))

941

942 return act

943

944

945 class SuwakoHatSkill(AccessoriesSkill):

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

947

948

949 class SuwakoHatEffect(UserAction):

950 def __init__(self, target, dcs):

951 self.source = self.target = target

952 self.dcs = dcs

953

954 def apply_action(self):

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

956 return True

957

958

959@register_eh

960 class SuwakoHatHandler(EventHandler):

961 interested = ['action_before']

962

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

968

969 return act

970

971

972 class YoumuPhantomSkill(AccessoriesSkill):

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

974

975

976 class YoumuPhantomHeal(basic.Heal):

977 pass

978

979

980@register_eh

981 class YoumuPhantomHandler(EventHandler):

982 interested = ['post_card_migration']

983

984 def handle(self, evt_type, arg):

985 if not evt_type == 'post_card_migration': return arg 985 ↛ exit line 985 didn't return from function 'handle', because the return on line 985 wasn't executed

986

987 from .definition import YoumuPhantomCard

988 trans = cast(MigrateCardsTransaction, arg)

989 g = self.game

990

991 for m in trans.movements:

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

993 continue

994

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

1000

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

1002 tgt = m.to.owner

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

1004

1005 return arg

1006

1007

1008 class IceWingSkill(RedUFOSkill):

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

1010 increment = 1

1011

1012

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

1018

1019 def apply_action(self):

1020 self.action.cancelled = True

1021 return True

1022

1023

1024@register_eh

1025 class IceWingHandler(EventHandler):

1026 interested = ['action_before']

1027 _effect_cls = spellcard.SealingArray, spellcard.FrozenFrog

1028

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

1030

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

1035

1036 return act

1037

1038

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

1048

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]

1056

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 ↛ 1061 line 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 ↛ exit line 1062 didn't return from function 'check', because the return on line 1062 wasn't executed

1063 return True

1064

1065

1066@register_eh

1067 class GrimoireHandler(EventHandler):

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

1069

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

1077

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

1079 raise ActionLimitExceeded

1080

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

1082 raise VitalityLimitExceeded

1083

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

1090

1091 return act

1092

1093

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

1100

1101 self.fatetell_cond = lambda c: c.suit == Card.SPADE and 1 <= c.number <= 8

1102

1103 def fatetell_action(self, ft):

1104 if not ft.succeeded:

1105 return False

1106

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

1112

1113

1114 class SinsackHat(ShieldSkill):

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

1116

1117

1118@register_eh

1119 class SinsackHatHandler(EventHandler):

1120 interested = ['action_after']

1121

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

1130

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

1132 return act

1133

1134 return act