ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/server/ext/NPC_Dialogue.pm
Revision: 1.20
Committed: Fri Mar 19 21:40:39 2010 UTC (14 years, 2 months ago) by root
Branch: MAIN
Changes since 1.19: +3 -2 lines
Log Message:
*** empty log message ***

File Contents

# User Rev Content
1 root 1.1 =head1 NAME
2    
3     NPC_Dialogue
4    
5     =head1 DESCRIPTION
6    
7     NPC dialogue support module.
8    
9 root 1.5 =over 4
10    
11 root 1.1 =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 root 1.4 $arg{ob} = $arg{pl}->ob;
28    
29 root 1.1 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 root 1.18 =item $find - see @find, below.
102 root 1.15
103 root 1.1 =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 elmex 1.6 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 elmex 1.7 =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 root 1.9 @eval my @g = grep { $_->slaying =~ /Gramp's walking stick/ } $who->inv; $g[0]->decrease;
127 elmex 1.7 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 elmex 1.6 =back
137    
138 root 1.1 =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 root 1.15 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 root 1.18 failing, it gathers all objects into an array and provides a reference to
162     the array in the C<$find> variable.
163 root 1.15
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 root 1.18 @cond @$find
170 root 1.15 It looks dirty.
171     @match see my spellbook
172     I can't see any, where do you have it?
173 root 1.1
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 root 1.20 topic is skipped. For more complex comparisons, see C<@cond> with
188 root 1.1 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 root 1.16 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 root 1.1 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 root 1.16 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 root 1.1
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 elmex 1.2 =item @trigger connected-id [state]
235 root 1.1
236 elmex 1.2 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 root 1.14 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 elmex 1.2
248     Trigger all objects with the given connected-id by 'releasing' the connection.
249 root 1.1
250 root 1.8 =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 root 1.1 =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 elmex 1.3 if ($_ eq "*" || $lcmsg =~ /\b\Q$_\E\b/i) {
279 root 1.1 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 root 1.15 my @find;
289    
290 root 1.1 my %vars = (
291     who => $self->{ob},
292     npc => $self->{npc},
293     state => $state,
294     flag => $flag,
295     msg => $msg,
296     match => \@match,
297 root 1.18 find => \@find,
298 root 1.1 );
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 root 1.8 } 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 root 1.1 } 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 root 1.15 } 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 root 1.1 } 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 root 1.19 defined $value ? $flag->{$name} = $value
347     : delete $flag->{$name};
348 root 1.1
349     } elsif ($cmd eq "setstate") {
350     my ($name, $value) = split /\s+/, $args, 2;
351 root 1.19 defined $value ? $state->{$name} = $value
352     : delete $state->{$name};
353 root 1.1
354     } elsif ($cmd eq "ifflag") {
355     my ($name, $value) = split /\s+/, $args, 2;
356 root 1.17 defined $value ? $flag->{$name} eq $value
357     : $flag->{$name}
358 root 1.1 or next topic;
359    
360     } elsif ($cmd eq "ifstate") {
361     my ($name, $value) = split /\s+/, $args, 2;
362 root 1.20 defined $value ? $state->{$name} eq $value
363     : $state->{$name}
364 root 1.1 or next topic;
365    
366     } elsif ($cmd eq "trigger") {
367 elmex 1.2 my ($con, $state) = split /\s+/, $args, 2;
368    
369     if (defined $state) {
370 root 1.14 $self->{npc}->map->trigger ($con, $state, $self->{npc}, $self->{ob});
371 elmex 1.2 } else {
372 root 1.14 my $rvalue = \$self->{npc}{dialog_trigger}{$con+0};
373 root 1.13 $self->{npc}->map->trigger ($con, $$rvalue = !$$rvalue, $self->{npc}, $self->{ob});
374 elmex 1.2 }
375 root 1.1
376     } elsif ($cmd eq "addtopic") {
377     push @kw, split /\|/, $args;
378     $self->{add_topic}->(split /\s*\|\s*/, $args) if $self->{add_topic};
379    
380     } elsif ($cmd eq "deltopic") {
381     # not yet implemented, do it out-of-band
382     $self->{del_topic}->(split /\s*\|\s*/, $args) if $self->{del_topic};
383    
384     } else {
385     warn "unknown dialogue command <$cmd,$args> used (from " . $self->{npc}->msg . ")";
386     }
387     }
388    
389     delete $self->{npc}{$self->{ob}->name}{dialog_state} unless %$state;
390     delete $self->{ob}{dialog_flag} unless %$flag;
391    
392     # ignores flags and npc from replies
393     $reply = join "\n", (map $_->[1], @replies), $reply;
394    
395     # now mark up all matching keywords
396     for my $match (@{ $self->{match} }) {
397     for (sort { (length $b) <=> (length $a) } split /\|/, $match->[0]) {
398     if ($reply =~ /\b\Q$_\E\b/i) {
399     push @kw, $_;
400     last;
401     }
402     }
403     }
404    
405 root 1.13 $self->{npc}->use_trigger ($self->{ob})
406     if $self->{npc}->type == cf::MAGIC_EAR;
407 root 1.12
408 root 1.1 return wantarray ? ($reply, @kw) : $reply;
409     }
410     }
411     }
412    
413     ()
414     }
415    
416     1