share/hedgewars/Data/Scripts/Multiplayer/Racer.lua
author Wuzzy <Wuzzy@disroot.org>
Wed, 28 Jun 2023 12:01:43 +0200
changeset 15964 d18261ddac4a
parent 15788 acf70c44065b
child 15970 a803428704fd
permissions -rw-r--r--
Fix errors if passing bad script param to [Tech]Racer

--[[
RACER
map-independant racing script
originally by mikade, edited heavily by others

-----------------------------------------
Script parameters:
rounds=N
--> The game will be played with N rounds (default: 3)

waypointradius=N
--> The waypoints have a radius of N pixels (default: 450)

maxwaypoints=N
--> The maximum number of waypoints to be placed (default: 8)

teamrope=true
--> The team will be colored in the color of the team.
-----------------------------------------

DEVELOPER WARNING - FOR OFFICIAL DEVELOPMENT --
Be careful when editig this script, do not introduce changes lightly!
This script is used for time records on the official Hedgewars server.
Introducing breaking changes means we have to invalidate past time records!

]]

-----------------------------
-- SCRIPT BEGINS
-----------------------------

HedgewarsScriptLoad("/Scripts/Locale.lua")
HedgewarsScriptLoad("/Scripts/OfficialChallenges.lua")
HedgewarsScriptLoad("/Scripts/Params.lua")

------------------
-- Got Variables?
------------------

local roundLimit = 3
local roundNumber = 0
local firstClan = 10

local fastX = {}
local fastY = {}
local fastCount = 0
local fastIndex = 0
local fastColour = 0xffffffff

local currX = {}
local currY = {}
local currCount = 0

local specialPointsX = {}
local specialPointsY = {}
local specialPointsCount = 0

local landObjectPoints = {}
local landObjects = {}

local TeamRope = false

local waypointCursor = false
local waypointPreview = nil

local officialChallenge

local ammoDelays

--------------------------
-- hog and team tracking variales
--------------------------

local numhhs = 0 -- store number of hedgehogs
local hhs = {} -- store hedgehog gears

local numTeams --  store the number of teams in the game
local teamNameArr = {}  -- store the list of teams
local teamClan = {}
local teamSize = {}     -- store how many hogs per team
local teamIndex = {} -- at what point in the hhs{} does each team begin

local teamComment = {}
local teamScore = {}

-------
-- racer vars
--------

local cGear = nil
local cameraGear = nil -- gear created to center the cameera on

local bestClan = 10
local bestTime = MAX_TURN_TIME

local gameBegun = false
local gameOver = false
local racerActive = false
local trackTime = 0

local wpCirc = {}
local wpX = {}
local wpY = {}
local wpCol = {}
local wpActive = {}
local wpRad = 450
local WAYPOINT_RADIUS_MIN = 40
local wpCount = 0
local wpLimit = 8

local usedWeapons = {}

local roundN
local lastRound
local RoundHasChanged
local turnSkipped = false

local boostX = 0
local boostY = 0
local boostValue = 1

-- themes with bright background
local brightThemes = {
	Bath = true,
	Bamboo = true,
	Beach = true,
	Blox = true,
	Compost = true,
	Desert = true,
	Fruit = true,
	Golf = true,
	Hoggywood = true,
	Jungle = true,
	Olympics = true,
	Sheep = true,
}
-- themes with medium or heavily mixed brightness.
-- only add themes here if both bright and dark waypoint
-- colors fail otherwise.
local mediumThemes = {
	Halloween = true,
}
-- All themes not explicitly listed above are assumed to
-- be "dark" and work with the default bright waypoints.

-- Waypoint colors in 3 color themes!
-- We do this so the waypoints are easy on the eyes,
-- at least in each of the default themes.

-- Bright waypoints (default)
local waypointColourBright = 0xFFFFFFFF -- Primary colour of inactive waypoints
local waypointColourBrightAtPlacement = 0xAAAAAAFF -- Colour of non-highlighted waypoints while placing
-- Medium bright waypoints
local waypointColourMedium = 0x606060FF
local waypointColourMediumAtPlacement = 0x404040FF
-- Dark waypoints
local waypointColourDark = 0x000000FF
local waypointColourDarkAtPlacement = 0x303030FF

-- Waypoints touched by the players assume the clan color, which is unchanged.
-- Touched waypoints are not important to be visible.

-- Default waypoint colors (only use these color variables in the code below)
local waypointColour = waypointColourBright
local waypointColourAtPlacement = waypointColourBrightAtPlacement

-------------------
-- general methods
-------------------

-- Returns brightness level of background from 1-3.
-- 1 = brightest
function GetBackgroundBrightness()
	-- This just looks at the theme names above.
	-- This code will fail for bright unofficial themes.
	-- TODO: Change how this thing works.
	-- Consider adding a function into the Lua API which looks
	-- up the theme's sky color, so we could use thit instead.
	if brightThemes[Theme] then
		return 1
	elseif mediumThemes[Theme] then
		return 2
	else
		return 3
	end
end

function onParameters()
    parseParams()
    if params["teamrope"] ~= nil then
        TeamRope = true
    end
    if params["rounds"] ~= nil then
        roundLimit = tonumber(params["rounds"])
        if type(roundLimit) ~= "number" then
             roundLimit = 3
        end
        roundLimit = math.max(1, math.floor(roundLimit))
    end
    if params["waypointradius"] ~= nil then
        wpRad = tonumber(params["waypointradius"])
        if type(wpRad) ~= "number" then
             wpRad = 450
        end
        wpRad = math.max(WAYPOINT_RADIUS_MIN, math.floor(wpRad))
    end
    if params["maxwaypoints"] ~= nil then
        wpLimit = tonumber(params["maxwaypoints"])
        if type(wpLimit) ~= "number" then
             wpLimit = 8
        end
        wpLimit = math.max(2, math.floor(wpLimit))
    end
end

function RebuildTeamInfo()


        -- make a list of individual team names
        for i = 0, (TeamsCount-1) do
                teamNameArr[i] = " "
                teamSize[i] = 0
                teamIndex[i] = 0
                teamScore[i] = MAX_TURN_TIME
        end
        numTeams = 0

        for i = 0, (numhhs-1) do

                z = 0
                unfinished = true
                while(unfinished == true) do

                        newTeam = true
                        tempHogTeamName = GetHogTeamName(hhs[i]) -- this is the new name

                        if tempHogTeamName == teamNameArr[z] then
                                newTeam = false
                                unfinished = false
                        end

                        z = z + 1

                        if z == TeamsCount then
                                unfinished = false
                                if newTeam == true then
                                        teamNameArr[numTeams] = tempHogTeamName
                                        numTeams = numTeams + 1
                                end
                        end

                end

        end

        -- find out how many hogs per team, and the index of the first hog in hhs
        for i = 0, (numTeams-1) do
                for z = 0, (numhhs-1) do
                        if GetHogTeamName(hhs[z]) == teamNameArr[i] then
                                teamClan[i] = GetHogClan(hhs[z])
                                if teamSize[i] == 0 then
                                        teamIndex[i] = z -- should give starting index
                                end
                                teamSize[i] = teamSize[i] + 1
                                --add a pointer so this hog appears at i in hhs
                        end
                end

        end

end


-----------------
-- RACER METHODS
-----------------

function onLeft()
	boostX = boostX +boostValue
end
function onLeftUp()
	boostX = boostX -boostValue
end
function onRight()
	boostX = boostX -boostValue
end
function onRightUp()
	boostX = boostX +boostValue
end
function onUp()
	boostY = boostY +boostValue
end
function onUpUp()
	boostY = boostY -boostValue
end
function onDown()
	boostY = boostY -boostValue
end
function onDownUp()
	boostY = boostY +boostValue
end

function CheckWaypoints()

        trackFinished = true

        for i = 0, (wpCount-1) do

                g1X, g1Y = GetGearPosition(CurrentHedgehog)
                g2X, g2Y = wpX[i], wpY[i]

                g1X = g1X - g2X
                g1Y = g1Y - g2Y
                dist = (g1X*g1X) + (g1Y*g1Y)

                NR = (48/100*wpRad)/2

                if dist < (NR*NR) then
                        wpCol[i] = GetClanColor(GetHogClan(CurrentHedgehog))
                        SetVisualGearValues(wpCirc[i], wpX[i], wpY[i], 64, 64, 1, 10, 0, wpRad, 5, wpCol[i])

                        wpRem = 0
                        for k = 0, (wpCount-1) do
                                if wpActive[k] == false then
                                        wpRem = wpRem + 1
                                end
                        end

                        if wpActive[i] == false then
                                local wpMessage = ""
                                if wpRem-1 == 0 then
                                         wpMessage = loc("Track completed!")
                                else
                                         wpMessage = string.format(loc("Waypoints remaining: %d"), wpRem-1)
                                end
                                AddCaption(wpMessage, 0xffba00ff, capgrpGameState)
                        end
                        wpActive[i] = true

                end

                if wpActive[i] == false then
                        trackFinished = false
                end

        end

        return(trackFinished)

end

function AdjustScores()
	bestTimeComment = loc("Did not finish")

        newScore = false

        -- update this clan's time if the new track is better
        for i = 0, (numTeams-1) do
                if teamClan[i] == GetHogClan(CurrentHedgehog) then
                        if trackTime < teamScore[i] then
                                teamScore[i] = trackTime
                                newScore = true
                        else
                                newScore = false
                        end
                end
        end

        -- find the best time out of those so far
        for i = 0, (numTeams-1) do
                if teamScore[i] < bestTime then
                        bestTime = teamScore[i]
                        bestClan = teamClan[i]
                end
        end

        if bestTime ~= MAX_TURN_TIME then
                bestTimeComment = string.format(loc("%.1fs"), (bestTime/1000))
        end

        if newScore == true then
                if trackTime == bestTime then -- best time of the race
                        ShowMission(loc("Racer"),
                        loc("Track completed!"),
                        string.format(loc("New race record: %.1fs"), (trackTime/1000)) .. "|" ..
                        string.format(loc("Winning time: %s"), bestTimeComment), 0, 4000)
                        PlaySound(sndHomerun)
                else    -- best time for the clan
                        ShowMission(loc("Racer"),
                        loc("Track completed!"),
                        string.format(loc("New clan record: %.1fs"), (trackTime/1000)) .. "|" ..
                        string.format(loc("Winning time: %s"), bestTimeComment), 4, 4000)
                end
        else -- not any kind of new score
                ShowMission(loc("Racer"),
                loc("Track completed!"),
                string.format(loc("Time: %.1fs"), (trackTime/1000)) .. "|" ..
                string.format(loc("Winning time: %s"), bestTimeComment), -amSkip, 4000)
                PlaySound(sndHellish)
        end

        for i = 0, (TeamsCount-1) do
                if teamNameArr[i] ~= " " and teamScore[i] ~= MAX_TURN_TIME then
                        SetTeamLabel(teamNameArr[i], string.format(loc("%.1fs"), teamScore[i]/1000))
                end
        end

        if bestTime == trackTime then

                fastColour = GetClanColor(GetHogClan(CurrentHedgehog))

                for i = 0, (currCount-1) do
                        fastX[i] = currX[i]
                        fastY[i] = currY[i]
                end

                fastCount = currCount
                fastIndex = 0

        else
                currCount = 0
                fastIndex = 0
        end


end

function onNewRound()

        roundNumber = roundNumber + 1

        totalComment = ""
        for i = 0, (TeamsCount-1) do
                        if teamNameArr[i] ~= " " and teamScore[i] ~= MAX_TURN_TIME then
                                teamComment[i] = string.format(loc("%s: %.1fs"), teamNameArr[i], (teamScore[i]/1000)) .. "|"
                        else
                                teamComment[i] = string.format(loc("%s: Did not finish"), teamNameArr[i]) .. "|"
                        end
                        totalComment = totalComment .. teamComment[i]
        end

        local icon
        if roundNumber >= roundLimit then
                icon = 0
        else
                icon = 2
        end
        ShowMission(    loc("Racer"),
                                        loc("Status update"),
                                        string.format(loc("Rounds complete: %d/%d"), roundNumber, roundLimit) .. "|" .. " " .. "|" ..
                                        loc("Best team times: ") .. "|" .. totalComment, icon, 4000)

        -- end game if its at round limit
        if roundNumber >= roundLimit then
                -- Sort the scores for the ranking list
                local unfinishedArray = {}
                local sortedTeams = {}
                local k = 1
                local c = 1
                local clanScores = {}
                local previousClan
                for i = 0, TeamsCount-1 do
                        local clan = GetTeamClan(teamNameArr[i])
                        if not clanScores[clan+1] then
	                       clanScores[clan+1] = {}
	                       clanScores[clan+1].index = clan
	                       clanScores[clan+1].score = teamScore[i]
                        end
                        if teamScore[i] ~= MAX_TURN_TIME and teamNameArr[i] ~= " " then
                               sortedTeams[k] = {}
                               sortedTeams[k].name = teamNameArr[i]
                               sortedTeams[k].score = teamScore[i]
	                       sortedTeams[k].clan = clan
                               k = k + 1
                        else
                               table.insert(unfinishedArray, string.format(loc("%s did not finish the race."), teamNameArr[i]))
                        end
                end
                table.sort(sortedTeams, function(team1, team2)
                        if team1.score == team2.score then
                                return team1.clan < team2.clan
                        else
                                return team1.score < team2.score
                        end
                end)
                table.sort(clanScores, function(clan1, clan2) return clan1.score < clan2.score end)
                local rank = 0
                local rankPlus = 0
                local prevScore
                local clanRanks = {}
                for c = 1, #clanScores do
                        rankPlus = rankPlus + 1
                        if clanScores[c].score ~= prevScore then
                                rank = rank + rankPlus
                                rankPlus = 0
                        end
                        prevScore = clanScores[c].score
                        clanRanks[clanScores[c].index] = rank
                end

                -- Write all the stats!
                for i = 1, #sortedTeams do
                        SendStat(siPointType, "!TIME")
			SendStat(siTeamRank, tostring(clanRanks[GetTeamClan(sortedTeams[i].name)]))
                        SendStat(siPlayerKills, sortedTeams[i].score, sortedTeams[i].name)
                end

		local roundDraw = false
		if #clanScores >= 2 and clanScores[1].score == clanScores[2].score and clanScores[1].score ~= MAX_TURN_TIME then
			roundDraw = true
                        SendStat(siGameResult, loc("Round draw"))
                        SendStat(siCustomAchievement, loc("The teams are tied for the fastest time."))
                elseif #sortedTeams >= 1 then
                        SendStat(siGameResult, string.format(loc("%s wins!"), sortedTeams[1].name))
                        SendStat(siCustomAchievement, string.format(loc("%s wins with a best time of %.1fs."), sortedTeams[1].name, (sortedTeams[1].score/1000)))
                        for i=1,#unfinishedArray do
                                 SendStat(siCustomAchievement, unfinishedArray[i])
                        end
                else
			roundDraw = true
                        SendStat(siGameResult, loc("Round draw"))
                        SendStat(siCustomAchievement, loc("Nobody managed to finish the race. What a shame!"))
                        if specialPointsCount > 0 then
                                SendStat(siCustomAchievement, loc("Maybe you should try an easier map next time."))
                        else
                                SendStat(siCustomAchievement, loc("Maybe you should try easier waypoints next time."))
                        end
                end

		-- Kill all the losers
		for i = 0, (numhhs-1) do
			if GetHogClan(hhs[i]) ~= bestClan or roundDraw then
				SetEffect(hhs[i], heResurrectable, 0)
				SetHealth(hhs[i],0)
			elseif not roundDraw then
				SetEffect(hhs[i], heInvulnerable, 1)
			end
		end

		gameOver = true
                for i=0, wpCount-1 do
                         -- Fade out waypoints
                         SetVisualGearValues(wpCirc[i], nil, nil, 0, 0, nil, 6)
                end
		EndTurn(true)

        end

end

function CheckForNewRound()

        if GetHogClan(CurrentHedgehog) == firstClan then
                onNewRound()
        end

end

function DisableTumbler(endTurn)
        if endTurn == nil then endTurn = true end
        if racerActive then
                currCount = 0
                fastIndex = 0
                if endTurn then
                        EndTurn(true)
                end
                racerActive = false -- newadd

		if trackFinished and not gameOver then
                         for i=0, wpCount-1 do
                       	         SetVisualGearValues(wpCirc[i], nil, nil, 255, 255, nil, 2)
                         end
                elseif not gameOver then
                         for i=0, wpCount-1 do
                       	         SetVisualGearValues(wpCirc[i], nil, nil, 32, 32, nil, 1)
                         end
                end
        end
end

function HandleGhost()

        -- get the current xy of the racer at this point
        currX[currCount] = GetX(CurrentHedgehog)
        currY[currCount] = GetY(CurrentHedgehog)
        currCount = currCount + 1

        -- draw a ping of smoke where the fastest player was at this point
        if (fastCount ~= 0) and (fastIndex < fastCount) then

                fastIndex = fastIndex + 1

                local tempE = AddVisualGear(fastX[fastIndex], fastY[fastIndex], vgtSmoke, 0, false)
                SetVisualGearValues(tempE, nil, nil, nil, nil, nil, nil, nil, nil, nil, fastColour )

        end

end

function TryRepositionHogs()

	if MapHasBorder() == true then

		for i = 0, (numhhs-1) do
			if hhs[i] ~= nil then
				SetGearPosition(hhs[i],GetX(hhs[i]), TopY-10)
			end
		end

	end

end

----------------------------------
-- GAME METHODS / EVENT HANDLERS
----------------------------------

function onGameInit()
        EnableGameFlags(gfInfAttack, gfSolidLand)
        -- Force-disable various game flags that would break the script
        DisableGameFlags(gfKing, gfSwitchHog, gfAISurvival, gfPlaceHog, gfTagTeam)
        CaseFreq = 0
        WaterRise = 0
        HealthDecrease = 0
end

function InstructionsBuild()
        ShowMission(
                loc("Racer"),
                loc("A Hedgewars mini-game"),
                loc("Build a track and race.") .. "|" ..
                string.format(loc("Round limit: %d"), roundLimit),
                4, 4000)
end

function InstructionsRace()
        ShowMission(loc("Racer"),
        	loc("A Hedgewars mini-game"),
        	loc("Touch all waypoints as fast as you can!"),
		1, 4000)
end

function onGameStart()

	-- Adjust pre-defined waypoints in scaled drawn maps
	if MapGen == mgDrawn and MapFeatureSize ~= 12 and specialPointsCount > 0 then
		local landW = RightX - LeftX + 1
		local landH = LAND_HEIGHT - TopY
		-- Reposition pre-defined waypoints
        	for i = 0, (specialPointsCount-1) do
        		specialPointsX[i] = LeftX + div(specialPointsX[i] * landW, 4096)
        		specialPointsY[i] = TopY + div(specialPointsY[i] * landH, 2048)
		end
		-- Scale waypoint size
		wpRad = math.max(WAYPOINT_RADIUS_MIN, div(wpRad * landW, 4096))
	end

	if ClansCount >= 2 then
		SendGameResultOff()
		SendRankingStatsOff()
        	SendHealthStatsOff()
		SendAchievementsStatsOff()
	end

        -- Keep track of land objects that got placed by the scheme (mines, air mines, barrels)
        for id, _ in pairs(landObjects) do
                table.insert(landObjectPoints, { type = GetGearType(id), x = GetX(id), y = GetY(id) })
        end

        SetSoundMask(sndIncoming, true)
        SetSoundMask(sndMissed, true)

        roundN = 0
        lastRound = TotalRounds
        RoundHasChanged = false
        officialChallenge = detectMapWithDigest()

	if GetBackgroundBrightness() == 1 then
		-- Dark waypoint colour theme
		waypointColour = waypointColourDark
		waypointColourAtPlacement = waypointColourDarkAtPlacement
	elseif GetBackgroundBrightness() == 2 then
		-- Medium waypoint colour theme
		waypointColour = waypointColourMedium
		waypointColourAtPlacement = waypointColourMediumAtPlacement
	end

        for i = 0, (specialPointsCount-1) do
                PlaceWayPoint(specialPointsX[i], specialPointsY[i], false)
        end

        RebuildTeamInfo()

        if specialPointsCount > 0 then
                InstructionsRace()
        else
                InstructionsBuild()
        end

        SetAmmoTexts(amAirAttack, loc("Place waypoint"), loc("Racer tool"),
                loc("Build an awesome race track by placing|waypoints which the hedgehogs have to|touch in any order to finish a round.") .. "|" ..
		loc("Hedgehogs will start in the first waypoint.") .. "|" ..
 		loc("Cursor: Place waypoint") .. "|" ..
 		loc("Precise: Remove previous waypoint"))

        SetAmmoTexts(amSkip, loc("Finish waypoint placement"), loc("Racer tool"),
                loc("Happy with your race track?|Then stop building and start racing!") .. "|" ..
                loc("Or let the next player place waypoints|if less than 2 waypoints have been placed.") .. "|" ..
                loc("Attack: Activate"))

        TryRepositionHogs()

end

function PlaceWayPoint(x,y,placedByUser)
    if not racerActive then
        if wpCount == 0 or wpX[wpCount - 1] ~= x or wpY[wpCount - 1] ~= y then

            wpX[wpCount] = x
            wpY[wpCount] = y
            wpCol[wpCount] = waypointColour
            wpCirc[wpCount] = AddVisualGear(wpX[wpCount],wpY[wpCount],vgtCircle,0,true)

            local flashing, minO, maxO
            if wpCount == 0 then
                -- First waypoint flashes. Useful to know since this is the spawn position.
                minO, maxO = 164, 255
                flashing = 5
            else
                -- Other waypoints are not animated (before the race starts)
                minO, maxO = 255, 255
                flashing = 0
            end
            SetVisualGearValues(wpCirc[wpCount], wpX[wpCount], wpY[wpCount], minO, maxO, 1, flashing, 0, wpRad, 5, wpCol[wpCount])

            -- Use alternate waypoint color for all waypoints but the last one. This gives a subtle “highlighting” effect.
            SetVisualGearValues(wpCirc[wpCount-1], nil, nil, nil, nil, nil, nil, nil, nil, nil, waypointColourAtPlacement)

            wpCount = wpCount + 1

            if placedByUser then
                AddCaption(string.format(loc("Waypoint placed. Available points remaining: %d"), wpLimit-wpCount))
                PlaySound(sndPlaced)
            end
        end
    end
end

function onPrecise()
    if not racerActive and CurrentHedgehog ~= nil and GetCurAmmoType() == amAirAttack then
        DeletePreviousWayPoint()
    end
end

function DeletePreviousWayPoint()
    if wpCount > 0 then
        wpCount = wpCount - 1
        wpX[wpCount] = nil
        wpY[wpCount] = nil
        wpCol[wpCount] = nil
        DeleteVisualGear(wpCirc[wpCount])
        wpCirc[wpCount] = nil
        SetVisualGearValues(wpCirc[wpCount-1], nil, nil, nil, nil, nil, nil, nil, nil, nil, waypointColour)
        AddCaption(string.format(loc("Waypoint removed. Available points: %d"), wpLimit-wpCount))
        PlaySound(sndBump)
    else
        PlaySound(sndDenied)
        AddCaption(loc("No waypoint to be removed!"))
    end
end

function onSpecialPoint(x,y,flag)
    if flag == 99 then
        fastX[fastCount] = x
        fastY[fastCount] = y
        fastCount = fastCount + 1
    else
        addHashData(x)
        addHashData(y)
        addHashData(flag)
        specialPointsX[specialPointsCount] = x
        specialPointsY[specialPointsCount] = y
        specialPointsCount = specialPointsCount + 1
    end
end

function onNewTurn()

        CheckForNewRound()
        TryRepositionHogs()

        racerActive = false
        turnSkipped = false

        trackTime = 0

        currCount = 0 -- hopefully this solves problem
        fastIndex = 0
        AddAmmo(CurrentHedgehog, amAirAttack, 0)
        gTimer = 0

        SetSoundMask(sndStupid, false)
        SetSoundMask(sndBugger, false)
        SetSoundMask(sndDrat, false)

        -- Remember ammo delays for later
        if ammoDelays == nil then
                ammoDelays = {}
                for a=0, AmmoTypeMax do
                        local _, _, delay = GetAmmo(a)
                        -- delay >= 10000 is special value used in hog placement phase.
                        -- This extracts the "true" delay
                        if delay >= 10000 then
                                delay = delay - 10000
                        end
                        ammoDelays[a] = delay
                end
        end

        -- Handle Starting Stage of Game
        if (gameOver == false) and (gameBegun == false) then
                if wpCount >= 2 then
                        gameBegun = true
                        roundNumber = 0
                        firstClan = GetHogClan(CurrentHedgehog)
                        if specialPointsCount == 0 then
                                InstructionsRace()
                        end

                        -- Restore old ammo delays
                        for a=0, AmmoTypeMax do
                                if a ~= amAirAttack and a ~= amSkip then
                                        SetAmmoDelay(a, ammoDelays[a])
                                end
                        end

                        SetAmmoTexts(amSkip, nil, nil, nil)
                else
                        local infoString
                        if wpLimit > 2 then
                                infoString = string.format(loc("Place 2-%d waypoints using the waypoint placement tool."), wpLimit)
                        else
                                infoString = loc("Place 2 waypoints using the waypoint placement tool.")
                        end
                        ShowMission(loc("Racer"),
                        loc("Waypoint placement phase"), infoString, -amAirAttack, 4000)
                        AddAmmo(CurrentHedgehog, amAirAttack, AMMO_INFINITE)
                        for a=0, AmmoTypeMax do
                                if a ~= amAirAttack and a ~= amSkip then
                                        SetAmmoDelay(a, 9999)
                                end
                        end
                        SetWeapon(amAirAttack)
                        -- Bots skip waypoint placement
                        if GetHogLevel(CurrentHedgehog) ~= 0 then
                                SkipTurn()
                        end
                end
        end

        if gameBegun and not gameOver then

                -- Reset land objects so each player starts with same racing conditions
                for id,_ in pairs(landObjects) do
                        DeleteGear(id)
                end
                for i=1, #landObjectPoints do
                        AddGear(landObjectPoints[i].x, landObjectPoints[i].y, landObjectPoints[i].type, 0, 0, 0, 0)
                end

                -- Set the waypoints to unactive
                for i = 0,(wpCount-1) do
                        wpActive[i] = false
                        wpCol[i] = waypointColour
                        local flashing, minO, maxO
                        if i == 0 then
                            -- Make first waypoint flash very noticably
                            minO, maxO = 92, 255
                            flashing = 2
                        else
                            minO, maxO = 164, 224
                            flashing = 10
                        end
                        SetVisualGearValues(wpCirc[i], nil, nil, minO, maxO, nil, flashing, nil, nil, nil, wpCol[i])
                end

                if cameraGear then
                        DeleteGear(cameraGear)
                end
                -- Move camera to first waypoint
		-- We use a dummy gear to feed FollowGear. It does not affect the race.
                cameraGear = AddGear(wpX[0], wpY[0], gtGenericFaller, 0, 0, 0, 5000)
                SetState(cameraGear, bor(GetState(cameraGear), gstNoGravity+gstInvisible))
                FollowGear(cameraGear)
        end

        if gameOver == true then
                gameBegun = false
                racerActive = false -- newadd
        end

        AddAmmo(CurrentHedgehog, amTardis, 0)
        AddAmmo(CurrentHedgehog, amResurrector, 0)
        AddAmmo(CurrentHedgehog, amInvulnerable, 0)
        AddAmmo(CurrentHedgehog, amDrillStrike, 0)
        AddAmmo(CurrentHedgehog, amMineStrike, 0)
        AddAmmo(CurrentHedgehog, amNapalm, 0)
        AddAmmo(CurrentHedgehog, amPiano, 0)
        AddAmmo(CurrentHedgehog, amSwitch, 0)
        AddAmmo(CurrentHedgehog, amKamikaze, 0)
        AddAmmo(CurrentHedgehog, amIceGun, 0)
        SetAmmoDelay(amAirAttack, 0)
end

function onGameTick20()

        -- airstrike detected, convert this into a potential waypoint spot
        if cGear ~= nil then
                local x,y = GetGearPosition(cGear)
                if x > -9000 then
                        local x,y = GetGearTarget(cGear)


                        if TestRectForObstacle(x-20, y-20, x+20, y+20, true) then
                                AddCaption(loc("Please place the waypoint in the air, within the map boundaries"))
                                PlaySound(sndDenied)
                        elseif (y > WaterLine-50) then
                                AddCaption(loc("Please place the waypoint further away from the waterline"))
                                PlaySound(sndDenied)
                        else
                                PlaceWayPoint(x, y, true)
                                if wpCount == wpLimit then
                                        AddCaption(loc("Race complexity limit reached"))
                                        EndTurn(true)
                                end
                        end
                else
                        DeleteGear(cGear)
                end
        	SetGearPosition(cGear, -10000, y)
        end


        -- start the player tumbling with a boom once their turn has actually begun
        if racerActive == false then

                if (TurnTimeLeft > 0) and (TurnTimeLeft ~= TurnTime) then

                        -- if the gamehas started put the player in the middle of the first
                        --waypoint that was placed
                        if gameBegun == true then
                                AddCaption(loc("Good to go!"))
                                racerActive = true
                                trackTime = 0

                                SetGearPosition(CurrentHedgehog, wpX[0], wpY[0])
                                Explode(GetX(CurrentHedgehog)+boostX,
                                        GetY(CurrentHedgehog)+boostY,
                                        50,
                                        EXPLNoDamage + EXPLAutoSound)
                                FollowGear(CurrentHedgehog)

                                HideMission()

                                -- don't start empty-handed
                                if (GetCurAmmoType() == amNothing) then
                                        SetNextWeapon()
                                end
                        else
                                -- still in placement mode
                        end

        	end
		if not racerActive and not gameBegun and GetCurAmmoType() == amAirAttack then
			waypointCursor = true
		else
			waypointCursor = false
		end
	else
		waypointCursor = false
	end

        -- has the player started?
        if (CurrentHedgehog ~= nil) then

                --airstrike conversion used to be here

                -- if the RACE has started, show tracktimes and keep tabs on waypoints
                if (racerActive == true) and (gameBegun == true) then

                        --ghost
                        if GameTime%40 == 0 then
                                HandleGhost()
                        end

                        trackTime = trackTime + 20

                        if GameTime%100 == 0 then

                                AddCaption(string.format(loc("Time: %.1fs"), (trackTime/1000)),GetClanColor(GetHogClan(CurrentHedgehog)),capgrpMessage2)

				-- Track completed, all waypoints touched
                                if (CheckWaypoints() == true) then
                                        SetSoundMask(sndStupid, true)
                                        SetSoundMask(sndBugger, true)
                                        SetSoundMask(sndDrat, true)
                                        AdjustScores()
                                        SetEffect(CurrentHedgehog, heInvulnerable, 1)
                                        DisableTumbler()
                                end

                        end

                end

                -- if the player has expended their time, stop
                if TurnTimeLeft <= 20 and not turnSkipped then
                        DisableTumbler()
                end

        end

end

function onGameTick()
	if waypointCursor then
		if not waypointPreview then
			waypointPreview = AddVisualGear(CursorX, CursorY, vgtCircle, 0, true)
		end
		SetVisualGearValues(waypointPreview, CursorX, CursorY, 200, 200, 0, 0, 0, div(wpRad, 5), 5, waypointColourAtPlacement)
	else
		if waypointPreview then
			DeleteVisualGear(waypointPreview)
			waypointPreview = nil
		end
	end
end

function onGearDamage(gear)

        if gear == CurrentHedgehog then
                DisableTumbler(false)
        end

end

function onGearResurrect(gear, vGear)

        if gear == CurrentHedgehog then
                DisableTumbler(false)
        end
        if vGear then
                DeleteVisualGear(vGear)
        end

end

function onGearAdd(gear)
        local gt = GetGearType(gear)
        if gt == gtHedgehog then
                hhs[numhhs] = gear
                numhhs = numhhs + 1
                SetEffect(gear, heResurrectable, 1)
        elseif gt == gtAirAttack then
                cGear = gear
		local x,y = GetGearPosition(cGear)
        	SetGearPosition(cGear, 10000, y)
        elseif (gt == gtMine or gt == gtAirMine or gt == gtExplosives) then
                landObjects[gear] = true
        elseif (not gameBegun) and gt == gtAirBomb then
		DeleteGear(gear)
        elseif gt == gtRope and TeamRope then
            SetTag(gear,1)
            SetGearValues(gear,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,GetClanColor(GetHogClan(CurrentHedgehog)))
        end
end

function onGearDelete(gear)

        if GetGearType(gear) == gtAirAttack then
                cGear = nil
        elseif landObjects[gear] == true then
                landObjects[gear] = nil
        elseif gear == cameraGear then
                cameraGear = nil
        end

end

function onAttack()
    at = GetCurAmmoType()

    usedWeapons[at] = 0
end

function onHogAttack(ammoType)
    if ammoType == amSkip then
        turnSkipped = true
    end
end

function onSkipTurn()
    turnSkipped = true
end

function onAchievementsDeclaration()
    usedWeapons[amSkip] = nil
    usedWeapons[amExtraTime] = nil

    usedRope = usedWeapons[amRope] ~= nil
    usedPortal = usedWeapons[amPortalGun] ~= nil
    usedSaucer = usedWeapons[amJetpack] ~= nil

    usedWeapons[amNothing] = nil
    usedWeapons[amRope] = nil
    usedWeapons[amPortalGun] = nil
    usedWeapons[amJetpack] = nil

    usedOther = next(usedWeapons) ~= nil

    if usedOther then -- smth besides nothing, skip, rope, portal or saucer used
        raceType = "unknown race"
    elseif usedRope and not usedPortal and not usedSaucer then
        raceType = "rope race"
    elseif not usedRope and usedPortal and not usedSaucer then
        raceType = "portal race"
    elseif not usedRope and not usedPortal and usedSaucer then
        raceType = "saucer race"
    elseif (usedRope or usedPortal or usedSaucer or usedOther) == false then -- no weapons used at all?
        raceType = "no tools race"
    else -- at least two of rope, portal and saucer used
        raceType = "mixed race"
    end

    for i = 0, (numTeams-1) do
        if teamScore[i] < MAX_TURN_TIME then
            DeclareAchievement(raceType, teamNameArr[i], officialChallenge, teamScore[i])
        end
    end

    if officialChallenge ~= nil and fastCount > 0 then
        StartGhostPoints(fastCount)

        for i = 0, (fastCount - 1) do
            DumpPoint(fastX[i], fastY[i])
        end
    end
end