-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathabilities.py
351 lines (272 loc) · 12.8 KB
/
abilities.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
import dice
import messages
import world
from mechanics import DnDRuleset as R
""" asahala 2020
https://github.com/asahala/DnD5e-CombatSimulator/ """
class Ability:
""" Base class for special creature actions and abilities
:param name ability name
:param dc save DC
:param save ability score tied to this save
:param to-hit to hit bonus
:param success damage multiplier if successful save
:param recharge recharge value
:type name str
:type dc int
:type save str
:type to-hit int
:type success float
:type recharge int """
def __init__(self, name, type_='ability', dc=None, save=None, to_hit=None,
success=None, recharge=0, **kwargs):
self.type = type_
self.name = name
self.dc = dc
self.save = save
self.success = success
self.to_hit = to_hit
self.recharge = recharge
self.available = True
def __repr__(self):
return "%s (DC: %i) " % (self.name, self.dc)
def check_and_recharge(self):
if not self.available:
if dice.roll(1, 6, 0) >= self.recharge:
self.available = True
class Restrain(Ability):
def use(self, source, target, total_damage=0, crit_multipiler=1):
if R.roll_hit(source, target, self):
target.set_restrain(True, self.dc, self.save)
self.available = False
class Paralysis(Ability):
def use(self, source, target, total_damage=0, crit_multipiler=1):
if not R.roll_save(target, self.save, self.dc):
target.set_paralysis(True, self.dc, self.save)
class Grapple(Ability):
def use(self, source, target, total_damage=0, crit_multipiler=1):
if not R.roll_save(target, self.save, self.dc):
target.set_grapple(True, self.dc, self.save, source)
class Knockdown(Ability):
def __init__(self, bonus_action=None, charge_distance=0, **kwargs):
super().__init__(**kwargs)
self.bonus_action = bonus_action
self.charge_distance = charge_distance
def use(self, source, target, total_damage=0, crit_multipiler=1):
if source.distance >= self.charge_distance:
if not R.roll_save(target, self.save, self.dc):
target.set_prone(True)
if self.bonus_action is not None:
self.bonus_action.use(source, target, always_hit=False)
source.distance = 0
class Swallow(Ability):
def use(self, source, target, total_damage=0, crit_multipiler=1):
if not R.roll_save(target, self.save, self.dc) \
and source.size - target.size >= 2:
target.set_swallowed(state=True, source=source)
source.focused_enemy = None
class Knockback(Ability):
def __init__(self, bonus_action=None,
charge_distance=0,
knockback_distance=0,
**kwargs):
super().__init__(**kwargs)
self.bonus_action = bonus_action
self.charge_distance = charge_distance
self.knockback_distance = knockback_distance
def use(self, source, target, total_damage=0, crit_multipiler=1):
if source.distance >= self.charge_distance:
if self.bonus_action is not None:
self.bonus_action.use(source, target, always_hit=True)
if not R.roll_save(target, self.save, self.dc):
target.set_prone(True)
knockback_path = world.get_opposite(target.position,
source.position,
self.knockback_distance)
world.force_move(source, target, knockback_path, self.name)
source.distance = 0
class Poison(Ability):
# TODO: Make this properly
# Saving throw has to be checked in two different places
# and thus it's now saved to target creature class as well as
# returned.
def __init__(self, damage, damage_type, duration=None, **kwargs):
super().__init__(**kwargs)
self.damage = dice.parse_damage(damage)
self.damage_type = damage_type
self.duration = duration
def use(self, source, target, total_damage=0, crit_multipiler=1):
messages.IO.reset()
messages.IO.log += f"{source.name} on-hit effect on {target.name}."
save_success = R.roll_save(target, self.save, self.dc)
if self.damage is not None:
R.roll_damage(source, target, self, crit_multipiler,
self.success, self.save, self.dc)
if not save_success and self.duration is not None:
self.apply_condition(target)
def apply_condition(self, target):
target.set_poison(state=True, dc=self.dc,
save=self.save, duration=self.duration)
class MummyRot(Poison):
# TODO: Define properly
def use(self, source, target, total_damage=0, crit_multipiler=1):
save_success = R.roll_save(target, self.save, self.dc)
if save_success:
pass
else:
multi = 1
if self.damage_type in target.immunities \
or self.name in target.immunities:
pass
elif self.damage_type in target.resistances:
multi = 0.5
else:
target.take_max_hp_damage(source, {'necrotic': 10*multi}, self.name)
target.prevent_heal = True
target.immunities.append(self.name)
source.damage_dealt += 10*multi
class DamageMaxHP(Ability):
def use(self, source, target, total_damage, crit_multipiler=1):
if not R.roll_save(target, self.save, self.dc):
target.take_max_hp_damage(source, total_damage, self.name)
""" ================================================================ """
""" ======================= SPECIAL FEATURES ======================= """
""" ================================================================ """
class Stomach:
""" Stomach for creatures that can swallow other creatures """
## handle swallowing creatures that have swallowed other creatures
## regurgitation also spawns creatures to the same coordinates with
## the swallower!
def __init__(self, name, damage, damage_type, breakout_dmg, breakout_dc):
self.name = name
self.contents = []
self.damage = dice.parse_damage(damage)
self.damage_type = damage_type
self.breakout_dmg = breakout_dmg
self.breakout_dc = breakout_dc
self.damage_count = 0
def regurgitate(self):
for target in self.contents:
target.set_swallowed(state=False)
self.contents = []
def check_status(self, source):
if self.damage_count >= self.breakout_dmg:
self.regurgitate()
self.damage_count = 0
else:
dissolved = set()
for target in self.contents:
messages.IO.reset()
messages.IO.log += "{source} digests {target}.".format(
source=source.name, target=target.name)
R.roll_damage(source, target, self)
""" If target dies, dissolve it """
if target.is_dead:
dissolved.add(target)
""" Destroy dissolved targets """
for target in dissolved:
self.contents.remove(target)
""" ================================================================ """
""" ======================= PASSIVE ABILITIES ====================== """
""" ================================================================ """
class FrightfulPresence(Ability):
def __init__(self, duration, range, **kwargs):
super().__init__(**kwargs)
self.duration = duration
self.range = range
self.type = 'on_start'
def use(self, creature, allies, enemies=[]):
for e in (e for e in enemies.get_alive()
if world.get_dist(creature.position, e.position) <= int(self.range / 5)
and self.name not in e.immunities):
if R.roll_save(e, self.save, self.dc):
e.immunities.append(self.name)
else:
e.set_fear(state=True, dc=self.dc, save=self.save, duration=self.duration, by=creature)
class DreadfulGlare(Ability):
""" This is really an action but here it is defined as a passive
ability that targets random enemy at the start of each turn """
def use(self, creature, allies, enemies=[]):
for e in (e for e in enemies.get_alive()
if world.get_dist(creature.position, e.position) <= 8
and self.name not in e.immunities):
if R.roll_save(e, self.save, self.dc):
e.immunities.append(self.name)
break
else:
e.set_fear(state=True, dc=self.dc, save=self.save, duration=2, by=creature)
if R.roll_save(e, self.save, self.dc+5):
e.set_paralysis(state=True, dc=self.dc+5, save=self.save, duration=2)
break
class Stench(Ability):
""" Undead stench that poisons nearby enemies. Targets gain
immunity to this ability on successful save """
def use(self, creature, allies, enemies=[]):
""" Check if enemies are nearby """
for enemy in (e for e in enemies.get_alive()):
if world.is_adjacent(enemy.position, creature.position):
""" Roll save and set immunity if success"""
if R.roll_save(enemy, self.save, self.dc):
enemy.immunities.append(self.name)
else:
""" Else apply poison """
if self.name not in enemy.immunities \
or not enemy.is_poisoned \
or 'poison' not in enemy.immunities:
enemy.set_poison(state=True, dc=self.dc,
save=self.save, duration=1)
class Regeneration(Ability):
def __init__(self, amount, **kwargs):
super().__init__(**kwargs)
self.amount = amount
def use(self, creature, allies=[], enemies=[]):
creature.heal(amount=self.amount, spellname=self.name)
class PackTactics:
""" Pack tactics gives and advantage to hit rolls if
adjacent squares contain allies """
type = 'on_start'
@staticmethod
def use(creature, allies, enemies=[]):
ally_positions = (a.position for a in allies.get_alive()
if a.position != creature.position)
if world.any_is_adjacent(creature.position, ally_positions):
creature.set_advantage('hit', 1)
else:
creature.set_advantage('hit', 0)
class AvoidDeath:
""" Avoid death allows creatures to drop to N hitpoints instead
of zero if it succeeds a save against the damage, e.g. Undead
Fortitude. This can be also used to prevent certain monsters from
dying unless specific damage type is used, e.g. trolls require
fire or acid (if no saves or crits, just give impossible negative
penalty and crit multiplier, see troll in definitions.
:param name ability name
:param penalty penalty to saving throw
:param save ability tied to save
:param minimum_hp amount of remaining hp if successful
:param vulnerabilities damage types that ignore this passive
:param min_crit_to_kill minimum crit multiplier that ignores this
passive
:type name str
:type penalty int
:type save str
:type minimum_hp int
:type vulnerabilities list(str, str ...)
:type min_crit_to_kill int """
def __init__(self, name, save, minimum_hp, penalty=0,
vulnerabilities=[], min_crit_to_kill=1):
self.name = name
self.penalty = penalty
self.type = 'avoid_death'
self.save = save
self.minimum_hp = minimum_hp
self.min_crit = min_crit_to_kill
self.vulnerabilities = vulnerabilities
def use(self, creature, damage, damage_type, critical):
if critical > self.min_crit or damage_type in self.vulnerabilities:
return creature.hp
else:
if R.roll_save(creature, self.save, damage + self.penalty):
messages.IO.conditions.append('%s resists death with %s!' % (creature.name, self.name))
return self.minimum_hp
return creature.hp