· 9 years ago · Feb 04, 2017, 08:10 AM
1import libtcodpy as libtcod
2import math
3import textwrap
4import random
5import shelve
6from bearlibterminal import terminal
7
8#size of window
9SCREEN_WIDTH = 80
10SCREEN_HEIGHT = 50
11
12#size of map
13MAP_WIDTH = 80
14MAP_HEIGHT = 43
15
16FOV_ALGO = 0 #default FOV algorithm
17FOV_LIGHT_WALLS = True #light walls or not
18TORCH_RADIUS = 5
19
20#sizes and coordinates relevant for the GUI
21BAR_WIDTH = 20
22PANEL_HEIGHT = 7
23PANEL_Y = SCREEN_HEIGHT - PANEL_HEIGHT
24MSG_X = BAR_WIDTH + 2
25MSG_WIDTH = SCREEN_WIDTH - BAR_WIDTH - 2
26MSG_HEIGHT = PANEL_HEIGHT - 1
27BAR_CHAR = 20
28INVENTORY_WIDTH = 50
29MENU_BACKGROUND_COLOR = 3716001477
30LEVEL_SCREEN_WIDTH = 40
31CHARACTER_SCREEN_WIDTH = 30
32
33MONSTER_SMELL_RANGE = 20
34
35#parameters for dungeon generator
36ROOM_MAX_SIZE = 10
37ROOM_MIN_SIZE = 6
38MAX_ROOMS = 30
39
40#Spell Values
41HEAL_AMOUNT = 4
42LIGHTNING_DAMAGE = 20
43LIGHTNING_RANGE = 5
44CONFUSE_RANGE = 8
45CONFUSE_NUM_TURNS = 10
46FIREBALL_RADIUS = 3
47FIREBALL_DAMAGE = 12
48
49#Experience and level-ups
50LEVEL_UP_BASE = 200
51LEVEL_UP_FACTOR = 150
52
53LIMIT_FPS = 10 #10 frames-per-second maximum
54
55tile_dark_wall = 14
56tile_light_wall = 13
57tile_dark_ground = 0
58tile_light_ground = 1
59
60class Tile:
61 #a tile of the map and its properties
62 def __init__(self, blocked, block_sight = None):
63 self.blocked = blocked
64
65 #all tiles start unexplored
66 self.explored = False
67
68 #by default, if a tile is blocked, it also blocks sight
69 if block_sight is None: block_sight = blocked
70 self.block_sight = block_sight
71
72class Rect:
73 #a rectangle on the map. used to characterize a room.
74 def __init__(self, x, y, w, h):
75 self.x1 = x
76 self.y1 = y
77 self.x2 = x + w
78 self.y2 = y + h
79
80 def center(self):
81 center_x = (self.x1 + self.x2) / 2
82 center_y = (self.y1 + self.y2) / 2
83 return (center_x, center_y)
84
85 def intersect(self, other):
86 #returns true if this rectangle intersects with another one
87 return (self.x1 <= other.x2 and self.x2 >= other.x1 and
88 self.y1 <= other.y2 and self.y2 >= other.y1)
89
90class Object:
91 #this is a generic object: the player, a monster, an item, the stairs...
92 #it's always represented by a character on screen.
93 def __init__(self, x, y, char, name, blocks=False, always_visible=False, fighter=None, ai=None, item=None, equipment=None):
94 self.x = x
95 self.y = y
96 self.char = char
97 self.name = name
98 self.blocks = blocks
99 self.always_visible = always_visible
100
101 self.fighter = fighter
102 if self.fighter: #let the fighter component know who owns it
103 self.fighter.owner = self
104
105 self.ai = ai
106 if self.ai: #let the AI component know who owns it
107 self.ai.owner = self
108
109 self.item = item
110 if self.item: #let the Item component know who owns it
111 self.item.owner = self
112
113 self.equipment = equipment
114 if self.equipment: #let the Equipment component know who owns it
115 self.equipment.owner = self
116 #there must be an Item component for the Equipment component to work properly
117 self.item = Item()
118 self.item.owner = self
119
120
121 def move(self, dx, dy):
122 #move by the given amount, if the destination is not blocked
123 if not is_blocked(self.x + dx, self.y + dy):
124
125 self.x += dx
126 self.y += dy
127
128 def move_astar(self, target):
129 #Create a FOV map that has the dimensions of the map
130 fov = libtcod.map_new(MAP_WIDTH, MAP_HEIGHT)
131
132 #Scan the current map each turn and set all the walls as unwalkable
133 for y1 in range(MAP_HEIGHT):
134 for x1 in range(MAP_WIDTH):
135 libtcod.map_set_properties(fov, x1, y1, not map[x1][y1].block_sight, not map[x1][y1].blocked)
136
137 #Scan all the objects to see if there are objects that must be navigated around
138 #Check also that the object isn't self or the target (so that the start and the end points are free)
139 #The AI class handles the situation if self is next to the target so it will not use this A* function anyway
140 for obj in objects:
141 if obj.blocks and obj != self and obj != target:
142 #Set the tile as a wall so it must be navigated around
143 libtcod.map_set_properties(fov, obj.x, obj.y, True, False)
144
145 #Allocate a A* path
146 #The 1.41 is the normal diagonal cost of moving, it can be set as 0.0 if diagonal moves are prohibited
147 my_path = libtcod.path_new_using_map(fov, 1.41)
148
149 #Compute the path between self's coordinates and the target's coordinates
150 libtcod.path_compute(my_path, self.x, self.y, target.x, target.y)
151
152 #Check if the path exists, and in this case, also the path is shorter than 25 tiles
153 #The path size matters if you want the monster to use alternative longer paths (for example through other rooms) if for example the player is in a corridor
154 #It makes sense to keep path size relatively low to keep the monsters from running around the map if there's an alternative path really far away
155 if not libtcod.path_is_empty(my_path) and libtcod.path_size(my_path) < 25:
156 #Find the next coordinates in the computed full path
157 x, y = libtcod.path_walk(my_path, True)
158 if x or y:
159 #Set self's coordinates to the next path tile
160 self.x = x
161 self.y = y
162 else:
163 #Keep the old move function as a backup so that if there are no paths (for example another monster blocks a corridor)
164 #it will still try to move towards the player (closer to the corridor opening)
165 self.move_towards(target.x, target.y)
166
167 #Delete the path to free memory
168 libtcod.path_delete(my_path)
169
170 def move_towards(self, target_x, target_y):
171 #vector from this object to the target, and distance
172 dx = target_x - self.x
173 dy = target_y - self.y
174 distance = math.sqrt(dx ** 2 + dy ** 2)
175
176 #normalize it to length 1 (preserving direction), then round it and
177 #convert to integer so the movement is restricted to the map grid
178 dx = int(round(dx / distance))
179 dy = int(round(dy / distance))
180 self.move(dx, dy)
181
182 def distance_to(self, other):
183 #return the distance to another object
184 dx = other.x - self.x
185 dy = other.y - self.y
186 return math.sqrt(dx ** 2 + dy ** 2)
187
188 def distance(self, x, y):
189 #return the distance to some coordinates
190 return math.sqrt((x - self.x) ** 2 + (y - self.y) ** 2)
191
192 def send_to_back(self):
193 #make this object be drawn first, so all others appear above it if they're in the same tile.
194 global objects
195 objects.remove(self)
196 objects.insert(0, self)
197
198 def draw(self):
199 #set the color and then draw the character that represents this object at its position
200 if (libtcod.map_is_in_fov(fov_map, self.x, self.y) or
201 (self.always_visible and map[self.x][self.y].explored)):
202 #set color and then draw character at location
203 if self.fighter:
204 terminal.layer(4)
205 else:
206 terminal.layer(3)
207 terminal.color("#FFFFFF")
208 terminal.put(self.x, self.y, self.char)
209
210 def clear(self):
211 #erase the character that represents this object
212 if self.fighter:
213 terminal.layer(4)
214 else:
215 terminal.layer(3)
216 terminal.put(self.x, self.y, ' ')
217
218class Fighter:
219 #combat-related properties and methods (monster, player, NPC).
220 def __init__(self, hp, defense, power, xp, death_function=None):
221 self.base_max_hp = hp
222 self.hp = hp
223 self.base_defense = defense
224 self.base_power = power
225 self.xp = xp
226 self.death_function = death_function
227
228 @property
229 def power(self): #return actual power, by summing up the bonuses from all equipped items
230 bonus = sum(equipment.power_bonus for equipment in get_all_equipped(self.owner))
231 return self.base_power + bonus
232
233 @property
234 def defense(self): #return actual defense, by summing up the bonuses from all equipped items
235 bonus = sum(equipment.defense_bonus for equipment in get_all_equipped(self.owner))
236 return self.base_defense + bonus
237
238 @property
239 def max_hp(self): #return actual max_hp, by summing up the bonuses from all equipped items
240 bonus = sum(equipment.max_hp_bonus for equipment in get_all_equipped(self.owner))
241 return self.base_max_hp + bonus
242
243 def attack(self, target):
244 #random-rolled attack damage, with crits
245 ran_hit = libtcod.random_get_float(0, 0, 1)
246 ran_def = libtcod.random_get_float(0, 0, 1)
247 if libtcod.random_get_float(0, 0, 100) > 90:
248 ran_hit = 2
249 message(self.owner.name.capitalize() + ' PERFORMS A CRITICAL HIT!', 4294901887)
250 damage = int(ran_hit * self.power - ran_def * target.fighter.defense)
251 if damage > 0:
252 #print self.owner.name.capitalize() + ' attacks ' + target.name + ' for ' + str(damage) + ' hit points.'
253 message(self.owner.name.capitalize() + ' attacks ' + target.name + ' for ' + str(damage) + ' hit points.',
254 4286578559)
255 target.fighter.take_damage(damage)
256 else:
257 #print self.owner.name.capitalize() + ' attacks ' + target.name + ' but has no effect.'
258 message(self.owner.name.capitalize() + ' attacks ' + target.name + ' but has no effect.', 4282335231)
259
260 def take_damage(self, damage):
261 #apply damage if possible
262 if damage > 0:
263 self.hp -= damage
264 #check for death. if there's a death function, call it
265 if self.hp <= 0:
266 function = self.death_function
267 if function is not None:
268 function(self.owner)
269 if self.owner != player: #yield experience to the player
270 player.fighter.xp += self.xp
271
272 def heal(self, amount):
273 #heal by the given amount, without going over the maximum
274 self.hp += amount
275 if self.hp > self.max_hp:
276 self.hp = self.max_hp
277
278class BasicMonster:
279 def __init__(self, has_seen_player=False):
280 self.has_seen_player = has_seen_player
281 #AI routine for a basic monster !!AI (for object)
282 def take_turn(self):
283 #basic mosnter takes turn. if you can see it, it can see you.
284 monster = self.owner
285 if libtcod.map_is_in_fov(fov_map, monster.x, monster.y):
286
287 self.has_seen_player = True
288 #move towards the player if far away
289 if monster.distance_to(player) >= 2:
290 monster.move_astar(player)
291
292 #close enough to attack (if player is alive still)
293 elif player.fighter.hp > 0:
294 monster.fighter.attack(player)
295 #print 'The attack of the ' + monster.name + ' bounces on your shiny carapace.'
296 elif (self.owner.distance_to(player) <= MONSTER_SMELL_RANGE) and self.has_seen_player:
297 monster.move_towards(player.x, player.y)
298
299class ConfusedMonster:
300 #AI for a temporarily confused monster (reverts to previous AI after a while).
301 def __init__(self, old_ai, num_turns=CONFUSE_NUM_TURNS):
302 self.old_ai = old_ai
303 self.num_turns = num_turns
304
305 def take_turn(self):
306 if self.num_turns > 0: #still confused...
307 #move in a random direction, and decrease the number of turns confused
308 self.owner.move(libtcod.random_get_int(0, -1, 1), libtcod.random_get_int(0, -1, 1))
309 self.num_turns -= 1
310
311 else: #restore the previous AI (this one will be deleted because it's not referenced anymore)
312 self.owner.ai = self.old_ai
313 message('The ' + self.owner.name + ' is no longer confused!', 'red')
314
315class Item:
316 #an item that can be picked up and used.
317 def __init__(self, use_function=None):
318 self.use_function = use_function
319
320 def pick_up(self):
321 #add to the player's inventory and remove from the map
322 if len(inventory) >= 26:
323 message('Your inventory is full, cannot pick up ' + self.owner.name + '.', 'red')
324 else:
325 inventory.append(self.owner)
326 objects.remove(self.owner)
327 message('You picked up a ' + self.owner.name + '!', 'green')
328
329 #special case: automatically equip, if the corresponding equipment slot is unused
330 equipment = self.owner.equipment
331 if equipment and get_equipped_in_slot(equipment.slot) is None:
332 equipment.equip()
333
334 def drop(self):
335 #special case: if the object has the Equipment component, dequip it before dropping
336 if self.owner.equipment:
337 self.owner.equipment.dequip
338 #add to the map and remove from the player's inventory. also, place it at the player's coordinates
339 objects.append(self.owner)
340 inventory.remove(self.owner)
341 self.owner.x = player.x
342 self.owner.y = player.y
343 message('You dropped a ' + self.owner.name + '.', 4294967040)
344
345
346 def use(self):
347 #special case: if the object has the Equipment component, the "use" action is to equip/dequip
348 if self.owner.equipment:
349 self.owner.equipment.toggle_equip()
350 return
351
352 #just call the "use_function" if it is defined
353 if self.use_function is None:
354 message('The ' + self.owner.name + ' cannot be used.')
355 else:
356 if self.use_function() != 'cancelled':
357 inventory.remove(self.owner) #destroy after use, unless it was cancelled for some reason
358
359class Equipment:
360 #an object that can be equipped, yielding bonuses. automatically adds the Item component.
361 def __init__(self, slot, power_bonus=0, defense_bonus=0, max_hp_bonus=0):
362 self.power_bonus = power_bonus
363 self.defense_bonus = defense_bonus
364 self.max_hp_bonus = max_hp_bonus
365 self.slot = slot
366 self.is_equipped = False
367
368 def toggle_equip(self): #toggle equip/dequip status
369 if self.is_equipped:
370 self.dequip()
371 else:
372 self.equip()
373
374 def equip(self):
375 #if the slot is already being used, dequip whatever is there first
376 old_equipment = get_equipped_in_slot(self.slot)
377 if old_equipment is not None:
378 old_equipment.dequip()
379
380 #equip object and show a message about it
381 self.is_equipped = True
382 message('Equipped ' + self.owner.name + ' on ' + self.slot + '.', 'green')
383
384 def dequip(self):
385 #dequip object and show a message about it
386 if not self.is_equipped: return
387 self.is_equipped = False
388 message('Dequipped ' + self.owner.name + ' from ' + self.slot + '.', 'yellow')
389
390def get_equipped_in_slot(slot): #returns the equipment in a slot, or None if it's empty
391 for obj in inventory:
392 if obj.equipment and obj.equipment.slot == slot and obj.equipment.is_equipped:
393 return obj.equipment
394 return None
395
396def get_all_equipped(obj): #returns a list of equipped items
397 if obj == player:
398 equipped_list = []
399 for item in inventory:
400 if item.equipment and item.equipment.is_equipped:
401 equipped_list.append(item.equipment)
402 return equipped_list
403 else:
404 return [] #other objects have no equipment
405
406def is_blocked(x, y):
407 #first test the map tile
408 if map[x][y].blocked:
409 return True
410
411 #now check for any blocking objects
412 for object in objects:
413 if object.blocks and object.x == x and object.y == y:
414 return True
415
416 return False
417
418def create_room(room):
419 global map
420 #go through the tiles in the rectangle and make them passable
421 for x in range(room.x1 + 1, room.x2):
422 for y in range(room.y1 + 1, room.y2):
423 map[x][y].blocked = False
424 map[x][y].block_sight = False
425
426def create_h_tunnel(x1, x2, y):
427 global map
428 #horizontal tunnel. min() and max() are used in case x1>x2
429 for x in range(min(x1, x2), max(x1, x2) + 1):
430 map[x][y].blocked = False
431 map[x][y].block_sight = False
432
433def create_v_tunnel(y1, y2, x):
434 global map
435 #vertical tunnel
436 for y in range(min(y1, y2), max(y1, y2) + 1):
437 map[x][y].blocked = False
438 map[x][y].block_sight = False
439
440def make_map():
441 global map, objects, stairs
442
443 #the list of objects with just the player
444 objects = [player]
445 #fill map with "blocked" tiles
446 map = [[ Tile(True)
447 for y in range(MAP_HEIGHT) ]
448 for x in range(MAP_WIDTH) ]
449
450 rooms = []
451 num_rooms = 0
452
453 for r in range(MAX_ROOMS):
454 #random width and height
455 w = libtcod.random_get_int(0, ROOM_MIN_SIZE, ROOM_MAX_SIZE)
456 h = libtcod.random_get_int(0, ROOM_MIN_SIZE, ROOM_MAX_SIZE)
457 #random position without going out of the boundaries of the map
458 x = libtcod.random_get_int(0, 0, MAP_WIDTH - w - 1)
459 y = libtcod.random_get_int(0, 0, MAP_HEIGHT - h - 1)
460
461 #"Rect" class makes rectangles easier to work with
462 new_room = Rect(x, y, w, h)
463
464 #run through the other rooms and see if they intersect with this one
465 failed = False
466 for other_room in rooms:
467 if new_room.intersect(other_room):
468 failed = True
469 break
470
471 if not failed:
472 #this means there are no intersections, so this room is valid
473
474 #"paint" it to the map's tiles
475 create_room(new_room)
476
477 #center coordinates of new room, will be useful later
478 (new_x, new_y) = new_room.center()
479
480 if num_rooms == 0:
481 #this is the first room, where the player starts at
482 player.x = new_x
483 player.y = new_y
484
485 else:
486 #all rooms after the first:
487 #connect it to the previous room with a tunnel
488
489 #center coordinates of previous room
490 (prev_x, prev_y) = rooms[num_rooms-1].center()
491
492 #draw a coin (random number that is either 0 or 1)
493 if libtcod.random_get_int(0, 0, 1) == 1:
494 #first move horizontally, then vertically
495 create_h_tunnel(prev_x, new_x, prev_y)
496 create_v_tunnel(prev_y, new_y, new_x)
497 else:
498 #first move vertically, then horizontally
499 create_v_tunnel(prev_y, new_y, prev_x)
500 create_h_tunnel(prev_x, new_x, new_y)
501
502 #add some contents to this room, such as monsters
503 place_objects(new_room)
504
505 #finally, append the new room to the list
506 rooms.append(new_room)
507 num_rooms += 1
508
509 #create stairs at the center of the last room
510 stairs = Object(new_x, new_y, 7, 'stairs', always_visible=True)
511 objects.append(stairs)
512 stairs.send_to_back() #so it's drawn below the monsters
513
514def random_choice_index(chances): #Choose one option from a list of chances
515 #The dice land on a number between 1 and sum
516 dice = libtcod.random_get_int(0, 1, sum(chances))
517
518 #Go through all chances
519 running_sum = 0
520 choice = 0
521 for w in chances:
522 running_sum += w
523
524 #Checking i dice landed in the part that corresponds to this choice
525 if dice <= running_sum:
526 return choice
527 choice +=1
528
529def random_choice(chances_dict):
530 #choose one option from dictionary of chances, returning its key
531 chances = chances_dict.values()
532 strings = chances_dict.keys()
533 return strings[random_choice_index(chances)]
534
535def from_dungeon_level(table):
536 #returns a value that depends on level. the table specifies what value occurs after each level, default is 0.
537 for (value, level) in reversed(table):
538 if dungeon_level >= level:
539 return value
540 return 0
541
542def place_objects(room):
543 #this is where we decide the chance of each monster or item appearing.
544
545 #maximum number of monsters per room
546 max_monsters = from_dungeon_level([[2, 1], [3, 4], [5, 6]])
547
548 #chance of each monster
549 monster_chances = {}
550 monster_chances['orc'] = 80 #orc always shows up, even if all other monsters have 0 chance
551 monster_chances['troll'] = from_dungeon_level([[15, 3], [30, 5], [60, 7]])
552
553 #maximum number of items per room
554 max_items = from_dungeon_level([[1, 1], [2, 4]])
555
556 #chance of each item (by default they have a chance of 0 at level 1, which then goes up)
557 item_chances = {}
558 item_chances['heal'] = 35 #healing potion always shows up, even if all other items have 0 chance
559 item_chances['lightning'] = from_dungeon_level([[25, 4]])
560 item_chances['fireball'] = from_dungeon_level([[25, 6]])
561 item_chances['confuse'] = from_dungeon_level([[10, 2]])
562 item_chances['sword'] = from_dungeon_level([[5, 4]])
563 item_chances['shield'] = from_dungeon_level([[15, 8]])
564
565
566 #choose random number of monsters
567 num_monsters = libtcod.random_get_int(0, 0, max_monsters)
568
569 for i in range(num_monsters):
570 #choose random spot for this monster
571 x = libtcod.random_get_int(0, room.x1+1, room.x2-1)
572 y = libtcod.random_get_int(0, room.y1+1, room.y2-1)
573
574 #only place it if the tile is not blocked
575 if not is_blocked(x, y):
576 choice = random_choice(monster_chances)
577 if choice == 'orc':
578 #create an orc
579 fighter_component = Fighter(hp=5, defense=0, power=3, xp=35, death_function=monster_death)
580 ai_component = BasicMonster()
581
582 monster = Object(x, y, 169, 'orc', blocks=True, fighter=fighter_component, ai=ai_component)
583
584 elif choice == 'troll':
585 #create a troll
586 fighter_component = Fighter(hp=30, defense=2, power=8, xp=100, death_function=monster_death)
587 ai_component = BasicMonster()
588
589 monster = Object(x, y, 162, 'slime', blocks=True, fighter=fighter_component, ai=ai_component)
590
591 objects.append(monster)
592
593 #choose random number of items
594 num_items = libtcod.random_get_int(0, 0, max_items)
595
596 for i in range(num_items):
597 #choose random spot for this item
598 x = libtcod.random_get_int(0, room.x1+1, room.x2-1)
599 y = libtcod.random_get_int(0, room.y1+1, room.y2-1)
600
601 #only place it if the tile is not blocked
602 if not is_blocked(x, y):
603 choice = random_choice(item_chances)
604 if choice == 'heal':
605 #create a healing potion
606 item_component = Item(use_function=cast_heal)
607 item = Object(x, y, 24, 'healing potion', item=item_component, always_visible=True)
608
609 elif choice == 'lightning':
610 #create a lightning bolt scroll
611 item_component = Item(use_function=cast_lightning)
612 item = Object(x, y, 21, 'scroll of lightning bolt', item=item_component, always_visible=True)
613
614 elif choice == 'fireball':
615 #create a fireball scroll
616 item_component = Item(use_function=cast_fireball)
617 item = Object(x, y, 21, 'scroll of fireball', 'light yellow', item=item_component, always_visible=True)
618
619 elif choice == 'confuse':
620 #create a confuse scroll
621 item_component = Item(use_function=cast_confuse)
622 item = Object(x, y, 21, 'scroll of confusion', 'light yellow', item=item_component, always_visible=True)
623
624 elif choice == 'sword':
625 #create a sword
626 equipment_component = Equipment(slot='right hand', power_bonus=3)
627 item = Object(x, y, 25, 'sword of might', equipment=equipment_component)
628
629 elif choice == 'shield':
630 #create a shield
631 equipment_component = Equipment(slot='left hand', defense_bonus=1)
632 item = Object(x, y, 134, 'shield', equipment=equipment_component)
633
634 objects.append(item)
635 item.send_to_back() #items appear below other objects
636 item.always_visible = True #items are visible even out-of-FOV, if in an explored area
637
638def render_bar(x, y, total_width, name, value, maximum, bar_color, back_color):
639 #adjust: move panel to desired location (bottom of console)
640 y = y + PANEL_Y
641 #set console to GUI layer
642 terminal.layer(5)
643 #render a bar (HP, experience, etc). first calculate the width of the bar
644 bar_width = int(float(value) / maximum * total_width)
645 #set total-width back bar color
646 terminal.color(back_color)
647 #terminal: draw a row of given length
648 a = 0
649 for b in range(total_width):
650 terminal.put(x + a, y, BAR_CHAR)
651 a += 1
652 #now render the bar on top
653 #set bar itself color
654 terminal.color(bar_color)
655 #libtcod.console_set_default_background(panel, bar_color)
656 if bar_width > 0:
657 a = 0
658 for b in range(bar_width):
659 terminal.put(x + a, y, BAR_CHAR)
660 a += 1
661 #finally, some centered text with the values
662 #first clear previous text, then draw new text, centred.
663 terminal.color("#FFFFFF")
664 terminal.layer(6)
665 terminal.clear_area(x, y, total_width, 1)
666 bar_center = len(name + ': ' + str(value) + '/' + str(maximum))/2
667 terminal.print_(x + total_width/2 - bar_center, y, name + ': ' + str(value) + '/' + str(maximum))
668
669def get_names_under_mouse():
670 #return a string with the names of all objects under the mouse
671 (x, y) = (terminal.state(terminal.TK_MOUSE_X), terminal.state(terminal.TK_MOUSE_Y))
672
673 #create a list with the names of all objects at the mouse's coordinates and in FOV
674 names = [obj.name for obj in objects
675 if obj.x == x and obj.y == y and libtcod.map_is_in_fov(fov_map, obj.x, obj.y)]
676 names = ', '.join(names) #join the names, separated by commas
677 return names.capitalize()
678
679def render_all():
680 global fov_map, color_dark_wall, color_light_wall
681 global color_dark_ground, color_light_ground
682 global fov_recompute, game_msgs
683
684 if fov_recompute:
685 #recompute FOV if needed (the player moved or something)
686 fov_recompute = False
687 libtcod.map_compute_fov(fov_map, player.x, player.y, TORCH_RADIUS, FOV_LIGHT_WALLS, FOV_ALGO)
688 terminal.color("#FFFFFF")
689 #go through all tiles, and set their background color according to the FOV
690 for y in range(MAP_HEIGHT):
691 for x in range(MAP_WIDTH):
692 visible = libtcod.map_is_in_fov(fov_map, x, y)
693 wall = map[x][y].block_sight
694 if not visible:
695 #if it's not visible right now, the player can only see it if it's explored
696 terminal.layer(2)
697 if map[x][y].explored:
698 if wall:
699 terminal.put(x, y, tile_dark_wall)
700 else:
701 terminal.put(x, y, tile_dark_ground)
702 else:
703 #it's visible
704 if wall:
705 terminal.put(x, y, tile_light_wall)
706 else:
707 terminal.put(x, y, tile_light_ground)
708 #since it's visible, explore it
709 map[x][y].explored = True
710
711 #draw all objects in the list, except the player. we want it to
712 for object in objects:
713 object.draw()
714
715 #prepare to render the GUI text
716 terminal.layer(6)
717 terminal.clear_area(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)
718 #print the game messages, one line at a time
719 y = 1
720 for (line, color) in game_msgs:
721 terminal.color(color)
722 terminal.print_(MSG_X, y+PANEL_Y, line)
723 y += 1
724
725 #show the player's hp
726 render_bar(1, 3, BAR_WIDTH, 'HP', player.fighter.hp, player.fighter.max_hp, 4294917951, 4286513152)
727
728 #show xp
729 level_up_xp = LEVEL_UP_BASE + player.level * LEVEL_UP_FACTOR
730 render_bar( 1, 4, BAR_WIDTH, 'XP', player.fighter.xp, level_up_xp, 4282384191, 4278222592)
731
732 #print dungeon level
733 terminal.layer(6)
734 terminal.color(4294967295)
735 terminal.print_(1,PANEL_Y + 5, 'Dungeon level ' + str(dungeon_level))
736
737 #display names of objects under the mouse
738 #still on layer 6 ***** (text layer)
739 terminal.print_(1, PANEL_Y, get_names_under_mouse())
740
741def message(new_msg, color = 'white'):
742 #split the message if necessary, among multiple lines
743 new_msg_lines = textwrap.wrap(new_msg, MSG_WIDTH)
744
745 for line in new_msg_lines:
746 #if the buffer is full, remove the first line to make room for the new one
747 if len(game_msgs) == MSG_HEIGHT:
748 del game_msgs[0]
749
750 #add the new line as a tuple, with the text and the color
751 game_msgs.append( (line, color) )
752
753def player_move_or_attack(dx, dy):
754 global fov_recompute
755 #the coordinates the player is moving to/attacking
756 x = player.x + dx
757 y = player.y + dy
758 #try to find an attackable object there
759 target = None
760 for object in objects:
761 if object.fighter and object.x == x and object.y == y:
762 target = object
763 break
764 #attack if target found, move otherwise
765 if target is not None:
766 player.fighter.attack(target)
767 else:
768 player.move(dx, dy)
769 fov_recompute = True
770
771def menu(header, options, width):
772 if len(options) > 26: raise ValueError('Cannot have a menu with more than 26 options.')
773
774 #calculate total height for the header (after auto-wrap) and one line per option
775 header_height = libtcod.console_get_height_rect(0, 0, 0, width, SCREEN_HEIGHT, header)
776 if header == '':
777 header_height = 0
778 height = len(options) + header_height + 2
779
780 #create an off-screen console that represents the menu's window
781 terminal.layer(2)
782 terminal.clear_area(0,0,SCREEN_WIDTH,SCREEN_HEIGHT)
783 #cursors position
784 c_pos = 0
785
786 output = None
787
788 x = SCREEN_WIDTH/2 - width/2
789 y = SCREEN_HEIGHT/2 - height/2
790
791 while True:
792 terminal.print_(x+1,y, '[color=white]' + header)
793
794 #print all the options
795 h = header_height
796 letter_index = ord('a')
797 run = 0
798 for option_text in options:
799 text = option_text
800
801 if run == c_pos:
802 terminal.print_(x+1,h+y+1, '[color=yellow]' + text)
803
804 else:
805 terminal.print_(x+1,h+y+1, '[color=white]' + text)
806 h += 1
807 letter_index += 1
808 run += 1
809
810 #present the root console to the player and wait for a key-press
811 terminal.refresh()
812
813 key = terminal.read()
814 if key == terminal.TK_ESCAPE:
815 break
816 elif key == terminal.TK_UP or key == terminal.TK_KP_8:
817 c_pos -= 1
818 if c_pos < 0:
819 c_pos = len(options)-1
820
821 elif key == terminal.TK_DOWN or key == terminal.TK_KP_2:
822 c_pos += 1
823 if c_pos == len(options):
824 c_pos = 0
825
826 elif key == terminal.TK_ENTER:
827 #convert the ASCII code to an index; if it corresponds to an option, return it
828 index = c_pos
829 #if index >= 0 and index < len(options):
830 output = index
831 break
832
833 terminal.clear_area(0,0,SCREEN_WIDTH,SCREEN_HEIGHT)
834 terminal.layer(0)
835 return output
836
837def inventory_menu(header):
838 global fov_recompute
839 #show a menu with each item of the inventory as an option
840 if len(inventory) == 0:
841 options = ['Inventory is empty.']
842 else:
843 options = []
844 for item in inventory:
845 text = item.name
846 #show additional info, in case it's equipped
847 if item.equipment and item.equipment.is_equipped:
848 text = text + ' (on ' + item.equipment.slot + ')'
849 options.append(text)
850 #options = [item.name for item in inventory]
851 index = menu(header, options, INVENTORY_WIDTH)
852 #if an item was chosen, return it
853 if index is None or len(inventory) == 0: return None
854 return inventory[index].item
855
856 terminal.layer(6)
857 terminal.clear_area(0, 0, MAP_WIDTH, MAP_HEIGHT)
858 fov_recompute = True
859
860def msgbox(text, width=50):
861 menu(text, [], width) #use menu() as a sort of "message box"
862
863def handle_keys():
864 terminal.set('window.resizeable=true')
865 global fov_recompute
866 # Read keyboard input
867 global key
868
869 key = terminal.read()
870 if key == terminal.TK_ESCAPE:
871 # Close terminal
872 return 'exit'
873 if game_state == 'playing':
874 #??? it was 'exit()'
875 a = 'player moved'
876 if key == terminal.TK_KP_2 or key == terminal.TK_DOWN:
877 player_move_or_attack(0, 1)
878 return a
879 elif key == terminal.TK_KP_8 or key == terminal.TK_UP:
880 player_move_or_attack(0, -1)
881 return a
882 elif key == terminal.TK_KP_6 or key == terminal.TK_RIGHT:
883 player_move_or_attack(1, 0)
884 return a
885 elif key == terminal.TK_KP_4 or key == terminal.TK_LEFT:
886 player_move_or_attack(-1, 0)
887 return a
888 elif key == terminal.TK_KP_7:
889 player_move_or_attack(-1, -1)
890 return a
891 elif key == terminal.TK_KP_9:
892 player_move_or_attack(1, -1)
893 return a
894 elif key == terminal.TK_KP_1:
895 player_move_or_attack(-1, 1)
896 return a
897 elif key == terminal.TK_KP_3:
898 player_move_or_attack(1, 1)
899 return a
900 elif key == terminal.TK_KP_5:
901 return a
902 else: #test for other keys
903 if key == terminal.TK_G:
904 #pick up an item
905 for object in objects: #look for an item in the player's tile
906 if object.x == player.x and object.y == player.y and object.item:
907 object.item.pick_up()
908 break
909
910 if key == terminal.TK_I:
911 #show the inventory; if an item is selected, use it
912 chosen_item = inventory_menu('Press the key next to an item to use it, or any other to cancel.\n')
913 if chosen_item is not None:
914 chosen_item.use()
915
916 if key == terminal.TK_D:
917 #show the inventory; if an item is selected, drop it
918 chosen_item = inventory_menu('Press the key next to an item to drop it, or any other to cancel.\n')
919 if chosen_item is not None:
920 chosen_item.drop()
921
922 if key == terminal.TK_C:
923 #show character info
924 level_up_xp = LEVEL_UP_BASE + player.level * LEVEL_UP_FACTOR
925 msgbox('Character Information\n\nLevel: ' + str(player.level) +
926 '\nExperience: ' + str(player.fighter.xp) + ' / ' + str(level_up_xp) +
927 '\n\nMaximum HP: ' + str(player.fighter.max_hp) +
928 '\nAttack: ' + str(player.fighter.power) + '\nDefense: ' + str(player.fighter.defense), CHARACTER_SCREEN_WIDTH)
929
930 if key == terminal.TK_SHIFT:
931 key = terminal.read()
932 if key == terminal.TK_PERIOD and stairs.x == player.x and stairs.y == player.y:
933 #go down stairs, if the player is on them
934 next_level()
935
936 key = terminal.read()
937 if key == terminal.TK_RESIZED:
938 new_columns = terminal.state(terminal.TK_WIDTH)
939 new_rows = terminal.state(terminal.TK_HEIGHT)
940
941
942 return 'didnt-take-turn'
943
944def check_level_up():
945 #see if the player's experience is enough to level up
946 level_up_xp = LEVEL_UP_BASE + player.level * LEVEL_UP_FACTOR
947 if player.fighter.xp >= level_up_xp:
948 #it is! level up
949 player.level += 1
950 player.fighter.xp -= level_up_xp
951 message('Your battle skills have improved! You reached level ' + str(player.level) + '!', 4294967040)
952 choice = None
953 while choice == None:
954 choice = menu('Level up! Choose a stat to raise:\n',
955 ['Constitution (+20 HP, from ' + str(player.fighter.max_hp) + ')',
956 'Strength (+1 attack, from ' + str(player.fighter.power) + ')',
957 'Agility (+1 defense, from ' + str(player.fighter.defense) + ')'], LEVEL_SCREEN_WIDTH)
958 if choice == 0:
959 player.fighter.base_max_hp += 20
960 player.fighter.hp += 20
961
962 elif choice == 1:
963 player.fighter.base_power += 1
964
965 elif choice == 2:
966 player.fighter.base_defense += 1
967
968 #clear level menu before refresh
969 terminal.layer(6)
970 terminal.clear_area(0, 0, MAP_WIDTH, MAP_HEIGHT)
971
972def player_death(player):
973 #the game ended!
974 global game_state
975 message('You died!', 4294901760)
976 game_state = 'dead'
977
978 #for added effect, transform the player into a corpse!
979 player.char = 22
980
981def monster_death(monster):
982 #transform it into a nasty corpse! it doesn't block, can't be
983 #attacked and doesn't move
984 message(monster.name.capitalize() + ' is dead! You gain ' + str(monster.fighter.xp) + 'xp.', 4294901760)
985 monster.char = 22
986 monster.blocks = False
987 monster.fighter = None
988 monster.ai = None
989 monster.name = 'remains of ' + monster.name
990 monster.send_to_back()
991 #clearing monster sprite from its layer, since corpse is not 'fighter' type
992 terminal.layer(4)
993 terminal.clear_area(monster.x, monster.y, 1, 1)
994
995#&&&&&&&&&&&&&&&TARGETTING TYPES&&&&&&&&&&&&&&&&&&&&
996def target_tile(max_range=None):
997 global key
998 #return the position of a tile left-clicked in player's FOV (optionally in a range), or (None,None) if right-clicked.
999 while True:
1000 #render the screen. this erases the inventory and shows the names of objects under the mouse.
1001 terminal.refresh()
1002 key = terminal.read()
1003 render_all()
1004 (x, y) = (terminal.state(terminal.TK_MOUSE_X), terminal.state(terminal.TK_MOUSE_Y))
1005
1006 if key == terminal.TK_MOUSE_RIGHT or key == terminal.TK_ESCAPE:
1007 message('Cancelled')
1008 return (None, None) #cancel if the player right-clicked or pressed Escape
1009
1010 #accept the target if the player clicked in FOV, and in case a range is specified, if it's in that range
1011 if (key == terminal.TK_MOUSE_LEFT and libtcod.map_is_in_fov(fov_map, x, y) and
1012 (max_range is None or player.distance(x, y) <= max_range)):
1013 return (x, y)
1014
1015def target_monster(max_range=None):
1016 #returns a clicked monster inside FOV up to a range, or None if right-clicked
1017 while True:
1018 (x, y) = target_tile(max_range)
1019 if x is None: #player cancelled
1020 return None
1021
1022 #return the first clicked monster, otherwise continue looping
1023 for obj in objects:
1024 if obj.x == x and obj.y == y and obj.fighter and obj != player:
1025 return obj
1026
1027def closest_monster(max_range):
1028 #find closest enemy, up to a maximum range, and in the player's FOV
1029 closest_enemy = None
1030 closest_dist = max_range + 1 #start with (slightly more than) maximum range
1031
1032 for object in objects:
1033 if object.fighter and not object == player and libtcod.map_is_in_fov(fov_map, object.x, object.y):
1034 #calculate distance between this object and the player
1035 dist = player.distance_to(object)
1036 if dist < closest_dist: #it's closer, so remember it
1037 closest_enemy = object
1038 closest_dist = dist
1039 return closest_enemy
1040#&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
1041
1042#################SPELL TYPES#########################
1043def cast_heal():
1044 #heal the player
1045 if player.fighter.hp == player.fighter.max_hp:
1046 message('You are already at full health.', 'red')
1047 return 'cancelled'
1048
1049 message('Your wounds start to feel better!', 'blue')
1050 player.fighter.heal(HEAL_AMOUNT)
1051
1052def cast_lightning():
1053 #find closest enemy (inside a maximum range) and damage it
1054 monster = closest_monster(LIGHTNING_RANGE)
1055 if monster is None: #no enemy found within maximum range
1056 message('No enemy is close enough to strike.', 'red')
1057 return 'cancelled'
1058
1059 #zap it!
1060 message('A lighting bolt strikes the ' + monster.name + ' with a loud thunder! The damage is '
1061 + str(LIGHTNING_DAMAGE) + ' hit points.', 'blue')
1062 monster.fighter.take_damage(LIGHTNING_DAMAGE)
1063
1064def cast_fireball():
1065 #ask the player for a target tile to throw a fireball at
1066 message('Left-click a target tile for the fireball, or right-click to cancel.', 'blue')
1067 (x, y) = target_tile()
1068 if x is None: return 'cancelled'
1069 message('The fireball explodes, burning everything within ' + str(FIREBALL_RADIUS) + ' tiles!', 'yellow')
1070
1071 for obj in objects: #damage every fighter in range, including the player
1072 if obj.distance(x, y) <= FIREBALL_RADIUS and obj.fighter:
1073 message('The ' + obj.name + ' gets burned for ' + str(FIREBALL_DAMAGE) + ' hit points.', 'yellow')
1074 obj.fighter.take_damage(FIREBALL_DAMAGE)
1075
1076def cast_confuse():
1077 #ask the player for a target to confuse
1078 message('Left-click an enemy to confuse it, or right-click to cancel.', 'blue')
1079 monster = target_monster(CONFUSE_RANGE)
1080 if monster is None: return 'cancelled'
1081
1082 #replace the monster's AI with a "confused" one; after some turns it will restore the old AI
1083 old_ai = monster.ai
1084 monster.ai = ConfusedMonster(old_ai)
1085 monster.ai.owner = monster #tell the new component who owns it
1086 message('The eyes of the ' + monster.name + ' look vacant, as he starts to stumble around!', 'blue')
1087#####################################################
1088
1089#%%%%%%%%%%%%%%%MENU FUNCTIONS%%%%%%%%%%%%%%%%%%%%%%%
1090def save_game():
1091 #open a new empty shelve (possibly overwriting an old one) to write the game data
1092 file = shelve.open('savegame', 'n')
1093 file['map'] = map
1094 file['objects'] = objects
1095 file['player_index'] = objects.index(player) #index of player in objects list
1096 file['inventory'] = inventory
1097 file['game_msgs'] = game_msgs
1098 file['game_state'] = game_state
1099 file['stairs_index'] = objects.index(stairs)
1100 file['dungeon_level'] = dungeon_level
1101 file.close()
1102
1103def load_game():
1104 #open the previously saved shelve and load the game data
1105 global map, objects, player, stairs, inventory, game_msgs, game_state, dungeon_level
1106
1107 file = shelve.open('savegame', 'r')
1108 map = file['map']
1109 objects = file['objects']
1110 player = objects[file['player_index']] #get index of player in objects list and access it
1111 stairs = objects[file['stairs_index']] #same for the stairs
1112 inventory = file['inventory']
1113 game_msgs = file['game_msgs']
1114 game_state = file['game_state']
1115 dungeon_level = file['dungeon_level']
1116 file.close()
1117 terminal.clear()
1118 initialize_fov()
1119
1120def new_game():
1121 global player, inventory, game_msgs, game_state, dungeon_level
1122
1123 #create object representing the player
1124 fighter_component = Fighter(hp=100, defense=1, power=2, xp=0, death_function=player_death)
1125 player = Object(0, 0, 17, 'player', blocks=True, fighter=fighter_component)
1126
1127 player.level = 1
1128
1129 #generate map (at this point it's not drawn to the screen)
1130 dungeon_level = 1
1131 make_map()
1132 initialize_fov()
1133
1134 game_state = 'playing'
1135 inventory = []
1136
1137 #create the list of game messages and their colors, starts empty
1138 game_msgs = []
1139
1140 #a warm welcoming message!
1141 message('You rise, your hair matted with blood, dazed and confused as to where you are.', 'red')
1142
1143 #initial equipment: a loincloth
1144 equipment_component = Equipment(slot='right hand')
1145 obj = Object(0, 0, '-', 'dagger', 'blue', equipment=equipment_component)
1146 inventory.append(obj)
1147 equipment_component.equip()
1148 obj.always_visible = True
1149
1150def next_level():
1151 #advance to the next level
1152 global dungeon_level
1153 message('You take a moment to rest, and recover your strength.', 'blue')
1154 player.fighter.heal(player.fighter.max_hp / 2) #heal the player by 50%
1155
1156 dungeon_level += 1
1157 message('After a rare moment of peace, you descend deeper into the heart of the dungeon...', 'red')
1158 make_map() #create a fresh new level!
1159 initialize_fov()
1160
1161def initialize_fov():
1162 global fov_recompute, fov_map
1163 fov_recompute = True
1164
1165 #create the FOV map, according to the generated map
1166 fov_map = libtcod.map_new(MAP_WIDTH, MAP_HEIGHT)
1167 for y in range(MAP_HEIGHT):
1168 for x in range(MAP_WIDTH):
1169 libtcod.map_set_properties(fov_map, x, y, not map[x][y].block_sight, not map[x][y].blocked)
1170
1171def play_game():
1172 player_action = None
1173
1174 #main loop
1175 while True:
1176 render_all()
1177 check_level_up()
1178
1179 #terminal.layer(5)
1180 #terminal.put(10, 10, 24) #place individual tile slime: 160:, troll=162: .
1181 terminal.refresh()
1182
1183 for object in objects:
1184 object.clear()
1185
1186 player_action = handle_keys()
1187 if player_action == 'exit':
1188 save_game()
1189 break
1190
1191 #let monsters take their turn
1192 if game_state == 'playing' and player_action != 'didnt-take-turn':
1193 for object in objects:
1194 if object.ai:
1195 object.ai.take_turn()
1196
1197def main_menu():
1198
1199 while True:
1200 terminal.clear()
1201 for y in range(50):
1202 for x in range(80):
1203 terminal.color(1678238975 - x)
1204 terminal.put(x, y, 20)
1205
1206 terminal.refresh()
1207
1208 #show the game's title and some credits
1209 terminal.layer(6)
1210 terminal.color(4294967103)
1211 terminal.print_(SCREEN_WIDTH/2 - 15, SCREEN_HEIGHT/2-8, 'GRIT AND STEEL: ATLANTIS RISING')
1212 terminal.print_(SCREEN_WIDTH/2, SCREEN_HEIGHT-2, 'By KM')
1213
1214 #show options and wait for player's choice
1215 choice = menu('', ['Play a new game', 'Continue last game', 'Save & Quit'], 24)
1216
1217 if choice == 0: #new game
1218 terminal.clear()
1219 new_game()
1220 play_game()
1221 if choice == 1: #load last game
1222 try:
1223 load_game()
1224 except:
1225 msgbox('\n No saved game to load.\n', 24)
1226 continue
1227 play_game()
1228 elif choice == 2: #quit
1229
1230 terminal.close()
1231 break
1232
1233#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1234
1235#####################################################
1236# Initialization & Main Loop
1237#####################################################
1238
1239#the new starting sequence
1240terminal.open()
1241terminal.set("window: size=" + str(SCREEN_WIDTH) + "x" + str(SCREEN_HEIGHT) + "; window.title='Grit and Steel: Atlantis Rising'; font: tiles.png, size=16x16; input: filter=[keyboard, mouse_left]")
1242
1243main_menu()