diff --git a/LuaRules/Gadgets/api_zombie_helper.lua b/LuaRules/Gadgets/api_zombie_helper.lua index fc81f73837..aafc23bed2 100644 --- a/LuaRules/Gadgets/api_zombie_helper.lua +++ b/LuaRules/Gadgets/api_zombie_helper.lua @@ -27,16 +27,34 @@ 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 @@ -50,39 +68,44 @@ local function GetFeatureResurrectData(featureID) return featureDefName, 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 +-- Zombie resurrect +-- Turns a feature into a unit if applicable. Has a callback returning featureID and unitID for data transfer. Returns unitID. +local function TurnFeatureIntoUnit(featureID,teamID,reclaimPercentHealthBool, unitReviveCallback) + local featureDefName,facing = GetFeatureResurrectData(featureID) + local x, y, z = Spring.GetFeaturePosition(featureID) + + local unitID = Spring.CreateUnit(featureDefName, x, y, z, facing, teamID) + + if not (unitID) then + return nil + end + + 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 reclaimPercentHealthBool 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 - - Spring.DestroyFeature(featureID) - return unitID -end + end + + -- Unit and Wreck exist both for value transfer + if (unitReviveCallback) then + unitReviveCallback(unitID,featureID) + end --- Works on non zombie units too. + Spring.DestroyFeature(featureID) + return unitID +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") @@ -103,14 +126,13 @@ 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 @@ -132,13 +154,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,12 +174,12 @@ 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) if IsTargetReallyReachable(unitID, rx, ry, rz, x, y, z) then orders[#orders+1] = {CMD.FIGHT, {rx, ry, rz}, CMD.OPT_SHIFT} @@ -168,6 +191,97 @@ local function GiveZombiesRandomOrders(unitID) RandomFactoryOrders(unitID, unitDefID) -- give factory something to do 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, rezFrameCallback) + local resName, face = GetFeatureResurrectData(featureID) + if resName and face and not resurrectingFeatures[featureID] then + local ud = resName and UnitDefNames[resName] + if ud and not NonZombies[resName] then + local rezBaseTime = ud.metalCost / buildpower + local rezTime = rezBaseTime + local _,_,_,_,reclaimPercent,_ = Spring.GetFeatureResources(featureID) + + rezTime = rezBaseTime + rezBaseTime * (1 - reclaimPercent) + + if (rezTime < minRezTime) then + rezTime = minRezTime + end + resurrectingFeatures[featureID] = { + rezInitFrame = gameframe, -- frame Feature was queued + rezBaseTime = rezBaseTime, -- base resurrect time in seconds + rezFrame = gameframe + rezTime * 32, -- frame the resurrect completes or the callback is fired. + 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 + resurrectingFeatures[featureID].rezFrameCallback(featureID) + else + local unitID = TurnFeatureIntoUnit(featureID,GaiaTeamID,true,nil) + SetZombieBehavior(unitID) + 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() @@ -175,9 +289,11 @@ function gadget:Initialize() mapHeight = Game.mapSizeZ GG.Zombies = { - TurnFeatureIntoUnit = TurnFeatureIntoUnit, - SetZombieSpeedMult = SetZombieSpeedMult, - SetZombieBehavior = GiveZombiesRandomOrders, - GetFeatureResurrectData = GetFeatureResurrectData -} -end + TurnFeatureIntoUnit = TurnFeatureIntoUnit, + SetZombieSpeedMult = SetZombieSpeedMult, + SetZombieBehavior = SetZombieBehavior, + 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/unit_global_zombie_resurrect.lua b/LuaRules/Gadgets/unit_global_zombie_resurrect.lua index 81ea147076..7c28a3edd1 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,48 +43,14 @@ 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 @@ -115,49 +77,6 @@ end local ZOMBIES_PARTIAL_RECLAIM = (tonumber(modOptions.zombies_partial_reclaim) == 1) -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 - -local CEG_SPAWN = [[zombie]] - -local function disSQ(x1, y1, x2, y2) - return (x1 - x2)^2 + (y1 - y2)^2 -end - -local function RemoveZombieToSpawn(featureID) - if not zombiesToSpawnMap[featureID] then - return false - end - zombies_to_spawn[featureID] = nil - reclaimed_data[featureID] = nil - - local index = zombiesToSpawnMap[featureID] - - zombiesToSpawnList[index] = zombiesToSpawnList[zombiesToSpawnCount] - zombiesToSpawnMap[zombiesToSpawnList[zombiesToSpawnCount]] = index - - zombiesToSpawnList[zombiesToSpawnCount] = nil - zombiesToSpawnMap[featureID] = nil - zombiesToSpawnCount = zombiesToSpawnCount - 1 - - return true -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 - end - return true -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) @@ -168,37 +87,6 @@ local function CheckZombieOrders() -- i can't rely on Idle because if for exampl 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 - end - end - end if (f%640) == 1 then CheckZombieOrders() end @@ -223,79 +111,50 @@ function gadget:UnitTaken(unitID, unitDefID, teamID, newTeamID) GG.Zombies.SetZombieSpeedMult(unitID, 1) end elseif newTeamID == GaiaTeamID then - gadget:UnitFinished(unitID, unitDefID, newTeamID) + GG.Zombies.SetZombieSpeedMult(unitID, ZOMBIES_PERMA_SLOW) + GG.Zombies.SetZombieBehavior(unitID) + zombies[unitID] = true end end function gadget:UnitCreated(unitID, unitDefID, teamID, builderID) - gadget:UnitFinished(unitID, unitDefID, teamID) -end - -local UnitFinished = function(_,_,_) end - -function gadget:UnitFinished(unitID, unitDefID, teamID) - UnitFinished(unitID, unitDefID, teamID) -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 + if (teamID == GaiaTeamID) and (builderID == GaiaTeamID) then + GG.Zombies.SetZombieBehavior(unitID) + zombies[unitID] = true + if ZOMBIES_PERMA_SLOW then + local maxHealth = select(2, spGetUnitHealth(unitID)) -- TODO is this check something necessary? or could it be removed + if maxHealth then + GG.Zombies.SetZombieSpeedMult(unitID, ZOMBIES_PERMA_SLOW) 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 end end end -function gadget:FeatureDestroyed(featureID, allyTeam) - if (zombies_to_spawn[featureID]) then - RemoveZombieToSpawn(featureID) - end +local function RezFrameCallback(featureID) + local unitID = GG.Zombies.TurnFeatureIntoUnit(featureID,GaiaTeamID,ZOMBIES_PARTIAL_RECLAIM, nil) + zombies[unitID] = true + GG.Zombies.SetZombieSpeedMult(unitID, ZOMBIES_PERMA_SLOW) + GG.Zombies.SetZombieBehavior(unitID) 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 +function gadget:FeatureCreated(featureID, allyTeam) + GG.Zombies.AddFeatureToZombieCountdown(featureID, ZOMBIES_REZ_SPEED, ZOMBIES_REZ_MIN, RezFrameCallback) +end + +local function ReInit() + local units = spGetAllUnits() + for i = 1, #units do + local unitID = units[i] + local unitTeam = spGetUnitTeam(unitID) + if (unitTeam == GaiaTeamID) then + zombies[unitID] = true + GG.Zombies.SetZombieSpeedMult(unitID,ZOMBIES_PERMA_SLOW) + GG.Zombies.SetZombieBehavior(unitID) 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 + local features = spGetAllFeatures() + for i = 1, #features do + GG.Zombies.AddFeatureToZombieCountdown(features[i], ZOMBIES_REZ_SPEED, ZOMBIES_REZ_MIN, RezFrameCallback) end end @@ -305,10 +164,10 @@ function gadget:Initialize() return end if (spGetGameFrame() > 1) then - ReInit(true) + ReInit() end end function gadget:GameStart() - ReInit(true) -- anything it does doesnt mess with existing zombies -end + ReInit() -- anything it does doesnt mess with existing zombies +end \ No newline at end of file