-- $LastChangedDate: 2010-04-16 22:43:07 -0500 (Fri, 16 Apr 2010) $
-- Game code.
-- See module.txt for documentation and pitfalls.

----------------------------
-- Game class (singleton) --
----------------------------

Game = { }

-- There are different kinds of scores.
Game.actors           = { }  -- entities (map)
Game.TEAM_NUM_NEUTRAL = 0
Game.TEAM_NUM_PLAYER  = 1
Game.TEAM_NUM_ENEMY   = 2
Game.oppositeTeamNums = { [0]=0, [1]=2, [2]=1 }
Game.showDeathRoll    = false  -- depends on cause of crash
Game.nonTargetNames   = {}
Game.nonTargetNames["Missile"] = true

-- Init.
function Game:Init()
    -- Do once.
    if ( not self.init ) then
        self.init = true
        Timer:Register( self, defs.GAME_TICK_FREQ )
        --self:AttachActor( PlayerActor )  -- too soon
    end
end

-- Reset.
function Game:Reset()
    PlayerActor:Reset()

    self.updatedScoreForPlayerCrash = false
    self.showDeathRoll = false

    -- Reset Actors.  Some might detach themselves (KEYS() can tolerate).
    for actor in KEYS(self.actors) do
        actor:Reset()
    end
end

Events:RegisterReset( function() Game:Reset() end )  -- need anon func

-- Attach actor.
function Game:AttachActor( actor )
    -- Don't re-attach.
    if ( not self.actors[actor] ) then
        self.actors[actor] = actor

        if ( actor:IfShowInHud() ) then
            sim.GameHud:AttachTarget( actor:GetObject(), actor:GetName() )
        end
    end
end

-- Detach actor.
function Game:DetachActor( actor )
    -- Don't re-detach.
    if ( self.actors[actor] ) then
        -- First remove from table/map (still being referenced).
        self.actors[actor] = nil

        -- Tell all other Actors to unreference this one.
        self:BroadcastUnreferenceActor( actor )

        -- Remove from GameHud.
        if ( actor:IfShowInHud() ) then
            -- Tolerate Actor:Destroy() nilifying Actor.object.
            local object = actor:GetObject()
            if ( object ) then
                sim.GameHud:DetachTarget( object )
            end
        end

        -- In Lua, must explicitly call destructor.
        actor:Destroy()
    end
end

-- Return Actor by name else nil.
function Game:GetActor( name )
    for actor in KEYS(self.actors) do
        if ( actor:GetName() == name ) then
            return actor
        end
    end
    return nil
end

-- Keep private to encourage use of CycleActor().
-- Get table of Actors.
--function Game:GetActors()
--  return self.actors
--end

-- Linearly, get next surviving Actor with matching teamNum (or any teamNum if nil).
-- Stops at end of table (linear).
-- Returns nil if no matching/surviving Actors remain.
function Game:NextActor( teamNum, curActor )
    -- Begin iterating after the current Actor (or first Actor if curActor==nil).
    for actor in lib.IteratorAfter( self.actors, curActor ) do
        if ( not actor:IfDestroyed() and (not teamNum or teamNum == actor:GetTeamNum()) ) then
            return actor  -- found it
        end
    end

    -- No match found.
    return nil
end

-- Cicurlary, get next surviving Actor with matching teamNum (or any teamNum if nil).
-- Cycles back to beginning then stops after wrapping back to curActor.
-- Returns nil if no matching/surviving Actors remain.
function Game:CycleActor( teamNum, curActor )
    -- Search {curActor+1,..,end}
    local actor = self:NextActor( teamNum, curActor )
    if ( actor ) then
        return actor
    else  -- either reached end or no match
        -- Search {begin,..,curActor-1}
        for actor in KEYS(self.actors) do
            if ( actor == curActor ) then
                break  -- stop at end of complement
            elseif ( not actor:IfDestroyed() and (not teamNum or teamNum == actor:GetTeamNum()) ) then
                return actor  -- found it
            end
        end
        -- No match found.
        return nil
    end
end

-- Map Actor to Object (raw pointer).
function Game:MapActorObject( objectPtr )
    for actor in KEYS(self.actors) do
        if ( sim.IfIdenticalObject( actor:GetObject(), objectPtr ) ) then
            return actor
        end
    end
    return nil  -- not found
end

-- Return true if Actor can be a target.
function Game:IfTarget( actor )
    return not self.nonTargetNames[actor:GetName()]
end

-- Return opposite team number of an actor.
function Game:OppositeTeamNum( actorOrTeamNum )
    -- Query arg's type.
    local teamNum = actorOrTeamNum
    if ( type(actorOrTeamNum) == "table" ) then
        teamNum = actorOrTeamNum:GetTeamNum() -- is an Actor table
    end
    return self.oppositeTeamNums[teamNum]
end

-- Return true if opposite teams (neutral excluded).
function Game:IfOppositeTeamNums( teamNum1, teamNum2 )
    return (teamNum1 == self.TEAM_NUM_PLAYER and teamNum2 == self.TEAM_NUM_ENEMY)
        or (teamNum2 == self.TEAM_NUM_PLAYER and teamNum1 == self.TEAM_NUM_ENEMY)
end

-- (private) Increment a type of score in binary configuration.
-- scoreKey is a key/string in the binary configuration.
function Game:WriteScoreInt( key, inc )
    -- Read score from ConfBin.
    local readOk,score = sim.ConfBin:ReadInt( key )
    if ( not readOk ) then
        -- This probably means the integer value isn't yet in the conf file.
        score = 0
    end

    -- Increment score.
    score = score + inc

    -- Write updated score to ConfBin.
    local writeOk = sim.ConfBin:WriteInt( key, score )
    if ( not writeOk ) then
        Log( "ERROR [Lua] accessing score in bin conf file" )
        return false
    else
        return true
    end
end

-- (private)
-- This polls the underlying C++ Objects to detect a crash/collision
-- which is propagated up to Lua Actors.
-- Updates scores stored in the binary configuration (~/.palomino.cfg).
function Game:UpdateScore()
    -- For each actor.
    for actor in KEYS(self.actors) do
        -- Player hit/crashed?
        -- Cut-scene: death roll with rescue helicopter.
        -- Ignore "crashes" such as missile-hit.
        if ( (actor == PlayerActor) and PlayerActor:IfDestroyed() and (not self.updatedScoreForPlayerCrash) ) then
            self.updatedScoreForPlayerCrash = true  -- increase score once

            -- Player has crashed or been hit.
            -- Default to skipping death-roll animation.
            self.showDeathRoll = false
            
            -- Minor kludge: Switch to chase view because
            -- usually the explosion fireballs aren't visible in OTW view.
            sim.SuperView:SetViewMode( sim.SuperView.eViewMode_CHASE )
            
            -- Show death-roll and rescue helicopter.
            -- Don't show helicopter at high altitudes.
            if ( sim.World:IfLowAltitude( PlayerActor:GetObject():GetPosition() ) ) then
                self.showDeathRoll = true
                RescueHelicopter:Start()
            end
            
            -- Update score.
            self:WriteScoreInt( "game_score_crashes", 1 )               
        end
    end
end

-- Schedule simple animation of an Object.
function Game:ScheduleAnimation( actor, stopTime )
    self:AttachActor( actor )
    actor.animationStopTime = stopTime
end

-- (private)
function Game:Animation()
    -- Some Actors had scheduled animation
    -- Such Actors have a "animationStopTime" member.   
    local elapsedTime = sim.Base:GetElapsedTime()
    for actor in KEYS(self.actors) do
        if ( actor.animationStopTime ) then
            if ( elapsedTime < actor.animationStopTime ) then
                self:AnimateActor( actor )
            end
        else
            -- Animation finished, clear member.
            actor.animationStopTime = nil
        end
    end

    -- Death-roll of chase-plane view.
    if ( PlayerActor:IfDestroyed() and self.showDeathRoll ) then
        -- Rotate slowly (player's chase-plane).
        ChaseView.chaseViewpointPlayer:Rotate( sim.ChaseViewpoint.AXIS_CHASE_PLANE_HORZ,
                                               -Keyboard:ChaseViewRotation() * 0.5 )
    end
end

-- (private)
function Game:AnimateActor( actor )
    -- This is very simplistic and limited to land object.
    local object = actor:GetObject()
    local pos = object:GetPosition()
    local inc = -sim.World:Meters2Sim( 0.15 )
    pos[XX] = pos[XX] + inc
    object:SetPosition( pos )
end

-- Update game every tick.
function Game:Tick()
    self:UpdateScore()
    self:Animation()
end

-- Handle event of two Objects colliding.
-- Update score if player has hit an enemy.
function Game:HandleCollision( objectPtr1, objectPtr2 )
    -- Correlate Lua Actor with C++ Object.
    local actor1 = self:MapActorObject( objectPtr1 )
    local actor2 = self:MapActorObject( objectPtr2 )
    if ( actor1 and actor2 ) then
        -- Did player and enemy Actors collide?
        if ( self:IfOppositeTeamNums( actor1:GetTeamNum(), actor2:GetTeamNum() ) ) then
            -- Find the enemy Actor of the pair.
            local enemy = actor1
            if ( actor2:GetTeamNum() == self.TEAM_NUM_ENEMY ) then
                enemy = actor2
            end
            -- Update score (don't repeat).
            if ( not enemy:IfDestroyed() ) then
                self:WriteScoreInt( "game_score_targets_destroyed", 1 )
            end
        end
        actor1:Destroy()
        actor2:Destroy()
    end
end

-- Special-case: Called by C++ core.
-- Name is "sim.HandleCollision" but is part of Game class.
function sim.HandleCollision( objectPtr1, objectPtr2 )
    Game:HandleCollision( objectPtr1, objectPtr2 )
    return 0
end

-- The C++ core broadcasts a DestroyObject event to Lua.
-- Lua turns it into the higher-level UnreferenceActor event.
function Game:HandleDestroyObject( objectPtr )
    -- Is an Actor based on this Object?
    local zombie = self:MapActorObject( objectPtr )
    if ( zombie ) then
        -- Broadcast event.
        self:BroadcastUnreferenceActor( zombie )

        -- Destroy and remove zombie Actor from Game.
        zombie:Destroy()
        self:DetachActor( zombie )
    end
end

function Game:BroadcastUnreferenceActor( zombie )
    -- Propagate to every Actor.
    assert( zombie )
    for actor in KEYS(self.actors) do
        actor:UnreferenceActor( zombie )
    end
end

-- Instantiate.
Game:Init()

----------------------------------------
-- RescueHelicopter class (singleton) --
----------------------------------------

RescueHelicopter = { }

function RescueHelicopter:Start()

    -- Already started?
    if ( self.rescueHelicopter ) then
        return
    end 

    -- Place rescue helicopter above player's burning wreck.
    local pos = PlayerActor:GetObject():GetPosition()
    pos[LAT] = pos[LAT] - sim.World.Meters2Sim(20.0)
    pos[LON] = pos[LON] - sim.World.Meters2Sim(20.0)
    pos[ALT] = pos[ALT] + sim.World.Meters2Sim(20.0)
    self.rescueHelicopter = sim.Aircraft:New( pos,
                                              sim.Aircraft.type_Sikorsky,
                                              sim.ModelCache.LOAD_SHARED )

    -- Play rotor sound.
    sim.Sound:Play( "rotor.wav.gz", sim.Sound.PLAY_LOOP, 0.6 )

    -- AircraftPhysics is N/A to helicopters.
    self.rescueHelicopter:GetPhysics():Enable( false )
    local placement = sim.BeginPlacement()
        self.rescueHelicopter:SetThrottle( 1.0 )
        self.rescueHelicopter:SetCollidable( false )
        self.rescueHelicopter:Rotate( AXIS_YAW, Deg2Rad(270.0) );
        sim.SceneGraph:AttachObject( self.rescueHelicopter )
    sim.EndPlacement( placement )

    -- Change view mode.
    sim.SuperView:SetViewMode( sim.SuperView.eViewMode_CHASE )
    ChaseView.chaseViewpoint = ChaseView.chaseViewpointPlayer
    ChaseView:Reset()
    local distance = sim.World.Meters2Sim( 3.5 * ChaseView.distance )
    ChaseView.chaseViewpoint:SetDistance( 0.0, 0.0, distance )
    ChaseView.chaseViewpoint:Rotate( sim.ChaseViewpoint.AXIS_CHASE_PLANE_VERT,
                                     Deg2Rad(-25.0) )
end

function RescueHelicopter:Stop()
    if ( self.rescueHelicopter ) then
        sim.SceneGraph:DetachObject( self.rescueHelicopter )
        self.rescueHelicopter = nil
    end
end

-- For rescue helicopter to disappear after program reset.
Events:RegisterReset( function() RescueHelicopter:Stop() end )  -- need anon func
