ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/server/ext/NPC_Dialogue.pm
Revision: 1.12
Committed: Thu Dec 18 02:49:22 2008 UTC (15 years, 5 months ago) by root
Branch: MAIN
CVS Tags: rel-2_73, rel-2_74
Changes since 1.11: +2 -0 lines
Log Message:
indent

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 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 $arg{ob} = $arg{pl}->ob;
28
29 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 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 =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 @eval my @g = grep { $_->slaying =~ /Gramp's walking stick/ } $who->inv; $g[0]->decrease;
125 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 =back
135
136 =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 =item @trigger connected-id [state]
198
199 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 When a state argument is given it should be either 0 or 1. 1 will 'push' the connection
207 and 0 will 'release' the connection. This is useful for example when you want to
208 let a npc control a door.
209
210 Trigger all objects with the given connected-id by 'releasing' the connection.
211
212 =item @playersound face-name
213
214 Plays the given sound face (either an alias or sound file path) so that
215 only the player talking to the npc can hear it.
216
217 =item @npcsound face-name
218
219 Plays the given sound face (either an alias or sound file path) as if
220 the npc had made that sound, i.e. it will be located at the npc and all
221 players near enough can hear it.
222
223 =item @addtopic topic
224
225 Adds the given topic names (separated by C<|>) to the list of topics
226 returned.
227
228 =back
229
230 =cut
231
232 sub tell {
233 my ($self, $msg) = @_;
234
235 my $lcmsg = lc $msg;
236
237 topic:
238 for my $match (@{ $self->{match} }) {
239 for (split /\|/, $match->[0]) {
240 if ($_ eq "*" || $lcmsg =~ /\b\Q$_\E\b/i) {
241 my $reply = $match->[1];
242 my @kw;
243
244 my @replies;
245 my @match; # @match/@parse command results
246
247 my $state = $self->{npc}{$self->{ob}->name}{dialog_state} ||= {};
248 my $flag = $self->{ob}{dialog_flag} ||= {};
249
250 my %vars = (
251 who => $self->{ob},
252 npc => $self->{npc},
253 state => $state,
254 flag => $flag,
255 msg => $msg,
256 match => \@match,
257 );
258
259 local $self->{ob}{record_replies} = \@replies;
260
261 # now execute @-commands (which can result in a no-match)
262 while ($reply =~ s/^\@(\w+)\s*([^\n]*)\n?//) {
263 my ($cmd, $args) = ($1, $2);
264
265 if ($cmd eq "parse" || $cmd eq "match") { # match is future rename
266 no re 'eval'; # default, but make sure
267 @match = $msg =~ /$args/i
268 or next topic;
269
270 } elsif ($cmd eq "comment") {
271 # nop
272
273 } elsif ($cmd eq "playersound") {
274 $self->{ob}->contr->play_sound (cf::sound::find $args);
275
276 } elsif ($cmd eq "npcsound") {
277 $self->{npc}->play_sound (cf::sound::find $args);
278
279 } elsif ($cmd eq "cond") {
280 cf::safe_eval $args, %vars
281 or next topic;
282
283 } elsif ($cmd eq "eval") {
284 cf::safe_eval $args, %vars;
285 warn "\@eval evaluation error: $@\n" if $@;
286
287 } elsif ($cmd eq "msg") {
288 push @replies, [$self->{npc}, (scalar cf::safe_eval $args, %vars)];
289
290 } elsif ($cmd eq "setflag") {
291 my ($name, $value) = split /\s+/, $args, 2;
292 $value ? $flag->{$name} = $value
293 : delete $flag->{$name};
294
295 } elsif ($cmd eq "setstate") {
296 my ($name, $value) = split /\s+/, $args, 2;
297 $value ? $state->{$name} = $value
298 : delete $state->{$name};
299
300 } elsif ($cmd eq "ifflag") {
301 my ($name, $value) = split /\s+/, $args, 2;
302 $flag->{$name} eq $value
303 or next topic;
304
305 } elsif ($cmd eq "ifstate") {
306 my ($name, $value) = split /\s+/, $args, 2;
307 $state->{$name} eq $value
308 or next topic;
309
310 } elsif ($cmd eq "trigger") {
311 my ($con, $state) = split /\s+/, $args, 2;
312 $con = $con * 1;
313
314 if (defined $state) {
315 $self->{npc}->map->trigger ($args, $state);
316 } else {
317 my $rvalue = \$self->{npc}{dialog_trigger}{$con};
318 $self->{npc}->map->trigger ($con, $$rvalue = !$$rvalue);
319 }
320
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 $self->{npc}->use_trigger if $self->{npc}->type == cf::MAGIC_EAR;
351
352 return wantarray ? ($reply, @kw) : $reply;
353 }
354 }
355 }
356
357 ()
358 }
359
360 1