ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/server/ext/NPC_Dialogue.pm
Revision: 1.5
Committed: Sat Jun 16 23:22:59 2007 UTC (16 years, 11 months ago) by root
Branch: MAIN
Changes since 1.4: +2 -0 lines
Log Message:
reorganised documentation and improved it

File Contents

# Content
1 =head1 NAME
2
3 NPC_Dialogue
4
5 =head1 DESCRIPTION
6
7 NPC dialogue support module.
8
9 =over 4
10
11 =cut
12
13 package NPC_Dialogue;
14
15 use strict;
16
17 sub has_dialogue($) {
18 my ($ob) = @_;
19
20 $ob->msg =~ /^\@match /;
21 }
22
23 sub parse_message($) {
24 map [split /\n/, $_, 2],
25 grep length,
26 split /^\@match /m,
27 $_[0]
28 }
29
30 sub new {
31 my ($class, %arg) = @_;
32
33 $arg{ob} = $arg{pl}->ob;
34
35 my $self = bless {
36 %arg,
37 }, $class;
38
39 $self->{match} ||= [parse_message $self->{npc}->msg];
40
41 $self;
42 }
43
44 sub greet {
45 my ($self) = @_;
46
47 $self->tell ("hi")
48 }
49
50 =item ($reply, @topics) = $dialog->tell ($msg)
51
52 Tells the dialog object something and returns its response and optionally
53 a number of topics that are refered to by this topic.
54
55 It supports a number of command constructs. They have to follow the
56 C<@match> directive, and there can be multiple commands that will be
57 executed in order.
58
59 =over 4
60
61 =item @comment text...
62
63 A single-line comment. It will be completely ignored.
64
65 =item @parse regex
66
67 Parses the message using a perl regular expression (by default
68 case-insensitive). Any matches will be available as C<< $match->[$index]
69 >>.
70
71 If the regular expression does not match, the topic is skipped.
72
73 Example:
74
75 @match deposit
76 @parse deposit (\d+) (\S+)
77 @eval bank::deposit $match->[0], $match->[1]
78
79 =item @cond perl
80
81 Evaluates the given perl code. If it returns false (or causes an
82 exception), the topic will be skipped, otherwise topic interpretation is
83 resumed.
84
85 The following local variables are defined within the expression:
86
87 =over 4
88
89 =item $who - The cf::object::player object that initiated the dialogue.
90
91 =item $npc - The NPC (or magic_ear etc.) object that is being talked to.
92
93 =item $msg - The actual message as passed to this method.
94
95 =item $match - An arrayref with previous results from C<@parse>.
96
97 =item $state - A hashref that stores state variables associated
98 with the NPC and the player, that is, it's values relate to the the
99 specific player-NPC interaction and other players will see a different
100 state. Useful to react to players in a stateful way. See C<@setstate> and
101 C<@ifstate>.
102
103 =item $flag - A hashref that stores flags associated with the player and
104 can be seen by all NPCs (so better name your flags uniquely). This is
105 useful for storing e.g. quest information. See C<@setflag> and C<@ifflag>.
106
107 =back
108
109 The environment is that standard "map scripting environment", which is
110 limited in the type of constructs allowed (no loops, for example).
111
112 =item @eval perl
113
114 Like C<@cond>, but proceed regardless of the outcome.
115
116 =item @msg perl
117
118 Like C<@cond>, but the return value will be stringified and prepended to
119 the message.
120
121 =item @setstate state value
122
123 Sets the named state C<state> to the given C<value>. State values are
124 associated with a specific player-NPC pair, so each NPC has its own state
125 with respect to a particular player, which makes them useful to store
126 information about previous questions and possibly answers. State values
127 get reset whenever the NPC gets reset.
128
129 See C<@ifstate> for an example.
130
131 =item @ifstate state value
132
133 Requires that the named C<state> has the given C<value>, otherwise this
134 topic is skipped. For more complex comparisons, see C<@cond> with
135 C<$state>. Example:
136
137 @match quest
138 @setstate question quest
139 Do you really want to help find the magic amulet of Beeblebrox?
140 @match yes
141 @ifstate question quest
142 Then fetch it, stupid!
143
144 =item @setflag flag value
145
146 Sets the named flag C<flag> to the given C<value>. Flag values are
147 associated with a specific player and can be seen by all NPCs. with
148 respect to a particular player, which makes them suitable to store quest
149 markers and other information (e.g. reputation/alignment). Flags are
150 persistent over the lifetime of a player, so be careful :)
151
152 See C<@ifflag> for an example.
153
154 =item @ifflag flag value
155
156 Requires that the named C<flag> has the given C<value>, otherwise this
157 topic is skipped. For more complex comparisons, see C<@cond> with
158 C<$flag>. Example:
159
160 @match I want to do the quest!
161 @setflag kings_quest 1
162 Then seek out Bumblebee in Navar, he will tell you...
163 @match I did the quest
164 @ifflag kings_quest 1
165 Really, which quets?
166
167 And Bumblebee might have:
168
169 @match hi
170 @ifflag kings_quest
171 Hi, I was told you want to do the kings quest?
172
173 =item @trigger connected-id [state]
174
175 Trigger all objects with the given connected-id.
176
177 When the state argument is omitted the trigger is stateful and retains an
178 internal state per connected-id. There is a limitation to the use of this: The
179 state won't be changed when the connection is triggered by other triggers. So
180 be careful when triggering the connection from other objects.
181
182 When a state argument is given it should be either 0 or 1. 1 will 'push' the connection
183 and 0 will 'release' the connection. This is useful for example when you want to
184 let a npc control a door.
185
186 Trigger all objects with the given connected-id by 'releasing' the connection.
187
188 =item @addtopic topic
189
190 Adds the given topic names (separated by C<|>) to the list of topics
191 returned.
192
193 =back
194
195 =cut
196
197 sub tell {
198 my ($self, $msg) = @_;
199
200 my $lcmsg = lc $msg;
201
202 topic:
203 for my $match (@{ $self->{match} }) {
204 for (split /\|/, $match->[0]) {
205 if ($_ eq "*" || $lcmsg =~ /\b\Q$_\E\b/i) {
206 my $reply = $match->[1];
207 my @kw;
208
209 my @replies;
210 my @match; # @match/@parse command results
211
212 my $state = $self->{npc}{$self->{ob}->name}{dialog_state} ||= {};
213 my $flag = $self->{ob}{dialog_flag} ||= {};
214
215 my %vars = (
216 who => $self->{ob},
217 npc => $self->{npc},
218 state => $state,
219 flag => $flag,
220 msg => $msg,
221 match => \@match,
222 );
223
224 local $self->{ob}{record_replies} = \@replies;
225
226 # now execute @-commands (which can result in a no-match)
227 while ($reply =~ s/^\@(\w+)\s*([^\n]*)\n?//) {
228 my ($cmd, $args) = ($1, $2);
229
230 if ($cmd eq "parse" || $cmd eq "match") { # match is future rename
231 no re 'eval'; # default, but make sure
232 @match = $msg =~ /$args/i
233 or next topic;
234
235 } elsif ($cmd eq "comment") {
236 # nop
237
238 } elsif ($cmd eq "cond") {
239 cf::safe_eval $args, %vars
240 or next topic;
241
242 } elsif ($cmd eq "eval") {
243 cf::safe_eval $args, %vars;
244 warn "\@eval evaluation error: $@\n" if $@;
245
246 } elsif ($cmd eq "msg") {
247 push @replies, [$self->{npc}, (scalar cf::safe_eval $args, %vars)];
248
249 } elsif ($cmd eq "setflag") {
250 my ($name, $value) = split /\s+/, $args, 2;
251 $value ? $flag->{$name} = $value
252 : delete $flag->{$name};
253
254 } elsif ($cmd eq "setstate") {
255 my ($name, $value) = split /\s+/, $args, 2;
256 $value ? $state->{$name} = $value
257 : delete $state->{$name};
258
259 } elsif ($cmd eq "ifflag") {
260 my ($name, $value) = split /\s+/, $args, 2;
261 $flag->{$name} eq $value
262 or next topic;
263
264 } elsif ($cmd eq "ifstate") {
265 my ($name, $value) = split /\s+/, $args, 2;
266 $state->{$name} eq $value
267 or next topic;
268
269 } elsif ($cmd eq "trigger") {
270 my ($con, $state) = split /\s+/, $args, 2;
271 $con = $con * 1;
272
273 if (defined $state) {
274 $self->{npc}->map->trigger ($args, $state);
275 } else {
276 my $rvalue = \$self->{npc}{dialog_trigger}{$con};
277 $self->{npc}->map->trigger ($con, $$rvalue = !$$rvalue);
278 }
279
280 } elsif ($cmd eq "addtopic") {
281 push @kw, split /\|/, $args;
282 $self->{add_topic}->(split /\s*\|\s*/, $args) if $self->{add_topic};
283
284 } elsif ($cmd eq "deltopic") {
285 # not yet implemented, do it out-of-band
286 $self->{del_topic}->(split /\s*\|\s*/, $args) if $self->{del_topic};
287
288 } else {
289 warn "unknown dialogue command <$cmd,$args> used (from " . $self->{npc}->msg . ")";
290 }
291 }
292
293 delete $self->{npc}{$self->{ob}->name}{dialog_state} unless %$state;
294 delete $self->{ob}{dialog_flag} unless %$flag;
295
296 # combine lines into paragraphs
297 $reply =~ s/(?<=\S)\n(?=\w)/ /g;
298 $reply =~ s/\n\n/\n/g;
299
300 # ignores flags and npc from replies
301 $reply = join "\n", (map $_->[1], @replies), $reply;
302
303 # now mark up all matching keywords
304 for my $match (@{ $self->{match} }) {
305 for (sort { (length $b) <=> (length $a) } split /\|/, $match->[0]) {
306 if ($reply =~ /\b\Q$_\E\b/i) {
307 push @kw, $_;
308 last;
309 }
310 }
311 }
312
313 return wantarray ? ($reply, @kw) : $reply;
314 }
315 }
316 }
317
318 ()
319 }
320
321 1