You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

646 lines
23 KiB
Python

import os
import math
import random
import lib.libtcodpy as libtcod
import constants as C
def dice(sides):
return random.randint(0, sides) == 0
class Dialogue():
def __init__(self, npc_name, npc_picture, dialogue):
self.npc_name = npc_name
self.npc_picture = npc_picture
self.dialogue = dialogue
class ItemBase(object):
"""
Inanimate items (foliage, water, walls) and map tiles.
"""
def __init__(self
,char=" ", name=""
,fgcolor=libtcod.white
,bgcolor=libtcod.darker_green):
self.x = 0
self.y = 0
self.char = char
self.name = name
self.fgcolor = fgcolor
self.bgcolor = bgcolor
self.blocking = False
self.seethrough = True
self.seen = False
self.drinkable = False
self.edible = False
self.carryable = False
self.fov_limit = None
self.message = None
self.quest_id = None
self.tag = None
def isblank(self):
return not self.blocking and not self.drinkable
class ActionAI(object):
"""
Handles interaction with other beings.
"""
def __init__(self, owner):
self.owner = owner
self.hostile = False
self.dialogue_text = None
self.attack_rating = 0
def interact_with(self, target, game_objects):
npc = self.owner
if isinstance(target, Player):
player = target
if self.dialogue_text:
# show dialogue to the player, and if we have a quest item,
# only if player is seeking it.
show_dialogue = True
if npc.quest_ai:
if npc.quest_ai.item:
if not player.seeks_quest(npc.quest_ai.quest_id):
show_dialogue = False
if show_dialogue:
if type(self.dialogue_text) is list:
# this is a list of dialogues, talk our ear off
while len(self.dialogue_text) > 0:
player.add_dialogue(Dialogue(npc.name, npc.picture
, self.dialogue_text.pop()))
self.dialogue_text = None
else:
# a one-liner dialogue
player.add_dialogue(Dialogue(npc.name, npc.picture
, self.dialogue_text))
self.dialogue_text = None
if self.hostile:
# enact some hostility
player.take_damage(npc, self.attack_rating)
player.msg("%s %c*bites*%c you!" % (npc.name, C.COL1, C.COLS))
if npc.see_message:
# now that we interacted, get rid of our see_message
npc.see_message = None
if npc.tag:
if npc.tag == "puppy":
npc.move_ai.behaviour = MoveAI.FRIENDLY
class ActionManual(ActionAI):
"""
The Action attribute for the player
"""
def __init__(self, owner):
self.owner = owner
def interact_with(self, target, game_objects):
player = self.owner
if isinstance(target, AnimalBase):
# engage
if target.action_ai:
if target.action_ai.hostile:
if player.weak:
# move away from the hostile
player.msg("%cYou are too weak to bite%c" % \
(C.COL2, C.COLS))
return False
else:
target.take_damage(player, self.attack_rating)
player.bites_taken = player.bites_taken + 1
player.msg("you %c*bite*%c the %s" % \
(C.COL1, C.COLS, target.name))
else:
player.msg("*sniffs* %s" % (target.name))
# let them have a go
if target.action_ai:
target.action_ai.interact_with(player, game_objects)
if target.quest_ai:
target.quest_ai.interact_with(player, game_objects)
else:
# action on inanimates, these are not tiles
# but items in game_objects that are not AnimalBase
player.msg("*sniffs* %s" % (target.name))
class MoveAI(object):
"""
Handles NPC movement.
SKITTISH: keeps its distance
NEUTRAL: indifferent
FRIENDLY: follows the player at random times
HUNTING: actively persues the player
"""
SKITTISH = 0x0
NEUTRAL = 0x1
FRIENDLY = 0x2
HUNTING = 0x3
def __init__(self, owner):
self.owner = owner
self.behaviour = None
self.erraticity = 10
self.prey_x = 0
self.prey_y = 0
def take_turn(self, game_map, fov_map, path_map, game_objects, playerxy):
npc = self.owner
x, y = (0, 0)
if self.behaviour == MoveAI.SKITTISH:
if dice(2):
npc.move(game_map, game_objects, random.randint(-1, 1), random.randint(-1, 1))
else:
x, y = playerxy
libtcod.map_compute_fov(fov_map, npc.x, npc.y, npc.fov_radius
,C.FOV_LIGHT_WALLS, C.FOV_ALGO)
if libtcod.map_is_in_fov(fov_map, x, y):
# player in sight!
if libtcod.path_compute(path_map, npc.x, npc.y, x, y):
x, y = libtcod.path_walk(path_map, True)
if not x is None:
npc.move(game_map, game_objects, npc.x - x, npc.y - y)
elif self.behaviour == MoveAI.NEUTRAL:
if dice(self.erraticity):
x = random.randint(-1, 1)
if dice(self.erraticity):
y = random.randint(-1, 1)
npc.move(game_map, game_objects, x, y)
elif self.behaviour == MoveAI.FRIENDLY:
x, y = playerxy
# player in sight!
if libtcod.path_compute(path_map, npc.x, npc.y, x, y):
# stick a little bit away
if libtcod.path_size(path_map) > 3:
x, y = libtcod.path_walk(path_map, True)
if not x is None:
npc.move(game_map, game_objects, x - npc.x, y - npc.y)
elif self.behaviour == MoveAI.HUNTING:
# look for prey
x, y = playerxy
libtcod.map_compute_fov(fov_map, npc.x, npc.y, npc.fov_radius
,C.FOV_LIGHT_WALLS, C.FOV_ALGO)
if libtcod.map_is_in_fov(fov_map, x, y):
# player in sight!
if libtcod.path_compute(path_map, npc.x, npc.y, x, y):
self.prey_x = x
self.prey_y = y
x, y = libtcod.path_walk(path_map, True)
if not x is None:
npc.move(game_map, game_objects, x - npc.x, y - npc.y)
else:
# prowl last know prey location
if libtcod.path_compute(path_map, npc.x, npc.y
,self.prey_x, self.prey_y):
x, y = libtcod.path_walk(path_map, True)
if not x is None:
npc.move(game_map, game_objects, x - npc.x, y - npc.y)
class QuestAI(object):
"""
Quest AI for NPC's.
"""
def __init__(self, quest_id=os.urandom(4)):
self.quest_id = quest_id
self.owner = None
self.item = None
self.title = None
self.success_dialogue = None
self.success_command = None
def end_quest(self):
self.owner.quest_ai = None
def interact_with(self, target, game_objects):
if isinstance(target, Player):
player = target
npc = self.owner
if self.item:
# this is the item carrier
if npc.action_ai.hostile:
# we must fight for the item. drop it if we scattered
if npc.move_ai.behaviour == MoveAI.SKITTISH:
# only drop item if the player has this as a quest!
if player.seeks_quest(self.quest_id):
target.msg("%s %c*drops*%c something" % (npc.name, C.COL3, C.COLS))
self.item.x = npc.x
self.item.y = npc.y
game_objects.append(self.item)
# no more quest for this npc
self.end_quest()
else:
if player.seeks_quest(self.quest_id):
# give it out for free
player.msg("%s %c*gives*%c you a %s" % \
(npc.name, C.COL3, C.COLS, self.item.name))
game_objects.append(self.item)
player.give_item(self.item)
# no more quest for this npc
self.end_quest()
else:
# this is the quest giver.
if player.has_quest_item(self.quest_id):
# player has our prized posession
if self.success_command:
# reward player
exec(self.success_command)
# say thanks
player.addscore(10)
player.remove_inventory()
player.remove_quest(self.quest_id)
while len(self.success_dialogue) > 0:
player.add_dialogue(Dialogue(npc.name, npc.picture
, self.success_dialogue.pop()))
# no more quest for this npc
self.end_quest()
elif not player.seeks_quest(self.quest_id):
# give the player a quest
player.give_quest(QuestData(self.quest_id, npc.name, self.title))
class QuestData(object):
"""
quest data conainer kept by player.quests[]
"""
def __init__(self, quest_id, npc_name=None, title=None):
self.quest_id = quest_id
self.npc_name = npc_name
self.title = title
class AnimalBase(object):
"""
Living things (NPC's) and the Player.
"""
def __init__(self):
self.x = 0
self.y = 0
self.hp = 100
self.char = "?"
self.name = "?"
self.fgcolor = None
self.seen = False
self.blocking = True
self.see_message = None
self.moves = 0
self.move_step = 1
self.carryable = False
self.carrying = None
self.fov_radius = C.FOV_RADIUS_DEFAULT
self.move_ai = None
self.action_ai = None
self.quest_ai = None
self.quest = None
self.flying = False
self.picture = None
self.tag = None #HACK: last minute, to check if we exit the level with puppy :p
def take_damage(self, attacker, damage):
self.hp = self.hp - damage
if self.hp < 0:
if self.move_ai:
if isinstance(attacker, Player):
attacker.msg("%s %c*flees*%c" % (self.name, C.COL2, C.COLS))
self.move_ai.behaviour = MoveAI.SKITTISH
def take_turn(self):
if self.move_ai:
self.move_ai.take_turn()
def move(self, game_map, game_objects, xoffset, yoffset):
"""
Move to the given xy offset if non blocking and not in deep water.
Return True if so.
"""
if xoffset == 0 and yoffset == 0:
return True
x = self.x + xoffset
y = self.y + yoffset
# test if within the map bounds, and no tiles block us
if x >= 0 and x < C.MAP_WIDTH and y >= 0 and y < C.MAP_HEIGHT:
tile = game_map[x][y]
# cant move into a drinkable tile if already on one
near_deep_water = tile.drinkable and \
game_map[self.x][self.y].drinkable
if not tile.blocking and not near_deep_water or self.flying:
# test if moving against another being
blocking_us = False
for being in game_objects:
if being.x == x and being.y == y and being.blocking:
blocking_us = True
if self.action_ai:
self.action_ai.interact_with(being, game_objects)
if self.quest_ai:
self.quest_ai.interact_with(being, game_objects)
return True
if not blocking_us:
self.moves = self.moves + 1
# but if we are little slow, we may need to way for next
# turn to move :p
if self.moves % self.move_step == 0:
self.x = x
self.y = y
return True
def get_xy_towards(self, x, y):
"""
Get the XY for moving towards the given location.
"""
dx = x - self.x
dy = y - self.y
distance = math.sqrt(dx ** 2 + dy ** 2)
# normalize to length 1 keeping direction
dx = int(round(dx / distance))
dy = int(round(dy / distance))
return (dx, dy)
class Player(AnimalBase):
"""
Tracks the player state and provides helper functions
"""
def __init__(self):
super(Player, self).__init__()
self.x = 1
self.y = 1
self.char = "@"
self.name = "topdog"
self.fgcolor = libtcod.white
self.blocking = True
self.weak = False
self.thirsty = False
self.hungry = False
self.mustpiddle = False
self.quenches = 0
self.level = 0
self.score = 0
self.message_trim_idx = 0
self.messages = []
self.seen = True
self.wizard = False
self.dialogues = []
self.quests = []
self.piddles_taken = 0
self.bites_taken = 0
self.treats_eaten = 0
def addscore(self, value):
self.score = self.score + 10
def heal(self, hp):
""" heal by the given hp """
self.hp = self.hp + hp
if selp.hp > 100:
self.hp = 100
def inventory_name(self, prefix=""):
if self.carrying:
if self.carrying.quest_id:
return "%s%c%s%c" % (prefix, C.COL4, self.carrying.name, C.COLS)
elif self.carrying.edible:
return "%s%c%s%c" % (prefix, C.COL5, self.carrying.name, C.COLS)
else:
return "%s%s" % (prefix, self.carrying.name)
return ""
def remove_inventory(self):
self.carrying = None
def eat_item(self):
if self.carrying:
if self.carrying.edible:
self.hp = self.hp + 25.0
if self.hp > 100:
self.hp = 100
self.carrying = None
self.treats_eaten = self.treats_eaten + 1
self.msg(random.choice(("Yum!", "*munch munch*", "*gulp*", "*chomp chomp*")))
if self.weak:
self.weak = False
self.msg("You don't feel weak anymore.")
else:
self.msg("You chew on %s" % (self.carrying.name))
def give_item(self, item):
"""
give player an inventory item, drops items if we have to.
"""
# drop the current
if self.carrying:
self.carrying.x, self.carrying.y = (self.x, self.y)
self.carrying = None
# set new inventory
self.carrying = item
self.carrying.x = 0
# self.msg("got a %s" % (item.name))
def give_quest(self, quest, silent=False):
self.quests.append(quest)
if not silent:
self.msg("%s gave you a %cquest%c" % (quest.npc_name, C.COL2, C.COLS))
def seeks_quest(self, quest_id):
""" return if the player is seeking quest_id item """
return len([e for e in self.quests if e.quest_id == quest_id])
def has_quest_item(self, quest_id):
""" return if player has quest_id item in posession """
if self.carrying:
if self.carrying.quest_id:
if self.carrying.quest_id == quest_id:
return True
else:
return False
def remove_quest(self, quest_id):
""" remove the given quest """
self.quests = [e for e in self.quests if e.quest_id != quest_id]
def add_dialogue(self, dialogue):
self.dialogues.insert(0, dialogue)
def take_damage(self, attacker, damage):
self.hp = self.hp - damage
if self.hp <= 0:
self.hp = 0
def get_hearts(self):
"""
Return a count of hearts indicating our hp.
"""
return int(self.hp / 10.0)
def pickup_item(self, objects):
for obj in objects:
if obj.x == self.x and obj.y == self.y and obj.carryable:
self.give_item(obj)
break
def move(self, game_map, game_objects, x, y):
"""
If our parent moves okay, do some special player checks.
"""
if super(Player, self).move(game_map, game_objects, x, y):
self.message_trim_idx += 1
self.pickup_item(game_objects)
if self.message_trim_idx % 4 == 0:
self.trim_message()
if self.moves % C.PLAYER_THIRST_INDEX == 0:
if self.thirsty:
self.weak = True
self.thirsty = True
tile = game_map[x][y]
if self.weak:
if dice(C.PLAYER_WEAK_HP_DICE):
self.hp = self.hp - 1
if tile.message:
self.msg(tile.message)
if tile.fov_limit:
self.fov_radius = tile.fov_limit
else:
self.fov_radius = C.FOV_RADIUS_DEFAULT
return True
def can_warp(self, game_map):
return isinstance(game_map[self.x][self.y], Hole)
def warp_prep(self):
# don't shift progression for death states
if self.hp == 0:
self.x, self.y = self.entryxy
self.hp = 100
else:
self.level = self.level + 1
self.moves = self.moves + 1
if self.x == 0:
self.x = C.MAP_WIDTH - 2
elif self.x == C.MAP_WIDTH - 1:
self.x = 1
if self.y == 0:
self.y = C.MAP_HEIGHT - 2
elif self.y == C.MAP_HEIGHT - 1:
self.y = 1
self.entryxy = (self.x, self.y)
# reset quests each level
self.quests = []
if self.level > 1:
self.messages = [random.choice((
"You crawl through the fence..." \
,"You smell biscuits..." \
,"You enter this yard..." \
,"You sense doggy treats..." \
,"The smell of pies floats over you..." \
,"You wag your tail..."))]
def msg(self, message, allow_duplicates=True):
if not self.messages:
self.messages.append(message)
else:
if allow_duplicates:
if self.messages[-1] != message:
self.messages.append(message)
else:
if self.messages.count(message) == 0:
self.messages.append(message)
self.messages = self.messages[-12:]
self.message_trim_idx = 1
def trim_message(self):
if len(self.messages) > 0:
self.messages.reverse()
self.messages.pop()
self.messages.reverse()
pass
def quench_thirst(self, game_map):
messages = ("%c*laps water*%c, woof!", "%c*lap*lap*gulp*%c"
, "%c*lap*lap*lap*%c")
if game_map[self.x][self.y].drinkable:
self.quenches = self.quenches + 1
self.thirsty = False
self.msg(random.choice(messages) % (C.COL5, C.COLS))
if self.quenches % C.PLAYER_PIDDLE_INDEX == 0:
self.mustpiddle = True
def piddle(self, game_map):
if self.mustpiddle:
# find something interesting to go against
surrounding = ((-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 0), (0, 1), (1, -1), (1, 0), (1, 1))
for i in range(10):
spot = random.choice(surrounding)
tile = game_map[self.x + spot[0]][self.y + spot[1]]
if not tile.isblank():
# relief!
self.msg("You *piddle* on the %s, yey!" % (tile.name))
self.mustpiddle = False
self.addscore(10)
self.piddles_taken = self.piddles_taken + 1
break
if self.mustpiddle:
self.addscore(1)
self.msg("*psssssss*")
self.mustpiddle = False
class GameState():
"""
Handles game state via a stack based finite machine.
push(constants.STATE) a state onto the stack
peek() a look at the current state
pop() the topmost item off the stack (and return it)
is_empty() returns True if all states are popped
"""
def __init__(self):
self.stack = [C.STATE_MENU]
def push(self, state):
self.stack.append(state)
def peek(self):
if len(self.stack) == 0: return 0
return self.stack[-1:][0]
def pop(self):
return self.stack.pop()
def is_empty(self):
return len(self.stack) == 0
class Hole(ItemBase):
"""
Represents a hole in the fence, similar to the stairs in a dungeon.
"""
def __init__(self):
super(Hole, self).__init__()
self.char = "O"
class KeyHandler(object):
"""
Handles keystrokes and maps them to functions.
Supports multiple game states. Neat, huh.
"""
def __init__(self):
self.actionsdb = {}
def add_actions(self, state, actions):
self.actionsdb[state] = actions
def handle_stroke(self, state):
"""
key may be a libtcod.KEY_CODE or a letter.
"""
key = libtcod.console_wait_for_keypress(True)
if not key.pressed:
return None # ignore key releases
if key.vk == libtcod.KEY_CHAR:
key = chr(key.c)
else:
key = key.vk
if self.actionsdb[state].has_key(key):
return self.actionsdb[state][key]
#=========================================================[[ Unit Test ]]
if __name__ == "__main__":
pass