diff --git a/LuaRules/Gadgets/api_zombie_helper.lua b/LuaRules/Gadgets/api_zombie_helper.lua index fc81f73837..2cf7ad2146 100644 --- a/LuaRules/Gadgets/api_zombie_helper.lua +++ b/LuaRules/Gadgets/api_zombie_helper.lua @@ -27,62 +27,84 @@ VFS.Include("LuaRules/Configs/CAI/accessory/targetReachableTester.lua") -- unsure if these are different, differentiating between gaia and "zombie" team somehow may be good? local GaiaTeamID = Spring.GetGaiaTeamID() local GaiaAllyTeamID = select(6, Spring.GetTeamInfo(GaiaTeamID, false)) +local WARNING_TIME = 5 -- set to 5 for sounds and such to sync well. +local random = math.random +local floor = math.floor local mapWidth local mapHeight +local gameframe = 0 + +local NonZombies = { + ["asteroid"] = true, +} + +local CEG_SPAWN = [[zombie]] local REZ_SOUND = "sounds/misc/resurrect.wav" +local ZOMBIE_SOUNDS = { + "sounds/misc/zombie_1.wav", + "sounds/misc/zombie_2.wav", + "sounds/misc/zombie_3.wav", +} + +local resurrectingFeatures = {} -- contains inital gameframe, base rez time, frame rez should complete and a callback function if someone wants to reuse the functionality --- Zombie resurrect -- unsure if necessary, seems like just an extra failsafe + +-- Get Feature Defname and facing local function GetFeatureResurrectData(featureID) - local featureDefName, facing = Spring.GetFeatureResurrect(featureID) - if featureDefName == "" then + local resDefName, facing = Spring.GetFeatureResurrect(featureID) + if resDefName == "" then local featureDef = FeatureDefs[Spring.GetFeatureDefID(featureID)] local featureName = featureDef.name or "" if featureDef.resurrectable == 1 then - featureDefName = featureName:gsub('(.*)_.*', '%1') --filter out _dead + resDefName = featureName:gsub('(.*)_.*', '%1') --filter out _dead facing = facing or 0 end end - return featureDefName, facing + return resDefName, facing end -local function TurnFeatureIntoUnit(featureID,teamID,reclaimPercentHealth) - - local featureDefName,facing = GetFeatureResurrectData(featureID) - local x, y, z = Spring.GetFeaturePosition(featureID) - - local unitID = Spring.CreateUnit(featureDefName, x, y, z, facing, teamID) - - -- Unit and Wreck exist both - -- could let there be a function passed to this function, that returns the Unit ID for transfer of values - - if (unitID) then - gadgetHandler:NotifyUnitCreatedByMechanic(unitID, false, "zombies") - local size = UnitDefNames[featureDefName].xsize - Spring.SpawnCEG("resurrect", x, y, z, 0, 0, 0, size) - Spring.GiveOrderToUnit(unitID, CMD.FIRE_STATE, 2, 0) - GG.PlayFogHiddenSound(REZ_SOUND, 12, x, y, z) - - if reclaimPercentHealth then - local currentMetal, maxMetal = Spring.GetFeatureResources(featureID) - if currentMetal and maxMetal and (maxMetal > 0) then - local health = Spring.GetUnitHealth(unitID) - if health then - Spring.SetUnitHealth(unitID, health*(currentMetal/maxMetal)) - end - end - end - end +-- Zombie resurrect +-- Turns a feature into a unit if applicable and returns the unitID +-- Instead of using the wreck, can receive a UnitDef Or UnitDefID to spawn a different unit +local function TurnFeatureIntoUnit(featureID,teamID,unitDefID) + local resDefName,facing = GetFeatureResurrectData(featureID) + local x, y, z = Spring.GetFeaturePosition(featureID) + local size = 3 + if unitDefID then + resDefName = unitDefID + size = UnitDefs[resDefName].xsize + else + size = UnitDefNames[resDefName].xsize + end - Spring.DestroyFeature(featureID) - return unitID + local unitID = Spring.CreateUnit(resDefName, x, y, z, facing, teamID) + + if not (unitID) then + return nil + end + gadgetHandler:NotifyUnitCreatedByMechanic(unitID, false, "zombies") --TODO should this be here? is this used anywhere? + Spring.SpawnCEG("resurrect", x, y, z, 0, 0, 0, size) + Spring.GiveOrderToUnit(unitID, CMD.FIRE_STATE, 2, 0) + GG.PlayFogHiddenSound(REZ_SOUND, 12, x, y, z) + return unitID end --- Works on non zombie units too. +local function SetHealthToReclaimPercent(featureID, unitID) + local currentMetal, maxMetal = Spring.GetFeatureResources(featureID) + if currentMetal and maxMetal and (maxMetal > 0) then + local health = Spring.GetUnitHealth(unitID) + if health then + Spring.SetUnitHealth(unitID, math.ceil(health*(currentMetal/maxMetal))) + end + end +end + +-- Sets the zombie specific speed multiplier. Works on non zombie units too. local function SetZombieSpeedMult(unitID,speedMult) if type(speedMult) ~= 'number' or speedMult < 0 then error("SetZombieSpeedMult: mult must be number >= 0") @@ -91,9 +113,7 @@ local function SetZombieSpeedMult(unitID,speedMult) GG.UpdateUnitAttributes(unitID) end - -- Zombie commands - local function RandomFactoryOrders(unitID, unitDefID) -- give factory something to do if Spring.GetUnitIsDead(unitID) then return @@ -103,14 +123,12 @@ local function RandomFactoryOrders(unitID, unitDefID) -- give factory something return end local orders = {} - for i = 1, math.random(10, 30) do - orders[#orders + 1] = {-buildopts[math.random(1, #buildopts)], 0, 0 } + for i = 1, random(10, 30) do + orders[#orders + 1] = {-buildopts[random(1, #buildopts)], 0, 0 } end Spring.GiveOrderArrayToUnit(unitID, orders) end - - local function GetUnitNearestAlly(unitID, range) local best_ally local best_dist @@ -123,6 +141,7 @@ local function GetUnitNearestAlly(unitID, range) if (allyID ~= unitID) and (allyTeam == GaiaTeamID) and (Spring.Utilities.getMovetype(UnitDefs[allyDefID]) ~= false) then local ox, oy, oz = Spring.GetUnitPosition(allyID) local dist = math.diag(x - ox, z - oz) + --TODO reinstate once engine thing is fixed, IsTargetReallyReachable enabled for testing if IsTargetReallyReachable(unitID, ox, oy, oz, x, y, z) and ((best_dist == nil) or (dist < best_dist)) then best_ally = allyID best_dist = dist @@ -132,13 +151,14 @@ local function GetUnitNearestAlly(unitID, range) return best_ally end -local function GiveZombiesRandomOrders(unitID) +-- Applies Random Attackmove orders and Factory commands to units. +local function SetZombieBehavior(unitID) local unitDefID = (not Spring.GetUnitIsDead(unitID)) and Spring.GetUnitDefID(unitID) if not unitDefID then return end - local rx,rz,ry + Spring.GiveOrderToUnit(unitID, CMD.MOVE_STATE, 2, 0) local orders = {} local near_ally if (UnitDefs[unitDefID].canAttack) then @@ -151,13 +171,14 @@ local function GiveZombiesRandomOrders(unitID) end end local x,y,z = Spring.GetUnitPosition(unitID) - if (near_ally) and math.random(0, 5) < 4 then -- 60% chance to guard nearest ally + if (near_ally) and random(0, 5) < 4 then -- 60% chance to guard nearest ally orders[#orders + 1] = {CMD.GUARD, {near_ally}, 0} end - for i = 1, math.random(10, 30) do - local rx = math.random(0, mapWidth) - local rz = math.random(0, mapHeight) + for i = 1, random(10, 30) do + local rx = random(0, mapWidth) + local rz = random(0, mapHeight) local ry = Spring.GetGroundHeight(rx,rz) + --TODO once engine is fixed and this doesnt crash readd, IsTargetReallyReachable enabled for testing if IsTargetReallyReachable(unitID, rx, ry, rz, x, y, z) then orders[#orders+1] = {CMD.FIGHT, {rx, ry, rz}, CMD.OPT_SHIFT} end @@ -169,15 +190,123 @@ local function GiveZombiesRandomOrders(unitID) end end +-- Adds a wreck into the zombie countdown table and returns it for further modification. +-- Use the rezFrameCallback to repurpose the system for other effects or chain into TurnFeatureIntoUnit for a revived unit with ID Callback. +-- If no callback is provided, revives the wreck as a unslowed zombie unit. +local function AddFeatureToZombieCountdown(featureID, buildpower, minRezTime, maxRezTime, rezFrameCallback) + if resurrectingFeatures[featureID] then + return resurrectingFeatures[featureID] + end + local resDefName, face = GetFeatureResurrectData(featureID) + if resDefName and face then + local ud = resDefName and UnitDefNames[resDefName] + if ud and not NonZombies[resDefName] then + local rezBaseTime = ud.metalCost / buildpower + local _,_,_,_,reclaimPercent,_ = Spring.GetFeatureResources(featureID) + if reclaimPercent ~= 1 then + rezBaseTime = rezBaseTime + rezBaseTime * (1 - reclaimPercent) + end + + if minRezTime and rezBaseTime < minRezTime then + rezBaseTime = minRezTime + end + + if maxRezTime and rezBaseTime > maxRezTime then + rezBaseTime = maxRezTime + end + + local rezFrame = gameframe + rezBaseTime * 32 + + resurrectingFeatures[featureID] = { + rezInitFrame = gameframe, -- frame Feature was queued + rezBaseTime = rezBaseTime, -- base resurrect time in seconds + rezFrame = rezFrame, -- frame the callback is fired or standard resurrect is performed. + rezFrameCallback = rezFrameCallback, -- callback function fired on rezFrame + rezWarningTime = WARNING_TIME, -- warning time in seconds for base particles and sfx, set 0 to hide. + reclaimPercent = reclaimPercent, -- reclaim left in feature in % to compare and adjust reztime + } + return resurrectingFeatures[featureID] + end + end + return nil +end + +-- Get and modify the table as you wish. +local function GetZombieResurrectData() + return resurrectingFeatures +end + +-- Reclaiming the wreck can stretch the revive time up to 2x the base time. +function gadget:AllowFeatureBuildStep(builderID, builderTeam, featureID, featureDefID, part) + if (resurrectingFeatures[featureID]) then + local rezInitFrame = resurrectingFeatures[featureID].rezInitFrame + local rezBaseTime = resurrectingFeatures[featureID].rezBaseTime + local reclaimPercent = resurrectingFeatures[featureID].reclaimPercent + + reclaimPercent = reclaimPercent + part + local rezTime = rezBaseTime + rezBaseTime * (1 - reclaimPercent) + resurrectingFeatures[featureID].rezFrame = rezInitFrame + rezTime * 32 + + resurrectingFeatures[featureID].reclaimPercent = reclaimPercent + end + return true +end + +function gadget:FeatureDestroyed(featureID, allyTeam) + if (resurrectingFeatures[featureID]) then + resurrectingFeatures[featureID] = nil + end +end + +function gadget:GameFrame(f) + gameframe = f + if f%32 ~= 0 then + return + end + local spSpawnCEG = Spring.SpawnCEG -- putting the localization here because cannot localize in global scope since spring 97 + for featureID in pairs(resurrectingFeatures) do + local rezFrame = resurrectingFeatures[featureID].rezFrame + if rezFrame <= gameframe then + if resurrectingFeatures[featureID].rezFrameCallback then + local callbackFunction = resurrectingFeatures[featureID].rezFrameCallback + resurrectingFeatures[featureID] = nil + callbackFunction(featureID) + else + local unitID = TurnFeatureIntoUnit(featureID,GaiaTeamID) + SetZombieBehavior(unitID) + SetHealthToReclaimPercent(featureID, unitID) + Spring.DestroyFeature(featureID) + end + else + local secondsTillRezCall = floor((rezFrame - f) / 32) + local rezWarningTime = resurrectingFeatures[featureID].rezWarningTime + --Spring.Echo("Time left for "..featureID..": "..secondsTillRezCall) + if secondsTillRezCall <= rezWarningTime then + local r = Spring.GetFeatureRadius(featureID) + local x, y, z = Spring.GetFeaturePosition(featureID) + spSpawnCEG(CEG_SPAWN, x, y, z, 0, 0, 0, 10 + r, 10 + r) + + if secondsTillRezCall == rezWarningTime then + local z_sound = ZOMBIE_SOUNDS[random(#ZOMBIE_SOUNDS)] + GG.PlayFogHiddenSound(z_sound, 4, x, y, z) + end + end + end + end +end + function gadget:Initialize() mapWidth = Game.mapSizeX mapHeight = Game.mapSizeZ GG.Zombies = { - TurnFeatureIntoUnit = TurnFeatureIntoUnit, - SetZombieSpeedMult = SetZombieSpeedMult, - SetZombieBehavior = GiveZombiesRandomOrders, - GetFeatureResurrectData = GetFeatureResurrectData -} -end + TurnFeatureIntoUnit = TurnFeatureIntoUnit, + SetZombieSpeedMult = SetZombieSpeedMult, + SetZombieBehavior = SetZombieBehavior, + SetHealthToReclaimPercent = SetHealthToReclaimPercent, + GetFeatureResurrectData = GetFeatureResurrectData, + GetZombieResurrectData = GetZombieResurrectData, -- I want to expose these for modification from outside if desired + AddFeatureToZombieCountdown = AddFeatureToZombieCountdown + } +end \ No newline at end of file diff --git a/LuaRules/Gadgets/game_over.lua b/LuaRules/Gadgets/game_over.lua index 4214c1602b..28bf24d337 100644 --- a/LuaRules/Gadgets/game_over.lua +++ b/LuaRules/Gadgets/game_over.lua @@ -69,6 +69,9 @@ local campaignBattleID = Spring.GetModOptions().singleplayercampaignbattleid and local planetIndex = Spring.GetModOptions().singleplayercampaignbattleid planetIndex = planetIndex and tonumber(planetIndex) +local zombies = tobool(modOptions.zombies) or false +local deadTeamCounter = 0 + local SPARE_PLANETWARS_UNITS = false local SPARE_REGULAR_UNITS = (modOptions.explodeondefeat == "0" or modOptions.explodeondefeat == 0) @@ -191,6 +194,20 @@ local function GetTeamIsChicken(teamID) return false end +local function ZombieIsSecondToLastStanding(allyTeamID) + if not zombies then + return false + end + if GG.ZombiesGlobal.GetGlobalReviveAllyTeam() == allyTeamID then + if #spGetAllyTeamList() == deadTeamCounter+3 then + Spring.Echo("Zombies are defeatable") + return false + end + return true + end + return false +end + local function CountAllianceUnits(allianceID) local teamlist = spGetTeamList(allianceID) or {} local count = 0 @@ -309,6 +326,11 @@ local function DestroyAlliance(allianceID, delayLossToNextGameFrame) alliancesToDestroy[#alliancesToDestroy + 1] = allianceID return end + + if ZombieIsSecondToLastStanding(allianceID) then + return + end + if not destroyedAlliances[allianceID] then destroyedAlliances[allianceID] = true local teamList = spGetTeamList(allianceID) @@ -342,6 +364,7 @@ local function DestroyAlliance(allianceID, delayLossToNextGameFrame) EchoUIMessage("Game Over: If this is true, then please resign.") return -- don't perform victory check else -- kaboom + deadTeamCounter = deadTeamCounter + 1 --TODO there is likely a better way of this if not (isMission or isScriptMission) then local name = Spring.GetGameRulesParam("allyteam_long_name_" .. allianceID) EchoUIMessage(name .. " has been destroyed!") @@ -553,6 +576,9 @@ local function ProcessLastAlly() if (not teamlist) then break end -- continue + if ZombieIsSecondToLastStanding(allyTeamID) then + break + end -- continue local hasActiveTeam = false local hasDroppedTeam = false diff --git a/LuaRules/Gadgets/unit_global_zombie_resurrect.lua b/LuaRules/Gadgets/unit_global_zombie_resurrect.lua index 81ea147076..135b5be8b6 100644 --- a/LuaRules/Gadgets/unit_global_zombie_resurrect.lua +++ b/LuaRules/Gadgets/unit_global_zombie_resurrect.lua @@ -36,10 +36,6 @@ local getMovetype = Spring.Utilities.getMovetype VFS.Include("LuaRules/Configs/CAI/accessory/targetReachableTester.lua") -- I'm scared... what exactly did including that file do and how do I use it? -local spGetGroundHeight = Spring.GetGroundHeight -local spGetUnitPosition = Spring.GetUnitPosition -local spGetFeaturePosition = Spring.GetFeaturePosition -local spCreateUnit = Spring.CreateUnit local spGetUnitDefID = Spring.GetUnitDefID local spGetUnitTeam = Spring.GetUnitTeam local spGetAllUnits = Spring.GetAllUnits @@ -47,256 +43,384 @@ local spGetGameFrame = Spring.GetGameFrame local spGetAllFeatures = Spring.GetAllFeatures local spGiveOrderToUnit = Spring.GiveOrderToUnit local spGetUnitCommandCount = Spring.GetUnitCommandCount -local spDestroyFeature = Spring.DestroyFeature -local spGetUnitIsDead = Spring.GetUnitIsDead -local spGiveOrderArrayToUnitArray = Spring.GiveOrderArrayToUnitArray -local spGetUnitsInCylinder = Spring.GetUnitsInCylinder local spSetTeamResource = Spring.SetTeamResource local spGetUnitHealth = Spring.GetUnitHealth -local spSetUnitRulesParam = Spring.SetUnitRulesParam local GaiaTeamID = Spring.GetGaiaTeamID() local GaiaAllyTeamID = select(6, Spring.GetTeamInfo(GaiaTeamID, false)) - -local gameframe = 0 -local LOS_ACCESS = {inlos = true} -local PRIVATE_ACCESS = {private = true} - -local random = math.random -local floor = math.floor - -local mapWidth -local mapHeight - -local reclaimed_data = {} -- holds initial gameframe, initial time in seconds, % of feature reclaimed -local zombies_to_spawn = {} -local zombiesToSpawnList = {} -local zombiesToSpawnMap = {} -local zombiesToSpawnCount = 0 -local zombies = {} - -local ZOMBIE_SOUNDS = { - "sounds/misc/zombie_1.wav", - "sounds/misc/zombie_2.wav", - "sounds/misc/zombie_3.wav", -} -local REZ_SOUND = "sounds/misc/resurrect.wav" - -local defined = false -- wordaround, because i meet some kind of racing condition, if any gadget spawns gaia BEFORE this gadget can process all the stuff... - -local NonZombies = { - ["asteroid"] = true, -} - -local WARNING_TIME = 5 -- seconds to start being scary before actual reanimation event -local ZOMBIES_REZ_MIN = tonumber(modOptions.zombies_delay) -if (tonumber(ZOMBIES_REZ_MIN) == nil) then - -- minimum of 10 seconds, max is determined by rez speed - ZOMBIES_REZ_MIN = 10 +local allyTeamCount = #Spring.GetAllyTeamList() -1 -- -1 for Gaia team + +local zombieAllyTeamID = GaiaAllyTeamID +local zombieTeamInput = tonumber(modOptions.zombies_team) or 0 + +if zombieTeamInput == 0 then -- 0 translates to gaia zombies + zombieAllyTeamID = GaiaAllyTeamID +elseif zombieTeamInput > allyTeamCount then -- larger than teamcount mean things revive onto their parent teams + zombieAllyTeamID = math.huge +else -- if inbetween 0 and max teamcount, we have a team to spawn on! + zombieAllyTeamID = zombieTeamInput -1 -- -1 for offset + if zombieAllyTeamID == GaiaAllyTeamID then --TODO there is likely a better solution, it seems sometimes Gaia is not the last team in the list + zombieAllyTeamID = zombieAllyTeamID +1 + end end -local ZOMBIES_REZ_SPEED = tonumber(modOptions.zombies_rezspeed) -if (tonumber(ZOMBIES_REZ_SPEED) == nil) then - -- 12m/s, big units have a really long time to respawn - ZOMBIES_REZ_SPEED = 12 -end +local zombieTeamFullSlow = tonumber(modOptions.zombies_team_full_speed_mult) or 0 +local zombieTeamStartsDead = tonumber(modOptions.zombies_team_starts_dead) or 0 -local ZOMBIES_PERMA_SLOW = tonumber(modOptions.zombies_permaslow) -if (tonumber(ZOMBIES_PERMA_SLOW) == nil) then - -- from 0 to 1, symbolises from 0% to 50% slow which is always on - ZOMBIES_PERMA_SLOW = 1 -end +local zombieRezSpeed = tonumber(modOptions.zombies_rezspeed) or 12 +local zombieRezSpeedScaler = tonumber(modOptions.zombies_rezspeed_scaler) or 0 +local zombieRezSpeedMinuteAdd = tonumber(modOptions.zombies_rezspeed_minute_add) or 0 + +local ZOMBIES_PERMA_SLOW = tonumber(modOptions.zombies_speed_mult) or 0.5 + +local ZOMBIES_REZ_MIN = tonumber(modOptions.zombies_delay_min) or 10 -- minimum of 10 seconds +local ZOMBIES_REZ_MAX = tonumber(modOptions.zombies_delay_max) or 100000 --600 only affects things above 7200 cost at 12 rezspeed. + +local ZOMBIES_PARTIAL_RECLAIM = tonumber(modOptions.zombies_partial_reclaim) or 0 +local zombieOverflowSpawn = tonumber(modOptions.zombies_partial_spawn) or 1 + +local zombieDeathOnCaptureChance = tonumber(modOptions.zombies_die_on_capture) or 0 + +local zombieReviveCyclesCount = tonumber(modOptions.zombies_revive_cycles) or 1 +local zombieWrecksRemain = tonumber(modOptions.zombies_wrecks_remain) or 0 + +local zombiesReviveOptionsAsString = (modOptions.zombies_revive_options) or nil +local zombieReviveOptionMultiRandomise = tonumber(modOptions.zombies_revive_options_multi_random) or 0 + +local zombieBudgetMult = tonumber(modOptions.zombies_budget_multiplier) or 1 +local zombieBudgetMultScaler = tonumber(modOptions.zombies_budget_multiplier_scaler) or 0 +local zombieBudgetMultMinuteAdd = tonumber(modOptions.zombies_budget_multiplier_minute_add) or 0 + +local zombieFlatBudget = tonumber(modOptions.zombies_flat_budget) or 0 +local zombieFlatBudgetScaler = tonumber(modOptions.zombies_flat_budget_scaler) or 0 +local zombieFlatBudgetMinuteAdd = tonumber(modOptions.zombies_flat_budget_minute_add) or 0 + +local zombieUnits = {} +local featureReviveCycleCount = {} +local excessBudget = 0 +local zombieReviveOptionCount = 0 +local zombieReviveOptions = {} -- allows duplicates -if ZOMBIES_PERMA_SLOW == 0 then - ZOMBIES_PERMA_SLOW = nil -else - ZOMBIES_PERMA_SLOW = 1 - ZOMBIES_PERMA_SLOW*0.5 +--Diagnostic variables +local totalBaseRevivedBudget = 0 +local totalRevivedBudget = 0 +local totalRevivedValue = 0 +local totalReviveCount = 0 + +--TODO there seems to be something with the comnames adding some more stuff? +local UnitDefBothNames = {} -- Includes humanName and name +local function AddName(name, unitDefId) + name = name:lower() + UnitDefBothNames[name] = UnitDefBothNames[name] or {} + UnitDefBothNames[name][#UnitDefBothNames[name] + 1] = unitDefId end -local ZOMBIES_PARTIAL_RECLAIM = (tonumber(modOptions.zombies_partial_reclaim) == 1) +if zombiesReviveOptionsAsString then -- TODO pilfered from lockunits_modoption, would there be a better way to do this? Maybe should have an api :O + zombiesReviveOptionsAsString = zombiesReviveOptionsAsString:gsub("[%s%+]*%+[%s%+]*","+"):gsub("^%s*",""):gsub("%s*$",""):lower() -local CMD_REPEAT = CMD.REPEAT -local CMD_MOVE_STATE = CMD.MOVE_STATE -local CMD_INSERT = CMD.INSERT -local CMD_FIGHT = CMD.FIGHT -local CMD_OPT_SHIFT = CMD.OPT_SHIFT -local CMD_GUARD = CMD.GUARD + for unitDefID = 1, #UnitDefs do + AddName(UnitDefs[unitDefID].humanName, unitDefID) + AddName(UnitDefs[unitDefID].name, unitDefID) + end -local CEG_SPAWN = [[zombie]] + for name in string.gmatch(zombiesReviveOptionsAsString, '([^+]+)') do + if UnitDefBothNames[name] then + for i = 1, #UnitDefBothNames[name] do + local unitDefID = UnitDefBothNames[name][i] + zombieReviveOptionCount = zombieReviveOptionCount + 1 + zombieReviveOptions[zombieReviveOptionCount] = unitDefID + end + end + end +end -local function disSQ(x1, y1, x2, y2) - return (x1 - x2)^2 + (y1 - y2)^2 +local function DiagnosticsVarDump() + --May be good to have a screen like chickens + Spring.Echo("-------------") + Spring.Echo("totalUnmodifiedReviveBudget: "..totalBaseRevivedBudget) + Spring.Echo("totalModifiedReviveBudget: "..totalRevivedBudget) + Spring.Echo("totalRevivedValue: "..totalRevivedValue) + Spring.Echo("totalReviveCount: "..totalReviveCount) + Spring.Echo("zombieFlatBudget: "..zombieFlatBudget) + Spring.Echo("zombieBudgetMult: "..zombieBudgetMult) + Spring.Echo("currentReviveSpeed: "..zombieRezSpeed) + Spring.Echo("zombieOverflowBudget: "..excessBudget) + Spring.Echo("-------------") end -local function RemoveZombieToSpawn(featureID) - if not zombiesToSpawnMap[featureID] then +-- TODO Is there a global utility function for this? +local function IsTeamInAllyTeam (allyTeamID, teamIDtoCheck) + local allyTeamList = Spring.GetTeamList(allyTeamID) + if not allyTeamList then -- shouldnt happen but oh well return false end - zombies_to_spawn[featureID] = nil - reclaimed_data[featureID] = nil + for index, teamID in ipairs(allyTeamList) do + if teamID == teamIDtoCheck then + return true + end + end + return false +end + +local function GetRandomTeamIDFromAllyTeam(allyTeamID) -- function since the team zombies revive onto could be changed mid game. + local allyTeamList = Spring.GetTeamList(allyTeamID) or GaiaAllyTeamID + local teamMateCount = 0 + local teamToSpawnOn + if allyTeamID == GaiaAllyTeamID then + return GaiaTeamID + end - local index = zombiesToSpawnMap[featureID] + for _,_ in ipairs(allyTeamList) do + teamMateCount = teamMateCount + 1 + end + teamToSpawnOn = allyTeamList[math.random(1,teamMateCount)] or nil + return teamToSpawnOn +end + +local function HandleZombieRevive(featureID, reviveBudget,reviveTeamID,unitDefIDTable) + local resDefName, facing = GG.Zombies.GetFeatureResurrectData(featureID) + local zombieUnitDefID = nil --nil means we use the features default revival thingy + local zombieCost = UnitDefNames[resDefName].metalCost + if not zombieCost or zombieCost <= 0 then + zombieCost = UnitDefNames[resDefName].buildTime + end + if unitDefIDTable then + zombieUnitDefID = unitDefIDTable[math.random(1,#unitDefIDTable)] + zombieCost = UnitDefs[zombieUnitDefID].metalCost or 0 + if zombieCost or zombieCost <= 0 then + zombieCost = UnitDefs[zombieUnitDefID].buildTime + end + end + + if not zombieCost or zombieCost <= 0 then + zombieCost = 200 + Spring.Echo("Unit has no cost set, defaulting to 200") + end + + while reviveBudget >= zombieCost do + local unitID = GG.Zombies.TurnFeatureIntoUnit(featureID,reviveTeamID,zombieUnitDefID) + if unitID then + zombieUnits[unitID] = true + GG.Zombies.SetZombieBehavior(unitID) + GG.Zombies.SetZombieSpeedMult(unitID, ZOMBIES_PERMA_SLOW) + totalReviveCount = totalReviveCount +1 + totalRevivedValue = math.floor(totalRevivedValue + zombieCost) + end + reviveBudget = reviveBudget - zombieCost + if zombieReviveOptionMultiRandomise == 1 and zombieUnitDefID then -- Randomises to a new unit if desired + zombieUnitDefID = unitDefIDTable[math.random(1,#unitDefIDTable)] + zombieCost = UnitDefs[zombieUnitDefID].metalCost or 0 + if not zombieCost or zombieCost <= 0 then + zombieCost = UnitDefs[zombieUnitDefID].buildTime + end + if not zombieCost or zombieCost <= 0 then + zombieCost = 200 + Spring.Echo("Unit has no cost set, defaulting to 200") + end + end + --TODO should we randomise for each spawn? So far yes, since it gives every possible member of the team a chance for a unit + if zombieAllyTeamID ~= GaiaAllyTeamID and not (zombieAllyTeamID > allyTeamCount) then + reviveTeamID = GetRandomTeamIDFromAllyTeam(zombieAllyTeamID) + end + end + if zombieOverflowSpawn == 1 and math.floor(reviveBudget) > 0 then -- We make a partial health zombie or overflow the budget to the next spawn + local unitID = GG.Zombies.TurnFeatureIntoUnit(featureID,reviveTeamID,zombieUnitDefID) + if not unitID then + return reviveBudget + end + zombieUnits[unitID] = true + GG.Zombies.SetZombieBehavior(unitID) + GG.Zombies.SetZombieSpeedMult(unitID, ZOMBIES_PERMA_SLOW) + if not ZOMBIES_PARTIAL_RECLAIM == 1 then + return 0 + end + totalReviveCount = totalReviveCount +1 + totalRevivedValue = math.floor(totalRevivedValue + zombieCost) + local health = Spring.GetUnitHealth(unitID) -- TODO something breaks here, check if unit exists? + if health then --Last zombie spawned gets its health cut by the % of metal it overspent + Spring.SetUnitHealth(unitID, health*(reviveBudget/zombieCost)) + reviveBudget = 0 + end + end + return math.floor(reviveBudget) +end + +local function ZombieBudgetProcessing(featureID,excessBudget) + local thisFeatureBudget + local currentMetal, maxMetal = Spring.GetFeatureResources(featureID) + local resDefName, facing = GG.Zombies.GetFeatureResurrectData(featureID) + local zombieBudget = UnitDefNames[resDefName].metalCost - zombiesToSpawnList[index] = zombiesToSpawnList[zombiesToSpawnCount] - zombiesToSpawnMap[zombiesToSpawnList[zombiesToSpawnCount]] = index + if ZOMBIES_PARTIAL_RECLAIM == 1 then -- partial reclaim reduces the available metal to spawn units + zombieBudget = math.floor(zombieBudget * (currentMetal/maxMetal)) + end + thisFeatureBudget = math.floor(excessBudget + zombieFlatBudget + zombieBudget * zombieBudgetMult) - zombiesToSpawnList[zombiesToSpawnCount] = nil - zombiesToSpawnMap[featureID] = nil - zombiesToSpawnCount = zombiesToSpawnCount - 1 + --TODO these scaling functions could use improvement in terms of design goal and intended effect + if zombieFlatBudgetScaler ~= 0 then + zombieFlatBudget = zombieFlatBudget + zombieFlatBudgetScaler * zombieBudget/100 --TODO Took out flooring, lets see if it desyncs eventually? + end + if zombieBudgetMultScaler ~= 0 then + zombieBudgetMult = zombieBudgetMult + 0.000001 * math.floor(zombieBudgetMultScaler * zombieBudget^1.05) + end + if zombieRezSpeedScaler ~= 0 then + zombieRezSpeed = zombieRezSpeed + zombieRezSpeedScaler * zombieBudget/1000 --TODO Took out flooring, lets see if it desyncs eventually? + end - return true + totalBaseRevivedBudget = math.floor(totalBaseRevivedBudget + zombieBudget) + totalRevivedBudget = math.floor(totalRevivedBudget + thisFeatureBudget) + return thisFeatureBudget end --- reclaiming zombies 'causes delay in rez, basically you have to have about ZOMBIES_REZ_SPEED/2 or bigger BP to reclaim faster than it resurrects... --- TODO do more math to figure out how to perform it better? -function gadget:AllowFeatureBuildStep(builderID, builderTeam, featureID, featureDefID, part) - if (zombies_to_spawn[featureID]) then - local base_time = reclaimed_data[featureID] - base_time[3] = base_time[3] - part - zombies_to_spawn[featureID] = base_time[1] + base_time[2] * (1+base_time[3]) * 32 +local function FeatureReviveCycles(featureID) + if featureReviveCycleCount[featureID] == nil then + featureReviveCycleCount[featureID] = zombieReviveCyclesCount - 1 -- one cycle has passed to reach this point + else + featureReviveCycleCount[featureID] = featureReviveCycleCount[featureID] - 1 end - return true + return featureReviveCycleCount[featureID] end -local function CheckZombieOrders() -- i can't rely on Idle because if for example unit is unloaded it doesnt count as idle... weird - for unitID, _ in pairs(zombies) do - local queueSize = spGetUnitCommandCount(unitID) - if not (queueSize) or not (queueSize > 0) then - GG.Zombies.SetZombieBehavior(unitID) - end +local function RezFrameCallback(featureID) + local reviveTeamID = GaiaTeamID + if zombieAllyTeamID ~= GaiaAllyTeamID and not (zombieAllyTeamID > allyTeamCount) then + reviveTeamID = GetRandomTeamIDFromAllyTeam(zombieAllyTeamID) + end + if zombieAllyTeamID > allyTeamCount then + reviveTeamID = Spring.GetFeatureTeam(featureID) or GaiaTeamID + end + + local zombieBudget = ZombieBudgetProcessing(featureID,excessBudget) + + if #zombieReviveOptions == 0 then + excessBudget = HandleZombieRevive(featureID, zombieBudget,reviveTeamID, nil) + else + excessBudget = HandleZombieRevive(featureID, zombieBudget,reviveTeamID, zombieReviveOptions) + end + + local zombieCyclesRemaining = 0 + if zombieReviveCyclesCount > 1 then + zombieCyclesRemaining = FeatureReviveCycles(featureID) + end + if zombieCyclesRemaining > 0 then + GG.Zombies.AddFeatureToZombieCountdown(featureID, zombieRezSpeed, ZOMBIES_REZ_MIN, ZOMBIES_REZ_MAX, RezFrameCallback) + return + end + + if zombieWrecksRemain == 0 then + Spring.DestroyFeature(featureID) end end -function gadget:GameFrame(f) - gameframe = f - if (f%32) == 0 then - local spSpawnCEG = Spring.SpawnCEG -- putting the localization here because cannot localize in global scope since spring 97 - local index = 1 - while index <= zombiesToSpawnCount do - local featureID = zombiesToSpawnList[index] - local time_to_spawn = zombies_to_spawn[featureID] - local x, y, z = spGetFeaturePosition(featureID) - - if time_to_spawn <= f then - if not RemoveZombieToSpawn(featureID) then - index = index + 1 - end - GG.Zombies.TurnFeatureIntoUnit(featureID,GaiaTeamID,true) - else - local steps_to_spawn = floor((time_to_spawn - f) / 32) - local resName, face = GG.Zombies.GetFeatureResurrectData(featureID) - if steps_to_spawn <= WARNING_TIME then - local r = Spring.GetFeatureRadius(featureID) - - spSpawnCEG(CEG_SPAWN, x, y, z, 0, 0, 0, 10 + r, 10 + r) - - if steps_to_spawn == WARNING_TIME then - local z_sound = ZOMBIE_SOUNDS[math.random(#ZOMBIE_SOUNDS)] - GG.PlayFogHiddenSound(z_sound, 4, x, y, z) - end - end - index = index + 1 +local function CheckZombieOrders() -- Only Gaia units recheck their orders + for unitID, _ in pairs(zombieUnits) do + if spGetUnitTeam(unitID) == GaiaTeamID then + local queueSize = spGetUnitCommandCount(unitID) + if not (queueSize) or not (queueSize > 0) then + GG.Zombies.SetZombieBehavior(unitID) end end end +end + +function gadget:GameFrame(f) if (f%640) == 1 then CheckZombieOrders() + DiagnosticsVarDump() + end + if f%1800 == 1 and f ~= 1 then + zombieBudgetMult = zombieBudgetMult + zombieBudgetMultMinuteAdd + zombieFlatBudget = zombieFlatBudget + zombieFlatBudgetMinuteAdd + zombieRezSpeed = zombieRezSpeed + zombieRezSpeedMinuteAdd end if f == 1 then spSetTeamResource(GaiaTeamID, "ms", 500) spSetTeamResource(GaiaTeamID, "es", 10500) + if zombieTeamStartsDead == 1 then -- TODO doing it on game start lets people plop a fac. maybe there is some other way of doing things + for _, unitID in ipairs(spGetAllUnits()) do + if zombieAllyTeamID == Spring.GetUnitAllyTeam(unitID)then + Spring.DestroyUnit(unitID) + end + end + end end end -- settings gaiastorage before frame 1 somehow doesnt work, well i can guess why... function gadget:UnitDestroyed(unitID, unitDefID, unitTeam) - if zombies[unitID] then - zombies[unitID] = nil + if zombieUnits[unitID] then + zombieUnits[unitID] = nil end end function gadget:UnitTaken(unitID, unitDefID, teamID, newTeamID) - if zombies[unitID] and newTeamID ~= GaiaTeamID then - zombies[unitID] = nil - -- taking away zombie from zombie team unpermaslows it - if ZOMBIES_PERMA_SLOW then - GG.Zombies.SetZombieSpeedMult(unitID, 1) + -- Taking a unit that is marked as a zombie unslows it/rolls for death on capture. + if zombieUnits[unitID] then + if zombieDeathOnCaptureChance > math.random(0,1) then + Spring.DestroyUnit(unitID) + return end - elseif newTeamID == GaiaTeamID then - gadget:UnitFinished(unitID, unitDefID, newTeamID) + GG.Zombies.SetZombieSpeedMult(unitID, 1) + zombieUnits[unitID] = nil + return + end + -- if Gaia captures, applies movement and slow + if newTeamID == GaiaTeamID then + zombieUnits[unitID] = true + GG.Zombies.SetZombieBehavior(unitID) + GG.Zombies.SetZombieSpeedMult(unitID, ZOMBIES_PERMA_SLOW) + return + end + -- if a zombieallyteam with full slow enabled captures, the unit is slowed but is not given orders + if IsTeamInAllyTeam(zombieAllyTeamID,newTeamID) and zombieTeamFullSlow == 1 then + zombieUnits[unitID] = true + GG.Zombies.SetZombieSpeedMult(unitID, ZOMBIES_PERMA_SLOW) end end + function gadget:UnitCreated(unitID, unitDefID, teamID, builderID) - gadget:UnitFinished(unitID, unitDefID, teamID) + -- GaiaTeam units are slowed and behave like zombies by default + if teamID == GaiaTeamID then + zombieUnits[unitID] = true + GG.Zombies.SetZombieBehavior(unitID) + GG.Zombies.SetZombieSpeedMult(unitID, ZOMBIES_PERMA_SLOW) + return + end + -- If on Zombie Allyteam with Full slow enabled, slows all created units + if IsTeamInAllyTeam(zombieAllyTeamID,teamID) and zombieTeamFullSlow == 1 then + zombieUnits[unitID] = true + GG.Zombies.SetZombieSpeedMult(unitID, ZOMBIES_PERMA_SLOW) + end end -local UnitFinished = function(_,_,_) end - -function gadget:UnitFinished(unitID, unitDefID, teamID) - UnitFinished(unitID, unitDefID, teamID) +function gadget:FeatureCreated(featureID, allyTeam) + GG.Zombies.AddFeatureToZombieCountdown(featureID, zombieRezSpeed, ZOMBIES_REZ_MIN, ZOMBIES_REZ_MAX, RezFrameCallback) end -function gadget:FeatureCreated(featureID, allyTeam) - local resName, face = GG.Zombies.GetFeatureResurrectData(featureID) - if resName and face and not zombies_to_spawn[featureID] then - local ud = resName and UnitDefNames[resName] - if ud and not NonZombies[resName] then - local rez_time = ud.metalCost / ZOMBIES_REZ_SPEED - if (rez_time < ZOMBIES_REZ_MIN) then - rez_time = ZOMBIES_REZ_MIN - end - reclaimed_data[featureID] = {gameframe, rez_time, 0} - zombies_to_spawn[featureID] = gameframe + (rez_time*32) - - zombiesToSpawnCount = zombiesToSpawnCount + 1 - zombiesToSpawnList[zombiesToSpawnCount] = featureID - zombiesToSpawnMap[featureID] = zombiesToSpawnCount +local function ReInit() + local units = spGetAllUnits() + for i = 1, #units do + local unitID = units[i] + local unitTeam = spGetUnitTeam(unitID) + if (unitTeam == GaiaTeamID) then + zombieUnits[unitID] = true + GG.Zombies.SetZombieSpeedMult(unitID,ZOMBIES_PERMA_SLOW) + GG.Zombies.SetZombieBehavior(unitID) end end + local features = spGetAllFeatures() + for i = 1, #features do + GG.Zombies.AddFeatureToZombieCountdown(features[i], zombieRezSpeed, ZOMBIES_REZ_MIN, ZOMBIES_REZ_MAX, RezFrameCallback) + end end -function gadget:FeatureDestroyed(featureID, allyTeam) - if (zombies_to_spawn[featureID]) then - RemoveZombieToSpawn(featureID) - end +local function SetGlobalReviveAllyTeam(newZombieAllyTeamID) + zombieAllyTeamID = newZombieAllyTeamID end -local function ReInit(reinit) - mapWidth = Game.mapSizeX - mapHeight = Game.mapSizeZ - if not (defined) then - UnitFinished = function(unitID, unitDefID, teamID, builderID) - if (teamID == GaiaTeamID) and not (zombies[unitID]) then - spGiveOrderToUnit(unitID, CMD_REPEAT, 1, 0) - spGiveOrderToUnit(unitID, CMD_MOVE_STATE, 2, 0) - GG.Zombies.SetZombieBehavior(unitID) - zombies[unitID] = true - if ZOMBIES_PERMA_SLOW then - local maxHealth = select(2, spGetUnitHealth(unitID)) - if maxHealth then - GG.Zombies.SetZombieSpeedMult(unitID, ZOMBIES_PERMA_SLOW) - end - end - end - end - defined = true - end - if (reinit) then - gameframe = spGetGameFrame() - local units = spGetAllUnits() - for i = 1, #units do - local unitID = units[i] - local unitTeam = spGetUnitTeam(unitID) - if (unitTeam == GaiaTeamID) then - gadget:UnitFinished(unitID, spGetUnitDefID(unitID), unitTeam) - end - end - local features = spGetAllFeatures() - for i = 1, #features do - gadget:FeatureCreated(features[i], 1) -- doesnt matter who is owner of feature - end - end +local function GetGlobalReviveAllyTeam() + return zombieAllyTeamID end function gadget:Initialize() @@ -305,10 +429,18 @@ function gadget:Initialize() return end if (spGetGameFrame() > 1) then - ReInit(true) + ReInit() end + + GG.ZombiesGlobal = { + SetGlobalReviveAllyTeam = SetGlobalReviveAllyTeam, + GetGlobalReviveAllyTeam = GetGlobalReviveAllyTeam, + } end function gadget:GameStart() - ReInit(true) -- anything it does doesnt mess with existing zombies -end + ReInit() -- anything it does doesnt mess with existing zombies + Spring.Echo("zombieAllyTeamID: "..zombieAllyTeamID) + Spring.Echo("AllyTeamCount: "..allyTeamCount) + Spring.Echo("GaiaAllyTeamID: "..GaiaAllyTeamID) +end \ No newline at end of file diff --git a/LuaRules/engine_compat_post.lua b/LuaRules/engine_compat_post.lua index 578403833a..a02b3d4066 100644 --- a/LuaRules/engine_compat_post.lua +++ b/LuaRules/engine_compat_post.lua @@ -13,8 +13,8 @@ end -- Spring.RequestPath causes desync https://github.com/beyond-all-reason/RecoilEngine/issues/2434 -if Script.IsEngineMinVersion(2025, 1, 0) and Spring.GetModOptions().luapathrequest ~= "1" and - ((tonumber(Spring.GetModOptions().zombies) ~= 1) or Spring.GetModOptions().singleplayercampaignbattleid) then +if true or Script.IsEngineMinVersion(2025, 1, 0) and +Spring.GetModOptions().luapathrequest ~= "1" then if widgetHandler then widgetHandler.WG.Disable_RequestPath = true end diff --git a/ModOptions.lua b/ModOptions.lua index 39f53d3c57..39050e6081 100644 --- a/ModOptions.lua +++ b/ModOptions.lua @@ -56,7 +56,7 @@ local options = { type = 'section', }, { - key = 'silly', -- lava, fun, zombies + key = 'silly', -- lava, fun name = 'Silly', desc = 'Silly options for trolling.', type = 'section', @@ -111,7 +111,15 @@ local options = { max=1, step=0.05, }, - +--[[ +TODO would be nice to a seperate section but I think it needs lobby changes. + { + key = 'zombies', + name = 'Zombies', + desc = 'Zombies settings.', + type = 'section', + }, +]] { key = 'zombies', name = 'Enable zombies', @@ -121,46 +129,227 @@ local options = { def = false, }, { - key = 'zombies_delay', + key = 'zombies_rezspeed', + name = 'Zombie resurrection speed', + desc = "In metal per second.", + type = 'number', + section= 'silly', + def=12, + min=1, + max=10000, + step=1, + }, + { + key = 'zombies_speed_mult', + name = 'Zombie speed multiplier', + desc = "Sets the speed multiplier of zombies", + type = 'number', + section= 'silly', + def=0.5, + min=0, + max=100, + step=0.01, + }, + { + key = 'zombies_delay_min', name = 'Zombie min spawn time', desc = "In seconds, unit will resurrection no faster than this.", type = 'number', section= 'silly', def=10, min=1, - max=600, + max=100000, step=1, }, { - key = 'zombies_rezspeed', - name = 'Zombie resurrection speed', - desc = "In metal per second.", + key = 'zombies_delay_max', + name = 'Zombie max base spawn time', + desc = "In seconds, unit will resurrection no slower than this. Is still extended by reclaiming.", + section= 'silly', + type = 'number', + def=100000, + min=1, + max=100000, + step=1, + }, + { + key = 'zombies_partial_reclaim', + name = 'Zombies partially reclaimable', + desc = "Partially reclaimed zombies are partially damaged. For Zombie revive options, reclaiming reduces spawn budget.", + type = 'bool', + section= 'silly', + def = true, + }, + { + key = 'zombies_partial_spawn', + name = 'Zombie partial spawn', + desc = "Spawns a partial health zombie if it exceeds available budget. If false, budget that can't be spent on the selected option is carried to the next zombie revive.", + type = "bool", + section= 'silly', + def = true, + }, + { + key = 'zombies_team', + name = 'Zombie Team', + desc = "Which Allyteam the zombies respawn into. Set 0 for Gaia/neutral zombies. Set higher than teamcount to revive onto the original owner team.", + section= 'silly', type = 'number', + def=0, + min=0, + max=100, + step=1, + }, + { + key = 'zombies_revive_options', + name = 'Zombies revive into these units', + desc = "Wrecks revive into one of these units instead of reviving normally, multiple if the wreck has multiple times the budget left. Specify multiple units to randomise through by using unitName +", + type = "string", section= 'silly', - def=12, + def = "", + }, + { + key = 'zombies_revive_options_multi_random', + name = 'Zombies revive multi randomise', + desc = "Zombies randomise into multiple revive options rather than choosing only one.", + type = "bool", + section= 'silly', + def = false, + }, + { + key = 'zombies_revive_cycles', + name = 'Zombie Revive Cycles', + desc = "Wrecks revive multiple times. Once a revive finishes and spawns a unit the next cycle starts.", + section= 'silly', + type = 'number', + def=1, min=1, max=10000, step=1, }, { - key = 'zombies_permaslow', - name = 'Zombie permaslow modifier', - desc = "If more than 0 zombies are permaslowed to half of that amount, so 1 means 50% slow.", - type = 'number', + key = 'zombies_wrecks_remain', + name = 'Zombie wrecks remain', + desc = "If final Zombie resurrection doesnt consume the wreck.", + type = 'silly', + section= 'silly', + def = false, + }, + { + key = 'zombies_rezspeed_minute_add', + name = 'Zombie resurrect speed minute addition', + desc = "Zombie resurrection speed is increased by this every minute.", + type = "number", + section= 'silly', + def=0, + min=-10000, + max=10000, + step=1, + }, + { + key = 'zombies_rezspeed_scaler', + name = 'Zombie resurrect speed scaling factor', + desc = "Each wreck revived the resurrect speed by this value * wreck unit cost/1000.", + type = "number", + section= 'silly', + def=0, + min=-10000, + max=10000, + step=1, + }, + --TODO These scaling mechanics below are maybe too detached. scaling rez speed seems alot more appropriate, but it runs into issues of shortcomings of budget for large zombie spawns. Still valuable options to have. + { + key = 'zombies_budget_multiplier', + name = 'Zombie budget starting multiplier', + desc = "Multiplies the budget wrecks have to revive into units with.", section= 'silly', + type = 'number', def=1, - min=0, - max=1, + min=0.01, + max=100, step=0.01, }, { - key = 'zombies_partial_reclaim', - name = 'Zombies partially reclaimable', - desc = "Partially reclaimed zombies are partially damaged.", + key = 'zombies_flat_budget', + name = 'Zombie flat starting budget', + desc = "Flat budget value added to all zombie wrecks to spawn units.", + type = "number", + section= 'silly', + def=0, + min=-10000, + max=10000, + step=1, + }, + { + key = 'zombies_budget_multiplier_minute_add', + name = 'Zombie budget mult minute addition', + desc = "Every Minute, add this value to the budget multiplier.", + type = "number", + section= 'silly', + def=0, + min=0, + max=100, + step=0.001, + }, + { + key = 'zombies_flat_budget_minute_add', + name = 'Zombie flat budget minute addition', + desc = "Every Minute, add this value to the flat budget.", + type = "number", + section= 'silly', + def=0, + min=0, + max=10000, + step=1, + }, + { + key = 'zombies_budget_multiplier_scaler', + name = 'Zombie budget mult scaling factor', + desc = "Each wreck revived adds a small fraction the budget multiplier for all future revives. It adds this factor * 0.000001 * zombieBaseBudget^1.05", + type = "number", + section= 'silly', + def=0, + min=0, + max=100, + step=0.001, + }, + { + key = 'zombies_flat_budget_scaler', + name = 'Zombie flat budget scaling factor', + desc = "Each wreck revived adds to the flat budget for all future revives by this value * wreck unit cost/100.", + type = "number", + section= 'silly', + def=0, + min=0, + max=100, + step=0.001, + }, + { + key = 'zombies_team_starts_dead', + name = 'Zombie Team starts dead', + desc = "If the zombie team units die on game start.", type = 'bool', section= 'silly', def = false, }, + { + key = 'zombies_team_full_speed_mult', + name = 'Zombie Team Full Speedmod', + desc = "If the whole Zombie team is effected by the speedmultiplier on start, aswell as all produced units. All gaia units affected by slow regardless", + type = 'bool', + section= 'silly', + def = false, + }, + { + key = 'zombies_die_on_capture', + name = 'Zombie death chance on capture', + desc = "Chance that zombies die instead of being captured. 0 all capture succeed, 1 makes all capture fail, ", + section= 'silly', + type = 'number', + def=0, + min=0, + max=1, + step=0.01, + }, { key = "max_com_level", name = "Commander level limit",