246 lines
10 KiB
Python
246 lines
10 KiB
Python
# CFDialog.py - Dialog helper class
|
|
#
|
|
# Copyright (C) 2007 Yann Chachkoff
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
#
|
|
# The author can be reached via e-mail at lauwenmark@gmail.com
|
|
#
|
|
|
|
# What is this about ?
|
|
#=======================
|
|
# This is a small set of utility classes, to help you creating
|
|
# complex dialogs. It is made for those who do not want to
|
|
# bother about complex programming, but just want to make a few
|
|
# dialogs that are better than the @match system used in the
|
|
# server.
|
|
#
|
|
# How to use this.
|
|
#=======================
|
|
# First, you need to import DialogRule and Dialog classes. Add the
|
|
# following line at the beginning of your script:
|
|
# from CFDialog import DialogRule, Dialog
|
|
# Then, you can go to the dialogs themselves. A dialog is made of
|
|
# several rules. Each rule is made of keywords, preconditions,
|
|
# postconditions, answers and pre/postfunction.
|
|
# - Keywords are what the rule will be an answer to. For example, if
|
|
# you want a rule to be triggered when the player will say "hi",
|
|
# then "hi" is the keyword to use. You can associate more than a
|
|
# keyword to a rule by concatenating them with the "|" character.
|
|
# Finally, "*" is a special keyword that means: "match everything".
|
|
# "*" is useful to provide generic answers.
|
|
# - Answers are what will be said when the rule is triggered. This is
|
|
# what the NPC replies to the player. Answers are stored in a list.
|
|
# When there is more than one answer in that list, one will be
|
|
# selected at random.
|
|
# - Preconditions are flags that must match for the rule to be triggered.
|
|
# Each precondition has a name and a value. The default value of a
|
|
# precondition is "0". The flags are stored into each player, and will
|
|
# survive between gaming sessions. They are useful to set the point in a
|
|
# dialog reached by a player - you can see those as an "NPC memory". All
|
|
# conditions must have a name and a value. The ":" and ";" characters are
|
|
# forbidden. For a rule to be triggered, all the player's flags should match
|
|
# the preconditions. A precondition is a list in the form: [key, value].
|
|
# All preconditions are stored in a list. Each "value" may be a single
|
|
# string or a multiple choice string where the options are concatenated
|
|
# together with the "|" character. When a value is set to "*", a match is
|
|
# not required to trigger the rule.
|
|
# - Postconditions are state changes to apply to the player's conditions
|
|
# after the rule has been triggered. The postcondition is a list in the
|
|
# form: [key, value] and the format is similar to preconditions except that
|
|
# the "|" has no special meaning in the value and is forbidden. A value of
|
|
# "*" means that the condition will not be touched.
|
|
# - A prefunction is an optional callback function that will be called
|
|
# when a rule's preconditions are all matched, but before the rule is
|
|
# validated.
|
|
# The callback can do additional tests, and should return 1 to allow the rule
|
|
# to be selected, 0 to block the rule. The function arguments are the player
|
|
# and the actual rule being tested.
|
|
# - A postfunction is an optional callback that is called when a rule has
|
|
# been applied, and after the message is said. It can do additional custom
|
|
# processing. The function arguments are the player and the actual rule
|
|
# having been used.
|
|
#
|
|
# Once you have defined your rules, you have to assemble them into a dialog.
|
|
# Each dialog involves somebody who triggers it, somebody who answers, and
|
|
# has a unique name so it cannot be confused with other dialogs.
|
|
# Typically, the "one who triggers" will be the player, and the "one who
|
|
# answers" is an NPC the player was taking to. You are free to chose whatever
|
|
# you want for the dialog name, as long as it contains no space or special
|
|
# characters, and is not used by another dialog.
|
|
# You can then add the rules you created to the dialog. Rules are parsed in
|
|
# a given order, so you must add the most generic answer last.
|
|
#
|
|
# Like the @match system, CFDialog converts both match strings and the things
|
|
# the player says to lowercase before checking for a match.
|
|
#
|
|
# A simple example
|
|
#=================
|
|
# I want to create a dialog for an old man. If I say "hello" or "hi" for the
|
|
# first time, grandpa will greet me. If I say it for the second time, he'll
|
|
# grumble (because he's like that, you know :)). I also need a generic answer
|
|
# if I say whatever else.
|
|
# In this example, the player is stored in 'player', and the old man in 'grandpa'.
|
|
# What the player said is in 'message'.
|
|
#
|
|
## Dialog creation:
|
|
# speech = Dialog(player, grandpa, "test_grandpa_01")
|
|
#
|
|
## The first rule is the "hello" answer, so we place it at index 0 of the
|
|
## rules list. The precondition is that we never said hello before.
|
|
## The postcondition is to mark "hello" as "1", to remember we already
|
|
## greeted grandpa.
|
|
# prer = [["hello","0"]]
|
|
# postr = [["hello", "1"]]
|
|
# rmsg = ["Hello, lad!","Hi, young fellow!","Howdy!"]
|
|
# speech.addRule(DialogRule("hello|hi", prer, rmsg, postr),0)
|
|
#
|
|
## The second rule is the answer to an hello if we already said it before.
|
|
## Notice that we used "*" for the postcondition value, meaning that we
|
|
## are leaving it as it is.
|
|
# prer = [["hello","1"]]
|
|
# postr = [["hello", "*"]]
|
|
# rmsg = ["I've heard, you know, I'm not deaf *grmbl*"]
|
|
# speech.addRule(DialogRule("hello|hi", prer, rmsg, postr),1)
|
|
#
|
|
## And finally, the generic answer. This is the last rule of the list.
|
|
## We don't need to match any condition, and we don't need to change them,
|
|
## so we use "*" in both cases this time.
|
|
# prer = [["hello","*"]]
|
|
# postr = [["hello", "*"]]
|
|
# rmsg = ["What ?", "Huh ?", "What do you want ?"]
|
|
# speech.addRule(DialogRule("*", prer, rmsg, postr),2)
|
|
#
|
|
# A more complex example
|
|
# ======================
|
|
# ../scorn/kar/gork.msg is an example that uses "|" in the precondition. Note
|
|
# how this lets the conversation fork and merge. Once Gork tells us he has a
|
|
# friend who lives in a tower, the player can fork the conversation to either
|
|
# a friend or a tower thread. Without the "|" in the precondition, it would
|
|
# be harder to construct the conversation in a way that either fork merges
|
|
# back into the main thread, while also allowing the player the player to
|
|
# "remember" something Gork said previously and still use it in the original
|
|
# context. Note also how easy it is to allow the conversation to loop about
|
|
# mid-thread.
|
|
#
|
|
import Crossfire
|
|
import string
|
|
import random
|
|
|
|
class DialogRule:
|
|
def __init__(self,keyword,presemaphores, message, postsemaphores, prefunction = None, postfunction = None):
|
|
self.__keyword = keyword
|
|
self.__presems = presemaphores
|
|
self.__message = message
|
|
self.__postsems= postsemaphores
|
|
self.__prefunction = prefunction
|
|
self.__postfunction = postfunction
|
|
def getKeyword(self):
|
|
return self.__keyword
|
|
def getMessage(self):
|
|
msg = self.__message
|
|
l = len(msg)
|
|
r = random.randint(0,l-1)
|
|
return msg[r]
|
|
def getPreconditions(self):
|
|
return self.__presems
|
|
def getPostconditions(self):
|
|
return self.__postsems
|
|
def getPrefunction(self):
|
|
return self.__prefunction
|
|
def getPostfunction(self):
|
|
return self.__postfunction
|
|
|
|
class Dialog:
|
|
def __init__(self,character,speaker,location):
|
|
self.__character = character
|
|
self.__location = location
|
|
self.__speaker = speaker
|
|
self.__rules = []
|
|
|
|
def addRule(self, rule, index):
|
|
self.__rules.insert(index,rule)
|
|
|
|
def speak(self, msg):
|
|
for rule in self.__rules:
|
|
if self.isAnswer(msg, rule.getKeyword())==1:
|
|
if self.matchConditions(rule)==1:
|
|
self.__speaker.Say(rule.getMessage())
|
|
self.setConditions(rule)
|
|
return 0
|
|
return 1
|
|
|
|
def isAnswer(self,msg, keyword):
|
|
if keyword=="*":
|
|
return 1
|
|
keys=string.split(keyword,"|")
|
|
for ckey in keys:
|
|
if string.find(msg.lower(),ckey.lower())!=-1:
|
|
return 1
|
|
return 0
|
|
|
|
def matchConditions(self,rule):
|
|
for condition in rule.getPreconditions():
|
|
status = self.getStatus(condition[0])
|
|
values=string.split(condition[1],"|")
|
|
for value in values:
|
|
if (status==value) or (value=="*"):
|
|
break
|
|
else:
|
|
return 0
|
|
if rule.getPrefunction() <> None:
|
|
return rule.getPrefunction()(self.__character, rule)
|
|
return 1
|
|
|
|
def setConditions(self,rule):
|
|
for condition in rule.getPostconditions():
|
|
key = condition[0]
|
|
val = condition[1]
|
|
if val!="*":
|
|
self.setStatus(key,val)
|
|
if rule.getPostfunction() <> None:
|
|
rule.getPostfunction()(self.__character, rule)
|
|
|
|
def getStatus(self,key):
|
|
character_status=self.__character.ReadKey("dialog_"+self.__location);
|
|
if character_status=="":
|
|
return "0"
|
|
pairs=string.split(character_status,";")
|
|
for i in pairs:
|
|
subpair=string.split(i,":")
|
|
if subpair[0]==key:
|
|
return subpair[1]
|
|
return "0"
|
|
|
|
def setStatus(self,key, value):
|
|
character_status=self.__character.ReadKey("dialog_"+self.__location);
|
|
finished=""
|
|
ishere=0
|
|
if character_status!="":
|
|
pairs=string.split(character_status,";")
|
|
for i in pairs:
|
|
subpair=string.split(i,":")
|
|
if subpair[0]==key:
|
|
subpair[1]=value
|
|
ishere=1
|
|
if finished!="":
|
|
finished=finished+";"
|
|
finished=finished+subpair[0]+":"+subpair[1]
|
|
if ishere==0:
|
|
if finished!="":
|
|
finished=finished+";"
|
|
finished=finished+key+":"+value
|
|
self.__character.WriteKey("dialog_"+self.__location, finished, 1)
|