|
1 --[=[ |
|
2 = Simple Mission Framework for Hedgewars = |
|
3 |
|
4 This is a simple library intended to make setting up simple missions an |
|
5 easy task for Lua scripters. The entire game logic and coding is |
|
6 abtracted away in a single function which you just need to feed |
|
7 a large definition table in which you define gears, goals, etc. |
|
8 |
|
9 This is ideal for missions in which you set up the entire scenario |
|
10 from the start and don't need any complex in-mission events. |
|
11 BUT! This is NOT suited for missions with scripted events, cut-scenes, |
|
12 branching story, etc. |
|
13 |
|
14 This library has the following features: |
|
15 * Add teams, clans, hogs |
|
16 * Spawn gears |
|
17 * Sensible defaults for almost everything |
|
18 * Set custom goals or use the default one (kill all enemies) |
|
19 * Add non-goals to fail the mission |
|
20 * Checks victory and failure automatically |
|
21 |
|
22 To use this library, you first have to load it and to call SimpleMission once with |
|
23 the appropriate parameters. |
|
24 See the comment of SimpleMission for a specification of all parameters. |
|
25 |
|
26 ]=] |
|
27 |
|
28 HedgewarsScriptLoad("/Scripts/Locale.lua") |
|
29 HedgewarsScriptLoad("/Scripts/Tracker.lua") |
|
30 HedgewarsScriptLoad("/Scripts/Utils.lua") |
|
31 |
|
32 --[[ |
|
33 SimpleMission(params) |
|
34 |
|
35 This function sets up the *entire* mission and needs one argument: params. |
|
36 The argument “params” is a table containing fields which describe the mission. |
|
37 |
|
38 Mandatory fields: |
|
39 - teams: Table of teams. There must be 1-8 teams. |
|
40 |
|
41 Optional fields |
|
42 - ammoConfig Table containing basic ammo values (default: infinite skip only) |
|
43 - initVars Table where you set up environment parameters such as MinesNum. |
|
44 - wind If set, the wind will permanently set to this value (-100..100). Implies gfDisableWind |
|
45 - gears: Table of objects. |
|
46 - girders Table of girders |
|
47 - rubbers Table of rubbers |
|
48 |
|
49 AMMO |
|
50 - ammoType ammo type |
|
51 - delay delay (default: 0) |
|
52 - numberInCrate ammo per crate (default: 1) |
|
53 - count default starter ammo for everyone, 9 for infinite (default: 0) |
|
54 - probability probability in crates (default: 0) |
|
55 |
|
56 TEAM DATA |
|
57 - isMissionTeam if true, this is the player's chosen team for this mission (default: false) |
|
58 - hogs table of hedgehogs in this team (must contain at least 1 hog) |
|
59 - clanID ID of the clan to which this team belongs to. Counting starts at 0. |
|
60 By default, each team goes into its own clan. |
|
61 Important: The clan of the player and allies MUST be 0. |
|
62 Important: You MUST either set the clan ID explicitly for all teams or none of them. |
|
63 These arguments will be ignored if this is a mission team: |
|
64 - name team name |
|
65 - flag flag name (default: hedgewars) |
|
66 - grave grave name (has default grave for each team) |
|
67 - fort fort name (default: Castle) |
|
68 - voice voicepack (default: Default_qau) |
|
69 |
|
70 HEDGEHOG DATA: |
|
71 - id optional identifier for goals |
|
72 - health hog health (default: 100) |
|
73 - ammo table of ammo types |
|
74 - x, y hog position (default: spawns randomly on land) |
|
75 - poisoned if true, hedgehog starts poisoned with 5 poison damage. Set to a number for other poison damage (default: false) |
|
76 - frozen if true, hedgehogs starts frozen (default: false) |
|
77 - faceLeft initial facing direction. true=left, false=false (default: false) |
|
78 These arguments will be ignored if the hog is in a mission team: |
|
79 - name hog name |
|
80 - botLevel 1-5: Bot level (lower=stronger). 0=human player (default: 0) |
|
81 - hat hat name (default: NoHat) |
|
82 |
|
83 GEAR TYPES: |
|
84 - type gear type |
|
85 ALL types: |
|
86 id optional identifier for goals |
|
87 x x coordinate of starting position (default: 0) |
|
88 y y coordinate of starting position (default: 0) |
|
89 dx initial x speed (default: 0) |
|
90 dy initial y speed (default: 0) |
|
91 - type=gtMine Mine |
|
92 timer Mine timer (only for non-duds). Default: MinesTime |
|
93 isDud Whether the mine is a dud. default: false |
|
94 isFrozen Whether the mine is frozen. If true, it implies being a dud as well. Default: false |
|
95 health Initial health of dud mines. Has no effect if isDud=false. Default: 36 |
|
96 - type=gtSMine Sticky mine |
|
97 timer Timer. Default: 500 |
|
98 - type=gtAirMine Air mine |
|
99 timer Timer. Default: (MinesTime/1000 * 250) |
|
100 - type=gtExplosives Barrel |
|
101 health Initial health. Default: 60 |
|
102 isFrozen Whether the barrel is frozen. Default: true with health > 60, false otherwise |
|
103 isRolling Whether the barrel starts in “rolling” state. Default: false |
|
104 - type=gtCase Crate |
|
105 crateType "health": Health crate |
|
106 "supply": Ammo or utility crate (select crate type automatically) |
|
107 "supply_ammo_explicit": Ammo crate (not recommened) |
|
108 "supply_utility_explicit": Utility crate (not recommededn) |
|
109 ammoType Contained ammo (only for ammo and utility crates). |
|
110 health Contained health (only for health crates). Default: HealthCaseAmount |
|
111 isFrozen Whether the crate is frozen. Default: false |
|
112 - type=gtKnife Cleaver |
|
113 - type=gtTarget Target |
|
114 |
|
115 GOALS: |
|
116 Note: If there are at least two opposing teams, a default goal is used, which is to defeat all the enemies of the |
|
117 player's team. If this is what you want, you can skip this section. |
|
118 |
|
119 The default goal is overwritten as if customGoals has been set. Set customGoals and other related parameters for |
|
120 defining your own special goals. In this case, the mission is won if all customGoals are completed. |
|
121 Note the mission will always fail if the player's hedgehogs and all their allies have been defeated. |
|
122 If there is only one team (for the player), there is no default goal and one must be set explicitly. |
|
123 - customGoals Table of custom goals (see below). All of them must be met to win. Some goal types might fail, |
|
124 rendering the mission unwinnable and leading to the loss of the mission. An example is |
|
125 blowing up a crate which you should have collected.ed. |
|
126 - customNonGoals Table of non-goals, the player loses if one of them is achieved |
|
127 - customGoalCheck When to check goals and non-goals. Values: "instant" (default), "turnStart", "turnEnd" |
|
128 |
|
129 - missionTitle: The name of the mission (highly recommended) |
|
130 - missionIcon: Icon of the mission panel, see documentation of ShowMission in the Lua API |
|
131 - goalText: A short string explaining the goal of the mission (use this if you set custom goals). |
|
132 |
|
133 GOAL TYPES: |
|
134 - type name of goal type |
|
135 - failText Optional. For non-goals, this text will be shown in the stats if mission fails due to this non-goal |
|
136 being completed. For goals which fail, this text will be displayed at failure. Note that |
|
137 all normal goals have sensible default fail texts. |
|
138 - type="destroy" Gear must be destroyed |
|
139 - id Gear to destroy |
|
140 - type="teamDefeat" Team must be defeated |
|
141 - teamName Name of team to defeat |
|
142 - type="collect" Crate must be collected |
|
143 FAIL CONDITION: Crate taken by enemy, or destroyed |
|
144 - id ID of crate gear to collect |
|
145 - collectors Optional table of gear IDs, any one of which must collect the gear (but nobody else!). |
|
146 By default, this is for the player's teams and allies. |
|
147 - type="turns" Achieved when a number of turns has been played |
|
148 - turns Number of played turns |
|
149 - type="rounds" Achieved when a number of rounds has been played |
|
150 - rounds Number of played rounds |
|
151 - type="suddenDeath" Sudden Death has started |
|
152 - type="inZone" A gear is within given coordinate bounds. Each of xMin, xMax, yMin and yMax is a sub-goal. |
|
153 Each sub-goal is only checked if not nil. |
|
154 You can use this to check if a gear left, right, above or below a given coordinate. |
|
155 To check if the gear is within a rectangle, just set all 4 sub-goals. |
|
156 FAIL CONDITION: Gear destroyed |
|
157 - id Gear to watch |
|
158 - xMin gear's X coordinate must be lower than this |
|
159 - xMax gear's X coordinate must be higher than this |
|
160 - yMin gear's Y coordinate must be lower than this |
|
161 - yMax gear's Y coordinate must be higher than this |
|
162 - type="distGearPos" Distance between a gear and a fixed position |
|
163 FAIL CONDITION: Gear destroyed |
|
164 - distance goal distance to compare to |
|
165 - relationship "greaterThan" or "smallerThan" |
|
166 - id gear to watch |
|
167 - x x coordinate to reach |
|
168 - y y coordinate to reach |
|
169 - type="distGearGear" Distance between two gears |
|
170 FAIL CONDITION: Any of both gears destroyed |
|
171 - distance goal distance to compare to |
|
172 - relationship "greaterThan" or "smallerThan" |
|
173 - id1 first gear to compare |
|
174 - id2 second gear to compare |
|
175 - type="damage" Gear took damage or was destroyed |
|
176 - id Gear to watch |
|
177 - damage Minimum amount of damage to take at a single blow. Default: 1 |
|
178 - canDestroy If false, this goal will fail if the gear was destroyed without taking the required damage |
|
179 - type="drown" Gear has drowned |
|
180 FAIL CONDITION: Gear destroyed by other means |
|
181 - id Gear to watch |
|
182 - type="poison" Gear must be poisoned |
|
183 FAIL CONDITION: Gear destroyed |
|
184 - id Gear to be poisoned |
|
185 - type="cure" Gear must exist and be free from poisoning |
|
186 FAIL CONDITION: Gear destroyed |
|
187 - id Gear to check |
|
188 - type="freeze" Gear must exist and be frozen |
|
189 FAIL CONDITION: Gear destroyed |
|
190 - id Gear to be frozen |
|
191 - type="melt" Gear must exist and be unfrozen |
|
192 FAIL CONDITION: Gear destroyed |
|
193 - id Gear to check |
|
194 - type="waterSkip" Gear must have skipped over water |
|
195 FAIL CONDITION: Gear destroyed before it reached the required number of skips |
|
196 - id |
|
197 - skips Total number of water skips required at least (default: 1) |
|
198 |
|
199 ]] |
|
200 |
|
201 local goals |
|
202 local teamHogs = {} |
|
203 |
|
204 --[[ |
|
205 HELPER VARIABLES |
|
206 ]] |
|
207 |
|
208 local defaultGraves = { |
|
209 "Grave", "Statue", "pyramid", "Simple", "skull", "Badger", "Duck2", "Flower" |
|
210 } |
|
211 local defaultFlags = { |
|
212 "hedgewars", "cm_birdy", "cm_eyes", "cm_spider", "cm_kiwi", "cm_scout", "cm_skull", "cm_bars" |
|
213 } |
|
214 |
|
215 -- Utility functions |
|
216 |
|
217 -- Returns value if it is non-nil, otherwise returns default |
|
218 local function def(value, default) |
|
219 if value == nil then |
|
220 return default |
|
221 else |
|
222 return value |
|
223 end |
|
224 end |
|
225 |
|
226 local errord = false |
|
227 |
|
228 -- This function generates the mission. See above for the meaning of params. |
|
229 function SimpleMission(params) |
|
230 if params.missionTitle == nil then |
|
231 params.missionTitle = loc("Scenario") |
|
232 end |
|
233 if params.missionIcon == nil then |
|
234 params.missionIcon = 1 -- target icon |
|
235 end |
|
236 if params.goalText == nil then |
|
237 params.goalText = loc("Eliminate the enemy.") |
|
238 end |
|
239 if params.customGoalCheck == nil and (params.customGoals ~= nil or params.customNonGoals ~= nil) then |
|
240 params.customGoalCheck = "instant" |
|
241 end |
|
242 |
|
243 _G.sm = {} |
|
244 |
|
245 _G.sm.isInSuddenDeath = false |
|
246 |
|
247 -- Number of completed turns |
|
248 _G.sm.gameTurns = 0 |
|
249 |
|
250 _G.sm.goalGears = {} |
|
251 |
|
252 _G.sm.params = params |
|
253 |
|
254 _G.sm.gameEnded = false |
|
255 |
|
256 _G.sm.playerClan = 0 |
|
257 |
|
258 _G.sm.wonVarWritten = false |
|
259 |
|
260 _G.sm.makeStats = function(winningClan, customAchievements) |
|
261 for t=0, TeamsCount-1 do |
|
262 local team = GetTeamName(t) |
|
263 local stats = GetTeamStats(team) |
|
264 local clan = GetTeamClan(team) |
|
265 if clan == winningClan then |
|
266 SendStat(siPlayerKills, stats.Kills, team) |
|
267 end |
|
268 end |
|
269 for t=0, TeamsCount-1 do |
|
270 local team = GetTeamName(t) |
|
271 local stats = GetTeamStats(team) |
|
272 local clan = GetTeamClan(team) |
|
273 if clan ~= winningClan then |
|
274 SendStat(siPlayerKills, stats.Kills, team) |
|
275 end |
|
276 end |
|
277 if customAchievements ~= nil then |
|
278 for a=1, #customAchievements do |
|
279 SendStat(siCustomAchievement, customAchievements[a]) |
|
280 end |
|
281 end |
|
282 end |
|
283 |
|
284 _G.sm.criticalGearFailText = function(gearSmid) |
|
285 local gear = _G.sm.goalGears[gearSmid] |
|
286 if GetGearType(gear) == gtHedgehog then |
|
287 return string.format(loc("%s is dead, who was critical to this mission!"), GetHogName(gear)) |
|
288 else |
|
289 return loc("We have lost an object which was critical to this mission.") |
|
290 end |
|
291 end |
|
292 |
|
293 _G.sm.checkGoal = function(goal) |
|
294 if goal.type == "destroy" then |
|
295 return getGearValue(_G.sm.goalGears[goal.id], "sm_destroyed") |
|
296 elseif goal.type == "collect" then |
|
297 local collector = getGearValue(_G.sm.goalGears[goal.id], "sm_collected") |
|
298 if collector then |
|
299 if not goal.collectors then |
|
300 if GetHogClan(collector) == _G.sm.playerClan then |
|
301 return true |
|
302 else |
|
303 -- Fail if the crate was collected by enemy |
|
304 return "fail", loc("The enemy has taken a crate which we really needed!") |
|
305 end |
|
306 else |
|
307 for c=1, #goal.collectors do |
|
308 if _G.sm.goalGears[goal.collectors[c]] == collector then |
|
309 return true |
|
310 end |
|
311 end |
|
312 -- Fail if the crate was collected by someone who was not supposed to get it |
|
313 return "fail", loc("The wrong hedgehog has taken the crate.") |
|
314 end |
|
315 else |
|
316 -- Fail goal if crate was destroyed |
|
317 if getGearValue(_G.sm.goalGears[goal.id], "sm_destroyed") then |
|
318 return "fail", loc("A crate critical to this mission has been destroyed.") |
|
319 end |
|
320 return false |
|
321 end |
|
322 elseif goal.type == "turns" then |
|
323 return sm.gameTurns >= goal.turns |
|
324 elseif goal.type == "rounds" then |
|
325 return (TotalRounds) >= goal.rounds |
|
326 elseif goal.type == "inZone" then |
|
327 if getGearValue(_G.sm.goalGears[goal.id], "sm_destroyed") then |
|
328 return "fail", _G.sm.criticalGearFailText(goal.id) |
|
329 end |
|
330 local gX, gY = GetGearPosition(_G.sm.goalGears[goal.id]) |
|
331 -- 4 sub-goals, each optional |
|
332 local g1 = (not goal.xMin) or gX >= goal.xMin |
|
333 local g2 = (not goal.xMax) or gX <= goal.xMax |
|
334 local g3 = (not goal.yMin) or gY >= goal.yMin |
|
335 local g4 = (not goal.yMax) or gY <= goal.yMax |
|
336 return g1 and g2 and g3 and g4 |
|
337 elseif goal.type == "distGearPos" or goal.type == "distGearGear" then |
|
338 local gX, gY, tX, tY |
|
339 if goal.type == "distGearPos" then |
|
340 if getGearValue(_G.sm.goalGears[goal.id], "sm_destroyed") then |
|
341 -- Fail if gear was destroyed |
|
342 return "fail", _G.sm.criticalGearFailText(goal.id) |
|
343 end |
|
344 gX, gY = GetGearPosition(_G.sm.goalGears[goal.id]) |
|
345 tX, tY = goal.x, goal.y |
|
346 elseif goal.type == "distGearGear" then |
|
347 -- Fail if one of the gears was destroyed |
|
348 if getGearValue(_G.sm.goalGears[goal.id1], "sm_destroyed") then |
|
349 return "fail", _G.sm.criticalGearFailText(goal.id1) |
|
350 elseif getGearValue(_G.sm.goalGears[goal.id2], "sm_destroyed") then |
|
351 return "fail", _G.sm.criticalGearFailText(goal.id2) |
|
352 end |
|
353 gX, gY = GetGearPosition(_G.sm.goalGears[goal.id1]) |
|
354 tX, tY = GetGearPosition(_G.sm.goalGears[goal.id2]) |
|
355 end |
|
356 |
|
357 local h = integerHypotenuse(gX - tX, gY - tY) |
|
358 if goal.relationship == "smallerThan" then |
|
359 return h < goal.distance |
|
360 elseif goal.relationship == "greaterThan" then |
|
361 return h > goal.distance |
|
362 end |
|
363 -- Invalid parameters! |
|
364 error("SimpleMission: Invalid parameters for distGearPos/distGearGear!") |
|
365 errord = true |
|
366 return false |
|
367 elseif goal.type == "suddenDeath" then |
|
368 return sm.isInSuddenDeath |
|
369 elseif goal.type == "damage" then |
|
370 local damage = goal.damage or 1 |
|
371 local gear = _G.sm.goalGears[goal.id] |
|
372 local tookEnoughDamage = getGearValue(gear, "sm_maxDamage") >= damage |
|
373 if getGearValue(gear, "sm_destroyed") then |
|
374 -- Fail if gear was destroyed without taking enough damage first |
|
375 if not tookEnoughDamage and goal.canDestroy == false then |
|
376 if GetGearType(gear) == gtHedgehog then |
|
377 return "fail", string.format(loc("%s has been killed before taking enough damage first."), GetHogName(gear)) |
|
378 else |
|
379 return "fail", loc("An object has been destroyed before it took enough damage.") |
|
380 end |
|
381 else |
|
382 -- By default, succeed if gear was destroyed |
|
383 return true |
|
384 end |
|
385 end |
|
386 return tookEnoughDamage |
|
387 elseif goal.type == "drown" then |
|
388 local drowned = getGearValue(_G.sm.goalGears[goal.id], "sm_drowned") |
|
389 -- Fail if gear was destroyed by something other than drowning |
|
390 if not drowned and getGearValue(_G.sm.goalGears[goal.id], "sm_destroyed") then |
|
391 return "fail", _G.sm.criticalGearFailText(goal.id) |
|
392 end |
|
393 return drowned |
|
394 elseif goal.type == "poison" then |
|
395 if getGearValue(_G.sm.goalGears[goal.id], "sm_destroyed") then |
|
396 return "fail", _G.sm.criticalGearFailText(goal.id) |
|
397 end |
|
398 return GetEffect(_G.sm.goalGears[goal.id], hePoisoned) >= 1 |
|
399 elseif goal.type == "freeze" then |
|
400 if getGearValue(_G.sm.goalGears[goal.id], "sm_destroyed") then |
|
401 return "fail", _G.sm.criticalGearFailText(goal.id) |
|
402 end |
|
403 return GetEffect(_G.sm.goalGears[goal.id], heFrozen) >= 256 |
|
404 elseif goal.type == "cure" then |
|
405 if getGearValue(_G.sm.goalGears[goal.id], "sm_destroyed") then |
|
406 return "fail", _G.sm.criticalGearFailText(goal.id) |
|
407 end |
|
408 return GetEffect(_G.sm.goalGears[goal.id], hePoisoned) == 0 |
|
409 elseif goal.type == "melt" then |
|
410 if getGearValue(_G.sm.goalGears[goal.id], "sm_destroyed") then |
|
411 return "fail", _G.sm.criticalGearFailText(goal.id) |
|
412 end |
|
413 return GetEffect(_G.sm.goalGears[goal.id], heFrozen) == 0 |
|
414 elseif goal.type == "waterSkip" then |
|
415 local skips = goal.skips or 1 |
|
416 local hasEnoughSkips = getGearValue(_G.sm.goalGears[goal.id], "sm_waterSkips") >= skips |
|
417 -- Fail if gear was destroyed before it got the required number of skips |
|
418 if not hasEnoughSkips and getGearValue(_G.sm.goalGears[goal.id], "sm_destroyed") then |
|
419 return "fail", _G.sm.criticalGearFailText(goal.id) |
|
420 end |
|
421 return hasEnoughSkips |
|
422 elseif goal.type == "teamDefeat" then |
|
423 return #teamHogs[goal.teamName] == 0 |
|
424 else |
|
425 return false |
|
426 end |
|
427 end |
|
428 |
|
429 --[[ Checks the custom goals. |
|
430 Returns true when all custom goals are met. |
|
431 Returns false when not all custom goals are met. |
|
432 Returns "fail" if any of the goals has failed (i.e. is impossible to complete). |
|
433 Returns nil when there are no custom goals ]] |
|
434 _G.sm.checkGoals = function() |
|
435 if params.customGoals ~= nil and #params.customGoals > 0 then |
|
436 for key, goal in pairs(params.customGoals) do |
|
437 local done, defaultFailText = _G.sm.checkGoal(goal) |
|
438 if done == false or done == "fail" then |
|
439 local failText |
|
440 if goal.failText then |
|
441 failText = goal.failText |
|
442 else |
|
443 failText = customFailText |
|
444 end |
|
445 return done, failText |
|
446 end |
|
447 end |
|
448 return true |
|
449 else |
|
450 return nil |
|
451 end |
|
452 end |
|
453 |
|
454 --[[ Checks the custom non-goals. |
|
455 Returns true when any non-goal is met. |
|
456 Returns false otherwise. ]] |
|
457 _G.sm.checkNonGoals = function() |
|
458 if params.customNonGoals ~= nil and #params.customNonGoals > 0 then |
|
459 for key, nonGoal in pairs(params.customNonGoals) do |
|
460 local done = _G.sm.checkGoal(nonGoal) |
|
461 if done == true then |
|
462 return true, nonGoal.failText |
|
463 end |
|
464 end |
|
465 end |
|
466 return false |
|
467 end |
|
468 |
|
469 -- Declare the game ended if all enemy teams are dead and player teams or allies are still alive |
|
470 _G.sm.checkRegularVictory = function() |
|
471 local victory = true |
|
472 for t=0, TeamsCount-1 do |
|
473 local team = GetTeamName(t) |
|
474 local defeat = _G.sm.checkGoal({type="teamDefeat", teamName=team}) |
|
475 if not defeat then |
|
476 -- Deep check, also look at damage of all hogs |
|
477 local dead = 0 |
|
478 for h=1, #teamHogs[team] do |
|
479 local _,_,_,_,_,_,_,_,_,_,_,Damage = GetGearValues(teamHogs[team][h]) |
|
480 if Damage >= GetHealth(teamHogs[team][h]) then |
|
481 dead = dead + 1 |
|
482 end |
|
483 end |
|
484 if dead >= #teamHogs[team] then |
|
485 defeat = true |
|
486 end |
|
487 end |
|
488 if (defeat == true) and (GetTeamClan(team) == _G.sm.playerClan) then |
|
489 victory = false |
|
490 break |
|
491 elseif (defeat == false) and (GetTeamClan(team) ~= _G.sm.playerClan) then |
|
492 victory = false |
|
493 break |
|
494 end |
|
495 end |
|
496 if victory then |
|
497 _G.sm.gameEnded = true |
|
498 end |
|
499 end |
|
500 |
|
501 -- Checks goals and non goals and wins or loses mission |
|
502 _G.sm.checkWinOrFail = function() |
|
503 if errord then |
|
504 return |
|
505 end |
|
506 local nonGoalStatus, nonGoalFailText = _G.sm.checkNonGoals() |
|
507 local goalStatus, goalFailText = _G.sm.checkGoals() |
|
508 if nonGoalStatus == true then |
|
509 _G.sm.lose(nonGoalFailText) |
|
510 elseif goalStatus == "fail" then |
|
511 _G.sm.lose(goalText) |
|
512 elseif goalStatus == true then |
|
513 _G.sm.win() |
|
514 end |
|
515 end |
|
516 |
|
517 _G.sm.win = function() |
|
518 if not _G.sm.gameEnded then |
|
519 _G.sm.gameEnded = true |
|
520 if not _G.sm.wonVarWritten then |
|
521 SaveMissionVar("Won", "true") |
|
522 _G.sm.wonVarWritten = true |
|
523 end |
|
524 AddCaption(loc("Victory!"), capcolDefault, capgrpGameState) |
|
525 SendStat(siGameResult, loc("Mission succeeded!")) |
|
526 _G.sm.makeStats(_G.sm.playerClan) |
|
527 EndGame() |
|
528 if GetHogLevel(CurrentHedgehog) == 0 then |
|
529 for team, hog in pairs(teamHogs[GetHogTeamName(CurrentHedgehog)]) do |
|
530 SetState(hog, gstWinner) |
|
531 PlaySound(sndVictory, hog) |
|
532 end |
|
533 end |
|
534 end |
|
535 end |
|
536 |
|
537 _G.sm.lose = function(failReason) |
|
538 if not _G.sm.gameEnded then |
|
539 _G.sm.gameEnded = true |
|
540 AddCaption(loc("Mission failed!"), capcolDefault, capgrpGameState) |
|
541 SendStat(siGameResult, loc("Mission failed!")) |
|
542 if failReason then |
|
543 SendStat(siCustomAchievement, failReason) |
|
544 end |
|
545 if GetHogLevel(CurrentHedgehog) == 0 then |
|
546 SetState(CurrentHedgehog, bor(GetState(CurrentHedgehog), gstLoser)) |
|
547 SetState(CurrentHedgehog, band(GetState(CurrentHedgehog), bnot(gstHHDriven))) |
|
548 end |
|
549 local clan = ClansCount-1 |
|
550 for t=0, TeamsCount-1 do |
|
551 local team = GetTeamName(t) |
|
552 -- Just declare any living team other than the player team the winner |
|
553 if (_G.sm.checkGoal({type="teamDefeat", teamName=team}) == false) and (GetTeamClan(team) ~= _G.sm.playerClan) then |
|
554 clan = GetTeamClan(team) |
|
555 break |
|
556 end |
|
557 end |
|
558 _G.sm.makeStats(clan) |
|
559 EndGame() |
|
560 end |
|
561 end |
|
562 |
|
563 _G.onSuddenDeath = function() |
|
564 _G.sm.isInSuddenDeath = true |
|
565 end |
|
566 |
|
567 _G.onGearWaterSkip = function(gear) |
|
568 increaseGearValue(gear, "sm_waterSkips") |
|
569 end |
|
570 |
|
571 _G.onGearAdd = function(gear) |
|
572 if GetGearType(gear) == gtHedgehog then |
|
573 local team = GetHogTeamName(gear) |
|
574 if teamHogs[team] == nil then |
|
575 teamHogs[team] = {} |
|
576 end |
|
577 table.insert(teamHogs[GetHogTeamName(gear)], gear) |
|
578 end |
|
579 setGearValue(gear, "sm_waterSkips", 0) |
|
580 setGearValue(gear, "sm_maxDamage", 0) |
|
581 setGearValue(gear, "sm_drowned", false) |
|
582 setGearValue(gear, "sm_destroyed", false) |
|
583 end |
|
584 |
|
585 _G.onGearResurrect = function(gear) |
|
586 if GetGearType(gear) == gtHedgehog then |
|
587 table.insert(teamHogs[GetHogTeamName(gear)], gear) |
|
588 end |
|
589 setGearValue(gear, "sm_destroyed", false) |
|
590 end |
|
591 |
|
592 _G.onGearDelete = function(gear) |
|
593 if GetGearType(gear) == gtCase and band(GetGearMessage(gear), gmDestroy) ~= 0 then |
|
594 -- Set ID of collector |
|
595 setGearValue(gear, "sm_collected", CurrentHedgehog) |
|
596 end |
|
597 if GetGearType(gear) == gtHedgehog then |
|
598 local team = GetHogTeamName(gear) |
|
599 local hogList = teamHogs[team] |
|
600 for h=1, #hogList do |
|
601 if hogList[h] == gear then |
|
602 table.remove(hogList, h) |
|
603 break |
|
604 end |
|
605 end |
|
606 end |
|
607 if band(GetState(gear), gstDrowning) ~= 0 then |
|
608 setGearValue(gear, "sm_drowned", true) |
|
609 end |
|
610 setGearValue(gear, "sm_destroyed", true) |
|
611 end |
|
612 |
|
613 _G.onGearDamage = function(gear, damage) |
|
614 local currentDamage = getGearValue(gear, "sm_maxDamage") |
|
615 if damage > currentDamage then |
|
616 setGearValue(gear, "sm_maxDamage", damage) |
|
617 end |
|
618 end |
|
619 |
|
620 _G.onGameInit = function() |
|
621 CaseFreq = 0 |
|
622 WaterRise = 0 |
|
623 HealthDecrease = 0 |
|
624 MinesNum = 0 |
|
625 Explosives = 0 |
|
626 |
|
627 for initVarName, initVarValue in pairs(params.initVars) do |
|
628 if initVarName == "GameFlags" then |
|
629 EnableGameFlags(initVarValue) |
|
630 else |
|
631 _G[initVarName] = initVarValue |
|
632 end |
|
633 end |
|
634 if #params.teams == 1 then |
|
635 EnableGameFlags(gfOneClanMode) |
|
636 end |
|
637 if params.wind then |
|
638 EnableGameFlags(gfDisableWind) |
|
639 end |
|
640 |
|
641 local clanCounter = 0 |
|
642 for teamID, teamData in pairs(params.teams) do |
|
643 local name, clanID, grave, fort, voice, flag |
|
644 name = def(teamData.name, string.format(loc("Team %d"), teamID)) |
|
645 if teamData.clanID == nil then |
|
646 clanID = clanCounter |
|
647 clanCounter = clanCounter + 1 |
|
648 else |
|
649 clanID = teamData.clanID |
|
650 end |
|
651 |
|
652 local realName |
|
653 if teamData.isMissionTeam then |
|
654 realName = AddMissionTeam(-(clanID+1)) |
|
655 _G.sm.playerClan = clanID |
|
656 else |
|
657 grave = def(teamData.grave, defaultGraves[math.min(teamID, 8)]) |
|
658 fort = def(teamData.fort, "Castle") |
|
659 voice = def(teamData.voice, "Default_qau") |
|
660 flag = def(teamData.flag, defaultFlags[math.min(teamID, 8)]) |
|
661 |
|
662 realName = AddTeam(name, -(clanID+1), grave, fort, voice, flag) |
|
663 end |
|
664 |
|
665 -- Update all teamDefeat goals if the real team name differs from the |
|
666 -- team configuration. |
|
667 -- (AddTeam might change the name due to naming collisions) |
|
668 if name ~= realName then |
|
669 local checks = { params.customGoals, params.customNonGoals } |
|
670 for c=1, 2 do |
|
671 if checks[c] then |
|
672 for k,goal in pairs(checks[c]) do |
|
673 if goal.type == "teamDefeat" and goal.teamName == name then |
|
674 goal.teamName = realName |
|
675 end |
|
676 end |
|
677 end |
|
678 end |
|
679 end |
|
680 |
|
681 for hogID, hogData in pairs(teamData.hogs) do |
|
682 local name, botLevel, health, hat |
|
683 name = def(hogData.name, string.format(loc("Hog %d"), hogID)) |
|
684 botLevel = def(hogData.botLevel, 0) |
|
685 health = def(hogData.health, 100) |
|
686 hat = def(hogData.hat, "NoHat") |
|
687 local hog |
|
688 if teamData.isMissionTeam then |
|
689 hog = AddMissionHog(health) |
|
690 else |
|
691 hog = AddHog(name, botLevel, health, hat) |
|
692 end |
|
693 if hogData.x ~= nil and hogData.y ~= nil then |
|
694 SetGearPosition(hog, hogData.x, hogData.y) |
|
695 end |
|
696 if hogData.faceLeft then |
|
697 HogTurnLeft(hog, true) |
|
698 end |
|
699 if hogData.poisoned == true then |
|
700 SetEffect(hog, hePoisoned, 5) |
|
701 elseif type(hogData.poisoned) == "number" then |
|
702 SetEffect(hog, hePoisoned, hogData.poisoned) |
|
703 end |
|
704 if hogData.frozen then |
|
705 SetEffect(hog, heFrozen, 199999) |
|
706 end |
|
707 |
|
708 if hog ~= nil and hogData.id ~= nil then |
|
709 _G.sm.goalGears[hogData.id] = hog |
|
710 setGearValue(hog, "sm_id", hogData.id) |
|
711 end |
|
712 |
|
713 -- Remember this hedgehog's gear ID for later use |
|
714 hogData.gearID = hog |
|
715 end |
|
716 end |
|
717 end |
|
718 |
|
719 _G.onNewTurn = function() |
|
720 _G.sm.gameStarted = true |
|
721 |
|
722 if params.customGoalCheck == "turnStart" then |
|
723 _G.sm.checkRegularVictory() |
|
724 _G.sm.checkWinOrFail() |
|
725 end |
|
726 end |
|
727 |
|
728 _G.onEndTurn = function() |
|
729 _G.sm.gameTurns = _G.sm.gameTurns + 1 |
|
730 |
|
731 if params.customGoalCheck == "turnEnd" then |
|
732 _G.sm.checkRegularVictory() |
|
733 _G.sm.checkWinOrFail() |
|
734 end |
|
735 end |
|
736 |
|
737 _G.onGameResult = function(winningClan) |
|
738 if (params.customGoals == nil) and (not _G.sm.wonVarWritten) and (winningClan == _G.sm.playerClan) then |
|
739 SendStat(siGameResult, loc("Mission succeeded!")) |
|
740 SaveMissionVar("Won", "true") |
|
741 _G.sm.wonVarWritten = true |
|
742 else |
|
743 SendStat(siGameResult, loc("Mission failed!")) |
|
744 end |
|
745 end |
|
746 |
|
747 _G.onAmmoStoreInit = function() |
|
748 local ammoTypesDone = {} |
|
749 -- Read script's stated ammo wishes |
|
750 if params.ammoConfig ~= nil then |
|
751 for ammoType, v in pairs(params.ammoConfig) do |
|
752 SetAmmo(ammoType, def(v.count, 0), def(v.probability, 0), def(v.delay, 0), def(v.numberInCrate, 1)) |
|
753 ammoTypesDone[ammoType] = true |
|
754 end |
|
755 end |
|
756 -- Apply default values for all ammo types which have not been set |
|
757 for a=0, AmmoTypeMax do |
|
758 if a ~= amNothing and ammoTypesDone[a] ~= true then |
|
759 local count = 0 |
|
760 if a == amSkip then |
|
761 count = 9 |
|
762 end |
|
763 SetAmmo(a, count, 0, 0, 1) |
|
764 end |
|
765 end |
|
766 end |
|
767 |
|
768 _G.onGameStart = function() |
|
769 -- Mention mines timer |
|
770 if MinesTime ~= 3000 and MinesTime ~= nil then |
|
771 if MinesTime < 0 then |
|
772 params.goalText = params.goalText .. "|" .. loc("Mines time: 0s-5s") |
|
773 elseif (MinesTime % 1000) == 0 then |
|
774 params.goalText = params.goalText .. "|" .. string.format(loc("Mines time: %ds"), MinesTime/1000) |
|
775 elseif (MinesTime % 100) == 0 then |
|
776 params.goalText = params.goalText .. "|" .. string.format(loc("Mines time: %.1fs"), MinesTime/1000) |
|
777 else |
|
778 params.goalText = params.goalText .. "|" .. string.format(loc("Mines time: %.2fs"), MinesTime/1000) |
|
779 end |
|
780 end |
|
781 if params.wind then |
|
782 SetWind(params.wind) |
|
783 end |
|
784 ShowMission(params.missionTitle, loc("Scenario"), params.goalText, params.missionIcon, 5000) |
|
785 |
|
786 -- Spawn objects |
|
787 |
|
788 if params.gears ~= nil then |
|
789 for listGearID, gv in pairs(params.gears) do |
|
790 local timer, state, x, y, dx, dy |
|
791 local g |
|
792 state = 0 |
|
793 if gv.type == gtMine then |
|
794 if gv.isFrozen then |
|
795 state = gstFrozen |
|
796 end |
|
797 g = AddGear(def(gv.x,0), def(gv.y,0), gv.type, state, def(gv.dx, 0), def(gv.dy, 0), def(gv.timer, MinesTime)) |
|
798 if gv.isDud then |
|
799 SetHealth(g, 0) |
|
800 if gv.health ~= nil then |
|
801 SetGearValues(g, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 36 - gv.health) |
|
802 end |
|
803 end |
|
804 elseif gv.type == gtSMine then |
|
805 g = AddGear(def(gv.x,0), def(gv.y,0), gv.type, 0, def(gv.dx,0), def(gv.dy,0), def(gv.timer, 500)) |
|
806 elseif gv.type == gtAirMine then |
|
807 if gv.isFrozen then |
|
808 state = gstFrozen |
|
809 end |
|
810 local timer = def(gv.timer, div(MinesTime, 1000) * 250) |
|
811 g = AddGear(def(gv.x,0), def(gv.y,0), gv.type, state, def(gv.dx,0), def(gv.dy,0), timer) |
|
812 SetGearValues(g, nil, nil, timer) -- WDTimer |
|
813 elseif gv.type == gtExplosives then |
|
814 if gv.isRolling then |
|
815 state = gsttmpFlag |
|
816 end |
|
817 g = AddGear(def(gv.x,0), def(gv.y,0), gv.type, state, def(gv.dx,0), def(gv.dy,0), 0) |
|
818 if gv.health then |
|
819 SetHealth(g, gv.health) |
|
820 end |
|
821 if gv.isFrozen ~= nil then |
|
822 if gv.isFrozen == true then |
|
823 SetState(g, bor(GetState(g, gstFrozen))) |
|
824 end |
|
825 elseif GetHealth(g) > 60 then |
|
826 SetState(g, bor(GetState(g, gstFrozen))) |
|
827 end |
|
828 elseif gv.type == gtCase then |
|
829 local x, y, spawnTrick |
|
830 spawnTrick = false |
|
831 x = def(gv.x, 0) |
|
832 y = def(gv.y, 0) |
|
833 if x==0 and y==0 then |
|
834 x=1 |
|
835 y=1 |
|
836 spawnTrick = true |
|
837 end |
|
838 g = AddGear(x, y, gv.type, 0, def(gv.dx,0), def(gv.dy,0), 0) |
|
839 if spawnTrick then |
|
840 SetGearPosition(g, 0, 0) |
|
841 end |
|
842 if gv.crateType == "supply" then |
|
843 g = SpawnSupplyCrate(def(gv.x, 0), def(gv.y, 0), gv.ammoType) |
|
844 elseif gv.crateType == "supply_ammo_explicit" then |
|
845 g = SpawnAmmoCrate(def(gv.x, 0), def(gv.y, 0), gv.ammoType) |
|
846 elseif gv.crateType == "supply_utility_explicit" then |
|
847 g = SpawnUtilityCrate(def(gv.x, 0), def(gv.y, 0), gv.ammoType) |
|
848 elseif gv.crateType == "health" then |
|
849 g = SpawnHealthCrate(def(gv.x, 0), def(gv.y, 0)) |
|
850 if gv.health ~= nil then |
|
851 SetHealth(g, gv.health) |
|
852 end |
|
853 end |
|
854 if gv.isFrozen then |
|
855 SetState(g, bor(GetState(g, gstFrozen))) |
|
856 end |
|
857 elseif gv.type == gtKnife or gv.type == gtTarget then |
|
858 g = AddGear(def(gv.x,0), def(gv.y,0), gv.type, 0, def(gv.dx,0), def(gv.dy,0), 0) |
|
859 end |
|
860 if g ~= nil and gv.id ~= nil then |
|
861 _G.sm.goalGears[gv.id] = g |
|
862 setGearValue(g, "sm_id", gv.id) |
|
863 end |
|
864 end |
|
865 end |
|
866 |
|
867 -- Spawn girders and rubbers |
|
868 if params.girders ~= nil then |
|
869 for i, girderData in pairs(params.girders) do |
|
870 PlaceGirder(girderData.x, girderData.y, girderData.frameIdx) |
|
871 end |
|
872 end |
|
873 if params.rubbers ~= nil then |
|
874 for i, rubberData in pairs(params.rubbers) do |
|
875 PlaceSprite(rubberData.x, rubberData.y, sprAmRubber, capcolDefault, rubberData.frameIdx, false, false, false, lfBouncy) |
|
876 end |
|
877 end |
|
878 |
|
879 -- Per-hedgehog ammo loadouts |
|
880 for teamID, teamData in pairs(params.teams) do |
|
881 for hogID, hogData in pairs(teamData.hogs) do |
|
882 if hogData.ammo ~= nil then |
|
883 for ammoType, count in pairs(hogData.ammo) do |
|
884 AddAmmo(hogData.gearID, ammoType, count) |
|
885 end |
|
886 end |
|
887 end |
|
888 end |
|
889 end |
|
890 |
|
891 _G.onGameTick20 = function() |
|
892 if params.customGoalCheck == "instant" then |
|
893 _G.sm.checkWinOrFail() |
|
894 end |
|
895 end |
|
896 |
|
897 end |
|
898 |