|
1 --[=[ |
|
2 Target Practice Mission Framework for Hedgewars |
|
3 |
|
4 This is a simple library intended to make setting up simple training missions a trivial |
|
5 task requiring just. The library has been created to reduce redundancy in Lua scripts. |
|
6 |
|
7 The training framework generates complete and fully usable training missions by just |
|
8 one function call. |
|
9 |
|
10 The missions generated by this script are all the same: |
|
11 - The player will get a team with a single hedgehog. |
|
12 - The team gets a single predefined weapon infinitely times. |
|
13 - A fixed sequence of targets will spawn at predefined positions. |
|
14 - When a target has been destroyed, the next target of the target sequence appears |
|
15 - The mission ends successfully when all targets have been destroyed |
|
16 - The mission ends unsuccessfully when the time runs out or the hedgehog dies |
|
17 - When the mission ends, a score is awarded, based on the performance (hit targets, |
|
18 accuracy and remaining time) of the hedgehog. When not all targets are hit, there |
|
19 will be no accuracy and time bonuses. |
|
20 |
|
21 To use this library, you first have to load it and to call TrainingMission once with |
|
22 the appropriate parameters. Really, that’s all! |
|
23 See the comment of TrainingMission for a specification of all parameters. |
|
24 |
|
25 Below is a template for your convenience, you just have to fill in the fields and delete |
|
26 optional arguments you don’t want. |
|
27 ----- snip ----- |
|
28 HedgewarsScriptLoad("/Scripts/Training.lua") |
|
29 params = { |
|
30 missionTitle = , |
|
31 map = , |
|
32 theme = , |
|
33 time = , |
|
34 ammoType = , |
|
35 gearType = , |
|
36 secondaryGearType = , |
|
37 targets = { |
|
38 { x = , y = }, |
|
39 { x = , y = }, |
|
40 -- etc. |
|
41 }, |
|
42 |
|
43 wind = , |
|
44 solidLand = , |
|
45 artillery = , |
|
46 clanColor = , |
|
47 goalText = , |
|
48 shootText = |
|
49 } |
|
50 TargetPracticeMission(params) |
|
51 ----- snip ----- |
|
52 ]=] |
|
53 |
|
54 HedgewarsScriptLoad("/Scripts/Utils.lua") |
|
55 HedgewarsScriptLoad("/Scripts/Locale.lua") |
|
56 |
|
57 local player = nil |
|
58 local scored = 0 |
|
59 local shots = 0 |
|
60 local end_timer = 1000 |
|
61 local game_lost = false |
|
62 local time_goal = 0 |
|
63 local total_targets |
|
64 local targets |
|
65 local target_radar = false |
|
66 local next_target_circle = nil |
|
67 local gearsInGameCount = 0 |
|
68 local gearsInGame = {} |
|
69 |
|
70 --[[ |
|
71 TrainingMission(params) |
|
72 |
|
73 This function sets up the *entire* training mission and needs one argument: params. |
|
74 The argument “params” is a table containing fields which describe the training mission. |
|
75 mandatory fields: |
|
76 - missionTitle: the name of the mission |
|
77 - map: the name map to be used |
|
78 - theme: the name of the theme (does not need to be a standalone theme) |
|
79 - time: the time limit in milliseconds |
|
80 - ammoType: the ammo type of the weapon to be used |
|
81 - gearType: the gear type of the gear which is fired (used to count shots and re-center camera) |
|
82 - targets: The coordinates of where the targets will be spawned. |
|
83 It is a table containing tables containing coordinates of format |
|
84 { x=value, y=value }. The targets will be spawned in the same |
|
85 order as specified the coordinate tables appear. Example: |
|
86 targets = { |
|
87 { x = 324, y = 43 }, |
|
88 { x = 123, y = 56 }, |
|
89 { x = 6, y = 0 }, |
|
90 } |
|
91 There must be at least 1 target. |
|
92 |
|
93 optional fields: |
|
94 - wind: the initial wind (-100 to 100) (default: 0 (no wind)) |
|
95 - solidLand: weather the terrain is indestructible (default: false) |
|
96 - artillery: if true, the hog can’t move (default: false) |
|
97 - secGearType: cluster of projectile gear (if present) (used to re-center camera) |
|
98 - clanColor: color of the (only) clan (default: -1, default first clan color) |
|
99 - faceLeft: if true, hog starts facing left, otherwise right (default: false) |
|
100 - goalText: A short string explaining the goal of the mission |
|
101 (default: "Destroy all targets within the time!") |
|
102 - shootText: A string which says how many times the player shot, “%d” is replaced |
|
103 by the number of shots. (default: "You have shot %d times.") |
|
104 - useRadar Whether to use target radar (small circles that mark the position |
|
105 of the next target). (default: true). Note: Still needs to be unlocked. |
|
106 - radarTint: RGBA color of the target radar (default: 0xFF3030FF). Use this field |
|
107 if the target radar would be hard to see against the background. |
|
108 ]] |
|
109 |
|
110 |
|
111 local getTargetsScore = function() |
|
112 return scored * math.ceil(6000/#targets) |
|
113 end |
|
114 |
|
115 function TargetPracticeMission(params) |
|
116 if params.goalText == nil then params.goalText = loc("Eliminate all targets before your time runs out.|You have unlimited ammo for this mission.") end |
|
117 if params.shootText == nil then params.shootText = loc("You have shot %d times.") end |
|
118 if params.clanColor == nil then params.clanColor = -1 end |
|
119 if params.faceLeft == nil then params.faceLeft = false end |
|
120 if params.wind == nil then params.wind = 0 end |
|
121 if params.radarTint == nil then params.radarTint = 0xFF3030FF end |
|
122 if params.useRadar == nil then params.useRadar = true end |
|
123 |
|
124 local solid, artillery |
|
125 if params.solidLand == true then solid = gfSolidLand else solid = 0 end |
|
126 if params.artillery == true then artillery = gfArtillery else artillery = 0 end |
|
127 |
|
128 targets = params.targets |
|
129 |
|
130 total_targets = #targets |
|
131 |
|
132 _G.onAmmoStoreInit = function() |
|
133 SetAmmo(params.ammoType, 9, 0, 0, 0) |
|
134 end |
|
135 |
|
136 _G.onGameInit = function() |
|
137 Seed = 1 |
|
138 ClearGameFlags() |
|
139 local attackMode |
|
140 if (params.ammoType == amBee) then |
|
141 attackMode = gfInfAttack |
|
142 else |
|
143 attackMode = gfMultiWeapon |
|
144 end |
|
145 EnableGameFlags(gfDisableWind, attackMode, gfOneClanMode, solid, artillery) |
|
146 TurnTime = params.time |
|
147 Map = params.map |
|
148 Theme = params.theme |
|
149 Goals = params.goalText |
|
150 CaseFreq = 0 |
|
151 MinesNum = 0 |
|
152 Explosives = 0 |
|
153 -- Disable Sudden Death |
|
154 WaterRise = 0 |
|
155 HealthDecrease = 0 |
|
156 |
|
157 SetWind(params.wind) |
|
158 |
|
159 AddMissionTeam(params.clanColor) |
|
160 |
|
161 player = AddMissionHog(1) |
|
162 SetGearPosition(player, params.hog_x, params.hog_y) |
|
163 HogTurnLeft(player, params.faceLeft) |
|
164 |
|
165 local won = GetMissionVar("Won") |
|
166 -- Unlock the target radar when the player has completed |
|
167 -- the target practice before (any score). |
|
168 -- Target radar might be disabled by config, however. |
|
169 if won == "true" and params.useRadar == true then |
|
170 target_radar = true |
|
171 end |
|
172 |
|
173 end |
|
174 |
|
175 _G.onGameStart = function() |
|
176 SendHealthStatsOff() |
|
177 local recordInfo = getReadableChallengeRecord("Highscore") |
|
178 ShowMission(params.missionTitle, loc("Aiming practice"), params.goalText .. "|" .. recordInfo, -params.ammoType, 5000) |
|
179 SetTeamLabel(GetHogTeamName(player), "0") |
|
180 spawnTarget() |
|
181 end |
|
182 |
|
183 _G.onNewTurn = function() |
|
184 SetWeapon(params.ammoType) |
|
185 end |
|
186 |
|
187 _G.spawnTarget = function() |
|
188 -- Spawn next target |
|
189 local gear = AddGear(0, 0, gtTarget, 0, 0, 0, 0) |
|
190 |
|
191 local x = targets[scored+1].x |
|
192 local y = targets[scored+1].y |
|
193 |
|
194 SetGearPosition(gear, x, y) |
|
195 |
|
196 -- Target radar: Highlight position of the upcoming target. |
|
197 -- This must be unlocked by the player first. |
|
198 if target_radar then |
|
199 if (not next_target_circle) and targets[scored+2] then |
|
200 next_target_circle = AddVisualGear(0,0,vgtCircle,90,true) |
|
201 end |
|
202 if targets[scored+2] then |
|
203 SetVisualGearValues(next_target_circle, targets[scored+2].x, targets[scored+2].y, 205, 255, 1, 20, nil, nil, 3, params.radarTint) |
|
204 elseif next_target_circle then |
|
205 DeleteVisualGear(next_target_circle) |
|
206 next_target_circle = nil |
|
207 end |
|
208 end |
|
209 |
|
210 return gear |
|
211 end |
|
212 |
|
213 _G.onGameTick20 = function() |
|
214 if TurnTimeLeft < 40 and TurnTimeLeft > 0 and scored < total_targets and game_lost == false then |
|
215 game_lost = true |
|
216 AddCaption(loc("Time’s up!"), capcolDefault, capgrpGameState) |
|
217 SetHealth(player, 0) |
|
218 time_goal = 1 |
|
219 end |
|
220 |
|
221 if band(GetState(player), gstDrowning) == gstDrowning and game_lost == false and scored < total_targets then |
|
222 game_lost = true |
|
223 time_goal = 1 |
|
224 end |
|
225 |
|
226 if scored == total_targets or game_lost then |
|
227 if end_timer == 0 then |
|
228 generateStats() |
|
229 EndGame() |
|
230 if scored == total_targets then |
|
231 SetState(player, gstWinner) |
|
232 end |
|
233 end |
|
234 end_timer = end_timer - 20 |
|
235 end |
|
236 |
|
237 for gear, _ in pairs(gearsInGame) do |
|
238 if band(GetState(gear), gstDrowning) ~= 0 then |
|
239 -- Re-center camera on hog if projectile gears drown |
|
240 gearsInGame[gear] = nil |
|
241 gearsInGameCount = gearsInGameCount - 1 |
|
242 if gearsInGameCount == 0 and GetHealth(CurrentHedgehog) then |
|
243 FollowGear(CurrentHedgehog) |
|
244 end |
|
245 end |
|
246 end |
|
247 end |
|
248 |
|
249 _G.onGearAdd = function(gear) |
|
250 if GetGearType(gear) == params.gearType then |
|
251 shots = shots + 1 |
|
252 end |
|
253 if GetGearType(gear) == params.gearType or (params.secGearType and GetGearType(gear) == params.secGearType) then |
|
254 gearsInGameCount = gearsInGameCount + 1 |
|
255 gearsInGame[gear] = true |
|
256 end |
|
257 end |
|
258 |
|
259 _G.onGearDamage = function(gear, damage) |
|
260 if GetGearType(gear) == gtTarget then |
|
261 scored = scored + 1 |
|
262 SetTeamLabel(GetHogTeamName(player), tostring(getTargetsScore())) |
|
263 if scored < total_targets then |
|
264 AddCaption(string.format(loc("Targets left: %d"), (total_targets-scored)), capcolDefault, capgrpMessage) |
|
265 spawnTarget() |
|
266 else |
|
267 if not game_lost then |
|
268 SaveMissionVar("Won", "true") |
|
269 AddCaption(loc("You have destroyed all targets!"), capcolDefault, capgrpGameState) |
|
270 ShowMission(params.missionTitle, loc("Aiming practice"), loc("Congratulations! You have destroyed all targets within the time."), 0, 0) |
|
271 if shots <= scored then |
|
272 -- No misses! |
|
273 PlaySound(sndFlawless, player) |
|
274 else |
|
275 PlaySound(sndVictory, player) |
|
276 end |
|
277 SetEffect(player, heInvulnerable, 1) |
|
278 time_goal = TurnTimeLeft |
|
279 -- Disable control |
|
280 SetInputMask(0) |
|
281 AddAmmo(player, params.ammoType, 0) |
|
282 SetTurnTimePaused(true) |
|
283 end |
|
284 end |
|
285 end |
|
286 |
|
287 if GetGearType(gear) == gtHedgehog then |
|
288 if not game_lost then |
|
289 game_lost = true |
|
290 |
|
291 SetHealth(player, 0) |
|
292 time_goal = 1 |
|
293 end |
|
294 end |
|
295 end |
|
296 |
|
297 _G.onGearDelete = function(gear) |
|
298 if GetGearType(gear) == gtTarget and band(GetState(gear), gstDrowning) ~= 0 then |
|
299 AddCaption(loc("You lost your target, try again!"), capcolDefault, capgrpGameState) |
|
300 local newTarget = spawnTarget() |
|
301 local x, y = GetGearPosition(newTarget) |
|
302 local success = PlaceSprite(x, y + 24, sprAmGirder, 0, 0xFFFFFFFF, false, false, false) |
|
303 if not success then |
|
304 WriteLnToConsole("ERROR: Failed to spawn girder under respawned target!") |
|
305 end |
|
306 elseif gearsInGame[gear] then |
|
307 gearsInGame[gear] = nil |
|
308 gearsInGameCount = gearsInGameCount - 1 |
|
309 if gearsInGameCount == 0 and GetHealth(CurrentHedgehog) then |
|
310 -- Re-center camera to hog after all projectile gears were destroyed |
|
311 FollowGear(CurrentHedgehog) |
|
312 end |
|
313 end |
|
314 end |
|
315 |
|
316 _G.generateStats = function() |
|
317 local accuracy, accuracy_int |
|
318 if(shots > 0) then |
|
319 accuracy = (scored/shots)*100 |
|
320 accuracy_int = div(scored*100, shots) |
|
321 end |
|
322 local end_score_targets = getTargetsScore() |
|
323 local end_score_overall |
|
324 if not game_lost then |
|
325 local end_score_time = math.ceil(time_goal/(params.time/6000)) |
|
326 local end_score_accuracy = 0 |
|
327 if(shots > 0) then |
|
328 end_score_accuracy = math.ceil(accuracy * 60) |
|
329 end |
|
330 end_score_overall = end_score_time + end_score_targets + end_score_accuracy |
|
331 SetTeamLabel(GetHogTeamName(player), tostring(end_score_overall)) |
|
332 |
|
333 SendStat(siGameResult, loc("You have finished the target practice!")) |
|
334 |
|
335 SendStat(siCustomAchievement, string.format(loc("You have destroyed %d of %d targets (+%d points)."), scored, total_targets, end_score_targets)) |
|
336 SendStat(siCustomAchievement, string.format(params.shootText, shots)) |
|
337 if(shots > 0) then |
|
338 SendStat(siCustomAchievement, string.format(loc("Your accuracy was %.1f%% (+%d points)."), accuracy, end_score_accuracy)) |
|
339 end |
|
340 SendStat(siCustomAchievement, string.format(loc("You had %.1fs remaining on the clock (+%d points)."), (time_goal/1000), end_score_time)) |
|
341 if (not target_radar) and (#targets > 1) and (params.useRadar == true) then |
|
342 SendStat(siCustomAchievement, loc("You have unlocked the target radar!")) |
|
343 end |
|
344 |
|
345 if(shots > 0) then |
|
346 updateChallengeRecord("AccuracyRecord", accuracy_int) |
|
347 end |
|
348 else |
|
349 SendStat(siGameResult, loc("Challenge over!")) |
|
350 |
|
351 SendStat(siCustomAchievement, string.format(loc("You have destroyed %d of %d targets (+%d points)."), scored, total_targets, end_score_targets)) |
|
352 SendStat(siCustomAchievement, string.format(params.shootText, shots)) |
|
353 if(shots > 0) then |
|
354 SendStat(siCustomAchievement, string.format(loc("Your accuracy was %.1f%%."), accuracy)) |
|
355 end |
|
356 end_score_overall = end_score_targets |
|
357 end |
|
358 SendStat(siPointType, "!POINTS") |
|
359 SendStat(siPlayerKills, tostring(end_score_overall), GetHogTeamName(player)) |
|
360 -- Update highscore |
|
361 updateChallengeRecord("Highscore", end_score_overall) |
|
362 end |
|
363 end |