ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/server/ext/NPC_Dialogue.pm
Revision: 1.14
Committed: Thu Jan 8 19:23:44 2009 UTC (15 years, 4 months ago) by root
Branch: MAIN
CVS Tags: rel-2_81, rel-2_80, rel-2_76, rel-2_77, rel-2_75, rel-2_79, rel-2_78
Changes since 1.13: +6 -6 lines
Log Message:
mapscript changes

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