share/hedgewars/Data/Missions/Training/Basic_Training_-_Flying_Saucer.lua
author spudpiggy <facetakers@gmail.com>
Fri, 05 Apr 2024 13:10:55 +0100
changeset 16006 1f9f971adec4
parent 14951 c173fae0a223
permissions -rw-r--r--
sndCover now falls back to sndWatchThis OR sndFire. sndDrat and sndBugger now fall back to each other.

--[[
	Flying Saucer Training
	This is a training mission which teaches many basic (and not-so-basic) moves
	with the flying saucer.

	Lesson plan:
	- Taking off
	- Basic flight
	- Landing safely
	- Managing fuel
	- Changing saucers in mid-flight
	- Diving
	- Dropping weapons from flying saucer
	- Firing from flying saucer with [Precise] + [Attack]
	- Aiming in flying saucer with [Precise] + [Up]/[Down]
	- Underwater attack
	- Free flight with inf. fuel and some weapons at end of training

	FIXME:
	- Bad respawn animation ("explosion" just happens randomly because of the way the resurrection effect works)
	- Hide fuel if infinite (probably needs engine support)
]]

HedgewarsScriptLoad("/Scripts/Locale.lua")
HedgewarsScriptLoad("/Scripts/Tracker.lua")

local Player = nil -- Pointer to hog created in: onGameInit
local Target = nil -- Pointer to target hog
local Objective = false -- Get to the target
local Flawless = true -- Track flawless victory

local TargetNumber = 0 -- The current target number
local GrenadeThrown = false -- Used for the Boom Target
local BazookasLeft = 0 -- Used by the Launch Target and the Unterwater Attack Target

local InfFuel = false -- If true, flying saucer has infinite fuel
local SaucerGear = nil -- Store flying saucer gear here (if one exists)
local TargetGears = {} -- List of remaining gears to collect or destroy in the current round
local TargetsRemaining = 0
local Barrels = {} -- Table contraining the explosive barrel gears

local CheckTimer = 500 -- Time to wait at least before checking safe landing
local Check = false -- The last target has recently been collected/destroyed and the CheckTimer is running
local GrenadeTimer = 0 -- Time after a grenade has been thrown

local TargetPos = {} -- Table of targets

local StartPos = { X = 742, Y = 316 }

--[[
List of all targets (or "objectives"). The player has to complete them one-by-one and must always land safely afterwards.
Some target numbers have names for easier reference.
]]
-- Intro
TargetPos[1] =  {
	Targets = {{ X = 1027, Y = 217 }},
	Ammo = { },
	Message = loc("Here you will learn how to fly the flying saucer|and get so learn some cool tricks.") .. "|" ..
	loc("Collect the first crate to begin!"),
	MessageIcon = -amJetpack, }

-- First flight, infinite fuel
TargetPos[2] = {
	Targets = {{ X = 1369, Y = 265 }},
	Ammo = { [amJetpack] = 100 },
	InfFuel = true,
	MessageTime = 10000,
	Message = loc("Get to the crate using your flying saucer!") .. "|" ..
	loc("Press [Attack] (space bar by default) to start,|repeatedly tap the up, left and right movement keys to accelerate.") .. "|" ..
	loc("Try to land softly, as you can still take fall damage!"), }

-- First flight, limited fuel
TargetPos[3] = {
	Targets = {{ X = 689, Y = 58 }},
	Ammo = { [amJetpack] = 100 },
	MessageTime = 5000,
	Message = loc("Now collect the next crate!") .. "|" .. loc("Be careful, your fuel is limited from now on!") .."|" ..
	loc("Tip: If you get stuck in this training, use \"Skip turn\" to restart the current objective.") }

-- The Double Target
TargetPos[4] = {
	Targets = { { X = 178, Y = -20 }, { X = 1962 , Y = -20 } },
	Ammo = { [amJetpack] = 2 },
	CratesContainAmmo = true,
	MessageTime = 9000,
	Message = loc("Now collect the 2 crates to the far left and right.") .. "|" ..
	loc("You only have 2 flying saucers this time.") .. "|" ..
	loc("Tip: You can change your flying saucer|in mid-flight by hitting the [Attack] key twice."), }

-- Intermission
TargetPos[5] = {
	Targets = {{ X = 47, Y = 804 }},
	Ammo = { [amJetpack] = 100 },
	MessageTime = 5000,
	Message = loc("Time for a more interesting stunt, but first just collect the next crate!"), }

-- First Dive
TargetPos[6] = {
	Targets = {{ X = 604, Y = 871}},
	MessageTime = 15000,
	Message = loc("You can dive with your flying saucer!") .. "|" ..
	loc("Try it now and dive here to collect the crate on the right girder.") .. "|" ..
	loc("You only have one flying saucer this time.") .. "|" ..
	loc("Beware, though, you will only be able to move slowly through the water.") .. "|" ..
	loc("Warning: Never ever leave the flying saucer while in water!"),
	Ammo = { [amJetpack] = 1 },
	Respawn = { X = 758, Y = 847, FaceLeft = false }, }

-- Second Dive
TargetPos[7] = { 
	Targets = {{ X = 1884, Y = 704 }},
	MessageTime = 6500,
	Message = loc("Now dive just one more time and collect the next crate.") .. "|" ..
		loc("Tip: Don't remain for too long in the water, or you won't make it."),
	Ammo = { [amJetpack] = 2 },
	Respawn = { X = 1968, Y = -1, FaceLeft = true }, }

-- The Grenade Drop Target
local BoomTarget = 8
TargetPos[8] = {
	Modifier = true, Func = function()
		Info(loc("Instructions"),
		loc("Now let's try to drop weapons while flying!") .. "|" ..
		loc("You have to destroy the target above by dropping a grenade on it from your flying saucer.") .. "|" ..
		loc("It's not that easy, so listen carefully:") .. "|" ..
		loc("Step 1: Activate your flying saucer but do NOT move yet!") .. "|" ..
		loc("Step 2: Select your grenade.") .. "|" ..
		loc("Step 3: Start flying and get yourself right above the target.") .. "|" ..
		loc("Step 4: Drop your grenade by pressing the [Long jump] key.") .. "|" ..
		loc("Step 5: Get away quickly and land safely anywhere.") .. "| |" ..
		loc("Note: We only give you grenades if you stay in your flying saucer."), nil, 20000)

		SpawnBoomTarget()

		if SaucerGear ~= nil then
			AddAmmo(Player, amGrenade, 1)
		else
			AddAmmo(Player, amGrenade, 0)
		end
		GrenadeThrown = false

	end,
	Ammo = { [amJetpack] = 100 },
	Respawn = { X = 2000, Y = 742, FaceLeft = true }, }

-- The Launch Target
local LaunchTarget = 9
TargetPos[9] = {
	Targets = {{ X = 1700, Y = 640, Type = gtTarget }, { X = 1460, Y = 775, Type = gtTarget }},
	MessageTime = 20000,
	Message = loc("Only the best pilots can master the following stunts.") .. "|" ..
		loc("As you've seen, the dropped grenade roughly fell into your flying direction.") .. "|" ..
		loc("You have to destroy two targets, but the previous technique would be very difficult or dangerous to use.") .. "|" ..
		loc("So you are able to launch projectiles into your aiming direction, always at full power.") .."|"..
		loc("To launch a projectile in mid-flight, hold [Precise] and press [Long jump].") .. "|" ..
		loc("You can even change your aiming direction in mid-flight if you first hold [Precise] and then press [Up] or [Down].") .. "|" ..
		loc("Tip: Changing your aim while flying is very difficult, so adjust it before you take off."),
	Ammo = { [amJetpack] = 1, },
	Respawn = { X = 1760, Y = 754, FaceLeft = true },
	ExtraFunc = function()
		if SaucerGear ~= nil then
			AddAmmo(Player, amBazooka, 2)
		else
			AddAmmo(Player, amBazooka, 0)
		end
		BazookasLeft = 2

	end }

-- The Underwater Attack Target
local UnderwaterAttackTarget = 10
TargetPos[10] = {
	MessageTime = 17000,
	Message = loc("Now for the supreme discipline of saucer flying, the underwater attack.") .. "|" ..
	loc("Basically this is a combination of diving and launching.") .. "|" ..
	loc("Dropping a weapon while in water would just drown it, but launching one would work.") .."|" ..
	loc("Based on what you've learned, destroy the target on the girder and as always, land safely!"), 
	Targets = {{ X = 1200, Y = 930, Type = gtTarget }},
	Ammo = { [amJetpack] = 1, },
	Respawn = { X = 1027, Y = 217, FaceLeft = true },
	ExtraFunc = function()
		if SaucerGear ~= nil then
			AddAmmo(Player, amBazooka, 1)
		else
			AddAmmo(Player, amBazooka, 0)
		end
		BazookasLeft = 1
	end }
-- Final target / Sandbox
TargetPos[11] = {
	Targets = {{ X = 742, Y = 290 }},
	MessageTime = 5000,
	Message = loc("This almost concludes our tutorial.") .. "|" ..
	loc("You now have infinite fuel, grenades and bazookas for fun.") .. "|" ..
	loc("Collect or destroy the final crate to finish the training."),
	Ammo = { [amJetpack] = 100, [amGrenade] = 100, [amBazooka] = 100 },
	InfFuel = true, }
-- Outro
TargetPos[12] = { Modifier = true, Func = function()
	Objective = true
	AddCaption(loc("Training complete!"), capcolDefault, capgrpGameState)
	Info(loc("Training complete!"), loc("Good bye!"), 4, 5000)

	if SaucerGear ~= nil then
		DeleteGear(SaucerGear)
	end
	SetState(Player, band(GetState(Player), bnot(gstHHDriven)))
	SetState(Player, bor(GetState(Player), gstWinner))
	if Flawless then
		PlaySound(sndFlawless, Player)
	else
		PlaySound(sndVictory, Player)
	end
	SaveMissionVar("Won", "true")

	SendStat(siGameResult, loc("You have finished the Flying Saucer Training!"))
	SendStat(siCustomAchievement, loc("Good job!"))

	EndTurn(true)
	EndGame()
	SetState(Player, gstWinner)
end,
}

-- Just a wrapper for ShowMission
function Info(Title, Text, Icon, Time)
	if Time == nil then Time = 0 end
	if Icon == nil then Icon = 2 end
	ShowMission(loc("Flying Saucer Training"), Title, Text, Icon, Time)
end

-- Spawn all the gears for the Boom Target
function SpawnBoomTarget()
	if TargetsRemaining < 1 then
		TargetGears[1] = AddGear(1602, 507, gtTarget, 0, 0, 0, 0)
		TargetsRemaining = TargetsRemaining + 1
	end

	if Barrels[1] == nil then
		Barrels[1] = AddGear(1563, 532, gtExplosives, 0, 0, 0, 0)
	end
	if Barrels[2] == nil then
		Barrels[2] = AddGear(1648, 463, gtExplosives, 0, 0, 0, 0)
	end
	if Barrels[3] == nil then
		Barrels[3] = AddGear(1513, 575, gtExplosives, 0, 0, 0, 0)
	end

	for i=1,#Barrels do
		SetHealth(Barrels[i], 1)
	end
end

-- Generic target spawning for the current target
function SpawnTargets()
	for i=1,#TargetPos[TargetNumber].Targets do
		if TargetGears[i] == nil then
			SpawnTarget(TargetPos[TargetNumber].Targets[i].X, TargetPos[TargetNumber].Targets[i].Y,
				TargetPos[TargetNumber].Targets[i].Type, i, TargetPos[TargetNumber].CratesContainAmmo )
		end
	end
end

function SpawnTarget( PosX, PosY, Type, ID, ContainsAmmo )
	if Type ~= nil and Type ~= gtCase then
		if Type == gtTarget then
			TargetGears[ID] = AddGear(PosX, PosY, gtTarget, 0, 0, 0, 0)
		end
	else
		if ContainsAmmo == true then
			TargetGears[ID] = SpawnSupplyCrate(PosX, PosY, amJetpack)
		else
			TargetGears[ID] = SpawnFakeUtilityCrate(PosX, PosY, false, false)
		end
	end
	TargetsRemaining = TargetsRemaining + 1
end

function AutoSpawn() -- Auto-spawn the next target after you've obtained the current target!
	TargetNumber = TargetNumber + 1
	TargetsRemaining = 0

	if TargetPos[TargetNumber].Ammo then
		for ammoType, count in pairs(TargetPos[TargetNumber].Ammo) do
			AddAmmo(Player, ammoType, count)
		end
		if GetCurAmmoType() ~= amJetpack then
			SetWeapon(amJetpack)
		end
	end
	if TargetPos[TargetNumber].InfFuel then
		InfFuel = true
	else
		InfFuel = false
	end
	UpdateInfFuel()

	-- Func (if present) will be run instead of the ordinary spawning handling
	if TargetPos[TargetNumber].Modifier then -- If there is a modifier, run the function
		TargetPos[TargetNumber].Func()
		return true
	end

	-- ExtraFunc is for additional events for a target
	if TargetPos[TargetNumber].ExtraFunc ~= nil then
		TargetPos[TargetNumber].ExtraFunc()
	end

	local subcap
	if TargetNumber == 1 then
		subcap = loc("Training")
	else
		subcap = loc("Instructions")
	end
	Info(subcap, TargetPos[TargetNumber].Message, TargetPos[TargetNumber].MessageIcon, TargetPos[TargetNumber].MessageTime)

	-- Spawn targets on the next position
	SpawnTargets()

	if TargetNumber > 1 then
		AddCaption(loc("Next target is ready!"), capcolDefault, capgrpMessage2)
	end
end

-- Returns true if the hedgehog has safely "landed" (alive, no flying saucer gear and not moving)
-- This is to ensure the training only continues when the player didn't screw up and to restart the current target
function HasHedgehogLandedYet()
	if band(GetState(Player), gstMoving) == 0 and SaucerGear == nil and GetHealth(Player) > 0 then
		return true
	else
		return false
	end
end

-- Clean up the gear mess left behind when the player failed to get a clean state after restarting
function CleanUpGears()
	-- (We track flames, grenades, bazooka shells)
	runOnGears(DeleteGear)
end

-- Completely restarts the current target/objective; the hedgehog is spawned at the last "checkpoint"
-- Called when hedgeghog is resurrected or skips turn
function ResetCurrentTarget()
	GrenadeThrown = false
	GrenadeTimer = 0
	if TargetNumber == LaunchTarget then
		BazookasLeft = 2
	elseif TargetNumber == UnderwaterAttackTarget then
		BazookasLeft = 1
	else
		BazookasLeft = 0
	end
	Check = false

	CleanUpGears()

	local X, Y, FaceLeft
	if TargetNumber == 1 then
		X, Y = StartPos.X, StartPos.Y
	else
		if TargetPos[TargetNumber-1].Modifier or TargetPos[TargetNumber-1].Respawn ~= nil then
			X, Y = TargetPos[TargetNumber-1].Respawn.X, TargetPos[TargetNumber-1].Respawn.Y
			FaceLeft = TargetPos[TargetNumber-1].Respawn.FaceLeft
		else
			X, Y = TargetPos[TargetNumber-1].Targets[1].X, TargetPos[TargetNumber-1].Targets[1].Y
		end
	end
	if TargetNumber == BoomTarget then
		SpawnBoomTarget()
	end
	if TargetPos[TargetNumber].Modifier ~= true then
		SpawnTargets()
	end
	if TargetPos[TargetNumber].Ammo then
		for ammoType, count in pairs(TargetPos[TargetNumber].Ammo) do
			AddAmmo(Player, ammoType, count)
		end
		if GetCurAmmoType() ~= amJetpack then
			SetWeapon(amJetpack)
		end
	end
	if TargetPos[TargetNumber].InfFuel then
		InfFuel = true
	else
		InfFuel = false
	end
	UpdateInfFuel()

	SetGearPosition(Player, X, Y)
	if FaceLeft ~= nil then
		HogTurnLeft(Player, FaceLeft)
	end
end

function onGameInit()
	Seed = 1
	GameFlags = gfInfAttack + gfOneClanMode + gfSolidLand + gfDisableWind
	TurnTime = MAX_TURN_TIME --[[ This effectively hides the turn time; a turn time above 1000s is not displayed.
			   	     We will also ensure this timer always stays above 999s later ]]
	CaseFreq = 0
	MinesNum = 0
	Explosives = 0
	Map = "Eyes"
	Theme = "EarthRise"
	SuddenDeathTurns = 50
	WaterRise = 0
	HealthDecrease = 0

	AddMissionTeam(-9)

	Player = AddMissionHog(1)
	SetGearPosition( Player, StartPos.X, StartPos.Y)
	SetEffect( Player, heResurrectable, 1 )
end

function onGameStart()
	SendHealthStatsOff()
	SendRankingStatsOff()

	-- Girder near first crate
	PlaceGirder(1257, 204, 6)

	-- The upper girders
	PlaceGirder(84, 16, 4)
	PlaceGirder(243, 16, 4)
	PlaceGirder(1967, 16, 4)

	-- The lower girder platform at the water pit
	PlaceGirder(509, 896, 4)
	PlaceGirder(668, 896, 4)
	PlaceGirder(421, 896, 2)
	PlaceGirder(758, 896, 2)

	-- Girders for the Launch Target and the Underwater Attack Target
	PlaceGirder(1191, 960, 4)
	PlaceGirder(1311, 960, 0)
	PlaceGirder(1460, 827, 3)
	PlaceGirder(1509, 763, 2)
	PlaceGirder(1605, 672, 4)
	PlaceGirder(1764, 672, 4)
	PlaceGirder(1803, 577, 6)

	-- Spawn our 1st target using the wrapper function
	AutoSpawn()
end

function onAmmoStoreInit()
	SetAmmo(amJetpack, 0, 0, 0, 1)
	SetAmmo(amGrenade, 0, 0, 0, 1)
	SetAmmo(amBazooka, 0, 0, 0, 1)

	-- Added for resetting current target/objective when player is stuck somehow
	SetAmmo(amSkip, 9, 0, 0, 0)
end

function onGearAdd(Gear)
	if GetGearType(Gear) == gtJetpack then
		SaucerGear = Gear
		if TargetNumber == BoomTarget and GrenadeThrown == false then
			AddAmmo(Player, amGrenade, 1)
		end
		if (TargetNumber == LaunchTarget or TargetNumber == UnderwaterAttackTarget) and BazookasLeft > 0 then
			AddAmmo(Player, amBazooka, BazookasLeft)
		end
		UpdateInfFuel()
		-- If player starts using saucer, the player probably finished reading and the mission panel
		-- would just get in the way. So we hide it!
		HideMission()
	end
	if GetGearType(Gear) == gtGrenade then
		GrenadeThrown = true
		GrenadeTimer = 0
	end
	if GetGearType(Gear) == gtShell then
		BazookasLeft = BazookasLeft - 1
	end
	if GetGearType(Gear) == gtFlame or GetGearType(Gear) == gtGrenade or GetGearType(Gear) == gtShell then
		trackGear(Gear)
	end
end

function onGearDelete(Gear)
	if GetGearType(Player) ~= nil and (GetGearType(Gear) == gtTarget or GetGearType(Gear) == gtCase) then
		for i=1, #TargetGears do
			if Gear == TargetGears[i] then
				TargetGears[i] = nil
				TargetsRemaining = TargetsRemaining - 1
			end
		end
		if TargetsRemaining <= 0 then
			if TargetNumber == BoomTarget or not HasHedgehogLandedYet() then
				if SaucerGear then
					AddCaption(loc("Objective completed! Now land safely."), capcolDefault, capgrpMessage2)
				end
				Check = true
				CheckTimer = 500
			else
				AutoSpawn()
			end
		end
	end
	if GetGearType(Gear) == gtGrenade then
		GrenadeTimer = 0
		GrenadeExploded = true
	end
	if GetGearType(Gear) == gtJetpack then
		SaucerGear = nil
		if TargetNumber == BoomTarget then
			AddAmmo(Player, amGrenade, 0)
		end
		if TargetNumber == LaunchTarget or TargetNumber == UnderwaterAttackTarget then
			AddAmmo(Player, amBazooka, 0)
		end
	end
	-- Fake crate collected
	if GetGearType(Gear) == gtCase and band(GetGearMessage(Gear), gmDestroy) ~= 0 and band(GetGearPos(Gear), 0x8) ~= 0 then
		PlaySound(sndShotgunReload)
	end
	if Gear == Barrels[1] then
		Barrels[1] = nil
	end
	if Gear == Barrels[2] then
		Barrels[2] = nil
		AddCaption(loc("Kaboom!"), capcolDefault, capgrpMessage)
	end
	if Gear == Barrels[3] then
		Barrels[3] = nil
	end
end



function onNewTurn()
	if GetAmmoCount(CurrentHedgehog, amJetpack) > 0 then
		SetWeapon(amJetpack)
	end
end

function onGameTick20()
	if (TurnTimeLeft < 1500000 and not Objective) then
		SetTurnTimeLeft(TurnTime)
	end
	if Check then
		CheckTimer = CheckTimer - 20
		if CheckTimer <= 0 then
			if HasHedgehogLandedYet() then
				AutoSpawn()
				Check = false
				GrenadeThrown = false
			end
		end
	end
	if GrenadeExploded and TargetNumber == BoomTarget and GetHealth(Player) then
		GrenadeTimer = GrenadeTimer + 20
		if GrenadeTimer > 1500 then
			GrenadeTimer = 0
			GrenadeThrown = false
			GrenadeExploded = false
			if SaucerGear and TargetNumber == BoomTarget and TargetsRemaining > 0 then
				PlaySound(sndShotgunReload)
				AddCaption(loc("+1 Grenade"), GetClanColor(GetHogClan(Player)), capgrpAmmoinfo)
				AddAmmo(Player, amGrenade, 1)
			end
		end
	end
end

function UpdateInfFuel()
	if SaucerGear then
		if InfFuel then
			SetHealth(SaucerGear, JETPACK_FUEL_INFINITE)
		elseif GetHealth(SaucerGear == JETPACK_FUEL_INFINITE) then
			SetHealth(SaucerGear, 2000)
		end
	end
end

function onGearDamage(Gear)
	if Gear == Player then
		Flawless = false
		CleanUpGears()
		GrenadeThrown = false
		Check = false
	end
end

function onGearResurrect(Gear, VGear)
	if Gear == Player then
		Flawless = false
		AddCaption(loc("Oh no! You have died. Try again!"), capcolDefault, capgrpMessage2)
		ResetCurrentTarget()
		if VGear then
			SetVisualGearValues(VGear, GetX(Gear), GetY(Gear))
		end
	end
end

function onSkipTurn()
	Flawless = false
	AddCaption(loc("Try again!"), capcolDefault, capgrpMessage2)
	ResetCurrentTarget()
end