ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/server/ext/NPC_Dialogue.pm
Revision: 1.22
Committed: Tue May 4 22:49:21 2010 UTC (14 years ago) by root
Branch: MAIN
CVS Tags: rel-3_1, rel-3_0, HEAD
Changes since 1.21: +1 -1 lines
Log Message:
more common sense

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