1-- Hedgewars SniperRifle Training
2-- Scripting Example
3
4-- Lines such as this one are comments - they are ignored
5-- by the game, no matter what kind of text is in there.
6-- It's also possible to place a comment after some real
7-- instruction as you see below. In short, everything
8-- following "--" is ignored.
9
10---------------------------------------------------------------
11-- At first we implement the localization library using loadfile.
12-- This allows us to localize strings without needing to think
13-- about translations.
14-- We can use the function loc(text) to localize a string.
15
16HedgewarsScriptLoad("/Scripts/Utils.lua")
17HedgewarsScriptLoad("/Scripts/Locale.lua")
18
19-- This variable will hold the number of destroyed targets.
20local score = 0
21-- This variable will hold the number of shots from the sniper rifle
22local shots = 0
23-- This variable represents the number of targets to destroy.
24local score_goal = 27
25-- This variable controls how many milliseconds/ticks we'd
26-- like to wait before we end the round once all targets
27-- have been destroyed.
28local end_timer = 1000 -- 1000 ms = 1 s
29-- This variable is set to true if the game is lost (i.e.
30-- time runs out).
31local game_lost = false
32-- This variable will point to the hog's gear
33local player = nil
34-- Current target gear
35local target = nil
36-- This variable will grab the time left at the end of the round
37local time_goal = 0
38
39-- Like score, but targets before a blow-up sequence count double.
40-- Used to calculate final target score
41local score_bonus = 0
42
43local cinematic = false
44
45-- Timer of dynamite (shorter than usual)
46local dynamiteTimer = 2000
47-- Number of dynamite gears currently in game
48local dynamiteCounter = 0
49-- Table of dynamite gears, indexed by gear ID
50local dynamiteGears = {}
51
52-- Position for delayed targets
53local delayedTargetTargetX, delayedTargetY
54
55-- Team name of the player's team
56local playerTeamName
57
58-- This is a custom function to make it easier to
59-- spawn more targets with just one line of code
60-- You may define as many custom functions as you
61-- like.
62
63-- Spawns a target at (x, y)
64function spawnTarget(x, y)
65	-- add a new target gear
66	target = AddGear(x, y, gtTarget, 0, 0, 0, 0)
67	-- have the camera move to the target so the player knows where it is
68	FollowGear(target)
69end
70
71-- Remembers position to spawn a target at (x, y) after a dynamite explosion
72function spawnTargetDelayed(x, y)
73	delayedTargetX = x
74	delayedTargetY = y
75	-- The previous target always counts double after destruction
76	score_bonus = score_bonus + 1
77end
78
79function getTargetScore()
80	return score_bonus * 200
81end
82
83-- Cut sequence to blow up land with dynamite
84function blowUp(x, y, follow)
85	if cinematic == false then
86		cinematic = true
87		SetCinematicMode(true)
88	end
89	-- Spawn dynamite with short timer
90	local dyna = AddGear(x, y, gtDynamite, 0, 0, 0, dynamiteTimer)
91	-- Fix dynamite animation due to non-default timer
92	SetTag(dyna, div(5000-dynamiteTimer, 166))
93	if follow then
94		FollowGear(dyna)
95	end
96end
97
98function onNewTurn()
99	SetWeapon(amSniperRifle)
100end
101
102-- This function is called before the game loads its
103-- resources.
104-- It's one of the predefined function names that will
105-- be called by the game. They give you entry points
106-- where you're able to call your own code using either
107-- provided instructions or custom functions.
108function onGameInit()
109	-- At first we have to overwrite/set some global variables
110	-- that define the map, the game has to load, as well as
111	-- other things such as the game rules to use, etc.
112	-- Things we don't modify here will use their default values.
113
114	-- The base number for the random number generator
115	Seed = 0
116	-- Game settings and rules
117	ClearGameFlags()
118	EnableGameFlags(gfMultiWeapon, gfOneClanMode, gfArtillery)
119	-- The time the player has to move each round (in ms)
120	TurnTime = 150000
121	-- The frequency of crate drops
122	CaseFreq = 0
123	-- The number of mines being placed
124	MinesNum = 0
125	-- The number of explosives being placed
126	Explosives = 0
127	-- The map to be played
128	Map = "Ropes"
129	-- The theme to be used
130	Theme = "Golf"
131	-- Disable Sudden Death
132	WaterRise = 0
133	HealthDecrease = 0
134
135	-- Create the player team
136	playerTeamName = AddMissionTeam(-1)
137	-- And add a hog to it
138	player = AddMissionHog(1)
139	SetGearPosition(player, 602, 1488)
140end
141
142-- This function is called when the round starts
143-- it spawns the first target that has to be destroyed.
144-- In addition it shows the scenario goal(s).
145function onGameStart()
146	-- Disable graph in stats screen
147	SendHealthStatsOff()
148	-- Spawn the first target.
149	spawnTarget(860,1020)
150
151	local highscore = getReadableChallengeRecord("Highscore")
152	-- Show some nice mission goals.
153	-- Parameters are: caption, sub caption, description,
154	-- extra text, icon and time to show.
155	-- A negative icon parameter (-n) represents the n-th weapon icon
156	-- A positive icon paramter (n) represents the (n+1)-th mission icon
157	-- A timeframe of 0 is replaced with the default time to show.
158	ShowMission(loc("Sniper Training"), loc("Aiming Practice"),
159	loc("Eliminate all targets before your time runs out.|You have unlimited ammo for this mission.")
160	.. "|" .. highscore, -amSniperRifle, 0)
161
162	-- Displayed initial player score
163	SetTeamLabel(playerTeamName, "0")
164end
165
166-- This function is called every game tick.
167-- Note that there are 1000 ticks within one second.
168-- You shouldn't try to calculate too complicated
169-- code here as this might slow down your game.
170function onGameTick20()
171	if game_lost then
172		return
173	end
174	-- If time's up, set the game to be lost.
175	-- We actually check the time to be "1 ms" as it
176	-- will be at "0 ms" right at the start of the game.
177	if TurnTimeLeft < 40 and TurnTimeLeft > 0 and score < score_goal and game_lost == false then
178		game_lost = true
179		-- and generate the stats and go to the stats screen
180		generateStats()
181		EndGame()
182		-- Just to be sure set the goal time to 1 ms
183		time_goal = 1
184	end
185	-- If the goal is reached or we've lost ...
186	if score == score_goal or game_lost then
187		-- ... check to see if the time we'd like to
188		-- wait has passed and then ...
189		if end_timer == 0 then
190			-- ... end the game ...
191			generateStats()
192			EndGame()
193			if score == score_goal then
194				SetState(CurrentHedgehog, gstWinner)
195			end
196		end
197        	end_timer = end_timer - 20
198	end
199end
200
201-- This function is called when the game is initialized
202-- to request the available ammo and probabilities
203function onAmmoStoreInit()
204	-- add an unlimited supply of shotgun ammo
205	SetAmmo(amSniperRifle, 9, 0, 0, 0)
206end
207
208--[[ Re-center camera to target after using sniper rifle.
209This makes it easier to find the target. If we don't
210do this, the camera would contantly bounce back to
211the hog which would be annoying. ]]
212function onAttack()
213	if target and GetCurAmmoType() == amSniperRifle then
214		FollowGear(target)
215	end
216end
217
218-- Insta-blow up dynamite with precise key
219function onPrecise()
220	for gear, _ in pairs(dynamiteGears) do
221		SetTimer(gear, 0)
222	end
223end
224
225-- This function is called when a new gear is added.
226-- We use it to count the number of shots, which we
227-- in turn use to calculate the final score and stats
228function onGearAdd(gear)
229	if GetGearType(gear) == gtSniperRifleShot then
230		shots = shots + 1
231	elseif GetGearType(gear) == gtDynamite then
232		dynamiteCounter = dynamiteCounter + 1
233		dynamiteGears[gear] = true
234	end
235end
236
237-- This function is called before a gear is destroyed.
238-- We use it to count the number of targets destroyed.
239function onGearDelete(gear)
240	local gt = GetGearType(gear)
241
242	if gt == gtCase then
243		game_lost = true
244		return
245	end
246
247	if (gt == gtDynamite) then
248		-- Dynamite blow-up, used to continue the game.
249		dynamiteCounter = dynamiteCounter - 1
250		dynamiteGears[gear] = nil
251
252		-- Wait for all dynamites to be destroyed before we continue.
253		-- Most cut scenes spawn multiple dynamites.
254		if dynamiteCounter == 0 then
255			if cinematic then
256				cinematic = false
257				SetCinematicMode(false)
258			end
259			-- Now *actually* spawn the delayed target
260			spawnTarget(delayedTargetX, delayedTargetY)
261		end
262		return
263	end
264
265	if gt == gtTarget then
266		target = nil
267		-- Add one point to our score/counter
268		score = score + 1
269		score_bonus = score_bonus + 1
270		-- If we haven't reached the goal ...
271		if score < score_goal then
272			-- ... spawn another target.
273			if score == 1 then
274				spawnTarget(1520,1350)
275			elseif score == 2 then
276				spawnTarget(1730,1040)
277			elseif score == 3 then
278				spawnTarget(2080,780)
279			elseif score == 4 then
280				-- Short cut scene, blows up up lots up land and prepares
281				-- next target position.
282				AddCaption(loc("Good so far!") .. " " .. loc("Keep it up!"));
283				blowUp(1730,1226)
284				blowUp(1440,1595)
285				blowUp(1527,1575)
286				blowUp(1614,1595)
287				blowUp(1420,1675, true)
288				blowUp(1527,1675)
289				blowUp(1634,1675)
290				blowUp(1440,1755)
291				blowUp(1527,1775)
292				blowUp(1614,1755)
293				-- Target appears *after* the cutscene.
294				spawnTargetDelayed(1527,1667)
295			elseif score == 5 then
296				spawnTarget(2175,1300)
297			elseif score == 6 then
298				spawnTarget(2250,940)
299			elseif score == 7 then
300				spawnTarget(2665,1540)
301			elseif score == 8 then
302				spawnTarget(3040,1160)
303			elseif score == 9 then
304				spawnTarget(2930,1500)
305			elseif score == 10 then
306				-- The "tricky" target.
307				-- It spawns behind a wall
308				-- and needs at least 2 shots.
309				AddCaption(loc("This one's tricky."));
310				spawnTarget(700,720)
311			elseif score == 11 then
312				AddCaption(loc("Well done."));
313				blowUp(914,1222)
314				blowUp(1050,1222)
315				blowUp(1160,1008)
316				blowUp(1160,1093)
317				blowUp(1160,1188)
318				blowUp(375,911)
319				blowUp(510,911)
320				blowUp(640,911)
321				blowUp(780,911)
322				blowUp(920,911)
323				blowUp(1060,913)
324				blowUp(1198,913, true)
325				spawnTargetDelayed(1200,830)
326			elseif score == 12 then
327				spawnTarget(1430,450)
328			elseif score == 13 then
329				spawnTarget(796,240)
330			elseif score == 14 then
331				spawnTarget(300,10)
332			elseif score == 15 then
333				spawnTarget(2080,820)
334			elseif score == 16 then
335				AddCaption(loc("Demolition is fun!"));
336				blowUp(2110,920)
337				blowUp(2210,920)
338				blowUp(2200,305)
339				blowUp(2300,305)
340				blowUp(2300,400, true)
341				blowUp(2300,500)
342				blowUp(2300,600)
343				blowUp(2300,700)
344				blowUp(2300,800)
345				blowUp(2300,900)
346				blowUp(2401,305)
347				blowUp(2532,305)
348				blowUp(2663,305)
349				spawnTargetDelayed(2300,760)
350			elseif score == 17 then
351				spawnTarget(2738,190)
352			elseif score == 18 then
353				spawnTarget(2590,-100)
354			elseif score == 19 then
355				AddCaption(loc("Will this ever end?"));
356				blowUp(2790,305)
357				blowUp(2930,305)
358				blowUp(3060,305)
359				blowUp(3190,305)
360				blowUp(3310,305, true)
361				blowUp(3393,613)
362				blowUp(2805,370)
363				blowUp(2805,500)
364				blowUp(2805,630)
365				blowUp(2805,760)
366				blowUp(2805,890)
367				blowUp(3258,370)
368				blowUp(3258,475)
369				blowUp(3264,575)
370				spawnTargetDelayed(3230,290)
371			elseif score == 20 then
372				spawnTarget(3670,250)
373			elseif score == 21 then
374				spawnTarget(2620,-100)
375			elseif score == 22 then
376				spawnTarget(2870,300)
377			elseif score == 23 then
378				spawnTarget(3850,900)
379			elseif score == 24 then
380				spawnTarget(3780,300)
381			elseif score == 25 then
382				spawnTarget(3670,0)
383			elseif score == 26 then
384				AddCaption(loc("Last Target!"));
385				spawnTarget(3480,1200)
386			end
387		elseif not game_lost then
388			-- Victory!
389			SaveMissionVar("Won", "true")
390			AddCaption(loc("Victory!"), capcolDefault, capgrpGameState)
391			ShowMission(loc("Sniper Training"), loc("Aiming Practice"), loc("Congratulations! You've eliminated all targets|within the allowed time frame."), 0, 0)
392			-- Play voice
393			if shots-1 <= score then
394				-- Flawless victory condition: Only 1 shot more than targets
395				-- (1 shot per "normal" target + 2 shots for the "tricky" target)
396				PlaySound(sndFlawless, CurrentHedgehog)
397			else
398				-- "Normal" victory
399				PlaySound(sndVictory, CurrentHedgehog)
400			end
401
402			FollowGear(CurrentHedgehog)
403
404			-- Unselect sniper rifle and disable hog controls
405			SetInputMask(0)
406			SetWeapon(amNothing)
407			AddAmmo(CurrentHedgehog, amSniperRifle, 0)
408
409			-- Save the time left so we may keep it.
410			time_goal = TurnTimeLeft
411
412			-- Freeze the clock because the challenge has been completed
413			SetTurnTimePaused(true)
414		end
415		SetTeamLabel(playerTeamName, getTargetScore())
416	end
417end
418
419-- This function calculates the final score of the player and provides some texts and
420-- data for the final stats screen
421function generateStats()
422	local accuracy
423	local accuracy_int
424	if shots > 0 then
425		-- NOTE: 100% accuracy is not possible due to the "tricky" target.
426		accuracy = (score/shots)*100
427		accuracy_int = div(score*100, shots)
428	end
429	local end_score_targets = getTargetScore()
430	local end_score_overall
431	if not game_lost then
432		local end_score_time = math.ceil(time_goal/5)
433		local end_score_accuracy = 0
434		if shots > 0 then
435			end_score_accuracy = math.ceil(accuracy * 100)
436		end
437		end_score_overall = end_score_time + end_score_targets + end_score_accuracy
438		SetTeamLabel(playerTeamName, tostring(end_score_overall))
439
440		SendStat(siGameResult, loc("You have successfully finished the sniper rifle training!"))
441		SendStat(siCustomAchievement, string.format(loc("You have destroyed %d of %d targets (+%d points)."), score, score_goal, end_score_targets))
442		SendStat(siCustomAchievement, string.format(loc("You have made %d shots."), shots))
443		if end_score_accuracy > 0 then
444			SendStat(siCustomAchievement, string.format(loc("Accuracy bonus: +%d points"), end_score_accuracy))
445		end
446		SendStat(siCustomAchievement, string.format(loc("You had %.2fs remaining on the clock (+%d points)."), (time_goal/1000), end_score_time))
447
448		if(shots > 0) then
449			updateChallengeRecord("AccuracyRecord", accuracy_int)
450		end
451	else
452		SendStat(siGameResult, loc("Challenge over!"))
453
454		SendStat(siCustomAchievement, string.format(loc("You have destroyed %d of %d targets (+%d points)."), score, score_goal, end_score_targets))
455		SendStat(siCustomAchievement, string.format(loc("You have made %d shots."), shots))
456		end_score_overall = end_score_targets
457	end
458	SendStat(siPointType, "!POINTS")
459	SendStat(siPlayerKills, tostring(end_score_overall), playerTeamName)
460	updateChallengeRecord("Highscore", end_score_overall)
461end
462
463