Hide keyboard shortcuts

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 

3 

4# -- stdlib -- 

5from typing import cast 

6 

7# -- third party -- 

8# -- own -- 

9from game.base import EventHandler 

10from thb.actions import ActionStage, ActionStageLaunchCard, AskForCard, Damage, DropCards 

11from thb.actions import FinalizeStage, ForEach, GenericAction, LaunchCard, MigrateCardsTransaction 

12from thb.actions import PrepareStage, UseCard, UserAction, VitalityLimitExceeded, register_eh 

13from thb.actions import user_choose_cards 

14 

15 

16# -- code -- 

17class BasicAction(UserAction): 

18 pass 

19 

20 

21class BaseAttack(UserAction): 

22 

23 def __init__(self, source, target, damage=1): 

24 self.source = source 

25 self.target = target 

26 self.damage = damage 

27 

28 def apply_action(self): 

29 g = self.game 

30 source, target = self.source, self.target 

31 rst = g.process_action(LaunchGraze(target)) 

32 self1, rst = g.emit_event('attack_aftergraze', (self, not rst)) 

33 assert self1 is self 

34 assert rst in (False, True) 

35 if rst: 

36 g.process_action(Damage(source, target, amount=self.damage)) 

37 return True 

38 else: 

39 return False 

40 

41 def is_valid(self): 

42 return not self.target.dead 

43 

44 

45class Attack(BaseAttack, BasicAction): 

46 pass 

47 

48 

49class InevitableAttack(Attack): 

50 def apply_action(self): 

51 g = self.game 

52 dmg = Damage(self.source, self.target, amount=self.damage) 

53 g.process_action(dmg) 

54 return True 

55 

56 

57@register_eh 

58class AttackCardRangeHandler(EventHandler): 

59 interested = ['calcdistance'] 

60 

61 def handle(self, evt_type, act): 

62 if evt_type == 'calcdistance': 

63 src, card, dist = act 

64 from .definition import AttackCard 

65 if card.is_card(AttackCard): 

66 self.fix_attack_range(src, dist) 

67 

68 return act 

69 

70 @staticmethod 

71 def attack_range_bonus(p): 

72 from .equipment import WeaponSkill 

73 l = [ 

74 s.range - 1 for s in p.skills 

75 if issubclass(s, WeaponSkill) and p.has_skill(s) 

76 ] 

77 return max(0, 0, *l) 

78 

79 @classmethod 

80 def fix_attack_range(cls, src, dist): 

81 l = cls.attack_range_bonus(src) 

82 for p in dist: 

83 dist[p] -= l 

84 

85 

86@register_eh 

87class AttackCardVitalityHandler(EventHandler): 

88 interested = ['action_before', 'action_shootdown'] 

89 

90 def handle(self, evt_type, act): 

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

92 from .definition import AttackCard 

93 src = act.source 

94 if act.card.is_card(AttackCard) and not self.is_disabled(src): 

95 act._['vitality-handler'] = 'already-handled' 

96 src.tags['vitality'] -= 1 

97 

98 elif evt_type == 'action_shootdown' and isinstance(act, ActionStageLaunchCard): 98 ↛ 99line 98 didn't jump to line 99, because the condition on line 98 was never true

99 from .definition import AttackCard 

100 if act.card.is_card(AttackCard): 

101 src = act.source 

102 if self.is_disabled(src): 

103 return act 

104 

105 if act._['vitality-handler']: 

106 return act 

107 

108 if src.tags['vitality'] > 0: 

109 return act 

110 

111 raise VitalityLimitExceeded 

112 

113 return act 

114 

115 @staticmethod 

116 def disable(p): 

117 p.tags['attack_card_vitality'] = p.tags['turn_count'] 

118 

119 @staticmethod 

120 def enable(p): 

121 p.tags['attack_card_vitality'] = 0 

122 

123 @staticmethod 

124 def is_disabled(p): 

125 return p.tags['attack_card_vitality'] >= p.tags['turn_count'] 

126 

127 

128@register_eh 

129class VitalityHandler(EventHandler): 

130 interested = ['action_before'] 

131 

132 def handle(self, evt_type, act): 

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

134 act.source.tags['vitality'] = 1 

135 elif evt_type == 'action_before' and isinstance(act, FinalizeStage): 135 ↛ 136line 135 didn't jump to line 136, because the condition on line 135 was never true

136 act.source.tags['vitality'] = 0 

137 

138 return act 

139 

140 

141class Heal(BasicAction): 

142 

143 def __init__(self, source, target, amount=1): 

144 self.source = source 

145 self.target = target 

146 self.amount = amount 

147 

148 def apply_action(self): 

149 target = self.target 

150 if target.life < target.maxlife: 

151 target.life = min(target.life + self.amount, target.maxlife) 

152 return True 

153 else: 

154 return False 

155 

156 def is_valid(self): 

157 tgt = self.target 

158 return not tgt.dead and tgt.life < tgt.maxlife 

159 

160 

161class GrazeAction(BasicAction): 

162 

163 def apply_action(self): 

164 return True 

165 

166 

167class UseAttack(AskForCard): 

168 card_usage = 'use' 

169 

170 def __init__(self, target): 

171 from thb.cards.definition import AttackCard 

172 AskForCard.__init__(self, target, target, AttackCard) 

173 

174 def process_card(self, card): 

175 g = self.game 

176 return g.process_action(UseCard(self.target, card)) 

177 

178 def ask_for_action_verify(self, p, cl, tl): 

179 return UseCard(p, cl[0]).can_fire() 

180 

181 

182class BaseUseGraze(AskForCard): 

183 def __init__(self, target): 

184 from thb.cards.definition import GrazeCard 

185 AskForCard.__init__(self, target, target, GrazeCard) 

186 

187 

188class UseGraze(BaseUseGraze): 

189 card_usage = 'use' 

190 

191 def process_card(self, card): 

192 g = self.game 

193 return g.process_action(UseCard(self.target, card)) 

194 

195 def ask_for_action_verify(self, p, cl, tl): 

196 return UseCard(p, cl[0]).can_fire() 

197 

198 

199class LaunchGraze(BaseUseGraze): 

200 card_usage = 'launch' 

201 

202 def process_card(self, card): 

203 g = self.game 

204 tgt = self.target 

205 return g.process_action(LaunchCard(tgt, [tgt], card, GrazeAction(tgt, tgt))) 

206 

207 def ask_for_action_verify(self, p, cl, tl): 

208 tgt = self.target 

209 return LaunchCard(tgt, [tgt], cl[0], GrazeAction(tgt, tgt)).can_fire() 

210 

211 

212class AskForHeal(AskForCard): 

213 card_usage = 'launch' 

214 

215 def __init__(self, source, target): 

216 from thb import cards 

217 AskForCard.__init__(self, source, target, cards.definition.HealCard) 

218 

219 def process_card(self, card): 

220 g = self.game 

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

222 heal_cls = card.associated_action or Heal 

223 return g.process_action(LaunchCard(tgt, [src], card, heal_cls(tgt, src))) 

224 

225 def ask_for_action_verify(self, p, cl, tl): 

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

227 card = cl[0] 

228 heal_cls = card.associated_action or Heal 

229 return LaunchCard(tgt, [src], card, heal_cls(tgt, src)).can_fire() 

230 

231 

232class Wine(BasicAction): 

233 def apply_action(self): 

234 self.target.tags['wine'] = True 

235 return True 

236 

237 def is_valid(self): 

238 return not self.target.dead 

239 

240 

241class SoberUp(GenericAction): 

242 def apply_action(self): 

243 self.target.tags['wine'] = False 

244 return True 

245 

246 

247class WineRevive(GenericAction): 

248 def __init__(self, act): 

249 self.act = act 

250 self.source = act.target 

251 self.target = act.target 

252 

253 def apply_action(self): 

254 self.act.amount -= 1 

255 tgt = self.target 

256 self.game.process_action(SoberUp(tgt, tgt)) 

257 return True 

258 

259 

260@register_eh 

261class WineHandler(EventHandler): 

262 interested = ['action_apply', 'action_before', 'post_choose_target'] 

263 

264 def handle(self, evt_type, act): 

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

266 pact = ForEach.get_actual_action(act) or act 

267 if getattr(pact, 'in_wine', False): 

268 act.damage += 1 

269 

270 elif evt_type == 'post_choose_target': 270 ↛ 271line 270 didn't jump to line 271, because the condition on line 270 was never true

271 act, tl = arg = act 

272 

273 from thb.cards.definition import AttackCard 

274 if act.card.is_card(AttackCard): 

275 src = act.source 

276 if src.tags['wine']: 

277 self.game.process_action(SoberUp(src, src)) 

278 act.card_action.in_wine = True 

279 

280 return arg 

281 

282 elif evt_type == 'action_apply' and isinstance(act, PrepareStage): 282 ↛ 283line 282 didn't jump to line 283, because the condition on line 282 was never true

283 src = act.target 

284 if src.tags['wine']: 

285 self.game.process_action(SoberUp(src, src)) 

286 

287 elif evt_type == 'action_before' and isinstance(act, Damage): 287 ↛ 288line 287 didn't jump to line 288, because the condition on line 287 was never true

288 if act.cancelled: return act 

289 if act.amount < 1: return act 

290 tgt = act.target 

291 if act.amount >= tgt.life and tgt.tags['wine']: 

292 g = self.game 

293 g.process_action(WineRevive(act)) 

294 

295 return act 

296 

297 

298class Exinwan(BasicAction): 

299 # 恶心丸 

300 def apply_action(self): 

301 return True 

302 

303 

304class ExinwanEffect(GenericAction): 

305 # 恶心丸 

306 card_usage = 'drop' 

307 

308 def apply_action(self): 

309 g = self.game 

310 tgt = self.target 

311 if tgt.dead: 

312 return False 

313 

314 cards = user_choose_cards(self, tgt, ('cards', 'showncards', 'equips')) 

315 

316 if cards: 

317 g.process_action(DropCards(tgt, tgt, cards)) 

318 else: 

319 g.process_action(Damage(None, tgt)) 

320 

321 return True 

322 

323 def cond(self, cards): 

324 if len(cards) != 2: return False 

325 from thb.cards.base import Skill 

326 if any(isinstance(c, Skill) for c in cards): return False 

327 return True 

328 

329 def is_valid(self): 

330 return not self.target.dead 

331 

332 

333@register_eh 

334class ExinwanHandler(EventHandler): 

335 # 恶心丸 

336 

337 interested = ['post_card_migration'] 

338 

339 def handle(self, evt_type, arg) -> None: 

340 from thb.cards.base import VirtualCard, HiddenCard 

341 from thb.cards.definition import ExinwanCard 

342 

343 if evt_type == 'post_card_migration': 

344 moves = cast(MigrateCardsTransaction, arg).movements 

345 moves = [m for m in moves if m.to.type == 'droppedcard'] 

346 

347 # cards to dropped area should all unwrapped 

348 assert not any( 

349 m.card.is_card(VirtualCard) or m.card.is_card(HiddenCard) 

350 for m in moves 

351 ) 

352 

353 moves = [m for m in moves if m.card.is_card(ExinwanCard)] 

354 

355 # no same card dropped twice in the same transaction 

356 assert len(moves) == len(set(m.card for m in moves)) 

357 

358 for m in moves: 

359 tgt = m.trans.action.source 

360 if tgt and not tgt.dead: 

361 act = ExinwanEffect(tgt, tgt) 

362 act.associated_card = m.card 

363 self.game.process_action(act) 

364 

365 return arg