# HG changeset patch # User Wuzzy # Date 1519229686 -3600 # Node ID f18cefc4309d73ae808441968d748cefebf707d2 # Parent 9dd724e8d620f1f7626cef27324cabd6fc919d0d Add SimpleMission lua library for easier mission creation diff -r 9dd724e8d620 -r f18cefc4309d ChangeLog.txt --- a/ChangeLog.txt Wed Feb 21 15:53:30 2018 +0100 +++ b/ChangeLog.txt Wed Feb 21 17:14:46 2018 +0100 @@ -64,6 +64,7 @@ * Fix green color channel on themes with sd-tint Lua API: + + New library: SimpleMission: Allows to create missions more easily + New call: WriteLnToChat(string): Add a line in the chat + New call: SetVampiric(bool): Toggle vampirism + New call: SetLaserSight(bool): Toggle laser sight diff -r 9dd724e8d620 -r f18cefc4309d share/hedgewars/Data/Scripts/SimpleMission.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/hedgewars/Data/Scripts/SimpleMission.lua Wed Feb 21 17:14:46 2018 +0100 @@ -0,0 +1,795 @@ +--[=[ += Simple Mission Framework for Hedgewars = + +This is a simple library intended to make setting up simple missions an +easy task for Lua scripters. The entire game logic and coding is +abtracted away in a single function which you just need to feed +a large definition table in which you define gears, goals, etc. + +This is ideal for missions in which you set up the entire scenario +from the start and don't need any complex in-mission events. +BUT! This is NOT suited for missions with scripted events, cut-scenes, +branching story, etc. + +This library has the following features: +* Add teams, clans, hogs +* Spawn gears +* Sensible defaults for almost everything +* Set custom goals or use the default one (kill all enemies) +* Add non-goals to fail the mission +* Checks victory and failure automatically + +To use this library, you first have to load it and to call SimpleMission once with +the appropriate parameters. +See the comment of SimpleMission for a specification of all parameters. + +]=] + +HedgewarsScriptLoad("/Scripts/Locale.lua") +HedgewarsScriptLoad("/Scripts/Tracker.lua") + +--[[ +SimpleMission(params) + +This function sets up the *entire* mission and needs one argument: params. +The argument “params” is a table containing fields which describe the mission. + + Mandatory fields: + - teams: Table of teams. There must be 1-8 teams. + + Optional fields + - ammoConfig Table containing basic ammo values (default: infinite skip only) + - initVars Table where you set up environment parameters such as MinesNum. + - wind If set, the wind will permanently set to this value (-100..100) + - gears: Table of objects. + - girders Table of girders + - rubbers Table of rubbers + + AMMO + - ammoType ammo type + - delay delay (default: 0) + - numberInCrate ammo per crate (default: 1) + - count default starter ammo for everyone, 9 for infinite (default: 0) + - probability probability in crates (default: 0) + + TEAM DATA + - hogs table of hedgehogs in this team (must contain at least 1 hog) + - name team name + - clanID ID of the clan to which this team belongs to. Counting starts at 0. + By default, each team goes into its own clan. + Important: The clan of the player and allies MUST be 0. + Important: You MUST either set the clan ID explicitly for all teams or none of them. + - flag flag name (default: hedgewars) + - grave grave name (has default grave for each team) + - fort fort name (default: Castle) + + HEDGEHOG DATA: + - id optional identifier for goals + - name hog name + - x, y hog position (default: spawns randomly on land) + - botLevel 1-5: Bot level (lower=stronger). 0=human player (default: 0) + - hat hat name (default: NoHat) + - health hog health (default: 100) + - poisoned if true, hedgehog starts poisoned with 5 poison damage. Set to a number for other poison damage (default: false) + - frozen if true, hedgehogs starts frozen (default: false) + - faceLeft initial facing direction. true=left, false=false (default: false) + - ammo table of ammo types + + GEAR TYPES: + - type gear type + ALL types: + id optional identifier for goals + x x coordinate of starting position (default: 0) + y y coordinate of starting position (default: 0) + dx initial x speed (default: 0) + dy initial y speed (default: 0) + - type=gtMine Mine + timer Mine timer (only for non-duds). Default: MinesTime + isDud Whether the mine is a dud. default: false + isFrozen Whether the mine is frozen. If true, it implies being a dud as well. Default: false + health Initial health of dud mines. Has no effect if isDud=false. Default: 36 + - type=gtSMine Sticky mine + timer Timer. Default: 500 + - type=gtAirMine Air mine + timer Timer. Default: (MinesTime/1000 * 250) + - type=gtExplosives Barrel + health Initial health. Default: 60 + isFrozen Whether the barrel is frozen. Default: true with health > 60, false otherwise + isRolling Whether the barrel starts in “rolling” state. Default: false + - type=gtCase Crate + crateType "health": Health crate + "supply": Ammo or utility crate (select crate type automatically) + "supply_ammo_explicit": Ammo crate (not recommened) + "supply_utility_explicit": Utility crate (not recommededn) + ammoType Contained ammo (only for ammo and utility crates). + health Contained health (only for health crates). Default: HealthCaseAmount + isFrozen Whether the crate is frozen. Default: false + - type=gtKnife Cleaver + - type=gtTarget Target + + GOALS: + Note: If there are at least two opposing teams, a default goal is used, which is to defeat all the enemies of the + player's team. If this is what you want, you can skip this section. + + The default goal is overwritten as if customGoals has been set. Set customGoals and other related parameters for + defining your own special goals. In this case, the mission is won if all customGoals are completed. + Note the mission will always fail if the player's hedgehogs and all their allies have been defeated. + If there is only one team (for the player), there is no default goal and one must be set explicitly. + - customGoals Table of custom goals (see below). All of them must be met to win. Some goal types might fail, + rendering the mission unwinnable and leading to the loss of the mission. An example is + blowing up a crate which you should have collected.ed. + - customNonGoals Table of non-goals, the player loses if one of them is achieved + - customGoalCheck When to check goals and non-goals. Values: "instant" (default), "turnStart", "turnEnd" + + - missionTitle: The name of the mission (highly recommended) + - customGoalText: A short string explaining the goal of the mission (use this if you set custom goals). + + GOAL TYPES: + - type name of goal type + - type="destroy" Gear must be destroyed + - id Gear to destroy + - type="teamDefeat" Team must be defeated + - teamName Name of team to defeat + - type="collect" Crate must be collected + FAIL CONDITION: Crate taken by enemy, or destroyed + - id ID of crate gear to collect + - collectors Optional table of gear IDs, any one of which must collect the gear (but nobody else!). + By default, this is for the player's teams and allies. + - type="turns" Achieved when a number of turns has been played + - turns Number of played turns + - type="rounds" Achieved when a number of rounds has been played + - rounds Number of played rounds + - type="suddenDeath" Sudden Death has started + - type="inZone" A gear is within given coordinate bounds. Each of xMin, xMax, yMin and yMax is a sub-goal. + Each sub-goal is only checked if not nil. + You can use this to check if a gear left, right, above or below a given coordinate. + To check if the gear is within a rectangle, just set all 4 sub-goals. + FAIL CONDITION: Gear destroyed + - id Gear to watch + - xMin gear's X coordinate must be lower than this + - xMax gear's X coordinate must be higher than this + - yMin gear's Y coordinate must be lower than this + - yMax gear's Y coordinate must be higher than this + - type="distGearPos" Distance between a gear and a fixed position + FAIL CONDITION: Gear destroyed + - distance goal distance to compare to + - relationship "greaterThan" or "lowerThan" + - id gear to watch + - x x coordinate to reach + - y y coordinate to reach + - type="distGearGear" Distance between two gears + FAIL CONDITION: Any of both gears destroyed + - distance goal distance to compare to + - relationship "greaterThan" or "lowerThan" + - id1 first gear to compare + - id2 second gear to compare + - type="damage" Gear took damage or was destroyed + - id Gear to watch + - damage Minimum amount of damage to take at a single blow. Default: 1 + - canDestroy If false, this goal will fail if the gear was destroyed without taking the required damage + - type="drown" Gear has drowned + FAIL CONDITION: Gear destroyed by other means + - id Gear to watch + - type="poison" Gear must be poisoned + FAIL CONDITION: Gear destroyed + - id Gear to be poisoned + - type="cure" Gear must exist and be free from poisoning + FAIL CONDITION: Gear destroyed + - id Gear to check + - type="freeze" Gear must exist and be frozen + FAIL CONDITION: Gear destroyed + - id Gear to be frozen + - type="melt" Gear must exist and be unfrozen + FAIL CONDITION: Gear destroyed + - id Gear to check + - type="waterSkip" Gear must have skipped over water + FAIL CONDITION: Gear destroyed before it reached the required number of skips + - id + - skips Total number of water skips required at least (default: 1) + +]] + +local goals +local teamHogs = {} + +--[[ + HELPER VARIABLES +]] + +local defaultClanColors = { + [0] = 0xff0204, -- red + [1] = 0x4980c1, -- blue + [2] = 0x1de6ba, -- cyan + [3] = 0xb541ef, -- purple + [4] = 0xe55bb0, -- magenta + [5] = 0x20bf00, -- green + [6] = 0xfe8b0e, -- orange + [7] = 0x5f3605, -- brown + [8] = 0xffff01, -- yellow +} +local defaultGraves = { + "Grave", "Statue", "pyramid", "Simple", "skull", "Badger", "Duck2", "Flower" +} +local defaultFlags = { + "hedgewars", "cm_birdy", "cm_eyes", "cm_spider", "cm_kiwi", "cm_scout", "cm_skull", "cm_bars" +} + +-- Utility functions + +-- Returns value if it is non-nil, otherwise returns default +local function def(value, default) + if value == nil then + return default + else + return value + end +end + +-- Get hypotenuse of a triangle with legs x and y +local function hypot(x, y) + local t + x = math.abs(x) + y = math.abs(y) + t = math.min(x, y) + x = math.max(x, y) + if x == 0 then + return 0 + end + t = t / x + return x * math.sqrt(1 + t * t) +end + +local errord = false + +-- This function generates the mission. See above for the meaning of params. +function SimpleMission(params) + if params.missionTitle == nil then + params.missionTitle = loc("Scenario") + end + if params.goalText == nil then + params.goalText = loc("Defeat the enemy!") + end + if params.customGoalCheck == nil and (params.customGoals ~= nil or params.customNonGoals ~= nil) then + params.customGoalCheck = "instant" + end + + _G.sm = {} + + _G.sm.isInSuddenDeath = false + + -- Number of completed turns + _G.sm.gameTurns = 0 + + _G.sm.goalGears = {} + + _G.sm.params = params + + _G.sm.gameEnded = false + + _G.sm.playerClan = 0 + + _G.sm.makeStats = function(winningClan, customAchievements) + for t=0, TeamsCount-1 do + local team = GetTeamName(t) + local stats = GetTeamStats(team) + local clan = GetTeamClan(team) + if clan == winningClan then + SendStat(siPlayerKills, stats.Kills, team) + end + end + for t=0, TeamsCount-1 do + local team = GetTeamName(t) + local stats = GetTeamStats(team) + local clan = GetTeamClan(team) + if clan ~= winningClan then + SendStat(siPlayerKills, stats.Kills, team) + end + end + if customAchievements ~= nil then + for a=1, #customAchievements do + SendStat(siCustomAchievement, customAchievements[a]) + end + end + end + + _G.sm.checkGoal = function(goal) + if goal.type == "destroy" then + return getGearValue(_G.sm.goalGears[goal.id], "sm_destroyed") + elseif goal.type == "collect" then + local collector = getGearValue(_G.sm.goalGears[goal.id], "sm_collected") + if collector then + if not goal.collectors then + if GetHogClan(collector) == _G.sm.playerClan then + return true + else + -- Fail if the crate was collected by enemy + return "fail" + end + else + for c=1, #goal.collectors do + if _G.sm.goalGears[goal.collectors[c]] == collector then + return true + end + end + -- Fail if the crate was collected by someone who was not supposed to get it + return "fail" + end + else + -- Fail goal if crate was destroyed + if getGearValue(_G.sm.goalGears[goal.id], "sm_destroyed") then + return "fail" + end + return false + end + elseif goal.type == "turns" then + return sm.gameTurns >= goal.turns + elseif goal.type == "rounds" then + return (TotalRounds) >= goal.rounds + elseif goal.type == "inZone" then + if getGearValue(_G.sm.goalGears[goal.id], "sm_destroyed") then + return "fail" + end + local gX, gY = GetGearPosition(_G.sm.goalGears[goal.id]) + -- 4 sub-goals, each optional + local g1 = (not goal.xMin) or gX >= goal.xMin + local g2 = (not goal.xMax) or gX <= goal.xMax + local g3 = (not goal.yMin) or gY >= goal.yMin + local g4 = (not goal.yMax) or gY <= goal.yMax + return g1 and g2 and g3 and g4 + elseif goal.type == "distGearPos" or goal.type == "distGearGear" then + local gX, tY, tX, tY + if goal.type == "distGearPos" then + if getGearValue(_G.sm.goalGears[goal.id], "sm_destroyed") then + -- Fail if gear was destroyed + return "fail" + end + gX, gY = GetGearPosition(_G.sm.goalGears[goal.id]) + tX, tY = goal.x, goal.y + elseif goal.type == "distGearGear" then + if getGearValue(_G.sm.goalGears[goal.id1], "sm_destroyed") or getGearValue(_G.sm.goalGears[goal.id2], "sm_destroyed") then + -- Fail if one of the gears was destroyed + return "fail" + end + gX, gY = GetGearPosition(_G.sm.goalGears[goal.id1]) + tX, tY = GetGearPosition(_G.sm.goalGears[goal.id2]) + end + + local h = hypot(gX - tX, gY - tY) + if goal.relationship == "smallerThan" then + return h < goal.distance + elseif goal.relationship == "greaterThan" then + return h > goal.distance + end + -- Invalid parameters! + error("SimpleMission: Invalid parameters for distGearPos/distGearGear!") + errord = true + return false + elseif goal.type == "suddenDeath" then + return sm.isInSuddenDeath + elseif goal.type == "damage" then + local damage = goal.damage or 1 + local tookEnoughDamage = getGearValue(_G.sm.goalGears[goal.id], "sm_maxDamage") >= damage + if getGearValue(_G.sm.goalGears[goal.id], "sm_destroyed") then + -- Fail if gear was destroyed without taking enough damage first + if not tookEnoughDamage and goal.canDestroy == false then + return "fail" + else + -- By default, succeed if gear was destroyed + return true + end + end + return tookEnoughDamage + elseif goal.type == "drown" then + local drowned = getGearValue(_G.sm.goalGears[goal.id], "sm_drowned") + -- Fail if gear was destroyed by something other than drowning + if not drowned and getGearValue(_G.sm.goalGears[goal.id], "sm_destroyed") then + return "fail" + end + return drowned + elseif goal.type == "poison" then + if getGearValue(_G.sm.goalGears[goal.id], "sm_destroyed") then + return "fail" + end + return GetEffect(_G.sm.goalGears[goal.id], hePoisoned) >= 1 + elseif goal.type == "freeze" then + if getGearValue(_G.sm.goalGears[goal.id], "sm_destroyed") then + return "fail" + end + return GetEffect(_G.sm.goalGears[goal.id], heFrozen) >= 256 + elseif goal.type == "cure" then + if getGearValue(_G.sm.goalGears[goal.id], "sm_destroyed") then + return "fail" + end + return GetEffect(_G.sm.goalGears[goal.id], hePoisoned) == 0 + elseif goal.type == "melt" then + if getGearValue(_G.sm.goalGears[goal.id], "sm_destroyed") then + return "fail" + end + return GetEffect(_G.sm.goalGears[goal.id], heFrozen) == 0 + elseif goal.type == "waterSkip" then + local skips = goal.skips or 1 + local hasEnoughSkips = getGearValue(_G.sm.goalGears[goal.id], "sm_waterSkips") >= skips + -- Fail if gear was destroyed before it got the required number of skips + if not hasEnoughSkips and getGearValue(_G.sm.goalGears[goal.id], "sm_destroyed") then + return "fail" + end + return hasEnoughSkips + elseif goal.type == "teamDefeat" then + return #teamHogs[goal.teamName] == 0 + else + return false + end + end + + --[[ Checks the custom goals. + Returns true when all custom goals are met. + Returns false when not all custom goals are met. + Returns "fail" if any of the goals has failed (i.e. is impossible to complete). + Returns nil when there are no custom goals ]] + _G.sm.checkGoals = function() + if params.customGoals ~= nil and #params.customGoals > 0 then + for key, goal in pairs(params.customGoals) do + local done = _G.sm.checkGoal(goal) + if done == false or done == "fail" then + return done + end + end + return true + else + return nil + end + end + + --[[ Checks the custom non-goals. + Returns true when any non-goal is met. + Returns false otherwise. ]] + _G.sm.checkNonGoals = function() + if params.customNonGoals ~= nil and #params.customNonGoals > 0 then + for key, nonGoal in pairs(params.customNonGoals) do + local done = _G.sm.checkGoal(nonGoal) + if done == true then + return true + end + end + end + return false + end + + -- Checks goals and non goals and wins or loses mission + _G.sm.checkWinOrFail = function() + if errord then + return + end + if _G.sm.checkNonGoals() == true or _G.sm.checkGoals() == "fail" then + _G.sm.lose() + elseif _G.sm.checkGoals() == true then + _G.sm.win() + end + end + + _G.sm.win = function() + if not _G.sm.gameEnded then + _G.sm.gameEnded = true + AddCaption(loc("Victory!"), 0xFFFFFFFF, capgrpGameState) + SendStat(siGameResult, loc("You win!")) + if GetHogLevel(CurrentHedgehog) == 0 then + SetState(CurrentHedgehog, bor(GetState(CurrentHedgehog), gstWinner)) + SetState(CurrentHedgehog, band(GetState(CurrentHedgehog), bnot(gstHHDriven))) + PlaySound(sndVictory, CurrentHedgehog) + end + _G.sm.makeStats(_G.sm.playerClan) + EndGame() + end + end + + _G.sm.lose = function() + if not _G.sm.gameEnded then + _G.sm.gameEnded = true + AddCaption(loc("Scenario failed!"), 0xFFFFFFFF, capgrpGameState) + SendStat(siGameResult, loc("You lose!")) + if GetHogLevel(CurrentHedgehog) == 0 then + SetState(CurrentHedgehog, bor(GetState(CurrentHedgehog), gstLoser)) + SetState(CurrentHedgehog, band(GetState(CurrentHedgehog), bnot(gstHHDriven))) + end + local clan = ClansCount-1 + for t=0, TeamsCount-1 do + local team = GetTeamName(t) + -- Just declare any living team other than the player team the winner + if (_G.sm.checkGoal({type="teamDefeat", teamName=team}) == false) and (GetTeamClan(team) ~= _G.sm.playerClan) then + clan = GetTeamClan(team) + break + end + end + _G.sm.makeStats(clan) + EndGame() + end + end + + _G.onSuddenDeath = function() + sm.isInSuddenDeath = true + end + + _G.onGearWaterSkip = function(gear) + increaseGearValue(gear, "sm_waterSkips") + end + + _G.onGearAdd = function(gear) + if GetGearType(gear) == gtHedgehog then + local team = GetHogTeamName(gear) + if teamHogs[team] == nil then + teamHogs[team] = {} + end + table.insert(teamHogs[GetHogTeamName(gear)], gear) + end + setGearValue(gear, "sm_waterSkips", 0) + setGearValue(gear, "sm_maxDamage", 0) + setGearValue(gear, "sm_drowned", false) + setGearValue(gear, "sm_destroyed", false) + end + + _G.onGearResurrect = function(gear) + if GetGearType(gear) == gtHedgehog then + table.insert(teamHogs[GetHogTeamName(gear)], gear) + end + setGearValue(gear, "sm_destroyed", false) + end + + _G.onGearDelete = function(gear) + if GetGearType(gear) == gtCase and band(GetGearMessage(gear), gmDestroy) ~= 0 then + -- Set ID of collector + setGearValue(gear, "sm_collected", CurrentHedgehog) + end + if GetGearType(gear) == gtHedgehog then + local team = GetHogTeamName(gear) + local hogList = teamHogs[team] + for h=1, #hogList do + if hogList[h] == gear then + table.remove(hogList, h) + break + end + end + end + if band(GetState(gear), gstDrowning) ~= 0 then + setGearValue(gear, "sm_drowned", true) + end + setGearValue(gear, "sm_destroyed", true) + end + + _G.onGearDamage = function(gear, damage) + local currentDamage = getGearValue(gear, "sm_maxDamage") + if damage > currentDamage then + setGearValue(gear, "sm_maxDamage", damage) + end + end + + _G.onGameInit = function() + CaseFreq = 0 + WaterRise = 0 + HealthDecrease = 0 + MinesNum = 0 + Explosives = 0 + + for initVarName, initVarValue in pairs(params.initVars) do + _G[initVarName] = initVarValue + end + if #params.teams == 1 then + EnableGameFlags(gfOneClanMode) + end + + local clanCounter = 0 + for teamID, teamData in pairs(params.teams) do + local name, clanID, grave, fort, voice, flag + name = def(teamData.name, string.format(loc("Team %d"), teamID)) + if teamData.clanID == nil then + clanID = clanCounter + clanCounter = clanCounter + 1 + else + clanID = teamData.clanID + end + grave = def(teamData.grave, defaultGraves[math.min(teamID, 8)]) + fort = def(teamData.fort, "Castle") + voice = def(teamData.voice, "Default") + flag = def(teamData.flag, defaultFlags[math.min(teamID, 8)]) + + AddTeam(name, defaultClanColors[clanID], grave, fort, voice, flag) + + for hogID, hogData in pairs(teamData.hogs) do + local name, botLevel, health, hat + name = def(hogData.name, string.format(loc("Hog %d"), hogID)) + botLevel = def(hogData.botLevel, 0) + health = def(hogData.health, 100) + hat = def(hogData.hat, "NoHat") + local hog = AddHog(name, botLevel, health, hat) + if hogData.x ~= nil and hogData.y ~= nil then + SetGearPosition(hog, hogData.x, hogData.y) + end + if hogData.faceLeft then + HogTurnLeft(hog, true) + end + if hogData.poisoned == true then + SetEffect(hog, hePoisoned, 5) + elseif type(hogData.poisoned) == "number" then + SetEffect(hog, hePoisoned, hogData.poisoned) + end + if hogData.frozen then + SetEffect(hog, heFrozen, 199999) + end + + if hog ~= nil and hogData.id ~= nil then + _G.sm.goalGears[hogData.id] = hog + setGearValue(hog, "sm_id", hogData.id) + end + + -- Remember this hedgehog's gear ID for later use + hogData.gearID = hog + end + end + end + + _G.onNewTurn = function() + if params.wind ~= nil then + SetWind(params.wind) + end + _G.sm.gameStarted = true + + if params.customGoalCheck == "turnStart" then + _G.sm.checkWinOrFail() + end + end + + _G.onEndTurn = function() + _G.sm.gameTurns = _G.sm.gameTurns + 1 + + if params.customGoalCheck == "turnEnd" then + _G.sm.checkWinOrFail() + end + end + + _G.onAmmoStoreInit = function() + local ammoTypesDone = {} + -- Read script's stated ammo wishes + if params.ammoConfig ~= nil then + for ammoType, v in pairs(params.ammoConfig) do + SetAmmo(ammoType, def(v.count, 0), def(v.probability, 0), def(v.delay, 0), def(v.numberInCrate, 1)) + ammoTypesDone[ammoType] = true + end + end + -- Apply default values for all ammo types which have not been set + for a=0, AmmoTypeMax do + if a ~= amNothing and ammoTypesDone[a] ~= true then + local count = 0 + if a == amSkip then + count = 9 + end + SetAmmo(a, count, 0, 0, 1) + end + end + end + + _G.onGameStart = function() + -- Mention mines timer + if MinesTime ~= 3000 and MinesTime ~= nil then + if MinesTime < 0 then + params.goalText = params.goalText .. "|" .. loc("Mines time: 0s-5s") + elseif (MinesTime % 1000) == 0 then + params.goalText = params.goalText .. "|" .. string.format(loc("Mines time: %ds"), MinesTime/1000) + elseif (MinesTime % 100) == 0 then + params.goalText = params.goalText .. "|" .. string.format(loc("Mines time: %.1fs"), MinesTime/1000) + else + params.goalText = params.goalText .. "|" .. string.format(loc("Mines time: %.2fs"), MinesTime/1000) + end + end + ShowMission(params.missionTitle, loc("Scenario"), params.goalText, 1, 5000) + + -- Spawn objects + + if params.gears ~= nil then + for listGearID, gv in pairs(params.gears) do + local timer, state, x, y, dx, dy + local g + state = 0 + if gv.type == gtMine then + if gv.isFrozen then + state = gstFrozen + end + 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)) + if gv.isDud then + SetHealth(g, 0) + if gv.health ~= nil then + SetGearValues(g, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 36 - gv.health) + end + end + elseif gv.type == gtSMine then + 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)) + elseif gv.type == gtAirMine then + if gv.isFrozen then + state = gstFrozen + end + local timer = def(gv.timer, div(MinesTime, 1000) * 250) + g = AddGear(def(gv.x,0), def(gv.y,0), gv.type, state, def(gv.dx,0), def(gv.dy,0), timer) + SetGearValues(g, nil, nil, timer) -- WDTimer + elseif gv.type == gtExplosives then + if gv.isRolling then + state = gsttmpFlag + end + g = AddGear(def(gv.x,0), def(gv.y,0), gv.type, state, def(gv.dx,0), def(gv.dy,0), 0) + if gv.health then + SetHealth(g, gv.health) + end + if gv.isFrozen ~= nil then + if gv.isFrozen == true then + SetState(g, bor(GetState(g, gstFrozen))) + end + elseif GetHealth(g) > 60 then + SetState(g, bor(GetState(g, gstFrozen))) + end + elseif gv.type == gtCase then + local x, y, spawnTrick + spawnTrick = false + x = def(gv.x, 0) + y = def(gv.y, 0) + if x==0 and y==0 then + x=1 + y=1 + spawnTrick = true + end + g = AddGear(x, y, gv.type, 0, def(gv.dx,0), def(gv.dy,0), 0) + if spawnTrick then + SetGearPosition(g, 0, 0) + end + if gv.crateType == "supply" then + g = SpawnSupplyCrate(def(gv.x, 0), def(gv.y, 0), gv.ammoType) + elseif gv.crateType == "supply_ammo_explicit" then + g = SpawnAmmoCrate(def(gv.x, 0), def(gv.y, 0), gv.ammoType) + elseif gv.crateType == "supply_utility_explicit" then + g = SpawnUtilityCrate(def(gv.x, 0), def(gv.y, 0), gv.ammoType) + elseif gv.crateType == "health" then + g = SpawnHealthCrate(def(gv.x, 0), def(gv.y, 0)) + if gv.health ~= nil then + SetHealth(g, gv.health) + end + end + if gv.isFrozen then + SetState(g, bor(GetState(g, gstFrozen))) + end + elseif gv.type == gtKnife or gv.type == gtTarget then + g = AddGear(def(gv.x,0), def(gv.y,0), gv.type, 0, def(gv.dx,0), def(gv.dy,0), 0) + end + if g ~= nil and gv.id ~= nil then + _G.sm.goalGears[gv.id] = g + setGearValue(g, "sm_id", gv.id) + end + end + end + + -- Spawn girders and rubbers + if params.girders ~= nil then + for i, girderData in pairs(params.girders) do + PlaceGirder(girderData.x, girderData.y, girderData.frameIdx) + end + end + if params.rubbers ~= nil then + for i, rubberData in pairs(params.rubbers) do + PlaceSprite(rubberData.x, rubberData.y, sprAmRubber, 0xFFFFFFFF, rubberData.frameIdx, false, false, false, lfBouncy) + end + end + + -- Per-hedgehog ammo loadouts + for teamID, teamData in pairs(params.teams) do + for hogID, hogData in pairs(teamData.hogs) do + if hogData.ammo ~= nil then + for ammoType, count in pairs(hogData.ammo) do + AddAmmo(hogData.gearID, ammoType, count) + end + end + end + end + end + + _G.onGameTick20 = function() + if params.customGoalCheck == "instant" then + _G.sm.checkWinOrFail() + end + end +end +