Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 162 additions & 46 deletions LuaRules/Gadgets/api_zombie_helper.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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}
Expand All @@ -168,16 +191,109 @@ 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)
Comment thread
Stiofan-K marked this conversation as resolved.
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)

Comment on lines +205 to +208

@Stiofan-K Stiofan-K Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Starting revive time is also based on reclaim % left in the wreck instead of assuming it is at 100%

This is new, but shouldnt change anything for base zombies as they revive on Feature creation at 100% reclaim. Seem logical and inline for how it would work for a delayed revival application however.

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]

@Stiofan-K Stiofan-K Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should allow anyone to modify the base values to their liking afterwards

Unsure if there would be a better way to do things for this function outside of me assuming some base functionality/behavior as a wanted standard and then exposing the output for tweaks.

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
Comment on lines +231 to +244

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was a bit scary to touch, but I think is functionally identical.
We count the reclaim % remaining down from 100 instead of reclaim taken up from 0.

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
Comment thread
Stiofan-K marked this conversation as resolved.
resurrectingFeatures[featureID].rezFrameCallback(featureID)
else
local unitID = TurnFeatureIntoUnit(featureID,GaiaTeamID,true,nil)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure if this sort of default behavior is good practice.
Can't be slowed because unslowing isn't handled here but in the global rez gadget.

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()

mapWidth = Game.mapSizeX
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
Loading