-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathpopulate_functions_old.py
More file actions
2713 lines (2614 loc) · 170 KB
/
populate_functions_old.py
File metadata and controls
2713 lines (2614 loc) · 170 KB
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
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
from __future__ import annotations
import csv
import json
import math
import os
from copy import deepcopy
from logging import debug
from random import randint
from typing import List, Tuple, Optional
import re
from dao_classes import CategoryType, DamageDice, Monster, Armor, RangeType, SpecialAbility, SpellCaster, Weapon, Race, \
SubRace, Proficiency, ClassType, Language, Equipment, WeaponProperty, WeaponRange, AbilityType, WeaponThrowRange, \
Trait, EquipmentCategory, \
Abilities, Action, Damage, ActionType, DamageType, Spell, ProfType, Condition, Inventory, AreaOfEffect, Cost
from populate_rpg_functions import load_armor_image_name, load_weapon_image_name
from tools.common import parse_challenge_rating, resource_path
""" CSV loads """
path = os.path.dirname(__file__)
def populate_names(race: Race) -> dict():
"""
:return: list of names (except humans and half-elf)
"""
names_list = dict()
with open(resource_path(f"data/names/{race.index}.csv"), newline='') as csv_file:
csv_data = csv.reader(csv_file, delimiter=',')
for gender, name in csv_data:
if gender not in names_list:
names_list[gender] = []
else:
names_list[gender].append(name)
return names_list
def populate_human_names() -> dict():
"""
:return: list of names (humans and half-elf)
"""
names_list = dict()
with open(resource_path("data/names/human.csv"), newline='') as csv_file:
csv_data = csv.reader(csv_file, delimiter=',')
for ethnic, sex, name in csv_data:
if ethnic not in names_list:
names_list[ethnic] = dict()
else:
if sex not in names_list[ethnic]:
names_list[ethnic][sex] = []
else:
names_list[ethnic][sex].append(name)
return names_list
def read_csvfile_old(filename: str):
"""
:param filename: csv file in Tables directory
:return: list of dictionaries
"""
result = []
with open(resource_path(f'Tables/{filename}'), newline='') as csv_file:
reader = csv.reader(csv_file, delimiter=';')
headers = next(reader, None)
csv_data = csv.DictReader(csv_file, delimiter=';')
for row in csv_data:
result.append({header: row(header) for header in headers})
return result
def read_csvfile(filename: str):
"""
:param filename: csv file in Tables directory
:return: list of dictionaries
"""
with open(resource_path(f'Tables/{filename}'), newline='') as csv_file:
reader = csv.reader(csv_file, delimiter=';')
next(reader, None)
return list(reader)
def height_weight_table() -> List:
"""
:return: List of race height/weight modifier's parameters
"""
"""Race;Base Height;Height Modifier;Base Weight;Weight Modifier"""
headers = ['Race', 'Base Height', 'Height Modifier',
'Base Weight', 'Weight Modifier']
hw_conv_table = []
with open(resource_path("Tables/Height and Weight-Height and Weight.csv"), newline='') as csv_file:
# csv_data = csv.reader(csv_file, delimiter=';')
# next(csv_data, None)
csv_data = csv.DictReader(csv_file, delimiter=';')
for row in csv_data:
hw_conv_table.append({header: row(header) for header in headers})
return hw_conv_table
""" JSON loads """
def populate(collection_name: str, key_name: str, with_url=False, collection_path: str = None) -> List[str]:
"""
Load collection data from dnd-5e-core (preferred) or local collections directory (fallback).
:param collection_name: Name of the collection file (without .json)
:param key_name: Key to extract from JSON (usually 'results')
:param with_url: If True, return tuples of (index, url)
:param collection_path: Optional custom path to collections directory
:return: List of collection names or (name, url) tuples
"""
# Try using dnd-5e-core first (preferred method)
try:
from dnd_5e_core.data import populate as core_populate
return core_populate(collection_name, key_name, with_url, collection_path)
except ImportError:
# Fallback to local implementation if dnd-5e-core not available
pass
except Exception as e:
# If dnd-5e-core fails for another reason, log and fallback
print(f"Warning: dnd-5e-core populate failed ({e}), using local fallback")
# Fallback: Use local collections directory
if not collection_path:
collection_path = 'collections'
try:
with open(resource_path(f"{collection_path}/{collection_name}.json"), "r") as f:
data = json.loads(f.read())
collection_json_list = data[key_name]
except Exception as e:
print(f'Error loading collection {collection_name}: {e}')
if 'f' in locals():
print(f'File: {f.name} - key_name: {key_name}')
exit(0)
if with_url:
data_list = [(json_data['index'], json_data['url'])
for json_data in collection_json_list]
else:
data_list = [json_data['index'] for json_data in collection_json_list]
return data_list
def request_damage_type(index_name: str) -> DamageType:
"""
Send a request to local database for a damage type's characteristic
:param index_name: name of the damage type
:return: DamageType object
"""
with open(resource_path(f"data/damage-types/{index_name}.json"), "r") as f:
data = json.loads(f.read())
return DamageType(index=data['index'], name=data['name'], desc=data['desc'])
def request_condition(index_name: str) -> Condition:
"""
Send a request to local database for a condition's characteristic
:param index_name: name of the condition
:return: Condition object
"""
with open(resource_path(f"data/conditions/{index_name}.json"), "r") as f:
data = json.loads(f.read())
return Condition(index=data['index'], name=data['name'], desc=data['desc'])
def request_other_actions(index_name: str) -> List[Action]:
actions: List[Action] = []
with open(resource_path(f"data/monsters/{index_name}.json"), "r") as f:
data = json.loads(f.read())
damages: List[Damage] = []
if index_name == 'rug-of-smothering':
effects: List[Condition] = []
for index in ('restrained', 'blinded'):
effect: Condition = request_condition(index)
if effect.index == 'restrained':
effect.dc_type = AbilityType.STR
effect.dc_value = 13
effects.append(effect)
damage_type: DamageType = request_damage_type(index_name='bludgeoning')
damages.append(Damage(type=damage_type, dd=DamageDice('2d6', 3)))
action_dict: dict = data['actions'][0]
action = Action(name=action_dict['name'], desc=action_dict['desc'], type=ActionType.MELEE,
attack_bonus=action_dict.get('attack_bonus'), damages=damages,
effects=effects)
actions.append(action)
return actions
def extract_recharge_on_roll_from(action) -> Optional[int]:
action_name: str = action['name']
if "usage" in action:
if action['usage'].get('type') == 'recharge on roll':
return action['usage']['min_value']
elif 'Recharge' in action_name:
pattern = r"Recharge (\d+)"
match = re.search(pattern, action_name)
if match:
action_name = action_name.split('(')[0].strip()
return int(match.group(1))
def extract_special_ability_from(action: str, recharge_on_roll: int) -> Optional[SpecialAbility]:
damages: List[Damage] = []
for damage in action['damage']:
if "damage_type" in damage:
can_attack = True
damage_type: DamageType = request_damage_type(index_name=damage['damage_type']['index'])
if '+' in damage['damage_dice']:
damage_dice, bonus = damage['damage_dice'].split('+')
bonus = int(bonus)
elif '-' in damage['damage_dice']:
damage_dice, bonus = damage['damage_dice'].split('-')
bonus = -int(bonus)
else:
damage_dice, bonus = damage['damage_dice'], 0
damages.append(Damage(type=damage_type, dd=DamageDice(damage_dice, bonus)))
area_of_effect: AreaOfEffect = AreaOfEffect(type='sphere', size=5)
if damages:
return SpecialAbility(name=action['name'],
desc=action.get('desc'),
damages=damages,
dc_type=action['dc']['dc_type']['index'],
dc_value=action['dc']['dc_value'],
dc_success=action['dc']['success_type'],
recharge_on_roll=recharge_on_roll,
area_of_effect=area_of_effect)
def request_monster(index_name: str) -> Monster:
"""
Send a request to local database for a monster's characteristic
:param index_name: name of the monster
:return: Monster object
"""
# print(index_name)
with open(resource_path(f"data/monsters/{index_name}.json"), "r") as f:
data = json.loads(f.read())
can_cast: bool = False
can_attack: bool = False
slots: List[int] = []
spells: List[Spell] = []
caster_level: int = None
dc: int = None
ability_modifier: int = 0
spell_caster: SpellCaster = None
special_abilities: List[SpecialAbility] = []
if "special_abilities" in data:
for special_ability in data['special_abilities']:
action_name: str = special_ability['name']
if special_ability['name'] == 'Spellcasting':
ability: dict = special_ability['spellcasting']
caster_level = ability['level']
dc_type = ability['ability']['index']
dc_value = ability['dc']
ability_modifier = ability['modifier']
slots = [s for s in ability['slots'].values()]
for spell_dict in ability['spells']:
spell_index_name: str = spell_dict['url'].split('/')[3]
spell = request_spell(spell_index_name)
if spell is None:
continue
spells.append(spell)
elif 'damage' in special_ability:
damages: List[Damage] = []
for damage in special_ability['damage']:
if "damage_type" in damage:
can_attack = True
damage_type: DamageType = request_damage_type(index_name=damage['damage_type']['index'])
if '+' in damage['damage_dice']:
damage_dice, bonus = damage['damage_dice'].split('+')
bonus = int(bonus)
elif '-' in damage['damage_dice']:
damage_dice, bonus = damage['damage_dice'].split('-')
bonus = -int(bonus)
else:
damage_dice, bonus = damage['damage_dice'], 0
damages.append(Damage(type=damage_type, dd=DamageDice(damage_dice, bonus)))
# TODO: parse range in
desc: str = special_ability['desc']
area_of_effect: AreaOfEffect = AreaOfEffect(type='sphere', size=15)
if 'dc' in special_ability:
dc_type = special_ability['dc']['dc_type']['index']
dc_value = special_ability['dc']['dc_value']
dc_success = special_ability['dc']['success_type']
else:
dc_type = dc_success = dc_value = None
if damages:
special_abilities.append(SpecialAbility(name=action_name,
desc=special_ability['desc'],
damages=damages,
dc_type=dc_type,
dc_value=dc_value,
dc_success=dc_success,
recharge_on_roll=None,
area_of_effect=area_of_effect))
if spells:
can_attack = True
spell_caster: SpellCaster = SpellCaster(level=caster_level,
spell_slots=slots,
learned_spells=spells,
dc_type=dc_type,
dc_value=dc_value + ability_modifier,
ability_modifier=ability_modifier)
actions: List[Action] = []
if "actions" in data:
# Melee attacks
for action in data['actions']:
# print(f"{data['name']} - action = {action}")
if action['name'] != 'Multiattack':
if "damage" in action:
normal_range = long_range = 5
is_melee_attack = re.search("Melee.*Attack", action['desc'])
is_ranged_attack = re.search("Ranged.*Attack", action['desc'])
if is_ranged_attack:
# desc = "Ranged Weapon Attack: +7 to hit, range 150/600 ft., one target. Hit: 7 (1d8 + 3) piercing damage plus 13 (3d8) poison damage, and the target must succeed on a DC 14 Constitution saving throw or be poisoned. The poison lasts until it is removed by the lesser restoration spell or similar magic."
range_pattern = r"range\s+(\d+)/(\d+)\s*ft\."
match = re.search(range_pattern, action['desc'])
if match:
normal_range = int(match.group(1))
long_range = int(match.group(2))
else:
normal_range = 5
long_range = None
damages: List[Damage] = []
for damage in action['damage']:
# print(f'damage = {damage}')
if "choose" in damage and damage['type'] == 'damage':
actions_count = int(damage['choose'])
damages_list = damage['from']
for damage_dict in damages_list:
damage_type: DamageType = request_damage_type(index_name=damage_dict['damage_type']['index'])
damage_choice = Damage(type=damage_type, dd=DamageDice(damage_dict['damage_dice']))
action_type = ActionType.MIXED if is_melee_attack and is_ranged_attack else ActionType.MELEE if is_melee_attack else ActionType.RANGED
actions.append(Action(name=action['name'], desc=action['desc'], type=action_type, normal_range=normal_range, long_range=long_range,
attack_bonus=action.get('attack_bonus'), multi_attack=None, damages=[damage_choice]))
elif "damage_type" in damage:
damage_type: DamageType = request_damage_type(index_name=damage['damage_type']['index'])
damages.append(Damage(type=damage_type, dd=DamageDice(damage['damage_dice'])))
if damages:
can_attack = True
action_type = ActionType.MIXED if is_melee_attack and is_ranged_attack else ActionType.MELEE if is_melee_attack else ActionType.RANGED
actions.append(Action(name=action['name'], desc=action['desc'], type=action_type, normal_range=normal_range, long_range=long_range,
attack_bonus=action.get('attack_bonus'), multi_attack=None, damages=damages))
# Multiattacks
for action in data['actions']:
if action['name'] == 'Multiattack':
can_attack = True
multi_attack: List[Action] = []
# Todo: verify if there could be more than one choice...
VALID_TYPES = {ActionType.MELEE, ActionType.RANGED}
choose_count: int = action['options']['choose']
for action_dict in action['options']['from'][0]:
try:
count = int(action_dict['count'])
action_match = next((a for a in actions if a.name == action_dict['name']), None)
if action_match and action_match.type in VALID_TYPES:
multi_attack.extend([action_match] * count)
except (ValueError, KeyError):
print(f"invalid count option for {index_name} : {action_dict['name']}")
# action_type: str = ActionType.MELEE if 'Melee' in action['desc'] else ActionType.RANGED if 'Ranged' in action['desc']
actions.append(Action(name=action['name'], desc=action['desc'], type=ActionType.MELEE, attack_bonus=None, multi_attack=multi_attack, damages=None))
# Special abilities
for action in data['actions']:
if 'dc' in action:
recharge_on_roll: Optional[int] = extract_recharge_on_roll_from(action)
if 'damage' in action:
sa: Optional[SpecialAbility] = extract_special_ability_from(action, recharge_on_roll)
if sa: special_abilities.append(sa)
elif 'attack_options' in action:
recharge_on_roll: Optional[int] = extract_recharge_on_roll_from(action)
options: str = action['attack_options']
if options.get('type') == 'attack':
choose_count: int = int(options.get('choose'))
if choose_count == 1:
for a in options.get('from'):
if 'dc' in a:
if 'damage' in a:
sa: Optional[SpecialAbility] = extract_special_ability_from(a, recharge_on_roll)
if sa: special_abilities.append(sa)
else:
# TODO: check if there are multi-attack defined in some monster json
continue
# TODO Check if there are other special abilities that we did not cover...
if not actions:
actions = request_other_actions(index_name)
proficiencies: List[Proficiency] = []
if 'proficiencies' in data:
for prof in data['proficiencies']:
proficiency: Proficiency = request_proficiency(index_name=prof['proficiency']['index'])
proficiency.value = prof.get('value')
proficiencies.append(proficiency)
# print(index_name)
speed: str = data['speed']['fly'] if 'fly' in data['speed'] else data['speed']['walk'] if 'walk' in data['speed'] else '30'
return Monster(id=-1,
image_name=f'monster_{index_name}.png',
x=-1, y=-1, old_x=-1, old_y=-1,
index=index_name,
name=data['name'],
abilities=Abilities(str=data['strength'], dex=data['dexterity'], con=data['constitution'],
int=data['intelligence'], wis=data['wisdom'], cha=data['charisma']),
proficiencies=proficiencies,
armor_class=data['armor_class'],
hit_points=data['hit_points'],
hit_dice=data['hit_dice'],
xp=data['xp'],
speed=int(speed.split()[0]),
challenge_rating=data['challenge_rating'],
actions=actions,
sc=spell_caster,
sa=special_abilities) # if can_attack else None
def get_special_monster_actions(name: str) -> tuple[List[Action], List[SpecialAbility], SpellCaster]:
actions: List[Action] = []
special_abilities: List[SpecialAbility] = []
spell_caster: SpellCaster = None
# print(name)
if name == "Orc Eye of Gruumsh":
damage_type: DamageType = request_damage_type(index_name='piercing')
# Ranged attack
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d6', bonus=3)),
Damage(type=damage_type, dd=DamageDice(dice='1d8'))]
action = Action(name='Spear', desc='', type=ActionType.RANGED, attack_bonus=5, damages=damages, normal_range=20, long_range=60)
actions.append(action)
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='2d8', bonus=3))]
action = Action(name='Spear', desc='', type=ActionType.MELEE, attack_bonus=5, damages=damages)
actions.append(action)
# Spellcasting
caster_level = 3
dc_type = 'wis'
dc_value = 11
spells = ['guidance', 'resistance', 'thaumaturgy']
spells += ['bless', 'command']
spells += ['augury', 'spiritual-weapon']
if spells:
spell_caster: SpellCaster = SpellCaster(level=caster_level,
spell_slots=[4, 2, 0, 0, 0],
learned_spells=list(filter(None, [request_spell(s) for s in spells])),
dc_type=dc_type,
dc_value=dc_value + 1,
ability_modifier=1)
elif name == "Ogre Bolt Launcher":
damage_type: DamageType = request_damage_type(index_name='bludgeoning')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='2d4', bonus=4))]
action = Action(name='Fist', desc='', type=ActionType.MELEE, attack_bonus=6, damages=damages)
actions.append(action)
# Ranged attack
damage_type: DamageType = request_damage_type(index_name='piercing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='3d10', bonus=1))]
action = Action(name='Bolt Launcher', desc='', type=ActionType.RANGED, attack_bonus=3, damages=damages, normal_range=120, long_range=480)
actions.append(action)
elif name == "Ogre Battering Ram":
damage_type: DamageType = request_damage_type(index_name='bludgeoning')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='2d10', bonus=4))]
action = Action(name='Bash', desc='', type=ActionType.MELEE, attack_bonus=6, damages=damages)
actions.append(action)
elif name == "Hobgoblin Captain":
# Multi Attack
damage_type: DamageType = request_damage_type(index_name='slashing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='2d6', bonus=2))]
multi_attack_action = Action(name='Greatsword', desc='', type=ActionType.MELEE, attack_bonus=4, damages=damages)
action = Action(name='Multiattack', desc='', type=ActionType.MELEE, multi_attack=[multi_attack_action] * 2)
actions.append(action)
# Single Attacks
damage_type: DamageType = request_damage_type(index_name='piercing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d6', bonus=2))]
action = Action(name='Javelin', desc='', type=ActionType.MELEE, attack_bonus=4, damages=damages)
actions.append(action)
# Ranged attack
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d6', bonus=2))]
action = Action(name='Javelin', desc='', type=ActionType.MIXED, attack_bonus=4, damages=damages, normal_range=30, long_range=120)
actions.append(action)
elif name == 'Piercer':
damage_type: DamageType = request_damage_type(index_name='piercing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice=f'{randint(1, 6)}d6', bonus=0))]
action = Action(name='Drop', desc='', type=ActionType.MELEE, attack_bonus=3, damages=damages)
actions.append(action)
elif name == "Illusionist":
# Multiple attack
damage_type: DamageType = request_damage_type(index_name='psychic')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='2d10', bonus=3))]
multi_attack_action = Action(name='Arcane Burst', desc='', type=ActionType.MIXED, attack_bonus=5, damages=damages, normal_range=120)
action = Action(name='Multiattack', desc='', type=ActionType.MIXED, multi_attack=[multi_attack_action] * 2)
actions.append(action)
# Ranged attack
# range = '120 ft'
# action = Action(name='Multiattack', desc='', type=ActionType.RANGED,
# attack_bonus=None,
# multi_attack=[multi_attack_action] * 2, damages=None)
# actions.append(action)
# Spellcasting
caster_level = 2
dc_type = 'int'
dc_value = 13
spells = ['dancing-lights', 'mage-hand', 'minor-illusion']
spells += ['disguise-self', 'invisibility', 'mage-armor', 'major-image', 'phantasmal-force', 'phantom-steed']
if spells:
spell_caster: SpellCaster = SpellCaster(level=caster_level,
spell_slots=[0, 1, 0, 0, 0],
learned_spells=list(filter(None, [request_spell(s) for s in spells])),
dc_type=dc_type,
dc_value=dc_value + 5,
ability_modifier=5)
elif name == "Goblin Boss":
# Multi Attack
# The goblin makes two attacks with its scimitar. The second attack has disadvantage.
damage_type: DamageType = request_damage_type(index_name='slashing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d6', bonus=2))]
multi_attack_action_1 = Action(name='Scimitar', desc='', type=ActionType.MELEE, attack_bonus=4, damages=damages)
multi_attack_action_2 = Action(name='Scimitar', desc='', type=ActionType.MELEE, attack_bonus=4, damages=damages, disadvantage=True)
action = Action(name='Multiattack', desc='', type=ActionType.MELEE, multi_attack=[multi_attack_action_1, multi_attack_action_2])
actions.append(action)
# Single Attacks
damage_type: DamageType = request_damage_type(index_name='piercing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d6', bonus=2))]
action = Action(name='Javelin', desc='', type=ActionType.MELEE, attack_bonus=4, damages=damages)
actions.append(action)
# Ranged attack
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d6', bonus=2))]
action = Action(name='Javelin', desc='', type=ActionType.MIXED, attack_bonus=2, damages=damages, normal_range=30, long_range=120)
actions.append(action)
elif name == "Xvart":
# Single Attacks
damage_type: DamageType = request_damage_type(index_name='piercing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d6', bonus=2))]
action = Action(name='Shortsword', desc='', type=ActionType.MELEE, attack_bonus=4, damages=damages)
actions.append(action)
# Ranged attack
damage_type: DamageType = request_damage_type(index_name='bludgeoning')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d4', bonus=2))]
action = Action(name='Sling', desc='', type=ActionType.RANGED, attack_bonus=4, damages=damages, normal_range=30, long_range=120)
actions.append(action)
elif name == "Kobold Inventor":
# Melee/Ranged Attacks
damage_type: DamageType = request_damage_type(index_name='piercing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d4', bonus=2))]
action = Action(name='Dagger', desc='', type=ActionType.MIXED, attack_bonus=4, damages=damages, normal_range=20, long_range=60)
actions.append(action)
# Ranged attack
damage_type: DamageType = request_damage_type(index_name='bludgeoning')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d4', bonus=2))]
action = Action(name='Sling', desc='', type=ActionType.RANGED,
attack_bonus=4, damages=damages, normal_range=30, long_range=120)
actions.append(action)
# TODO "Weapon Invention"
# "The kobold uses one of the following options (choose one or roll a {@dice d8}); the kobold can use each one no more than once per day:"
elif name == "Half-ogre":
# Single Attacks
damage_type: DamageType = request_damage_type(index_name='slashing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='2d8', bonus=3))]
action = Action(name='Battleaxe', desc='', type=ActionType.MELEE, attack_bonus=5, damages=damages)
actions.append(action)
# Ranged attack
damage_type: DamageType = request_damage_type(index_name='piercing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='2d6', bonus=3))]
action = Action(name='Javelin', desc='', type=ActionType.MIXED, attack_bonus=5, damages=damages, normal_range=30, long_range=120)
actions.append(action)
elif name == "Water Weird":
# TODO: implement condition restrain
# Single Attacks
# "If the target is Medium or smaller, it is {@condition grappled} (escape {@dc 13}) and pulled 5 feet toward the water weird. Until this grapple ends, the target is {@condition restrained}, the water weird tries to drown it, and the water weird can't constrict another target."
damage_type: DamageType = request_damage_type(index_name='bludgeoning')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='3d6', bonus=3))]
action = Action(name='Constrict', desc='', type=ActionType.MIXED, attack_bonus=5, damages=damages, normal_range=10)
actions.append(action)
elif name == "Apprentice Wizard":
# Multiple attack
damage_type: DamageType = request_damage_type(index_name='psychic')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='2d10', bonus=3))]
action = Action(name='Arcane Burst', desc='', type=ActionType.MIXED, attack_bonus=4, damages=damages, normal_range=120)
actions.append(action)
# Spellcasting
caster_level = 1
dc_type = 'int'
dc_value = 12
spells = ['mage-hand', 'prestidigitation']
spells += ['burning hands', 'disguise-self', 'mage-armor']
if spells:
spell_caster: SpellCaster = SpellCaster(level=caster_level,
spell_slots=[1, 0, 0, 0, 0],
learned_spells=list(filter(None, [request_spell(s) for s in spells])),
dc_type=dc_type,
dc_value=dc_value + 4,
ability_modifier=5)
elif name == "Orc War Chief":
# Multiple attack
# "The orc makes two attacks with its greataxe or its spear."
# Great Axe * 2
damage_type: DamageType = request_damage_type(index_name='slashing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d12', bonus=4)),
Damage(type=damage_type, dd=DamageDice(dice='1d8'))]
multi_attack_action = Action(name='Greataxe', desc='', type=ActionType.MELEE, attack_bonus=6, damages=damages)
action = Action(name='Multiattack', desc='', type=ActionType.MELEE, multi_attack=[multi_attack_action] * 2)
actions.append(action)
# Spear * 2
damage_type: DamageType = request_damage_type(index_name='piercing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d6', bonus=4))]
multi_attack_action = Action(name='Spear', desc='', type=ActionType.MIXED, attack_bonus=6, damages=damages, normal_range=20, long_range=60)
action = Action(name='Multiattack', desc='', type=ActionType.MIXED, multi_attack=[multi_attack_action] * 2)
actions.append(action)
elif name == "Deathlock":
# TODO implement spell casting
# Multiple attack
# "The deathlock makes two Deathly Claw or Grave Bolt attacks."
# Deathly Claw * 2
damage_type: DamageType = request_damage_type(index_name='necrotic')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='2d6', bonus=2))]
multi_attack_action = Action(name='Deathly Claw', desc='', type=ActionType.MELEE, attack_bonus=4, damages=damages)
action = Action(name='Multiattack', desc='', type=ActionType.MELEE, multi_attack=[multi_attack_action] * 2)
actions.append(action)
# Grave Bolt * 2
damage_type: DamageType = request_damage_type(index_name='necrotic')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='2d10', bonus=3))]
multi_attack_action = Action(name='Grave Bolt', desc='', type=ActionType.RANGED, attack_bonus=5, damages=damages, normal_range=120)
action = Action(name='Multiattack', desc='', type=ActionType.RANGED, multi_attack=[multi_attack_action] * 2)
actions.append(action)
# Spellcasting
caster_level = 1
dc_type = 'cha'
dc_value = 11
spells = ['detect-magic', 'disguise-self', 'mage-armor', 'mage-hand']
spells += ['dispel-magic', 'hunger-of-Hadar', 'invisibility', 'spider-climb']
if spells:
spell_caster: SpellCaster = SpellCaster(level=caster_level,
spell_slots=[1, 0, 0, 0, 0],
learned_spells=list(filter(None, [request_spell(s) for s in spells])),
dc_type=dc_type,
dc_value=dc_value + 4,
ability_modifier=4)
elif name == "Allip":
# Single attacks
damage_type: DamageType = request_damage_type(index_name='psychic')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='4d6', bonus=3))]
action = Action(name='Maddening Touch', desc='', type=ActionType.MELEE, attack_bonus=6, damages=damages)
actions.append(action)
# Special attacks
# N.B.: "Whispers of Compulsion" effective only with a party
damage_type: DamageType = request_damage_type(index_name='psychic')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='2d8', bonus=3))]
area_of_effect: AreaOfEffect = AreaOfEffect(type='sphere', size=30)
# TODO: implement condition stunned
sa: SpecialAbility = SpecialAbility(name='Howling Babble',
desc='',
damages=damages,
dc_type='wis',
dc_value=14,
dc_success='half',
recharge_on_roll=1,
area_of_effect=area_of_effect)
special_abilities.append(sa)
elif name == 'Orog':
# Melee/Ranged attacks
damage_type: DamageType = request_damage_type(index_name='psychic')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d6', bonus=4))]
action = Action(name='Javelin', desc='', type=ActionType.MIXED, attack_bonus=6, damages=damages, normal_range=30, long_range=120)
actions.append(action)
# Multiple attack
# "The orog makes two greataxe attacks."
damage_type: DamageType = request_damage_type(index_name='slashing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d12', bonus=4))]
multi_attack_action = Action(name='Greataxe', desc='', type=ActionType.MELEE, attack_bonus=6, damages=damages)
action = Action(name='Multiattack', desc='', type=ActionType.MELEE, multi_attack=[multi_attack_action] * 2)
actions.append(action)
elif name == 'Warlock of the Great Old One':
# Multiple attack
# "The warlock makes two Dagger attacks."
# Melee/Ranged Attacks
damage_type: DamageType = request_damage_type(index_name='piercing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d4', bonus=2))]
multi_attack_action = Action(name='Dagger', desc='', type=ActionType.MIXED, attack_bonus=4, damages=damages, normal_range=20, long_range=60)
action = Action(name='Multiattack', desc='', type=ActionType.MIXED, multi_attack=[multi_attack_action] * 2)
actions.append(action)
# Special attacks
damage_type: DamageType = request_damage_type(index_name='psychic')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='2d8', bonus=0))]
# TODO: implement condition frightened and area of effect
# "The warlock opens a momentary extraplanar rift within 60 feet of it.
# The rift is a scream-filled, 20-foot cube. Each creature in that area must make a {@dc 15} Wisdom saving throw.
# On a failed save, a creature takes 9 ({@damage 2d8}) psychic damage and is {@condition frightened} of the warlock until the start of the warlock's next turn.
# On a successful save, a creature takes half as much damage and isn't {@condition frightened}."
area_of_effect: AreaOfEffect = AreaOfEffect(type='cube', size=20)
sa: SpecialAbility = SpecialAbility(name='Howling Void',
desc='',
damages=damages,
dc_type='wis',
dc_value=15,
dc_success='half',
recharge_on_roll=1,
area_of_effect=area_of_effect)
special_abilities.append(sa)
# Spellcasting
caster_level = 1
dc_type = 'cha'
dc_value = 15
spells = ['detect-magic', 'guidance', 'levitate', 'mage-armor', 'mage-hand', 'minor-illusion', 'prestidigitation']
spells += ['arcane-gate', 'detect-thoughts', 'true-seeing']
if spells:
spell_caster: SpellCaster = SpellCaster(level=caster_level,
spell_slots=[1, 0, 0, 0, 0],
learned_spells=list(filter(None, [request_spell(s) for s in spells])),
dc_type=dc_type,
dc_value=dc_value + 4,
ability_modifier=4)
elif name == "Star Spawn Grue":
# Single Attacks
# TODO ???
# The target must succeed on a {@dc 10} Wisdom saving throw or attack rolls against it have advantage until the start of the grue's next turn."
damage_type: DamageType = request_damage_type(index_name='piercing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='2d4', bonus=1))]
action = Action(name='Confounding Bite', desc='', type=ActionType.MELEE, attack_bonus=3, damages=damages)
actions.append(action)
elif name == "Star Spawn Mangler":
# Multiple attack
# "The mangler makes two Claw attacks."
damage_type: DamageType = request_damage_type(index_name='slashing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d8', bonus=4))]
damage_type: DamageType = request_damage_type(index_name='psychic')
damages += [Damage(type=damage_type, dd=DamageDice(dice='2d6'))]
multi_attack_action = Action(name='Claw', desc='', type=ActionType.MELEE, attack_bonus=7, damages=damages)
action = Action(name='Multiattack', desc='', type=ActionType.MELEE, multi_attack=[multi_attack_action] * 2)
actions.append(action)
# Special attacks
damage_type: DamageType = request_damage_type(index_name='psychic')
# TODO implements multiple attacks in sa (and not multiple damages)
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='2d8'))] * 6
area_of_effect: AreaOfEffect = AreaOfEffect(type='cube', size=5)
sa: SpecialAbility = SpecialAbility(name='Flurry of Claws',
desc='',
damages=damages,
dc_type='wis',
dc_value=15,
dc_success='half',
recharge_on_roll=5,
area_of_effect=area_of_effect)
special_abilities.append(sa)
elif name == "Adult Oblex":
# Multiple attack
# "The oblex makes two pseudopod attacks, and it uses Eat Memories."
damage_type: DamageType = request_damage_type(index_name='bludgeoning')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='2d6', bonus=4))]
damage_type: DamageType = request_damage_type(index_name='psychic')
damages += [Damage(type=damage_type, dd=DamageDice(dice='2d6'))]
multi_attack_action = Action(name='Pseudopod', desc='', type=ActionType.MELEE, attack_bonus=7, damages=damages)
action = Action(name='Multiattack', desc='', type=ActionType.MELEE, multi_attack=[multi_attack_action] * 2)
actions.append(action)
# Special attacks
damage_type: DamageType = request_damage_type(index_name='psychic')
# TODO implements memory drained effect
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='4d8', bonus=0))]
area_of_effect: AreaOfEffect = AreaOfEffect(type='cube', size=5)
sa: SpecialAbility = SpecialAbility(name='Eat Memories',
desc='',
damages=damages,
dc_type='wis',
dc_value=15,
dc_success='half',
recharge_on_roll=1,
area_of_effect=area_of_effect)
special_abilities.append(sa)
# Spellcasting
caster_level = 12
dc_type = 'int'
dc_value = 15
spells = ['identify', 'ray-of-sickness']
spells += ['hold-person', 'locate-object']
spells += ['bestow-curse', 'counterspell', 'lightning-bolt']
spells += ['phantasmal-killer', 'polymorph']
spells += ['contact-other-plane', 'scrying']
spells += ['eyebite']
if spells:
spell_caster: SpellCaster = SpellCaster(level=caster_level,
spell_slots=[4, 3, 3, 3, 2, 1],
learned_spells=list(filter(None, [request_spell(s) for s in spells])),
dc_type=dc_type,
dc_value=dc_value + 7,
ability_modifier=7)
elif name == 'Vampiric Mist':
# Special attacks
damage_type: DamageType = request_damage_type(index_name='necrotic')
# TODO implements life drain effect
# "The mist touches one creature in its space.
# The target must succeed on a {@dc 13} Constitution saving throw (Undead and Constructs automatically succeed),
# or it takes 10 ({@damage 2d6 + 3}) necrotic damage, the mist regains 10 hit points,
# and the target's hit point maximum is reduced by an amount equal to the necrotic damage taken.
# This reduction lasts until the target finishes a long rest.
# The target dies if its hit point maximum is reduced to 0."
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='2d6', bonus=3))]
area_of_effect: AreaOfEffect = AreaOfEffect(type='cube', size=5)
sa: SpecialAbility = SpecialAbility(name='Life Drain',
desc='',
damages=damages,
dc_type='wis',
dc_value=13,
dc_success='half',
recharge_on_roll=1,
area_of_effect=area_of_effect)
special_abilities.append(sa)
elif name == 'Spawn of Kyuss':
# Multiple attack
# "The spawn of Kyuss makes two Claw attacks, and it uses Burrowing Worm."
damage_type: DamageType = request_damage_type(index_name='slashing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d6', bonus=3))]
damage_type: DamageType = request_damage_type(index_name='necrotic')
damages += [Damage(type=damage_type, dd=DamageDice(dice='2d6', bonus=0))]
multi_attack_action = Action(name='Claw', desc='', type=ActionType.MELEE, attack_bonus=6, damages=damages)
# Special attacks
damage_type: DamageType = request_damage_type(index_name='necrotic')
# TODO implements Burrowing Worm effect (curse effect)
# "A worm launches from the spawn of Kyuss at one Humanoid that the spawn can see within 10 feet of it.
# The worm latches onto the target's skin unless the target succeeds on a {@dc 11} Dexterity saving throw.
# The worm is a Tiny Undead with AC 6, 1 hit point, a 2 (-4) in every ability score, and a speed of 1 foot.
# While on the target's skin, the worm can be killed by normal means or scraped off using an action (the spawn can use Burrowing Worm to launch a scraped-off worm at a Humanoid it can see within 10 feet of the worm).
# Otherwise, the worm burrows under the target's skin at the end of the target's next turn, dealing 1 piercing damage to it.
# At the end of each of its turns thereafter, the target takes 7 ({@damage 2d6}) necrotic damage per worm infesting it (maximum of {@damage 10d6}), and if it drops to 0 hit points, it dies and then rises 10 minutes later as a spawn of Kyuss.
# If a worm-infested target is targeted by an effect that cures disease or removes a curse, all the worms infesting it wither away."
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='2d6', bonus=3))]
area_of_effect = AreaOfEffect(type='cube', size=10)
sa: SpecialAbility = SpecialAbility(name='Burrowing Worm',
desc='',
damages=damages,
dc_type='dex',
dc_value=11,
dc_success='half',
area_of_effect=area_of_effect,
recharge_on_roll=1)
action = Action(name='Multiattack', desc='', type=ActionType.MELEE, multi_attack=[multi_attack_action, multi_attack_action, sa])
actions.append(action)
# special_abilities.append(sa)
elif name == "Hobgoblin Warlord":
# Multiple attack
# "The hobgoblin makes three melee attacks. Alternatively, it can make two ranged attacks with its javelins."
damage_type: DamageType = request_damage_type(index_name='slashing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d10', bonus=3))]
multi_attack_action = Action(name='Longsword', desc='', type=ActionType.MELEE, attack_bonus=9, damages=damages)
action = Action(name='Multiattack', desc='', type=ActionType.MELEE, multi_attack=[multi_attack_action] * 3)
actions.append(action)
damage_type: DamageType = request_damage_type(index_name='slashing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d6', bonus=3))]
multi_attack_action = Action(name='Javelin', desc='', type=ActionType.MIXED, attack_bonus=9, damages=damages, normal_range=30, long_range=120)
action = Action(name='Multiattack', desc='', type=ActionType.MIXED, multi_attack=[multi_attack_action] * 2)
actions.append(action)
# TODO
# "Shield Bash" Melee attack & knock
# "Leadership (Recharges after a Short or Long Rest)"
elif name == "Duergar Mind Master":
# Multiple attack
# "The duergar makes two Mind-Poison Dagger attacks. It can replace one attack with a use of Mind Mastery."
damage_type: DamageType = request_damage_type(index_name='piercing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d4', bonus=3))]
damage_type: DamageType = request_damage_type(index_name='psychic')
damages += [Damage(type=damage_type, dd=DamageDice(dice='2d6'))]
multi_attack_action = Action(name='Mind-Poison Dagger', desc='', type=ActionType.MELEE, attack_bonus=5, damages=damages)
action = Action(name='Multiattack', desc='', type=ActionType.MELEE, multi_attack=[multi_attack_action] * 3)
actions.append(action)
# TODO Mind Mastery (Condition Charmed)
# TODO Invisibility {@recharge 4} (Concentration)
elif name == "Duergar Screamer":
# Multiple attack
# "The screamer makes one Drill attack, and it uses Sonic Scream."
damage_type: DamageType = request_damage_type(index_name='piercing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d4', bonus=3))]
multi_attack_action_1: Action = Action(name='Drill', desc='', type=ActionType.MELEE, attack_bonus=6, damages=damages)
damage_type: DamageType = request_damage_type(index_name='thunder')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='2d6'))]
area_of_effect = AreaOfEffect(type='cube', size=15)
multi_attack_action_2: SpecialAbility = SpecialAbility(name='Burrowing Worm',
desc='',
damages=damages,
dc_type='str',
dc_value=11,
dc_success='half',
recharge_on_roll=1,
area_of_effect=area_of_effect,
effects=[Condition('prone', 'prone', '')])
action = Action(name='Multiattack', desc='', type=ActionType.MIXED, multi_attack=[multi_attack_action_1, multi_attack_action_2])
actions.append(action)
# Reactions (to attack)
reactions: dict[str, Action] = {"Engine of Pain": multi_attack_action_1}
elif name == "Duergar Kavalrachni":
# "The duergar makes two War Pick attacks."
# TODO: implement poison effect
damage_type: DamageType = request_damage_type(index_name='piercing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d8', bonus=2))]
damage_type: DamageType = request_damage_type(index_name='poison')
damages += [Damage(type=damage_type, dd=DamageDice(dice='2d4'))]
multi_attack_action = Action(name='War Pick', desc='', type=ActionType.MELEE, attack_bonus=4, damages=damages)
action = Action(name='Multiattack', desc='', type=ActionType.MELEE, multi_attack=[multi_attack_action] * 2)
actions.append(action)
damage_type: DamageType = request_damage_type(index_name='piercing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d10'))]
action = Action(name='Heavy Crossbow', desc='', type=ActionType.RANGED, attack_bonus=6, damages=damages, normal_range=100, long_range=400)
actions.append(action)
elif name == "Female Steeder":
damage_type: DamageType = request_damage_type(index_name='piercing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d8', bonus=3))]
damage_type: DamageType = request_damage_type(index_name='poison')
damages += [Damage(type=damage_type, dd=DamageDice(dice='2d8'))]
action = Action(name='Bite', desc='', type=ActionType.MELEE, attack_bonus=6, damages=damages)
actions.append(action)
# TODO Implement "Sticky Leg"
elif name == "Succubus":
damage_type: DamageType = request_damage_type(index_name='slashing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d6', bonus=3))]
action = Action(name='Claw (Fiend Form Only)', desc='', type=ActionType.MELEE, attack_bonus=5, damages=damages)
actions.append(action)
# TODO Implement "Charm", "Draining Kiss"
elif name == "Incubus":
damage_type: DamageType = request_damage_type(index_name='slashing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d6', bonus=3))]
action = Action(name='Claw (Fiend Form Only)', desc='', type=ActionType.MELEE, attack_bonus=5, damages=damages)
actions.append(action)
# TODO Implement "Charm", "Draining Kiss"
elif name == "Sea Hag":
damage_type: DamageType = request_damage_type(index_name='slashing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='2d6', bonus=3))]
action = Action(name='Claws', desc='', type=ActionType.MELEE, attack_bonus=5, damages=damages)
actions.append(action)
# TODO Death glare
# "The hag targets one {@condition frightened} creature she can see within 30 feet of her.
# If the target can see the hag, it must succeed on a {@dc 11} Wisdom saving throw against this magic or drop to 0 hit points."
# area_of_effect = AreaOfEffect(type='cube', size=30)
# action: SpecialAbility = SpecialAbility(name='Death glare',
# desc='',
# damages=[],
# dc_type='wis',
# dc_value=11,
# dc_success='none',
# recharge_on_roll=1,
# area_of_effect=area_of_effect,
# effects=[Condition('frightened')])
# actions.append(action)
elif name == "Kuo-toa Archpriest":
# Spellcasting
# "The kuo-toa is a 10th-level spellcaster. Its spellcasting ability is Wisdom (spell save {@dc 14}, {@hit 6} to hit with spell attacks). The kuo-toa has the following cleric spells prepared:"
# Multi-attack
damage_type: DamageType = request_damage_type(index_name='bludgeoning')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d6', bonus=3))]
damage_type = request_damage_type(index_name='lightning')
damages += [Damage(type=damage_type, dd=DamageDice(dice='4d6'))]
multi_attack_action_1 = Action(name='Scepter', desc='', type=ActionType.MELEE, attack_bonus=6, damages=damages)
damage_type: DamageType = request_damage_type(index_name='bludgeoning')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d4', bonus=3))]
multi_attack_action_2 = Action(name='Unarmed Strike', desc='', type=ActionType.MELEE, attack_bonus=6, damages=damages)
action = Action(name='Multiattack', desc='', type=ActionType.MELEE, multi_attack=[multi_attack_action_1, multi_attack_action_2])
actions.append(action)
# Spellcasting
caster_level = 10
dc_type = 'wis'
dc_value = 14
spells = ['guidance', 'sacred-flame', 'thaumaturgy']
spells += ['detect-magic', 'sanctuary', 'shield-of-faith']
spells += ['hold-person', 'spiritual-weapon']
spells += ['spirit-guardians', 'tongues']
spells += ['control-water', 'divination']
spells += ['mass-cure-wounds', 'scrying']
spells += ['phantasmal-killer', 'polymorph']
spells += ['contact-other-plane', 'scrying']
spells += ['eyebite']
if spells:
spell_caster: SpellCaster = SpellCaster(level=caster_level,
spell_slots=[4, 3, 3, 3, 2],
learned_spells=list(filter(None, [request_spell(s) for s in spells])),
dc_type=dc_type,
dc_value=dc_value + 7,
ability_modifier=7)
elif name == "Kuo-toa":
damage_type: DamageType = request_damage_type(index_name='piercing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d4', bonus=1))]
action = Action(name='Bite', desc='', type=ActionType.MELEE, attack_bonus=3, damages=damages)
actions.append(action)
damage_type: DamageType = request_damage_type(index_name='piercing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d6', bonus=1))]
action = Action(name='Spear', desc='', type=ActionType.MIXED, attack_bonus=3, damages=damages, normal_range=20, long_range=60)
actions.append(action)
damage_type: DamageType = request_damage_type(index_name='piercing')
damages: List[Damage] = [Damage(type=damage_type, dd=DamageDice(dice='1d8', bonus=1))]
action = Action(name='Spear', desc='', type=ActionType.MIXED, attack_bonus=3, damages=damages, normal_range=20, long_range=60)
actions.append(action)
# TODO "Net" (condition restrained)
# "{@atk rw} {@hit 3} to hit, range 5/15 ft., one Large or smaller creature.
# {@h}The target is {@condition restrained}.
# A creature can use its action to make a {@dc 10} Strength check to free itself or another creature in a net, ending the effect on a success.
# Dealing 5 slashing damage to the net (AC 10) frees the target without harming it and destroys the net."
# TODO Reaction "Sticky Shield"
# "When a creature misses the kuo-toa with a melee weapon attack, the kuo-toa uses its sticky shield to catch the weapon.
# The attacker must succeed on a {@dc 11} Strength saving throw, or the weapon becomes stuck to the kuo-toa's shield.
# If the weapon's wielder can't or won't let go of the weapon, the wielder is {@condition grappled} while the weapon is stuck. While stuck, the weapon can't be used.
# A creature can pull the weapon free by taking an action to make a {@dc 11} Strength check and succeeding."
elif name == "Kuo-toa Whip":
# Spellcasting