ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/server/ext/NPC_Dialogue.pm
Revision: 1.2
Committed: Thu Jan 4 15:20:11 2007 UTC (17 years, 4 months ago) by elmex
Branch: MAIN
CVS Tags: rel-2_0
Changes since 1.1: +22 -5 lines
Log Message:
added state argument to the @trigger npc dialog command.

File Contents

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