share/hedgewars/Data/Scripts/Multiplayer/TechRacer.lua
author nemo
Fri, 20 May 2022 17:24:14 -0400
changeset 15851 9bb85e43e492
parent 15788 acf70c44065b
permissions -rw-r--r--
add more checks on nil current hedgehog gear, remove checks on nil current hedgehog which should never happen.

--------------
-- TECH RACER
--------------

-- 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!

--------------
-- TO DO
--------------
-- allow scrolling of maps (was going to add this in the engine itself, but it can be done now by refreshing preview)

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

HedgewarsScriptLoad("/Scripts/Locale.lua")
HedgewarsScriptLoad("/Scripts/OfficialChallenges.lua")
HedgewarsScriptLoad("/Scripts/Tracker.lua")
HedgewarsScriptLoad("/Scripts/Params.lua")
HedgewarsScriptLoad("/Scripts/Utils.lua")
HedgewarsScriptLoad("/Scripts/TechMaps.lua")

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

local atkArray = {
	{amBazooka, 	"amBazooka",		0},
	{amBee, 	"amBee",		0},
	{amMortar, 	"amMortar",		0},
	{amDrill, 	"amDrill",		0},
	{amSnowball, 	"amSnowball",		0},
	{amGrenade,	"amGrenade",		0},
	{amClusterBomb,	"amClusterBomb",	0},
	{amMolotov, 	"amMolotov",		0},
	{amWatermelon, 	"amWatermelon",		0},
	{amHellishBomb,	"amHellishBomb",	0},
	{amGasBomb, 	"amGasBomb",		0},

	{amShotgun,	"amShotgun",		0},
	{amDEagle,	"amDEagle",		0},
	{amFlamethrower,"amFlamethrower",	0},
	{amSniperRifle,	"amSniperRifle",	0},
	{amSineGun, 	"amSineGun",		0},
	{amIceGun, 	"amIceGun",		0},
	{amLandGun,	"amLandGun",		0},

	{amFirePunch, 	"amFirePunch",		0},
	{amWhip,	"amWhip",		0},
	{amBaseballBat, "amBaseballBat",	0},
	{amKamikaze, 	"amKamikaze",		0},
	{amSeduction, 	"amSeduction",		0},
	{amHammer,	"amHammer",		0},

	{amMine, 	"amMine",		0},
	{amDynamite, 	"amDynamite",		0},
	{amCake, 	"amCake",		0},
	{amBallgun, 	"amBallgun",		0},
	{amRCPlane,	"amRCPlane",		0},
	{amSMine,	"amSMine",		0},
	{amAirMine,	"amAirMine",		0},

	{amAirAttack,	"amAirAttack",		0},
	{amMineStrike,	"amMineStrike",		0},
	{amDrillStrike,	"amDrillStrike",	0},
	{amAirMine,	"amAirMine",		0},
	{amNapalm, 	"amNapalm",		0},
	{amPiano,	"amPiano",		0},

	{amKnife,	"amKnife",		0},

	{amBirdy,	"amBirdy",		0}
}

local utilArray = {
	{amBlowTorch, 	"amBlowTorch",		0},
	{amPickHammer,	"amPickHammer",		0},
	{amGirder, 	"amGirder",		0},
	{amRubber, 	"amRubber",		0},
	{amPortalGun,	"amPortalGun",		0},

	{amRope, 	"amRope",		0},
	{amParachute, 	"amParachute",		0},
	{amTeleport,	"amTeleport",		0},
	{amJetpack,	"amJetpack",		0},

	{amInvulnerable,"amInvulnerable",	0},
	{amLaserSight,	"amLaserSight",		0},
	{amVampiric,	"amVampiric",		0},

	{amLowGravity, 	"amLowGravity",		0},
	{amExtraDamage, "amExtraDamage",	0},
	{amExtraTime,	"amExtraTime",		0},

	{amResurrector, "amResurrector",	0},
	{amTardis, 	"amTardis",		0},

	{amSwitch,	"amSwitch",		0}
}

local activationStage = 0
local jet = nil
portalDistance = 5000
ufoFuel = 0
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 specialPointsFlag = {}
local specialPointsCount = 0

mapID = nil

--------------------------
-- 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

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 wpCount = 0
local wpLimit = 20

local usedWeapons = {}

local roundN
local lastRound
local RoundHasChanged

local cnthhs = 0

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

function RebuildTeamInfo()

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

	for i = 0, (numhhs-1) do

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

			local newTeam = true
			local 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
-----------------

-- Returns min opacity, max opacity and flashing speed (`FrameTicks`)
-- for the waypoint visual gears
function FlashingHelper(wpIndex)
	local minO, maxO, flashing
	if wpIndex == 0 then
		-- Notable flashing of first waypoint
		minO, maxO = 92, 255
		flashing = 2
	else
		-- Slow pulsation
		minO, maxO = 164, 224
		flashing = 10
	end
	return minO, maxO, flashing
end

function CheckWaypoints()

	trackFinished = true

	for i = 0, (wpCount-1) do

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

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

		local NR = (48/100*wpRad)/2

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

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

			if not wpActive[i] 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()

	local bestTimeComment = loc("Did not finish")

	local 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("TechRacer"),
			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("TechRacer"),
			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("TechRacer"),
			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

	local totalComment = ""
	for i = 0, (TeamsCount-1) do
		if teamNameArr[i] ~= " " then
			if 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]
		elseif teamNameArr[i] == " " then
			teamComment[i] = "|"
		end
	end

	local icon
	if roundNumber >= roundLimit then
		icon = 0
	else
		icon = 2
	end
	ShowMission(loc("TechRacer"),
		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!"))
			SendStat(siCustomAchievement, loc("Maybe you should try an easier TechRacer map."))
		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)
			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
		racerActive = false -- newadd
		if endTurn then
			 EndTurn(true)
		end
		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 BoomGirder(x,y,rot)
	local girTime = 1
	if rot < 4 then
		AddGear(x, y, gtGrenade, 0, 0, 0, girTime)
	elseif rot == 4 then
		AddGear(x-45, y, gtGrenade, 0, 0, 0, girTime) -- needed?
		AddGear(x-30, y, gtGrenade, 0, 0, 0, girTime)
		AddGear(x, y, gtGrenade, 0, 0, 0, girTime) -- needed?
		AddGear(x+30, y, gtGrenade, 0, 0, 0, girTime)
		AddGear(x+45, y, gtGrenade, 0, 0, 0, girTime) -- needed?
	elseif rot == 5 then ------- diag
		AddGear(x+45, y+45, gtGrenade, 0, 0, 0, girTime) --n
		AddGear(x+30, y+30, gtGrenade, 0, 0, 0, girTime)
		AddGear(x, y, gtGrenade, 0, 0, 0, girTime) -- needed?
		AddGear(x-30, y-30, gtGrenade, 0, 0, 0, girTime)
		AddGear(x-45, y-45, gtGrenade, 0, 0, 0, girTime) --n
	elseif rot == 6 then
		AddGear(x, y-45, gtGrenade, 0, 0, 0, girTime) -- needed?
		AddGear(x, y+30, gtGrenade, 0, 0, 0, girTime)
		AddGear(x, y, gtGrenade, 0, 0, 0, girTime) -- needed?
		AddGear(x, y-30, gtGrenade, 0, 0, 0, girTime)
		AddGear(x, y+45, gtGrenade, 0, 0, 0, girTime) -- needed?
	elseif rot == 7 then -------
		AddGear(x+45, y-45, gtGrenade, 0, 0, 0, girTime) --n
		AddGear(x+30, y-30, gtGrenade, 0, 0, 0, girTime)
		AddGear(x, y, gtGrenade, 0, 0, 0, girTime) -- needed?
		AddGear(x-30, y+30, gtGrenade, 0, 0, 0, girTime)
		AddGear(x-45, y+45, gtGrenade, 0, 0, 0, girTime) --n
	end
end

function RemoveGear(gear)
	if (isATrackedGear(gear) == true) and (GetGearType(gear) ~= gtHedgehog) then
		DeleteGear(gear)
	end
end

function ClearMap()
	runOnGears(RemoveGear)
end

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

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

			local minO, maxO, flashing = FlashingHelper(wpCount)
			-- Make first waypoint flash very noticably before the hog starts racing
			SetVisualGearValues(wpCirc[wpCount], wpX[wpCount], wpY[wpCount], minO, maxO, 1, flashing, 0, wpRad, 5, wpCol[wpCount])

			wpCount = wpCount + 1
		end
	end
end



function HandleFreshMapCreation()

	if activationStage ~= 1 then
		return
	end

	-- the boom stage, boom girders, reset ammo, and delete other map objects
	ClearMap()

	-- the creation stage, place girders and needed gears, grant ammo
	InterpretPoints()

	-- these are from onParameters()
	if (mapID == nil) or (mapID == 0) then
		LoadMap(2000)
	else
		LoadMap(mapID)
	end

	if gameOver then
		for i = 0,(wpCount-1) do
			SetVisualGearValues(wpCirc[wpCount], wpX[wpCount], wpY[wpCount], 164, 224, 1, 10, 0, wpRad, 5, wpCol[wpCount])
		end

	else
		for i = 0,(wpCount-1) do
			DeleteVisualGear(wpCirc[i])
		end
		wpCount = 0

		for i = 1, techCount-1 do
			CallBob(techX[i],techY[i])
		end
	end

	if ufoFuel == 2000 then
		SetAmmoDescriptionAppendix(amJetpack, loc("On this map you get infinite fuel."))
	elseif ufoFuel ~= nil and ufoFuel ~= 0 then
		SetAmmoDescriptionAppendix(amJetpack, string.format(loc("On this map you get %d%% fuel."), div(ufoFuel, 20)))
	end

	activationStage = 200

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 onParameters()

	parseParams()
	mapID = tonumber(params["m"])

	roundLimit = tonumber(params["rounds"])

	if (roundLimit == 0) or (roundLimit == nil) then
		roundLimit = 3
	end

	if mapID == nil then
		mapID = 2 + GetRandom(7)
	end

end

function onGameInit()

	TemplateFilter = 0

	if MapGen == mgDrawn then
		eraseMap(false)
	else
		MapGen = mgDrawn
	end
	MapFeatureSize = 12

	if mapID == nil then
		mapID = 2 + GetRandom(7)
	end

	addHashData(mapID)

	Theme = "Cave"
	Map = ""

	EnableGameFlags(gfInfAttack, gfDisableWind, gfBorder)
	DisableGameFlags(gfSolidLand)
	DisableGameFlags(gfKing, gfSwitchHog, gfAISurvival, gfPlaceHog, gfTagTeam)
	CaseFreq = 0
	TurnTime = 90000
	WaterRise = 0
	HealthDecrease = 0

	for x = 1, 16 do
		AddPoint(x*100,100,5)
	end

	FlushPoints()

end

function limitHogs(gear)

	cnthhs = cnthhs + 1
	if cnthhs > 1 then
		DeleteGear(gear)
	end

end

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

function InterpretPoints()

	-- flags run from 0 to 127
	for i = 0, (specialPointsCount-1) do

		-- Mines
		if specialPointsFlag[i] == 1 then
			SetTimer(AddGear(specialPointsX[i], specialPointsY[i], gtMine, 0, 0, 0, 0), 1)
		elseif specialPointsFlag[i] == 2 then
			SetTimer(AddGear(specialPointsX[i], specialPointsY[i], gtMine, 0, 0, 0, 0), 1000)
		elseif specialPointsFlag[i] == 3 then
			SetTimer(AddGear(specialPointsX[i], specialPointsY[i], gtMine, 0, 0, 0, 0), 2000)
		elseif specialPointsFlag[i] == 4 then
			SetTimer(AddGear(specialPointsX[i], specialPointsY[i], gtMine, 0, 0, 0, 0), 3000)
		elseif specialPointsFlag[i] == 5 then
			SetTimer(AddGear(specialPointsX[i], specialPointsY[i], gtMine, 0, 0, 0, 0), 4000)
		elseif specialPointsFlag[i] == 6 then
			SetTimer(AddGear(specialPointsX[i], specialPointsY[i], gtMine, 0, 0, 0, 0), 5000)

		-- Sticky Mines
		elseif specialPointsFlag[i] == 7 then
			AddGear(specialPointsX[i], specialPointsY[i], gtSMine, 0, 0, 0, 0)

		-- Air Mines
		elseif specialPointsFlag[i] == 8 then
			AddGear(specialPointsX[i], specialPointsY[i], gtAirMine, 0, 0, 0, 0)

		-- Health Crates
		elseif specialPointsFlag[i] == 9 then
			SetHealth(SpawnHealthCrate(specialPointsX[i],specialPointsY[i]),25)
		elseif specialPointsFlag[i] == 10 then
			SetHealth(SpawnHealthCrate(specialPointsX[i],specialPointsY[i]),50)
		elseif specialPointsFlag[i] == 11 then
			SetHealth(SpawnHealthCrate(specialPointsX[i],specialPointsY[i]),75)
		elseif specialPointsFlag[i] == 12 then
			SetHealth(SpawnHealthCrate(specialPointsX[i],specialPointsY[i]),100)

		-- Cleaver
		elseif specialPointsFlag[i] == 13 then
			AddGear(specialPointsX[i], specialPointsY[i], gtKnife, 0, 0, 0, 0)

		-- Target
		elseif specialPointsFlag[i] == 14 then
			AddGear(specialPointsX[i], specialPointsY[i], gtTarget, 0, 0, 0, 0)

		--Barrels
		elseif specialPointsFlag[i] == 15 then
			SetHealth(AddGear(specialPointsX[i], specialPointsY[i], gtExplosives, 0, 0, 0, 0),1)
		elseif specialPointsFlag[i] == 16 then
			SetHealth(AddGear(specialPointsX[i], specialPointsY[i], gtExplosives, 0, 0, 0, 0),25)
		elseif specialPointsFlag[i] == 17 then
			SetHealth(AddGear(specialPointsX[i], specialPointsY[i], gtExplosives, 0, 0, 0, 0),50)
		elseif specialPointsFlag[i] == 18 then
			SetHealth(AddGear(specialPointsX[i], specialPointsY[i], gtExplosives, 0, 0, 0, 0),75)
		elseif specialPointsFlag[i] == 19 then
			SetHealth(AddGear(specialPointsX[i], specialPointsY[i], gtExplosives, 0, 0, 0, 0),100)

		-- There are about 58+- weps / utils
		-- Weapon Crates
		elseif (specialPointsFlag[i] >= 20) and (specialPointsFlag[i] < (#atkArray+20)) then
			SpawnAmmoCrate(specialPointsX[i],specialPointsY[i],atkArray[specialPointsFlag[i]-19][1])


		-- Utility Crates
		elseif (specialPointsFlag[i] >= (#atkArray+20)) and (specialPointsFlag[i] < (#atkArray+20+#utilArray)) then
			SpawnUtilityCrate(specialPointsX[i],specialPointsY[i],utilArray[specialPointsFlag[i]-19-#atkArray][1])

		--79-82 (reserved for future wep crates)
		--89,88,87,86 and 85,84,83,82 (reserved for the 2 custom sprites and their landflags)

		--90-99 reserved for scripted structures

		elseif specialPointsFlag[i] == 98 then
			portalDistance = specialPointsX[i]
			ufoFuel = specialPointsY[i]

		-- Normal Girders
		elseif specialPointsFlag[i] == 100 then
			PlaceSprite(specialPointsX[i], specialPointsY[i], sprAmGirder, 0, 4294967295, nil, nil, nil, lfNormal)
		elseif specialPointsFlag[i] == 101 then
			PlaceSprite(specialPointsX[i], specialPointsY[i], sprAmGirder, 1, 4294967295, nil, nil, nil, lfNormal)
		elseif specialPointsFlag[i] == 102 then
			PlaceSprite(specialPointsX[i], specialPointsY[i], sprAmGirder, 2, 4294967295, nil, nil, nil, lfNormal)
		elseif specialPointsFlag[i] == 103 then
			PlaceSprite(specialPointsX[i], specialPointsY[i], sprAmGirder, 3, 4294967295, nil, nil, nil, lfNormal)
		elseif specialPointsFlag[i] == 104 then
			PlaceSprite(specialPointsX[i], specialPointsY[i], sprAmGirder, 4, 4294967295, nil, nil, nil, lfNormal)
		elseif specialPointsFlag[i] == 105 then
			PlaceSprite(specialPointsX[i], specialPointsY[i], sprAmGirder, 5, 4294967295, nil, nil, nil, lfNormal)
		elseif specialPointsFlag[i] == 106 then
			PlaceSprite(specialPointsX[i], specialPointsY[i], sprAmGirder, 6, 4294967295, nil, nil, nil, lfNormal)
		elseif specialPointsFlag[i] == 107 then
			PlaceSprite(specialPointsX[i], specialPointsY[i], sprAmGirder, 7, 4294967295, nil, nil, nil, lfNormal)

		-- Invulnerable Girders
		elseif specialPointsFlag[i] == 108 then
			PlaceSprite(specialPointsX[i], specialPointsY[i], sprAmGirder, 0, 2516582650, nil, nil, nil, lfIndestructible)
		elseif specialPointsFlag[i] == 109 then
			PlaceSprite(specialPointsX[i], specialPointsY[i], sprAmGirder, 1, 2516582650, nil, nil, nil, lfIndestructible)
		elseif specialPointsFlag[i] == 110 then
			PlaceSprite(specialPointsX[i], specialPointsY[i], sprAmGirder, 2, 2516582650, nil, nil, nil, lfIndestructible)
		elseif specialPointsFlag[i] == 111 then
			PlaceSprite(specialPointsX[i], specialPointsY[i], sprAmGirder, 3, 2516582650, nil, nil, nil, lfIndestructible)
		elseif specialPointsFlag[i] == 112 then
			PlaceSprite(specialPointsX[i], specialPointsY[i], sprAmGirder, 4, 2516582650, nil, nil, nil, lfIndestructible)
		elseif specialPointsFlag[i] == 113 then
			PlaceSprite(specialPointsX[i], specialPointsY[i], sprAmGirder, 5, 2516582650, nil, nil, nil, lfIndestructible)
		elseif specialPointsFlag[i] == 114 then
			PlaceSprite(specialPointsX[i], specialPointsY[i], sprAmGirder, 6, 2516582650, nil, nil, nil, lfIndestructible)
		elseif specialPointsFlag[i] == 115 then
			PlaceSprite(specialPointsX[i], specialPointsY[i], sprAmGirder, 7, 2516582650, nil, nil, nil, lfIndestructible)

		-- Icy Girders
		elseif specialPointsFlag[i] == 116 then
			PlaceSprite(specialPointsX[i], specialPointsY[i], sprAmGirder, 0, 16448250, nil, nil, nil, lfIce)
		elseif specialPointsFlag[i] == 117 then
			PlaceSprite(specialPointsX[i], specialPointsY[i], sprAmGirder, 1, 16448250, nil, nil, nil, lfIce)
		elseif specialPointsFlag[i] == 118 then
			PlaceSprite(specialPointsX[i], specialPointsY[i], sprAmGirder, 2, 16448250, nil, nil, nil, lfIce)
		elseif specialPointsFlag[i] == 119 then
			PlaceSprite(specialPointsX[i], specialPointsY[i], sprAmGirder, 3, 16448250, nil, nil, nil, lfIce)
		elseif specialPointsFlag[i] == 120 then
			PlaceSprite(specialPointsX[i], specialPointsY[i], sprAmGirder, 4, 16448250, nil, nil, nil, lfIce)
		elseif specialPointsFlag[i] == 121 then
			PlaceSprite(specialPointsX[i], specialPointsY[i], sprAmGirder, 5, 16448250, nil, nil, nil, lfIce)
		elseif specialPointsFlag[i] == 121 then
			PlaceSprite(specialPointsX[i], specialPointsY[i], sprAmGirder, 6, 16448250, nil, nil, nil, lfIce)
		elseif specialPointsFlag[i] == 123 then
			PlaceSprite(specialPointsX[i], specialPointsY[i], sprAmGirder, 7, 16448250, nil, nil, nil, lfIce)

		-- Rubber Bands
		elseif specialPointsFlag[i] == 124 then
			PlaceSprite(specialPointsX[i], specialPointsY[i], sprAmRubber, 0, 4294967295, nil, nil, nil, lfBouncy)
		elseif specialPointsFlag[i] == 125 then
			PlaceSprite(specialPointsX[i], specialPointsY[i], sprAmRubber, 1, 4294967295, nil, nil, nil, lfBouncy)
		elseif specialPointsFlag[i] == 126 then
			PlaceSprite(specialPointsX[i], specialPointsY[i], sprAmRubber, 2, 4294967295, nil, nil, nil, lfBouncy)
		elseif specialPointsFlag[i] == 127 then
			PlaceSprite(specialPointsX[i], specialPointsY[i], sprAmRubber, 3, 4294967295, nil, nil, nil, lfBouncy)

		-- Waypoints
		else -- 0 / no value
			CallBob(specialPointsX[i], specialPointsY[i])
		end

	end

end

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

	trackTeams()

	roundN = 0
	lastRound = TotalRounds
	RoundHasChanged = false
	WriteLnToConsole("TechRacer: Using map #"..tostring(mapID-1).." (mapID="..tostring(mapID)..")")
	officialChallenge = detectMapWithDigest()

	RebuildTeamInfo()

	for i=0 , TeamsCount - 1 do
		cnthhs = 0
		runOnHogsInTeam(limitHogs, teamNameArr[i])
	end

	ShowMission(
		loc("TechRacer"),
		loc("A Hedgewars mini-game"),

		loc("Complete the track as fast as you can!") .. "|" ..
		loc("Round limit:") .. " " .. roundLimit .. "|" ..
		loc("You can further customize the race by changing the scheme script parameter.") .. "|",
		4, 4000
		)

	TryRepositionHogs()

	activationStage = 1
	HandleFreshMapCreation()

end



function onNewTurn()

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

	CheckForNewRound()
	TryRepositionHogs()

	racerActive = false

	activationStage = 1

	trackTime = 0

	currCount = 0 -- hopefully this solves problem

	-- Set the waypoints to unactive on new round
	if not gameOver then
		for i = 0,(wpCount-1) do
			wpActive[i] = false
			wpCol[i] = 0xffffffff
			local minO, maxO, flashing = FlashingHelper(i)
			SetVisualGearValues(wpCirc[i], wpX[i], wpY[i], minO, maxO, 1, flashing, 0, wpRad, 5, wpCol[i])
		end
	end

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

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

	AddAmmo(CurrentHedgehog, amTardis, 0)
	AddAmmo(CurrentHedgehog, amDrillStrike, 0)
	AddAmmo(CurrentHedgehog, amMineStrike, 0)
	AddAmmo(CurrentHedgehog, amNapalm, 0)
	AddAmmo(CurrentHedgehog, amPiano, 0)

end

function onGameTick20()

	if (jet ~= nil) and (ufoFuel ~= 0) then
		if ufoFuel == 2000 then
			SetHealth(jet, 2000)
		end
	end

	runOnGears(PortalEffects)

	-- airstrike detected, convert this into a potential waypoint spot
	if cGear ~= nil then
		local x,y = GetGearPosition(cGear)
		if x > -9000 then
			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 and 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
				CallBob(x, y)
				if wpCount == wpLimit then
					AddCaption(loc("Race complexity limit reached"))
					DisableTumbler()
				end
			end
		else
			DeleteGear(cGear)
		end
		SetGearPosition(cGear, -10000, 0)
	end


	if activationStage < 200 then
		HandleFreshMapCreation()

		if not gameOver and gameBegun and not racerActive then
			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

	end


	-- start the player 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])
				SetGearMessage(CurrentHedgehog,gmLeft)

				FollowGear(CurrentHedgehog)

				HideMission()
				activationStage = 201
			end

		end

	elseif (activationStage == 201) and (TurnTimeLeft > 0) and (TurnTimeLeft ~= TurnTime) then
		SetGearMessage(CurrentHedgehog,0)
		activationStage = 202
	end

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

		-- if the RACE has started, show tracktimes and keep tabs on waypoints
		if (racerActive == true) and (activationStage == 202) 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(sndDrat, true)
					SetSoundMask(sndBugger, true)
					AdjustScores()
					DisableTumbler()
				end

			end

			-- If hedgehog is not controlled anymore, stop racing mode
			if band(GetState(CurrentHedgehog), gstHHDriven) == 0 then
				DisableTumbler(false)
			end

		end

	end

end

-- handle short range portal gun
function PortalEffects(gear)

	if GetGearType(gear) == gtPortal then

		local tag = GetTag(gear)
		local col
		if tag == 0 then
			col = 0xfab02aFF -- orange ball
		elseif tag == 1 then
			col = 0x00FF00FF -- orange portal
		elseif tag == 2 then
			col = 0x364df7FF  -- blue ball
		elseif tag == 3 then
			col = 0xFFFF00FF  -- blue portal
		end

		if (tag == 0) or (tag == 2) then -- i.e ball form
			local remLife = getGearValue(gear,"life")
			remLife = remLife - 1
			setGearValue(gear, "life", remLife)

			-- Limited range portal ball dies
			if remLife == 0 then

				-- Make portal ball disappear in a puff of smoke
				local tempE = AddVisualGear(GetX(gear)+15, GetY(gear), vgtSmoke, 0, false)
				SetVisualGearValues(tempE, nil, nil, nil, nil, nil, nil, nil, nil, nil, col)

				tempE = AddVisualGear(GetX(gear)-15, GetY(gear), vgtSmoke, 0, false)
				SetVisualGearValues(tempE, nil, nil, nil, nil, nil, nil, nil, nil, nil, col)

				tempE = AddVisualGear(GetX(gear), GetY(gear)+15, vgtSmoke, 0, false)
				SetVisualGearValues(tempE, nil, nil, nil, nil, nil, nil, nil, nil, nil, col)

				tempE = AddVisualGear(GetX(gear), GetY(gear)-15, vgtSmoke, 0, false)
				SetVisualGearValues(tempE, nil, nil, nil, nil, nil, nil, nil, nil, nil, col)

				PlaySound(sndVaporize)
				DeleteGear(gear)

			end

		end

	end

end

function onGearResurrect(gear, vGear)

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

end

function isATrackedGear(gear)
	if 	(GetGearType(gear) == gtHedgehog) or
		(GetGearType(gear) == gtGrenade) or
		(GetGearType(gear) == gtTarget) or
		(GetGearType(gear) == gtFlame) or
		(GetGearType(gear) == gtExplosives) or
		(GetGearType(gear) == gtPortal) or
		(GetGearType(gear) == gtMine) or
		(GetGearType(gear) == gtSMine) or
		(GetGearType(gear) == gtAirMine) or
		(GetGearType(gear) == gtCase)
	then
		return(true)
	else
		return(false)
	end
end

function onGearAdd(gear)

       if isATrackedGear(gear) then
		trackGear(gear)

		if GetGearType(gear) == gtPortal then
			setGearValue(gear,"life",portalDistance)
		elseif GetGearType(gear) == gtHedgehog then
			hhs[numhhs] = gear
			numhhs = numhhs + 1
			SetEffect(gear, heResurrectable, 1)
		end

	end

	if GetGearType(gear) == gtAirAttack then
		cGear = gear
	elseif GetGearType(gear) == gtJetpack then
		jet = gear
		if (ufoFuel ~= 0) then
			SetHealth(jet, ufoFuel)
		end
	end


end

function onGearDelete(gear)

	if isATrackedGear(gear) then
		trackDeletion(gear)
	elseif GetGearType(gear) == gtAirAttack then
		cGear = nil
	elseif GetGearType(gear) == gtJetpack then
		jet = nil
	elseif gear == cameraGear then
		cameraGear = nil
	end

end

function onAttack()
	local at = GetCurAmmoType()

	usedWeapons[at] = 0
end

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

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

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

	local usedOther = next(usedWeapons) ~= nil

	local raceType
	if usedOther then -- smth besides 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

function onAmmoStoreInit()

	SetAmmo(amSkip, 9, 0, 0, 0)

	for i = 1, #atkArray do
		SetAmmo(atkArray[i][1], 0, 0, 0, 1)
	end

	for i = 1, #utilArray do
		SetAmmo(utilArray[i][1], 0, 0, 0, 1)
	end

end