ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/server/ext/NPC_Dialogue.pm
Revision: 1.15
Committed: Tue Oct 13 00:24:14 2009 UTC (14 years, 7 months ago) by root
Branch: MAIN
CVS Tags: rel-2_82
Changes since 1.14: +44 -1 lines
Log Message:
new npc dialiogue options

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     See C<@ifflag> for an example.
205    
206     =item @ifflag flag value
207    
208     Requires that the named C<flag> has the given C<value>, otherwise this
209     topic is skipped. For more complex comparisons, see C<@cond> with
210     C<$flag>. Example:
211    
212     @match I want to do the quest!
213     @setflag kings_quest 1
214     Then seek out Bumblebee in Navar, he will tell you...
215     @match I did the quest
216     @ifflag kings_quest 1
217     Really, which quets?
218    
219     And Bumblebee might have:
220    
221     @match hi
222     @ifflag kings_quest
223     Hi, I was told you want to do the kings quest?
224    
225 elmex 1.2 =item @trigger connected-id [state]
226 root 1.1
227 elmex 1.2 Trigger all objects with the given connected-id.
228    
229     When the state argument is omitted the trigger is stateful and retains an
230     internal state per connected-id. There is a limitation to the use of this: The
231     state won't be changed when the connection is triggered by other triggers. So
232     be careful when triggering the connection from other objects.
233    
234 root 1.14 When a state argument is given it should be a positive integer. Any value
235     C<!= 0> will 'push' the connection (in general, you should specify C<1>
236     for this) and C<0> will 'release' the connection. This is useful for
237     example when you want to let an NPC control a door.
238 elmex 1.2
239     Trigger all objects with the given connected-id by 'releasing' the connection.
240 root 1.1
241 root 1.8 =item @playersound face-name
242    
243     Plays the given sound face (either an alias or sound file path) so that
244     only the player talking to the npc can hear it.
245    
246     =item @npcsound face-name
247    
248     Plays the given sound face (either an alias or sound file path) as if
249     the npc had made that sound, i.e. it will be located at the npc and all
250     players near enough can hear it.
251    
252 root 1.1 =item @addtopic topic
253    
254     Adds the given topic names (separated by C<|>) to the list of topics
255     returned.
256    
257     =back
258    
259     =cut
260    
261     sub tell {
262     my ($self, $msg) = @_;
263    
264     my $lcmsg = lc $msg;
265    
266     topic:
267     for my $match (@{ $self->{match} }) {
268     for (split /\|/, $match->[0]) {
269 elmex 1.3 if ($_ eq "*" || $lcmsg =~ /\b\Q$_\E\b/i) {
270 root 1.1 my $reply = $match->[1];
271     my @kw;
272    
273     my @replies;
274     my @match; # @match/@parse command results
275    
276     my $state = $self->{npc}{$self->{ob}->name}{dialog_state} ||= {};
277     my $flag = $self->{ob}{dialog_flag} ||= {};
278    
279 root 1.15 my @find;
280    
281 root 1.1 my %vars = (
282     who => $self->{ob},
283     npc => $self->{npc},
284     state => $state,
285     flag => $flag,
286     msg => $msg,
287     match => \@match,
288     );
289    
290     local $self->{ob}{record_replies} = \@replies;
291    
292     # now execute @-commands (which can result in a no-match)
293     while ($reply =~ s/^\@(\w+)\s*([^\n]*)\n?//) {
294     my ($cmd, $args) = ($1, $2);
295    
296     if ($cmd eq "parse" || $cmd eq "match") { # match is future rename
297     no re 'eval'; # default, but make sure
298     @match = $msg =~ /$args/i
299     or next topic;
300    
301     } elsif ($cmd eq "comment") {
302     # nop
303    
304 root 1.8 } elsif ($cmd eq "playersound") {
305     $self->{ob}->contr->play_sound (cf::sound::find $args);
306    
307     } elsif ($cmd eq "npcsound") {
308     $self->{npc}->play_sound (cf::sound::find $args);
309    
310 root 1.1 } elsif ($cmd eq "cond") {
311     cf::safe_eval $args, %vars
312     or next topic;
313    
314     } elsif ($cmd eq "eval") {
315     cf::safe_eval $args, %vars;
316     warn "\@eval evaluation error: $@\n" if $@;
317    
318 root 1.15 } elsif ($cmd eq "check") {
319     eval {
320     cf::match::match $args, $self->{ob}, $self->{npc}, $self->{ob}
321     or next topic;
322     };
323     warn "\@check evaluation error: $@\n" if $@;
324    
325     } elsif ($cmd eq "find") {
326     @find = eval {
327     cf::match::match $args, $self->{ob}, $self->{npc}, $self->{ob}
328     };
329     warn "\@find evaluation error: $@\n" if $@;
330    
331 root 1.1 } elsif ($cmd eq "msg") {
332     push @replies, [$self->{npc}, (scalar cf::safe_eval $args, %vars)];
333    
334     } elsif ($cmd eq "setflag") {
335     my ($name, $value) = split /\s+/, $args, 2;
336     $value ? $flag->{$name} = $value
337     : delete $flag->{$name};
338    
339     } elsif ($cmd eq "setstate") {
340     my ($name, $value) = split /\s+/, $args, 2;
341     $value ? $state->{$name} = $value
342     : delete $state->{$name};
343    
344     } elsif ($cmd eq "ifflag") {
345     my ($name, $value) = split /\s+/, $args, 2;
346     $flag->{$name} eq $value
347     or next topic;
348    
349     } elsif ($cmd eq "ifstate") {
350     my ($name, $value) = split /\s+/, $args, 2;
351     $state->{$name} eq $value
352     or next topic;
353    
354     } elsif ($cmd eq "trigger") {
355 elmex 1.2 my ($con, $state) = split /\s+/, $args, 2;
356    
357     if (defined $state) {
358 root 1.14 $self->{npc}->map->trigger ($con, $state, $self->{npc}, $self->{ob});
359 elmex 1.2 } else {
360 root 1.14 my $rvalue = \$self->{npc}{dialog_trigger}{$con+0};
361 root 1.13 $self->{npc}->map->trigger ($con, $$rvalue = !$$rvalue, $self->{npc}, $self->{ob});
362 elmex 1.2 }
363 root 1.1
364     } elsif ($cmd eq "addtopic") {
365     push @kw, split /\|/, $args;
366     $self->{add_topic}->(split /\s*\|\s*/, $args) if $self->{add_topic};
367    
368     } elsif ($cmd eq "deltopic") {
369     # not yet implemented, do it out-of-band
370     $self->{del_topic}->(split /\s*\|\s*/, $args) if $self->{del_topic};
371    
372     } else {
373     warn "unknown dialogue command <$cmd,$args> used (from " . $self->{npc}->msg . ")";
374     }
375     }
376    
377     delete $self->{npc}{$self->{ob}->name}{dialog_state} unless %$state;
378     delete $self->{ob}{dialog_flag} unless %$flag;
379    
380     # ignores flags and npc from replies
381     $reply = join "\n", (map $_->[1], @replies), $reply;
382    
383     # now mark up all matching keywords
384     for my $match (@{ $self->{match} }) {
385     for (sort { (length $b) <=> (length $a) } split /\|/, $match->[0]) {
386     if ($reply =~ /\b\Q$_\E\b/i) {
387     push @kw, $_;
388     last;
389     }
390     }
391     }
392    
393 root 1.13 $self->{npc}->use_trigger ($self->{ob})
394     if $self->{npc}->type == cf::MAGIC_EAR;
395 root 1.12
396 root 1.1 return wantarray ? ($reply, @kw) : $reply;
397     }
398     }
399     }
400    
401     ()
402     }
403    
404     1