Add CFNPCDialog.py and the wrapper script dialog/npc_dialog.py, a wrapper script for advanced dialog handling.
git-svn-id: svn://svn.code.sf.net/p/crossfire/code/maps/trunk@12970 282e977c-c81d-0410-88c4-b93c2d0d6712master
parent
b42aba51ef
commit
1ceafbb899
|
@ -0,0 +1,610 @@
|
|||
# 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 CFDialog?
|
||||
# =================
|
||||
#
|
||||
# This is a small set of utility classes, to help you create 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 CFDialog
|
||||
# ===================
|
||||
#
|
||||
# First, create a script that imports the DialogRule and Dialog classes. Add
|
||||
# the following line at the beginning of your script:
|
||||
#
|
||||
# from CFDialog import DialogRule, Dialog
|
||||
#
|
||||
# Next, build the dialog by creating a sequence of several rules made up of
|
||||
# keywords, answers, preconditions, and postconditions. Optionally, define
|
||||
# prefunctions or postfunctions to enhance the capabilities of the rule.
|
||||
#
|
||||
# - Keywords are what the rule answers to. For example, if you want a rule to
|
||||
# trigger when the player says "hi", then "hi" must appear in the keyword
|
||||
# list. One or more keywords are specified in a string list in the form
|
||||
# ["keyword1", "keyword2" ...]. A "*" character is a special keyword that
|
||||
# means: "match everything", and is useful to create rules that provide
|
||||
# generic answers no matter what the player character says.
|
||||
#
|
||||
# NOTE: Like the @match system, CFDialog converts both keywords and the
|
||||
# things the player says to lowercase before checking for a match,
|
||||
# so it is never necessary to include multiple keywords that only
|
||||
# differ in case.
|
||||
#
|
||||
# - Answers are what the rule will respond, or say, to the player when it is
|
||||
# triggered. This is what the NPC replies to the player. Answers are stored
|
||||
# in a list of one or more strings in the form ["Answer1", "Answer2" ...].
|
||||
# When there is more than one answer in that list, each time the rule is
|
||||
# triggered, a single random reply will be selected from the list.
|
||||
#
|
||||
# NOTE: Answers may contain line breaks. To insert one, use "\n".
|
||||
#
|
||||
# - Preconditions are flags that must match specific values in order for a
|
||||
# rule to be triggered. These flags persist across gaming sessions and are
|
||||
# useful for tracking the state of a conversation with an NPC. Because of
|
||||
# this, it is possible for the same word to elicit different NPC responses
|
||||
# depending on how flags have been set. If dialogs are set to use identical
|
||||
# locations, the flags and preconditions can be used by other NPC dialogs so
|
||||
# that other NPCs can detect that the player heard specific information from
|
||||
# another NPC. The flags can also be used to help an individual NPC
|
||||
# remember what he has said to the player in the past. Flag settings are
|
||||
# stored in the player file, so they persist as long as the character exists
|
||||
# in the game. Each rule contains a list of one or more preconditions, if
|
||||
# any. Supply an empty list [] if no preconditions exist, but otherwise,
|
||||
# each of the preconditions is required to be a list that contains at least
|
||||
# a flag name and one or more values in the following format: [["flag1",
|
||||
# "value1", "value2" ...], ["flag2", "value3"] ...] where "..." indicates
|
||||
# that the pattern may be repeated. The flag name is always the first item
|
||||
# in a precondition list. ":" and ";" characters are forbidden in the flag
|
||||
# names and values. For a rule to be triggered, all its preconditions must
|
||||
# be satisfied by settings in the player file. To satisfy a precondition,
|
||||
# one of the precondition values must match the identified flag setting in
|
||||
# the player file. The default value of any precondition that has not been
|
||||
# specifically set in the player file is "0". If one of the precondition
|
||||
# values is set to "*", a match is not required.
|
||||
#
|
||||
# - Postconditions are state changes to apply to the player file flags after
|
||||
# the rule triggers. If a rule is not intended to set a flag, supply an
|
||||
# empty list [] when specifying postconditions, otherwise, postconditions
|
||||
# are supplied in a nested list that has the same format as the precondition
|
||||
# list except that each postcondition list only contains one value. This is
|
||||
# because the other main difference is that whereas a precondition checks a
|
||||
# player file to see if a flag has a certain value, the postcondition causes
|
||||
# a value to be stored into the player file, and it does not make sense to
|
||||
# store more than one value into a single flag. A value of "*" means that
|
||||
# the player file flag will not be changed.
|
||||
#
|
||||
# - 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, or 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 the rules are all defined, assemble them into a dialog. Each dialog
|
||||
# involves somebody who triggers it, somebody who answers, and also 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 choose whatever you want for the dialog name,
|
||||
# as long as it contains no whitespace or special characters, and as long as
|
||||
# it 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.
|
||||
#
|
||||
# A simple example
|
||||
# ================
|
||||
#
|
||||
# If I want to create a dialog for an old man, I might want him to respond to
|
||||
# "hello" or "hi" differently the first time the player meets the NPC, and
|
||||
# differently for subsequent encounters. In this example, grandpa greets the
|
||||
# player cordially the first time, but grumbles subequent times (because he's
|
||||
# like that, you know :)). This example grandpa also has a generic answer for
|
||||
# what ever else is said to him. In the example, the player is stored in
|
||||
# 'player', and the old man in 'grandpa', and 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 saves a value of "1" into a player file flag named "hello"
|
||||
## so grandpa remembers he has already met this player before.
|
||||
#
|
||||
# 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 a greeting if he as already met the player
|
||||
## before. Notice that "*" is used for the postcondition value, meaning that
|
||||
## the flag will remain set as it was prior to the rule triggering.
|
||||
#
|
||||
# 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)
|
||||
#
|
||||
## Finally, the generic answer is written. This is the last rule of the list.
|
||||
## We don't need to match any condition, and don't need to change any flags,
|
||||
## 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)
|
||||
#
|
||||
# The following link points to a page on the Crossfire Wiki shows all the
|
||||
# details needed to actually place this example in an actual game map:
|
||||
#
|
||||
# http://wiki.metalforge.net/doku.php/cfdialog?s=cfdialog#a_simple_example
|
||||
#
|
||||
# A more complex example
|
||||
# ======================
|
||||
#
|
||||
# A ./misc/npc_dialog.py script has been written that uses CFDialog, but
|
||||
# allows the dialog data to be written in a slightly different format.
|
||||
# ../scorn/kar/gork.msg is an example that uses multiple keywords and multiple
|
||||
# precondition values. Whereas the above example has a linear and predicable
|
||||
# conversation paths, note how a conversation with Gork can fork, merge, and
|
||||
# loop back on itself. The example also illustrates how CFDialog can allow
|
||||
# dialogs to affect how other NPCs react to a player. ../scorn/kar/mork.msg
|
||||
# is a completely different dialog, but it is part of a quest that requires
|
||||
# the player to interact with both NPCs in a specific way before the quest
|
||||
# prize can be obtained. With the old @match system, once the player knew
|
||||
# the key words, he could short-circuit the conversation the map designer
|
||||
# intended to occur. CFDialog constrains the player to follow the proper
|
||||
# conversation thread to qualify to receive the quest reward.
|
||||
#
|
||||
# Debugging
|
||||
# =========
|
||||
#
|
||||
# When debugging, if changes are made to this file, the Crossfire Server must
|
||||
# be restarted for it to register the changes.
|
||||
|
||||
import Crossfire
|
||||
import string
|
||||
import random
|
||||
import sys
|
||||
import CFItemBroker
|
||||
|
||||
class DialogRule:
|
||||
def __init__(self, keywords, presemaphores, messages, postsemaphores, suggested_response = None, required_response = None):
|
||||
self.__keywords = keywords
|
||||
self.__presems = presemaphores
|
||||
self.__messages = messages
|
||||
self.__postsems = postsemaphores
|
||||
self.__suggestions = suggested_response
|
||||
self.__requirements = required_response
|
||||
|
||||
# The keyword is a string. Multiple keywords may be defined in the string
|
||||
# by delimiting them with vertical bar (|) characters. "*" is a special
|
||||
# keyword that matches anything.
|
||||
def getKeyword(self):
|
||||
return self.__keywords
|
||||
|
||||
# Messages are stored in a list of strings. One or more messages may be
|
||||
# defined in the list. If more than one message is present, a random
|
||||
# string is returned.
|
||||
def getMessage(self):
|
||||
msg = self.__messages
|
||||
l = len(msg)
|
||||
r = random.randint(0, l - 1)
|
||||
return msg[r]
|
||||
|
||||
# Return the preconditions of a rule. They are a list of one or more lists
|
||||
# that specify a flag name to check, and one or more acceptable values it
|
||||
# may have in order to allow the rule to be triggered.
|
||||
def getPreconditions(self):
|
||||
return self.__presems
|
||||
|
||||
# Return the postconditions for a rule. They are a list of one or more
|
||||
# lists that specify a flag to be set in the player file and what value it
|
||||
# should be set to.
|
||||
def getPostconditions(self):
|
||||
return self.__postsems
|
||||
|
||||
# Return the possible responses to this rule
|
||||
# This is when a message is sent.
|
||||
def getSuggests(self):
|
||||
return self.__suggestions
|
||||
|
||||
# Return the required responses to this rule, this is like a suggestion,
|
||||
# except that no other response will make sense in context (eg, a yes/no question)
|
||||
def getRequires(self):
|
||||
return self.__requirements
|
||||
|
||||
class Dialog:
|
||||
# A character is the source that supplies keywords that drive the dialog.
|
||||
# The speaker is the NPC that responds to the keywords. A location is an
|
||||
# unique identifier that is used to distinguish dialogs from each other.
|
||||
def __init__(self, character, speaker, location):
|
||||
self.__character = character
|
||||
self.__location = location
|
||||
self.__speaker = speaker
|
||||
self.__rules = []
|
||||
|
||||
# Create rules of the DialogRule class that define dialog flow. An index
|
||||
# defines the order in which rules are processed. FIXME: addRule could
|
||||
# very easily create the index. It is unclear why this mundane activity
|
||||
# is left for the dialog maker.
|
||||
def addRule(self, rule, index):
|
||||
self.__rules.insert(index, rule)
|
||||
|
||||
# A function to call when saying something to an NPC to elicit a response
|
||||
# based on defined rules. It iterates through the rules and determines if
|
||||
# the spoken text matches a keyword. If so, the rule preconditions and/or
|
||||
# prefunctions are checked. If all conditions they define are met, then
|
||||
# the NPC responds, and postconditions, if any, are set. Postfunctions
|
||||
# also execute if present.
|
||||
# some variable substitution is done on the message here, $me and $you
|
||||
# are replaced by the names of the npc and the player respectively
|
||||
def speak(self, msg):
|
||||
for rule in self.__rules:
|
||||
if self.isAnswer(msg, rule.getKeyword()) == 1:
|
||||
print "Checking whether to say: ", rule.getMessage()
|
||||
if self.matchConditions(rule) == 1:
|
||||
message = rule.getMessage()
|
||||
message = message.replace('$me', self.__speaker.QueryName())
|
||||
message = message.replace('$you', self.__character.QueryName())
|
||||
|
||||
self.__speaker.Say(message)
|
||||
if rule.getRequires() == None:
|
||||
if rule.getSuggests() != None:
|
||||
self.__speaker.Say(rule.getSuggests())
|
||||
else:
|
||||
self.__speaker.Say(rule.getRequires())
|
||||
self.setConditions(rule)
|
||||
return 0
|
||||
return 1
|
||||
|
||||
# Determine if the message sent to an NPC matches a string in the keyword
|
||||
# list. The match check is case-insensitive, and succeeds if a keyword
|
||||
# string is found in the message. This means that the keyword string(s)
|
||||
# only need to be a substring of the message in order to trigger a reply.
|
||||
def isAnswer(self, msg, keywords):
|
||||
for ckey in keywords:
|
||||
if ckey == "*" or msg.lower().find(ckey.lower()) != -1:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
# Check the preconditions specified in rule have been met. Preconditions
|
||||
# are lists of one or more conditions to check. Each condition specifies
|
||||
# a check to perform and the options it should act on.
|
||||
# separate functions are called for each type of check.
|
||||
def matchConditions(self, rule):
|
||||
|
||||
for condition in rule.getPreconditions():
|
||||
#try:
|
||||
print 'attempting to match rule', condition
|
||||
if condition[0] == 'quest':
|
||||
print self.matchquest(condition[1:])
|
||||
if self.matchquest(condition[1:]) == 0:
|
||||
return 0
|
||||
elif condition[0] == 'item':
|
||||
print self.matchitem(condition[1:])
|
||||
if self.matchitem(condition[1:]) == 0:
|
||||
return 0
|
||||
elif condition[0] == 'level':
|
||||
print self.matchlevel(condition[1:])
|
||||
if self.matchlevel(condition[1:]) == 0:
|
||||
return 0
|
||||
elif condition[0] == 'age':
|
||||
print self.checkage(condition[1:])
|
||||
if self.checkage(condition[1:]) == 0:
|
||||
return 0
|
||||
elif condition[0] == 'token':
|
||||
print self.checktoken(condition[1:])
|
||||
if self.checktoken(condition[1:]) == 0:
|
||||
return 0
|
||||
else:
|
||||
Crossfire.Log(Crossfire.LogError, "CFDialog: Preconditon called with unknown action.")
|
||||
#except:
|
||||
# Crossfire.Log(Crossfire.LogDebug, "CFDialog: Bad Precondition")
|
||||
# return 0
|
||||
return 1
|
||||
|
||||
# Checks whether the token arg[0] has been set to any of the values in args[1] onwards
|
||||
def checktoken(self, args):
|
||||
status = self.getStatus(args[0])
|
||||
for value in args[1:]:
|
||||
if (status == value) or (value == "*"):
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
# This returns 1 if the token in args[0] was set at least
|
||||
# args[1] years
|
||||
# args[2] months
|
||||
# args[3] days
|
||||
# args[4] hours
|
||||
# args[5] minutes
|
||||
# ago (in game time).
|
||||
def checkage(self, args):
|
||||
# maximum months, days, hours, as defined by the server.
|
||||
# minutes per hour is hardcoded to approximately 60
|
||||
MAXTIMES = [Crossfire.Time.MONTHS_PER_YEAR, Crossfire.Time.WEEKS_PER_MONTH*Crossfire.Time.DAYS_PER_WEEK,
|
||||
Crossfire.Time.HOURS_PER_DAY, 60]
|
||||
# we have three times to consider, the old time, the current time, and the desired time difference.
|
||||
if len(args) != 6:
|
||||
return 0
|
||||
markername = args[0]
|
||||
oldtime = self.getStatus(markername).split("-")
|
||||
oldtime = map(int, oldtime)
|
||||
if len(oldtime) !=5:
|
||||
|
||||
# The marker hasn't been set yet
|
||||
return 0
|
||||
|
||||
desireddiff = map(int, args[1:])
|
||||
currenttime = (Crossfire.GetTime())[:5]
|
||||
actualdiff = []
|
||||
|
||||
for i in range(5):
|
||||
actualdiff.append(currenttime[i]-oldtime[i])
|
||||
|
||||
for i in range(4,0,-1):
|
||||
# let's tidy up desireddiff first
|
||||
if desireddiff[i] > MAXTIMES[i-1]:
|
||||
desireddiff[i-1] += desireddiff[i] // MAXTIMES[i-1]
|
||||
desireddiff[i] %= MAXTIMES[i-1]
|
||||
# Then actualdiff
|
||||
if actualdiff[i] < 0:
|
||||
actualdiff[i] += MAXTIMES[i-1]
|
||||
actualdiff[i-1] -=1
|
||||
|
||||
print 'tidied up desired difference', desireddiff
|
||||
print 'actual difference', actualdiff
|
||||
for i in range(5):
|
||||
if actualdiff[i] < desireddiff[i]:
|
||||
return 0
|
||||
return 1
|
||||
|
||||
|
||||
# is passed a list, returns 1 if the character has at least args[1] of an item called args[0]
|
||||
# if args[1] isn't given, then looks for 1 of the item.
|
||||
def matchitem(self, args):
|
||||
itemname = args[0]
|
||||
if len(args) == 2:
|
||||
quantity = args[1]
|
||||
else:
|
||||
quantity = 1
|
||||
if itemname == "money":
|
||||
if self.__character.Money >= int(quantity):
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
inv = self.__character.CheckInventory(itemname)
|
||||
if inv:
|
||||
if inv.Quantity >= int(quantity):
|
||||
return 1
|
||||
return 0
|
||||
|
||||
# is passed a list, returns 1 if the player is at least at stage args[1] in quest args[0]
|
||||
def matchquest(self, args):
|
||||
questname = args[0]
|
||||
stage = args[1]
|
||||
print 'I am looking for stage ', stage, ' current stage is ', self.__character.QuestGetState(questname)
|
||||
if stage == "complete":
|
||||
# todo: implement this
|
||||
pass
|
||||
if self.__character.QuestGetState(questname) < int(stage):
|
||||
return 0
|
||||
return 1
|
||||
|
||||
# is passed a list, returns 1 if the player is at least at level args[0] either overall or in the skill corresponding to any of the following arguments.
|
||||
def matchlevel(self, args):
|
||||
targetlevel = int(args[0])
|
||||
if len(args) == 1:
|
||||
if self.__character.Level >= targetlevel:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
else:
|
||||
pass
|
||||
#TODO
|
||||
|
||||
# If a rule triggers, this function is called to make identified player
|
||||
# file changes, and to call any declared postfunctions to implement more
|
||||
# dramatic effects than the setting of a flag in the player file.
|
||||
def setConditions(self, rule):
|
||||
for condition in rule.getPostconditions():
|
||||
# try:
|
||||
if 1:
|
||||
print 'trying to apply', condition
|
||||
action = condition[0]
|
||||
if action == 'quest':
|
||||
self.setquest(condition[1:])
|
||||
elif action == 'connection':
|
||||
self.__speaker.Map.TriggerConnected(int(condition[1]), 1, self.__speaker)
|
||||
elif action == 'takeitem':
|
||||
self.takeitem(condition[1:])
|
||||
elif action == 'giveitem':
|
||||
self.giveitem(condition[1:], False)
|
||||
elif action == 'givecontents':
|
||||
self.giveitem(condition[1:], True)
|
||||
elif action == 'marktime':
|
||||
self.marktime(condition[1:])
|
||||
elif action == 'settoken':
|
||||
self.setStatus(condition[1],condition[2])
|
||||
else:
|
||||
Crossfire.Log(Crossfire.LogError, "CFDialog: Post Block called with unknown action.")
|
||||
else:
|
||||
#except:
|
||||
Crossfire.Log(Crossfire.LogError, "CFDialog: Bad Postcondition")
|
||||
Crossfire.Log(Crossfire.LogError, sys.exc_info()[0])
|
||||
return 0
|
||||
|
||||
def marktime(self, args):
|
||||
markername = args[0]
|
||||
timestamp = map(str, (Crossfire.GetTime())[:5])
|
||||
self.setStatus(markername, "-".join(timestamp))
|
||||
|
||||
# moves player to stage args[1] of quest args[0]
|
||||
def setquest(self, args):
|
||||
questname = args[0]
|
||||
stage = args[1]
|
||||
if self.__character.QuestGetState(questname) == 0:
|
||||
print 'starting quest', questname, ' at stage ', stage
|
||||
self.__character.QuestStart(questname, int(stage))
|
||||
elif int(stage) > self.__character.QuestGetState(questname):
|
||||
print 'advancing quest', questname, 'to stage ', stage
|
||||
self.__character.QuestSetState(questname, int(stage))
|
||||
else:
|
||||
Crossfire.Log(Crossfire.LogError, "CFDialog: Tried to advance a quest backwards.")
|
||||
|
||||
# places args[1] copies of item called args[0], into the inventory of the player.
|
||||
# the item must be in the inventory of the NPC first for this to work.
|
||||
# if args[1] is not specified, assume this means 1 copy of the item.
|
||||
# if contents is 'true' then we don't give the player the item, but treat this item as a container,
|
||||
# and give the player the exact contents of the container.
|
||||
def giveitem(self, args, contents):
|
||||
itemname = args[0]
|
||||
if len(args) == 2:
|
||||
quantity = int(args[1])
|
||||
else:
|
||||
quantity = 1
|
||||
if itemname == "money":
|
||||
# we can't guarentee that the player has any particular type of coin already
|
||||
# so create the object first, then add 1 less than the total.
|
||||
if quantity >= 50:
|
||||
id = self.__character.CreateObject('platinum coin')
|
||||
CFItemBroker.Item(id).add(int(quantity/50))
|
||||
if quantity % 50 > 0:
|
||||
id = self.__character.CreateObject('gold coin')
|
||||
CFItemBroker.Item(id).add(int((quantity % 50)/10))
|
||||
if quantity % 50 > 0:
|
||||
id = self.__character.CreateObject('silver coin')
|
||||
CFItemBroker.Item(id).add(int(quantity % 10))
|
||||
else:
|
||||
# what we will do, is increase the number of items the NPC is holding, then
|
||||
# split the stack into the players inventory.
|
||||
# first we will check if there is an NPC_Gift_Box, and look in there.
|
||||
lookin = self.__speaker.CheckInventory("NPC_Gift_Box")
|
||||
if lookin:
|
||||
inv = lookin.CheckInventory(itemname)
|
||||
if not inv:
|
||||
# ok, the NPC has no 'Gift Box', we'll check the other items.
|
||||
inv = self.__speaker.CheckInventory(itemname)
|
||||
else:
|
||||
inv = self.__speaker.CheckInventory(itemname)
|
||||
|
||||
if inv:
|
||||
if contents:
|
||||
nextob=inv.Inventory
|
||||
while nextob:
|
||||
# when giving the contents of a container, always give the
|
||||
# number of items in the container, not the quantity number.
|
||||
quantity = nextob.Quantity
|
||||
if quantity == 0:
|
||||
# if quantity is 0, then we need to set it to one, otherwise bad things happen.
|
||||
nextob.Quantity = 1
|
||||
quantity = 1
|
||||
newob = nextob.Clone(0)
|
||||
newob.Quantity = quantity
|
||||
newob.InsertInto(self.__character)
|
||||
nextob=nextob.Below
|
||||
else:
|
||||
if quantity == 0:
|
||||
nextob.Quantity = 2
|
||||
quantity = 1
|
||||
else:
|
||||
CFItemBroker.Item(inv).add(quantity+1)
|
||||
newob = inv.Split(quantity)
|
||||
|
||||
newob.InsertInto(self.__character)
|
||||
else:
|
||||
# ok, we didn't find any
|
||||
Crossfire.Log(Crossfire.LogError, "Dialog script tried to give a non-existant item to a player")
|
||||
|
||||
# removes args[1] copies of item called args[0], this should only be used if
|
||||
# you have checked the player has those items beforehand.
|
||||
# if args[1] is zero, will take all copies of the item from the first matching stack.
|
||||
# if it is not specified, take 1 copy of the item.
|
||||
def takeitem(self, args):
|
||||
itemname = args[0]
|
||||
if len(args) == 2:
|
||||
quantity = args[1]
|
||||
else:
|
||||
quantity = 1
|
||||
print 'trying to take ', quantity, ' of item ', itemname
|
||||
if itemname == "money":
|
||||
paid = self.__character.PayAmount(int(quantity))
|
||||
if paid == 0:
|
||||
Crossfire.Log(Crossfire.LogError, "Tried to make player pay more than they had")
|
||||
else:
|
||||
inv = self.__character.CheckInventory(itemname)
|
||||
if inv:
|
||||
if quantity == 0:
|
||||
CFItemBroker.Item(inv).subtract(inv.Quantity)
|
||||
else:
|
||||
CFItemBroker.Item(inv).subtract(int(quantity))
|
||||
# we might have been wearing an item that was taken.
|
||||
self.__character.Fix()
|
||||
else:
|
||||
Crossfire.Log(Crossfire.LogError, "Dialog script tried to remove non-existant item from player")
|
||||
|
||||
|
||||
# Search the player file for a particular flag, and if it exists, return
|
||||
# its value. Flag names are combined with the unique dialog "location"
|
||||
# identifier, and are therefore are not required to be unique. This also
|
||||
# prevents flags from conflicting with other non-dialog-related contents
|
||||
# in the player file.
|
||||
def getStatus(self, key):
|
||||
character_status=self.__character.ReadKey("dialog_"+self.__location);
|
||||
if character_status == "":
|
||||
return "0"
|
||||
pairs=character_status.split(";")
|
||||
for i in pairs:
|
||||
subpair=i.split(":")
|
||||
if subpair[0] == key:
|
||||
return subpair[1]
|
||||
return "0"
|
||||
|
||||
# Store a flag in the player file and set it to the specified value. Flag
|
||||
# names are combined with the unique dialog "location" identifier, and are
|
||||
# therefore are not required to be unique. This also prevents flags from
|
||||
# conflicting with other non-dialog-related contents in the player file.
|
||||
def setStatus(self, key, value):
|
||||
if value == "*":
|
||||
return
|
||||
ishere = 0
|
||||
finished = ""
|
||||
character_status = self.__character.ReadKey("dialog_"+self.__location);
|
||||
if character_status != "":
|
||||
pairs = character_status.split(";")
|
||||
for i in pairs:
|
||||
subpair = i.split(":")
|
||||
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)
|
|
@ -0,0 +1,139 @@
|
|||
# npc_dialog.py - Dialog helper class
|
||||
#
|
||||
# Copyright (C) 2007 David Delbecq
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
#
|
||||
# This is a simple script that makes use of CFDialog.py and that receives
|
||||
# parameters from a JSON inside the event message. Alternatively, the JSON
|
||||
# parameters, if >= 4096 characters, can be stored in a separate file.
|
||||
# Use the classical script parameter to specify relative location of dialog.
|
||||
#
|
||||
# An example of a map file entry is:
|
||||
#
|
||||
# arch guildmaster
|
||||
# name Sigmund
|
||||
# msg
|
||||
#
|
||||
# endmsg
|
||||
# x 11
|
||||
# y 7
|
||||
# resist_physical 100
|
||||
# resist_magic 100
|
||||
# weight 50000000
|
||||
# friendly 1
|
||||
# stand_still 1
|
||||
# arch event_say
|
||||
# name start/sigmund.msg
|
||||
# title Python
|
||||
# slaying /python/misc/npc_dialog.py
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# An example of a JSON dialog similar to the one described in CFDialog.py is:
|
||||
#
|
||||
# {
|
||||
# "location" : "test_grandpa_01",
|
||||
# "rules": [
|
||||
# {
|
||||
# "match" : ["hello","hi"],
|
||||
# "pre" : [["hello","0"]],
|
||||
# "post" : [["hello","1"]],
|
||||
# "msg" : ["Hello, lad!","Hi, young fellow!","Howdy!"]
|
||||
# },
|
||||
# {
|
||||
# "match": ["hello","hi"],
|
||||
# "pre" :[["hello","1"]],
|
||||
# "post" :[["hello", "*"]],
|
||||
# "msg" : ["I've heard, you know, I'm not deaf *grmbl*"]
|
||||
# },
|
||||
# {
|
||||
# "match" : ["*"],
|
||||
# "pre" : [["hello","*"]],
|
||||
# "post" : [["hello", "*"]],
|
||||
# "msg" : ["What ?", "Huh ?", "What do you want ?"]
|
||||
# }
|
||||
# ]}
|
||||
#
|
||||
# For detailed descriptions of the match, pre, post, and msg formats, see the
|
||||
# ../CFDialog.py script.
|
||||
#
|
||||
# "match" is a list of keyword strings, and corresponds to what the player says
|
||||
# that the dialog will respond to.
|
||||
#
|
||||
# In the above example, the first rule is applied if the player/character says
|
||||
# "hello" or "hi" and if the "hello" flag is set to "0" (default). When the
|
||||
# rule is applied, the "hello" flag is then set to "1".
|
||||
#
|
||||
# "pre" is a list of preconditions that identifies flags that must be set to a
|
||||
# particular value in order to trigger a response if a match is detected.
|
||||
#
|
||||
# "post" is a list of postconditions that specify flags that are to be set if a
|
||||
# response is triggered.
|
||||
#
|
||||
# All of the rule values are lists, and must be enclosed by square braces, but
|
||||
# pre and post are lists of lists, so the nested square braces ([[]]) are
|
||||
# required except that using an empty list [] is the best way to indicate when
|
||||
# the rule does not need to check preconditions or set postconditions.
|
||||
#
|
||||
# "msg" defines one or more responses that will be given if the rule triggers.
|
||||
# When more than one "msg" value is set up, the NPC randomly selects which one
|
||||
# to say each time the rule is applied.
|
||||
#
|
||||
# A relatively complex example of an npc_dialog.py dialog is given in the Gork
|
||||
# treasure room quest. See ../scorn/kar/gork.msg in particular as it
|
||||
# demonstrates how multiple precondition flag values may be exploited to
|
||||
# produce non-linear and variable-path conversations that are less likely to
|
||||
# frustrate a player. Refer also to ../scorn/kar/mork.msg to see how more
|
||||
# than one dialog can reference the same dialog flags.
|
||||
|
||||
import Crossfire
|
||||
import os
|
||||
from CFNPCDialog import DialogRule, Dialog
|
||||
import cjson
|
||||
|
||||
npc = Crossfire.WhoAmI()
|
||||
event = Crossfire.WhatIsEvent()
|
||||
player = Crossfire.WhoIsActivator()
|
||||
if (Crossfire.ScriptParameters() != None):
|
||||
filename = os.path.join(Crossfire.DataDirectory(),
|
||||
Crossfire.MapDirectory(),
|
||||
Crossfire.ScriptParameters())
|
||||
try:
|
||||
f = open(filename,'rb')
|
||||
except:
|
||||
Crossfire.Log(Crossfire.LogDebug, "Error loading NPC dialog %s" % filename)
|
||||
raise
|
||||
else:
|
||||
Crossfire.Log(Crossfire.LogDebug, "Loading NPC dialog %s" % filename)
|
||||
parameters = cjson.decode(f.read())
|
||||
f.close()
|
||||
else:
|
||||
parameters = cjson.decode(event.Message)
|
||||
location = parameters["location"];
|
||||
speech = Dialog(player, npc, location)
|
||||
index = 0;
|
||||
|
||||
for jsonRule in parameters["rules"]:
|
||||
rule = DialogRule(jsonRule["match"],
|
||||
jsonRule["pre"],
|
||||
jsonRule["msg"],
|
||||
jsonRule["post"])
|
||||
speech.addRule(rule, index)
|
||||
index = index + 1
|
||||
|
||||
if speech.speak(Crossfire.WhatIsMessage()) == 0:
|
||||
Crossfire.SetReturnValue(1)
|
Loading…
Reference in New Issue