def cast_fireball(params=(10, 3)): '''ask the player for a target tile to throw a fireball at''' pwr, radius = params target = target_tile() if target is None: Message('Your spell fizzles.') else: x, y = target check = True # Choice is True by default, but can be set to False if the player decided not to hit him-/herself if gv.player.distance_to_coord(x, y) <= radius: check = menu('The spell would hit you as well. Proceed?', ['No', 'Yes'], 40) if check: Message( 'The fireball explodes, burning everything within {} tiles!'. format(str(radius)), colors.orange) for obj in gv.actors: #damage every actor in range, including the player if obj.distance_to_coord(x, y) <= radius: dmg = randint(pwr / 2, pwr) Message( 'The {} gets burned for {} hit points.'.format( obj.name, str(dmg)), colors.orange) obj.take_damage(dmg) else: Message('Your spell fizzles.')
def change_level(interacting_ent, arch_entity, game): results = [] if interacting_ent.pos != arch_entity.pos: results = [{ 'message': Message( f'{interacting_ent.address_colored.title()} need to be on top of {arch_entity.address_colored}.', category=MessageCategory.OBSERVATION) }] elif arch_entity.char == '<': # up results = [ { 'level_change': -1 }, ] elif arch_entity.char == '>': # down results = [ { 'level_change': 1 }, ] elif arch_entity.char == '0': results = [{ 'message': Message(f'PLACEHOLDER: You leave the Dungeon.', category=MessageCategory.OBSERVATION, type=MessageType.SYSTEM) }] return results
def move(self): ''' Confused monsters stumble around and attack randomly ''' x, y = self.owner.x, self.owner.y dx, dy = randint(-1, 1), randint(-1, 1) to_x, to_y = x + dx, y + dy if gv.game_map.walkable[to_x][to_y]: check = True for obj in gv.gameobjects: target = None if [obj.x, obj.y] == [ to_x, to_y ] and obj.blocks: # check if there is something in the way check = False if obj in gv.actors: target = obj break if check and target is None: self.owner.x += dx self.owner.y += dy Message('The ' + self.owner.name + ' stumbles around.', colors.white) # if blocking object is an enemy target elif not check and target: self.owner.attack(target) else: Message('The ' + self.owner.name + ' bumbs into a wall.', colors.white)
def cast_heal(params=0): '''heal the player''' hp = params if gv.player.hp == gv.player.max_hp: Message('You are already at full health.', colors.red) return 'cancelled' Message('Your wounds start to feel better!', colors.light_violet) gv.player.heal(hp)
def cast_powerup(params=0): '''modify characters power''' pwr = params if pwr: Message('Your power has been increased!', colors.light_violet) else: Message('The potion of power was cursed!', colors.light_violet) gv.player.modpwr(pwr)
def attack(self, target): '''a simple formula for attack damage''' damage = self.power - target.defense if damage: #make the target take some damage Message('{} attacks {} for {} hit points.'.format(self.name.capitalize(), target.name, str(damage)), log=gv.combat_log) target.take_damage(damage) else: Message('{} attacks {} but it has no effect!'.format(self.name.capitalize(), target.name), log=gv.combat_log)
def cast_magicmissile(params=(10, 3)): '''ask the player for a target tile to throw a magic missile at it''' pwr, radius = params target = target_tile() monster = next(obj for obj in gv.actors if (obj.x, obj.y) == target) if not monster: # if no actor is at the selected location, the spell fails Message('There is no target at the position and your spell fizzles.') else: Message( 'Your magical projectile hits the {} for {} damage!'.format( monster.name, str(pwr)), colors.turquoise) monster.take_damage(pwr)
def move(self, dx, dy, running=False): ''' Move the player, after checking if the target space is legitimate''' running = running if gv.game_map.walkable[self.x + dx][self.y + dy]: check = True for obj in gv.gameobjects: target = None # check if there is something in the way if [obj.x, obj.y] == [self.x + dx, self.y + dy] and obj.blocks: check = False if obj in gv.actors and (not obj == gv.player): # if it's another actor, target it target = obj break if check and target is None: self.x += dx self.y += dy # if entity is running, re-call the move function once if (running): self.move(dx, dy, running=False) # if blocking object is an enemy target elif not check and target not is None: if running: Message('You bump into the {}.'.format(target.name), colors.red) self.is_running = False else: self.attack(target)
def process_cursor_interaction(game, action, targeting_item, debug_spawn): results = [] player = game.player cursor = game.cursor fov_map = game.fov_map move = action.get('move') direction = action.get('dir') exit = action.get('exit') confirm = action.get('confirm') if move: dx, dy = direction destination_x = cursor.x + dx destination_y = cursor.y + dy if fov_map.fov[destination_y, destination_x]: if game.state == GameState.CURSOR_TARGETING: if player.active_weapon_is_ranged and targeting_item is None: # If player is aiming with a ranged weapon dist = player.distance_to_pos(destination_x, destination_y) if dist == 0 or dist in range(*player.active_weapon.attack_range): cursor.try_move(dx, dy, game, ignore_entities=True) elif targeting_item is not None and player.distance_to_pos(destination_x, destination_y) in\ range(*targeting_item.item.useable.on_use_params.get('range', 1)): # If player is aiming with an item cursor.try_move(dx, dy, game, ignore_entities=True) else: cursor.move(dx, dy) if confirm: if targeting_item is not None: # Targeting with an item inv = player.inventory if targeting_item in player.inventory else player.qu_inventory item_use_results = inv.use(targeting_item, game, target_pos=cursor.pos) results.extend(item_use_results) elif debug_spawn is not None: # DEBUG MENU # success = gen_entity_at_pos(debug_spawn, cursor.pos, game) if not success: results.append({'message':Message('Illegal position', type=MessageType.SYSTEM)}) elif player.active_weapon_is_ranged: # Using a ranged weapon # target = entity_at_pos(game.blocking_ents, *cursor.pos) ranged_attack_results = player.f.ranged_attack(cursor.pos, game, draw_projectile=True) results.extend(ranged_attack_results) # result = animate_projectile(*player.pos, *cursor.pos, game, color=colors.beige) # if isinstance(result, Entity) is not None and result.fighter is not None: # results.extend(player.f.attack_setup(result, game)) # else: # player.f.exert() # results.append({'ranged_attack': True}) # todo refactor: always end up attacking # if target is not None and target.fighter is not None: # attack_results = player.f.attack_setup(target, game) # results.extend(attack_results) # else: # # todo return entity if it connects with one; then execute attack -> refactor as if/else is no longer required then # animate_projectile(*player.pos, *cursor.pos, game, color=colors.beige) # TODO ranged projectile color can later differ by weapon/ammo type results.append({'ranged_attack': True}) if exit and (targeting_item or player.active_weapon_is_ranged): results.append({'targeting_cancelled': True}) return results
def target_tile(): '''Display a targeting cursor''' Message( 'Select a target by moving your cursor. Enter to confirm the target, press ESC to cancel.' ) if gv.gamestate is not GameStates.CURSOR_ACTIVE: # If the player-state is not yet targeting, enable it and create the cursor gv.gamestate = GameStates.CURSOR_ACTIVE gv.cursor.activate('X', colors.red) while gv.gamestate == GameStates.CURSOR_ACTIVE: # While the player is considered targeting, suspend game-play to control the cursor and get a target # update the screen render_all() tdl.flush() player_action = handle_keys(tdl.event.key_wait()) if player_action is not None: if 'move' in player_action: # if key is a movement key process input as normal (will move the cursor) process_input(player_action) elif 'confirm' in player_action: # if enter was pressed, return the coordinates of the cursor gv.gamestate = GameStates.ENEMY_TURN gv.cursor.deactivate() return ( gv.cursor.x, gv.cursor.y ) #Return the cursor's current coordinates to the calling function elif 'cancel' in player_action: gv.gamestate = GameStates.ENEMY_TURN gv.cursor.deactivate() break
def drop(self): '''add to the map and remove from the gv.player's gv.player.inventory. also, place it at the gv.player's coordinates''' gv.gameobjects.append(self) gv.player.inventory.remove(self) self.x = gv.player.x self.y = gv.player.y Message('You dropped ' + self.name.title() + '.', colors.yellow)
def dequip(self, item_ent): results = [] e_to = item_ent.item.equipment.e_to e_type = item_ent.type.name.lower() qu_slots = item_ent.item.equipment.qu_slots extremity = self.get_corresponding_extremity(e_to) equipped_item = getattr(extremity, e_type) if equipped_item: if qu_slots: # TODO make sure items over capacity are moved to regular inventory self.owner.qu_inventory.capacity -= qu_slots # Disable active weapon if necessary if equipped_item == self.owner.f.active_weapon: self.owner.f.active_weapon = None # Disable blocking if necessary if self.owner.f.is_blocking and e_type == 'shield': self.owner.f.is_blocking = False setattr(extremity, e_type, None) self.owner.inventory.add(equipped_item) results.append({'item_dequipped': item_ent, 'message': Message(f'You remove the {item_ent.name}.')}) else: logging.error('Trying to dequip something that is not equipped.') return results
def equip(self, item_ent, game=None): """ TODO: refactor so game param is no longer necessary: return conflicting item, let calling function handle menu :param item_ent: :type item_ent: :param game: only required if equip() is allowed to prompt window :type game: :return: :rtype: """ results = [] e_type = item_ent.type.name.lower() # Entity.type is a enum member of the ItemType Class. equip_ent = item_ent.item.equipment e_to = equip_ent.e_to qu_slots = equip_ent.qu_slots extremity = self.get_corresponding_extremity(e_to) equipped_item = getattr(extremity, e_type) offhand_item = self.shield_arm.carried # If new item is two-handed, check if shield arm is occupied # if equip_ent.two_handed_only and offhand_item and game is not None: choice = yesno_menu('Remove Offhand Item', f'Remove {offhand_item.name_colored} to equip the two-handed {item_ent.name_colored}?', game) if choice: results.extend(self.dequip(offhand_item)) else: return None # If new item is shield, check for two-handed weapons # if e_to == EquipTo.SHIELD_ARM and self.two_handed_weapons and game is not None: while (len(self.two_handed_weapons) > 0): # loop is required, as both melee and ranged weapon could be two-handed choice = item_list_menu(self.owner, self.two_handed_weapons, game,'Remove Two-Handed Weapon', f'Remove which weapon to equip {item_ent.name_colored}?') if choice: results.extend(self.dequip(choice)) else: return None # After resolving conflicts for two-handed items, remove items occupying the same slot if equipped_item and game is not None: choice = yesno_menu('Remove Item',f'Unequip your {equipped_item.name_colored}?', game) if choice: results.extend(self.dequip(equipped_item)) else: return None # After resolving all conflicts, equip new item setattr(extremity, e_type, item_ent) self.owner.inventory.remove(item_ent) if qu_slots: self.owner.qu_inventory.capacity += qu_slots if self.owner.f.active_weapon is None and e_to == EquipTo.WEAPON_ARM: # If there is no active weapon, set the new weapon as active # self.owner.f.active_weapon = item_ent results.append({'item_equipped': item_ent, 'message': Message(f'You equip the {item_ent.name_colored}.')}) return results
def pick_up(self, actor): '''add to the gv.player's inventory and remove from the map''' if len(actor.inventory) >= 26: Message( 'Your gv.player.inventory is full, cannot pick up ' + self.name + '.', colors.red) else: actor.inventory.append(self) gv.gameobjects.remove(self)
def take_turn(self): if self.num_turns: #still confused... #move in a random direction, and decrease the number of turns confused self.move() self.num_turns -= 1 else: #restore the previous AI (this one will be deleted because it's not referenced anymore) self.owner.ai = self.old_ai Message('The ' + self.owner.name + ' is no longer confused!', colors.red)
def cast_lightning(params=(0, 0)): '''zap something''' pwr, spell_range = params #find closest enemy (inside a maximum range) and damage it if spell_range: monster = closest_monster(spell_range) if monster is None: #no enemy found within maximum range Message('No enemy is close enough to strike.', colors.red) return 'cancelled' else: monster = gv.player Message('The scroll of lightning was cursed!', colors.light_violet) #zap it! Message( 'A lighting bolt strikes the {} with a loud thunder! The damage is {} hit points.' .format(monster.name, str(pwr)), colors.light_blue) monster.take_damage(pwr)
def cast_confusion(params=(6, 3)): '''find closest enemy in-range and confuse it''' #TODO: Make confused monster attack random monsters dur, spell_range = params monster = None if spell_range: monster = closest_monster(spell_range) if monster is None: #no enemy found within maximum range Message('No enemy is close enough to confuse.', colors.red) return 'cancelled' else: old_ai = monster.ai monster.ai = ConfusedMonster(old_ai, num_turns=dur) monster.ai.owner = monster #tell the new component who owns it Message( 'The eyes of the {} look vacant, as he starts to stumble around!' .format(monster.name), colors.light_green) else: Message('The scroll of confusion was cursed!') gv.player.confused = True # Placeholder
def use(self, game, user, **kwargs): item_entity = self.owner.owner results = [] if self.on_use_function is None: results.append({'message': Message(f'The {item_entity.name} cannot be used like this.', category=MessageCategory.OBSERVATION)}) else: if self.targeted and not game.state in [GameState.CURSOR_ACTIVE, GameState.CURSOR_TARGETING]: results.append({'targeting': item_entity,'message': Message('Move the cursor over the intended target, press Enter to confirm.')}) else: item_use_results = self.on_use_function(game=game, user=user, used_item=item_entity, projectile=self.projectile, **kwargs, **self.on_use_params, **self.on_use_effect) if self.on_use_msg: item_use_results.append({'message': Message(self.on_use_msg)}) results.extend(item_use_results) self.charges -= 1 if self.charges == 0: results.append({'consumed':item_entity}) return results
def open_container(interacting_ent, container_ent, game): results = [] # TODO locks & traps # display chest_contents if container_ent.inventory.is_empty: results.append({ 'message': Message(f'{container_ent.address_colored.title()} is empty.', category=MessageCategory.OBSERVATION) }) # TODO ability to put things into container else: selection = item_list_menu(container_ent, container_ent.inventory, game, title=f'{container_ent.name}') if selection: if not interacting_ent.inventory.is_full: results.append({ 'message': Message( f'{interacting_ent.address_colored.title()} take {selection.address_colored} from {container_ent.address_colored}.', category=MessageCategory.OBSERVATION) }) container_ent.inventory.remove(selection) interacting_ent.inventory.add(selection) else: results.append({ 'message': Message( f'{interacting_ent.possessive_colored.title()} inventory is full.', category=MessageCategory.OBSERVATION) }) if container_ent.inventory.is_empty and not '(e)' in container_ent.name: container_ent.name += ' (e)' container_ent.descr += '\n\nIt is empty.' return results
def execute(self, game: Game, **kwargs): user = self.owner game.map.gib_area(user.x, user.y, randint(2, 4), user.color_blood, chunks=True) msg1 = Message(f'{user.address_colored.title()} hatches!', type=type, category=MessageCategory.OBSERVATION) self.create_hatchling() results = [{'message': msg1}] return results
def add(self, item): results = [] if self.is_full: results.append({ 'item_added': None, 'message': Message('You cannot carry any more, your inventory is full.', category=MessageCategory.OBSERVATION) }) else: results.append({ 'item_added': item, 'message': Message(f'You pick up the {item.name_colored}.', category=MessageCategory.OBSERVATION) }) self.items.append(item) return results
def execute(self, target: Entity, game: Game, **kwargs): results = [] user = self.owner duration = kwargs.get('duration', 0) results.extend(target.f.set_effect(State.ENTANGLED, True, duration)) results.append({ 'message': Message(f'The {user.name} wraps itself around {target.name}!', category=MessageCategory.OBSERVATION, type=MessageType.COMBAT) }) return results
def gen_game(newgame): ''' sets up a new game ''' if newgame: # new game # reset other global variables gv.gameobjects = [] gv.actors = [] gv.game_log = MessageLog(settings.MSG_X, settings.MSG_WIDTH, settings.MSG_HEIGHT) gv.combat_log = MessageLog(settings.MSG_X, settings.MSG_WIDTH, settings.MSG_HEIGHT) gv.dungeon_level = 1 # create the player & cursor gv.player = gen_Player(0, 0) gv.player.inventory = [] gv.cursor = Cursor(0, 0) # Setup an initial inventory gen_inventory() # introductionary messages msgbox('Welcome stranger! Prepare to perish in {}.'.format( settings.DUNGEONNAME), width=35, text_color=colors.red) Message('Press ? to open the manual.', color=colors.green) else: # new dungeon level gv.dungeon_level += 1 # Increase the dungeon leavel by one for obj in gv.gameobjects: # Remove all old objects from the game if not obj in [gv.player, gv.cursor]: obj.delete() # Reset arrays containing enemies and objects gv.gameobjects = [gv.player, gv.cursor] gv.actors = [gv.player] msgbox('You are now on level {} of the {}'.format( gv.dungeon_level, settings.DUNGEONNAME), width=30, text_color=colors.red) # Generate a new map gen_map(settings.MAP_WIDTH, settings.MAP_HEIGHT) # Generate map content gen_map_content() # clear the old console gv.con.clear()
def prepare(self, item): results = [] inventory = self.owner.inventory qu_inventory = self.owner.qu_inventory if item in qu_inventory.items: if not inventory.is_full: qu_inventory.remove(item) inventory.add(item) results.append({ 'item_prepared': item, 'message': Message(f'You de-prepare the {item.name_colored}.') }) else: results.append({ 'message': Message( f'Your full inventory prevents you from de-preparing the {item.name_colored}.' ) }) elif len(qu_inventory) >= qu_inventory.capacity: results.append({ 'message': Message('You cannot prepare any more items.', category=MessageCategory.OBSERVATION) }) else: qu_inventory.add(item) inventory.remove(item) results.append({ 'item_prepared': item, 'message': Message(f'You prepare the {item.name}.') }) return results
def direct_damage(target = None, string='attack', ignore_def=False, **kwargs): # NOTE: Does currently not take any defenses into account. amount = randint(*kwargs.get('pwr')) if target is None: target = kwargs.get('user') results = [] results.append({'message': Message( f'The {string} causes %{target.f.hpdmg_color(amount)}%{target.f.hpdmg_string(amount)}%% damage to {target.address}!', category=MessageCategory.COMBAT, type=MessageType.COMBAT_INFO)}) results.extend(target.f.take_damage(amount)) return results
def execute(self, pos_list: list, game: Game, **kwargs): user = self.owner user.color_bg = None # Reset the entities bg-color, which the skill preparation had changed results = [] results.append({ 'message': Message(f'{user.address_colored.title()} charges forward!', category=MessageCategory.OBSERVATION, type=MessageType.COMBAT) }) # TODO attack_string defined in their own data file anim_completed = animate_move_to(user, *pos_list[-1], game) # If the animation was completed successfully, without hitting anything, do two more steps in the current direction, overshooting # the target. This is done here, rather than when creating the pos-list during the prep-phase, as adding the overshot too early # will result in a different movement pattern when moving the entity and could prompt unexpected collisions (as the free line of positions # was checked without the overshot ) if anim_completed is True: d_x, d_y = direction_between_pos(*pos_list[-2], *pos_list[-1]) tx, ty = pos_list[-1][0] + d_x + d_x, pos_list[-1][1] + d_y + d_y anim_completed = animate_move_to(user, tx, ty, game) if anim_completed is False: # if a wall is hit during the charge, damage the charging entity results.extend( user.f.attack_setup(user, game, dmg_mod_multipl=0.5, verb='hurt', ignore_moveset=True)) elif not isinstance( anim_completed, bool): # if missed is not bool, another entity was hit ent = anim_completed if ent.fighter is not None: # if another actor was hit, that actor is damaged results.extend( user.f.attack_setup(ent, game, dmg_mod_multipl=2, verb='gore', ignore_moveset=True)) results.extend(ent.f.set_effect(State.DAZED, True, 2)) elif ent.architecture is not None: # if architecture was hit, user damages themselves results.extend( user.f.attack_setup(user, game, dmg_mod_multipl=0.5, verb='ram', ignore_moveset=True)) return results
def direct_heal(**kwargs): target = kwargs.get('target') if target is None: target = kwargs.get('user') percentage = kwargs.get('percentage', False) if not percentage: amount = randint(*kwargs.get('pwr')) else: amount = target.f.max_hp/100 * randint(*kwargs.get('pwr')) if target is None: target = kwargs.get('user') results = [] if not target.f.hp_full: results.append({'message': Message( f'{target.name} heals for {target.f.hpdmg_string(amount)} effect!', category=MessageCategory.OBSERVATION, type=MessageType.COMBAT_INFO)}) target.f.heal(amount) else: results.append({'message': Message(f'{target.pronoun.title()} {target.state_verb_past} already at full health.')}) return results
def death(self): ''' Generic death for all actors ''' Message('The {} is dead!'.format(self.name.capitalize()), log=gv.combat_log, color=colors.green) x, y = (self.x, self.y) item = gen_Corpse(x, y, self) #item = Useable(self.x,self.y, (self.name + ' corpse'), '%', colors.dark_red,use_function=iu.eat_corpse,params=self.name) gv.game_map.gibbed[x][y] = True # Set the tile to 'gibbed' (will be rendered red) for i in range(1, randint(3, 5)): x, y = (randint(x - 1, x + 1), randint(y - 1, y + 1)) gv.game_map.gibbed[x][y] = True for i in range(1, randint(1, 3)): x, y = (randint(x - 2, x + 2), randint(y - 2, y + 2)) if randint(0, 100) < 40: item = gen_Corpsebits(x, y, self) #item = Useable(x,y,(self.name + ' bits'), '~', colors.darker_red,use_function=iu.eat_corpse,params=self.name) self.delete()
def drop(self, item): results = [] item.x, item.y = self.owner.x, self.owner.y if item in self.owner.paperdoll.equipped_items: self.owner.paperdoll.dequip(item) self.remove(item) results.append({ 'item_dropped': item, 'message': Message(f'You dropped the {item.name_colored}.') }) return results
def smash_object(interacting_ent, object_ent, game): message = f'{interacting_ent.address_colored.title()} smash {object_ent.address_colored}.' object_ent.char = '%' object_ent.color *= 0.3 object_ent.blocks[BlockLevel.WALK] = False object_ent.blocks[BlockLevel.SIGHT] = False object_ent.architecture = None # TODO not a very elegant solution to prevent rendering in the objects panel if object_ent.inventory: for i in object_ent.inventory: i.x, i.y = object_ent.x, object_ent.y game.entities.append(i) return [{ 'message': Message(message, category=MessageCategory.OBSERVATION) }]