# Coverage for thb/characters/reimu.py : 73%

## 184 statements 142 run 42 missing 0 excluded 8 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# -- third party --

6# -- own --

7 from game.base import InterruptActionFlow

8 from thb.actions import ActionStage, AskForCard, Damage, DrawCards, FinalizeStage, LaunchCard

9 from thb.actions import PlayerRevive, PlayerTurn, UserAction, migrate_cards, ttags

10 from thb.cards.base import Card, PhysicalCard, Skill

11 from thb.cards.classes import AttackCard, GreenUFOSkill, RejectCard, TreatAs, UFOSkill, t_None

12 from thb.characters.base import Character, register_character_to

13 from thb.inputlets import ChooseOptionInputlet

14 from thb.mode import THBEventHandler

15

16

17# -- code --

18 class Flight(GreenUFOSkill):

19 skill_category = ['character', 'passive', 'compulsory']

20

21 @staticmethod

22 def increment(src):

23 for c in src.equips:

24 if issubclass(c.equipment_skill, UFOSkill):

25 return 0

26

27 return 1

28

29

30 class SpiritualAttack(TreatAs, Skill):

31 skill_category = ['character', 'active']

32 treat_as = RejectCard

33

34 def check(self):

35 cl = self.associated_cards

36 if not(cl and len(cl) == 1 and cl[0].color == Card.RED):

37 return False

38

39 c = cl[0]

40 if c.resides_in is None or c.resides_in.type not in (

41 'cards', 'showncards'

42 ): return False

43

44 return True

45

46

47 class TributeTarget(Skill):

48 associated_action = None

49 skill_category = ['character', 'passive', 'boss']

50 target = t_None()

51

52

53 class TributeAction(UserAction):

54 def apply_action(self):

55 tgt = self.target

56 migrate_cards([self.associated_card], tgt.showncards, unwrap=True)

57 src = self.source

58 src.tags['tribute_tag'] = src.tags['turn_count']

59 return True

60

61 def is_valid(self):

62 p = self.source

63

64 if p.tags.get('turn_count', 0) <= p.tags.get('tribute_tag', 0):

65 return False

66

67 tgt = self.target

68

69 if tgt.dead: return False

70 if len(tgt.cards) + len(tgt.showncards) >= tgt.maxlife: return False

71 return True

72

73

74 class Tribute(Skill):

75 associated_action = TributeAction

76 skill_category = ['active']

77 no_drop = True

78 usage = 'handover'

79

80 def check(self):

81 cl = self.associated_cards

82 rst = cl and len(cl) == 1 and (

83 cl[0].resides_in is not None and

84 cl[0].resides_in.type in ('cards', 'showncards') and

85 cl[0].is_card(PhysicalCard)

86 )

87 return rst

88

89 def target(self, src, tl):

90 tl = [t for t in tl if not t.dead and t.has_skill(TributeTarget)]

91 try:

92 tl.remove(src)

93 except ValueError:

94 pass

95 return (tl[-1:], bool(len(tl)))

96

97

98 class TributeHandler(THBEventHandler):

99 interested = ['action_after', 'game_begin', 'switch_character']

100

101 def handle(self, evt_type, arg):

102 if evt_type == 'game_begin':

103 self.manage_tribute()

104

105 elif evt_type == 'switch_character':

106 self.manage_tribute()

107

108 elif evt_type == 'action_after' and isinstance(arg, PlayerRevive): 108 ↛ 109 line 108 didn't jump to line 109, because the condition on line 108 was never true

109 self.manage_tribute()

110

111 return arg

112

113 def manage_tribute(self):

114 g = self.game

115 cond = any([

116 isinstance(p, Character) and p.has_skill(TributeTarget)

117 for p in g.players

118 ])

119

120 if cond: 120 ↛ 121 line 120 didn't jump to line 121, because the condition on line 120 was never true

121 for p in g.players:

122 if not isinstance(p, Character): continue

123 if p.has_skill(TributeTarget): continue

124 if not p.has_skill(Tribute):

125 p.skills.append(Tribute)

126 else:

127 for p in g.players:

128 if not isinstance(p, Character): continue 128 ↛ 127 line 128 didn't jump to line 127, because the continue on line 128 wasn't executed

129 try:

130 p.skills.remove(Tribute)

131 except ValueError:

132 pass

133

134

135# -----------------------------------------

136

137

138 class ReimuExterminate(Skill):

139 associated_action = None

140 skill_category = ['character', 'passive']

141 target = t_None()

142

143

144 class ReimuExterminateLaunchCard(LaunchCard):

145 def __init__(self, source, target, card, cause):

146 LaunchCard.__init__(self, source, [target], card, bypass_check=True)

147 self.cause = cause # for ui

148

149

150 class ReimuExterminateAction(AskForCard):

151 card_usage = 'launch'

152

153 def __init__(self, source, target, cause):

154 AskForCard.__init__(self, source, source, AttackCard)

155 self.victim = target

156 self.cause = cause # for ui

157

158 def process_card(self, c):

159 g = self.game

160 return g.process_action(ReimuExterminateLaunchCard(self.source, self.victim, c, self.cause))

161

162

163 class ReimuExterminateHandler(THBEventHandler):

164 interested = ('action_apply', 'action_after')

165 execute_after = ('DyingHandler',

166 'CheatingHandler',

167 'IbukiGourdHandler',

168 'DisarmHandler',

169 'FreakingPowerHandler',

170 'LunaticHandler',

171 'AyaRoundfanHandler',

172 'NenshaPhoneHandler',

173 )

174

175 def handle(self, evt_type, act):

176 if evt_type == 'action_apply' and isinstance(act, Damage):

177 if not act.source: return act

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

179 g = self.game

180

181 try:

182 current = PlayerTurn.get_current(g).target

183 except IndexError:

184 return act

185

186 if src is not current: return act

187 if src is tgt: return act 187 ↛ exit line 187 didn't return from function 'handle', because the return on line 187 wasn't executed

188 ttags(src)['did_damage'] = True

189

190 elif evt_type == 'action_after' and isinstance(act, Damage):

191 if not act.source: return act

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

193 g = self.game

194 try:

195 cur = PlayerTurn.get_current(g).target

196 except IndexError:

197 return act

198 if not cur: return act 198 ↛ exit line 198 didn't return from function 'handle', because the return on line 198 wasn't executed

199 if not tgt.has_skill(ReimuExterminate): return act

200 if cur.dead: return act 200 ↛ exit line 200 didn't return from function 'handle', because the return on line 200 wasn't executed

201 if cur is tgt: return act

202 g.process_action(ReimuExterminateAction(tgt, cur, 'damage'))

203

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

205 tgt = act.target

206 if not ttags(tgt)['did_damage']:

207 return act

208

209 if tgt.dead: 209 ↛ 210 line 209 didn't jump to line 210, because the condition on line 209 was never true

210 return act

211

212 g = self.game

213 current = PlayerTurn.get_current(g).target

214 for actor in g.players.rotate_to(current):

215 if tgt is actor:

216 continue

217

218 if not actor.has_skill(ReimuExterminate):

219 continue

220

221 g.process_action(ReimuExterminateAction(actor, tgt, 'finalize'))

222

223 return act

224

225

226 class ReimuClear(Skill):

227 associated_action = None

228 skill_category = ['character', 'passive']

229 target = t_None()

230

231

232 class ReimuClearAction(UserAction):

233 def apply_action(self):

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

235 g = self.game

236 g.process_action(DrawCards(src, 1))

237 g.process_action(DrawCards(tgt, 1))

238 current = PlayerTurn.get_current(g).target

239 if current is src:

240 return True

241 else:

242 for act in reversed(g.action_stack):

243 if isinstance(act, ActionStage):

244 raise InterruptActionFlow(unwind_to=act)

245 else:

246 return True

247

248

249 class ReimuClearHandler(THBEventHandler):

250 interested = ('action_after',)

251 execute_before = (

252 'AyaRoundfanHandler',

253 'NenshaPhoneHandler',

254 'DilemmaHandler',

255 'DecayDamageHandler',

256 'EchoHandler',

257 'MelancholyHandler',

258 'MajestyHandler',

259 'MasochistHandler',

260 )

261

262 execute_after = (

263 'IbukiGourdHandler',

264 )

265

266 def handle(self, evt_type, act):

267 if evt_type == 'action_after' and isinstance(act, Damage):

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

269 if not src: return act

270 if not src.has_skill(ReimuClear): return act

271 if src is tgt: return act 271 ↛ exit line 271 didn't return from function 'handle', because the return on line 271 wasn't executed

272 if src.dead or tgt.dead: return act

273

274 g = self.game

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

276 g.process_action(ReimuClearAction(src, tgt))

277

278 return act

279

280

281@register_character_to('common', 'boss')

282 class Reimu(Character):

283 # skills = [SealingArraySkill, Flight, TributeTarget]

284 # skills = [SpiritualAttack, Flight]

285 skills = [ReimuExterminate, ReimuClear]

286 boss_skills = [TributeTarget]

287 eventhandlers = [ReimuExterminateHandler, ReimuClearHandler, TributeHandler]

288 maxlife = 4