require 'Collision.pl'; require 'DataHelper.pl'; require 'Damage.pl'; package Fighter; use strict; =comment Fighter's members are: ID int The ID of the fighter STATS hash Reference to the fighter's stats (includes GENDER) NAME string The name of the character, e.g. "Ulmar". FRAMES array The character's frame description. STATES hash The character's state description. OK bool Is the fighter good to go? TEAMSIZE int The number of fighters left in this figther's team, including this one. 0 when the last one dies. NUMBER int Player number (either 0 or 1) X int The fighter's current anchor, horizontal coordinate. Y int The fighter's current anchor, vertical coordinate. ST string The name of the fighter's current state. FR int The number of the fighter's current frame (same as STATES->{ST}->{F}). DEL int The amount of time before the character moves to the next state. NEXTST string The name of the next state after this one (calculated by Advance and Event, user by Update). DIR int -1: the character is facing left; 1: if the character is facing right. PUSHY int The character's vertical momentum, from jumping/hits. PUSHX int The character's horizontal momentum, from hits. HP int Hit points, from 100 down to 0. IDLE int The amount of game time since the player is ready. CHECKEHIT int 1 if the hit needs to be checked soon. DELIVERED int 1 if a hit was delivered in this state. COMBO int The number of consecutive hits delivered to this fighter. COMBOHP int The amount of HP delivered in the last combo. OTHER Fighter A reference to the other Fighter LANDINGPENALTY int This is added to DEL when the character lands (used to penaltize blocked jumpkicks). Becomes DELPENALTY upon landing. DELPENALTY int This is added to DEL in the next state. BOUNDSCHECK bool Should horizontal bounds checking be done for this fighter (for team mode when new fighter enters)' new Reset Advance CheckHit IsHitAt Event Update Needs to be subclassed: Reset, NextState =cut sub new { my ($class) = @_; my $self = { 'NUMBER'=> 0, 'ST' => 'Start', 'FR' => 0, 'DEL' => 0, 'DIR' => 1, 'PUSHY' => 0, 'PUSHX' => 0, }; bless $self, $class; return $self; } sub RewindData { my ($self) = @_; return $self; } sub Reset { my ($self, $fighterenum) = @_; die "Insufficient parameters." unless defined $self; $fighterenum = $self->{ID} unless defined $fighterenum; my ($number, $stats); $number = $self->{NUMBER}; $stats = ::GetFighterStats($fighterenum); die "Couldn't load stats of fighter $fighterenum\n" unless defined $stats; unless (defined $stats->{STATES}) { print "ERROR: The fighter $fighterenum is not yet usable.\n"; $self->{OK} = 0; return; } print STDERR "Resetting fighter $number to character $fighterenum\n"; $self->{ID} = $fighterenum; $self->{STATS} = $stats; $self->{NAME} = $stats->{NAME}; $self->{FRAMES} = $stats->{FRAMES}; $self->{STATES} = $stats->{STATES}; $self->{X} = (( $number ? 540 : 100 ) << $::GAMEBITS) + $::BgPosition; $self->{X} = (( $number ? 620 : 180 ) << $::GAMEBITS) + $::BgPosition if $::WIDE; $self->{Y} = $::GROUND2; $self->{ST} = 'Start'; $self->{FR} = $self->GetCurrentState()->{F}; $self->{DEL} = 0; $self->{DIR} = ($number ? -1 : 1); $self->{PUSHY} = 0; $self->{PUSHX} = 0; $self->{HP} = 100; $self->{IDLE} = 0; $self->{CHECKHIT} = 0; $self->{DELIVERED} = 0; $self->{COMBO} = 0; $self->{COMBOHP}= 0; $self->{LANDINGPENALTY} = 0; $self->{DELPENALTY} = 0; $self->{BOUNDSCHECK} = 1; $self->{OK} = 1; &{$self->{STATS}->{STARTCODE}}($self); } =comment Returns a "random" number that is deterministic in the sense that it is always the same, given the current state of the character and the current game time. =cut sub QuasiRandom($$) { my ( $self, $randmax ) = @_; $randmax = 1 unless $randmax; return int( $self->{HP} * 543857 + $self->{NUMBER} * 834973 + $self->{DEL} * 4358397 + $self->{IDLE} * 92385029 + $::gametick * 23095839 + 5304981 ) % $randmax; } =comment Advance should be called once every game tick. It advances the fighter, preparing his variables for drawing him in the next tick. Advance also processes the input if it is appropriate to do so in the current state. Advance prepares the NEXTST attribute of the player. It does not modify the current ST yet, because that may depend on hits, event, etc. Advance should be followed by CheckHit and Event calls, and finally Update. =cut sub Advance { my ($self, $in) = @_; # print STDERR "$::gametick\t$self->{X},$self->{HP}\t$self->{NUMBER}\t$self->{DEL}\t$self->{IDLE}\n"; my ($stname, # The name of the current state. $st, # The descriptor of the current state. $move, # The move associated with the current state. $i, $j, # temp var. @body ); # collision detection $self->{NEXTST} = ''; $stname = $self->{ST}; return if ( $stname eq 'Dead' ); $st = $self->{'STATES'}->{$stname}; # 1. DECREMENT 'DEL' PROPERTY $self->{'DEL'}--; if ( $st->{SITU} eq 'Ready' ) { $self->{IDLE} = $self->{IDLE} + 1; } else { $self->{IDLE} = 0; } # 2. UPDATE THE HORIZONTAL POSITION # 2.1. 'MOVE' PLAYER IF NECESSARY if ( defined $st->{'MOVE'} ) { $move = $st->{'MOVE'}*$self->{'DIR'} * 6; $self->{'X'} += $move; } # 2.2. 'PUSH' PLAYER IF NECESSARY if ( $self->{PUSHX} ) { $i = ($self->{PUSHX}+7) / 8; # 1/1; syntax highlight.. $self->{X} += $i; $self->{PUSHX} -= $i if $self->{PUSHY} == 0; } # 2.3. MAKE SURE THE TWO FIGHTERS DON'T "WALK" INTO EACH OTHER if ( $::BgScrollEnabled and $self->{Y} >= $::GROUND2 ) { my ( $other, $centerX, $centerOtherX, $pushDir ); $centerX = $self->GetCenterX; for ($i=0; $i<$::NUMPLAYERS; ++$i) { $other = $::Fighters[$i]; next if ( $other->{Y} < $::GROUND2 ) or ( $other->{ST} eq 'Dead' ); $centerOtherX = $other->GetCenterX; if ( abs($centerX - $centerOtherX) < 60 ) { $pushDir = ($centerX > $centerOtherX) ? 1 : -1; $self->{X} += 10 * $pushDir; $other->{X} -= 10 * $pushDir; } } } # 2.4. HORIZONTAL BOUNDS CHECKING if ( $self->{BOUNDSCHECK} ) { $self->{X} = $::BgPosition + $::MOVEMARGIN2 if $self->{X} < $::BgPosition + $::MOVEMARGIN2; $self->{X} = $::BgPosition + $::SCRWIDTH2 - $::MOVEMARGIN2 if $self->{X} > $::BgPosition + $::SCRWIDTH2 - $::MOVEMARGIN2; } # 3. FLYING OR FALLING if ( $self->{Y} < $::GROUND2 ) { $self->{PUSHY} += 3; $self->{Y} += $self->{PUSHY}; if ($self->{Y} >= $::GROUND2) { # LANDING $self->{DELPENALTY} = $self->{LANDINGPENALTY}; $self->{LANDINGPENALTY} = 0; $self->{BOUNDSCHECK} = 1; if ( $st->{SITU} eq 'Falling') { ::AddEarthquake( $self->{PUSHY} / 20 ); # 1/1 push @::Sounds, ('PLAYER_FALLS') if $self->{PUSHY} > 40; # print "PUSHY = ", $self->{PUSHY}, "; "; if ( $self->{PUSHY} > 30 ) { # Bouncing after a fall $self->{PUSHY} = - $self->{PUSHY} / 2; # 1/1 $self->{Y} = $::GROUND2 - 1; } else { # Getting up after a fall. $self->{PUSHY} = 0; $self->{Y} = $::GROUND2; $self->{DEL} = 0; if ( $self->{HP} <= 0 ) { $self->{NEXTST} = 'Dead'; } else { $self->{NEXTST} = 'Getup'; } } return; } else { push @::Sounds, ('PLAYER_LANDS'); $self->{PUSHY} = 0; $self->{Y} = $::GROUND2; if ( substr($self->{ST},0,4) eq 'Jump' ) { # Landing after a jump $self->{DEL} = 0; $self->{NEXTST} = 'Stand'; } else { print "Landed; state: ", $self->{ST}, "\n"; } return; } } } return if $self->{'DEL'} > 0; # 4. DELAYING OVER. MAKE END-OF-STATE CHANGES. # 4.1. 'DELTAX' displacement if so specified if ($st->{DELTAX}) { $self->{X} += $st->{DELTAX} * $self->{DIR} * $::GAMEBITS2; } # 4.2. 'TURN' if so specified if ($st->{TURN}) { $self->{DIR} = - $self->{DIR}; # Swap input keys.. $i = $in->{Keys}->[2]; $in->{Keys}->[2] = $in->{Keys}->[3]; $in->{Keys}->[3] = $i; } # 5. CALCULATE THE NEXT STATE my ($nextst, $con, $input, $mod, $action, $dist); $nextst = $st->{NEXTST}; $con = $st->{CON}; undef $con if $::ko; if ( defined $con ) { $dist = ($self->{OTHER}->{X} - $self->{X}) * $self->{DIR}; $self->ComboEnds(); # 5.1. The current state has connections! ($input,$mod) = $in->GetAction(); $mod = '' unless defined $mod; $action = $input; # Try to find the best matching connection. for ( $i = length($mod); $i>=0; --$i ) { $action = $input . substr( $mod, 0, $i ); last if defined $con->{$action}; } if ( defined $con->{$action} ) { # print "dist = $dist. "; if ( ($action eq 'hpunch' or $action eq 'lpunch') and ($dist>=0) and ($dist<800) ) { $action = 'hpunchF' if defined $con->{'hpunchF'}; } elsif ( ($action eq 'hkick' or $action eq 'lkick') and ($dist>=0) and ($dist<900) ) { $action = 'lkickF' if defined $con->{'lkickF'}; } $nextst = $con->{$action}; $in->ActionAccepted; if ( $nextst eq 'Back' and $::NUMPLAYERS != 2 ) { $nextst = 'Turn'; } } } $self->{'NEXTST'} = $nextst; } =comment Checks for hits on all other fighters. Returns a list of ($self,$fighter,$hit) triplets. =cut sub CheckHit($) { my ($self) = @_; return () unless $self->{CHECKHIT}; $self->{CHECKHIT} = 0; my ($st, # A reference to the next state. $nextst, # The name of the next state. @hit, # The hit array @hit2, $i, $j, @retval, $other, ); $st = $self->{STATES}->{$self->{ST}}; return @retval unless $st->{HIT}; return @retval unless defined $self->{FRAMES}->[$st->{F}]->{hit}; @hit = @{$self->{FRAMES}->[$st->{F}]->{hit}}; if ( $self->{DIR}<0 ) { ::MirrorPolygon( \@hit ); } ::OffsetPolygon( \@hit, $self->{X} / $::GAMEBITS2, $self->{Y} / $::GAMEBITS2 ); for ( $j=0; $j<$::NUMPLAYERS; ++$j ) { $other = $::Fighters[$j]; next if $other == $self; @hit2 = @hit; $i = $other->IsHitAt( \@hit2 ); if ( $i ) { $self->{DELIVERED} = 1; push @retval, ([$self, $other, $i]); } } if ( scalar(@retval) == 0 ) { # Make the 'Woosh' sound - maybe. $nextst = $st->{NEXTST}; print "NEXTST = $nextst"; $nextst = $self->{STATES}->{$nextst}; print "NEXTST->HIT = ", $nextst->{HIT}, "\n"; push @::Sounds, ('ATTACK_MISSES') unless $nextst->{HIT} and defined $self->{FRAMES}->[$nextst->{F}]->{hit}; } return @retval; } sub IsHitAt { my ($self, $poly) = @_; my ($frame, @a); $frame = $self->{FRAMES}->[$self->{FR}]; ::OffsetPolygon( $poly, -$self->{X} / $::GAMEBITS2, -$self->{Y} / $::GAMEBITS2 ); if ( $self->{DIR}<0 ) { ::MirrorPolygon( $poly ); } #print "IsHitAt (", join(',',@{$poly}),")\n (", # join(',',@{$frame->{head}}),")\n (", # join(',',@{$frame->{body}}),")\n (", # join(',',@{$frame->{legs}}),")\n"; return 1 if ::Collisions( $poly, $frame->{head} ); return 2 if ::Collisions( $poly, $frame->{body} ); return 3 if ::Collisions( $poly, $frame->{legs} ); return 0; } =comment Event($self, $event, $eventpar): Handles the following events: 'Won', 'Hurt', 'Thread', 'Fun', 'Turn' =cut sub Event($$$) { my ($self, $event, $eventpar) = @_; my ($st); $st = $self->GetCurrentState(); if ( ($st->{SITU} eq 'Ready' or $st->{SITU} eq 'Stand') and $event eq 'Won' ) { $self->{NEXTST} = 'Won' unless substr($self->{ST}, 0, 3) eq 'Won' ; return; } if ( $st->{SITU} eq 'Ready' and $self->{NEXTST} eq '' ) { $self->{IDLE} = 0; if ( $event =~ /Hurt|Threat|Fun|Turn/ ) { if ( $event eq 'Fun' and defined $self->{STATES}->{'Funb'} and $self->QuasiRandom(2)==1 ) { $event = 'Funb'; } $self->{NEXTST} = $event; } } } =comment Event($self, $event, $eventpar): Handles the following events: 'Highhit', 'Uppercut', 'Hit', 'Groinhit', 'Leghit', 'Fall' Parameters: $other Fighter The fighter who hit me. $event string The name of the event (any of the above events) $eventpar int/string 1: head hit; 2: body hit; 3: leg hit; Maxcombo: I did a max combo (=fall with 0 damage) =cut sub HitEvent($$$$) { my ($self, $other, $event, $eventpar) = @_; my ($st, $blocked, $damage); $st = $self->GetCurrentState(); # Do events: Highhit, Uppercut, Hit, Groinhit, Leghit, Fall $eventpar = '' unless defined $eventpar; # Supress useless warning if ( $eventpar eq 'Maxcombo' ) { $damage = 0; } else { $damage = ::GetDamage( $other->{NAME}, $other->{ST} ); } $blocked = $st->{BLOCK}; $blocked = 0 if ( $self->IsBackTurnedTo($other) ); print "Event '$event', '$eventpar'\n"; print "Blocked.\n" if ( $blocked ); # Hit point adjustment here. $self->{HP} -= $blocked ? $damage >> 3 : $damage; # Turn if we must. $self->{DIR} = ( $self->{X} > $other->{X} ) ? -1 : 1; # Handle the unfortunate event of the player "dying". if ( $self->{HP} <= 0 ) { push @::Sounds, ('PLAYER_KO'); $self->{NEXTST} = 'Falling'; $self->{PUSHX} = -20 * 3 * $self->{DIR}; $self->{PUSHY} = -80; $self->{Y} -= 10; $self->{COMBO} += 1; $self->{COMBOHP} += $damage; $self->ComboEnds(); $::ko = 1 if $self->{TEAMSIZE} <= 1 and $::ActiveTeams <= 2; return; } # Handle blocked attacks. if ( $blocked ) { push @::Sounds, ('ATTACK_BLOCKED'); $self->HitPush( $other, - $damage * 20 * $self->{DIR} ); $other->{DEL} += 20 * $::DELMULTIPLIER; if ( $other->{Y} < $::GROUND2 ) { $other->{PUSHY} = -20 if $other->{PUSHY} > 0; $other->{PUSHX} *= 0.2; $other->{LANDINGPENALTY} = 30 * $::DELMULTIPLIER; } # $self->{PUSHX} = - $damage * 5 * $self->{DIR}; return; } # Handle the rest of the events. if ( $event eq 'Uppercut' ) { push @::Sounds, ('UPPERCUT_HITS'); ::AddEarthquake( 20 ); } elsif ($event eq 'Groinhit') { push @::Sounds, ('GROINKICK_HITS'); } else { push @::Sounds, ('ATTACK_HITS'); } $self->{COMBO} += 1; $self->{COMBOHP} += $damage; $damage *= $self->{COMBO}; # Only for the purpose of pushing if ( $self->{COMBO} >= $::MAXCOMBO ) { $self->ComboEnds(); $event = 'Uppercut'; $other->HitEvent( 'Fall', 'Maxcombo' ); } if ( $st->{SITU} eq 'Crouch' ) { if ( $event eq 'Uppercut' or $event eq 'Fall' ) { $self->{NEXTST} = 'Falling'; $self->{PUSHX} = -48 * 3 * $self->{DIR}; $self->{PUSHY} = -100; $self->{Y} -= 10; } else { if ($eventpar == 1) { $self->{NEXTST} = 'KneelingPunched'; } else { $self->{NEXTST} = 'KneelingKicked'; } $self->HitPush( $other, - $damage * 20 * $self->{DIR} ); # $self->{PUSHX} = - $damage * 20 * $self->{DIR}; } return; } if ( $st->{SITU} eq 'Falling' ) { $self->{PUSHY} -= 50; $self->{PUSHY} = -80 if $self->{PUSHY} > -80; $self->{Y} -= 10; return; } if ( $self->{Y} < $::GROUND2 ) { $self->{NEXTST} = 'Falling'; $self->{PUSHY} -= 50; $self->{PUSHX} = -48 * 3 * $self->{DIR}; return; } if ( $event eq 'Highhit' ) { my ( $doodadx, $doodady ); ($doodadx, $doodady) = ::GetPolygonCenter( $self->{FRAMES}->[$self->{FR}]->{head} ); # my ( $doodadx, $doodady ) = @{$self->{FRAMES}->[$self->{FR}]->{head}}; Doodad::CreateDoodad( $self->{X} + $doodadx * $::GAMEBITS2 * $self->{DIR}, $self->{Y} + $doodady * $::GAMEBITS2, 'Tooth', - $self->{DIR}, $self->{NUMBER} ); $self->{NEXTST} = 'HighPunched'; $self->HitPush( $other, - $damage * 20 * $self->{DIR} ); # $self->{PUSHX} = - $damage * 20 * $self->{DIR}; } elsif ( $event eq 'Hit' ) { if ($eventpar == 1) { $self->{NEXTST} = 'HighPunched'; } elsif ($eventpar == 2) { $self->{NEXTST} = 'LowPunched'; } else { $self->{NEXTST} = 'Swept'; } $self->HitPush( $other, - $damage * 20 * $self->{DIR} ); # $self->{PUSHX} = - $damage * 20 * $self->{DIR}; } elsif ( $event eq 'Groinhit' ) { $self->{NEXTST} = 'GroinKicked'; $self->HitPush( $other, - $damage * 20 * $self->{DIR} ); # $self->{PUSHX} = - $damage * 20 * $self->{DIR}; } elsif ( $event eq 'Leghit' ) { $self->{NEXTST} = 'Swept'; $self->HitPush( $other, - $damage * 20 * $self->{DIR} ); # $self->{PUSHX} = - $damage * 20 * $self->{DIR}; } elsif ( $event eq 'Uppercut' or $event eq 'Fall' ) { $self->{NEXTST} = 'Falling'; $self->{PUSHX} = -48 * 3 * $self->{DIR}; $self->{PUSHY} = -100; $self->{Y} -= 10; } else { die "Unknown event: $event, $eventpar"; } } sub GetCurrentState { my ($self, $statename) = @_; my ($stateref); die unless defined $self; $statename = $self->{ST} unless defined $statename; $stateref = $self->{STATES}->{$statename}; return $stateref; } sub ComboEnds { my ($self) = @_; my ($combo, $ismaxcombo); $combo = $self->{COMBO}; $ismaxcombo = $combo >= $::MAXCOMBO; return unless $combo; if ( $self->{COMBO} > 1 ) { my ( $head, $doodad, $x, $y, $combotext ); $combotext = $ismaxcombo ? ::Translate("MAX COMBO!!!") : sprintf( ::Translate('%d-hit combo!'), $self->{COMBO} ); $head = $self->{FRAMES}->[$self->{FR}]->{head}; $x = $self->{X} + $head->[0] * $::GAMEBITS2 * $self->{DIR}; $y = $self->{Y} + $head->[1] * $::GAMEBITS2; $doodad = Doodad::CreateTextDoodad( $x, $y - 30 * $::GAMEBITS2, $self->{NUMBER}, $combotext ); $doodad->{LIFETIME} = $ismaxcombo ? 120 : 80; $doodad->{SPEED} = [-3,-3]; $doodad = Doodad::CreateTextDoodad( $x, $y - 10 * $::GAMEBITS2, $self->{NUMBER}, sprintf( ::Translate('%d%% damage'), int($self->{COMBOHP}*$::HitPointScale/10) ) ); $doodad->{LIFETIME} = 80; $doodad->{SPEED} = [+3,-3]; print $self->{COMBO}, "-hit combo for ", $self->{COMBOHP}, " damage.\n"; push @::Sounds, ( $ismaxcombo ? 'MAX_COMBO' : 'COMBO'); } $self->{COMBO} = 0; $self->{COMBOHP} = 0; } =comment Update moves the fighter to his next state, if there is a NEXTST set. NEXTST should be calculated before calling this method. No further branching or CON checking is done. If the next state has JUMP, the character is set aflying. =cut sub Update { my ($self) = @_; my ($nextst, # The name of the next state $st); # The descriptor of the next state. # Is there a next state defined? $nextst = $self->{'NEXTST'}; # If there isn't, no updating is necessary. return unless $nextst; # ADMINISTER END OF THE STATE MACHINE (WON2 OR DEAD2) if ( $nextst eq 'Won2' or $nextst eq 'Dead' ) { $self->{DELPENALTY} = 1000000; # Something awfully large $self->{HP} = -10000 if $nextst eq 'Dead'; --$::ActiveFighters; --$::ActiveTeams if $self->{TEAMSIZE}<=1 or $nextst eq 'Won2'; } # ADMINISTER THE MOVING TO THE NEXT STATE $st = $self->GetCurrentState( $nextst ); $self->{'ST'} = $nextst; $self->{'FR'} = $st->{'F'}; die "ERROR IN STATE $nextst" unless defined $st->{DEL}; $self->{'DEL'} = $st->{'DEL'} * $::DELMULTIPLIER; $self-> {'DEL'} += $self->{'DELPENALTY'}; $self->{'DELPENALTY'} = 0; # HANDLE THE JUMP and PUSH ATTRIBUTE if ( defined ($st->{'JUMP'}) ) { $self->{'PUSHY'} = -($st->{'JUMP'}); $self->{'Y'} += $self->{'PUSHY'}; } if ( defined ($st->{'PUSHX'}) ) { $self->{'PUSHX'} += $st->{'PUSHX'} * $self->{DIR}; } # HANDLE THE HIT ATTRIBUTE if ( defined ($st->{HIT}) ) { $self->{CHECKHIT} = 1 unless $self->{DELIVERED}; } else { $self->{DELIVERED} = 0; } # HANDLE THE SOUND ATTRIBUTE if ( defined $st->{SOUND} ) { push @::Sounds, ($st->{SOUND}); } # HANDLE THE CODE ATTRIBUTE if ( defined ($st->{CODE}) ) { eval ($st->{CODE}); print $@ if $@; } # HANDLE DOODADS if ( defined ($st->{DOODAD}) ) { # Create a doodad (probably a shot) my ($frame, $hit, $doodad, $doodadname); $frame = $self->{FRAMES}->[$self->{FR}]; $hit = $frame->{hit}; if ( defined $hit ) { $doodadname = $st->{DOODAD}; $doodad = Doodad::CreateDoodad( $self->{X} + $hit->[0] * $::GAMEBITS2 * $self->{DIR}, $self->{Y} + $hit->[1] * $::GAMEBITS2, $doodadname, $self->{DIR}, $self->{NUMBER} ); } } } =comment Pushes the fighter back due to being hit. If the fighter is cornered, the other fighter will be pushed back instead. =cut sub HitPush($$$) { my ($self, $other, $pushforce) = @_; if ( $self->IsCornered ) { $other->{PUSHX} -= $pushforce; } else { $self->{PUSHX} += $pushforce; } } =comment Returns the characters 'centerline' in physical coordinates. =cut sub GetCenterX { my ($self) = @_; my ($body, $x); $body = $self->{FRAMES}->[$self->{FR}]->{body}; $x = $body->[0] + $body->[2] + $body->[4] + $body->[6]; return $self->{X} / $::GAMEBITS2 + $x / 4 * $self->{DIR}; } =comment Is my back turned to my opponent? Returns true if it is. =cut sub IsBackTurned { my ($self) = @_; return ( ($self->{X} - $self->{OTHER}->{X}) * ($self->{DIR}) > 0 ); } =comment Is my back turned to my opponent? Returns true if it is. =cut sub IsBackTurnedTo($) { my ($self, $other) = @_; return ( ($self->{X} - $other->{X}) * ($self->{DIR}) > 0 ); } =comment Returns true if the character is at either end of the arena. =cut sub IsCornered { my ($self) = @_; return (($self->{X} <= $::MOVEMARGIN2 + 16) or ($self->{X} >= $::BGWIDTH2 - $::MOVEMARGIN2 - 16)); } return 1;