1276 lines
59 KiB
Python
1276 lines
59 KiB
Python
# gamebase.py
|
|
|
|
import re as _re
|
|
import heapq as _hq
|
|
import gamemap as _gm
|
|
import gameevents as _ge
|
|
import random as _ra
|
|
import os as _os
|
|
import sys as _sys
|
|
import pickle as _pi
|
|
import ruamel.yaml as _yaml
|
|
import textwrap as _tw
|
|
import gamethings as _gt
|
|
import gamelocus as _gl
|
|
import gamesequence as _gs
|
|
import gameutil as _gu
|
|
|
|
class GameError(RuntimeError):
|
|
pass
|
|
|
|
#class _ScriptBreak(object):
|
|
#
|
|
# def __init__(self, value):
|
|
# self.value = value
|
|
|
|
class GameBase(object):
|
|
|
|
#coordRegex = _re.compile(r'(?:(-?[a-zA-Z]+) ?(-?[0-9]+))|(?:(-?[0-9]+),? (-?[0-9]+))|(?:\(([0-9]+), ([0-9]+)\))')
|
|
#coordRegex1 = _re.compile(r'(-?[a-zA-Z]+) ?(-?[0-9]+)') # "B12" or "B 12"
|
|
#coordRegex2 = _re.compile(r'\(([0-9]+), ([0-9]+)\)') # "(2, 12)"
|
|
#coordRegex3 = _re.compile(r'(-?[0-9]+),? (-?[0-9]+)') # "2 12" or "2, 12"
|
|
|
|
def __init__(self):
|
|
self.outstream = _sys.stdout
|
|
self.__useFuncs = {}
|
|
self.__behaviors = {}
|
|
self.__gameEvents = {}
|
|
self.__IOCalls = {} # {str : function}
|
|
self.__scripts = {} # functions with the same signature, but not callable by the user
|
|
self.customValues = {} # for setting flags and such
|
|
self.level = None
|
|
self.persist = {} # {level : {thingID : thing}}
|
|
self.singletons = {} # {thingName : thing}
|
|
self.prefabs = {} # {thingName : thing}
|
|
self.ps2 = '? '
|
|
self.eventQueue = []
|
|
self.gameTime = 0.0
|
|
self.skipLoop = True
|
|
self.nextThing = 1
|
|
|
|
# player info
|
|
self.playerName = 'You'
|
|
self.playerDescription = 'The main character.'
|
|
self.player = None # reference to the player's 'thing'
|
|
self.autoMove = True
|
|
|
|
# function deligates
|
|
self.onLevelLoad = None # str : level name? -> None
|
|
|
|
# default events and useFuncs
|
|
self.registerEvent('noop', self.handleNoOp)
|
|
self.registerEvent('go', self.handleGo)
|
|
self.registerEvent('arrive', self.handleArrive)
|
|
self.registerEvent('use', self.handleUse)
|
|
self.registerEvent('useon', self.handleUseOn)
|
|
self.registerEvent('take', self.handleTake)
|
|
self.registerEvent('drop', self.handleDrop)
|
|
self.registerEvent('behave', self.handleBehave)
|
|
self.registerUseFunc('examine', self.examine)
|
|
self.registerUseFunc('key', self.key)
|
|
self.registerUseFunc('container', self.container)
|
|
self.registerUseFunc('info', self.info)
|
|
self.registerUseFunc('wear', self.wear)
|
|
self.registerBehavior('stand', self.stand)
|
|
self.registerBehavior('wander', self.wander)
|
|
self.registerBehavior('follow', self.follow)
|
|
#self.registerScript('if', self.ifScript)
|
|
self.registerScript('printf', self.printfScript)
|
|
#self.registerScript('get', self.getCustomValue)
|
|
#self.registerScript('set', self.setCustomValue)
|
|
#self.registerScript('del', self.delCustomValue)
|
|
self.registerScript('spawn', self.spawnThing)
|
|
self.registerScript('give', self.giveToPlayer)
|
|
self.registerScript('move', self.moveThingScript)
|
|
self.registerScript('cvget', self.cvget)
|
|
self.registerScript('cvmod', self.cvmod)
|
|
self.registerScript('cvdel', self.cvdel)
|
|
self.registerScript('playercmd', self.playerCmdScript)
|
|
|
|
# Helper functions
|
|
|
|
def requestInput(self, prompt = ''):
|
|
"""Like input by default, but should be overridden if you don't want
|
|
to get input from stdin."""
|
|
return input(prompt)
|
|
|
|
def justifyText(self, text, width = 80):
|
|
ret = []
|
|
text = text.lstrip().rstrip()
|
|
|
|
# start by making it all one long line.
|
|
text = _re.sub(r'\s{2,}', r' ', text)
|
|
|
|
# then add newlines as needed.
|
|
#i = 80
|
|
#i = get_terminal_size()[0]
|
|
i = width
|
|
while i < len(text):
|
|
while text[i] != ' ':
|
|
i -= 1
|
|
ret.append(text[:i])
|
|
text = text[i+1:]
|
|
ret.append(text)
|
|
|
|
return '\n'.join(ret)
|
|
|
|
def smoothMove(self, actor: _gt.Thing, loc: _gl.Locus, speed: float, action = None, immediate = False, closeEnough = True):
|
|
"""Move a thing smoothly at a constant speed.
|
|
|
|
actor should be a Thing.
|
|
loc should be a locus.
|
|
speed should be a number.
|
|
action should be a callable with 0 arguments. A lambda would be good here.
|
|
If immediate is True, the go event would happen immediately.
|
|
closeEnough should be True only if the path is to a non-passable point locus."""
|
|
if not isinstance(actor, _gt.Thing):
|
|
raise TypeError("The actor passed to smoothMove must be a Thing.")
|
|
if not isinstance(loc, _gl.Locus):
|
|
raise TypeError("The locus passed to smoothMove must be a Locus.")
|
|
if (actor.x, actor.y) in loc:
|
|
self.setEvent(0.0, _ge.ArriveEvent(actor, self.level.coordsToInt(actor.x, actor.y), speed, action, loc, timeTaken = speed))
|
|
return
|
|
dist, path, endPoint = self.level.path(actor.x, actor.y, loc, closeEnough)
|
|
#print(path)
|
|
if dist == -1:
|
|
print('{0} cannot reach there.'.format(actor.name), file = self.outstream)
|
|
return
|
|
elif dist == 1:
|
|
self.setEvent(speed, _ge.ArriveEvent(actor, path[0], speed, action, loc, timeTaken = speed))
|
|
return speed
|
|
elif dist == 0:
|
|
# We should try to avoid this branch, because it means an error happened.
|
|
self.setEvent(0.0, _ge.ArriveEvent(actor, self.level.coordsToInt(actor.x, actor.y), speed, action, loc, timeTaken = speed))
|
|
return
|
|
elif immediate:
|
|
self.setEvent(0, _ge.GoEvent(actor, path, speed, action, loc, closeEnough))
|
|
return speed * (dist - 1)
|
|
else:
|
|
self.setEvent(speed, _ge.GoEvent(actor, path, speed, action, loc, closeEnough, timeTaken = speed))
|
|
return speed * dist
|
|
|
|
# commands
|
|
|
|
def wait(self, args):
|
|
"""wait seconds"""
|
|
if len(args) < 1:
|
|
return
|
|
self.setEvent(float(args[0]), _ge.NoOpEvent())
|
|
|
|
def go(self, args):
|
|
"""go [-r] [to [the]] destination [additional arguments...]
|
|
Go to a location. "walk" and "move" are aliases of go.
|
|
-r: run to the location. "r" and "run" are aliases of -r.
|
|
The "run" command is an alias of "go -r".
|
|
"to" and "the" do nothing, and are intended only to make certain commands
|
|
make more sense from a linguistic perspective (for instance, one could
|
|
say "go to the balcony" rather than just "go balcony").
|
|
Destination can be a coordinate pair or object name. For instance, if one
|
|
wanted to go to D6, one could say "go to D6", "go to d 6", or "go to 3 6".
|
|
The letter is not case-sensitive."""
|
|
if self.level == None:
|
|
raise GameError("Cannot move: No level has been loaded.")
|
|
if self.player == None:
|
|
raise GameError("Cannot move: Player character doesn't exist.")
|
|
if len(args) == 0:
|
|
print(f"{self.player.name} goes nowhere.", file = self.outstream)
|
|
return
|
|
speed = 0.6666667
|
|
if args[0] == '-r' or args[0] == 'r' or args[0] == 'run':
|
|
speed = 0.3333333
|
|
args.pop(0)
|
|
if args[0] == 'to':
|
|
args.pop(0)
|
|
if args[0] == 'the':
|
|
args.pop(0)
|
|
thing, x, y = _gu.parseCoords(self.level, args)
|
|
|
|
# Now we have a heading! Let's see if we can get there...
|
|
self.smoothMove(self.player, _gl.PointLocus(x, y), speed)
|
|
return
|
|
|
|
def look(self, args):
|
|
"""look [at [the]] object
|
|
Describe an object.
|
|
"at" and "the" do nothing, and are intended only to make certain commands
|
|
make more sense from a linguistic perspective (for instance, one could
|
|
say "look at the balcony" rather than just "look balcony").
|
|
Object can be the name of the object, or its coordinates."""
|
|
if self.level == None:
|
|
raise GameError("Cannot look: No level has been loaded.")
|
|
if self.player == None:
|
|
raise GameError("Cannot look: Player character doesn't exist.")
|
|
if len(args) == 0:
|
|
# no arguments: print the level description
|
|
print(self.justifyText(self.level.description), file = self.outstream)
|
|
else:
|
|
if args[0] == 'at':
|
|
args.pop(0)
|
|
if args[0] == 'the':
|
|
args.pop(0)
|
|
thing, x, y = _gu.parseCoords(self.level, args, usePlayerCoords = False, player = self.player)
|
|
if thing == None:
|
|
print("There is nothing to see here.", file = self.outstream)
|
|
return
|
|
elif thing in self.player.inventory:
|
|
print(self.justifyText(str(thing)), file = self.outstream)
|
|
return
|
|
elif not self.level.lineOfSight(self.player.x, self.player.y, x, y):
|
|
if self.autoMove:
|
|
self.smoothMove(self.player, _gm.LoSLocus(x, y, self.level), 0.3333333, lambda: print(self.justifyText(str(thing)), file = self.outstream), closeEnough = False)
|
|
else:
|
|
print("{} cannot see that.".format(self.player.name), file = self.outstream)
|
|
return
|
|
else:
|
|
print(self.justifyText(str(thing)), file = self.outstream)
|
|
|
|
def talk(self, args):
|
|
"""talk [to [the]] character
|
|
Talk to a character.
|
|
"to" and "the" do nothing, and are intended only to make certain commands
|
|
make more sense from a linguistic perspective (for instance, one could
|
|
say "talk to the guard" rather than just "talk guard").
|
|
Character can be the name of the character, or their coordinates."""
|
|
if self.level == None:
|
|
raise GameError("Cannot talk: No level has been loaded.")
|
|
if self.player == None:
|
|
raise GameError("Cannot talk: Player character doesn't exist.")
|
|
if len(args) == 0:
|
|
print(f'{self.player.name} says, "Hello!" Nobody responds.', file = self.outstream)
|
|
return
|
|
else:
|
|
if args[0] == 'to':
|
|
args.pop(0)
|
|
if args[0] == 'the':
|
|
args.pop(0)
|
|
thing, x, y = _gu.parseCoords(self.level, args, usePlayerCoords = False)
|
|
if thing == None:
|
|
if len(args) > 0:
|
|
print(f"There is nobody named {' '.join(args)}.", file = self.outstream)
|
|
else:
|
|
print(f"There is nobody there.", file = self.outstream)
|
|
return
|
|
elif not self.level.lineOfSight(self.player.x, self.player.y, x, y):
|
|
if self.autoMove:
|
|
self.smoothMove(self.player, _gm.LoSLocus(x, y, self.level), 0.3333333, lambda: _gu.startDialog(thing, self.outstream, self.dialog), closeEnough = False)
|
|
else:
|
|
print("{} cannot talk to {}.".format(self.player.name, thing.name), file = self.outstream)
|
|
return
|
|
else:
|
|
_gu.startDialog(thing, self.outstream, self.dialog)
|
|
|
|
def use(self, args):
|
|
"""use [-r] [the] object [on [the] object2]
|
|
Use an object. If the player is not already close to it, they will go to it.
|
|
use [-r] [the] item on [the] object
|
|
Use an item from the player's inventory on an object.
|
|
-r: run to the location. "r" and "run" are aliases of -r.
|
|
"the" does nothing, and is intended only to make certain commands
|
|
make more sense from a linguistic perspective (for instance, one could
|
|
say "use the lever" rather than just "use lever").
|
|
Object can be the name of the object, or its coordinates. It can also be
|
|
the name of an item in the player's inventory."""
|
|
if self.level == None:
|
|
raise GameError("Cannot use: No level has been loaded.")
|
|
if self.player == None:
|
|
raise GameError("Cannot use: Player character doesn't exist.")
|
|
if len(args) == 0:
|
|
print(f"{self.player.name} pokes the air as if pressing a button.", file = self.outstream)
|
|
return
|
|
speed = 0.6666667
|
|
useArgs = []
|
|
if args[0] == '-r' or args[0] == 'r' or args[0] == 'run':
|
|
speed = 0.3333333
|
|
args.pop(0)
|
|
if args[0] == 'the':
|
|
args.pop(0)
|
|
if 'on' in args:
|
|
self.useOn(args, speed)
|
|
return
|
|
if 'with' in args:
|
|
useArgs = args[args.index('with')+1:]
|
|
thing, x, y = _gu.parseCoords(self.level, args, player = self.player)
|
|
if thing == None:
|
|
print("There is nothing to use.", file = self.outstream)
|
|
#_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent()))
|
|
return
|
|
if thing.thingType != 'u' and thing not in self.player.inventory:
|
|
print("The {0} cannot be used.".format(thing.name), file = self.outstream)
|
|
#_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent()))
|
|
return
|
|
|
|
# Similar to go, but not quite the same.
|
|
if thing.thingType == 'i': # it must be in inventory to pass the last check.
|
|
self.setEvent(0.125, _ge.UseEvent(self.player, thing, useArgs))
|
|
else:
|
|
self.smoothMove(self.player, _gl.PointLocus(x, y), speed, lambda: self.setEvent(0.125, _ge.UseEvent(self.player, thing, useArgs)))
|
|
return
|
|
|
|
def useOn(self, args, speed):
|
|
"""Called by use when there is an 'on' clause"""
|
|
onIndex = args.index('on')
|
|
if len(args[:onIndex]) == 0:
|
|
# just starts with 'use on ...'
|
|
print(f"{self.player.name} tries to use nothing, but nothing happens.", file = self.outstream)
|
|
return
|
|
if len(args[onIndex + 1:]) == 0:
|
|
# just ends with '... on'
|
|
print(f"{self.player.name} tries to use the {' '.join(args[:onIndex])} on nothing, but nothing happens.", file = self.outstream)
|
|
return
|
|
item, x, y = _gu.parseCoords(self.level, args[:onIndex], usePlayerCoords = False, player = self.player)
|
|
if args[onIndex+1] == 'the':
|
|
onIndex += 1
|
|
thing, x, y = _gu.parseCoords(self.level, args[onIndex+1:], usePlayerCoords = True, player = self.player)
|
|
useArgs = []
|
|
if 'with' in args:
|
|
useArgs = args[args.index('with')+1:]
|
|
if item == None or item not in self.player.inventory:
|
|
print("There is no such item in the inventory.", file = self.outstream)
|
|
#_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent()))
|
|
return
|
|
if thing == None and x < 0 and y < 0:
|
|
print("Argument contains 'to' but with no real predicate.", file = self.outstream)
|
|
#_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent()))
|
|
return
|
|
|
|
if not item.ranged:
|
|
# Similar to go, but not quite the same.
|
|
self.smoothMove(self.player, _gl.PointLocus(x, y), speed, lambda: self.setEvent(0.125, _ge.UseOnEvent(self.player, item, thing, useArgs)))
|
|
else:
|
|
if not self.level.lineOfSight(self.player.x, self.player.y, x, y):
|
|
if self.autoMove:
|
|
self.smoothMove(self.player, _gm.LoSLocus(x, y, self.level), 0.3333333, lambda: self.setEvent(0.125, _ge.UseOnEvent(self.player, item, thing, useArgs)), closeEnough = False)
|
|
else:
|
|
print("{} cannot see that.".format(self.player.name), file = self.outstream)
|
|
return
|
|
|
|
def take(self, args):
|
|
"""take [the] item
|
|
Take an item. 'get' is an alias of take.
|
|
If the player is not already close to it, they will go to it.
|
|
"the" does nothing, and is intended only to make certain commands
|
|
make more sense from a linguistic perspective (for instance, one could
|
|
say "take the flask" rather than just "take flask").
|
|
Object can be the name of the object, or its coordinates."""
|
|
if self.level == None:
|
|
raise GameError("Cannot take: No level has been loaded.")
|
|
if self.player == None:
|
|
raise GameError("Cannot take: Player character doesn't exist.")
|
|
speed = 0.6666667
|
|
if len(args) == 0:
|
|
print(f"{self.player.name} reaches out and grasps at nothing.", file = self.outstream)
|
|
return
|
|
if args[0] == '-r' or args[0] == 'r' or args[0] == 'run':
|
|
speed = 0.3333333
|
|
args.pop(0)
|
|
if args[0] == 'the':
|
|
args.pop(0)
|
|
thing, x, y = _gu.parseCoords(self.level, args)
|
|
if thing == None:
|
|
print("There is nothing to take.", file = self.outstream)
|
|
return
|
|
if thing.thingType != 'i':
|
|
print("The {0} cannot be taken.".format(thing.name), file = self.outstream)
|
|
#_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent()))
|
|
return
|
|
|
|
# Similar to go, but not quite the same.
|
|
self.smoothMove(self.player, _gl.PointLocus(x, y), speed, lambda: self.setEvent(0.125, _ge.TakeEvent(self.player, thing)))
|
|
|
|
def drop(self, args):
|
|
"""drop [the] item"""
|
|
if self.level == None:
|
|
raise GameError("Cannot drop: No level has been loaded.")
|
|
if self.player == None:
|
|
raise GameError("Cannot drop: Player character doesn't exist.")
|
|
if len(args) == 0:
|
|
print(f"{self.player.name} falls over.", file = self.outstream)
|
|
return
|
|
if args[0] == 'the':
|
|
args.pop(0)
|
|
thingName = ' '.join(args)
|
|
if thingName in self.player.thingNames:
|
|
self.setEvent(0.0, _ge.DropEvent(self.player, self.player.getThingByName(thingName)))
|
|
else:
|
|
print('{0} does not have a {1}.'.format(self.player.name, args[0]), file = self.outstream)
|
|
|
|
def loadMap(self, args):
|
|
# loadMap (fileName)
|
|
# loadMap (fileName, entrance)
|
|
# loadMap (fileName, x, y)
|
|
# save persistent things in previous level
|
|
if self.level != None:
|
|
if self.level.name not in self.persist:
|
|
self.persist[self.level.name] = {}
|
|
for i in self.level.persistent:
|
|
self.persist[self.level.name][i] = self.level.getThingByID(i)
|
|
|
|
preLoaded = False
|
|
if args[0] in self.persist:
|
|
preLoaded = True
|
|
|
|
self.clearEvents()
|
|
|
|
# load the new level
|
|
if len(args) == 2:
|
|
self.level, self.nextThing = _gm.GameMap.read(args[0], int(args[1]), self.singletons, self.prefabs, preLoaded, self.nextThing)
|
|
else:
|
|
self.level, self.nextThing = _gm.GameMap.read(args[0], None, self.singletons, self.prefabs, preLoaded, self.nextThing)
|
|
|
|
if self.level == None:
|
|
raise GameError("Map could not be loaded.")
|
|
|
|
# get persistent things from it
|
|
if args[0] in self.persist:
|
|
persistedThings = tuple(self.persist[args[0]].keys())
|
|
for i in persistedThings:
|
|
self.nextThing = self.level.addThing(self.persist[args[0]][i], self.prefabs, self.nextThing, True) #nextThing shouldn't change
|
|
del self.persist[args[0]][i] # delete them from the persist dict to prevent item duplication
|
|
|
|
print(self.level.openingText, file = self.outstream)
|
|
#print(self.outstream.getvalue())
|
|
mx, my = self.level.playerStart
|
|
if len(args) == 3:
|
|
mx, my = int(args[1]), int(args[2])
|
|
if self.player == None:
|
|
# create a player to put in the level.
|
|
self.player = _gt.PlayerCharacter(x = mx, y = my, description = self.playerDescription,
|
|
inventory = {}, customValues = {}, name = self.playerName)
|
|
#print("Player created.")
|
|
else:
|
|
self.player.x, self.player.y = mx, my
|
|
#print("Player moved.")
|
|
self.player.prevx, self.player.prevy = self.player.x, self.player.y
|
|
self.nextThing = self.level.addThing(self.player, self.prefabs, self.nextThing) # The player needs to be added to the new level.
|
|
if self.onLevelLoad != None:
|
|
self.onLevelLoad()
|
|
self.parseScript(self.level.enterScript)
|
|
|
|
def unloadMap(self, args):
|
|
"""Args is only there just in case."""
|
|
self.level = None
|
|
self.player = None
|
|
|
|
def saveGame(self, args):
|
|
if len(args) < 1:
|
|
print("Save file must have a name!", file = self.outstream)
|
|
return
|
|
|
|
# choose pickle protocol depending on python version:
|
|
# 3 for Python 3.0.0 to 3.3.x, 4 for Python 3.4.0 to 3.7.x
|
|
prot = _pi.HIGHEST_PROTOCOL
|
|
|
|
# save persistent things so that the current level can be recalled as-is
|
|
if self.level != None:
|
|
if self.level.name not in self.persist:
|
|
self.persist[self.level.name] = {}
|
|
for i in self.level.persistent:
|
|
self.persist[self.level.name][i] = self.level.getThingByID(i)
|
|
|
|
# build data object to be saved
|
|
data = (self.player, self.level.name, self.persist, self.eventQueue, self.customValues, self.gameTime, self.nextThing)
|
|
|
|
# save it!
|
|
fileName = 'saves/' + args[0].replace(' ', '_') + '.dat'
|
|
if args[0].endswith('.dat'): # This is really for absolute paths, but doesn't really check for that.
|
|
fileName = args[0]
|
|
try:
|
|
f = open(fileName, 'wb')
|
|
_pi.dump(data, f, protocol=prot)
|
|
except FileNotFoundException as err:
|
|
_os.mkdir('saves')
|
|
try:
|
|
f = open(fileName, 'wb')
|
|
_pi.dump(data, f, protocol=prot)
|
|
except OSError as newErr:
|
|
print("Save failed: {}".format(newErr), file = self.outstream)
|
|
|
|
# delete things in the current map from the persist dict to prevent item duplication
|
|
persistedThings = tuple(self.persist[self.level.name].keys())
|
|
for i in persistedThings:
|
|
del self.persist[self.level.name][i]
|
|
|
|
# push a no-op event so that saving doesn't cost player characters time
|
|
#_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent()))
|
|
return
|
|
|
|
def loadGame(self, args):
|
|
if len(args) < 1:
|
|
print("Save file must have a name!", file = self.outstream)
|
|
print('\n'.join(_os.listdir('./saves')), file = self.outstream)
|
|
return
|
|
|
|
# pickle protocol 4 for Python 3.4.0 to 3.7.x
|
|
prot = 4
|
|
fileName = 'saves/' + args[0].replace(' ', '_') + '.dat'
|
|
if args[0].endswith('.dat'):
|
|
fileName = args[0]
|
|
x, y, levelname = 1, 1, 'testing/test1.txt'
|
|
with open(fileName, 'rb') as f:
|
|
self.player, levelname, self.persist, self.eventQueue, self.customValues, self.gameTime, self.nextThing = _pi.load(f)
|
|
#print(levelname, x, y, file = self.outstream)
|
|
self.loadMap((levelname, self.player.x, self.player.y))
|
|
#_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent()))
|
|
return
|
|
|
|
def dialog(self, dialogName, conversant = None):
|
|
yaml = _yaml.YAML()
|
|
dialog = None
|
|
with open(dialogName, 'r') as f:
|
|
dialog = yaml.load(f)
|
|
self.getIO('startDialog')()
|
|
self.runDialog(dialog)
|
|
self.getIO('endDialog')()
|
|
|
|
def runDialog(self, dialogObj):
|
|
#print(dialogObj)
|
|
if 'script' in dialogObj:
|
|
self.parseScript(dialogObj['script'])
|
|
if 'cond' in dialogObj:
|
|
cases = dialogObj['cond']
|
|
ret = 0
|
|
for case in cases:
|
|
cond = case['case'].split() # should be list like ["value", "==", 1]
|
|
if len(cond) == 1 and (cond[0] == 'else' or _gs.getValueFromString(cond[0], _gs.ScriptEnvironment(self.customValues, {}))):
|
|
ret = self.runDialog(case)
|
|
break
|
|
elif len(cond) == 3 and _gs.compareValues(cond, _gs.ScriptEnvironment(self.customValues, {})):
|
|
ret = self.runDialog(case)
|
|
break
|
|
else:
|
|
raise GameError("All routes are false: {}".format(testValue))
|
|
if ret > 1:
|
|
return ret - 1
|
|
else:
|
|
return ret
|
|
# if ret == 1 or ret == 0, go back again.
|
|
elif 'opener' in dialogObj:
|
|
self.getIO('openDialog')(dialogObj['opener'].split('\n')) # split by lines so that fill doesn't erase newlines
|
|
while isinstance(dialogObj, dict):
|
|
if 'action' in dialogObj:
|
|
action = dialogObj['action']
|
|
if action == 'answer':
|
|
answer = 0
|
|
skips = []
|
|
j = 0 # follower to i
|
|
options = []
|
|
if 'answers' in dialogObj and isinstance(dialogObj['answers'], list):
|
|
for i in range(len(dialogObj['answers'])):
|
|
ans = dialogObj['answers'][i]
|
|
if ans[0] == '?':
|
|
condEnd = ans.index(':')
|
|
cond = ans[1:condEnd].strip().split()
|
|
if _gs.compareValues(cond, _gs.ScriptEnvironment(self.customValues, {})):
|
|
options.append(ans[condEnd+1:].strip())
|
|
j += 1
|
|
else:
|
|
skips.append(i)
|
|
else:
|
|
options.append(ans)
|
|
j += 1
|
|
answer = self.getIO('respondDialog')(options)
|
|
# account for offset if there were answer options that didn't meet conditions
|
|
for i in skips:
|
|
if i <= answer:
|
|
answer += 1
|
|
else:
|
|
break
|
|
if 'replies' in dialogObj and isinstance(dialogObj['replies'], list):
|
|
ret = self.runDialog(dialogObj['replies'][answer])
|
|
if ret > 1:
|
|
return ret - 1
|
|
elif ret == 0:
|
|
return 0
|
|
# if ret == 1, then do this dialog again
|
|
|
|
elif len(action) >= 4 and action[:4] == 'back':
|
|
if len(action) == 4:
|
|
return 1
|
|
return int(action[5:])
|
|
elif action == 'exit':
|
|
return 0
|
|
else:
|
|
raise RuntimeError('Malformed action: {}'.format(action))
|
|
else:
|
|
raise RuntimeError("Dialog branch with neither switch nor openner.")
|
|
|
|
def parseScript(self, instr: str):
|
|
"""A thin wrapper for _gs.parseScript."""
|
|
return _gs.parseScript(instr, self.customValues, self.__scripts)
|
|
|
|
def loadGameData(self, dataFile):
|
|
yaml = _yaml.YAML()
|
|
yaml.register_class(_gt.Prefab)
|
|
yaml.register_class(_gt.Item)
|
|
yaml.register_class(_gt.Useable)
|
|
yaml.register_class(_gt.NPC)
|
|
yaml.register_class(_gt.Door)
|
|
yaml.register_class(_gt.MapExit)
|
|
yaml.register_class(_gt.MapEntrance)
|
|
data = None
|
|
with open(dataFile, 'r') as f:
|
|
data = yaml.load(f)
|
|
self.prefabs = {}
|
|
if 'prefabs' in data:
|
|
for thing in data['prefabs']:
|
|
if not isinstance(thing, _gt.Thing):
|
|
print("Non-thing in prefabs, ignoring.\n", _sys.stderr)
|
|
continue
|
|
thing.thingID = self.nextThing
|
|
self.nextThing += 1
|
|
if thing.thingType in 'iun':
|
|
self.nextThing = _gm.GameMap.addThingRecursive(thing.customValues, self.prefabs, self.nextThing)
|
|
if thing.thingType == 'n':
|
|
for i in thing.tempInventory:
|
|
if i.thingID == -1:
|
|
i.thingID = self.nextThing
|
|
self.nextThing = _gm.GameMap.addThingRecursive(i.customValues, self.prefabs, self.nextThing + 1)
|
|
# for prefabs, we don't add their items to their "real" inventory and don't delete the temporary one.
|
|
self.prefabs[thing.name] = thing
|
|
# In this context, 'singleton' means a 'thing' that can be accessed by
|
|
# any map. This is useful for when you have major characters who will
|
|
# show up in many scenes, and their inventory must stay the same, for
|
|
# example.
|
|
#print(data)
|
|
self.singletons = {}
|
|
if 'singletons' in data:
|
|
for thing in data['singletons']:
|
|
#thing = data['singletons'][datum]
|
|
if not isinstance(thing, _gt.Thing):
|
|
print("Non-thing in singletons, ignoring.\n", _sys.stderr)
|
|
continue
|
|
thing.thingID = self.nextThing
|
|
self.nextThing += 1
|
|
if thing.thingType in 'iun':
|
|
self.nextThing = _gm.GameMap.addThingRecursive(thing.customValues, self.prefabs, self.nextThing)
|
|
if thing.thingType == 'n':
|
|
for i in thing.tempInventory:
|
|
if i.thingID == -1:
|
|
i.thingID = self.nextThing
|
|
self.nextThing = _gm.GameMap.addThingRecursive(i.customValues, self.prefabs, self.nextThing + 1)
|
|
thing.addThing(i)
|
|
del thing.tempInventory
|
|
self.singletons[thing.name] = thing
|
|
#print(self.singletons)
|
|
return data
|
|
|
|
def gameEventLoop(self):
|
|
#print(self.skipLoop)
|
|
if self.skipLoop:
|
|
return
|
|
#print(self.skipLoop)
|
|
while len(self.eventQueue) > 0:
|
|
ev = _hq.heappop(self.eventQueue)
|
|
self.gameTime = ev[0]
|
|
e = ev[1]
|
|
ret = False
|
|
if e.eventType in self.__gameEvents:
|
|
for i in self.__gameEvents[e.eventType]:
|
|
ret = ret or i(e)
|
|
self.observe(e) # An event can still be observed even if it has no callback.
|
|
if ret:
|
|
break
|
|
if len(self.eventQueue) == 0:
|
|
self.gameTime = 0.0
|
|
_ge.resetEventNum()
|
|
self.skipLoop = True
|
|
|
|
def setEvent(self, timeFromNow: float, event: _ge.GameEvent, skip = False):
|
|
_hq.heappush(self.eventQueue, (self.gameTime + timeFromNow, event))
|
|
self.skipLoop = skip
|
|
|
|
def clearEvents(self, actor = None):
|
|
if isinstance(actor, _gt.Thing):
|
|
i = 0
|
|
while i < len(self.eventQueue):
|
|
if actor is self.eventQueue[i].actor:
|
|
del self.eventQueue[i]
|
|
else:
|
|
i += 1
|
|
_hq.heapify(self.eventQueue)
|
|
else:
|
|
self.eventQueue.clear()
|
|
|
|
# default event handlers
|
|
|
|
def handleNoOp(self, e):
|
|
return True
|
|
|
|
def handleGo(self, e):
|
|
if e.actor == None:
|
|
raise GameError("'Go' event raised for no object.")
|
|
if self.level.isPassable(e.path[0]):
|
|
#print(e.actor.name, e.actor.x, e.actor.y)
|
|
self.level.moveThing(e.actor, e.path[0])
|
|
#print(e.actor.x, e.actor.y)
|
|
#print(e.action)
|
|
if len(e.path) > 1:
|
|
self.setEvent(e.speed, _ge.GoEvent(e.actor, e.path[1:], e.speed, e.action, e.locus, e.closeEnough, e.timeTaken + e.speed))
|
|
else:
|
|
self.setEvent(e.speed, _ge.ArriveEvent(e.actor, e.path[0], e.speed, e.action, e.locus, e.timeTaken + e.speed))
|
|
elif isinstance(e.locus, _gl.Locus):
|
|
self.smoothMove(e.actor, e.locus, e.speed, e.action, True, e.closeEnough)
|
|
else:
|
|
print('{0} cannot reach there.'.format(e.actor.name), file = self.outstream)
|
|
return False
|
|
|
|
def handleArrive(self, e):
|
|
if e.actor == None:
|
|
raise GameError("'Go' event raised for no object.")
|
|
#print(e.actor, "arriving at", e.pos, self.level.isPassable(e.pos))
|
|
if self.level.isPassable(e.pos) or (e.actor.x, e.actor.y) == self.level.intToCoords(e.pos):
|
|
thing = self.level.getThingAtPos(e.pos)
|
|
self.level.moveThing(e.actor, e.pos)
|
|
if e.action == None:
|
|
#print('{0} arrived at {1}{2} after {3:.1f} seconds.'.format(e.actor.name, _gu.numberToLetter(e.x), e.y, e.t), file = self.outstream)
|
|
#print ("action = None")
|
|
if thing:
|
|
#print("thing != None")
|
|
if thing.thingType == 'x':
|
|
#print("thing is exit")
|
|
if e.actor == self.player:
|
|
#print("actor = player")
|
|
self.parseScript(thing.onUse)
|
|
if (isinstance(thing.key, bool) and thing.key == True) or (isinstance(thing.key, str) and self.parseScript(thing.key)):
|
|
#print("key")
|
|
a = self.requestInput('Do you want to go {0}? (Y/n)'.format(str(thing)))
|
|
if a != 'n' and a != 'N':
|
|
self.loadMap((thing.destination, thing.exitid))
|
|
else:
|
|
print('{0} went {1}.'.format(actor.name, str(thing)), file = self.outstream)
|
|
self.level.removeThing(actor.name)
|
|
else:
|
|
e.action()
|
|
return False # so that use works.
|
|
#elif isinstance(e.locus, _gl.Locus):
|
|
# self.smoothMove(e.actor, e.locus, e.speed, e.action, immediate = True)
|
|
else:
|
|
# checking the locus again would inevitably fail, so we just give up.
|
|
print('{0} cannot reach there.'.format(e.actor.name), file = self.outstream)
|
|
return e.actor == self.player
|
|
|
|
def handleUse(self, e: _ge.UseEvent):
|
|
"""Called when a UseEvent needs processed."""
|
|
if e.thing.useFunc == '':
|
|
print('The {0} cannot be used by itself.'.format(e.thing.name), file = self.outstream)
|
|
return True
|
|
self.setEvent(self.__useFuncs[e.thing.useFunc](e.thing, e.args), _ge.NoOpEvent())
|
|
return False
|
|
|
|
def handleUseOn(self, e: _ge.UseOnEvent):
|
|
"""Called when a UseOnEvent needs processed.
|
|
It calls the item's useOnFunc in the useFuncs map with the item, target, and arguments."""
|
|
if e.item.useOnFunc == '':
|
|
print('The {0} cannot be used on other objects.'.format(e.item.name), file = self.outstream)
|
|
return True
|
|
self.setEvent(self.__useFuncs[e.item.useOnFunc](e.item, e.target, e.args), _ge.NoOpEvent())
|
|
return False
|
|
|
|
def handleTake(self, e):
|
|
if e.actor == None or e.actor.thingType not in 'np' or e.item == None:
|
|
raise GameError("'Take' event cannot be handled.")
|
|
self.level.removeThingByID(e.item.thingID)
|
|
e.actor.addThing(e.item)
|
|
return True
|
|
|
|
def handleDrop(self, e):
|
|
if e.actor == None or e.actor.thingType not in 'np':
|
|
raise GameError("'Drop' event cannot be handled.")
|
|
e.item.x = e.actor.x
|
|
e.item.y = e.actor.y
|
|
self.nextThing = self.level.addThing(e.item, self.prefabs, self.nextThing, True) # nextThing shouldn't change
|
|
e.actor.removeThing(e.item)
|
|
return True
|
|
|
|
def handleBehave(self, e):
|
|
"""Allow an NPC to make another move."""
|
|
if not isinstance(e.actor, _gt.Observer):
|
|
raise GameError("Non-observer is trying to behave.")
|
|
if len(e.actor.behaviorQueue) > 0:
|
|
nextBehavior = _hq.heappop(e.actor.behaviorQueue)
|
|
timeTaken = self.__behaviors[e.actor.behaviors[nextBehavior[1].eventType][1]](e.actor, nextBehavior[1])
|
|
self.setEvent(timeTaken, _ge.BehaveEvent(e.actor))
|
|
else:
|
|
e.actor.busy = False
|
|
return False
|
|
|
|
# default useFuncs: take a list of arguments, return the time the use took
|
|
|
|
def examine(self, thing, args):
|
|
"""Just prints the given text."""
|
|
if 'pattern' not in thing.customValues or 'text' not in thing.customValues:
|
|
raise ValueError("Non-examinable thing {0} examined.".format(thing.name))
|
|
# Start out with a cursor at 0.
|
|
if not 'cursor' in thing.customValues:
|
|
thing.customValues['cursor'] = 0
|
|
|
|
# With 'single', the cursor will not be modified.
|
|
if thing.customValues['pattern'] == 'single':
|
|
if isinstance(thing.customValues['text'], str):
|
|
print(self.justifyText(str(thing.customValues['text'])), file = self.outstream)
|
|
elif isinstance(thing.customValues['text'], list):
|
|
print(self.justifyText(str(thing.customValues['text'][cursor])), file = self.outstream)
|
|
|
|
# With 'loop', the given strings will keep looping.
|
|
elif thing.customValues['pattern'] == 'loop':
|
|
cursor = thing.customValues['cursor']
|
|
print(self.justifyText(str(thing.customValues['text'][cursor])), file = self.outstream)
|
|
thing.customValues['cursor'] = (cursor + 1) % len(thing.customValues['text'])
|
|
|
|
# With 'once', the strings will be used one by one, until it gets to the end. The last one is then repeated.
|
|
elif thing.customValues['pattern'] == 'once':
|
|
if not 'cursor' in thing.customValues:
|
|
thing.customValues['cursor'] = 0
|
|
cursor = thing.customValues['cursor']
|
|
print(self.justifyText(str(thing.customValues['text'][cursor])), file = self.outstream)
|
|
if cursor < len(thing.customValues['text']) - 1:
|
|
thing.customValues['cursor'] += 1
|
|
|
|
|
|
elif thing.customValues['pattern'] == 'random':
|
|
cursor = _ra.randrange(len(thing.customValues['text']))
|
|
print(self.justifyText(str(thing.customValues['text'][cursor])), file = self.outstream)
|
|
thing.customValues['cursor'] = cursor
|
|
|
|
if 'set' in thing.customValues:
|
|
if 'cursor' in thing.customValues:
|
|
self.customValues[thing.customValues['set']] = thing.customValues[cursor]
|
|
else:
|
|
self.customValues[thing.customValues['set']] = True
|
|
return 0.0
|
|
|
|
def container(self, thing, args):
|
|
"""Acts as a container. Items can be traded between the container and the player's inventory."""
|
|
items = list(thing.customValues['items'])
|
|
thing.customValues['items'], timeOpen = self.getIO('container')(self.player, items)
|
|
return timeOpen
|
|
|
|
def info(self, thing, args):
|
|
"""Acts as a bookshelf, filing cabinet, bulletin board, or anything that contains raw info."""
|
|
items = dict(thing.customValues['items'])
|
|
return self.getIO('info')(items)
|
|
|
|
def wear(self, item, args):
|
|
"""Wear clotherg or otherwise equip a passive item."""
|
|
# An item must be in the player's inventory in order to wear it.
|
|
inInv = item in self.player.inventory
|
|
if not inInv:
|
|
print("You cannot wear what you are not carrying.", file = self.outstream)
|
|
return 0.0
|
|
if 'wearing' not in self.customValues:
|
|
self.customValues['wearing'] = {}
|
|
if item.customValues['slot'] not in self.customValues['wearing']:
|
|
self.customValues['wearing'][item.customValues['slot']] = self.player.removeThing(item)
|
|
# This is so a player can't put on a garment, then drop it while
|
|
# still also wearing it.
|
|
print("{} put on the {}.".format(self.player.name, item.name), file = self.outstream)
|
|
else: # the player must be wearing something that will get in the way
|
|
# put the old clothes back in the inventory
|
|
self.player.addThing(self.customValues['wearing'][item.customValues['slot']])
|
|
self.customValues['wearing'][item.customValues['slot']] = self.player.removeThing(item)
|
|
# This is so a player can't put on a garment, then drop it while
|
|
# still also wearing it.
|
|
print("{} took off the {} and put on the {}.".format(self.player.name, oldItem.name, item.name), file = self.outstream)
|
|
return 1.0
|
|
|
|
# item use-on functions
|
|
|
|
def key(self, key : _gt.Item, door : _gt.Door, args):
|
|
"""Item is a key, which unlocks a door. This may be implemented for containers later."""
|
|
if isinstance(door, tuple) or door.thingType != 'd':
|
|
print("That is not a door.", file = self.outstream)
|
|
return 0.0
|
|
if door.lock(key.name):
|
|
print("The key fits the lock.", file = self.outstream)
|
|
if not door.passable and self.player.x == door.x and self.player.y == door.y:
|
|
self.player.x, self.player.y = self.player.prevx, self.player.prevy
|
|
else:
|
|
print("The key doesn't fit that lock.", file = self.outstream)
|
|
return 0.125
|
|
|
|
# default scripts
|
|
|
|
def printfScript(self, args):
|
|
"""Assume the first argument is a stringable object, and format all others."""
|
|
ret = args[0].format(*[self.getValueFromString(i) for i in args[1:]])
|
|
print(ret, file = self.outstream)
|
|
return ret
|
|
|
|
def spawnThing(self, args):
|
|
"""Spawns a thing. [type name=... location=... etc]"""
|
|
if args[0].casefold() not in ('item', 'useable', 'npc', 'door', 'mapexit', 'mapentrance'):
|
|
raise GameError("{} not a spawnable thing type.".format(args[0]))
|
|
|
|
# Get the standard fields that most Things have in common: name, coordinates, graphic.
|
|
name = None
|
|
x, y = -1, -1
|
|
fgc, bgc, shape = None, None, None
|
|
persist = False
|
|
for i in args[1:]:
|
|
if i[0:5].casefold() == 'name=':
|
|
name = i[5:]
|
|
elif i[0:9].casefold() == 'location=':
|
|
_, x, y = _gu.parseCoords(self.level, i[9:].split(), usePlayerCoords = False)
|
|
elif i[0:4].casefold() == 'fgc=':
|
|
if len(i[4:]) != 7 or i[4] != '#':
|
|
raise GameError("Invalid color: {}".format(i[4:]))
|
|
for j in i[5:]:
|
|
if j not in '0123456789abcdefABCDEF':
|
|
raise GameError("Invalid color: {}".format(i[4:]))
|
|
fgc = i[4:]
|
|
elif i[0:4].casefold() == 'bgc=':
|
|
if len(i[4:]) != 7 or i[4] != '#':
|
|
raise GameError("Invalid color: {}".format(i[4:]))
|
|
for j in i[5:]:
|
|
if j not in '0123456789abcdefABCDEF':
|
|
raise GameError("Invalid color: {}".format(i[4:]))
|
|
bgc = i[4:]
|
|
elif i[0:6].casefold() == 'shape=':
|
|
if i[6] not in 'ox^ #|-':
|
|
raise GameError("Invalid shape: {}".format(i[6]))
|
|
shape = i[6]
|
|
elif i == 'persist':
|
|
persist = True
|
|
if x < 0 or y < 0:
|
|
raise GameError("Cannot spawn thing: bad location")
|
|
|
|
# Unfortunately, there needs to be a case for each type.
|
|
thing = None
|
|
if args[0].casefold() == 'item':
|
|
# spawn an item
|
|
description = 'A nondescript item.'
|
|
useFunc = ''
|
|
useOnFunc = ''
|
|
customValues = {}
|
|
ranged = False
|
|
if name == None:
|
|
name = 'item {}'.format(self.nextThing)
|
|
if bgc == None:
|
|
bgc = _gt.Item.defaultGraphic.background
|
|
if fgc == None:
|
|
fgc = _gt.Item.defaultGraphic.foreground
|
|
if shape == None:
|
|
shape = _gt.Item.defaultGraphic.shape
|
|
for i in args[1:]:
|
|
if i[0:12].casefold() == 'description=':
|
|
description = i[12:]
|
|
elif i[0:8].casefold() == 'usefunc=':
|
|
useFunc = i[8:]
|
|
elif i[0:10].casefold() == 'useonfunc=':
|
|
useOnFunc = i[10:]
|
|
elif i.casefold() == 'ranged':
|
|
ranged = True
|
|
elif i[0:3].casefold() == 'cv:':
|
|
cvEnv = customValues
|
|
equalsign = i.index('=')
|
|
cv = i[3:equalsign]
|
|
ns = cv.find(':')
|
|
while ns != -1:
|
|
cvEnv[cv[1:ns]] = {}
|
|
cvEnv = cvEnv[cv[1:ns]]
|
|
cvEnv[cv] = self.getValueFromString(i[equalsign+1:])
|
|
thing = _gt.Item(name, x, y, description, useFunc, useOnFunc, customValues, ranged, _gt.ThingGraphic(bgc, fgc, shape))
|
|
elif args[0].casefold() == 'useable':
|
|
# spawn a useable thing
|
|
description = 'A nondescript useable thing.'
|
|
useFunc = ''
|
|
customValues = {}
|
|
playerx, playery = x, y
|
|
if name == None:
|
|
name = 'useable {}'.format(self.nextThing)
|
|
if bgc == None:
|
|
bgc = _gt.Useable.defaultGraphic.background
|
|
if fgc == None:
|
|
fgc = _gt.Useable.defaultGraphic.foreground
|
|
if shape == None:
|
|
shape = _gt.Useable.defaultGraphic.shape
|
|
for i in args[1:]:
|
|
if i[0:12].casefold() == 'description=':
|
|
description = i[12:]
|
|
elif i[0:8].casefold() == 'usefunc=':
|
|
useFunc = i[8:]
|
|
elif i[0:10].casefold() == 'playerpos=':
|
|
_, playerx, playery = _gu.parseCoords(self.level, i[10:].split())
|
|
elif i[0:3].casefold() == 'cv:':
|
|
equalsign = i.index('=')
|
|
cv = i[3:equalsign]
|
|
customValues[cv] = self.getValueFromString(i[equalsign+1:])
|
|
thing = _gt.Useable(name, x, y, description, useFunc, customValues, playerx, playery, _gt.ThingGraphic(bgc, fgc, shape))
|
|
elif args[0].casefold() == 'npc':
|
|
# spawn an NPC
|
|
description = 'A nondescript character.'
|
|
behavior = 'stand'
|
|
inv = []
|
|
customValues = {}
|
|
playerx, playery = x, y
|
|
if name == None:
|
|
name = 'character {}'.format(self.nextThing)
|
|
if bgc == None:
|
|
bgc = _gt.NPC.defaultGraphic.background
|
|
if fgc == None:
|
|
fgc = _gt.NPC.defaultGraphic.foreground
|
|
if shape == None:
|
|
shape = _gt.NPC.defaultGraphic.shape
|
|
for i in args[1:]:
|
|
if i[0:12].casefold() == 'description=':
|
|
description = i[12:]
|
|
elif i[0:9].casefold() == 'behavior=':
|
|
useFunc = i[9:]
|
|
elif i[0:10].casefold() == 'playerpos=':
|
|
_, playerx, playery = _gu.parseCoords(self.level, i[10:].split())
|
|
elif i[0:3].casefold() == 'cv:':
|
|
equalsign = i.index('=')
|
|
cv = i[3:equalsign]
|
|
customValues[cv] = self.getValueFromString(i[equalsign+1:])
|
|
thing = _gt.NPC(name, x, y, description, behavior, inv, customValues, playerx, playery, _gt.ThingGraphic(bgc, fgc, shape))
|
|
elif args[0].casefold() == 'door':
|
|
# spawn a door
|
|
description = 'a nondescript door.'
|
|
locked = False
|
|
key = None
|
|
if name == None:
|
|
name = 'door {}'.format(self.nextThing)
|
|
if bgc == None:
|
|
bgc = _gt.Door.defaultGraphic.background
|
|
if fgc == None:
|
|
fgc = _gt.Door.defaultGraphic.foreground
|
|
if shape == None:
|
|
shape = _gt.Door.defaultGraphic.shape
|
|
for i in args[1:]:
|
|
if i[0:12].casefold() == 'description=':
|
|
description = i[12:]
|
|
elif i.casefold() == 'locked':
|
|
locked = True
|
|
thing = _gt.Door(name, x, y, locked, description, key, _gt.ThingGraphic(bgc, fgc, shape))
|
|
elif args[0].casefold() == 'mapexit':
|
|
# spawn an exit to another map (use with EXTREME caution!)
|
|
destination = ''
|
|
prefix = None
|
|
exitid = 0
|
|
onUse = ''
|
|
key = True
|
|
if name == None:
|
|
name = 'somewhere'
|
|
if bgc == None:
|
|
bgc = _gt.MapExit.defaultGraphic.background
|
|
if fgc == None:
|
|
fgc = _gt.MapExit.defaultGraphic.foreground
|
|
if shape == None:
|
|
shape = _gt.MapExit.defaultGraphic.shape
|
|
for i in args[1:]:
|
|
if i[0:12].casefold() == 'destination=':
|
|
destination = i[12:]
|
|
elif i[0:7].casefold() == 'prefix=':
|
|
prefix = i[7:]
|
|
elif i[0:7].casefold() == 'exitid=':
|
|
exitid = int(i[7:])
|
|
elif i[0:6].casefold() == 'onuse=':
|
|
onUse = i[6:]
|
|
elif i[0:4].casefold() == 'key=':
|
|
key = i[4:]
|
|
thing = _gt.MapExit(name, x, y, exitid, destination, prefix, onUse, key, _gt.ThingGraphic(bgc, fgc, shape))
|
|
elif args[0].casefold() == 'mapentrance':
|
|
# spawn a map entrance
|
|
exitid = 0
|
|
for i in args[1:]:
|
|
if i[0:7].casefold() == 'exitid=':
|
|
exitid = int(i[7:])
|
|
thing = _gt.MapEntrance(x, y, exitid, name)
|
|
elif args[0].casefold() == 'singleton':
|
|
if name in self.singletons:
|
|
single = self.singletons[name]
|
|
single.x, single.y = x, y
|
|
single.prevx, single.prevy = x, y
|
|
else:
|
|
raise GameError("{} not a valid thing type.".format(args[0]))
|
|
self.nextThing = self.level.addThing(thing, self.prefabs, self.nextThing, persist)
|
|
return thing
|
|
|
|
def giveToPlayer(self, args):
|
|
"""We get to assume it's an item."""
|
|
name = 'item {}'.format(self.nextThing)
|
|
x, y = self.player.x, self.player.y
|
|
fgc, bgc, shape = _gt.Item.defaultGraphic.foreground, _gt.Item.defaultGraphic.background, _gt.Item.defaultGraphic.shape
|
|
description = 'A nondescript item.'
|
|
persist = True
|
|
useFunc = ''
|
|
useOnFunc = ''
|
|
customValues = {}
|
|
ranged = False
|
|
for i in args[0:]:
|
|
if i[0:5].casefold() == 'name=':
|
|
name = i[5:]
|
|
elif i[0:10].casefold() == 'location=':
|
|
_, x, y = _gu.parseCoords(self.level, i[10:].split(), usePlayerCoords = False)
|
|
elif i[0:4].casefold() == 'fgc=':
|
|
if len(i[4:]) != 7 or i[0] != '#':
|
|
raise GameError("Invalid color: {}".format(i[4:]))
|
|
for j in i[4:]:
|
|
if j not in '0123456789abcdefABCDEF':
|
|
raise GameError("Invalid color: {}".format(i[4:]))
|
|
fgc = i[4:]
|
|
elif i[0:4].casefold() == 'bgc=':
|
|
if len(i[4:]) != 7 or i[0] != '#':
|
|
raise GameError("Invalid color: {}".format(i[4:]))
|
|
for j in i[4:]:
|
|
if j not in '0123456789abcdefABCDEF':
|
|
raise GameError("Invalid color: {}".format(i[4:]))
|
|
bgc = i[4:]
|
|
elif i[0:6].casefold() == 'shape=':
|
|
if len(i[6:]) != 7 or i[0] != '#':
|
|
raise GameError("Invalid color: {}".format(i[6:]))
|
|
for j in i[6:]:
|
|
if j not in '0123456789abcdefABCDEF':
|
|
raise GameError("Invalid color: {}".format(i[6:]))
|
|
shape = i[6:]
|
|
elif i == 'persist':
|
|
persist = True
|
|
elif i[0:12].casefold() == 'description=':
|
|
description = i[12:]
|
|
elif i[0:8].casefold() == 'usefunc=':
|
|
useFunc = i[8:]
|
|
elif i[0:10].casefold() == 'useonfunc=':
|
|
useOnFunc = i[10:]
|
|
elif i.casefold() == 'ranged':
|
|
ranged = True
|
|
elif i[0:3].casefold() == 'cv:':
|
|
equalsign = i.index('=')
|
|
cv = i[3:equalsign]
|
|
customValues[cv] = getValueFromString(i[equalsign+1:])
|
|
thing = _gt.Item(name, x, y, description, useFunc, useOnFunc, customValues, ranged, _gt.ThingGraphic(bgc, fgc, shape))
|
|
thing.thingID = self.nextThing
|
|
self.player.addThing(thing)
|
|
self.nextThing += 1
|
|
return thing
|
|
|
|
def moveThingScript(self, args):
|
|
colon = args.index(':')
|
|
thing, x, y = _gu.parseCoords(self.level, args[0:colon], usePlayerCoords = False)
|
|
if thing == None:
|
|
return False
|
|
_, newX, newY = self.parseCoords(args[colon+1:], usePlayerCoords = True, allowInventory = False)
|
|
self.level.moveThing(thing, newX, newY)
|
|
|
|
def cvget(self, args):
|
|
thing, x, y = _gu.parseCoords(self.level, args[0:-1], usePlayerCoords = False, player = self.player)
|
|
if thing != None and thing.thingType in 'ciu':
|
|
return _gs.getCustomValue(args[-1:], {'scriptLocal': thing.customValues, 'global': {}})
|
|
else:
|
|
raise GameError("Thing described in cvmod doesn't exist or isn't a character, item, or useable.")
|
|
|
|
def cvmod(self, args):
|
|
"""Modify a custom value in a thing.
|
|
args = thing identifier operator value"""
|
|
thing, x, y = _gu.parseCoords(args[0:-3], usePlayerCoords = False, player = self.player)
|
|
if thing != None and thing.thingType in 'ciu':
|
|
return _gs.setCustomValue(args[-3:], {'scriptLocal': thing.customValues, 'global': {}})
|
|
else:
|
|
raise GameError("Thing described in cvmod doesn't exist or isn't a character, item, or useable.")
|
|
|
|
def cvdel(self, args):
|
|
"""Delete custom values."""
|
|
colon = args.index(':')
|
|
thing, x, y = _gu.parseCoords(args[0:colon], usePlayerCoords = False, player = self.player)
|
|
if thing != None and thing.thingType in 'ciu':
|
|
return _gs.delCustomValue(args[colon+1:], {'scriptLocal': {}, 'global': thing.customValues})
|
|
else:
|
|
raise GameError("Thing described in cvmod doesn't exist or isn't a character, item, or useable.")
|
|
|
|
def playerCmdScript(self, args):
|
|
"""Run a player command from a script."""
|
|
self.getIO('playercmd')(line[1:])
|
|
return False
|
|
|
|
# Observer
|
|
|
|
def observe(self, e):
|
|
if self.level == None:
|
|
# Maybe not an error? Not sure yet.
|
|
return
|
|
for thingID in self.level.things:
|
|
thing = self.level.things[thingID]
|
|
if isinstance(thing, _gt.Observer):
|
|
if e.eventType in thing.behaviors:
|
|
if not thing.busy:
|
|
thing.busy = True
|
|
timeTaken = self.__behaviors[thing.behaviors[e.eventType][1]](thing, e)
|
|
if timeTaken >= 0:
|
|
self.setEvent(timeTaken, _ge.BehaveEvent(thing))
|
|
# We assume that otherwise, the behavior itself decides when it's done.
|
|
else:
|
|
# negative priority means just forget it;
|
|
# otherwise, lower number means higher priority
|
|
if thing.behaviors[e.eventType][0] >= 0:
|
|
#print("'{}' event added to {}'s queue".format(e.eventType, thing.name))
|
|
_hq.heappush(thing.behaviorQueue, (thing.behaviors[e.eventType][0], e))
|
|
|
|
# behaviors
|
|
|
|
def stand(self, actor):
|
|
return 0
|
|
|
|
def wander(self, actor):
|
|
return 0
|
|
|
|
def follow(self, actor, event):
|
|
# make sure we only follow who we want to
|
|
if "follow" not in actor.customValues:
|
|
return 0
|
|
if "isFollowing" not in actor.customValues["follow"]:
|
|
return 0
|
|
if actor.customValues["follow"]["isFollowing"]:
|
|
if event.actor.name != actor.customValues["follow"]["target"]:
|
|
if actor.customValues["follow"]["target"] == '':
|
|
# This is a quick-and-dirty fix for an issue I was having with followers.
|
|
actor.customValues["follow"]["target"] = actor
|
|
return 0
|
|
else:
|
|
dest = 0
|
|
#print(event.eventType)
|
|
if event.eventType == 'go':
|
|
dest = event.path[-1]
|
|
elif event.eventType == 'arrive':
|
|
dest = event.pos
|
|
x, y = self.level.intToCoords(dest)
|
|
self.smoothMove(actor, _gl.CircleLocus(x, y, 2), event.speed, action = lambda: self.setEvent(0, _ge.BehaveEvent(actor)), closeEnough = False)
|
|
return -1
|
|
else:
|
|
return 0
|
|
|
|
# stuff for extended classes to use
|
|
def registerUseFunc(self, name, func):
|
|
"""Registers a function for use by things in the map, but not directly
|
|
callable by the player."""
|
|
self.__useFuncs[name] = func
|
|
|
|
def registerBehavior(self, name, func):
|
|
"""Registers a function for use as an NPC's behavior."""
|
|
self.__behaviors[name] = func
|
|
|
|
def registerEvent(self, name, func):
|
|
"""Registers a function to handle an event in the event loop.
|
|
They should take the event as an argument, and return True if it should
|
|
always give the player a turn, False otherwise."""
|
|
if name in self.__gameEvents:
|
|
self.__gameEvents[name].add(func)
|
|
else:
|
|
self.__gameEvents[name] = set((func,))
|
|
#self.__gameEvents[name] = func
|
|
|
|
def registerIO(self, name, func):
|
|
"""Registers a function for useFuncs and such to use."""
|
|
self.__IOCalls[name] = func
|
|
|
|
def registerScript(self, name, func):
|
|
"""Registers a function as a script callable from yaml files."""
|
|
self.__scripts[name] = func
|
|
|
|
def getIO(self, name):
|
|
"""This is so derived classes can access their IOCalls."""
|
|
if name not in self.__IOCalls:
|
|
raise GameError("No IO call for {}.".format(name))
|
|
return self.__IOCalls[name]
|
|
|
|
# |\_/|
|
|
# /0 0\
|
|
# \o/
|