File Coverage

blib/lib/Games/Roguelike/Mob.pm
Criterion Covered Total %
statement 40 243 16.4
branch 3 118 2.5
condition 1 39 2.5
subroutine 8 25 32.0
pod 17 19 89.4
total 69 444 15.5


line stmt bran cond sub pod time code
1 5     5   28550 use strict;
  5         17  
  5         327  
2              
3             package Games::Roguelike::Mob;
4              
5 5     5   569 use Games::Roguelike::Utils qw(:all);
  5         12  
  5         1972  
6 5     5   6020 use Games::Roguelike::Console;
  5         56  
  5         307  
7 5     5   30 use Games::Roguelike::Area;
  5         9  
  5         132  
8 5     5   25761 use Data::Dumper;
  5         59571  
  5         419  
9 5     5   50 use Carp qw(croak confess);
  5         10  
  5         26411  
10              
11             =head1 NAME
12              
13             Games::Roguelike::Mob - Roguelike mobile object
14              
15             =head1 SYNOPSIS
16              
17             package myMob;
18             use base 'Games::Roguelike::Mob';
19              
20             $area = Games::Roguelike::Area->new();
21             $m = myMob->new($area, sym=>'D', x=>5,y=>6); # creates a mob at location 5, 6
22             # with symbol 'D', inside area $area
23              
24             $m->autoex() # moves the mob towards the nearest unexplored area
25             $m->kbdmove($c) # moves the mob according to keystroke '$c' using traditional roguelike semantics
26              
27             =head1 DESCRIPTION
28              
29             Mobile object used by drawing routines in Roguelke::Area
30              
31             =head2 METHODS
32              
33             =over 4
34              
35             =item new($area, %options)
36              
37             Area is an ::Area object, common options are:
38              
39             sym=>'@', # symbol to use when rendering
40             items=>[], # array ref of contained items
41             hasmem=>1, # whether the mob uses the "memory" feature
42             pov=>-1, # distance the mob can "see" (-1 = infinite, 0 = blind)
43             singleminded=>0, # whether the mob will "wander" when the movetoward function is called
44              
45             All "unknown" options are saved in the object's hash, with the assumption that they
46             will be used by the game, for example "->{MaxHp}", etc.
47              
48             =cut
49              
50             sub new {
51 16     16 1 93 my $pkg = shift;
52 16         29 my $area = shift;
53 16 50 33     214 croak("can't create mob without area") unless $area && $area->isa('Games::Roguelike::Area');
54              
55 16         34 my $self = {};
56              
57 16         42 $self->{area} = $area;
58 16         40 $self->{sym}='@';
59 16         43 $self->{msglog} = [];
60 16         45 $self->{items} = [];
61 16         36 $self->{odir} = '';
62 16         42 $self->{hasmem} = 1;
63 16         32 $self->{pov} = -1;
64 16         46 $self->{singleminded} = 0; # whether it tries to wander around in pursuit of a goal
65              
66 16         88 while( my ($k, $v) = splice(@_, 0, 2)) {
67 58         189 $self->{$k} = $v;
68             }
69              
70 16 100       50 if (!defined($self->{x})) {
71 6         48 ($self->{x}, $self->{y}) = $area->findrandmap($area->{fsym}, 0, 1);
72             }
73              
74 16         64 bless $self, $pkg;
75 16         91 $area->addmob($self);
76 16         278 return $self;
77             }
78              
79             =item area([new])
80              
81             Either returns the current area (no arguments) or set the area (one argument).
82              
83             If an area is supplied, the old area has "delmob" called on it,and the new area has "addmob" called on it.
84              
85             =cut
86              
87             sub area {
88 0     0 1 0 my $self = shift;
89 0 0       0 if (@_) {
90 0         0 $self->{area}->delmob($self);
91 0         0 $self->{area} = $_[0];
92 0         0 $_[0]->addmob($self);
93 0 0       0 if ($self->{area}->{world}) {
94 0         0 $self->{area}->{world}->area($self->{area});
95             }
96             }
97 0         0 return $self->{area};
98             }
99              
100             =item x()
101              
102             =item y()
103              
104             Returns the location of the mob
105              
106             =cut
107              
108             sub x {
109 0     0 1 0 return $_[0]->{x};
110             }
111              
112             sub y {
113 0     0 1 0 return $_[0]->{y};
114             }
115              
116             =item on()
117              
118             Returns the map symbol from the current area at the mob's current x, y location.
119              
120             =cut
121              
122             sub on {
123 0     0 1 0 my $self = shift;
124 0         0 return $self->{area}->{map}->[$self->{x}][$self->{y}];
125             }
126              
127             =item movetofeature(@ARGS)
128              
129             Calls the "findfeature" function on the current area with the @ARGS, and, if one is returned, reposition with x/y coordinates to match.
130              
131             Checkmove/aftermove are not called.
132              
133             =cut
134              
135             sub movetofeature {
136 0     0 1 0 my $self = shift;
137 0         0 my ($cx,$cy) = $self->{area}->findfeature(@_);
138 0 0       0 if (defined($cx)) {
139 0         0 $self->{x} = $cx;
140 0         0 $self->{y} = $cy;
141 0         0 return 1;
142             } else {
143 0         0 return 0;
144             }
145             }
146              
147             my %DIAGS = (
148             'nw'=>['n','w'],
149             'ne'=>['n','e'],
150             'sw'=>['s','w'],
151             'se'=>['s','e'],
152             'n',=>['nw','ne'],
153             's',=>['sw','se'],
154             'e',=>['ne','ne'],
155             'w',=>['sw','nw'],
156             );
157              
158             =item movetoward($x, $y, $error)
159              
160             Moves the mob toward the point specified. If error is specified, the destination point is "blurred" by the error radius.
161              
162             =cut
163              
164             sub movetoward {
165 0     0 1 0 my $self = shift;
166 0         0 my ($x, $y, $err) = @_;
167 0         0 my ($dx, $dy) = ($x - $self->{x}, $y - $self->{y});
168            
169 0 0       0 if ($err > 0) {
170 0         0 $dx += (randi($err*2+1)- $err);
171 0         0 $dy += (randi($err*2+1)- $err);
172 0         0 intify($dx, $dy);
173             }
174              
175 0 0 0     0 return 0 if $dx == 0 && $dy == 0;
176 0         0 my $d;
177              
178 0 0       0 if ($dy > 0) {
    0          
179 0         0 $d = 's'
180             } elsif ($dy < 0) {
181 0         0 $d = 'n'
182             }
183              
184 0 0       0 if ($dx > 0) {
    0          
185 0         0 $d .= 'e'
186             } elsif ($dx < 0) {
187 0         0 $d .= 'w'
188             }
189              
190             # nonzero means move happened
191 0         0 my $ok = $self->move($d);
192 0 0       0 return $ok if $ok;
193              
194             # try moving orthoganally again sometimes... to range farther
195 0 0 0     0 if (($self->{singleminded}==0) && rand(2) > 1 && $self->{odir}) {
      0        
196 0         0 $ok = $self->move($self->{odir});
197 0 0       0 $self->{area}->dprint("ortho repeat of $self->{odir}") if $ok;
198 0 0       0 return $ok if $ok;
199             }
200              
201             # try moving diags of move
202 0         0 my @d;
203            
204 0 0       0 if (abs($dy) > abs($dx)) {
    0          
205 0         0 @d = @{$DIAGS{$d}};
  0         0  
206             } elsif (abs($dy) < abs($dx)) {
207 0         0 @d = ($DIAGS{$d}->[1],$DIAGS{$d}->[0]);
208             } else {
209 0         0 @d = randsort(@{$DIAGS{$d}});
  0         0  
210             }
211            
212 0         0 for (@d) {
213 0         0 $ok = $self->move($_);
214 0 0       0 $self->{odir} = '' if $ok;
215 0 0       0 return $ok if $ok;
216             }
217              
218 0 0       0 return 0 if $self->{singleminded} > 1;
219              
220             # try moving orthoganally to the way you want to go
221 0         0 for (randsort(orthogs($d))) {
222 0         0 $ok = $self->move($_);
223 0 0       0 $self->{area}->dprint("moved orthog $_") if $ok;
224 0 0       0 $self->{odir} = $_ if $ok;
225 0 0       0 return $ok if $ok;
226             }
227              
228 0         0 return 0;
229             }
230              
231             my %ORTHOGS = (
232             'n'=>['e','w','ne','nw'],
233             's'=>['e','w','se','sw'],
234             'e'=>['n','s','ne','se'],
235             'w'=>['n','s','nw','sw'],
236             'ne'=>['nw','se'],
237             'nw'=>['ne','sw'],
238             'se'=>['ne','sw'],
239             'sw'=>['nw','se'],
240             );
241              
242             sub orthogs {
243 0     0 0 0 my ($d) = @_;
244 0         0 return @{$ORTHOGS{$d}};
  0         0  
245             }
246              
247             =item kbdmove($c[, $testonly])
248              
249             Moves the mob in direction '$c': 'h' is LEFT, 'l' is RIGHT, etc.
250              
251             The testonly flag is passed to the "move" function.
252              
253             =cut
254              
255             sub kbdmove {
256 0     0 1 0 my $self = shift;
257 0         0 my ($c, $testonly) = @_;
258 0 0       0 if ($c eq '.') {
259 0         0 return $self->move('.', $testonly);
260             }
261 0 0 0     0 if ($c eq 'h' || $c eq 'LEFT') {
262 0         0 return $self->move('w', $testonly);
263             }
264 0 0 0     0 if ($c eq 'l' || $c eq 'RIGHT') {
265 0         0 return $self->move('e', $testonly);
266             }
267 0 0 0     0 if ($c eq 'j' || $c eq 'DOWN') {
268 0         0 return $self->move('s', $testonly);
269             }
270 0 0 0     0 if ($c eq 'k' || $c eq 'UP') {
271 0         0 return $self->move('n', $testonly);
272             }
273 0 0       0 if ($c eq 'y') {
274 0         0 return $self->move('nw', $testonly);
275             }
276 0 0       0 if ($c eq 'b') {
277 0         0 return $self->move('sw', $testonly);
278             }
279 0 0       0 if ($c eq 'u') {
280 0         0 return $self->move('ne', $testonly);
281             }
282 0 0       0 if ($c eq 'n') {
283 0         0 return $self->move('se', $testonly);
284             }
285 0         0 return 0;
286             }
287              
288             =item safetomove()
289              
290             Returns true if it's safe to continue autoexploring.
291              
292             Default behavior is to return false if any mobs are in view.
293              
294             =cut
295              
296             sub safetomove {
297 0     0 1 0 my $self = shift;
298 0         0 my $area = $self->{area};
299 0         0 for my $m (@{$area->{mobs}}) {
  0         0  
300 0 0       0 next if $m eq $self;
301 0 0       0 if ($area->checkpov($self, $m->{x}, $m->{y})) {
302 0         0 return 0;
303             }
304             }
305 0         0 return 1;
306             }
307              
308             =item autoex ([bool only1])
309              
310             Find closest unexplored square and move towards it until it's no longer unexplored.
311              
312             If world is specified, this loops and draws the map. Otherwise, it moves only 1 step.
313              
314             =cut
315              
316             sub autoex {
317 0     0 1 0 my $self = shift;
318 0         0 my ($world) = @_;
319              
320             # flood fill find unexplored area
321 0         0 my $area = $self->{area};
322 0         0 my ($x1, $y1) = ($self->{x}, $self->{y});
323 0         0 my $f;
324              
325             my @f;
326 0         0 push @f, [$x1, $y1, []];
327 0         0 my @bread;
328 0         0 my ($cx, $cy) = ($x1, $y1);
329 0         0 my $minlen = 1000000;
330              
331 0 0       0 if (!$self->safetomove()) {
332 0         0 return 0;
333             }
334              
335 0 0 0     0 if (!$world && $self->{autopath} && @{$self->{autopath}}) {
  0   0     0  
336 0         0 my $moved = $self->move(shift(@{$self->{autopath}}));
  0         0  
337 0 0       0 if ($self->{memory}->{$self->{area}->{name}}->[$self->{autocx}][$self->{autocy}]) {
338 0         0 $self->{autopath} = undef;
339             }
340 0         0 return $moved;
341             }
342            
343 0         0 my $path; # path to take
344 0         0 while (@f) {
345 0         0 my $c = shift @f; # breadth first
346 0         0 for (my $d=0;$d<8;++$d) {
347 0 0       0 next unless $self->{memory}->{$self->{area}->{name}}->[$c->[0]][$c->[1]]; # has to be "moving from" a place we have seen
348              
349 0         0 my $tx = $DD[$d]->[0]+$c->[0];
350 0         0 my $ty = $DD[$d]->[1]+$c->[1];
351 0         0 my $p = [@{$c->[2]}, $DIRS[$d]];
  0         0  
352              
353             # not off edge
354 0 0 0     0 next if $tx < 0 || $ty < 0;
355 0 0 0     0 next if $tx >= $area->{w} || $ty >= $area->{h};
356              
357 0 0       0 next if $bread[$tx][$ty];
358 0         0 $bread[$tx][$ty]='.'; #been there in this algorithm
359              
360 0         0 my $seen = $self->{memory}->{$self->{area}->{name}}->[$tx][$ty];
361              
362 0 0       0 if (!$seen) { # not explored already;
363 0         0 $path = $p;
364 0         0 $cx = $tx;
365 0         0 $cy = $ty;
366 0         0 @f = ();
367 0         0 last;
368             }
369              
370             # not thru void
371 0 0       0 next if !defined($area->{map}->[$tx][$ty]);
372 0 0       0 next if $area->{map}->[$tx][$ty] eq '';
373              
374             # not thru wall
375 0 0       0 next if index($area->{nomove}, $area->{map}->[$tx][$ty]) >= 0;
376            
377 0         0 push @f, [$tx, $ty, $p]; #add to list of places can get to;
378             }
379             }
380              
381 0 0       0 if ($path) {
382 0 0       0 if (!$world) {
383 0         0 my $moved = $self->move(shift(@{$path}));
  0         0  
384 0 0       0 if (!($self->{memory}->{$self->{area}->{name}}->[$cx][$cy])) {
385 0         0 $self->{autopath} = $path;
386 0         0 $self->{autocx} = $cx;
387 0         0 $self->{autocy} = $cy;
388             }
389 0         0 return $moved;
390             } else {
391 0         0 my $con = $world->{con};
392 0         0 my $stm=1;
393 0         0 for (@$path) {
394 0         0 my $bc = $con->nbgetch();
395 0 0 0     0 if ($bc eq 'q' || $bc eq 'ESC') {
396 0         0 $stm = 0;
397 0         0 last;
398             }
399 0         0 my $moved = $self->move($_);
400 0         0 $stm = $self->safetomove();
401 0 0       0 last if !$stm;
402             # explored the one we were looking for...remove this to reduce recusion at the expense of wasted moves
403 0 0       0 if (($self->{memory}->{$self->{area}->{name}}->[$cx][$cy])) {
404 0         0 last;
405             }
406             }
407 0 0       0 if (!$stm) {
408 0         0 return 1;
409             } else {
410 0         0 $world->drawmap();
411 0         0 $self->autoex(@_);
412             }
413             }
414             }
415             }
416              
417              
418             =item move (direction[, testonly])
419              
420             Uses checkmove to see whether the direction is ok. If it returns > 0, then moves the mob,
421             changing its x,y position, and saving the move in "lastmove".
422              
423             Aftermove is then called if the return value of checkmove was nonzero.
424              
425             =item lastmove
426              
427             Returns the direction parameter passed to "move" that resulted in a successful move.
428              
429             =cut
430              
431             sub move {
432 0     0 1 0 my $self = shift;
433 0         0 my ($d, $testonly) = @_; # news direction
434 0         0 my $nx = $self->{x} + $DD{$d}->[0];
435 0         0 my $ny = $self->{y} + $DD{$d}->[1];
436 0         0 my $r;
437 0         0 $r = $self->checkmove($nx, $ny, scalar $self->{area}->mobat($nx, $ny), $testonly);
438             # less than eq zero means remain still (but move may have occurred)
439 0 0       0 if (!$testonly) {
440 0 0       0 if ($r > 0) {
    0          
441 0         0 $self->{area}->dprint("moved $d");
442 0         0 $self->{x} = $nx;
443 0         0 $self->{y} = $ny;
444 0         0 $self->{lastmove} = $d;
445 0         0 $self->aftermove($d);
446             } elsif ($r < 0) {
447 0         0 $self->aftermove(undef);
448             }
449             }
450 0         0 return $r;
451             }
452              
453             sub getmovexy {
454 0     0 0 0 my $self = shift;
455 0         0 my ($d, $flag) = @_; # news direction
456 0         0 my $nx = $self->{x} + $DD{$d}->[0];
457 0         0 my $ny = $self->{y} + $DD{$d}->[1];
458 0         0 return ($nx, $ny);
459             }
460              
461             sub lastmove {
462 0     0 1 0 return $_[0]->{lastmove};
463             }
464              
465             =item aftermove (direction)
466              
467             Called after the mob moved with the direction it moved.
468              
469             If the mob attacks or otherwise moves "nowhere" it is called with 'undef' as the direction.
470              
471             =cut
472              
473 0     0 1 0 sub aftermove {
474             }
475              
476             =item checkmove (new-x, new-y, othermob (at location x/y), testonly)
477              
478             Called before the mob moves with the direction it will move if allowed.
479              
480             Return value 0 = no move occurs
481             Return value 1 = move occurs
482             Return value -1 = attack/move occured, but keep in the same place
483              
484             =cut
485              
486             sub checkmove {
487 0     0 1 0 my $self = shift;
488 0         0 my ($x, $y, $othermob, $testonly) = @_;
489 0 0       0 return 0 unless $self->{area}->{map}->[$x][$y] eq $self->{area}->{fsym};
490 0 0       0 return 0 unless !$othermob;
491 0         0 return 1;
492             }
493              
494             =item additem (item)
495              
496             Adds item to inventory. Override this to add pack full messages, etc.
497              
498             Return value 0 = can't add, too full
499             Return value 1 = add ok
500             Return value -1 = move occured, but not added
501              
502             =cut
503              
504             sub additem {
505 2     2 1 4 my $self = shift;
506 2         22 my $item = shift;
507             # i'm never full
508 2         13 return $item->setcont($self);
509             }
510              
511             =item delitem (item)
512              
513             Removes item from the mob.
514              
515             =cut
516              
517             sub delitem {
518 0     0 1   my $self = shift;
519 0           my $ob = shift;
520 0 0         confess("not a mob") unless $self->isa('Games::Roguelike::Mob');
521 0           $self->{area}->dprint("h1");
522 0           my $i = 0;
523 0           for (@{$self->{items}}) {
  0            
524 0 0         if ($_ == $ob) {
525 0           splice @{$self->{items}}, $i, 1;
  0            
526 0           return $_;;
527             }
528 0           ++$i;
529             }
530 0           return undef;
531             }
532              
533             =item dropitem (item)
534              
535             Removes item, changes it's coordinates, and then tries to put it in the "area".
536              
537             Returns the result of the "additem" from the area object (which may be a failure).
538              
539             =cut
540              
541             sub dropitem {
542 0     0 1   my $self = shift;
543 0           my $item = shift;
544 0           $item->{x} = $self->{x};
545 0           $item->{y} = $self->{y};
546 0           $self->{area}->additem($item);
547 0           return 1;
548             }
549              
550             =back
551              
552             =head1 AUTHOR
553              
554             Erik Aronesty C
555              
556             =head1 LICENSE
557              
558             This program is free software; you can redistribute it and/or
559             modify it under the same terms as Perl itself.
560              
561             See L or the included LICENSE file.
562              
563             =cut
564              
565             1;