ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/server/ext/NPC_Dialogue.pm
Revision: 1.18
Committed: Wed Nov 4 19:20:49 2009 UTC (14 years, 6 months ago) by root
Branch: MAIN
CVS Tags: rel-2_90, rel-2_92
Changes since 1.17: +5 -3 lines
Log Message:
*** empty log message ***

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 parse_message($) {
18 map [split /\n/, $_, 2],
19 grep length,
20 split /^\@match /m,
21 $_[0]
22 }
23
24 sub new {
25 my ($class, %arg) = @_;
26
27 $arg{ob} = $arg{pl}->ob;
28
29 my $self = bless {
30 %arg,
31 }, $class;
32
33 $self->{match} ||= [parse_message $self->{npc}->msg];
34
35 $self;
36 }
37
38 sub greet {
39 my ($self) = @_;
40
41 $self->tell ("hi")
42 }
43
44 =item ($reply, @topics) = $dialog->tell ($msg)
45
46 Tells the dialog object something and returns its response and optionally
47 a number of topics that are refered to by this topic.
48
49 It supports a number of command constructs. They have to follow the
50 C<@match> directive, and there can be multiple commands that will be
51 executed in order.
52
53 =over 4
54
55 =item @comment text...
56
57 A single-line comment. It will be completely ignored.
58
59 =item @parse regex
60
61 Parses the message using a perl regular expression (by default
62 case-insensitive). Any matches will be available as C<< $match->[$index]
63 >>.
64
65 If the regular expression does not match, the topic is skipped.
66
67 Example:
68
69 @match deposit
70 @parse deposit (\d+) (\S+)
71 @eval bank::deposit $match->[0], $match->[1]
72
73 =item @cond perl
74
75 Evaluates the given perl code. If it returns false (or causes an
76 exception), the topic will be skipped, otherwise topic interpretation is
77 resumed.
78
79 The following local variables are defined within the expression:
80
81 =over 4
82
83 =item $who - The cf::object::player object that initiated the dialogue.
84
85 =item $npc - The NPC (or magic_ear etc.) object that is being talked to.
86
87 =item $msg - The actual message as passed to this method.
88
89 =item $match - An arrayref with previous results from C<@parse>.
90
91 =item $state - A hashref that stores state variables associated
92 with the NPC and the player, that is, it's values relate to the the
93 specific player-NPC interaction and other players will see a different
94 state. Useful to react to players in a stateful way. See C<@setstate> and
95 C<@ifstate>.
96
97 =item $flag - A hashref that stores flags associated with the player and
98 can be seen by all NPCs (so better name your flags uniquely). This is
99 useful for storing e.g. quest information. See C<@setflag> and C<@ifflag>.
100
101 =item $find - see @find, below.
102
103 =back
104
105 The environment is that standard "map scripting environment", which is
106 limited in the type of constructs allowed (no loops, for example).
107
108 Here is a example:
109
110 =over 4
111
112 =item B<matching for an item name>
113
114 @match hi
115 @cond grep $_->name =~ /royalty/, $who->inv
116 You got royalties there! Wanna have!
117
118 You may want to change the C<name> method there to something like C<title>,
119 C<slaying> or any other method that is allowed to be called on a
120 C<cf::object> here.
121
122 =item B<matching for an item name and removing the matched item>
123
124 @match found earhorn
125 @cond grep $_->slaying =~ /Gramp's walking stick/, $who->inv
126 @eval my @g = grep { $_->slaying =~ /Gramp's walking stick/ } $who->inv; $g[0]->decrease;
127 Thanks for the earhorn!
128
129 This example is a bit more complex. The C<@eval> statement will search
130 the players inventory for the same term as the C<@cond> and then
131 decreases the number of objects used there.
132
133 (See also the map: C<scorn/houses/cornerbrook.map> for an example how this is
134 used in the real world :-)
135
136 =back
137
138 =item @eval perl
139
140 Like C<@cond>, but proceed regardless of the outcome.
141
142 =item @msg perl
143
144 Like C<@cond>, but the return value will be stringified and prepended to
145 the reply message.
146
147 =item @check match expression
148
149 Executes a match expression (see
150 http://pod.tst.eu/http://cvs.schmorp.de/deliantra/server/lib/cf/match.pm)
151 to see if it matches.
152
153 C<self> is the npc object, C<object>, C<source> and C<originator> are the
154 player communicating with the NPC.
155
156 If the check fails, the match is skipped.
157
158 =item @find match expression
159
160 Like C<@check> in that it executes a match expression, but instead of
161 failing, it gathers all objects into an array and provides a reference to
162 the array in the C<$find> variable.
163
164 When you want to skip the match when no objects have been found, combine
165 C<@find> with C<@cond>:
166
167 @match see my spellbook
168 @find type=SPELLBOOK in inv
169 @cond @$find
170 It looks dirty.
171 @match see my spellbook
172 I can't see any, where do you have it?
173
174 =item @setstate state value
175
176 Sets the named state C<state> to the given C<value>. State values are
177 associated with a specific player-NPC pair, so each NPC has its own state
178 with respect to a particular player, which makes them useful to store
179 information about previous questions and possibly answers. State values
180 get reset whenever the NPC gets reset.
181
182 See C<@ifstate> for an example.
183
184 =item @ifstate state value
185
186 Requires that the named C<state> has the given C<value>, otherwise this
187 topic is skipped. For more complex comparisons, see C<@cond> with
188 C<$state>. Example:
189
190 @match quest
191 @setstate question quest
192 Do you really want to help find the magic amulet of Beeblebrox?
193 @match yes
194 @ifstate question quest
195 Then fetch it, stupid!
196
197 =item @setflag flag value
198
199 Sets the named flag C<flag> to the given C<value>. Flag values are
200 associated with a specific player and can be seen by all NPCs. with
201 respect to a particular player, which makes them suitable to store quest
202 markers and other information (e.g. reputation/alignment). Flags are
203 persistent over the lifetime of a player, so be careful :)
204
205 Perversely enough, using C<@setfflag> without a C<value> clears the flag
206 as if it was never set, so always provide a flag value (e.g. C<1>) when
207 you want to set the flag.
208
209 See C<@ifflag> for an example.
210
211 =item @ifflag flag value
212
213 Requires that the named C<flag> has the given C<value>, otherwise this
214 topic is skipped. For more complex comparisons, see C<@cond> with
215 C<$flag>.
216
217 If no C<value> is given, then the ifflag succeeds when the flag is true.
218
219 Example:
220
221 @match I want to do the quest!
222 @setflag kings_quest 1
223 Then seek out Bumblebee in Navar, he will tell you...
224 @match I did the quest
225 @ifflag kings_quest 1
226 Really, which quets?
227
228 And Bumblebee might have:
229
230 @match hi
231 @ifflag kings_quest
232 Hi, I was told you want to do the kings quest?
233
234 =item @trigger connected-id [state]
235
236 Trigger all objects with the given connected-id.
237
238 When the state argument is omitted the trigger is stateful and retains an
239 internal state per connected-id. There is a limitation to the use of this: The
240 state won't be changed when the connection is triggered by other triggers. So
241 be careful when triggering the connection from other objects.
242
243 When a state argument is given it should be a positive integer. Any value
244 C<!= 0> will 'push' the connection (in general, you should specify C<1>
245 for this) and C<0> will 'release' the connection. This is useful for
246 example when you want to let an NPC control a door.
247
248 Trigger all objects with the given connected-id by 'releasing' the connection.
249
250 =item @playersound face-name
251
252 Plays the given sound face (either an alias or sound file path) so that
253 only the player talking to the npc can hear it.
254
255 =item @npcsound face-name
256
257 Plays the given sound face (either an alias or sound file path) as if
258 the npc had made that sound, i.e. it will be located at the npc and all
259 players near enough can hear it.
260
261 =item @addtopic topic
262
263 Adds the given topic names (separated by C<|>) to the list of topics
264 returned.
265
266 =back
267
268 =cut
269
270 sub tell {
271 my ($self, $msg) = @_;
272
273 my $lcmsg = lc $msg;
274
275 topic:
276 for my $match (@{ $self->{match} }) {
277 for (split /\|/, $match->[0]) {
278 if ($_ eq "*" || $lcmsg =~ /\b\Q$_\E\b/i) {
279 my $reply = $match->[1];
280 my @kw;
281
282 my @replies;
283 my @match; # @match/@parse command results
284
285 my $state = $self->{npc}{$self->{ob}->name}{dialog_state} ||= {};
286 my $flag = $self->{ob}{dialog_flag} ||= {};
287
288 my @find;
289
290 my %vars = (
291 who => $self->{ob},
292 npc => $self->{npc},
293 state => $state,
294 flag => $flag,
295 msg => $msg,
296 match => \@match,
297 find => \@find,
298 );
299
300 local $self->{ob}{record_replies} = \@replies;
301
302 # now execute @-commands (which can result in a no-match)
303 while ($reply =~ s/^\@(\w+)\s*([^\n]*)\n?//) {
304 my ($cmd, $args) = ($1, $2);
305
306 if ($cmd eq "parse" || $cmd eq "match") { # match is future rename
307 no re 'eval'; # default, but make sure
308 @match = $msg =~ /$args/i
309 or next topic;
310
311 } elsif ($cmd eq "comment") {
312 # nop
313
314 } elsif ($cmd eq "playersound") {
315 $self->{ob}->contr->play_sound (cf::sound::find $args);
316
317 } elsif ($cmd eq "npcsound") {
318 $self->{npc}->play_sound (cf::sound::find $args);
319
320 } elsif ($cmd eq "cond") {
321 cf::safe_eval $args, %vars
322 or next topic;
323
324 } elsif ($cmd eq "eval") {
325 cf::safe_eval $args, %vars;
326 warn "\@eval evaluation error: $@\n" if $@;
327
328 } elsif ($cmd eq "check") {
329 eval {
330 cf::match::match $args, $self->{ob}, $self->{npc}, $self->{ob}
331 or next topic;
332 };
333 warn "\@check evaluation error: $@\n" if $@;
334
335 } elsif ($cmd eq "find") {
336 @find = eval {
337 cf::match::match $args, $self->{ob}, $self->{npc}, $self->{ob}
338 };
339 warn "\@find evaluation error: $@\n" if $@;
340
341 } elsif ($cmd eq "msg") {
342 push @replies, [$self->{npc}, (scalar cf::safe_eval $args, %vars)];
343
344 } elsif ($cmd eq "setflag") {
345 my ($name, $value) = split /\s+/, $args, 2;
346 $value ? $flag->{$name} = $value
347 : delete $flag->{$name};
348
349 } elsif ($cmd eq "setstate") {
350 my ($name, $value) = split /\s+/, $args, 2;
351 $value ? $state->{$name} = $value
352 : delete $state->{$name};
353
354 } elsif ($cmd eq "ifflag") {
355 my ($name, $value) = split /\s+/, $args, 2;
356 defined $value ? $flag->{$name} eq $value
357 : $flag->{$name}
358 or next topic;
359
360 } elsif ($cmd eq "ifstate") {
361 my ($name, $value) = split /\s+/, $args, 2;
362 $state->{$name} eq $value
363 or next topic;
364
365 } elsif ($cmd eq "trigger") {
366 my ($con, $state) = split /\s+/, $args, 2;
367
368 if (defined $state) {
369 $self->{npc}->map->trigger ($con, $state, $self->{npc}, $self->{ob});
370 } else {
371 my $rvalue = \$self->{npc}{dialog_trigger}{$con+0};
372 $self->{npc}->map->trigger ($con, $$rvalue = !$$rvalue, $self->{npc}, $self->{ob});
373 }
374
375 } elsif ($cmd eq "addtopic") {
376 push @kw, split /\|/, $args;
377 $self->{add_topic}->(split /\s*\|\s*/, $args) if $self->{add_topic};
378
379 } elsif ($cmd eq "deltopic") {
380 # not yet implemented, do it out-of-band
381 $self->{del_topic}->(split /\s*\|\s*/, $args) if $self->{del_topic};
382
383 } else {
384 warn "unknown dialogue command <$cmd,$args> used (from " . $self->{npc}->msg . ")";
385 }
386 }
387
388 delete $self->{npc}{$self->{ob}->name}{dialog_state} unless %$state;
389 delete $self->{ob}{dialog_flag} unless %$flag;
390
391 # ignores flags and npc from replies
392 $reply = join "\n", (map $_->[1], @replies), $reply;
393
394 # now mark up all matching keywords
395 for my $match (@{ $self->{match} }) {
396 for (sort { (length $b) <=> (length $a) } split /\|/, $match->[0]) {
397 if ($reply =~ /\b\Q$_\E\b/i) {
398 push @kw, $_;
399 last;
400 }
401 }
402 }
403
404 $self->{npc}->use_trigger ($self->{ob})
405 if $self->{npc}->type == cf::MAGIC_EAR;
406
407 return wantarray ? ($reply, @kw) : $reply;
408 }
409 }
410 }
411
412 ()
413 }
414
415 1