/
main.py
1385 lines (1128 loc) · 45.2 KB
/
main.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
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
# this file has too much stuff in it. need to learn to import.
import libtcodpy as libtcod
#used sqrt at some points
import math
#used textwrap for messages
import textwrap
#used this to save/load
import shelve
SCREEN_WIDTH = 80
SCREEN_HEIGHT = 50
#GUI element constants
BAR_WIDTH = 20
PANEL_HEIGHT = 7
PANEL_Y = SCREEN_HEIGHT - PANEL_HEIGHT
MSG_X = BAR_WIDTH + 2
MSG_WIDTH = SCREEN_WIDTH - BAR_WIDTH - 2
MSG_HEIGHT = PANEL_HEIGHT - 1
INVENTORY_WIDTH = 50
LEVEL_SCREEN_WIDTH = 40
CHARACTER_SCREEN_WIDTH = 30
#amount of healing player gets from a healing potion
HEAL_AMOUNT = 40
#stats of spell-scroll effects, otherwise self-explanatory
LIGHTNING_DAMAGE = 40
LIGHTNING_RANGE = 5
CONFUSE_NUM_TURNS = 10
CONFUSE_RANGE = 8
FIREBALL_RADIUS = 3
FIREBALL_DAMAGE = 25
#xp required to reach level 2
LEVEL_UP_BASE = 200
#additional xp required to level up for each previous level gained
LEVEL_UP_FACTOR = 150
#Maximum pth length a monster will tolerate when using A* to chase the player
#Higher values may lead to extreme detours or monsters forgetting where they're going.
#Conversely, lower values may make it hard for distant monsters to chase the player.
#Really low values will result in monsters ignoring obvious flanking routes.
MAX_ASTAR_PATH_LENGTH = 20
TURNS_MONSTERS_CHASE_PLAYER_AFTER_LOSING_CONTACT = 5
SHOUT_RADIUS = 7
MAP_WIDTH = 80
MAP_HEIGHT = 43
ROOM_MAX_SIZE = 10
ROOM_MIN_SIZE = 6
MAX_ROOMS = 30
FOV_ALGO = libtcod.FOV_PERMISSIVE_8
FOV_LIGHT_WALLS = True
TORCH_RADIUS = 10
color_dark_wall = libtcod.Color(0, 0, 100)
color_light_wall = libtcod.Color(130, 110, 50)
color_dark_ground = libtcod.Color(50, 50, 150)
color_light_ground = libtcod.Color(200, 180, 50)
LIMIT_FPS = 20
######################
# message(): log an in-game message, deleting ones that overflow GUI
######################
def message(new_msg, color = libtcod.white):
new_msg_lines = textwrap.wrap(new_msg, MSG_WIDTH)
for line in new_msg_lines:
if len(game_msgs) == MSG_HEIGHT:
del game_msgs[0]
game_msgs.append((line, color))
######################
# Rect: a rectangular region of the map
######################
class Rect:
def __init__(self, x, y, w, h):
self.x1 = x
self.y1 = y
self.x2 = x + w
self.y2 = y + h
#get center coordinates
def center(self):
center_x = (self.x1 + self.x2) / 2
center_y = (self.y1 + self.y2) / 2
return (center_x, center_y)
#check if Rect intersects another Rect
def intersect(self, other):
return (self.x1 <= other.x2 and self.x2 >= other.x1 and
self.y1 <= other.y2 and self.y2 >= other.y1)
################
# Mapgen functions
################
#create a room using Rect
def create_room(room):
global map
for x in range(room.x1 + 1, room.x2):
for y in range(room.y1 + 1, room.y2):
map[x][y].sight_blocker = False
map[x][y].move_blocker = False
def create_h_tunnel(x1, x2, y):
global map
for x in range(min(x1,x2), max(x1,x2)+1):
map[x][y].sight_blocker = False
map[x][y].move_blocker = False
def create_v_tunnel(y1, y2, x):
global map
for y in range(min(y1,y2), max(y1,y2)+1):
map[x][y].sight_blocker = False
map[x][y].move_blocker = False
##############
# object: movable thing with a character representing it on the map
##############
class object:
def __init__(self, x, y, char, name, color, move_blocker=False, always_visible=False, fighter = None, ai = None, item = None, equipment = None):
self.x = x
self.y = y
self.char = char
self.name = name
self.color = color
self.move_blocker = move_blocker
self.always_visible = always_visible
self.fighter = fighter
if self.fighter:
self.fighter.owner = self
self.ai = ai
if self.ai:
self.ai.owner = self
self.item = item
if self.item:
self.item.owner = self
self.equipment = equipment
if self.equipment:
self.equipment.owner = self
#equipment is always an item
self.item = Item()
self.item.owner = self
#move by the given amount if not blocked, returning True if it worked
def move(self, dx, dy):
if not move_blocker(self.x + dx, self.y + dy):
self.x += dx
self.y += dy
return True
else:
return False
#approximate a straight line path using "vector mathematics"
#now able to maneuver around trivial obstacles, like corners.
def move_towards(self, target_x, target_y):
#get vector, distance
distx = target_x - self.x
disty = target_y - self.y
#using circledist here because it's prettier and has little impact
distance = math.sqrt(distx ** 2 + disty ** 2)
#figure out what grid-locked move approximates a step along the vector?
dx = int(round(distx / distance))
dy = int(round(disty / distance))
#try alternative moves if the direct approach doesn't work
if self.move(dx, dy) != True:
if abs(dx) == abs(dy):
#diagonal didn't work, so try moving only along the more distant axis
#if that doesn't work, try the other way
if abs(distx) > abs(disty):
dy = 0
if self.move(dx,dy) != True:
dx = 0
dy = -1 if disty < 0 else 1
self.move(dx,dy)
else:
dx = 0
if self.move(dx,dy) != True:
dy = 0
dx = -1 if distx < 0 else 1
self.move(dx,dy)
elif abs(dx) > abs(dy) and disty != 0:
#x-dir alone didn't work, so try adding a y-component
dy = -1 if disty < 0 else 1
if self.move(dx,dy) != True:
#diag didn't work, so remove the x-component
dx = 0
self.move(dx,dy)
elif abs(dy) > abs(dx) and distx != 0:
#as above, but with x and y reversed
dx = -1 if distx < 0 else 1
if self.move(dx,dy) != True:
dy = 0
self.move(dx,dy)
#move to dest using A* pathfinding
def move_astar(self, target):
fov = libtcod.map_new(MAP_WIDTH, MAP_HEIGHT)
#set move, sight blockers
for y1 in range(MAP_HEIGHT):
for x1 in range(MAP_WIDTH):
libtcod.map_set_properties(fov, x1, y1, not map[x1][y1].sight_blocker, not map[x1][y1].move_blocker)
#Treat tiles occupied by monsters as move blocked
for obj in objects:
if obj.move_blocker and obj != self and obj != target:
libtcod.map_set_properties(fov, obj.x, obj.y, True, False)
#Allocate path. Use roguelike geometry (diagonals = cardinals).
my_path = libtcod.path_new_using_map(fov, 1.0)
#Compute path
libtcod.path_compute(my_path, self.x, self.y, target.x, target.y)
#Confirm path was found, and is short, then take step.
if not libtcod.path_is_empty(my_path) and libtcod.path_size(my_path) < MAX_ASTAR_PATH_LENGTH:
x, y = libtcod.path_walk(my_path, True)
if x or y:
#self.move takes dx, dy so don't use that
self.x = x
self.y = y
#If the path is bad, take direct path to player.
#This happens if, say, player is behind a monster in a corridor.
else:
self.move_towards(target.x, target.y)
#Deallocate path memory
libtcod.path_delete(my_path)
#Distance to object using roguelike geometry.
def distance_to(self, other):
return self.distance(other.x, other.y)
#Distance to tile using roguelike geometry.
#In a world where squares are circles, dist is just greater of x or y.
def distance(self, x, y):
dx = x - self.x
dy = y - self.y
return max( abs(dx), abs(dy) )
def draw(self):
#draw this object at its current map position
if (libtcod.map_is_in_fov(fov_map, self.x, self.y) or (self.always_visible and map[self.x][self.y].explored)):
libtcod.console_set_default_foreground(console, self.color)
libtcod.console_put_char(console, self.x, self.y, self.char, libtcod.BKGND_NONE)
def send_to_back(self):
#send this monster to the front of the list so it's drawn first
#used to make corpses overdrawn by living creatures
#seems like a hack tbqh
#does result in bad behavior: corpses overwritten on the map,
#but also listed first when looking at tile with a living creature
global objects
objects.remove(self)
objects.insert(0, self)
def clear(self):
#erase this object from the console
if libtcod.map_is_in_fov(fov_map, self.x, self.y):
libtcod.console_put_char_ex(console, self.x, self.y, '.', libtcod.white, libtcod.dark_blue)
###########################
# Fighter: combat component of a combat-capable object
###########################
class Fighter:
def __init__(self, hp, defense, power, xp, ranged = 0, death_function=None):
self.base_max_hp = hp
self.hp = hp
self.base_defense = defense
self.base_power = power
self.base_ranged = ranged
self.xp = xp
self.death_function = death_function
#calculate effective values of many stats based on equipment
@property
def max_hp(self):
bonus = sum(equipment.max_hp_bonus for equipment in get_all_equipped(self.owner))
return self.base_max_hp + bonus
@property
def power(self):
bonus = sum(equipment.power_bonus for equipment in get_all_equipped(self.owner))
return self.base_power + bonus
@property
def defense(self):
bonus = sum(equipment.defense_bonus for equipment in get_all_equipped(self.owner))
return self.base_defense + bonus
@property
def ranged(self):
#bonus = items don't give ranged bonuses yet
return self.base_ranged# + bonus
def take_damage(self, damage):
if damage > 0:
self.hp -= damage
#death handling
if self.hp <= 0:
if self.owner != player: #give player xp for kill
player.fighter.xp += self.xp
function = self.death_function
if function is not None:
function(self.owner)
def attack(self, target):
damage = self.power - target.fighter.defense
if damage > 0:
message(self.owner.name.capitalize() + ' attacks ' + target.name + ' for ' + str(damage) + ' hit points!')
target.fighter.take_damage(damage)
else:
message(self.owner.name.capitalize() + ' attacks ' + target.name + ' to no effect.')
def shoot(self, target):
#TODO: should probably animate this to identify the shooter :/
damage = self.ranged - target.fighter.defense
if damage > 0:
message(self.owner.name.capitalize() + ' shoots ' + target.name + ' for ' + str(damage) + ' hit points!')
target.fighter.take_damage(damage)
else:
message(self.owner.name.capitalize() + ' shoots ' + target.name + ' to no effect.')
def heal(self, amount):
self.hp += amount
if self.hp > self.max_hp:
self.hp = self.max_hp
##############################
# cast_heal(): heal the player (e.g. for using a potion)
##############################
def cast_heal():
if player.fighter.hp == player.fighter.max_hp:
message('You are already at full health.', libtcod.red)
return 'cancelled'
message('Your wounds start to feel better!', libtcod.light_violet)
player.fighter.heal(HEAL_AMOUNT)
#############################
# cast_lightning(): hit the monster nearest the player with lightning bolt
#############################
def cast_lightning():
monster = closest_monster(LIGHTNING_RANGE)
if monster is None:
message('No enemy is close enough to strike.', libtcod.light_blue)
return 'cancelled'
message('A lightning bolt strikes the ' + monster.name + ' with a thunderous zap! The damage is ' + str(LIGHTNING_DAMAGE) + ' hit points.', libtcod.light_blue)
monster.fighter.take_damage(LIGHTNING_DAMAGE)
#############################
# cast_confuse(): hit the monster nearest to the player with confusion
#############################
def cast_confuse():
message('Left-click an enemy to confuse it, or right-click to cancel.', libtcod.light_cyan)
monster = target_monster(CONFUSE_RANGE)
if monster is None: return 'cancelled'
#Swap out the monster's brain with temporary porridge
old_ai = monster.ai
monster.ai = ConfusedMonster(old_ai)
monster.ai.owner = monster
message('The eyes of the ' + monster.name + ' look vacant, as it starts to stumble around!', libtcod.light_green)
##################################
# cast_fireball(): target arbitrary point near player with a fireball
##################################
def cast_fireball():
message('Left-click a target tile for the fireball, or right-click to cancel.', libtcod.light_cyan)
(x, y) = target_tile()
if x is None: return 'cancelled'
message('The fireball explodes, burning everything within ' + str(FIREBALL_RADIUS) + ' tiles!', libtcod.orange)
for object in objects:
if object.distance(x,y) <= FIREBALL_RADIUS and object.fighter:
message('The ' + object.name + ' gets burned for ' + str(FIREBALL_DAMAGE) + ' hit points.', libtcod.orange)
object.fighter.take_damage(FIREBALL_DAMAGE)
#############################
# closest_monster(): return the closest monster to the player within range
#############################
def closest_monster(max_range):
closest_enemy = None
closest_dist = max_range + 1
for object in objects:
if object.fighter and not object == player and libtcod.map_is_in_fov(fov_map, object.x, object.y):
dist = player.distance_to(object)
if dist < closest_dist:
closest_dist = dist
closest_enemy = object
return closest_enemy
############################
# Item: inventory item component of an object that can be picked up
############################
class Item:
def __init__(self, use_function=None):
self.use_function = use_function
def pick_up(self):
#add to inventory, remove from map
if len(inventory) >= 26:
message('Your inventory is full, cannot pick up ' + self.owner.name + '.', libtcod.red)
else:
inventory.append(self.owner)
objects.remove(self.owner)
message('You picked up a ' + self.owner.name + '!', libtcod.green)
#auto-equip to empty slots
equipment = self.owner.equipment
if equipment and get_equipped_in_slot(equipment.slot) is None:
equipment.equip()
def drop(self):
#dequip item if necessary
if self.owner.equipment:
self.owner.equipment.dequip()
#add item to map, remove item from player inventory
objects.append(self.owner)
inventory.remove(self.owner)
self.owner.x = player.x
self.owner.y = player.y
message('You dropped a ' + self.owner.name + '.', libtcod.yellow)
def use(self):
if self.owner.equipment:
self.owner.equipment.toggle_equip()
return
if self.use_function is None:
message('The ' + self.owner.name + ' cannot be used.')
else:
if self.use_function() != 'cancelled':
inventory.remove(self.owner) #consume items on use
###################################################
# Equipment: item that can be equipped. Automatically adds the item component.
###################################################
class Equipment:
def __init__(self, slot, power_bonus = 0, defense_bonus = 0, max_hp_bonus = 0):
self.slot = slot
self.power_bonus = power_bonus
self.defense_bonus = defense_bonus
self.max_hp_bonus = max_hp_bonus
self.is_equipped = False
def toggle_equip(self):
if self.is_equipped:
self.dequip()
else:
self.equip()
def equip(self):
old_equipment = get_equipped_in_slot(self.slot)
if old_equipment is not None:
old_equipment.dequip()
self.is_equipped = True
message('Equipped ' + self.owner.name + ' on ' + self.slot + '.', libtcod.light_green)
def dequip(self):
if not self.is_equipped: return
self.is_equipped = False
message('Dequipped ' + self.owner.name + ' from ' + self.slot + '.', libtcod.light_yellow)
###################################
# get_equipped_in_slot(): returns the equipment in a slot, or None
###################################
def get_equipped_in_slot(slot):
for obj in inventory:
if obj.equipment and obj.equipment.slot == slot and obj.equipment.is_equipped:
return obj.equipment
return None
################################################################
# get_all_equipped(): gets a list of all the object's equipped items
# it does an object because I guess the tutorial wants to encourage
# you to implement monsters equipping items?
################################################################
def get_all_equipped(obj):
if obj == player:
equipped_list = []
for item in inventory:
if item.equipment and item.equipment.is_equipped:
equipped_list.append(item.equipment)
return equipped_list
else:
return [] #no monster equipment :(
############################
# BasicMonster: AI for "charge and melee" enemies
############################
class BasicMonster:
def __init__(self):
self.turns_to_chase_player = 0
def take_turn(self):
monster = self.owner
#simple reciprocal fov by mooching off player's fov_map
can_see_player = libtcod.map_is_in_fov(fov_map, monster.x, monster.y)
#reset chase timer if monster can see the player
if can_see_player:
#alert nearby monsters if monster wasn't already alert
if self.turns_to_chase_player == 0:
message("The " + self.owner.name + " shouts!", libtcod.red)
for object in objects:
if object != monster and object.distance(monster.x, monster.y) <= SHOUT_RADIUS and object.ai:
object.ai.turns_to_chase_player = TURNS_MONSTERS_CHASE_PLAYER_AFTER_LOSING_CONTACT
self.turns_to_chase_player = TURNS_MONSTERS_CHASE_PLAYER_AFTER_LOSING_CONTACT
#chase the player if the chase timer is nonzero (and decrement timer too)
if self.turns_to_chase_player > 0:
self.turns_to_chase_player -= 1
#close on distant player
if monster.distance_to(player) >= 2:
if monster.fighter.ranged and can_see_player:
monster.fighter.shoot(player)
else:
monster.move_astar(player)
#kill adjacent, alive player
elif player.fighter.hp > 0:
monster.fighter.attack(player)
###############################
# ConfusedMonster: AI for monsters recently hit by Confuse scroll
###############################
class ConfusedMonster:
def __init__(self, old_ai, num_turns=CONFUSE_NUM_TURNS):
self.old_ai = old_ai
self.num_turns = num_turns
def take_turn(self):
if self.num_turns > 0:
#randomwalk and decrement timer
self.owner.move(libtcod.random_get_int(0,-1,1), libtcod.random_get_int(0,-1,1))
self.num_turns -= 1
else:
self.owner.ai = self.old_ai
message('The ' + self.owner.name + ' is no longer confused!', libtcod.red)
#############################
# player_death(player): handle player death (lose game)
#############################
def player_death(player):
global game_state
message('You died!',libtcod.red)
game_state = 'dead'
#represent player as corpse
player.char = '%'
player.color = libtcod.dark_red
#############################
# boss_death(player): handle boss death (wins game atm)
#############################
def boss_death(boss):
monster_death(boss)
message('You\'ve killed the dragon, and won the game! Press escape to exit.', libtcod.gold)
####################################
# monster_death(monster): handle monster death
####################################
def monster_death(monster):
message(monster.name.capitalize() + ' is dead! You gain ' + str(monster.fighter.xp) + ' experience points.',libtcod.orange)
#convert monster to a corpse
monster.char = '%'
monster.color = libtcod.dark_red
monster.move_blocker = False
monster.fighter = None
monster.ai = None
monster.name = 'remains of ' + monster.name
#send monster to front of list so it's drawn before/under other objects
monster.send_to_back()
###########################
# Tile: immovable square of map terrain
###########################
#note that tiles don't have a character or color right now
class Tile:
def __init__(self, move_blocker, sight_blocker = None):
self.move_blocker = move_blocker
#by default, tile blocks sight only if it blocks movement
if sight_blocker is None: sight_blocker = move_blocker
self.sight_blocker = sight_blocker
self.explored = False
######################
# move_blocker(x, y) [function]: check if a location blocks movement
######################
def move_blocker(x, y):
if map[x][y].move_blocker:
return True
for object in objects:
if object.move_blocker and object.x == x and object.y == y:
return True
return False
#################################################
# random_choice_index(): choose one index from a list of chances, raffle-style
#################################################
def random_choice_index(chances):
dice = libtcod.random_get_int(0, 1, sum(chances))
running_sum = 0
choice = 0
for w in chances:
running_sum += w
if dice <= running_sum:
return choice
choice += 1
##################################################
# random_choice(): choose one string from a list of (string, chance), raffle-style
# in other words it's the previous function but it documents what the options represent better
##################################################
def random_choice(chances_dict):
chances = chances_dict.values()
strings = chances_dict.keys()
return strings[random_choice_index(chances)]
################################
# place_objects(): spawns monsters and items in existing rooms.
################################
def place_objects(room):
#define the possible random results by dungeon level
#monster spawning rules
max_monsters = from_dungeon_level([[2, 1], [3, 4], [5, 6]])
monster_chances = {}
monster_chances['orc'] = 80 #orcs always have 80 chances
#15 troll chances at dungeon level 3, 30 chances at dlvl 5, etc.
monster_chances['troll'] = from_dungeon_level([[15, 3], [30, 5], [60, 7]])
monster_chances['orc archer'] = from_dungeon_level([[20, 4], [40, 6], [80, 8]])
#item spawning rules
max_items = from_dungeon_level([[1, 1], [2, 4]])
item_chances = {}
item_chances['heal'] = 35
item_chances['confuse'] = from_dungeon_level([[10, 2]])
item_chances['lightning'] = from_dungeon_level([[25, 4]])
item_chances['fireball'] = from_dungeon_level([[25, 6]])
item_chances['sword'] = from_dungeon_level([[5, 4]])
item_chances['shield'] = from_dungeon_level([[15, 8]])
#place monsters first
num_monsters = libtcod.random_get_int(0, 0, max_monsters)
for i in range (num_monsters):
x = libtcod.random_get_int(0, room.x1+1, room.x2-1)
y = libtcod.random_get_int(0, room.y1+1, room.y2-1)
if not move_blocker(x, y):
monster_roll = random_choice(monster_chances)
if monster_roll == 'orc':
fighter_component = Fighter(hp = 20, defense = 0, power = 4, xp = 35, death_function = monster_death)
ai_component = BasicMonster()
monster = object(x, y, 'o', 'orc', libtcod.desaturated_green, move_blocker = True, fighter = fighter_component, ai = ai_component)
elif monster_roll == 'troll':
fighter_component = Fighter(hp = 30, defense = 2, power = 8, xp = 100, death_function = monster_death)
ai_component = BasicMonster()
monster = object(x, y, 'T', 'troll', libtcod.darker_green, move_blocker = True, fighter = fighter_component, ai = ai_component)
elif monster_roll == 'orc archer':
fighter_component = Fighter(hp = 15, defense = 0, power = 2, ranged = 4, xp = 35, death_function = monster_death)
ai_component = BasicMonster()
monster = object(x, y, 'o', 'orc archer', libtcod.light_green, move_blocker = True, fighter = fighter_component, ai = ai_component)
objects.append(monster)
#place items second
num_items = libtcod.random_get_int(0, 0, max_items)
for i in range(num_items):
x = libtcod.random_get_int(0, room.x1+1, room.x2-1)
y = libtcod.random_get_int(0, room.y1+1, room.y2-1)
#only place items if the tile is not blocked (possibly a wall)
#ideally this would distinguish between wall-like terrain and monsters but it doesn't matter much
if not move_blocker(x, y):
itemRoll = random_choice(item_chances)
if itemRoll == 'heal':
item_component = Item(use_function = cast_heal)
item = object(x, y, '!', 'healing potion', libtcod.violet, item = item_component, always_visible = True)
elif itemRoll == 'lightning':
item_component = Item(use_function = cast_lightning)
item = object(x, y, '?', 'scroll of lightning bolt', libtcod.light_blue, item = item_component, always_visible = True)
elif itemRoll == 'fireball':
item_component = Item(use_function = cast_fireball)
item = object(x, y, '?', 'scroll of fireball', libtcod.light_yellow, item = item_component, always_visible = True)
elif itemRoll == 'confuse':
item_component = Item(use_function = cast_confuse)
item = object(x, y, '?', 'scroll of confuse monster', libtcod.light_green, item = item_component, always_visible = True)
elif itemRoll == 'sword':
equipment_component = Equipment(slot='right hand', power_bonus = 3)
item = object(x, y, '/', 'sword', libtcod.sky, equipment = equipment_component)
elif itemRoll == 'shield':
equipment_component = Equipment(slot='left hand', defense_bonus = 1)
item = object(x, y, '[', 'shield', libtcod.darker_orange, equipment = equipment_component)
objects.append(item)
#hack to ensure monsters are drawn over items
item.send_to_back()
###########################################
# from_dungeon_level(): returns a value that depends on the current dungeon level from a table
###########################################
def from_dungeon_level(table):
for (value, level) in reversed(table):
if dungeon_level >= level:
return value
return 0
############################
# Make_map(): Creates the dungeon map
############################
def make_map():
global map, objects, stairs
objects = [player]
#default = void that blocks nothing
map = [[ Tile(True)
for y in range(MAP_HEIGHT) ]
for x in range(MAP_WIDTH) ]
rooms = []
num_rooms = 0
for r in range(MAX_ROOMS):
#make random stats for another random room
w = libtcod.random_get_int(0, ROOM_MIN_SIZE, ROOM_MAX_SIZE)
h = libtcod.random_get_int(0, ROOM_MIN_SIZE, ROOM_MAX_SIZE)
x = libtcod.random_get_int(0, 0, MAP_WIDTH - w - 1)
y = libtcod.random_get_int(0, 0, MAP_HEIGHT - h - 1)
new_room = Rect(x, y, w, h)
room_intersects = False
for other_room in rooms:
if new_room.intersect(other_room):
room_intersects = True
break
if not room_intersects:
create_room(new_room)
(new_x, new_y) = new_room.center()
#place player in first room
if num_rooms == 0:
player.x = new_x
player.y = new_y
#rooms 2 ... MAX_ROOMS must have a connecting corridor
else:
(prev_x, prev_y) = rooms[num_rooms-1].center()
#randomize whether vertical or horizontal displacement is corrected first
if libtcod.random_get_int(0, 0, 1) == 1:
create_h_tunnel(prev_x, new_x, prev_y)
create_v_tunnel(prev_y, new_y, new_x)
else:
create_v_tunnel(prev_y, new_y, prev_x)
create_h_tunnel(prev_x, new_x, new_y)
#place monsters in the room
place_objects(new_room)
#need to track placed rooms
rooms.append(new_room)
num_rooms += 1
#last room only has stairs if there's no boss
if dungeon_level < 9:
stairs = object(new_x, new_y, '<', 'stairs', libtcod.white, always_visible = True)
objects.append(stairs)
stairs.send_to_back() #tired of this hack
else:
#place boss
stairs = None
x = libtcod.random_get_int(0, new_room.x1+1, new_room.x2-1)
y = libtcod.random_get_int(0, new_room.y1+1, new_room.y2-1)
#BIG BUG this while loop sometimes loops endlessly, probably (not 100% certain this caused it but it obviously theoretically could and I didn't add any other looping structure when it happened)
#Conversely, spawning the monster on top of another monster will look strange, but probably mostly work okay.
#A better fix is to give place_objects parameter(s) that tell it what kind of things to place...
#while move_blocker(x, y):
#x = libtcod.random_get_int(0, new_room.x1+1, new_room.x2-1)
#y = libtcod.random_get_int(0, new_room.y1+1, new_room.y2-1)
fighter_component = Fighter(hp = LIGHTNING_DAMAGE * 3 + 1, defense = 4, power = 20, xp = 0, death_function = boss_death)
ai_component = BasicMonster()
boss = object(x, y, 'D', 'the dragon', libtcod.red, move_blocker = True, fighter = fighter_component, ai = ai_component)
objects.append(boss)
###############################
# next_level(): place player on next dungeon level
###############################
def next_level():
global dungeon_level
message('You take a moment to rest, and recover your strength.', libtcod.light_violet)
#heal 50% hp
player.fighter.heal(player.fighter.max_hp / 2)
message('After a rare moment of peace, you descend deeper into the heart of the dungeon...', libtcod.red)
dungeon_level += 1
make_map()
initialize_fov()
#this is a good time to autosave, in case of a fatal bug
save_game()
########################################
# check_level_up(): increase the player's character level if eligible
########################################
def check_level_up():
level_up_xp = LEVEL_UP_BASE + player.level * LEVEL_UP_FACTOR
if player.fighter.xp >= level_up_xp:
player.level += 1
player.fighter.xp -= level_up_xp
message('Your battle skills grow stronger! You reached level ' + str(player.level) + '!', libtcod.yellow)
#ask player to improve a stat
#i'm pretty sure these options are terribly unbalanced and agility is the god stat right now
choice = None
while choice == None:
choice = menu('Level up! Choose a stat to raise:\n',
['Constitution (+20 HP, from ' + str(player.fighter.base_max_hp) + ')',
'Strength (+1 attack, from ' + str(player.fighter.base_power) + ')',
'Agility (+1 defense, from ' + str(player.fighter.base_defense) + ')'], LEVEL_SCREEN_WIDTH)
if choice == 0:
player.fighter.base_max_hp += 20
player.fighter.hp += 20
elif choice == 1:
player.fighter.base_power += 1
elif choice == 2:
player.fighter.base_defense += 1
###########################
# render_bar(): render a colored bar (e.g. for health in GUI)
###########################
def render_bar(x, y, total_width, name, value, maximum, bar_color, back_color, text_color):
bar_width = int(float(value) / maximum * total_width)
#draw maximum bar
libtcod.console_set_default_background(panel, back_color)
libtcod.console_rect(panel, x, y, total_width, 1, False, libtcod.BKGND_SCREEN)
#draw filled portion of bar
if bar_width > 0:
libtcod.console_set_default_background(panel, bar_color)
libtcod.console_rect(panel, x, y, bar_width, 1, False, libtcod.BKGND_SCREEN)
#draw label with exact numbers and what the bar represents
libtcod.console_set_default_foreground(panel, text_color)
libtcod.console_print_ex(panel, x + total_width / 2, y, libtcod.BKGND_NONE, libtcod.CENTER, name + ': ' + str(value) + '/' + str(maximum))
##########################
# render_all(): Renders everything to the visible console
##########################
def render_all():
global fov_recompute
global fov_map
if fov_recompute:
fov_recompute = False
libtcod.map_compute_fov(fov_map, player.x, player.y, TORCH_RADIUS, FOV_LIGHT_WALLS, FOV_ALGO)
for y in range(MAP_HEIGHT):
for x in range(MAP_WIDTH):
wall = map[x][y].sight_blocker
visible = libtcod.map_is_in_fov(fov_map, x, y)
if not visible:
if map[x][y].explored:
if wall:
libtcod.console_put_char_ex(console, x, y, "#", color_dark_wall, libtcod.BKGND_SET)
else:
libtcod.console_put_char_ex(console, x, y, ".", color_dark_ground, libtcod.BKGND_SET)
else:
if wall:
libtcod.console_put_char_ex(console, x, y, "#", color_light_wall, libtcod.BKGND_SET)
else:
libtcod.console_put_char_ex(console, x, y, ".", color_light_ground, libtcod.BKGND_SET)
map[x][y].explored = True
#quick fix to "item is drawn instead of the monster standing on it" issue
objects.sort(key = lambda x: x.move_blocker)
for object in objects:
object.draw()
libtcod.console_blit(console, 0, 0, MAP_WIDTH, MAP_HEIGHT, 0, 0, 0)
#redraw GUI elements
libtcod.console_set_default_background(panel, libtcod.black)
libtcod.console_clear(panel)
#show player's current HP
render_bar(1, 1, BAR_WIDTH, 'HP', player.fighter.hp, player.fighter.max_hp, libtcod.red, libtcod.darkest_red, libtcod.white)
#show dungeon level
libtcod.console_set_default_foreground(panel, libtcod.white)
libtcod.console_print_ex(panel, 1, 3, libtcod.BKGND_NONE, libtcod.LEFT, 'Dungeon level ' + str(dungeon_level))
#show names of objects at cursor if they're in fov
libtcod.console_set_default_foreground(panel, libtcod.light_gray)
libtcod.console_print_ex(panel, 1, 0, libtcod.BKGND_NONE, libtcod.LEFT, get_names_under_mouse())
#show short message log
y = 1
for(line, color) in game_msgs:
libtcod.console_set_default_foreground(panel, color)
libtcod.console_print_ex(panel, MSG_X, y, libtcod.BKGND_NONE, libtcod.LEFT, line)
y += 1
#blit GUI to root
libtcod.console_blit(panel, 0, 0, SCREEN_WIDTH, PANEL_HEIGHT, 0, 0, PANEL_Y)
#######################
# menu(): show a menu and also allow interaction with it
# header: the menu's title
# options: a list of strings that can be selected from the menu
# width: the width of the menu panel
#######################
def menu(header, options, width):
if len(options) > 26: raise ValueError('Cannot have a menu with more than 26 options.')
#calculate height of panel
header_height = libtcod.console_get_height_rect(console, 0, 0, width, SCREEN_HEIGHT, header)
if header == '':
header_height = 0
height = len(options) + header_height
#draw to additional console to preserve current console
window = libtcod.console_new(width, height)
#print header with wrap
libtcod.console_set_default_foreground(window, libtcod.white)
libtcod.console_print_rect_ex(window, 0, 0, width, height, libtcod.BKGND_NONE, libtcod.LEFT, header)
#print formatted menu options
y = header_height
letter_index = ord('a')
for option_text in options: