import sys
import os
from os.path import dirname, realpath, join
sys.path.append(join(dirname(realpath(__file__)), "../CommonFunctions"))
from setCalico import *
from utilityFunctions import *
from makeShapeFunctions import makePic
from response import Response
from Myro import getRobot
[docs]class Thread(object):
"""
Keeps a list of consecutive reponses of different NPCs in a conversation.
Can be associated with an area, such that the Thread only becomes active
when the players is in that area.
"""
def __init__(self, actor=None):
"""
Constructs a Thread object.
By default the a Thread is always active if it contains responses, but
if an actor is passed to the constructor, the thread will only be active
if player is within a certain area of that actor.
Args:
actor: A :class:`SpriteActor` associated with this response.
"""
#: A list of responses
self.responses = []
#: Index pointing to the current response
self.currentResponseIndex = 0
#: Area that indicates where this thread is active
self.area = None
self.cachedPortrait = None
self.debug=False
self.blockNextSkip = False
if actor:
self.setActor(actor)
[docs] def setActor(self, actor, size=150):
"""
Sets an actor for this Thread.
When an actor is set, as square area around the actor will be set as the
area for this Thread. In addition, the image returned by getAvatar()
will be used as the portrait of this Thread.
Args:
actor: The SpriteActor to associate with this Thread.
size: The size of the area around the actor in which this Thread
will be active.
"""
self.area = [actor.getX()-size, actor.getX()+size,
actor.getY()-size, actor.getY()+size]
self.cachedPortrait = Picture(actor.getAvatar())
[docs] def addResponse(self, portrait=None, text=None, action=None, sticky=False):
"""
Adds a response to this Thread.
Args:
portait: The portrait that will be shown with this response.
text: A string containing the text associated with this response.
action: A function executed when this response is 'spoken'.
sticky: If sticky is True, this response will not be removed from
the Thread, meaning that this response will be repeated over and
untill some other event 'unsticks' it.
"""
if not self.hasNext() and self.currentResponse().sticky:
if self.debug:
print("Current response was sticky: current response skipped.")
self.currentResponseIndex += 1
if isinstance(portrait, Response):
if self.debug:
print("Manually added response:", portrait.text)
self.responses.append(portrait)
elif (text is not None) and isinstance(portrait, tuple):
# If portrait is a tuple, assume it is a portrait-action pair
if action is not None:
action = [portrait[1], action]
else:
action = [portrait[1]]
if self.debug:
print("Portrait action pair (experimental):", text)
r = Response(text, action, makePic(portrait[0]), sticky)
self.responses.append(r)
elif (text is None) or hasattr(text, "__call__"):
if self.debug:
print("Text as first argument (deprecated):", portrait)
r = Response(portrait, text, makePic(self.cachedPortrait), sticky)
self.responses.append(r)
else:
if self.debug: print("Added text:", text)
self.cachedPortrait = portrait
r = Response(text, action, makePic(portrait), sticky)
self.responses.append(r)
[docs] def blockSkip(self):
"""
Blocks the next skip command for this Thread.
Blocking the skip command may be necessary to prevent skipping
animations that would otherwise block progress.
"""
self.blockNextSkip = True
[docs] def setResponse(self, portrait=None, text="", action=None, sticky=False):
"""
Adds a response to this speech bubble and make it the current response.
Args:
text: A string containing the text associated with this response.
action: A function executed when this response is 'spoken'.
"""
if self.debug: print("Response set")
self.currentResponseIndex = len(self.responses)
self.addResponse(portrait, text, action, sticky)
[docs] def currentResponse(self):
"""
Retrieve the current response.
Returns:
A Response object.
"""
if self.currentResponseIndex >= len(self.responses):
return Response("")
return self.responses[self.currentResponseIndex]
[docs] def active(self):
"""
Indicates whether this Thread is active.
By default a Thread will be active is the player is within the provided
area, or if no such area is defined for this Thread.
Return:
Boolean indicating whether this Thread is active.
"""
if self.currentResponseIndex >= len(self.responses):
return False
bb = self.area
if not bb:
return True
r = getRobot()
if not r:
return False
x = r.frame.x
y = r.frame.y
return x > bb[0] and x < bb[1] and y > bb[2] and y < bb[3]
[docs] def setArea(self, actor, size=150):
"""
Sets the area of this Thread around an actor.
In contrast to setActor, this will not set the portrait of this Thread.
Args:
actor: The actor around whom to center the area.
size: The size of the area.
"""
self.area = [actor.getX()-size, actor.getX()+size,
actor.getY()-size, actor.getY()+size]
[docs] def hasNext(self):
"""
Indicates whether this Thread has a next response.
Return:
True if this Thread has a response left, False otherwise.
"""
return self.currentResponseIndex < (len(self.responses)-1)
[docs] def hasCurrent(self):
"""
Indicates whether this Thread has a current response.
Return:
True if this Thread has a current response, False otherwise.
"""
return self.currentResponseIndex < len(self.responses)
[docs] def nextResponse(self):
"""
Skips to the next response, if there is one.
"""
if self.currentResponse().sticky and not self.hasNext(): return
if(self.hasCurrent()):
if self.debug:
print("Passed response:", self.currentResponse().text)
self.currentResponseIndex += 1
[docs] def skip(self,launchAction=True):
"""
Skips all responses and launches all actions, except sticky ones.
This should allow skipping in conversations where the last response
should never be removed.
Args:
launchAction: If set to False, reponses are still skipped, but the
associated actions are not executed.
"""
if self.debug: print("Responses skipped")
while self.hasCurrent() and not self.blockNextSkip:
if self.debug:
print("Launched action for: ", self.currentResponse().text)
# Temporarily storing response so we the response is skipped before
# its action is launched.
tempResponse = self.currentResponse()
self.nextResponse()
tempResponse.launchActions()
#if self.debug:
# print("Launched action for: ", self.currentResponse().text)
#self.currentResponse().launchActions()
#self.nextResponse()
[docs] def clear(self,launchAction=True):
"""
Clears all responses and launches any actions associated with them.
The difference between :meth:`skip` and :meth:`clear` is that clear()
ignores sticky actions or :meth:`blockSkip`, and guarentees that the
response list is empty once called. Essentially resets the
:class:`Thread`.
Args:
launchAction: If set to False, reponses are still skipped, but the
associated actions are not executed.
"""
if self.debug: print("Responses cleared")
if launchAction:
while self.hasNext():
self.currentResponse().sticky = False
self.currentResponse().launchActions()
self.nextResponse()
self.currentResponse().launchActions()
self.responses=[]
self.currentResponseIndex = 0
self.area = None
self.cachedPortrait = None
[docs]class Conversation(object):
"""
Class for keeping track of a conversation between actors.
A conversation can consist of several threads, which can be activated
or deactivated seperately. Using different threads can be useful when
a conversation has a choice, or when you want certain threads to be
active only when the player is close enough to the relevant NPS.
If no threads are added, all responses will be added to the default
thread (threads[0]).
"""
def __init__(self):
"""
Creates a Conversation object.
"""
self.threads = [Thread()]
#self.responses = []
#self.currentResponseIndex = 0
self.blocked = False
self.autoUnpause = False
self.speechBoxYPos = None
self.debug=False
[docs] def getActiveThread(self):
"""
Returns the currently active thread.
Returns the first active thread from the threads list. if no threads
are active, the default (threads[0]) thread is returned.
Return:
The first active Thread object.
"""
for thread in self.threads:
if thread.active():
return thread
return self.threads[0]
[docs] def addThread(self, thread):
"""
Adds a new conversation Thread to the conversation.
Args:
thread: The Thread to add.
"""
self.threads.append(thread)
[docs] def addResponse(self, portrait=None, text="", action=None, sticky=False):
"""
Adds a response to the default Thread.
Args:
text: A string containing the text associated with this response.
action: A function executed when this response is 'spoken'.
"""
self.threads[0].addResponse(portrait, text, action, sticky)
#if isinstance(text, Response):
# self.responses.append(text)
#else:
# r = Response(text, action, makePic(portrait), sticky)
# self.responses.append(r)
[docs] def setResponse(self, portrait=None, text="", action=None, sticky=False):
"""
Adds a response to this speech bubble and make it the current response.
Args:
text: A string containing the text associated with this response.
action: A function executed when this response is 'spoken'.
"""
self.currentResponseIndex = len(self.responses)
self.addResponse(portrait, text, action, sticky)
[docs] def currentResponse(self):
"""
Retrieve the current response of the default thread.
Returns:
A Response object.
"""
if self.blocked:
return Response("")
return self.threads[0].currentResponse()
[docs] def hasNext(self):
"""
Checks if the default thread has a next response available.
Return:
Boolean indicating whether the default thread has a response left.
"""
return self.threads[0].hasNext()
[docs] def nextResponse(self):
"""
Skips to the next response, if there is one.
"""
self.threads[0].nextResponse()
[docs] def block(self):
"""
Sets the conversation to blocked.
While the conversation is blocked, calling the current response will
return an empty response.
"""
self.blocked = True
[docs] def unblock(self):
"""
Unblocks the conversation.
"""
self.blocked = False
[docs] def setDebug(self, value=True):
"""
Enables debug mode for this conversation, printing extra information.
Args:
value: Booleans indicating whether to enable (True) or disable
(false) debug mode.
"""
self.debug=value
for thread in self.threads:
thread.debug=value