File Coverage

blib/lib/Term/Animation.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Term::Animation;
2              
3 1     1   25186 use 5.006;
  1         4  
  1         39  
4 1     1   5 use strict;
  1         2  
  1         34  
5 1     1   4 use warnings;
  1         7  
  1         35  
6 1     1   5 use Carp;
  1         1  
  1         117  
7 1     1   480 use Curses;
  0            
  0            
8             use Term::Animation::Entity;
9              
10             use Data::Dumper;
11              
12             =head1 NAME
13              
14             Term::Animation - ASCII sprite animation framework
15              
16             =head1 SYNOPSIS
17              
18             use Term::Animation;
19              
20             # Constructors
21             $anim = Term::Animation->new();
22             $anim = Term::Animation->new($curses_window);
23              
24             =head1 ABSTRACT
25              
26             A framework to produce sprite animations using ASCII art.
27              
28             =head1 DESCRIPTION
29              
30             This module provides a framework to produce sprite animations using
31             ASCII art. Each ASCII 'sprite' is given one or more frames, and placed
32             into the animation as an 'animation object'. An animation object can
33             have a callback routine that controls the position and frame of the
34             object.
35              
36             If the constructor is passed no arguments, it assumes that it is
37             running full screen, and behaves accordingly. Alternatively, it can
38             accept a curses window (created with the Curses I call) as an
39             argument, and will draw into that window.
40              
41             =head1 EXAMPLES
42              
43             This example moves a small object across the screen from left to right.
44              
45             use Term::Animation;
46             use Curses;
47              
48             $anim = Term::Animation->new();
49              
50             # set the delay for getch
51             halfdelay( 2 );
52              
53             # create a simple shape we can move around
54             $shape = "<=O=>";
55              
56             # turn our shape into an animation object
57             $anim->new_entity(
58             shape => $shape, # object shape
59             position => [3, 7, 10], # row / column / depth
60             callback_args => [1, 0, 0, 0], # the default callback
61             # routine takes a list
62             # of x,y,z,frame deltas
63             wrap => 1 # turn screen wrap on
64             );
65              
66             # animation loop
67             while(1) {
68             # run and display a single animation frame
69             $anim->animate();
70              
71             # use getch to control the frame rate, and get input at the
72             # same time. (not a good idea if you are expecting much input)
73             my $input = getch();
74             if($input eq 'q') { last; }
75             }
76              
77             # cleanly end the animation, to avoid hosing up the user's terminal
78             $anim->end();
79              
80             This illustrates how to draw your animation into an existing Curses window.
81              
82             use Term::Animation;
83             use Curses;
84              
85             # Term::Animation will not call initscr for you if
86             # you pass it a window
87             initscr();
88              
89             $win = newwin(5,10,8,7);
90              
91             $anim = Term::Animation->new($win);
92              
93             Everything else would be identical to the previous example.
94              
95             =head1 METHODS
96              
97             =over 4
98              
99             =cut
100              
101             our $VERSION = '2.6';
102              
103             our ($color_names, $color_ids) = _color_list();
104              
105             =item I
106              
107             $anim = Term::Animation->new();
108             $anim = Term::Animation->new($curses_window);
109              
110             The constructor. Optionally takes an existing curses window
111             to draw in.
112              
113             =cut
114             sub new {
115             my $proto = shift;
116             my $class = ref($proto) || $proto;
117             my $self = {};
118              
119             $self->{ENTITIES} = {};
120             $self->{ENTITYCOUNT} = 0;
121             $self->{PHYSICALENTITIES} = {};
122             $self->{PHYSICALCOUNT} = 0;
123             $self->{COLOR_ENABLED} = 0;
124             $self->{LAST_FRAME_TIME} = 0;
125              
126             # framerate related settings
127             $self->{TRACK_FRAMERATE} = 1;
128             $self->{FRAMERATE} = 0;
129             $self->{FRAMES_THIS_SECOND} = 0;
130              
131             $self->{WIN} = shift;
132             if(defined($self->{WIN})) {
133             unless(ref($self->{WIN}) eq 'Curses::Window') {
134             carp("Expecting Curses::Window object, recieved " . ref($self->{WIN}));
135             return undef;
136             }
137             $self->{FULLSCREEN} = 0;
138             } else {
139             # this is the method in the docs...
140             $self->{WIN} = new Curses;
141             # ...but apparently it's broken with some versions of Curses or ncurses.
142             # this seems to work everywhere, but the Curses.pm docs
143             # say to call the constructor when using objects.
144             unless(defined($self->{WIN})) {
145             $self->{WIN} = Curses::initscr();
146             }
147              
148             noecho();
149             curs_set(0);
150             $self->{FULLSCREEN} = 1;
151             }
152              
153             ($self->{WIDTH}, $self->{HEIGHT}, $self->{ASSUMED_SIZE}) = _get_term_size($self->{WIN});
154             bless ($self, $class);
155             return $self;
156             }
157              
158             sub DESTROY {
159             my ($self) = @_;
160             if($self->{FULLSCREEN}) {
161             endwin();
162             }
163             }
164              
165             =item I
166              
167             $anim->new_entity(
168             shape => $shape,
169             position => [ 1, 2, 3 ],
170             callback_args => [ 1, 0, 0 ]
171             );
172              
173             Creates a new Term::Animation::Entity object and adds it to the
174             animation. This is identical to:
175              
176             my $entity = Term::Animation::Entity->new(...);
177             $anim->add_entity($entity);
178              
179             See L and L
180             in L for details on calling this method.
181              
182             =cut
183             sub new_entity {
184             my ($self, @ent_args) = @_;
185             my $entity = Term::Animation::Entity->new(@ent_args);
186             $self->add_entity($entity);
187             return $entity;
188             }
189              
190             ##################### COLOR UTILITIES #######################
191              
192             # create lists mapping full color names (eg. 'blue') and
193             # single character color ids (eg. 'b')
194             sub _color_list {
195             my %color_n;
196             my %color_i = (
197             black => 'k',
198             white => 'w',
199             red => 'r',
200             green => 'g',
201             blue => 'b',
202             cyan => 'c',
203             magenta => 'm',
204             yellow => 'y',
205             );
206              
207             for (keys %color_i) {
208             $color_i{uc($_)} = uc($color_i{$_});
209             }
210              
211             for (keys %color_i) {
212             $color_n{$color_i{$_}} = $_;
213             $color_n{$_} = $_;
214             $color_n{uc($_)} = uc($_);
215             }
216              
217             for(qw{ k w r g b c m y }) {
218             $color_i{$_} = $_;
219             $color_i{uc($_)} = uc($_);
220             }
221              
222             return (\%color_n, \%color_i);
223             }
224              
225             # build a list of every color combination for our current
226             # background color
227             sub _set_colors {
228             my ($self) = @_;
229              
230             my $cid = 1;
231              
232             my $bg = eval "Curses::COLOR_$self->{BG}";
233              
234             for my $f ('w', 'r', 'g', 'b', 'c', 'm', 'y', 'k') {
235             my $c = uc(color_name($f));
236             init_pair($cid, eval "Curses::COLOR_$c", $bg);
237             $self->{COLORS}{$f} = COLOR_PAIR($cid);
238             $cid++;
239             }
240             }
241              
242             =item I
243              
244             $name = $anim->color_name( $color );
245              
246             Returns the full name of a color, given either a full
247             name or a single character abbreviation.
248              
249             =cut
250             sub color_name {
251             my ($color) = @_;
252             if(defined($color_names->{$color})) {
253             return $color_names->{$color};
254             } else {
255             carp("Attempt to allocate unknown color: $color");
256             return undef;
257             }
258             }
259              
260             =item I
261              
262             $id = $anim->color_id( $color );
263              
264             Returns the single character abbreviation for a color,
265             given either a full name or abbreviation.
266              
267             =cut
268             sub color_id {
269             my ($color) = @_;
270             if(defined($color_ids->{$color})) {
271             return $color_ids->{$color};
272             } else {
273             carp("Attempt to allocate unknown color: $color");
274             return undef;
275             }
276             }
277              
278             =item I
279              
280             my $is_valid = $anim->is_valid_color($color_name);
281              
282             Returns true if the supplied string is a valid color name ('blue')
283             or a valid color id ('b').
284              
285             =cut
286             sub is_valid_color {
287             my ($color) = @_;
288             return(defined($color_ids->{$color}));
289             }
290              
291             =item I
292              
293             my $state = $anim->color();
294             $anim->color($new_state);
295              
296             Enable or disable ANSI color. This MUST be called immediately after creating
297             the animation object if you want color, because the Curses start_color call must
298             be made immediately. You can then turn color on and off whenever you want.
299              
300             =cut
301             sub color {
302             my $self = shift;
303             if(@_) {
304             my $enable = shift;
305             if($enable != $self->{COLOR_ENABLED}) {
306             if($enable) {
307             start_color();
308             unless(defined($self->{BG})) { $self->{BG} = 'BLACK'; }
309             $self->_set_colors();
310             $self->{WIN}->bkgdset($self->{COLORS}{'w'});
311             }
312             $self->{COLOR_ENABLED} = $enable;
313             }
314             }
315             return $self->{COLOR_ENABLED};
316             }
317              
318             =item I
319              
320             $anim->background( $color );
321              
322             Change the background color. The default background color is black. You
323             can only have one background color for the entire Curses window that
324             the animation is running in.
325              
326             =cut
327             sub background {
328             my $self = shift;
329             if(@_) {
330             my $color = shift;
331             my $bg_color = color_name($color);
332             if(defined($bg_color)) {
333             $self->{BG} = uc($bg_color);
334             $self->_set_colors();
335             $self->{WIN}->bkgdset($self->{COLORS}{'w'});
336             }
337             }
338             return $self->{BG};
339             }
340              
341             ########## END COLOR UTILITIES ###########
342              
343             ########################## PHYSICS UTILITIES ##########################
344              
345              
346             # go through all of the physical entities looking for
347             # collisions.
348             sub _find_collisions {
349             my ($self) = @_;
350              
351             my @col_set = ();
352             my @coord = ();
353             my @size = ();
354             my @name = ();
355              
356             for my $ent (values %{$self->{ENTITIES}}) {
357             next unless($ent->physical());
358             push(@coord, [ $ent->position() ]);
359             push(@size, [ $ent->size() ]);
360             push(@name, $ent->name());
361              
362             for my $i (0..($#name-1)) {
363             # X
364             if( ($coord[$i][0] <= $coord[-1][0] and $coord[-1][0] < $coord[$i][0] + $size[$i][0]) or
365             ($coord[-1][0] <= $coord[$i][0] and $coord[$i][0] < $coord[-1][0] + $size[-1][0]) ) {
366             # Y
367             if( ($coord[$i][1] <= $coord[-1][1] and $coord[-1][1] < $coord[$i][1] + $size[$i][1]) or
368             ($coord[-1][1] <= $coord[$i][1] and $coord[$i][1] < $coord[-1][1] + $size[-1][1]) ) {
369             # Z
370             if( ($coord[$i][2] <= $coord[-1][2] and $coord[-1][2] < $coord[$i][2] + $size[$i][2]) or
371             ($coord[-1][2] <= $coord[$i][2] and $coord[$i][2] < $coord[-1][2] + $size[-1][2]) ) {
372             push( @{$ent->{COLLISIONS}}, $self->{ENTITIES}{$name[$i]} );
373             push( @{$self->{ENTITIES}{$name[$i]}{COLLISIONS}}, $ent );
374              
375             }
376             }
377             }
378             }
379             }
380              
381             return;
382             }
383              
384             # update the list of physical entities when the physical state
385             # of an entity changes
386             sub _update_physical {
387             my ($self, $entity) = @_;
388             if($entity->{PHYSICAL} && !defined($self->{PHYSICALENTITIES}{$entity->{NAME}})) {
389             $self->{PHYSICALCOUNT}++;
390             $self->{PHYSICALENTITIES}{$entity->{NAME}} = $entity;
391             } elsif(defined($self->{PHYSICALENTITIES}{$entity->{NAME}})) {
392             $self->{PHYSICALCOUNT}--;
393             delete $self->{PHYSICALENTITIES}{$entity->{NAME}};
394             }
395             }
396              
397             ########## END PHYSICS UTILITIES ###########
398              
399             =item I
400              
401             $anim->animate();
402              
403             Perform a single animation cycle. Runs all of the callbacks,
404             does collision detection, and updates the display.
405              
406             =cut
407             sub animate {
408             my ($self) = @_;
409             $self->_do_callbacks();
410             if($self->{PHYSICALCOUNT} > 0) {
411             $self->_find_collisions();
412             $self->_collision_handlers();
413             }
414             $self->_remove_deleted_entities();
415             $self->_move_followers();
416             $self->_build_screen();
417             $self->_display_screen();
418             $self->_track_frame_rate() if $self->{TRACK_FRAMERATE};
419             }
420              
421             sub _track_frame_rate {
422             my ($self) = @_;
423             my $time = time();
424             if($time > $self->{LAST_FRAME_TIME}) {
425             $self->{LAST_FRAME_TIME} = $time;
426             $self->{FRAMERATE} = ($self->{FRAMERATE} + ($self->{FRAMES_THIS_SECOND} * 2) ) / 3;
427             $self->{FRAMES_THIS_SECOND} = 1;
428             } else {
429             $self->{FRAMES_THIS_SECOND}++;
430             }
431             }
432              
433             =item I
434              
435             $anim->track_framerate(1);
436             $tracking_framerate = $anim->track_framerate();
437              
438             Get or set the flag that indicates whether the module
439             should keep track of the animation framerate. This is
440             enabled by default.
441              
442             =cut
443             sub track_framerate {
444             my ($self) = @_;
445             if(@_) {
446             $self->{TRACK_FRAMERATE} = shift;
447             }
448             return $self->{TRACK_FRAMERATE};
449             }
450              
451             =item I
452              
453             $frames_per_second = $anim->framerate();
454              
455             Returns the approximate number of frames being displayed
456             per second, as indicated by calls to the I method.
457              
458             =cut
459             sub framerate {
460             my ($self) = @_;
461             return $self->{FRAMERATE};
462             }
463              
464             =item I
465              
466             my ($width, $height, $assumed_size) = $anim->screen_size();
467              
468             Returns the width and height of the screen. The third value
469             returned is a boolean indicating whether or not the default
470             screen size was used, because the size could not be determined.
471              
472             =cut
473             sub screen_size {
474             my $self = shift;
475             return($self->{WIDTH}, $self->{HEIGHT}, $self->{ASSUMED_SIZE});
476             }
477              
478             =item I
479              
480             $anim->update_term_size();
481              
482             Call this if you suspect the terminal size has changed (eg. if you
483             get a SIGWINCH signal). Call I after this if
484             you want to recreate your animation from scratch.
485              
486             =cut
487             sub update_term_size {
488             my $self = shift;
489             # dunno how portable this is. i should probably be using
490             # resizeterm.
491             endwin();
492             refresh();
493             ($self->{WIDTH}, $self->{HEIGHT}, $self->{ASSUMED_SIZE}) = _get_term_size($self->{WIN});
494             }
495              
496             # try to figure out the terminal size, and set
497             # a reasonable size if we can't. the 'assumed_size'
498             # variable will let programs know if we had to
499             # guess or not.
500             sub _get_term_size {
501             my $win = shift;
502             my ($width, $height, $assumed_size);
503             # find the width and height of the terminal
504             $width = $win->getmaxx();
505             $height = $win->getmaxy();
506             if($width and $height) {
507             $assumed_size = 0; # so we know if we can limit the max size or not
508             } else {
509             $assumed_size = 1;
510             $width = 80;
511             $height = 24;
512             }
513             return($width, $height, $assumed_size);
514             }
515              
516             # write to the curses window
517             sub _build_screen {
518             my($self) = @_;
519              
520             # clear the window before we start redrawing
521             $self->{WIN}->addstr( 0, 0, ' 'x$self->size() );
522              
523             return unless($self->{ENTITYCOUNT});
524             foreach my $entity (sort {$b->{'Z'} <=> $a->{'Z'}} values %{$self->{ENTITIES}}) {
525             _draw_entity($self, $entity);
526             }
527             }
528              
529             # draw an entity into the curses window in memory
530             sub _draw_entity {
531             my ($self, $entity) = @_;
532              
533             # a few temporary variables to make the code below easier to read
534             my $shape = $entity->{SHAPE}[$entity->{CURR_FRAME}];
535             my $colors = $self->{COLORS};
536             my $fg = $entity->{COLOR}[$entity->{CURR_FRAME}];
537             my $attrs = $entity->{ATTR}[$entity->{CURR_FRAME}];
538             my ($x, $y) = ($entity->{'X'}, $entity->{'Y'});
539             my ($w, $h) = ($self->{WIDTH}, $self->{HEIGHT});
540             my $wrap = $entity->{WRAP};
541             my $trans = $entity->{TRANSPARENT};
542             my $win = $self->{WIN};
543             my $color_enabled = $self->{COLOR_ENABLED};
544             my $attr;
545              
546             for my $i (0..$#{$shape}) {
547             my $y_pos = $y+$i;
548              
549             for my $j (0..$#{$shape->[$i]}) {
550             unless($shape->[$i][$j] eq $trans) { # transparent char
551             my $x_pos = $x+$j;
552              
553             if($wrap) {
554             while($x_pos >= $w) { $x_pos -= $w; }
555             while($y_pos >= $h) { $y_pos -= $h; }
556             } elsif($x_pos >= $w or $y_pos >= $h) {
557             next;
558             }
559              
560             unless($x_pos < 0 or $y_pos < 0) {
561             if($color_enabled) {
562             if(defined($attrs->[$i][$j])) {
563             $attr = $colors->{$fg->[$i][$j]} | $attrs->[$i][$j];
564             } else {
565             $attr = $colors->{$fg->[$i][$j]};
566             }
567              
568             $win->attron( $attr );
569             $win->addstr( int($y_pos), int($x_pos), $shape->[$i][$j]);
570             $win->attroff( $attr );
571             } else {
572             $win->addstr( int($y_pos), int($x_pos), $shape->[$i][$j]);
573             }
574             }
575             }
576             }
577             }
578             }
579              
580             =item I
581              
582             $anim->add_entity( $entity1, $entity2, $entity3 );
583              
584             Add one or more animation entities to the animation.
585              
586             =cut
587             sub add_entity {
588             my ($self, @entities) = @_;
589             foreach my $entity (@entities) {
590             $self->{ENTITYCOUNT}++;
591             if($entity->{PHYSICAL}) {
592             $self->{PHYSICALCOUNT}++;
593             $self->{PHYSICALENTITIES}{$entity->{NAME}} = $entity;
594             }
595             $self->{ENTITIES}{$entity->{NAME}} = $entity;
596             $entity->{ANIMATION} = $self;
597             }
598             }
599              
600             =item I
601              
602             $anim->del_entity( $entity_name );
603             $anim->del_entity( $entity_ref );
604              
605             Removes an entity from the animation. Accepts either an entity
606             name or a reference to the entity itself.
607              
608             =cut
609             sub del_entity {
610             my ($self, $entity) = @_;
611             if(ref($entity)) {
612             $entity = $entity->name();
613             }
614             if(defined($self->{ENTITIES}{$entity})) {
615             push(@{$self->{DELETEQUEUE}}, $entity);
616             } else {
617             carp("Attempted to destroy nonexistant entity '$entity'");
618             }
619             }
620              
621             # go through the list of entities that have been queued for
622             # deletion using del_entity and remove them
623             sub _remove_deleted_entities {
624             my ($self) = @_;
625             while(my $entity_name = shift @{$self->{DELETEQUEUE}}) {
626             my $entity = $self->{ENTITIES}{$entity_name};
627             if(defined($entity->{DEATH_CB})) {
628             $entity->{DEATH_CB}->($entity, $self);
629             }
630             if($entity->{PHYSICAL}) {
631             $self->{PHYSICALCOUNT}--;
632             delete $self->{PHYSICALENTITIES}{$entity_name};
633             }
634             delete $self->{ENTITIES}{$entity_name};
635             $self->{ENTITYCOUNT}--;
636             }
637             }
638              
639             =item I
640              
641             $anim->remove_all_entities();
642              
643             Removes every animation object. This is useful if you need to start the
644             animation over (eg. after a screen resize)
645              
646             =cut
647             sub remove_all_entities {
648             my ($self) = @_;
649             $self->{ENTITYCOUNT} = 0;
650             $self->{PHYSICALCOUNT} = 0;
651             $self->{PHYSICALENTITIES} = {};
652             $self->{ENTITIES} = {};
653             }
654              
655             =item I
656              
657             $number_of_entities = $anim->entity_count();
658              
659             Returns the number of entities in the animation.
660              
661             =cut
662             sub entity_count {
663             my ($self) = @_;
664             my $count = 0;
665             foreach (keys %{$self->{ENTITIES}}) {
666             $count++;
667             }
668             return $count;
669             }
670              
671             =item I
672              
673             $entity_list = $anim->get_entities();
674              
675             Returns a reference to a list of all entities in the animation.
676              
677             =cut
678             sub get_entities {
679             my ($self) = @_;
680             my @entities = keys %{$self->{ENTITIES}};
681             return \@entities;
682             }
683              
684             =item I
685              
686             $entity_list = $anim->get_entities_of_type( $type );
687              
688             Returns a reference to a list of all entities in the animation
689             that have the given type.
690              
691             =cut
692             sub get_entities_of_type {
693             my ($self, $type) = @_;
694             my @entities;
695             foreach my $entity (values %{$self->{ENTITIES}}) {
696             if($entity->{TYPE} eq $type) {
697             push(@entities, $entity->{NAME});
698             }
699             }
700             return \@entities;
701             }
702              
703             =item I
704              
705             my $is_living = $anim->is_living( $entity );
706              
707             Return 1 if the entity name or reference is in the animation
708             and is not scheduled for deletion. Returns 0 otherwise.
709              
710             =cut
711             sub is_living {
712             my ($self, $entity) = @_;
713              
714             if(ref($entity) eq 'Term::Animation::Entity') {
715             $entity = $entity->name();
716             }
717              
718             unless(exists($self->{'ENTITIES'}{$entity})) {
719             return 0;
720             }
721              
722             foreach my $dying_ent (@{$self->{DELETEQUEUE}}) {
723             if($dying_ent eq $entity) {
724             return 0;
725             }
726             }
727              
728             return 1;
729             }
730              
731             =item I
732              
733             $entity_ref = $anim->entity( $entity_name );
734              
735             If the animation contains an entity with the given name,
736             the Term::Animation::Entity object associated with the name
737             is returned. Otherwise, undef is returned.
738              
739             =cut
740             sub entity {
741             my ($self, $entity_name) = @_;
742             if(defined($self->{ENTITIES}{$entity_name})) {
743             return $self->{ENTITIES}{$entity_name};
744             } else {
745             return undef;
746             }
747             }
748              
749             =item I
750              
751             $width = $anim->width();
752              
753             Returns the width of the screen
754              
755             =cut
756             sub width {
757             my ($self) = @_;
758             return $self->{WIDTH};
759             }
760              
761             =item I
762              
763             $height = $anim->height();
764              
765             Returns the height of the screen
766              
767             =cut
768             sub height {
769             my ($self) = @_;
770             return $self->{HEIGHT};
771             }
772              
773             =item I
774              
775             $size = $anim->size();
776              
777             Returns the number of characters in the curses window (width * height)
778              
779             =cut
780             sub size {
781             my ($self) = @_;
782             return ( $self->{HEIGHT} * $self->{WIDTH} )
783             }
784              
785             =item I
786              
787             $anim->redraw_screen();
788              
789             Clear everything from the screen, and redraw what should be there. This
790             should be called after I, or if the user indicates that
791             the screen should be redrawn to get rid of artifacts.
792              
793             =cut
794             sub redraw_screen {
795             my ($self) = @_;
796             $self->{WIN}->clear();
797             $self->{WIN}->refresh();
798             $self->_build_screen();
799             $self->{WIN}->move($self->{HEIGHT}-1, $self->{WIDTH}-1);
800             $self->{WIN}->refresh();
801             }
802              
803             # draw the elements of the screen that have changed since the last update
804             sub _display_screen {
805             my ($self) = @_;
806             $self->{WIN}->move($self->{HEIGHT}-1, $self->{WIDTH}-1);
807             $self->{WIN}->refresh();
808             }
809              
810              
811             =item I
812              
813             # gen_path (x,y,z, x,y,z, [ frame_pattern ], [ steps ])
814              
815             $anim->gen_path( $x1, $y1, $z1, $x2, $y2, $z2, [ 1, 2, 0, 2 ], 'longest' );
816              
817             Given beginning and end points, this will return a path for the
818             entity to follow that can be given to the default callback routine,
819             I. The first set of x,y,z coordinates are the point
820             the entity will begin at, the second set is the point the entity
821             will end at.
822              
823             You can optionally supply a list of frames to cycle through. The list
824             will be repeated as many times as needed to finish the path. If no
825             list of frames is supplied, only the first frame will be used.
826              
827             You can also request the number of steps you would like for the entity
828             to take to finish the path. The default is 'shortest'.
829             Valid arguments are:
830             longest The longer of the X and Y distances
831             shortest The shorter of the X and Y distances
832             X,Y or Z The x, y or z distance
833             Explicitly specify the number of steps to take
834              
835             =cut
836             sub gen_path {
837             my ($self, $x_start, $y_start, $z_start, $x_end, $y_end, $z_end, $frame_pattern, $steps_req) = @_;
838             my @path = ();
839             my $steps;
840              
841             my $x_dis = $x_end - $x_start;
842             my $y_dis = $y_end - $y_start;
843             my $z_dis = $z_end - $z_start;
844              
845             unless(defined($frame_pattern)) {
846             $frame_pattern = [ 0 ];
847             }
848              
849             # default path length if none specified
850             unless(defined($steps_req)) {
851             $steps_req = 'shortest';
852             }
853              
854             if($steps_req eq 'shortest' or $steps_req eq 'longest') {
855             if($x_dis == $y_dis) { $steps = $y_dis; }
856             elsif($x_dis == 0) { $steps = $y_dis; }
857             elsif($y_dis == 0) { $steps = $x_dis; }
858             elsif(abs($x_dis) < abs($y_dis)) {
859             if($steps_req eq 'shortest') { $steps = $x_dis; }
860             else { $steps = $y_dis; }
861             } else {
862             if($steps_req eq 'shortest') { $steps = $y_dis; }
863             else { $steps = $x_dis; }
864             }
865             }
866             elsif($steps_req =~ /^\d+$/) { $steps = $steps_req; }
867             elsif(uc($steps_req) eq 'X') { $steps = $x_dis; }
868             elsif(uc($steps_req) eq 'Y') { $steps = $y_dis; }
869             elsif(uc($steps_req) eq 'Z') { $steps = $z_dis; }
870             else {
871             carp("Unknown path length method: $steps_req"); return();
872             }
873              
874             $steps = abs($steps);
875              
876             if($steps == 0) { carp("Cannot create a zero length path!"); return (); }
877             elsif($steps == 1) {
878             # a path length of one is a special case where we just move from the origin to the destination
879             $path[0] = [($x_end - $x_start), ($y_end - $y_start), ($z_end - $z_start), $frame_pattern->[0]];
880             return \@path;
881             }
882              
883             my $x_incr = $x_dis / $steps;
884             my $y_incr = $y_dis / $steps;
885             my $z_incr = $z_dis / $steps;
886              
887             my ($x_pos, $y_pos, $z_pos) = ($x_start, $y_start, $z_start);
888             my ($x_act, $y_act, $z_act) = ($x_start, $y_start, $z_start);
889              
890             for(0..$steps-2) {
891             my ($x_prev, $y_prev, $z_prev) = ($x_pos, $y_pos, $z_pos);
892              
893             $x_pos+=$x_incr; $y_pos+=$y_incr; $z_pos+=$z_incr;
894             my $f_pos = $frame_pattern->[${_}%($#{$frame_pattern}+1)];
895              
896             my ($x_mov, $y_mov, $z_mov) = (int($x_pos) - int($x_prev), int($y_pos) - int($y_prev), int($z_pos) - int($z_prev));
897             $x_act += $x_mov; $y_act += $y_mov; $z_act += $z_mov;
898              
899             $path[$_] = [$x_mov, $y_mov, $z_mov, $f_pos];
900             }
901              
902             # through rounding errors, we might end up with a final position that is off by one from
903             # what we actually wanted. ending up in the right place is the most important thing,
904             # so we just set the final position to put us where we want to be
905             $path[$steps-1] = [$x_end - $x_act, $y_end - $y_act, $z_end - $z_act, $frame_pattern->[($steps - 1)%($#{$frame_pattern}+1)]];
906              
907             return \@path;
908             }
909              
910              
911             # run the callback routines for all entities that have them, and update
912             # the entity accordingly. also checks for auto death status
913             sub _do_callbacks {
914             my ($self) = @_;
915              
916             foreach my $entity (keys %{$self->{ENTITIES}}) {
917             my $ent = $self->{ENTITIES}{$entity};
918            
919             # check for methods to automatically die
920             if(defined($ent->{'DIE_TIME'}) and $ent->{'DIE_TIME'} <= time()) {
921             del_entity($self, $entity); next;
922             }
923              
924             if(defined($ent->{'DIE_FRAME'}) and ($ent->{'DIE_FRAME'}--) <= 0) {
925             del_entity($self, $entity); next;
926             }
927              
928             if(defined($ent->{'DIE_ENTITY'}) and !$self->is_living($ent->{'DIE_ENTITY'}) ) {
929             del_entity($self, $entity); next;
930             }
931              
932             if($ent->{'DIE_OFFSCREEN'}) {
933             if($ent->{X} >= $self->{WIDTH} or $ent->{Y} >= $self->{HEIGHT} or
934             $ent->{X} < (0 - $ent->{WIDTH}) or $ent->{Y} < (0 - $ent->{HEIGHT})) {
935             del_entity($self, $entity); next;
936             }
937             }
938              
939             if(defined($ent->{CALLBACK})) {
940             my ($x, $y, $z, $f) = $ent->{CALLBACK}->($ent, $self);
941             if(defined($x)) {
942             if($ent->{WRAP}) {
943             if($x >= $self->{WIDTH}) { $x = ($x - int($x)) + ($x % $self->{WIDTH}); }
944             elsif($x < 0) { $x = ($x - int($x)) + ($x % $self->{WIDTH}); }
945             }
946             $ent->{X} = $x;
947             }
948             if(defined($y)) {
949             if($ent->{WRAP}) {
950             if($y >= $self->{HEIGHT}) { $y = ($y - int($y)) + ($y % $self->{HEIGHT}); }
951             elsif($y < 0) { $y = ($y - int($y)) + ($y % $self->{HEIGHT}); }
952             }
953             $ent->{Y} = $y;
954             }
955             $ent->{Z} = defined($z) ? $z : $ent->{Z};
956             $ent->{CURR_FRAME} = defined($f) ? $f : $ent->{CURR_FRAME};
957            
958             }
959             }
960             }
961              
962             # called after all other updates. moves any entities that
963             # follow another entity
964             sub _move_followers {
965             my ($self) = @_;
966              
967             foreach my $entity_name (keys %{$self->{ENTITIES}}) {
968             my $follower = $self->{ENTITIES}{$entity_name};
969              
970             next unless(defined($follower->{FOLLOW_ENTITY}));
971              
972             my $leader = $self->entity($follower->{FOLLOW_ENTITY});
973             next unless(defined($leader));
974             my $offset = $follower->{FOLLOW_OFFSET};
975              
976             if(defined($offset->[0])) { $follower->x( $offset->[0] + $leader->x ); }
977             if(defined($offset->[1])) { $follower->y( $offset->[1] + $leader->y ); }
978             if(defined($offset->[2])) { $follower->z( $offset->[2] + $leader->z ); }
979             if(defined($offset->[3])) { $follower->frame( $offset->[3] + $leader->frame ); }
980             }
981             }
982              
983             sub _collision_handlers {
984             my ($self) = @_;
985             foreach my $entity (values %{$self->{ENTITIES}}) {
986             if(defined($entity->{COLL_HANDLER}) && defined($entity->{COLLISIONS})) {
987             $entity->{COLL_HANDLER}->($entity, $self);
988             }
989             $entity->{COLLISIONS} = ();
990             }
991             }
992              
993             =item I
994              
995             $anim->end();
996              
997             Run the Curses endwin function to get your terminal back to its
998             normal mode. This is called automatically when the object is
999             destroyed if the animation is running full screen (if you
1000             did not pass an existing Curses window to the constructor).
1001              
1002             =cut
1003             sub end {
1004             my ($self) = @_;
1005             endwin;
1006             }
1007              
1008             # write to a log file, for debugging
1009             sub _elog {
1010             my ($mesg) = @_;
1011             open(F, ">>", "elog.log");
1012             print F "$mesg\n";
1013             close(F);
1014             }
1015              
1016             1;
1017              
1018             =back
1019              
1020             =head1 CALLBACK ROUTINES
1021              
1022             Callback routines for all entities are called each time I
1023             is called. A default callback routine is supplied, I, which
1024             is sufficient for most basic movement. If you want to create an entity
1025             that exhibits more complex behavior, you will have to write a custom
1026             callback routine for it.
1027              
1028             Callback routines take two arguments, a reference to the Term::Animation::Entity
1029             object that it should act on, and a reference to the Term::Animation instance
1030             that called it. Any arguments required to tell the callback what to do with
1031             the object, or any state that needs to be maintained, should be put
1032             in the I element of the object. I is only
1033             referenced by the callback routine, and thus can contain any datastructure
1034             that you find useful.
1035              
1036             Here is an example custom callback that will make an entity move randomly
1037             around the screen:
1038              
1039             sub random_movement {
1040             my ($entity, $anim) = @_;
1041              
1042             # get the current position of the entity
1043             my ($x, $y, $z) = $entity->position();
1044              
1045             # we'll use callback_args to store the last axis we moved in
1046             my $last_move = $entity->callback_args();
1047              
1048             # if we moved in x last time, move in y this time
1049             if($last_move eq 'x') {
1050             $entity->callback_args('y');
1051             # move by -1, 0 or 1
1052             $y += int(rand(3)) - 1;
1053             } else {
1054             $entity->callback_args('x');
1055             $x += int(rand(3)) - 1;
1056             }
1057              
1058             # return the absolute x,y,z coordinates to move to
1059             return ($x, $y, $z);
1060             }
1061              
1062             The return value of your callback routine should be of the form:
1063              
1064             return ($x, $y, $z, $frame)
1065              
1066             $x, $y and $z represent the X, Y and Z coordinates to which the object
1067             should move. $frame is the frame number that the object should display,
1068             if it has multiple frames of animation. Any values that are unspecified
1069             or undef will remain unchanged.
1070              
1071             You can also call the default callback from within your callback, if
1072             you want it to handle movement for you. For example, if your callback
1073             is simply used to decide when an entity should die:
1074              
1075             sub wait_for_file {
1076             my ($entity, $anim) = @_;
1077              
1078             # kill this entity if a certain file shows up
1079             if(-e "/path/to/file") {
1080             $entity->kill();
1081             return();
1082             }
1083              
1084             # use the default callback to handle the actual movement
1085             return $entity->move_entity($anim);
1086             }
1087              
1088             If you use this, be aware that I relies on
1089             I, so you cannot use it to store your own
1090             arbitrary data.
1091              
1092             =head1 COLOR
1093              
1094             ANSI color is available for terminals that support it. Only a single
1095             background color can be used for the window (it would look terrible
1096             in most cases otherwise anyway). Colors for entities are specified by
1097             using a 'mask' that indicates the color for each character. For
1098             example, say we had a single frame of a bird:
1099              
1100             $bird = q#
1101              
1102             ---. .-. .---
1103             --\'v'/--
1104             \ /
1105             " "
1106             #;
1107              
1108             To indicate the colors you want to use for the bird, create a matching
1109             mask, with the first letter of each color in the appropriate position
1110             (except black, which is 'k'). Pass this mask as the I parameter.
1111              
1112             $mask = q#
1113              
1114             BBBB BBB BBBB
1115             BBBWYWBBB
1116             B B
1117             Y Y
1118             #;
1119              
1120             When specifying a color, using uppercase indicates the color should be
1121             bold. So 'BLUE' or 'B' means bold blue, and 'blue' or 'b' means non-bold
1122             blue. 'Blue' means you get an error message.
1123              
1124             You can also provide a default color with the default_color parameter.
1125             This color will be used for any character that does
1126             not have an entry in the mask. If you want the entire entity to be
1127             a single color, you can just provide a default color with no mask.
1128              
1129             The available colors are: red, green, blue, cyan, magenta, yellow, black
1130             and white.
1131              
1132             Here's an example call to build_object for the bird above.
1133              
1134             $anim->new_entity (
1135             name => "Bird",
1136             shape => $bird,
1137             position => [ 5, 8, 7 ],
1138             callback_args => [ 1, 2, 0, 0 ],
1139             color => $mask,
1140             default_color => "BLUE"
1141             );
1142              
1143             =head1 AUTHOR
1144              
1145             Kirk Baucom, Ekbaucom@schizoid.comE
1146              
1147             =head1 SEE ALSO
1148              
1149             L
1150              
1151             =cut