Line of sight now works, and is required to look at things. Foundations for NPCs has also been laid.

This commit is contained in:
Patrick Marsee 2019-05-15 17:56:16 -04:00
parent 267f0e9123
commit e0b88e7a45
4 changed files with 151 additions and 17 deletions

View file

@ -235,10 +235,12 @@ Object can be the name of the object, or its coordinates."""
if args[0] == 'the': if args[0] == 'the':
args.pop(0) args.pop(0)
thing, x, y = self.parseCoords(args, usePlayerCoords = False) thing, x, y = self.parseCoords(args, usePlayerCoords = False)
if thing: if not self.level.lineOfSight(self.playerx, self.playery, x, y):
print(self.justifyText(str(thing)), file = self.outstream) print("{} cannot see that.".format(self.playerName))
else: elif thing == None:
print("There is nothing to see here.\n", file = self.outstream) print("There is nothing to see here.\n", file = self.outstream)
else:
print(self.justifyText(str(thing)), file = self.outstream)
#_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) #_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent()))
def use(self, args): def use(self, args):

View file

@ -98,3 +98,8 @@ class DropEvent(GameEvent):
def __init__(self, item): def __init__(self, item):
super(DropEvent, self).__init__('drop') super(DropEvent, self).__init__('drop')
self.item = item self.item = item
class BehaveEvent(GameEvent):
def __init__(self, actor):
super(BehaveEvent, self).__init__('behave')
self.actor = actor

View file

@ -3,6 +3,7 @@ import re
import heapq import heapq
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import ruamel.yaml import ruamel.yaml
import math as _mt
from ruamel.yaml.comments import CommentedMap # for loading classes from ruamel.yaml.comments import CommentedMap # for loading classes
class Thing(object): class Thing(object):
@ -122,9 +123,6 @@ class Useable(Thing):
self.customValues = customValues self.customValues = customValues
self.graphic = graphic self.graphic = graphic
def use(self):
pass
@classmethod @classmethod
def to_yaml(cls, representer, node): def to_yaml(cls, representer, node):
# save usual things # save usual things
@ -182,12 +180,15 @@ class Useable(Thing):
parts['description'], useFunc, customValues, playerx, playery, graphic) parts['description'], useFunc, customValues, playerx, playery, graphic)
class NPC(Thing): class NPC(Thing):
yaml_flag = u'!NPC'
defaultGraphic = ('clear', '#000000', 'o')
def __init__(self, name, x: int, y: int, description: str, friendly = True, following = False, graphic = ('clear', '#000000', 'o')): def __init__(self, name, x: int, y: int, description: str, behavior: str, inventory: list, customValues: dict, playerx = None, playery = None, following = False, graphic = defaultGraphic):
super(NPC, self).__init__('c', name, x, y, description, 6) super(NPC, self).__init__('c', name, x, y, description, 6, playerx, playery)
self.behavior = behavior
self.following = following self.following = following
self.friendly = friendly self.inventory = inventory
self.inventory = [] self.customValues = customValues
self.graphic = graphic self.graphic = graphic
def give(self, item): def give(self, item):
@ -199,6 +200,69 @@ class NPC(Thing):
def dialog(self): def dialog(self):
pass pass
@classmethod
def to_yaml(cls, representer, node):
# save usual things
ret = {'name': node.name, 'location': (node.x, node.y),
'description': node.description, 'behavior': node.behavior}
# save graphic
graphic = {}
if node.graphic[0] != NPC.defaultGraphic[0]:
graphic['bgc'] = node.graphic[0]
if node.graphic[1] != NPC.defaultGraphic[1]:
graphic['fgc'] = node.graphic[1]
if node.graphic[2] != NPC.defaultGraphic[2]:
graphic['shape'] = node.graphic[2]
if len(graphic) > 0:
ret['graphic'] = graphic
# save use functions
if node.following:
ret['following'] = node.following
if len(node.inventory) > 0:
ret['inventory'] = node.inventory
if len(node.customValues) > 0:
ret['customValues'] = node.customValues
if node.x != node.playerx or node.y != node.playery:
ret['useLocation'] = (node.playerx, node.playery)
return representer.represent_mapping(cls.yaml_flag, ret)
@classmethod
def from_yaml(cls, constructor, node):
parts = CommentedMap()
constructor.construct_mapping(node, parts, True)
# set default values for optional arguments
following = False
inventory = []
customValues = {}
playerx, playery = parts['location']
bgc = NPC.defaultGraphic[0]
fgc = NPC.defaultGraphic[1]
shape = NPC.defaultGraphic[2]
# load graphic
if 'graphic' in parts:
if 'bgc' in parts['graphic']:
bgc = parts['graphic']['bgc']
if 'fgc' in parts['graphic']:
fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']:
shape = parts['graphic']['shape']
graphic = (bgc, fgc, shape)
# load use functions
if 'following' in parts:
useFunc = parts['following']
if 'inventory' in parts:
inventory = parts['inventory']
if 'customValues' in parts:
customValues = dict(parts['customValues'])
for v in customValues:
if isinstance(customValues[v], tuple):
customValues[v] = list(customValues[v])
if 'useLocation' in parts:
playerx, playery = parts['useLocation']
return cls(parts['name'], parts['location'][0], parts['location'][1],
parts['description'], parts['behavior'], inventory, customValues,
playerx, playery, following, graphic)
class Door(Thing): class Door(Thing):
yaml_flag = u'!Door' yaml_flag = u'!Door'
defaultGraphic = ('clear', '#7F3F00', '#') defaultGraphic = ('clear', '#7F3F00', '#')
@ -713,19 +777,65 @@ The closeEnough parameter will create a path that lands beside the source if nec
# return -1, [] # meaning you can't get there # return -1, [] # meaning you can't get there
def lineOfSight(self, x1, y1, x2, y2): def lineOfSight(self, x1, y1, x2, y2):
pass Dx = x2 - x1
Dy = y2 - y1
y = y1 + 0.5
x = x1 + 0.5
lst = []
if abs(Dx) >= abs(Dy):
if Dx < 0:
x = x2 + 0.5
y = y2 + 0.5
dy = Dy / Dx
while(x < max(x2, x1) - 0.5):
x += 1
lst.append(self.coordsToInt(_mt.floor(x), _mt.floor(y)))
if _mt.floor(y) != _mt.floor(y + dy):
lst.append(self.coordsToInt(_mt.floor(x), _mt.floor(y + dy)))
y += dy
elif abs(Dx) < abs(Dy):
if Dy < 0:
x = x2 + 0.5
y = y2 + 0.5
dx = Dx / Dy
while(y < max(y2, y1) - 0.5):
y += 1
lst.append(self.coordsToInt(_mt.floor(x), _mt.floor(y)))
if _mt.floor(x) != _mt.floor(x + dx):
lst.append(self.coordsToInt(_mt.floor(x + dx), _mt.floor(y)))
x += dx
# Here is where we actually check:
for space in lst:
if not self.isPassable(space):
return False
return True
def isPassable(self, x, y = -1):
pos = x
if y == -1:
x, y = self.intToCoords(x)
else:
pos = self.coordsToInt(x, y)
if self.mapMatrix[y][x][0] == 'w':
return False
thingsInSpace = self.getThingsAtPos(pos)
for thing in thingsInSpace:
if not thing.passable:
return False
return True
@staticmethod @staticmethod
def __coordsToInt(x, y, width): def __coordsToInt(x, y, width):
return x + y * width return x + y * width
def coordsToInt(self, x, y, width = -1): def coordsToInt(self, x: int, y: int, width = -1):
if width < 0: if width < 0:
return x + y * self.dimensions[0] return x + y * self.dimensions[0]
else: else:
return x + y * width return x + y * width
def intToCoords(self, pos): def intToCoords(self, pos: int):
return pos % self.dimensions[0], int(pos / self.dimensions[0]) return pos % self.dimensions[0], int(pos / self.dimensions[0])
def getThingAtCoords(self, x, y): def getThingAtCoords(self, x, y):
@ -747,7 +857,7 @@ The closeEnough parameter will create a path that lands beside the source if nec
ret.append(self.things[i]) ret.append(self.things[i])
return ret return ret
else: else:
return None return []
def getThingByName(self, name): def getThingByName(self, name):
if name in self.thingNames: if name in self.thingNames:
@ -762,7 +872,7 @@ The closeEnough parameter will create a path that lands beside the source if nec
ret.append(self.things[i]) ret.append(self.things[i])
return ret return ret
else: else:
return None return []
def getThingByID(self, thingID): def getThingByID(self, thingID):
if thingID in self.things: if thingID in self.things:
@ -806,7 +916,7 @@ The closeEnough parameter will create a path that lands beside the source if nec
ret.append(self.removeThingByThing(self.things[i])) ret.append(self.removeThingByThing(self.things[i]))
return ret return ret
else: else:
return None return []
def removeThingByName(self, name): def removeThingByName(self, name):
if name in self.thingNames: if name in self.thingNames:
@ -821,7 +931,7 @@ The closeEnough parameter will create a path that lands beside the source if nec
ret.append(self.removeThingByThing(self.things[i])) ret.append(self.removeThingByThing(self.things[i]))
return ret return ret
else: else:
return None return []
def removeThingByID(self, thingID): def removeThingByID(self, thingID):
if thingID in self.things: if thingID in self.things:
@ -845,7 +955,9 @@ The closeEnough parameter will create a path that lands beside the source if nec
self.thingPos[newPos] = [name] self.thingPos[newPos] = [name]
else: else:
self.thingPos[newPos].append(name) self.thingPos[newPos].append(name)
relPlayerx, relPlayery = thing.playerx - thing.x, thing.playery - thing.y
thing.x, thing.y = x, y thing.x, thing.y = x, y
thing.playerx, thing.playery = thing.x + relPlayerx, thing.y + relPlayery
else: else:
raise RuntimeError("There is nothing to move.".format(name)) raise RuntimeError("There is nothing to move.".format(name))

15
testing/testDialog.yml Normal file
View file

@ -0,0 +1,15 @@
%YAML 1.2
---
# This is a sample dialog tree, represented as nested dicts in YAML.
opener: Hello, I'm a sample-dialog NPC. I assure you, I eventually turn
out to be a really well-rounded character.
action: answer
answers:
- Okay.
- Really? Well, tell me more!
replies:
- opener: Yup.
action: exit
- opener: I would, but there's still a bunch of foreshadowing and stuff we
have to get through first.
action: back