ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/server/ext/NPC_Dialogue.pm
Revision: 1.6
Committed: Wed Aug 8 07:55:57 2007 UTC (16 years, 10 months ago) by elmex
Branch: MAIN
Changes since 1.5: +16 -0 lines
Log Message:
added example for @cond

File Contents

# Content
1 =head1 NAME
2
3 NPC_Dialogue
4
5 =head1 DESCRIPTION
6
7 NPC dialogue support module.
8
9 =over 4
10
11 =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 $arg{ob} = $arg{pl}->ob;
34
35 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 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 =back
127
128 =item @eval perl
129
130 Like C<@cond>, but proceed regardless of the outcome.
131
132 =item @msg perl
133
134 Like C<@cond>, but the return value will be stringified and prepended to
135 the message.
136
137 =item @setstate state value
138
139 Sets the named state C<state> to the given C<value>. State values are
140 associated with a specific player-NPC pair, so each NPC has its own state
141 with respect to a particular player, which makes them useful to store
142 information about previous questions and possibly answers. State values
143 get reset whenever the NPC gets reset.
144
145 See C<@ifstate> for an example.
146
147 =item @ifstate state value
148
149 Requires that the named C<state> has the given C<value>, otherwise this
150 topic is skipped. For more complex comparisons, see C<@cond> with
151 C<$state>. Example:
152
153 @match quest
154 @setstate question quest
155 Do you really want to help find the magic amulet of Beeblebrox?
156 @match yes
157 @ifstate question quest
158 Then fetch it, stupid!
159
160 =item @setflag flag value
161
162 Sets the named flag C<flag> to the given C<value>. Flag values are
163 associated with a specific player and can be seen by all NPCs. with
164 respect to a particular player, which makes them suitable to store quest
165 markers and other information (e.g. reputation/alignment). Flags are
166 persistent over the lifetime of a player, so be careful :)
167
168 See C<@ifflag> for an example.
169
170 =item @ifflag flag value
171
172 Requires that the named C<flag> has the given C<value>, otherwise this
173 topic is skipped. For more complex comparisons, see C<@cond> with
174 C<$flag>. Example:
175
176 @match I want to do the quest!
177 @setflag kings_quest 1
178 Then seek out Bumblebee in Navar, he will tell you...
179 @match I did the quest
180 @ifflag kings_quest 1
181 Really, which quets?
182
183 And Bumblebee might have:
184
185 @match hi
186 @ifflag kings_quest
187 Hi, I was told you want to do the kings quest?
188
189 =item @trigger connected-id [state]
190
191 Trigger all objects with the given connected-id.
192
193 When the state argument is omitted the trigger is stateful and retains an
194 internal state per connected-id. There is a limitation to the use of this: The
195 state won't be changed when the connection is triggered by other triggers. So
196 be careful when triggering the connection from other objects.
197
198 When a state argument is given it should be either 0 or 1. 1 will 'push' the connection
199 and 0 will 'release' the connection. This is useful for example when you want to
200 let a npc control a door.
201
202 Trigger all objects with the given connected-id by 'releasing' the connection.
203
204 =item @addtopic topic
205
206 Adds the given topic names (separated by C<|>) to the list of topics
207 returned.
208
209 =back
210
211 =cut
212
213 sub tell {
214 my ($self, $msg) = @_;
215
216 my $lcmsg = lc $msg;
217
218 topic:
219 for my $match (@{ $self->{match} }) {
220 for (split /\|/, $match->[0]) {
221 if ($_ eq "*" || $lcmsg =~ /\b\Q$_\E\b/i) {
222 my $reply = $match->[1];
223 my @kw;
224
225 my @replies;
226 my @match; # @match/@parse command results
227
228 my $state = $self->{npc}{$self->{ob}->name}{dialog_state} ||= {};
229 my $flag = $self->{ob}{dialog_flag} ||= {};
230
231 my %vars = (
232 who => $self->{ob},
233 npc => $self->{npc},
234 state => $state,
235 flag => $flag,
236 msg => $msg,
237 match => \@match,
238 );
239
240 local $self->{ob}{record_replies} = \@replies;
241
242 # now execute @-commands (which can result in a no-match)
243 while ($reply =~ s/^\@(\w+)\s*([^\n]*)\n?//) {
244 my ($cmd, $args) = ($1, $2);
245
246 if ($cmd eq "parse" || $cmd eq "match") { # match is future rename
247 no re 'eval'; # default, but make sure
248 @match = $msg =~ /$args/i
249 or next topic;
250
251 } elsif ($cmd eq "comment") {
252 # nop
253
254 } elsif ($cmd eq "cond") {
255 cf::safe_eval $args, %vars
256 or next topic;
257
258 } elsif ($cmd eq "eval") {
259 cf::safe_eval $args, %vars;
260 warn "\@eval evaluation error: $@\n" if $@;
261
262 } elsif ($cmd eq "msg") {
263 push @replies, [$self->{npc}, (scalar cf::safe_eval $args, %vars)];
264
265 } elsif ($cmd eq "setflag") {
266 my ($name, $value) = split /\s+/, $args, 2;
267 $value ? $flag->{$name} = $value
268 : delete $flag->{$name};
269
270 } elsif ($cmd eq "setstate") {
271 my ($name, $value) = split /\s+/, $args, 2;
272 $value ? $state->{$name} = $value
273 : delete $state->{$name};
274
275 } elsif ($cmd eq "ifflag") {
276 my ($name, $value) = split /\s+/, $args, 2;
277 $flag->{$name} eq $value
278 or next topic;
279
280 } elsif ($cmd eq "ifstate") {
281 my ($name, $value) = split /\s+/, $args, 2;
282 $state->{$name} eq $value
283 or next topic;
284
285 } elsif ($cmd eq "trigger") {
286 my ($con, $state) = split /\s+/, $args, 2;
287 $con = $con * 1;
288
289 if (defined $state) {
290 $self->{npc}->map->trigger ($args, $state);
291 } else {
292 my $rvalue = \$self->{npc}{dialog_trigger}{$con};
293 $self->{npc}->map->trigger ($con, $$rvalue = !$$rvalue);
294 }
295
296 } elsif ($cmd eq "addtopic") {
297 push @kw, split /\|/, $args;
298 $self->{add_topic}->(split /\s*\|\s*/, $args) if $self->{add_topic};
299
300 } elsif ($cmd eq "deltopic") {
301 # not yet implemented, do it out-of-band
302 $self->{del_topic}->(split /\s*\|\s*/, $args) if $self->{del_topic};
303
304 } else {
305 warn "unknown dialogue command <$cmd,$args> used (from " . $self->{npc}->msg . ")";
306 }
307 }
308
309 delete $self->{npc}{$self->{ob}->name}{dialog_state} unless %$state;
310 delete $self->{ob}{dialog_flag} unless %$flag;
311
312 # combine lines into paragraphs
313 $reply =~ s/(?<=\S)\n(?=\w)/ /g;
314 $reply =~ s/\n\n/\n/g;
315
316 # ignores flags and npc from replies
317 $reply = join "\n", (map $_->[1], @replies), $reply;
318
319 # now mark up all matching keywords
320 for my $match (@{ $self->{match} }) {
321 for (sort { (length $b) <=> (length $a) } split /\|/, $match->[0]) {
322 if ($reply =~ /\b\Q$_\E\b/i) {
323 push @kw, $_;
324 last;
325 }
326 }
327 }
328
329 return wantarray ? ($reply, @kw) : $reply;
330 }
331 }
332 }
333
334 ()
335 }
336
337 1