File Coverage

blib/lib/Games/Roguelike/World.pm
Criterion Covered Total %
statement 83 216 38.4
branch 18 92 19.5
condition 4 39 10.2
subroutine 17 32 53.1
pod 17 21 80.9
total 139 400 34.7


line stmt bran cond sub pod time code
1             package Games::Roguelike::World;
2              
3             # purpose of library:
4             #
5             # keep track of map/location
6             # convenience for collision, line of sight, path-finding
7             # assume some roguelike concepts (mobs/items)
8             # allow someone to make 7-day rl's in 7-days
9              
10             =head1 NAME
11              
12             Games::Roguelike::World - Roguelike World
13              
14             =head1 SYNOPSIS
15              
16             package myWorld;
17             use base 'Games::Roguelike::World';
18              
19             $r = myWorld->new(w=>80,h=>50,dispw=>40,disph=>18); # creates a world with specified width/height & map display width/height
20             $r->area(new Games::Roguelike::Area(name=>'1')); # create a new area in this world called "1"
21             $r->area->genmaze2(); # make a cavelike maze
22             $char = Games::Roguelike::Mob->new($r->area, sym=>'@', pov=>8); # add a mobile object with symbol '@'
23             $r->setvp($char); # set viewpoint to be from $char's perspective
24             $r->drawmap(); # draw the active area map from the current perspective
25             while (!((my $c = $r->getch()) eq 'q')) {
26             $char->kbdmove($c);
27             $r->drawmap();
28             }
29              
30             =head1 DESCRIPTION
31              
32             General pupose object which pulls together field of view, item, mob handling and map drawing code.
33              
34             * contains a hash of Games::Roguelike::Area's for each "level" or "region" in the game
35             * uses the Games::Roguelike::Console library to draw the current area
36             * assumes the user will be using overridden Games::Roguelike::Mob's as characters in the game
37             * assumes the user will be using overridden Games::Roguelike::Item's as items in the game
38              
39             =head2 METHODS
40              
41             =over 4
42              
43             =cut
44              
45 3     3   30027 use strict;
  3         9  
  3         145  
46 3     3   600 use Games::Roguelike::Utils qw(:all);
  3         13  
  3         1496  
47 3     3   300 use Games::Roguelike::Console;
  3         6  
  3         141  
48 3     3   19 use Games::Roguelike::Mob;
  3         8  
  3         78  
49              
50 3     3   9413 use Math::Trig;
  3         130200  
  3         689  
51 3     3   42 use Data::Dumper;
  3         7  
  3         158  
52 3     3   19 use Carp qw(croak confess carp);
  3         8  
  3         13227  
53              
54             our $AUTOLOAD;
55             our $VERSION = '0.4.' . [qw$Revision: 256 $]->[1];
56              
57             =item new(OPT1=>VAL1, OPT2=>VAL2...)
58            
59             Options can also all be set/get as class accessors:
60              
61             vp => undef # Games::Roguelike::Mob that is the 'viewpoint'
62             dispx, dispy => (0,1) # x/y location, of the map
63             dispw, disph => (60,24) # width & height of the map
64             msgx, msgy => (0,0) # x/y location of the "scrolling message box"
65             msgw, msgh => (60, 1) # width & height of the "scrolling message box"
66             maxlog => 80, # maximum number of rows stored message log
67             msgoldcolor => 'gray', # color of non-curent messages (if left blank, color is left alone)
68             wsym => '#', # default wall symbol
69             fsym => '.', # default floor symbol
70             dsym => '+', # default door symbol
71             debugmap => 0, # turn on map coordinate display
72             debug => 0, # debug level (higher = more)
73             noview => '#+', # list of symbols that block view
74             nomove => '#', # list of symbols that block movement
75             area => undef, # Games::Roguelike::Area that contains the currrent map
76            
77             None of these features have to be used, and can be easily ignored or overridden.
78              
79             =cut
80              
81             sub new {
82 5     5 1 15381 my $pkg = shift;
83 5 50       31 croak "usage: Games::Roguelike::World->new()" unless $pkg;
84              
85 5         19 my $self = bless {}, $pkg;
86 5         45 $self->init(@_);
87 5         92 return $self;
88             }
89              
90             sub init {
91 5     5 0 10 my $self = shift;
92              
93 5         53 $self->{hasmem} = 1;
94 5         17 $self->{dispy} = 1;
95 5         16 $self->{dispx} = 0;
96 5         14 $self->{h} = 40;
97 5         14 $self->{w} = 80;
98 5         15 $self->{maxlog} = 80;
99 5         12 $self->{msgx} = 0;
100 5         19 $self->{msgoldcolor} = 'gray';
101 5         16 $self->{msgy} = 0;
102 5         11 $self->{msgh} = 1;
103 5         28 $self->{noview} = '#+';
104 5         15 $self->{wsym} = '#'; # default wall symbol
105 5         12 $self->{fsym} = '.'; # default floor symbol
106 5         17 $self->{dsym} = '+';
107 5         11 $self->{debugmap} = 0;
108 5         20 $self->{vp} = undef;
109 5         15 $self->{dn} = 0;
110 5         16 $self->{memcolor} = 'gray';
111              
112             # allow all of the above to be overridden by params
113 5         43 while( my ($k, $v) = splice(@_, 0, 2)) {
114 16         58 $self->{$k} = $v;
115             }
116            
117 5 50       28 $self->{nomove} = $self->{wsym} unless $self->{nomove}; # by default, can't move through walls
118 5 100       31 $self->{disph} = min(24, $self->{h}) unless $self->{disph}; # default display sizes
119 5 100       26 $self->{dispw} = min(60,$self->{w}) unless $self->{dispw};
120 5 50       46 $self->{msgw} = min(60,$self->{dispw}) unless $self->{msgw}; # default message window size
121              
122             # create console object
123 5 100 66     91 $self->{con} = new Games::Roguelike::Console(noinit=>$self->{noinit}, type=>$self->{console_type})
124             unless $self->{con} || $self->{noconsole};
125             }
126              
127             =item area([name or Games::Roguelike::Area])
128              
129             No arguments: returns the current area
130              
131             Specify a scalar name: returns an area with that name
132              
133             Specify an Games::Roguelike::Area object: stores that object in the area hash,
134             overwriting any with the same name, then makes it the active area
135              
136             =cut
137              
138             sub area {
139 12     12 1 27 my $self = shift;
140 12 100       36 if (@_) {
141 5 100       14 if (ref($_[0])) {
142 4         17 my $area = shift;
143 4         30 $self->addarea($area);
144 4         10 $self->{area} = $area;
145             } else {
146 1         13 return $self->{areas}->{$_[0]};
147             }
148             }
149 11         228 return $self->{area};
150             }
151              
152             sub areas {
153 0     0 0 0 my $self = shift;
154 0         0 return values(%{$self->{areas}});
  0         0  
155             }
156              
157             sub addarea {
158 4     4 0 6 my $self = shift;
159 4         7 my $area = shift;
160 4 50 33     24 confess("this world already has an area named $area->{name}")
161             if $self->{areas}->{$area->{name}} && $self->{areas}->{$area->{name}} != $area;
162 4         14 $self->{areas}->{$area->{name}} = $area;
163             }
164              
165             # perl accessors are slow compared to just accessing the hash directly
166             # autoload is even slower
167             sub AUTOLOAD {
168 3     3   66 my $self = shift;
169 3 50       15 my $pkg = ref($self) or croak "$self is not an object";
170              
171 3         9 my $name = $AUTOLOAD;
172 3         20 $name =~ s/.*://; # strip fully-qualified portion
173              
174 3 50 33     36 $name =~ s/^set// if @_ && !exists $self->{$name};
175              
176 3 50       14 unless (exists $self->{$name}) {
177 0         0 croak "Can't access `$name' field in class $pkg";
178             }
179              
180 3 50       13 if (@_) {
181 3         51 return $self->{$name} = $_[0];
182             } else {
183 0         0 return $self->{$name};
184             }
185             }
186              
187 1     1   108 sub DESTROY {
188             }
189              
190             =item dprint ( msg1 [,msg2...msgn] [,level] )
191              
192             Debug print messages
193              
194             For now, hard coded to far right side of screen, at col 82, past most terminal game widths
195              
196             =cut
197            
198             sub dprint {
199 0     0 1 0 my $self = shift;
200              
201 0         0 my $level = 1;
202              
203             # last arg is an integer number
204 0 0       0 $level = pop if int(0+$_[$#_]) eq $_[$#_];
205              
206 0 0       0 return unless $self->{debug} >= $level;
207              
208             #windows cant have a "wide" console
209 0 0 0     0 if ($self->{con} && ref($self->{con}) !~ /win32/i && ref($self->{con}) !~ /dump/i) {
      0        
210 0         0 my $msg = substr(join("\t",@_),0,40);
211 0         0 $self->{con}->addstr($self->{dn},82,$msg . (" " x (40-length($msg))));
212 0         0 ++$self->{dn};
213 0 0       0 $self->{dn} = 0 if $self->{dn} > 30;
214             } else {
215 0         0 my $msg = join("\t",@_);
216 0         0 open DEBUG, ">>rll-debug.txt";
217 0         0 print DEBUG scalar(localtime), "\t", $msg, "\n";
218 0         0 close DEBUG;
219             }
220             }
221              
222             =item getch ()
223              
224             Read one character, blocks until a char is pressed.
225              
226             =cut
227              
228             sub getch {
229 3     3 1 45 my $self = shift;
230 3         23 $self->{con}->getch();
231             }
232              
233             =item getstr ([echo=>1[,empty=>0]])
234              
235             Calls getch repeatedly, optionally echoing characters to the active console. If "empty" is not
236             set to true, it will not return empty strings.
237              
238             =cut
239              
240             sub getstr {
241 0     0 1 0 my $self = shift;
242 0         0 my %opts = @_;
243 0 0       0 $opts{max} = 40 if !defined $opts{max};
244 0 0       0 $opts{echo} = 1 if !defined $opts{echo};
245 0 0       0 $opts{empty} = 0 if !defined $opts{empty};
246              
247 0         0 $self->{con}->cursor(1);
248 0         0 my ($c, $str);
249 0         0 while (1) {
250 0         0 $c = $self->{con}->getch();
251 0 0       0 if ($c =~ /[\n\r]/) {
252 0 0 0     0 last if length($str) > 0 || $opts{empty};
253             }
254 0 0 0     0 if ($opts{echo} && length($str) < $opts{max}) {
255 0 0 0     0 if ($c eq 'BACKSPACE') {
    0 0        
256 0         0 $self->{con}->addch(chr(8));
257 0         0 $self->{con}->addch(' ');
258 0         0 $self->{con}->addch(chr(8));
259             } elsif ((length($c)==1) && (ord($c) > 30) && (ord($c) < 128)) {
260 0         0 $self->{con}->addch($c);
261             }
262             }
263 0         0 $self->{con}->refresh();
264 0 0 0     0 if ($c eq 'BACKSPACE') {
    0 0        
265 0         0 $str = substr($str, 0, -1);
266             } elsif ((length($c)==1) && (ord($c) > 30) && (ord($c) < 128)) {
267 0         0 $str .= $c;
268             };
269 0 0       0 $c = '' if !length($str);
270             }
271              
272 0         0 $self->{con}->cursor(0);
273 0         0 chomp $str;
274 0         0 return $str;
275             }
276              
277              
278             =item refresh ()
279              
280             Refreshes the console display.
281              
282             =cut
283              
284             sub refresh {
285 0     0 1 0 my $self = shift;
286 0         0 $self->{con}->refresh();
287             }
288              
289             =item nbgetch ()
290              
291             Read one character, nonblocking, returns undef if none are available.
292              
293             =cut
294              
295             sub nbgetch {
296 0     0 1 0 my $self = shift;
297 0         0 $self->{con}->nbgetch();
298             }
299              
300             =item findfeature (symbol)
301              
302             searches "map feature list" for the given symbol, returns coordinates if found
303              
304             =cut
305              
306             sub findfeature {
307 0     0 1 0 my $self = shift;
308 0         0 return $self->{area}->findfeature(@_);
309             }
310              
311             =item dispclear ()
312              
313             Erases the "display world", and resets the "display line" (used by dispstr)
314              
315             Useful for displaying an in-game menu, inventory, ability or skill list, etc.
316              
317             =cut
318              
319             sub dispclear {
320 0     0 1 0 my $self = shift;
321              
322 0         0 my ($y) = @_;
323 0 0       0 $y = $self->{dispy} if ! defined $y;
324              
325 0         0 for (my $i = $y; $i < ($self->{disph}+$self->{dispy}); ++$i) {
326 0         0 $self->{con}->addstr($i,$self->{dispx}," " x ($self->{dispw}));
327             }
328 0         0 $self->{displine} = $self->{dispy};
329             }
330              
331             =item dispstr (str[, line])
332              
333             Draws a tagged string at the "displine" position and increments the "displine".
334              
335             Return value: 0 (offscreen, did not draw), 1 (ok), 2 (ok, but next call will be offscreen).
336              
337             =cut
338              
339             sub dispstr {
340 0     0 1 0 my $self = shift;
341 0         0 my ($str, $line) = @_;
342            
343 0         0 my $ret = 1;
344              
345 0 0       0 if ($line) {
346 0         0 $self->{displine} = $line;
347             }
348              
349 0         0 for (split(/\n/, $str)) {
350 0 0       0 if ($self->{displine} >= ($self->{dispy} + $self->{disph})) {
351 0         0 return 0;
352             }
353 0         0 $self->{con}->tagstr($self->{displine}, $self->{dispx}, rpad($_, $self->{dispw}));
354 0         0 $self->{con}->move($self->{displine}, $self->{dispx}+length($_));
355 0         0 $self->{displine} += 1;
356             }
357              
358 0 0       0 if ($self->{displine} >= ($self->{dispy} + $self->{disph})) {
359 0         0 $ret = 2;
360             }
361              
362 0         0 return $ret;
363             }
364              
365             =item drawmap ()
366              
367             Draws the map, usually do this after each move
368              
369             =cut
370              
371             sub drawmap {
372 3     3 1 27 my $self = shift;
373 3         23 $self->{area}->draw($self);
374             }
375              
376             =item prompt (msg[, match])
377              
378             Same as showmsg, but also shows the cursor, and gets a character response, optionally waiting until it matches.
379              
380             =cut
381              
382             sub prompt {
383 0     0 1   my $self = shift;
384 0           my ($msg, $match) = @_;
385 0 0         $match = '.' if !$match;
386 0           $self->showmsg($msg);
387 0           $self->{con}->cursor(1);
388 0           $self->{con}->move($self->{msgy},$self->{msgx}+length($msg)+1);
389 0           my $c;
390 0           do {
391 0           $c = $self->getch();
392             } while ($c !~ /$match/);
393 0           $self->{con}->cursor(0);
394 0           return $c;
395             }
396              
397             =item cursor (bool)
398              
399             Turn on/off display of cursor for next operation.
400              
401             =cut
402              
403             sub cursor {
404 0     0 1   my $self = shift;
405 0           $self->{con}->cursor(@_);
406             }
407              
408             =item pushmsg (msg, color)
409              
410             Shows a message and pushes it into the log. Use of color argument is deprecated. Prefer to use "<$color>$msg" tagged strings.
411              
412             =cut
413              
414             sub pushmsg {
415 0     0 1   return showmsg(@_[0..2],1);
416             }
417              
418             =item showmsg (msg, color[, push])
419              
420             Shows a message at msgx, msgy coorinates and optionally logs it. Also displays up to (msgh-1) old messages.
421              
422             =cut
423              
424             sub showmsg {
425 0     0 1   my $self = shift;
426 0           my ($msg, $color, $keep) = @_;
427 0           $msg = substr($msg, 0, $self->{msgw});
428              
429             # use the character's log, unless there is none
430 0 0         my $msglog = $self->{vp} ? $self->{vp}->{msglog} : $self->{msglog} ? $self->{msglog} : ($self->{msglog} = []);
    0          
431              
432 0           push @$msglog, [$msg, $color];
433            
434 0 0         if (@$msglog > $self->{maxlog}) {
435 0           shift @$msglog;
436             }
437              
438 0           my $mlx = $#{$msglog};
  0            
439 0           for (my $i = 0; $i < $self->{msgh}; ++$i) {
440 0 0         next unless $i <= $mlx; # no more messages in log
441 0           my ($m, $a) = @{$msglog->[$mlx-$i]};
  0            
442 0 0         if ($self->{msgoldcolor}) {
443 0 0         $a = $self->{msgoldcolor} if $i > 0;
444 0           $m =~ s/<[^<>]*>//g;
445             }
446 0 0         $m = "<$a>$m" if $a;
447 0           $self->{con}->tagstr($self->{msgy}+$i, $self->{msgx}, $m.(' 'x($self->{msgw}-length($m))));
448             }
449              
450 0           $self->{con}->move($self->{msgy},$self->{msgx}+length($msglog->[0]->[0]));
451              
452 0 0         if (!$keep) {
453 0           pop @$msglog;
454             }
455              
456 0           $self->{con}->cursor(0);
457 0           $self->{con}->refresh();
458             }
459              
460             sub showmsglog {
461 0     0 0   my @sort;
462 0           my $self = shift;
463 0           my $x = $self->{dispx};
464 0           my $y = $self->{dispy};
465 0           my $h = $self->{disph};
466 0 0 0       if ($x == $self->{msgx} && ($self->{msgy} + $self->{msgh}) == $y) {
467 0           $y=$self->{msgy};
468             }
469 0 0 0       if ($x == $self->{msgx} && ($y + $self->{disph}) == $self->{msgy}) {
470 0           $h = $self->{disph} + $self->{msgh};
471             }
472 0           for (@{$self->{vp}->{msglog}}) {
  0            
473 0           my ($msg,$color) = @$_;
474 0           $self->{con}->attrstr($color,$y,$x,$msg.(' 'x($self->{dispw}-length($msg))));
475 0           ++$y;
476 0 0         last if $y >= $h;
477             }
478             }
479              
480             =item save ([file])
481              
482             Saves the world (!), optionally specify filename which defaults to "rll.world".
483              
484             =cut
485              
486             sub save {
487 0     0 1   my $self = shift;
488 0           my $fn = shift;
489 0 0         $fn = "rll.world" if (!$fn);
490 3     3   5772 use Storable;
  3         13389  
  3         519  
491 0           local $self->{con} = undef;
492 0           store $self,$fn;
493             }
494              
495             =item load ([file])
496              
497             Loads a world, optionally specify filename, returns a reference to the new world.
498              
499             Console is not initialized, and is, instead, copied from the current world.
500              
501             =cut
502              
503             sub load {
504 0     0 1   my $self = shift;
505 0           my $fn = shift;
506 0 0         $fn = "rll.world" if (!$fn);
507 3     3   34 use Storable;
  3         6  
  3         347  
508              
509 0           my $n = retrieve $fn;
510              
511 0           $n->{con} = $self->{con};
512 0           $n->{console_type} = $self->{console_type};
513              
514 0           return $n;
515             }
516              
517             =back
518              
519             =head1 SEE ALSO
520              
521             L, L, L
522              
523             =head1 AUTHOR
524              
525             Erik Aronesty C
526              
527             =head1 LICENSE
528              
529             This program is free software; you can redistribute it and/or
530             modify it under the same terms as Perl itself.
531              
532             See L or the included LICENSE file.
533              
534             =cut
535              
536             1;
537