###############################################################################
# File:        guBattle.py
# Appointment: Battle Viewer
# Project:     GalaxyU
# Author:      AR
# History:
# 26.02.2015 Removed annoying scaling control: it was too much unnecessary rescaling
# 10.10.2013 Added bombing support
# 01.10.2013 Corrected constants in IsEscape
# 11.08.2013 Processing Count in Escape Action
# 10.08.2013 Added support for MoveAction Count
# 05.08.2013 gu: (XV) fixed bug in escape
# 04.08.2013 gu: (XIV) using sequenced actions in GUI battle
# 03.08.2013 gu: (XIII) adding action sequences to battle records
# 31.07.2013 gu: (XI) delegated group number of units from API; initial units number processing
# 27.01.2013 Created
###############################################################################
import sys, pygame, os, math, copy
import random
from ctypes import *

import zx
import zxLog
import guApp
import guObjects
import guShipLayout
import guTools
import guStarDust
import guReport
import pgTools
import pgViewer


#==============================================================================
# Bombing record
#==============================================================================
class CBombRecord:
    #---------------------------------------------------------------------------
    def __init__(self, aStarId = 0,
                       aOwner = "",
                       aAttacker = "",
                       aCol = 0,
                       aPop = 0,
                       aInd = 0):
        self.StarId = int(aStarId)
        self.Owner = aOwner
        self.Attacker = aAttacker
        self.Col = float(aCol)
        self.Pop = float(aPop)
        self.Ind = float(aInd)
        
#------------------------------------------------------------------------------


#==============================================================================
# Battle sequence
#==============================================================================
class CBattleSequence:
    #---------------------------------------------------------------------------
    # Battle unit
    #---------------------------------------------------------------------------
    class CUnit:
        #-----------------------------------------------------------------------
        def __init__(self, aUnitId = "",
                           aShipTypeName = "", 
                           aMass = 0, 
                           aEfficiency = 0, 
                           aX = 0, aY = 0, 
                           aIsMissile = False,
                           aLaunchedFromUnitId = "",
                           aNumber = 1):
            self.UnitId = aUnitId
            self.ShipTypeName = aShipTypeName    
            self.Mass = float(aMass)
            self.Efficiency = float(aEfficiency)
            self.X = float(aX)
            self.Y = float(aY)
            self.IsMissile = aIsMissile
            self.LaunchedFromUnitId = aLaunchedFromUnitId
            self.Number = int(aNumber)

        #-----------------------------------------------------------------------
        # Returns nation name (extracts from UnitId)
        def GetNation(self):
            return self.UnitId[:self.UnitId.find('_')]

        #-----------------------------------------------------------------------
        # Returns ship id (extracts from UnitId)
        def GetShipId(self):
            return self.UnitId[self.UnitId.find('_') + 1:]

    #---------------------------------------------------------------------------
    # Fire action
    #---------------------------------------------------------------------------
    class CFireAction:
        # Fire types
        FIRE_TYPE_LASER = 0
        FIRE_TYPE_MISSILE = 1
        
        # Damage types
        DAMAGE_TYPE_NONE = 0
        DAMAGE_TYPE_MISSED = 1
        DAMAGE_TYPE_SHIELD = 2
        DAMAGE_TYPE_DAMAGED = 3
        DAMAGE_TYPE_DESTROYED = 4

        #-----------------------------------------------------------------------
        def __init__(\
            self, aSeqNumber = 0, aUnitId = "", aTargetId = 0, aFireType = 0, aDamageType = 0,
            aShotSize = 0, aShotPower = 0, aTargetDamageAmount = 0,
            aTargetRepulsedByShield = 0, aTargetEfficiency = 0, aId = 0):
            
            self.SeqNumber = int(aSeqNumber)
            self.Id = aId
            self.UnitId = aUnitId
            self.TargetId = aTargetId
            self.FireType = int(aFireType)
            self.DamageType = int(aDamageType)
            self.ShotSize = float(aShotSize)
            self.ShotPower = float(aShotPower)
            self.TargetDamageAmount = float(aTargetDamageAmount)
            self.TargetRepulsedByShield = float(aTargetRepulsedByShield)
            self.TargetEfficiency = float(aTargetEfficiency)
            self.MoveAction = None # for missiles only


    #---------------------------------------------------------------------------
    # Move action
    #---------------------------------------------------------------------------
    class CMoveAction:
        #-----------------------------------------------------------------------
        def __init__(self, aSeqNumber = 0, aUnitId = "", aNewX = 0, aNewY = 0, aCount = 1):
            self.SeqNumber = int(aSeqNumber)
            self.UnitId = aUnitId
            self.NewX = float(aNewX)
            self.NewY = float(aNewY)
            self.Count = int(aCount)
       
        def IsEscape(self):
            return self.NewX == -999999999 or self.NewY == -999999999
        
    #---------------------------------------------------------------------------
    def __init__(self):
        self.Units = []
        self.FireActions = []
        self.MissileHits = []
        self.ShipMoves = []
        self.MissileMoves = []
        
        # All actions
        self.AllActions = []
        # Current Action
        self.CurrentAction = 0


    #---------------------------------------------------------------------------
    # Prepares all actions to run sequences
    def PrepareActions(self):
        self.GoFirstAction()
        
        # Merge all actions in one container and sort by sequence
        if len(self.AllActions) == 0:
            self.AllActions.extend(self.FireActions)
            self.AllActions.extend(self.MissileMoves)
            self.AllActions.extend(self.ShipMoves)
            self.AllActions.extend(self.MissileHits)
            self.AllActions.sort(key = lambda x: x.SeqNumber);

    
    #---------------------------------------------------------------------------
    # Returns current action
    def GetCurrentAction(self):
        guApp.Application.Log(\
            zxLog.LOG_INFO,
            "____GetCurrentAction: self.CurrentAction=" + str(self.CurrentAction) + \
            ", len(self.AllActions)=" + str(len(self.AllActions)), 
            zxLog.LEVEL_DEBUG)
        return self.AllActions[self.CurrentAction] \
                if self.CurrentAction < len(self.AllActions) and self.CurrentAction >= 0 \
                else None
        
    #---------------------------------------------------------------------------
    # Returns True if there are any actions to play
    def HasActions(self):
        return self.CurrentAction < len(self.AllActions) - 1

    #---------------------------------------------------------------------------
    # Move to next action. Returns true if next action exists
    def GoNextAction(self):
        if self.HasActions():
            self.CurrentAction += 1
            return True
        return False

    #---------------------------------------------------------------------------
    # Move to previous action. Returns true if prev action exists
    def GoPrevAction(self):
        if self.CurrentAction > 0:
            self.CurrentAction -= 1
            return True
        return False

    #---------------------------------------------------------------------------
    def GoFirstAction(self):
        self.CurrentAction = 0
    def GoLastAction(self):
        self.CurrentAction = len(self.AllActions) - 1
#------------------------------------------------------------------------------


#==============================================================================
# Battle Simulator
#==============================================================================
class CBattleSimulator:
    #-----------------
    # Ships descriptor
    class CShipsDesc:
        #-----------------------------
        def __init__(self, aNationName, 
                           aShipTypeName, 
                           aShipTypeCode, # ship type code in a shape 'D10,S5,L10x5...'
                           aNumber = 1, # number of ships
                           aShipClass = 0, 
                           aCF = 1, # combat factor (TODO: currently not used)
                           aTechs = [] # techs set (TODO: currently not used)
                           ):
            self.NationName = aNationName
            self.ShipTypeName = aShipTypeName
            self.ShipClass = int(aShipClass)
            self.ShipTypeCode = aShipTypeCode
            self.Number = int(aNumber)
            self.CF = aCF
            self.Techs = aTechs
        
        #--------------------
        # Returns string code
        def ToString(self):
            return self.NationName + ":" + \
                   self.ShipTypeName + ":" + \
                   str(self.ShipClass) + ":" + \
                   self.ShipTypeCode + ":" + \
                   ":" + \
                   ":" + \
                   str(self.Number)

    #------------------
    def __init__(self):
        # List of ship descriptors
        self.ShipDescriptors = [] 
        # Map of colours assigned to nations: {nation name: colour}
        self.NationsColours = {} 
        
    #--------------------------------------------
    # Produces string represention all components
    def ToString(self):
        s = ""
        for c in self.ShipDescriptors:
            s += c.ToString() + ";"
        return s
            
    #--------------------
    # Adds new descriptor
    def AddDesc(self, aNationName, 
                      aShipTypeName, 
                      aShipTypeCode, # ship type code in a shape 'D10,S5,L10x5...'
                      aNumber = 1, # number of ships
                      aShipClass = 0, 
                      aCF = 1, # combat factor (TODO: currently not used)
                      aTechs = []):
        self.ShipDescriptors.append(\
            CBattleSimulator.CShipsDesc(\
                aNationName, aShipTypeName, 
                aShipTypeCode, aNumber, aShipClass,
                aCF, aTechs))

    #-------------------------------------------------------------
    # Returns descriptor by nation/type name or None if not found
    def GetShipTypeCmd(self, aNationName, aShipTypeName):
        for sd in self.ShipDescriptors:
            if sd.NationName == aNationName and sd.ShipTypeName == aShipTypeName:
                return sd
        return None
    
    #----------------------------                                        
    # Assigns a colour for nation                                                                
    def AssignNationColour(self, aNationName, aColour):
        self.NationsColours[aNationName] = aColour
    
    #---------------------
    # Simulates the battle
    def Simulate(self, aStar):
        if guApp.Application.API.guImitateBattle(self.ToString()):
            bs = guApp.Application.GetReport().GetBattleSequences(-1)
            if len(bs) > 0:
                # Hard coded colour schemes
                colours = [(255, 255, 255), \
                           (255,   0,   0), \
                           (255, 255,   0), \
                           (  0,   0, 255), \
                           (  0, 255,   0), \
                           (255,   0, 255), \
                           (  0, 255, 255), \
                          ]
                colourIndex = 0
                # Create images for the viewer
                shipImages = {}
                for unit in bs[0].Units:
                    # Find descriptor
                    sd = self.GetShipTypeCmd(unit.GetNation(), unit.ShipTypeName)
                    if sd is None:
                        guApp.Application.Log(\
                            zxLog.LOG_ERROR, 
                            "Unknown simulated unit: cannot find descriptor for Nation " + \
                            unit.GetNation() + ", ship type: " + unit.ShipTypeName)
                    else:
                        # Re-create ship type from descriptor
                        shipType = guObjects.CShipType(sd.ShipTypeName,
                                                       sd.NationName,
                                                       sd.ShipClass)
                        components = guApp.Application.GetReport().LoadShipComponentsByCB(\
                            lambda cb: guApp.Application.API.guGetShipTypeComponentsFromCmd(sd.ShipTypeCode, cb))
                        if len(components) == 0:
                            guApp.Application.Log(\
                                zxLog.LOG_ERROR,
                                'Invalid ship type <' + shipType.Name + ">: no components")
                        else:
                            shipType.Components = components
                            if sd.NationName not in self.NationsColours:
                                self.NationsColours[sd.NationName] = \
                                    colours[colourIndex] \
                                        if colourIndex < len(colours) \
                                        else \
                                            (100 + random.random()*150, 
                                             100 + random.random()*150, 
                                             100 + random.random()*150)
                                colourIndex += 1
                            shipImages[unit.UnitId] = \
                                (guObjects.CShipType.CreateImage(\
                                    self.NationsColours[sd.NationName], 
                                    guShipLayout.CShipLayout(shipType.Components)),
                                    self.NationsColours[sd.NationName])
                
                # Simulate
                ViewBattle(aStar, bs, shipImages)

#------------------------------------------------------------------------------


#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
# Helper: view battle on aStar
#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def ViewBattle(aStar, aSequences, aShipImages):
    starMap = guApp.Application.GetStarMap()
    CBattleViewer(aStar, aSequences, aShipImages,
                  starMap.GetPlanetImage(aStar.Id), starMap.GetStarImage(aStar.Id)).Go()
#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<    
    
    
#==============================================================================
# Confugurable pygame dysplay viewer
#==============================================================================
class CBattleViewer(pgViewer.CViewer):
    ORIGINAL_SCALE = 10 # scale, appropriate to show ships in 1:1
    
    # Internal class - unit statistic
    class CUnitStatistic:
        def __init__(self):
            self.DestroyedShips = 0
            self.DestroyedShipsMass = 0.00
            self.DamagedMass = 0.00
    
    #--------------------------------------------------------------------------
    # aSequences - battle sequences
    # aShipImages - pre-set ship images map <Unit_Id : (image, colour)>
    def __init__(self, aStar, aSequences, aShipImages, aPlanetImage, aStarImage):
        pgViewer.CViewer.__init__(\
            self, guApp.Application.Config,
            "GUI", "BattleViewer", self.ORIGINAL_SCALE, 0, 0, 30, 60, 1600, 900)
        pygame.display.set_caption(\
            "Space Battle at " + guApp.Application.GetReport().GetStarName(aStar.Id))
        self.__star = aStar

        # Select position of planet and star
        planetRect = aPlanetImage.get_rect();
        self.__planetImage = \
            pygame.transform.scale(aPlanetImage, (planetRect.width*2, planetRect.width*2))
        self.__starImage = aStarImage
        self.__planetRect = self.__planetImage.get_rect();
        self.__starRect = self.__starImage.get_rect()
        r = random.random()
        if r < 0.25:
            self.__planetRect.centerx = 0
            self.__planetRect.centery = 0
            self.__starRect.right = self.GetWorkingWidth() - 50
            self.__starRect.bottom = self.GetWorkingHeight() - 50
        elif r >= 0.25 and r < 0.5:
            self.__planetRect.centerx = 0
            self.__planetRect.centery = self.GetWorkingHeight()
            self.__starRect.right = self.GetWorkingWidth() - 50
            self.__starRect.top = 50
        elif r >= 0.5 and r < 0.75:
            self.__planetRect.centerx = self.GetWorkingWidth()
            self.__planetRect.centery = 0
            self.__starRect.left = 50
            self.__starRect.bottom = self.GetWorkingHeight() - 50
        else:
            self.__planetRect.centerx = self.GetWorkingWidth()
            self.__planetRect.centery = self.GetWorkingHeight()
            self.__starRect.left = 50
            self.__starRect.top = 50

        self.__originalSequences = aSequences
        self.Sequences = copy.deepcopy(self.__originalSequences)
        self.__CurrentSeq = 0    # current sequence index
        self.ShipImages = aShipImages

        # Make 1st sequence ready
        if len(self.Sequences) > 0:
            self.PrepareSequence()

        # Action info: updated during action play
        self.__actionInfo = ""
        
        # Info font
        self.InfoFont = pgTools.LoadFont("Fonts/SelfDestructButtonBB_reg.ttf", 20)
        # Ship title font
        self.ShipFont = None # calculated in AfterScaleChanged
        self.AfterScaleChanged(self.Scale)
        # Statistic font
        self.StatisticFont = pgTools.LoadFont("Fonts/SelfDestructButtonBB_reg.ttf", 30)

        # Star dust
        random.seed(self.__star.Id)
        self.__starDust = guStarDust.Generate(1000, zx.CArea(0, 0, self.MaxWidth, self.MaxHeight))

        # Collection of laser images
        self.__ShotImages = {}
        
        # Removed objects tracker: <unit_id: (sequence, action index)>
        self.__RemovedObjects = {}
        # Units angle tracker <unit_id: angle>
        self.__UnitsAngles = {}

        # Flags
        self.__PlayStarted = False
        self.__ShowMissed = self.Cfg.GetValue("ShowMissed", "1") == "1"
        self.__ShowShield = self.Cfg.GetValue("ShowShield", "1") == "1"
        
        # Statistic (map <nation name: CUnitStatistic>)
        self.__statistic = {}


    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
    # Helper: indicated play start/stop
    def __ShowPlayStarted(self, aPlayStarted):
        self.__PlayStarted = aPlayStarted
        self.DrawInfoArea()
        pygame.display.flip()
        

    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # Redefined virtual
    def OnClose(self):
        self.Cfg.SetValue("ShowMissed", "1" if self.__ShowMissed else "0")
        self.Cfg.SetValue("ShowShield", "1" if self.__ShowShield else "0")


    #--------------------------------------------------------------------------    
    # Restores objects removed in sequence aSeq/aActionIndex or after
    def RestoreObjects(self, aSeq, aActionIndex):
        # Undo removed in this action objects
        for ui in self.Objects.keys():
            if ui in self.__RemovedObjects:
                ro = self.__RemovedObjects[ui]
                if ro[0] >= aSeq and ro[1] >= aActionIndex:
                    # This object was deleted in this action or after - restore it
                    self.Objects[ui].IsVisible = True
                    del self.__RemovedObjects[ui]
                else:
                    self.Objects[ui].IsVisible = False
        
    #--------------------------------------------------------------------------    
    # Returns shot image (will be used in the future for another weapon)
    def GetShotImage(self, aUnitId, aSize):
        if aUnitId in self.__ShotImages:
            image = self.__ShotImages[aUnitId]
        else:
            sz = 20
            image = pygame.Surface((sz, sz))
            image.fill((0,0,0))
            image.set_colorkey((0,0,0)) # make image transparent
            r = int(sz/2)
            pygame.draw.circle(image, self.ShipImages[aUnitId][1], (int(r), int(r)), r, 0)
            self.__ShotImages[aUnitId] = image
            
        return pygame.transform.scale(image, (aSize, aSize))

    
    #--------------------------------------------------------------------------    
    # Preparing sequence
    def PrepareSequence(self, aReset = True):
        # Clear objects first
        self.ResetObjects(aReset)
        if not aReset:
            self.VirtualArea = zx.CArea()
        
        # Prepare actions steps if not ready yet
        self.Sequences[self.__CurrentSeq].PrepareActions()
        
        # Add all sequence objects
        for unit in self.Sequences[self.__CurrentSeq].Units:
            self.AddUnitObject(unit)
            
        # Fictive invisible object to make moves
        mtId = "_fictive_99999"
        self.AddUnitObject(CBattleSequence.CUnit(mtId, mtId, 1))
        self.__MoveToObject = self.Objects[mtId]
        self.__MoveToObject.IsVisible = False
            
        # Manually resize re-created objects according to scale
        if not aReset:
            self.ReScale(self.Scale, False, True)
    

    #--------------------------------------------------------------------------
    # Adds screen object for unit
    def AddUnitObject(self, aUnit, aColour = None):
        # Calc ship diameter (mocking a ship command line)
        sz = guApp.Application.API.guCalcShipTypeParam(0, "S" + str(aUnit.Mass), 5)
        if aUnit.IsMissile:
            sz /= 3
        if sz < 1:
            sz = 1 # to avoid the missiles be too small on the screen
        d = self.ArrangeMinSize(int(sz) * self.ORIGINAL_SCALE)
        if aUnit.UnitId not in self.ShipImages:
            if aColour is None:
                aColour = guApp.Application.GetReport().GetNationColour(aUnit.GetNation())
            self.ShipImages[aUnit.UnitId] = \
                (guApp.Application.GetReport().GetShipTypeImage(\
                    aUnit.GetNation(), 
                    aUnit.ShipTypeName, aColour), aColour)
        self.AddObject(\
            aUnit.UnitId,
            pygame.transform.scale(self.ShipImages[aUnit.UnitId][0], (d, d)),
            (aUnit.X, aUnit.Y), d)


    #--------------------------------------------------------------------------
    # Jumping to battle sequence
    def JumpToSequence(self, aShift):
        newSeq = self.__CurrentSeq + aShift
        if newSeq >= len(self.Sequences):
            newSeq = len(self.Sequences) - 1
        elif newSeq < 0:
            newSeq = 0
        while newSeq != self.__CurrentSeq or self.Sequences[self.__CurrentSeq].CurrentAction != 0:
            if newSeq > self.__CurrentSeq:
                self.PlayNextAction(False)
            else:
                self.GoPrevAction()


    #--------------------------------------------------------------------------
    # Shows hyperspace jump
    def ShowJump(self, aBkGround, aPos, aRadius, aColour):
        clock = pygame.time.Clock()
        bkrect = aBkGround.get_rect()
        citers = 20
        cvm = pgTools.CColourVolMaker(aColour, citers, 0.3)
        dd = float(aRadius)/citers
        d = 0
        for i in range(citers):
            clock.tick(100)
            self.Screen.blit(aBkGround, bkrect)
            pygame.draw.circle(self.Screen, cvm.GetNextColour(), aPos, aRadius - int(d) + 1)
            d += dd
            pygame.display.flip()
            clock.tick(100)
            self.Screen.blit(aBkGround, bkrect)
            pygame.display.flip()

                
    #--------------------------------------------------------------------------
    # Returns unit by Id                
    def FindUnit(self, aUnitId):
        return zx.Find(self.Sequences[self.__CurrentSeq].Units, lambda u: u.UnitId == aUnitId)
    

    #--------------------------------------------------------------------------
    # Playing current action
    # if aShow is False, the action is played silently, no showning fire
    def PlayAction(self, aShow = True):
        if self.__CurrentSeq < len(self.Sequences):
            action = self.Sequences[self.__CurrentSeq].GetCurrentAction()
            if action is not None:
                unit = self.Objects[action.UnitId] if action.UnitId in self.Objects else None

                #---------------------------------------------------------------
                # Helper function: hitting the target
                def Hit(action, target):
                    if target is not None:
                        targetUnit = self.FindUnit(action.TargetId)
                        if not targetUnit.IsMissile:
                            self.__AddActionInfo(action.UnitId + " -> " + action.TargetId, True)
                        shieldRadius = int(float(target.Rect.width)/1.7)
                        if action.DamageType == CBattleSequence.CFireAction.DAMAGE_TYPE_MISSED:
                            if not targetUnit.IsMissile:
                                self.__AddActionInfo("-MISSED-")
                        
                        if action.DamageType == CBattleSequence.CFireAction.DAMAGE_TYPE_SHIELD:
                            guTools.ShowShield(\
                                self.Screen, self.GetBackground(),
                                (target.Rect.centerx, target.Rect.centery), shieldRadius, (0,150,255), 3)
                            if not targetUnit.IsMissile:
                                self.__AddActionInfo("(SHIELD)")

                        if action.DamageType == CBattleSequence.CFireAction.DAMAGE_TYPE_DAMAGED:
                            if action.TargetRepulsedByShield > 0:
                                guTools.ShowShield(\
                                    self.Screen, self.GetBackground(),
                                    (target.Rect.centerx, target.Rect.centery), shieldRadius, (255,0,0), 2)
                            if not targetUnit.IsMissile:
                                self.__AddActionInfo("<DAMAGED>: " + "{0:.2f}".format(action.TargetDamageAmount))
                            self.__AddUnitStatistic(targetUnit, False, action.TargetDamageAmount)
                            guTools.ShowExplosion(\
                                self.Screen, self.GetBackground(),
                                (target.Rect.centerx, target.Rect.centery),
                                target.Rect.width + int(action.TargetDamageAmount))

                        # Remove target if destroyed
                        if action.DamageType == CBattleSequence.CFireAction.DAMAGE_TYPE_DESTROYED:
                            if not targetUnit.IsMissile:
                                self.__AddActionInfo("[DESTROYED]")
                            if aShow:
                                if action.TargetRepulsedByShield > 0:
                                    guTools.ShowShield(\
                                        self.Screen, self.GetBackground(),
                                        (target.Rect.centerx, target.Rect.centery), shieldRadius, (255,0,0), 2)
                                guTools.ShowExplosion(\
                                    self.Screen, self.GetBackground(),
                                    (target.Rect.centerx, target.Rect.centery),
                                    target.Rect.width * 3 + int(math.sqrt(action.ShotPower)),
                                    20 if targetUnit.IsMissile else 10) # for ships slower explosion
                            targetUnit.Number -= 1
                            self.__AddUnitStatistic(targetUnit, True, action.TargetDamageAmount)
                            if targetUnit.Number <= 0:
                                self.__RemovedObjects[action.TargetId] = \
                                    (self.__CurrentSeq, self.Sequences[self.__CurrentSeq].CurrentAction)
                                target.IsVisible = False
                #---------------------------------------------------------------
                
                if isinstance(action, CBattleSequence.CFireAction):
                    target = self.Objects[action.TargetId] if action.TargetId in self.Objects else None
                    if target is None:
                        # This can happen if unit is a missile just launched. This is a missile launch action
                        #
                        self.MakeObjectsOnScreen([unit])
                        # Find unit launcing the missile
                        u = self.FindUnit(action.UnitId)
                        # Add a new temporary unit to the sequence
                        missile = CBattleSequence.CUnit(action.TargetId,
                                                        "_missile_", 
                                                        action.ShotSize, 
                                                        1, 
                                                        u.X, u.Y, 
                                                        True,
                                                        action.UnitId)
                        self.Sequences[self.__CurrentSeq].Units.append(missile)
                        # Add missile unit coloured as launching ship
                        bk = self.GetBackground()
                        guTools.ShowSmoke(\
                            self.Screen,
                            guApp.Application.OtherImages[guApp.Application.IMAGE_SMOKE],
                            bk, (unit.Rect.centerx, unit.Rect.centery), unit.Rect.width)

                        self.AddUnitObject(missile, self.ShipImages[u.UnitId][1])
                        unit = self.Objects[action.TargetId]
                        self.ReScaleObject(unit, float(self.Scale/self.NaturalScale))
                        if aShow:
                            self.ReDraw()
                        return
                    
                    if aShow:
                        # Laser shot or missile hit
                        u = self.FindUnit(action.UnitId)
                        isMissile = u is not None and u.IsMissile
                        if not isMissile and \
                           (not self.__ShowShield and action.DamageType == \
                                CBattleSequence.CFireAction.DAMAGE_TYPE_SHIELD or \
                            not self.__ShowMissed and action.DamageType == \
                                CBattleSequence.CFireAction.DAMAGE_TYPE_MISSED):
                            # Do not show shield or/and miss for laser if specified
                            return

                        # Make sure both targets visible
                        self.MakeObjectsOnScreen([unit, target])

                        if not isMissile:
                            if u is not None:
                                self.__AddUnitStatistic(u, False, 0) # just 0 statistic for final info
                            
                            # Laser shot

                            bk = self.GetBackground()
                            guTools.ShowSmoke(\
                                self.Screen,
                                guApp.Application.OtherImages[guApp.Application.IMAGE_LASER_FLASH],
                                bk, (unit.Rect.centerx, unit.Rect.centery), unit.Rect.width)
                            
                            if action.DamageType == CBattleSequence.CFireAction.DAMAGE_TYPE_MISSED:
                                tx = target.Rect.left if random.random() > 0.5 else target.Rect.right
                                ty = target.Rect.top if random.random() > 0.5 else target.Rect.bottom
                                dx = (tx - unit.Rect.centerx)/10
                                dy = (ty - unit.Rect.centery)/10
                                rect = pygame.Rect(tx, ty, 1, 1)
                                while self.IsRectOnScreen(rect):
                                    rect.left += dx
                                    rect.top += dy
                                    
                                tx = rect.left
                                ty = rect.top
                            else:
                                tx = target.Rect.centerx
                                ty = target.Rect.centery

                            d = self.ArrangeMinSize(int(math.sqrt((self.Scale/self.NaturalScale) * action.ShotSize)))
                            print 'd=', d
                            pygame.draw.line(
                                self.Screen, self.ShipImages[action.UnitId][1], 
                                (unit.Rect.centerx, unit.Rect.centery), 
                                (tx, ty),
                                d)
                            pygame.display.flip()
                            clock = pygame.time.Clock()
                            clock.tick(100)
                        else:
                            # It's a missile - remove missile unit
                            self.__RemovedObjects[action.UnitId] = \
                                (self.__CurrentSeq, self.Sequences[self.__CurrentSeq].CurrentAction)
                            unit.IsVisible = False
                            self.ReDraw()

                        Hit(action, target)
                        pygame.display.flip()

                if isinstance(action, CBattleSequence.CMoveAction):
                    # Hide object object from its original location
                    if aShow:
                        # Make sure object is on the screen (target point affected only if not escape!)
                        self.__MoveToObject.Pos = (action.NewX, action.NewY)
                        self.MakeObjectsOnScreen([unit] if action.IsEscape() else [unit, self.__MoveToObject])
                        unit.IsVisible = False # invisibility flag
                        self.ReDraw() # redraw the picture
                    
                    # New rect position
                    newRectPos = self.VirtualToScreen((action.NewX, action.NewY))
                    
                    # Move unit to the new position
                    if action.IsEscape():
                        u = self.FindUnit(action.UnitId)
                        if u is not None:
                            self.ShowJump(\
                                self.GetBackground(),
                                (unit.Rect.centerx, unit.Rect.centery),
                                unit.Rect.width, self.ShipImages[action.UnitId][1])
                            u.Number -= action.Count
                            if u.Number <= 0:
                                self.__RemovedObjects[action.UnitId] =\
                                    (self.__CurrentSeq, self.Sequences[self.__CurrentSeq].CurrentAction)
                                unit.IsVisible = False
                                self.ReDraw()
                            else:
                                unit.IsVisible = True
                        else:
                            guApp.Application.Log(zxLog.LOG_ERROR, "Unit " + str(action.UnitId) + " not found")
                        # Make object visible again
                        return
                    else:
                        if aShow:
                            u = self.FindUnit(action.UnitId)
                            isMissile = u is not None and u.IsMissile
                            guTools.MoveImage(self, self.GetObjScaledImage(action.UnitId, unit), 
                                              self.GetBackground(), 
                                              (unit.Rect.centerx, unit.Rect.centery), 
                                              newRectPos, 4 if isMissile else 1)
                    # Assign new position to the object and rect
                    unit.Pos = (action.NewX, action.NewY)
                    unit.Rect.centerx = newRectPos[0]
                    unit.Rect.centery = newRectPos[1]

                    # Make object visible again
                    unit.IsVisible = True


    #---------------------------------------------------------------------------
    # Returns True if there are any actions to play
    def HasActions(self):
        return self.__CurrentSeq < len(self.Sequences) - 1 and \
            self.Sequences[self.__CurrentSeq].HasActions()

    #--------------------------------------------------------------------------
    # Go to next action
    def GoNextAction(self):
        # Try to move to next action inside the sequence
        if not self.Sequences[self.__CurrentSeq].GoNextAction():
            # No more actions - move to next seq
            if self.__CurrentSeq < len(self.Sequences) - 1:
                self.__CurrentSeq += 1
                self.PrepareSequence(False)
                self.Sequences[self.__CurrentSeq].GoFirstAction()
            else:
                return False
        self.ReDraw()
        return True

    #--------------------------------------------------------------------------
    # Go to next action
    def GoPrevAction(self):
        # Try to move to next action inside the sequence
        if not self.Sequences[self.__CurrentSeq].GoPrevAction():
            # No more actions - move to prev seq
            if self.__CurrentSeq > 0:
                self.__CurrentSeq -= 1
                self.PrepareSequence(False)
                self.Sequences[self.__CurrentSeq].GoLastAction()
                
                # Silently move all objects to last action position
                for action in self.Sequences[self.__CurrentSeq].AllActions:
                    if isinstance(action, CBattleSequence.CMoveAction) and not action.IsEscape():
                        unit = self.Objects[action.UnitId]
                        newRectPos = self.VirtualToScreen((action.NewX, action.NewY))
                        unit.Pos = (action.NewX, action.NewY)
                        unit.Rect.centerx = newRectPos[0]
                        unit.Rect.centery = newRectPos[1]
            else:
                return False
                
        # Undo removed in this action objects
        self.RestoreObjects(self.__CurrentSeq, self.Sequences[self.__CurrentSeq].CurrentAction)
        
        # Undo move action in previous sequence
        action = self.Sequences[self.__CurrentSeq].GetCurrentAction()
        if isinstance(action, CBattleSequence.CMoveAction) and not action.IsEscape():
            unit = self.Objects[action.UnitId]
            if unit.Pos == (action.NewX, action.NewY):
                # Unit has been moved - return it back
                # Find original coordinates in units
                u = self.FindUnit(action.UnitId)
                if u is not None:
                    # Found - always should be - restore orig position of unit and rect
                    unit.Pos = (u.X, u.Y)
                    newRectPos = self.VirtualToScreen(unit.Pos)
                    unit.Rect.centerx = newRectPos[0]
                    unit.Rect.centery = newRectPos[1]
                
        # Update picture
        self.ReDraw()
        return True

    #--------------------------------------------------------------------------
    # Plays next action. Returns False if there are no more actions to play
    def PlayNextAction(self, aShow = True):
        self.PlayAction(aShow)
        if self.GoNextAction():
            return True
            
        # Show final statistic
        c = (0,255,100)
        rect = pgTools.DrawText(self.Screen, "BATTLE LOSSES STATISTIC", 
                                50,
                                100,
                                self.StatisticFont, c,
                                False, False)        
        rect = pgTools.DrawText(self.Screen, "State                Lost Ships     Lost Ships Mass     Damaged Ships Mass", 
                                rect.left,
                                rect.bottom,
                                self.StatisticFont, c,
                                False, False)        
        rect = pgTools.DrawText(self.Screen, "--------------------------------------------------------------------------", 
                                rect.left,
                                rect.bottom,
                                self.StatisticFont, c,
                                False, False)        

        for nation, stat in self.__statistic.iteritems():
            c = guApp.Application.GetReport().GetNationColour(nation)
            pgTools.DrawText(self.Screen, nation, 
                             rect.left,
                             rect.bottom,
                             self.StatisticFont, c,
                             False, False)        
            pgTools.DrawText(self.Screen, str(stat.DestroyedShips), 
                             260,
                             rect.bottom,
                             self.StatisticFont, c,
                             False, False)        
            pgTools.DrawText(self.Screen, "{0:.2f}".format(stat.DestroyedShipsMass), 
                             420,
                             rect.bottom,
                             self.StatisticFont, c,
                             False, False)        
            r = pgTools.DrawText(self.Screen, "{0:.2f}".format(stat.DamagedMass), 
                             660,
                             rect.bottom,
                             self.StatisticFont, c,
                             False, False)
            rect.bottom = r.bottom
        return False
        

    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # Redefined virtual
    def AfterScaleChanged(self, aScale):
        self.ShipFont = pgTools.LoadFont(\
            "Fonts/SelfDestructButtonBB_reg.ttf",
            int(10 + ((self.Scale - self.ORIGINAL_SCALE/2) \
                if self.Scale >= self.ORIGINAL_SCALE/2 else 0)))
    

    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # Redefined virtual
    def GetObjScaledImage(self, aKey, aObject):
        # Check if this object has a move action
        unit = self.FindUnit(aKey)
        angle = self.__UnitsAngles[aKey] if aKey in self.__UnitsAngles else None
        for action in self.Sequences[self.__CurrentSeq].AllActions:
            if action.UnitId == aKey and isinstance(action, CBattleSequence.CMoveAction):
                # Rotate image to destination
                dx,dy = action.NewX - aObject.Pos[0], -(action.NewY - aObject.Pos[1])
                if dx != 0 or dy != 0:
                    angle = math.degrees(math.atan2(dy, dx))
                    if dx < 0 and dy > 0 or dx > 0 and dy < 0:
                        angle += 180
                    else:
                        if dx < 0:
                            angle += 180
                        if dy > 0:
                            angle += 180
                    angle -= 180
                break
 
        if angle != None:
            self.__UnitsAngles[aKey] = angle
            return pygame.transform.rotate(aObject.ScaledImage, angle)
                
        return aObject.ScaledImage
        
    
    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # Redefined virtual
    # Draw ship title
    def AfterDrawObject(self, aId, aObject):
        unit = self.FindUnit(aId)
        if unit is not None and not unit.IsMissile and self.ShipFont is not None:
            if self.ShipFont is None:
                self.AfterScaleChanged(self.Scale)
            text = self.ShipFont.render(((str(unit.Number) + " ") if unit.Number > 1 else "") + unit.UnitId,
                                        True, self.ShipImages[aId][1])
            textRect = text.get_rect()
            textRect.centerx = aObject.Rect.centerx
            textRect.top = aObject.Rect.bottom
            self.Screen.blit(text, textRect)


    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # Redefined virtual
    def IsObjectOnScreen(self, o):
        # Make sure not only object but also a title is visible
        rect = copy.deepcopy(o.Rect)
        if self.ShipFont is not None:
            rect = pygame.Rect(o.Rect.left, o.Rect.top, o.Rect.width, o.Rect.height + self.ShipFont.get_height())
        return self.IsRectOnScreen(rect)


    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    # Helper: adds a new porion to action info
    def __AddActionInfo(self, aText, aStart = False):
        if aStart:
            self.__actionInfo = ""    
        self.__actionInfo += " " + aText
        
        
    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    # Helper: adds statistic for unit
    def __AddUnitStatistic(self, aUnit, aIsDestroyed, aDamagedMass):
        if aUnit.IsMissile:
            return # missiles are not considered
        if not aUnit.GetNation() in self.__statistic:
            self.__statistic[aUnit.GetNation()] = self.CUnitStatistic()
        stat = self.__statistic[aUnit.GetNation()]
        stat.DamagedMass += aDamagedMass
        if aIsDestroyed:
            stat.DamagedMass -= aUnit.Mass        # remove ship mass from 'damaged' mass
            if stat.DamagedMass < 0:
                stat.DamagedMass = 0
            stat.DestroyedShipsMass += aUnit.Mass # add ship mass to 'destroyed' mass
            stat.DestroyedShips += 1              # increase destroyed ships number
    
    
    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # Redefined virtual: info/control panel
    def DrawTopInfo(self, aRects):
        self.DrawInfoRects(aRects, (20,0,20))

        # Current battle sequence info
        s = "Seq " + str(self.__CurrentSeq + 1) + "/" + str(len(self.Sequences)) + \
            " Action " + str(self.Sequences[self.__CurrentSeq].CurrentAction + 1) + "/" + \
            str(len(self.Sequences[self.__CurrentSeq].AllActions)) + \
            " Scale " + "{0:.2f}".format(self.Scale)
                                    
        # Use current action info
        s += " *** LAST FIRE ACTION INFO: " + self.__actionInfo
         
        # Display text                           
        text = self.InfoFont.render(s, True, (0, 150, 255))
                                    
        textRect = text.get_rect()
        textRect.left = 0
        textRect.centery = aRects[0].top + aRects[0].height/2
        self.Screen.blit(text, textRect)


    #--------------------------------------------------------------------------
    # Redefined virtual: info/control panel
    def DrawBottomInfo(self, aRects):
        # Back ground
        self.DrawInfoRects(aRects, (20,0,20))
        
        if self.HasActions():
            s = "ENTER/ESC-stop" if self.__PlayStarted else "ENTER-start"
        else:
            s = "ESC-quit"

        if not self.__PlayStarted:
            s += " CTRL-RIGHT-next sequence CTRL-LEFT-prev sequence CTRL-HOME-begin CTRL-END-end '>'-next action '<'-prev action '+'/'-'-change scale"
            
        rect = pgTools.DrawText(self.Screen, s, 0, aRects[0].top,
                                self.InfoFont, guApp.Application.GetReport().NEUTRAL_COLOUR,
                                False, False)

        s = "BACKSPACE-" + ("dont't show missed" if self.__ShowMissed else "show missed")
        s += " SPACE-" + ("dont't show shield" if self.__ShowShield else "show shield")

        rect = pgTools.DrawText(self.Screen, s, 0, rect.top + rect.height,
                                self.InfoFont, guApp.Application.GetReport().NEUTRAL_COLOUR,
                                False, False)
                                
        if self.__ShowMissed or self.__ShowShield:
            s = ""
            if self.__ShowMissed:
                s = "MISSED"
            if self.__ShowShield:
                s += ("/" if s != "" else "") + "SHIELD"
            rect = pgTools.DrawText(self.Screen, "Showing <" + s + ">", 0, rect.top + rect.height,
                                    self.InfoFont, (255, 255, 255),
                                    False, False)
        
        # Show statistic
        s = ""
        for nation, stat in self.__statistic.iteritems():
            s += nation + ": " + str(stat.DestroyedShips) + "/" + \
                "{0:.2f}".format(stat.DestroyedShipsMass) + "/" + \
                "{0:.2f}".format(stat.DamagedMass) + " "
            
        if s != "":
            pgTools.DrawText(\
                self.Screen, " *** CURRENT LOSSES: " + s, 
                (rect.right + 10) if self.__ShowMissed or self.__ShowShield else 0,
                rect.top + (0 if self.__ShowMissed or self.__ShowShield else rect.height),
                self.InfoFont, guApp.Application.GetReport().NEUTRAL_COLOUR,
                False, False)        


    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # Redefined virtual - Draws background
    def DrawBackground(self):
        # Background
        self.Screen.fill((0,0,0))
        
        # Stardust
        guStarDust.Draw(self.__starDust, self.Screen)

        # Draw the planet
        ownership = guReport.CStarOwnership(self.__star)
        if ownership.IsOwn or ownership.IsKnown or guApp.Application.GetReport().IsGM():
            self.Screen.blit(self.__planetImage, self.__planetRect)

        # Draw star
        self.Screen.blit(self.__starImage, self.__starRect)


    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    # Helper: processes show flags according to event keys
    def __ProcessShowFlags(self, aKey):
        flagChanged = False
        if aKey == pygame.K_SPACE:
            self.__ShowShield = not self.__ShowShield
            flagChanged = True
        if aKey == pygame.K_BACKSPACE:
            self.__ShowMissed = not self.__ShowMissed
            flagChanged = True
        if flagChanged:
            self.DrawInfoArea()
            pygame.display.flip()
        return flagChanged

    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # Redefined event
    def OnKeyDown(self, aEvent):
        if aEvent.key == pygame.K_RETURN: # start/stop
            self.__ShowPlayStarted(True)
            
            # Battle loop
            while self.PlayNextAction():
                events = pygame.event.get([pygame.KEYDOWN])
                for e in events:
                    if e.key == pygame.K_RETURN or e.key == pygame.K_ESCAPE:
                        self.__ShowPlayStarted(False)
                        return True
                    self.__ProcessShowFlags(e.key)
                        
            # All battle steps have finished
            self.__ShowPlayStarted(False)

        if aEvent.mod == pygame.KMOD_LCTRL:
            if aEvent.key == pygame.K_RIGHT:
                self.JumpToSequence(1)
                return True

            if aEvent.key == pygame.K_END:
                self.JumpToSequence(999999) # jump to the end
                return True

            if aEvent.key == pygame.K_LEFT:
                self.JumpToSequence(-1)
                return True
            if aEvent.key == pygame.K_HOME:
                self.__statistic = {} # reset statistic
                self.JumpToSequence(-999999) # jump to the begining
                return True

        if aEvent.unicode == '>':
            self.PlayNextAction()
            return True

        if aEvent.unicode == '<':
            self.GoPrevAction()
            return True
            
        if self.__ProcessShowFlags(aEvent.key):
            return True

        return False # not processed
    
#------------------------------------------------------------------------------

