ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/server/ext/NPC_Dialogue.pm
Revision: 1.4
Committed: Sat Jun 16 14:35:41 2007 UTC (16 years, 11 months ago) by root
Branch: MAIN
Changes since 1.3: +2 -0 lines
Log Message:
- use a per-player attachment in dialog code instead of a timer
  to detect distance.
- store currently active dialog in $pl->{npc_dialog}

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