ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/server/ext/NPC_Dialogue.pm
Revision: 1.16
Committed: Mon Oct 26 02:48:02 2009 UTC (14 years, 7 months ago) by root
Branch: MAIN
Changes since 1.15: +10 -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.15 =item @find - see @find, below.
102    
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     failing, it gathers all objects matched into the C<@find> array variable.
162    
163     When you want to skip the match when no objects have been found, combine
164     C<@find> with C<@cond>:
165    
166     @match see my spellbook
167     @find type=SPELLBOOK in inv
168     @cond @find
169     It looks dirty.
170     @match see my spellbook
171     I can't see any, where do you have it?
172 root 1.1
173     =item @setstate state value
174    
175     Sets the named state C<state> to the given C<value>. State values are
176     associated with a specific player-NPC pair, so each NPC has its own state
177     with respect to a particular player, which makes them useful to store
178     information about previous questions and possibly answers. State values
179     get reset whenever the NPC gets reset.
180    
181     See C<@ifstate> for an example.
182    
183     =item @ifstate state value
184    
185     Requires that the named C<state> has the given C<value>, otherwise this
186     topic is skipped. For more complex comparisons, see C<@cond> with
187     C<$state>. Example:
188    
189     @match quest
190     @setstate question quest
191     Do you really want to help find the magic amulet of Beeblebrox?
192     @match yes
193     @ifstate question quest
194     Then fetch it, stupid!
195    
196     =item @setflag flag value
197    
198     Sets the named flag C<flag> to the given C<value>. Flag values are
199     associated with a specific player and can be seen by all NPCs. with
200     respect to a particular player, which makes them suitable to store quest
201     markers and other information (e.g. reputation/alignment). Flags are
202     persistent over the lifetime of a player, so be careful :)
203    
204 root 1.16 Perversely enough, using C<@setfflag> without a C<value> clears the flag
205     as if it was never set, so always provide a flag value (e.g. C<1>) when
206     you want to set the flag.
207    
208 root 1.1 See C<@ifflag> for an example.
209    
210     =item @ifflag flag value
211    
212     Requires that the named C<flag> has the given C<value>, otherwise this
213 root 1.16 topic is skipped. For more complex comparisons, see C<@cond> with
214     C<$flag>.
215    
216     If no C<value> is given, then the ifflag succeeds when the flag is true.
217    
218     Example:
219 root 1.1
220     @match I want to do the quest!
221     @setflag kings_quest 1
222     Then seek out Bumblebee in Navar, he will tell you...
223     @match I did the quest
224     @ifflag kings_quest 1
225     Really, which quets?
226    
227     And Bumblebee might have:
228    
229     @match hi
230     @ifflag kings_quest
231     Hi, I was told you want to do the kings quest?
232    
233 elmex 1.2 =item @trigger connected-id [state]
234 root 1.1
235 elmex 1.2 Trigger all objects with the given connected-id.
236    
237     When the state argument is omitted the trigger is stateful and retains an
238     internal state per connected-id. There is a limitation to the use of this: The
239     state won't be changed when the connection is triggered by other triggers. So
240     be careful when triggering the connection from other objects.
241    
242 root 1.14 When a state argument is given it should be a positive integer. Any value
243     C<!= 0> will 'push' the connection (in general, you should specify C<1>
244     for this) and C<0> will 'release' the connection. This is useful for
245     example when you want to let an NPC control a door.
246 elmex 1.2
247     Trigger all objects with the given connected-id by 'releasing' the connection.
248 root 1.1
249 root 1.8 =item @playersound face-name
250    
251     Plays the given sound face (either an alias or sound file path) so that
252     only the player talking to the npc can hear it.
253    
254     =item @npcsound face-name
255    
256     Plays the given sound face (either an alias or sound file path) as if
257     the npc had made that sound, i.e. it will be located at the npc and all
258     players near enough can hear it.
259    
260 root 1.1 =item @addtopic topic
261    
262     Adds the given topic names (separated by C<|>) to the list of topics
263     returned.
264    
265     =back
266    
267     =cut
268    
269     sub tell {
270     my ($self, $msg) = @_;
271    
272     my $lcmsg = lc $msg;
273    
274     topic:
275     for my $match (@{ $self->{match} }) {
276     for (split /\|/, $match->[0]) {
277 elmex 1.3 if ($_ eq "*" || $lcmsg =~ /\b\Q$_\E\b/i) {
278 root 1.1 my $reply = $match->[1];
279     my @kw;
280    
281     my @replies;
282     my @match; # @match/@parse command results
283    
284     my $state = $self->{npc}{$self->{ob}->name}{dialog_state} ||= {};
285     my $flag = $self->{ob}{dialog_flag} ||= {};
286    
287 root 1.15 my @find;
288    
289 root 1.1 my %vars = (
290     who => $self->{ob},
291     npc => $self->{npc},
292     state => $state,
293     flag => $flag,
294     msg => $msg,
295     match => \@match,
296     );
297    
298     local $self->{ob}{record_replies} = \@replies;
299    
300     # now execute @-commands (which can result in a no-match)
301     while ($reply =~ s/^\@(\w+)\s*([^\n]*)\n?//) {
302     my ($cmd, $args) = ($1, $2);
303    
304     if ($cmd eq "parse" || $cmd eq "match") { # match is future rename
305     no re 'eval'; # default, but make sure
306     @match = $msg =~ /$args/i
307     or next topic;
308    
309     } elsif ($cmd eq "comment") {
310     # nop
311    
312 root 1.8 } elsif ($cmd eq "playersound") {
313     $self->{ob}->contr->play_sound (cf::sound::find $args);
314    
315     } elsif ($cmd eq "npcsound") {
316     $self->{npc}->play_sound (cf::sound::find $args);
317    
318 root 1.1 } elsif ($cmd eq "cond") {
319     cf::safe_eval $args, %vars
320     or next topic;
321    
322     } elsif ($cmd eq "eval") {
323     cf::safe_eval $args, %vars;
324     warn "\@eval evaluation error: $@\n" if $@;
325    
326 root 1.15 } elsif ($cmd eq "check") {
327     eval {
328     cf::match::match $args, $self->{ob}, $self->{npc}, $self->{ob}
329     or next topic;
330     };
331     warn "\@check evaluation error: $@\n" if $@;
332    
333     } elsif ($cmd eq "find") {
334     @find = eval {
335     cf::match::match $args, $self->{ob}, $self->{npc}, $self->{ob}
336     };
337     warn "\@find evaluation error: $@\n" if $@;
338    
339 root 1.1 } elsif ($cmd eq "msg") {
340     push @replies, [$self->{npc}, (scalar cf::safe_eval $args, %vars)];
341    
342     } elsif ($cmd eq "setflag") {
343     my ($name, $value) = split /\s+/, $args, 2;
344     $value ? $flag->{$name} = $value
345     : delete $flag->{$name};
346    
347     } elsif ($cmd eq "setstate") {
348     my ($name, $value) = split /\s+/, $args, 2;
349     $value ? $state->{$name} = $value
350     : delete $state->{$name};
351    
352     } elsif ($cmd eq "ifflag") {
353     my ($name, $value) = split /\s+/, $args, 2;
354     $flag->{$name} eq $value
355     or next topic;
356    
357     } elsif ($cmd eq "ifstate") {
358     my ($name, $value) = split /\s+/, $args, 2;
359     $state->{$name} eq $value
360     or next topic;
361    
362     } elsif ($cmd eq "trigger") {
363 elmex 1.2 my ($con, $state) = split /\s+/, $args, 2;
364    
365     if (defined $state) {
366 root 1.14 $self->{npc}->map->trigger ($con, $state, $self->{npc}, $self->{ob});
367 elmex 1.2 } else {
368 root 1.14 my $rvalue = \$self->{npc}{dialog_trigger}{$con+0};
369 root 1.13 $self->{npc}->map->trigger ($con, $$rvalue = !$$rvalue, $self->{npc}, $self->{ob});
370 elmex 1.2 }
371 root 1.1
372     } elsif ($cmd eq "addtopic") {
373     push @kw, split /\|/, $args;
374     $self->{add_topic}->(split /\s*\|\s*/, $args) if $self->{add_topic};
375    
376     } elsif ($cmd eq "deltopic") {
377     # not yet implemented, do it out-of-band
378     $self->{del_topic}->(split /\s*\|\s*/, $args) if $self->{del_topic};
379    
380     } else {
381     warn "unknown dialogue command <$cmd,$args> used (from " . $self->{npc}->msg . ")";
382     }
383     }
384    
385     delete $self->{npc}{$self->{ob}->name}{dialog_state} unless %$state;
386     delete $self->{ob}{dialog_flag} unless %$flag;
387    
388     # ignores flags and npc from replies
389     $reply = join "\n", (map $_->[1], @replies), $reply;
390    
391     # now mark up all matching keywords
392     for my $match (@{ $self->{match} }) {
393     for (sort { (length $b) <=> (length $a) } split /\|/, $match->[0]) {
394     if ($reply =~ /\b\Q$_\E\b/i) {
395     push @kw, $_;
396     last;
397     }
398     }
399     }
400    
401 root 1.13 $self->{npc}->use_trigger ($self->{ob})
402     if $self->{npc}->type == cf::MAGIC_EAR;
403 root 1.12
404 root 1.1 return wantarray ? ($reply, @kw) : $reply;
405     }
406     }
407     }
408    
409     ()
410     }
411    
412     1