ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/server/ext/NPC_Dialogue.pm
Revision: 1.7
Committed: Sat Aug 25 16:51:38 2007 UTC (16 years, 9 months ago) by elmex
Branch: MAIN
Changes since 1.6: +14 -0 lines
Log Message:
added decrease_ob_nr to the save environment and added an example
to the npc dialogues.

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 has_dialogue($) {
18     my ($ob) = @_;
19    
20     $ob->msg =~ /^\@match /;
21     }
22    
23     sub parse_message($) {
24     map [split /\n/, $_, 2],
25     grep length,
26     split /^\@match /m,
27     $_[0]
28     }
29    
30     sub new {
31     my ($class, %arg) = @_;
32    
33 root 1.4 $arg{ob} = $arg{pl}->ob;
34    
35 root 1.1 my $self = bless {
36     %arg,
37     }, $class;
38    
39     $self->{match} ||= [parse_message $self->{npc}->msg];
40    
41     $self;
42     }
43    
44     sub greet {
45     my ($self) = @_;
46    
47     $self->tell ("hi")
48     }
49    
50     =item ($reply, @topics) = $dialog->tell ($msg)
51    
52     Tells the dialog object something and returns its response and optionally
53     a number of topics that are refered to by this topic.
54    
55     It supports a number of command constructs. They have to follow the
56     C<@match> directive, and there can be multiple commands that will be
57     executed in order.
58    
59     =over 4
60    
61     =item @comment text...
62    
63     A single-line comment. It will be completely ignored.
64    
65     =item @parse regex
66    
67     Parses the message using a perl regular expression (by default
68     case-insensitive). Any matches will be available as C<< $match->[$index]
69     >>.
70    
71     If the regular expression does not match, the topic is skipped.
72    
73     Example:
74    
75     @match deposit
76     @parse deposit (\d+) (\S+)
77     @eval bank::deposit $match->[0], $match->[1]
78    
79     =item @cond perl
80    
81     Evaluates the given perl code. If it returns false (or causes an
82     exception), the topic will be skipped, otherwise topic interpretation is
83     resumed.
84    
85     The following local variables are defined within the expression:
86    
87     =over 4
88    
89     =item $who - The cf::object::player object that initiated the dialogue.
90    
91     =item $npc - The NPC (or magic_ear etc.) object that is being talked to.
92    
93     =item $msg - The actual message as passed to this method.
94    
95     =item $match - An arrayref with previous results from C<@parse>.
96    
97     =item $state - A hashref that stores state variables associated
98     with the NPC and the player, that is, it's values relate to the the
99     specific player-NPC interaction and other players will see a different
100     state. Useful to react to players in a stateful way. See C<@setstate> and
101     C<@ifstate>.
102    
103     =item $flag - A hashref that stores flags associated with the player and
104     can be seen by all NPCs (so better name your flags uniquely). This is
105     useful for storing e.g. quest information. See C<@setflag> and C<@ifflag>.
106    
107     =back
108    
109     The environment is that standard "map scripting environment", which is
110     limited in the type of constructs allowed (no loops, for example).
111    
112 elmex 1.6 Here is a example:
113    
114     =over 4
115    
116     =item B<matching for an item name>
117    
118     @match hi
119     @cond grep $_->name =~ /royalty/, $who->inv
120     You got royalties there! Wanna have!
121    
122     You may want to change the C<name> method there to something like C<title>,
123     C<slaying> or any other method that is allowed to be called on a
124     C<cf::object> here.
125    
126 elmex 1.7 =item B<matching for an item name and removing the matched item>
127    
128     @match found earhorn
129     @cond grep $_->slaying =~ /Gramp's walking stick/, $who->inv
130     @eval my @g = grep { $_->slaying =~ /Gramp's walking stick/ } $who->inv; $g[0]->decrease_ob_nr (1);
131     Thanks for the earhorn!
132    
133     This example is a bit more complex. The C<@eval> statement will search
134     the players inventory for the same term as the C<@cond> and then
135     decreases the number of objects used there.
136    
137     (See also the map: C<scorn/houses/cornerbrook.map> for an example how this is
138     used in the real world :-)
139    
140 elmex 1.6 =back
141    
142 root 1.1 =item @eval perl
143    
144     Like C<@cond>, but proceed regardless of the outcome.
145    
146     =item @msg perl
147    
148     Like C<@cond>, but the return value will be stringified and prepended to
149     the message.
150    
151     =item @setstate state value
152    
153     Sets the named state C<state> to the given C<value>. State values are
154     associated with a specific player-NPC pair, so each NPC has its own state
155     with respect to a particular player, which makes them useful to store
156     information about previous questions and possibly answers. State values
157     get reset whenever the NPC gets reset.
158    
159     See C<@ifstate> for an example.
160    
161     =item @ifstate state value
162    
163     Requires that the named C<state> has the given C<value>, otherwise this
164     topic is skipped. For more complex comparisons, see C<@cond> with
165     C<$state>. Example:
166    
167     @match quest
168     @setstate question quest
169     Do you really want to help find the magic amulet of Beeblebrox?
170     @match yes
171     @ifstate question quest
172     Then fetch it, stupid!
173    
174     =item @setflag flag value
175    
176     Sets the named flag C<flag> to the given C<value>. Flag values are
177     associated with a specific player and can be seen by all NPCs. with
178     respect to a particular player, which makes them suitable to store quest
179     markers and other information (e.g. reputation/alignment). Flags are
180     persistent over the lifetime of a player, so be careful :)
181    
182     See C<@ifflag> for an example.
183    
184     =item @ifflag flag value
185    
186     Requires that the named C<flag> has the given C<value>, otherwise this
187     topic is skipped. For more complex comparisons, see C<@cond> with
188     C<$flag>. Example:
189    
190     @match I want to do the quest!
191     @setflag kings_quest 1
192     Then seek out Bumblebee in Navar, he will tell you...
193     @match I did the quest
194     @ifflag kings_quest 1
195     Really, which quets?
196    
197     And Bumblebee might have:
198    
199     @match hi
200     @ifflag kings_quest
201     Hi, I was told you want to do the kings quest?
202    
203 elmex 1.2 =item @trigger connected-id [state]
204 root 1.1
205 elmex 1.2 Trigger all objects with the given connected-id.
206    
207     When the state argument is omitted the trigger is stateful and retains an
208     internal state per connected-id. There is a limitation to the use of this: The
209     state won't be changed when the connection is triggered by other triggers. So
210     be careful when triggering the connection from other objects.
211    
212     When a state argument is given it should be either 0 or 1. 1 will 'push' the connection
213     and 0 will 'release' the connection. This is useful for example when you want to
214     let a npc control a door.
215    
216     Trigger all objects with the given connected-id by 'releasing' the connection.
217 root 1.1
218     =item @addtopic topic
219    
220     Adds the given topic names (separated by C<|>) to the list of topics
221     returned.
222    
223     =back
224    
225     =cut
226    
227     sub tell {
228     my ($self, $msg) = @_;
229    
230     my $lcmsg = lc $msg;
231    
232     topic:
233     for my $match (@{ $self->{match} }) {
234     for (split /\|/, $match->[0]) {
235 elmex 1.3 if ($_ eq "*" || $lcmsg =~ /\b\Q$_\E\b/i) {
236 root 1.1 my $reply = $match->[1];
237     my @kw;
238    
239     my @replies;
240     my @match; # @match/@parse command results
241    
242     my $state = $self->{npc}{$self->{ob}->name}{dialog_state} ||= {};
243     my $flag = $self->{ob}{dialog_flag} ||= {};
244    
245     my %vars = (
246     who => $self->{ob},
247     npc => $self->{npc},
248     state => $state,
249     flag => $flag,
250     msg => $msg,
251     match => \@match,
252     );
253    
254     local $self->{ob}{record_replies} = \@replies;
255    
256     # now execute @-commands (which can result in a no-match)
257     while ($reply =~ s/^\@(\w+)\s*([^\n]*)\n?//) {
258     my ($cmd, $args) = ($1, $2);
259    
260     if ($cmd eq "parse" || $cmd eq "match") { # match is future rename
261     no re 'eval'; # default, but make sure
262     @match = $msg =~ /$args/i
263     or next topic;
264    
265     } elsif ($cmd eq "comment") {
266     # nop
267    
268     } elsif ($cmd eq "cond") {
269     cf::safe_eval $args, %vars
270     or next topic;
271    
272     } elsif ($cmd eq "eval") {
273     cf::safe_eval $args, %vars;
274     warn "\@eval evaluation error: $@\n" if $@;
275    
276     } elsif ($cmd eq "msg") {
277     push @replies, [$self->{npc}, (scalar cf::safe_eval $args, %vars)];
278    
279     } elsif ($cmd eq "setflag") {
280     my ($name, $value) = split /\s+/, $args, 2;
281     $value ? $flag->{$name} = $value
282     : delete $flag->{$name};
283    
284     } elsif ($cmd eq "setstate") {
285     my ($name, $value) = split /\s+/, $args, 2;
286     $value ? $state->{$name} = $value
287     : delete $state->{$name};
288    
289     } elsif ($cmd eq "ifflag") {
290     my ($name, $value) = split /\s+/, $args, 2;
291     $flag->{$name} eq $value
292     or next topic;
293    
294     } elsif ($cmd eq "ifstate") {
295     my ($name, $value) = split /\s+/, $args, 2;
296     $state->{$name} eq $value
297     or next topic;
298    
299     } elsif ($cmd eq "trigger") {
300 elmex 1.2 my ($con, $state) = split /\s+/, $args, 2;
301     $con = $con * 1;
302    
303     if (defined $state) {
304     $self->{npc}->map->trigger ($args, $state);
305     } else {
306     my $rvalue = \$self->{npc}{dialog_trigger}{$con};
307     $self->{npc}->map->trigger ($con, $$rvalue = !$$rvalue);
308     }
309 root 1.1
310     } elsif ($cmd eq "addtopic") {
311     push @kw, split /\|/, $args;
312     $self->{add_topic}->(split /\s*\|\s*/, $args) if $self->{add_topic};
313    
314     } elsif ($cmd eq "deltopic") {
315     # not yet implemented, do it out-of-band
316     $self->{del_topic}->(split /\s*\|\s*/, $args) if $self->{del_topic};
317    
318     } else {
319     warn "unknown dialogue command <$cmd,$args> used (from " . $self->{npc}->msg . ")";
320     }
321     }
322    
323     delete $self->{npc}{$self->{ob}->name}{dialog_state} unless %$state;
324     delete $self->{ob}{dialog_flag} unless %$flag;
325    
326     # combine lines into paragraphs
327     $reply =~ s/(?<=\S)\n(?=\w)/ /g;
328     $reply =~ s/\n\n/\n/g;
329    
330     # ignores flags and npc from replies
331     $reply = join "\n", (map $_->[1], @replies), $reply;
332    
333     # now mark up all matching keywords
334     for my $match (@{ $self->{match} }) {
335     for (sort { (length $b) <=> (length $a) } split /\|/, $match->[0]) {
336     if ($reply =~ /\b\Q$_\E\b/i) {
337     push @kw, $_;
338     last;
339     }
340     }
341     }
342    
343     return wantarray ? ($reply, @kw) : $reply;
344     }
345     }
346     }
347    
348     ()
349     }
350    
351     1