diff --git a/LuaRules/Configs/UnitAttributeHandlers/AbilityDisabled.lua b/LuaRules/Configs/UnitAttributeHandlers/AbilityDisabled.lua new file mode 100644 index 0000000000..0a2c930c57 --- /dev/null +++ b/LuaRules/Configs/UnitAttributeHandlers/AbilityDisabled.lua @@ -0,0 +1,32 @@ +local spSetUnitRulesParam=Spring.SetUnitRulesParam + +return { + ---@type AttributesHandlerFactory + AbilityDisabled = { + handledAttributeNames={abilityDisabled=true}, + new = function(unitID, unitDefID) + local abilityDisabledCur = false + + return { + newDataHandler = function(frame) + local abilityDisabled = false + + return { + fold = function(data) + abilityDisabled = abilityDisabled or data.abilityDisabled + end, + apply = function() + if abilityDisabled ~= abilityDisabledCur then + spSetUnitRulesParam(unitID, "att_abilityDisabled", abilityDisabled and 1 or 0) + abilityDisabledCur = abilityDisabled + end + end + } + end, + clear = function() + -- Reset logic can be added here if needed + end + } + end + } +} \ No newline at end of file diff --git a/LuaRules/Configs/UnitAttributeHandlers/BuildSpeed.lua b/LuaRules/Configs/UnitAttributeHandlers/BuildSpeed.lua new file mode 100644 index 0000000000..1ae099f5f8 --- /dev/null +++ b/LuaRules/Configs/UnitAttributeHandlers/BuildSpeed.lua @@ -0,0 +1,50 @@ +local REPAIR_ENERGY_COST_FACTOR = Game.repairEnergyCostFactor +local spSetUnitBuildSpeed=Spring.SetUnitBuildSpeed +local spSetUnitRulesParam=Spring.SetUnitRulesParam +local INLOS_ACCESS = {inlos = true} + +GG.attRaw_BuildSpeed={} +return { + ---@type AttributesHandlerFactory + BuildSpeed = { + handledAttributeNames={ + build=true + }, + new = function(unitID, unitDefID) + local ud = UnitDefs[unitDefID] + local buildSpeed = ud.buildSpeed or 0 + + local buildMultCur = 1 + + return { + newDataHandler = function(frame) + local buildMult = 1 + + return { + fold = function(data) + buildMult = buildMult * (data.build or 1) + end, + apply = function() + GG.attRaw_BuildSpeed[unitID] = buildSpeed*buildMult + spSetUnitRulesParam(unitID, "totalBuildPowerChange", buildMult, INLOS_ACCESS) + if buildMult ~= buildMultCur and buildSpeed > 0 then + local newBuildSpeed = buildSpeed * buildMult + spSetUnitBuildSpeed(unitID, + newBuildSpeed, -- build + newBuildSpeed / REPAIR_ENERGY_COST_FACTOR, -- repair + newBuildSpeed, -- reclaim + 0.5 * newBuildSpeed -- rezz + ) + buildMultCur = buildMult + end + end + } + end, + clear = function() + GG.attRaw_BuildSpeed[unitID] = nil + -- Reset logic can be added here if needed + end + } + end + } +} \ No newline at end of file diff --git a/LuaRules/Configs/UnitAttributeHandlers/DeathExplosion.lua b/LuaRules/Configs/UnitAttributeHandlers/DeathExplosion.lua new file mode 100644 index 0000000000..b375493697 --- /dev/null +++ b/LuaRules/Configs/UnitAttributeHandlers/DeathExplosion.lua @@ -0,0 +1,31 @@ +local spSetUnitRulesParam=Spring.SetUnitRulesParam +local INLOS_ACCESS = {inlos = true} + +GG.att_DeathExplodeMult={} +return { + ---@type AttributesHandlerFactory + DeathExplosion = { + handledAttributeNames={deathExplode=true}, + new = function(unitId) + return { + newDataHandler = function() + local deathExplodeMult = 1 + + return { + fold = function(data) + deathExplodeMult = deathExplodeMult * (data.deathExplode or 1) + + end, + apply = function() + GG.att_DeathExplodeMult[unitId] = deathExplodeMult + spSetUnitRulesParam(unitId, "deathExplodeMult", deathExplodeMult, INLOS_ACCESS) + end + } + end, + clear = function() + GG.att_DeathExplodeMult[unitId] = nil + end + } + end + } +} \ No newline at end of file diff --git a/LuaRules/Configs/UnitAttributeHandlers/Economy.lua b/LuaRules/Configs/UnitAttributeHandlers/Economy.lua new file mode 100644 index 0000000000..c321a947c7 --- /dev/null +++ b/LuaRules/Configs/UnitAttributeHandlers/Economy.lua @@ -0,0 +1,43 @@ +local INLOS_ACCESS = {inlos = true} +local spSetUnitRulesParam=Spring.SetUnitRulesParam + +GG.att_EconomyChange={} +return { + ---@type AttributesHandlerFactory + Economy = { + handledAttributeNames={econ=true,energy=true}, + new = function(unitID, unitDefID) + local econMultCur = 1 + local energyMultCur = 1 + + return { + newDataHandler = function(frame) + local econMult = 1 + local energyMult = 1 + + return { + fold = function(data) + econMult = econMult * (data.econ or 1) + energyMult = energyMult * (data.energy or 1) + end, + apply = function() + GG.att_EconomyChange[unitID] = econMult + if econMult ~= econMultCur or energyMult ~= energyMultCur then + spSetUnitRulesParam(unitID, "totalEconomyChange", econMult, INLOS_ACCESS) + spSetUnitRulesParam(unitID, "metalGenerationFactor", econMult, INLOS_ACCESS) + spSetUnitRulesParam(unitID, "energyGenerationFactor", econMult * energyMult, INLOS_ACCESS) + + econMultCur = econMult + energyMultCur = energyMult + end + end + } + end, + clear = function() + GG.att_EconomyChange[unitID] = nil + -- Reset logic can be added here if needed + end + } + end + } +} \ No newline at end of file diff --git a/LuaRules/Configs/UnitAttributeHandlers/HealthCostMass.lua b/LuaRules/Configs/UnitAttributeHandlers/HealthCostMass.lua new file mode 100644 index 0000000000..b26e1dde1f --- /dev/null +++ b/LuaRules/Configs/UnitAttributeHandlers/HealthCostMass.lua @@ -0,0 +1,94 @@ +local INLOS_ACCESS = {inlos = true} + +local function GetMass(health, cost) + return (((cost/2) + (health/8))^0.6)*6.5 +end +local spGetUnitHealth=Spring.GetUnitHealth +local spSetUnitMaxHealth=Spring.SetUnitMaxHealth +local spSetUnitHealth=Spring.SetUnitHealth +local spSetUnitCosts=Spring.SetUnitCosts +local spSetUnitMass=Spring.SetUnitMass +local spSetUnitRulesParam=Spring.SetUnitRulesParam + +GG.att_CostMult={} +GG.att_HealthMult={} + +---@type {[string|number]:AttributesHandlerFactory} +return{ + ---@type AttributesHandlerFactory + HealthCostMass={ + handledAttributeNames={ + healthMult=true,healthAdd=true,cost=true,mass=true + }, + new=function (unitID,unitDefID) + local ud=UnitDefs[unitDefID] + local origUnitHealth= ud.health + local origUnitCost= ud.buildTime + local HealthMultCur=1 + local HealthAddCur=0 + local CostMultCur=1 + local MassMultCur=1 + ---@type AttributesHandler + return{ + newDataHandler=function (frame) + local healthMult=1 + local healthAdd=0 + local costMult=1 + local massMult=1 + + ---@type AttributesDataHandler + return { + ---@param data {healthAdd:number?,healthMult:number?,cost:number?,mass:number?} + fold=function (data) + healthMult=healthMult*(data.healthMult or 1) + healthAdd=healthAdd+(data.healthAdd or 0) + costMult=costMult*(data.cost or 1) + massMult=massMult*(data.mass or 1) + end, + apply=function () + GG.att_CostMult[unitID] = costMult + GG.att_HealthMult[unitID] = healthMult + spSetUnitRulesParam(unitID, "costMult", costMult, INLOS_ACCESS) + + if CostMultCur~=costMult or HealthAddCur~=healthAdd or MassMultCur~=massMult or HealthMultCur~=healthMult then + + local newMaxHealth = (origUnitHealth + healthAdd) * healthMult + local oldHealth, oldMaxHealth = spGetUnitHealth(unitID) + spSetUnitMaxHealth(unitID, newMaxHealth) + spSetUnitHealth(unitID, oldHealth * newMaxHealth / oldMaxHealth) + + local origCost = origUnitCost + local cost = origCost*costMult + spSetUnitCosts(unitID, { + metalCost = cost, + energyCost = cost, + buildTime = cost, + }) + + if massMult == 1 then + -- Default to update mass based on new stats, if a multiplier is not set. + local mass = GetMass(newMaxHealth, cost) + spSetUnitMass(unitID, mass) + spSetUnitRulesParam(unitID, "massOverride", mass, INLOS_ACCESS) + else + local mass = GetMass(origUnitHealth, origCost) * massMult + spSetUnitMass(unitID, mass) + spSetUnitRulesParam(unitID, "massOverride", mass, INLOS_ACCESS) + end + CostMultCur=costMult + HealthAddCur=healthAdd + MassMultCur=massMult + HealthMultCur=healthMult + end + end + } + end, + clear=function () + GG.att_CostMult[unitID] = nil + GG.att_HealthMult[unitID] = nil + + end + } + end + } +} \ No newline at end of file diff --git a/LuaRules/Configs/UnitAttributeHandlers/HealthRegen.lua b/LuaRules/Configs/UnitAttributeHandlers/HealthRegen.lua new file mode 100644 index 0000000000..68b2e89f1c --- /dev/null +++ b/LuaRules/Configs/UnitAttributeHandlers/HealthRegen.lua @@ -0,0 +1,32 @@ + +local INLOS_ACCESS = {inlos = true} + +GG.att_RegenChange={} +return { + ---@type AttributesHandlerFactory + StaticAttributes = { + handledAttributeNames = {healthRegen=true}, + new = function(unitID, unitDefID) + + return { + newDataHandler = function(frame) + + local healthRegen = 1 + + return { + fold = function(data) + healthRegen = healthRegen*(data.healthRegen or 1) + end, + apply = function() + GG.att_RegenChange[unitID] = healthRegen + end + } + end, + clear = function() + GG.att_RegenChange[unitID] = nil + -- Reset logic can be added here if needed + end + } + end + } +} \ No newline at end of file diff --git a/LuaRules/Configs/UnitAttributeHandlers/JumpRange.lua b/LuaRules/Configs/UnitAttributeHandlers/JumpRange.lua new file mode 100644 index 0000000000..e2aece68d5 --- /dev/null +++ b/LuaRules/Configs/UnitAttributeHandlers/JumpRange.lua @@ -0,0 +1,33 @@ +local spSetUnitRulesParam=Spring.SetUnitRulesParam +local INLOS_ACCESS = {inlos = true} + +GG.att_JumpRangeChange={} +return { + ---@type AttributesHandlerFactory + JumpRange = { + handledAttributeNames={ + build=true + }, + new = function(unitID, unitDefID) + return { + newDataHandler = function(frame) + local jumpRangeMult = 1 + + return { + fold = function(data) + jumpRangeMult = jumpRangeMult * (data.jumpRange or 1) + end, + apply = function() + GG.att_JumpRangeChange[unitID] = jumpRangeMult + spSetUnitRulesParam(unitID, "jumpRangeMult", jumpRangeMult, INLOS_ACCESS) + end + } + end, + clear = function() + GG.att_JumpRangeChange[unitID] = nil + -- Reset logic can be added here if needed + end + } + end + } +} \ No newline at end of file diff --git a/LuaRules/Configs/UnitAttributeHandlers/MovementSpeed.lua b/LuaRules/Configs/UnitAttributeHandlers/MovementSpeed.lua new file mode 100644 index 0000000000..b2b6516d86 --- /dev/null +++ b/LuaRules/Configs/UnitAttributeHandlers/MovementSpeed.lua @@ -0,0 +1,143 @@ +local WACKY_CONVERSION_FACTOR_1 = 2184.53 + +local workingGroundMoveType = true -- not ((Spring.GetModOptions() and (Spring.GetModOptions().pathfinder == "classic") and true) or false) + +local getMovetype = Spring.Utilities.getMovetype + +local spMoveCtrlGetTag=Spring.MoveCtrl.GetTag + +local spSetAirMoveTypeData=Spring.MoveCtrl.SetAirMoveTypeData +local spSetGunshipMoveTypeData=Spring.MoveCtrl.SetGunshipMoveTypeData +local spSetGroundMoveTypeData=Spring.MoveCtrl.SetGroundMoveTypeData + +local spSetUnitCOBValue = Spring.SetUnitCOBValue + +local math_max=math.max + +local spGetUnitPosition=Spring.GetUnitPosition +local spGetGroundHeight=Spring.GetGroundHeight +local spSetUnitVelocity=Spring.SetUnitVelocity + +local spSetUnitRulesParam=Spring.SetUnitRulesParam +local INLOS_ACCESS = {inlos = true} + +GG.att_MoveChange={} + +return { + ---@type AttributesHandlerFactory + MovementSpeed = { + handledAttributeNames={ + move=true,turn=true,accel=true + }, + new = function(unitID, unitDefID) + local ud = UnitDefs[unitDefID] + local moveData = Spring.GetUnitMoveTypeData(unitID) + + local origSpeed = ud.speed + local origReverseSpeed = (moveData.name == "ground") and moveData.maxReverseSpeed or ud.speed + local origTurnRate = ud.turnRate + local origTurnAccel = (ud.turnRate or 1) * (ud.customParams.turn_accel_factor or 1) + local origMaxRudder = ud.maxRudder + local origMaxAcc = ud.maxAcc + local origMaxDec = ud.maxDec + local movetype = getMovetype(ud) + + local speedFactorCur = 1 + local turnAccelFactorCur = 1 + local maxAccelerationFactorCur = 1 + + return { + newDataHandler = function(frame) + local speedFactor = 1 + local turnAccelFactor = 1 + local maxAccelerationFactor = 1 + + return { + fold = function(data) + speedFactor = speedFactor * (data.move or 1) + turnAccelFactor = turnAccelFactor * (data.turn or data.move or 1) + maxAccelerationFactor = maxAccelerationFactor * (data.accel or data.move or 1) + end, + apply = function() + + GG.att_MoveChange[unitID] = speedFactor + spSetUnitRulesParam(unitID, "totalMoveSpeedChange", speedFactor, INLOS_ACCESS) + if speedFactor ~= speedFactorCur or turnAccelFactor ~= turnAccelFactorCur or maxAccelerationFactor ~= maxAccelerationFactorCur then + if spMoveCtrlGetTag(unitID) ~= nil then + return + end + + local decFactor = maxAccelerationFactor + local isSlowed = (speedFactor < 1) + if isSlowed then + decFactor = 1000 + end + speedFactor = math_max(speedFactor, 0) + turnAccelFactor = math_max(turnAccelFactor, 0) + local turnFactor = math_max(turnAccelFactor, 0.001) + maxAccelerationFactor = math_max(maxAccelerationFactor, 0.001) + + if speedFactor == 0 then + local x, y, z = spGetUnitPosition(unitID) + if x then + local h = spGetGroundHeight(x, z) + if h and h >= y then + spSetUnitVelocity(unitID, 0, 0, 0) + end + end + end + + if movetype == 0 then -- Air + turnFactor = (speedFactor > 0) and (turnFactor / speedFactor) or 1 + local attribute={ + maxSpeed = origSpeed * speedFactor, + maxAcc = origMaxAcc * maxAccelerationFactor, + maxRudder = origMaxRudder * turnFactor, + } + spSetAirMoveTypeData(unitID, attribute) + spSetAirMoveTypeData(unitID, attribute) + elseif movetype == 1 then -- Gunship + spSetGunshipMoveTypeData(unitID, { + maxSpeed = origSpeed * speedFactor, + turnRate = origTurnRate * turnFactor, + accRate = origMaxAcc * maxAccelerationFactor, + decRate = origMaxDec * maxAccelerationFactor, + }) + GG.ForceUpdateWantedMaxSpeed(unitID, unitDefID) + elseif movetype == 2 then -- Ground + if workingGroundMoveType then + local accRate = origMaxAcc * maxAccelerationFactor + if isSlowed and accRate > speedFactor then + accRate = speedFactor + end + spSetGroundMoveTypeData(unitID, { + maxSpeed = origSpeed * speedFactor, + maxReverseSpeed = (isSlowed and 0) or origReverseSpeed, + turnRate = origTurnRate * turnFactor, + accRate = accRate, + decRate = origMaxDec * decFactor, + turnAccel = origTurnAccel * turnAccelFactor, + }) + GG.ForceUpdateWantedMaxSpeed(unitID, unitDefID) + + else + spSetUnitCOBValue(unitID, COB.MAX_SPEED, math.ceil(origSpeed*speedFactor*WACKY_CONVERSION_FACTOR_1)) + + end + end + + speedFactorCur = speedFactor + turnAccelFactorCur = turnAccelFactor + maxAccelerationFactorCur = maxAccelerationFactor + end + end + } + end, + clear = function() + GG.att_MoveChange[unitID] = nil + -- Reset logic can be added here if needed + end + } + end + } +} \ No newline at end of file diff --git a/LuaRules/Configs/UnitAttributeHandlers/SensorAndJamm.lua b/LuaRules/Configs/UnitAttributeHandlers/SensorAndJamm.lua new file mode 100644 index 0000000000..3e18ef5266 --- /dev/null +++ b/LuaRules/Configs/UnitAttributeHandlers/SensorAndJamm.lua @@ -0,0 +1,103 @@ + +local spSetUnitRulesParam=Spring.SetUnitRulesParam +local INLOS_ACCESS = {inlos = true} +local spSetUnitSensorRadius=Spring.SetUnitSensorRadius + +local function tobool(val) + local t = type(val) + if (t == 'nil') then + return false + elseif (t == 'boolean') then + return val + elseif (t == 'number') then + return (val ~= 0) + elseif (t == 'string') then + return ((val ~= '0') and (val ~= 'false')) + end + return false +end + +---@type {[string|number]:AttributesHandlerFactory} +return{ + ---@type AttributesHandlerFactory + SensorAndJamm={ + handledAttributeNames={ + setRadar=true,setSonar=true,setJammer=true,setSight=true,sense=true,abilityDisabled=true + }, + new=function (unitID,unitDefID) + local ud=UnitDefs[unitDefID] + local udSightDistance= ud.sightDistance + local udRadarDistance= ud.radarDistance + local udSonarDistance = ud.sonarDistance + local udSonarCanDisable = tobool(ud.customParams.sonar_can_be_disabled) + local udRadarDistanceJam = ud.radarDistanceJam + + local senseMultCur = 1 + local setRadarCur = false + local setSonarCur = false + local setJammerCur = false + local setSightCur = false + + local abilityDisabledCur=false + ---@type AttributesHandler + return{ + newDataHandler=function (frame) + local senseMult = 1 + ---@type nil|number + local radarOverride = nil + ---@type nil|number + local sonarOverride = nil + ---@type nil|number + local jammerOverride = nil + ---@type nil|number + local sightOverride = nil + ---@type nil|boolean + local abilityDisabled=nil + + ---@type AttributesDataHandler + return { + ---@param data {abilityDisabled:boolean,sense:number?,setRadar:number?,setJammer:number?,setSonar:number?,setSight:number?} + fold=function (data) + senseMult=senseMult*(data.sense or 1) + radarOverride=radarOverride or data.setRadar + sonarOverride=sonarOverride or data.setSonar + jammerOverride=jammerOverride or data.setJammer + sightOverride=sightOverride or data.setSight + abilityDisabled=abilityDisabled or data.abilityDisabled + + end, + apply=function () + spSetUnitRulesParam(unitID, "senseMult", senseMult, INLOS_ACCESS) + if senseMult~=senseMultCur or radarOverride~=setRadarCur or sonarOverride~=setSonarCur or jammerOverride~=setJammerCur or sightOverride~=setSightCur or abilityDisabledCur~=abilityDisabled then + + local abilityMult=(not abilityDisabled) and 1 or 0 + + if radarOverride or udRadarDistance>0 then + spSetUnitSensorRadius(unitID, "radar", abilityMult*(radarOverride or udRadarDistance)*senseMult) + end + + if sonarOverride or udSonarDistance then + local sonarAbilityMult=1 + if udSonarCanDisable and abilityDisabled then + sonarAbilityMult=0 + end + --sonarCanDisable and abilityMult or 1 + --there will be a day for humanity to be cooked by this + spSetUnitSensorRadius(unitID, "sonar", (sonarAbilityMult)*(sonarOverride or udSonarDistance)*senseMult) + end + if jammerOverride or udRadarDistanceJam then + spSetUnitSensorRadius(unitID, "radarJammer", abilityMult*(jammerOverride or udRadarDistanceJam)*senseMult) + end + spSetUnitSensorRadius(unitID, "los", (sightOverride or udSightDistance)*senseMult) + spSetUnitSensorRadius(unitID, "airLos", (sightOverride or udSightDistance)*senseMult) + end + end + } + end, + clear=function () + + end + } + end + } +} \ No newline at end of file diff --git a/LuaRules/Configs/UnitAttributeHandlers/Shield.lua b/LuaRules/Configs/UnitAttributeHandlers/Shield.lua new file mode 100644 index 0000000000..e3c2df5dea --- /dev/null +++ b/LuaRules/Configs/UnitAttributeHandlers/Shield.lua @@ -0,0 +1,52 @@ + +local spSetUnitRulesParam = Spring.SetUnitRulesParam +local spSetUnitShieldState=Spring.SetUnitShieldState +local spGetUnitRulesParam=Spring.GetUnitRulesParam +GG.att_ShieldRegenChange={} +GG.att_ShieldMaxMult={} +return { + ---@type AttributesHandlerFactory + Shield = { + handledAttributeNames={ + shieldDisabled=true,shieldRegen=true,shieldMax=true + }, + new = function(unitID, unitDefID) + local shieldDisabledCur = false + + return { + newDataHandler = function(frame) + local shieldDisabled = false + local shieldRegen = 1 + local shieldMaxMult = 1 + + return { + fold = function(data) + shieldDisabled = shieldDisabled or data.shieldDisabled + shieldRegen = shieldRegen*(data.shieldRegen or 1) + shieldMaxMult = shieldMaxMult*(data.shieldMax or 1) + end, + apply = function() + GG.att_ShieldRegenChange[unitID] = shieldRegen + GG.att_ShieldMaxMult[unitID] = shieldMaxMult + if shieldDisabled ~= shieldDisabledCur then + spSetUnitRulesParam(unitID, "att_shieldDisabled", shieldDisabled and 1 or 0) + if shieldDisabled then + spSetUnitShieldState(unitID, -1, 0) + end + if spGetUnitRulesParam(unitID, "comm_shield_max") ~= 0 then + local shieldNum = Spring.GetUnitRulesParam(unitID, "comm_shield_num")--[[@as number]] or -1 + spSetUnitShieldState(unitID, shieldNum, not shieldDisabled) + end + shieldDisabledCur = shieldDisabled + end + end + } + end, + clear = function() + GG.att_ShieldRegenChange[unitID] = nil + GG.att_ShieldMaxMult[unitID] = nil + end + } + end + } +} \ No newline at end of file diff --git a/LuaRules/Configs/UnitAttributeHandlers/StaticAttributes.lua b/LuaRules/Configs/UnitAttributeHandlers/StaticAttributes.lua new file mode 100644 index 0000000000..f9338783eb --- /dev/null +++ b/LuaRules/Configs/UnitAttributeHandlers/StaticAttributes.lua @@ -0,0 +1,53 @@ + +local INLOS_ACCESS = {inlos = true} +local spSetUnitRulesParam=Spring.SetUnitRulesParam +GG.att_StaticBuildRateMult={} +return { + + ---@type AttributesHandlerFactory + StaticAttributes = { + handledAttributeNames={ + build=true,econ=true,energy=true,shieldRegen=true,healthRegen=true + }, + new = function(unitID, unitDefID) + -- local staticBuildpowerMultCur = 1 + + return { + newDataHandler = function(frame) + local staticBuildpowerMult = 1 + local staticMetalMult = 1 + local staticEnergyMult = 1 + local staticShieldRegen = 1 + local staticHealthRegen = 1 + local staticMoveMult = 1 + + return { + fold = function(data) + if data.static then + staticBuildpowerMult = staticBuildpowerMult * (data.build or 1) + staticMetalMult = staticMetalMult * (data.econ or 1) + staticEnergyMult = staticEnergyMult * (data.econ or 1) * (data.energy or 1) + staticShieldRegen = staticShieldRegen * (data.shieldRegen or 1) + staticHealthRegen = staticHealthRegen * (data.healthRegen or 1) + staticMoveMult = staticMoveMult * (data.move or 1) + end + end, + apply = function() + GG.att_StaticBuildRateMult[unitID] = staticBuildpowerMult + spSetUnitRulesParam(unitID, "totalStaticBuildpowerMult", staticBuildpowerMult, INLOS_ACCESS) + spSetUnitRulesParam(unitID, "totalStaticMetalMult", staticMetalMult, INLOS_ACCESS) + spSetUnitRulesParam(unitID, "totalStaticEnergyMult", staticEnergyMult, INLOS_ACCESS) + spSetUnitRulesParam(unitID, "totalStaticShieldRegen", staticShieldRegen, INLOS_ACCESS) + spSetUnitRulesParam(unitID, "totalStaticHealthRegen", staticHealthRegen, INLOS_ACCESS) + spSetUnitRulesParam(unitID, "totalStaticMoveSpeedChange", staticMoveMult, INLOS_ACCESS) + end + } + end, + clear = function() + GG.att_StaticBuildRateMult[unitID] = nil + -- Reset logic can be added here if needed + end + } + end + } +} \ No newline at end of file diff --git a/LuaRules/Configs/UnitAttributeHandlers/Weapons.lua b/LuaRules/Configs/UnitAttributeHandlers/Weapons.lua new file mode 100644 index 0000000000..526dda1efb --- /dev/null +++ b/LuaRules/Configs/UnitAttributeHandlers/Weapons.lua @@ -0,0 +1,313 @@ +local floor=math.floor +local spGetUnitWeaponState=Spring.GetUnitWeaponState +local spSetUnitWeaponState=Spring.SetUnitWeaponState +local spSetUnitWeaponDamages=Spring.SetUnitWeaponDamages +local INLOS_ACCESS = {inlos = true} +local spSetUnitRulesParam=Spring.SetUnitRulesParam + +local HALF_FRAME = 1 / (2 * Game.gameSpeed) + +local function defaultWeaponSpecificMod() + return { + reloadMult = 1, + rangeMult = 1, + projSpeedMult = 1, + projectilesMult = 1, + burstMult=1, + burstRateMult=1, + sprayAngleAdd=0, + damageMult=1, + } +end + +local origUnitWeapons={} + +local UnitReloadPause=GG.UnitReloadPause + + +local projectileSpeedLock = {} +local rangeUpdater = {} + + +local function ApplyWeaponMods(unitId,state,weaponMods,gameFrame,minSpray) + local rangeUpdateRequired=true -- dk how to + local weaponModDef=weaponMods.def + local + reloadSpeedFactor, rangeFactor, projSpeedFactor, projectilesFactor,burstFactor,burstRateFactor,sprayAngleAdd,damageFactor= + weaponModDef.reloadMult, + weaponModDef.rangeMult, + weaponModDef.projSpeedMult, + weaponModDef.projectilesMult, + weaponModDef.burstMult, + weaponModDef.burstRateMult, + weaponModDef.sprayAngleAdd, + weaponModDef.damageMult + + + if damageFactor ~= 1 and not GG.ATT_ENABLE_DAMAGE then + Spring.Utilities.UnitEcho(unitId, "damage attribute requires GG.ATT_ENABLE_DAMAGE") + end + local maxRangeModified = state.maxWeaponRange*rangeFactor + + for wpnNum = 1, state.weaponCount do + local w = state.weapon[wpnNum] + local reloadState = spGetUnitWeaponState(unitId, wpnNum , 'reloadState') + local reloadTime = spGetUnitWeaponState(unitId, wpnNum , 'reloadTime') + local wmod=weaponMods[wpnNum] + + + local moddedReloadSpeedFactor = reloadSpeedFactor + + + local moddedRange = w.range*rangeFactor + local moddedProjectiles = w.projectiles*projectilesFactor + + local moddedSprayAngle = w.sprayAngle+sprayAngleAdd + local moddedBurst=w.burst and w.burst*burstFactor + + local moddedBurstRateFactor=burstRateFactor + + local moddedProjSpeedFactor=projSpeedFactor + + if wmod then + moddedReloadSpeedFactor = moddedReloadSpeedFactor * (wmod.reloadMult or 1) + + moddedRange = moddedRange * (wmod.rangeMult or 1) + + moddedProjectiles = moddedProjectiles*(wmod.projectilesMult or 1) + + moddedSprayAngle = moddedSprayAngle+(wmod.sprayAngleAdd or 0) + + moddedBurst=moddedBurst and moddedBurst * (wmod.burstMult or 1) + moddedBurstRateFactor = moddedBurstRateFactor * ( wmod.burstRateMult or 1 ) + moddedProjSpeedFactor = moddedProjSpeedFactor * (wmod.projSpeedMult) + end + + moddedBurstRateFactor=moddedBurstRateFactor / moddedReloadSpeedFactor + + local moddedBurstRate = w.burstRate and w.burstRate * moddedBurstRateFactor + + moddedSprayAngle = math.max(moddedSprayAngle, minSpray) + + if moddedBurstRate then + spSetUnitWeaponState(unitId,wpnNum,"burstRate",moddedBurstRate + HALF_FRAME) + end + + if moddedReloadSpeedFactor <= 0 then + UnitReloadPause.UnitReloadPause(unitId,wpnNum,reloadState,reloadTime,gameFrame) + else + UnitReloadPause.UnitReloadUnpause(unitId,wpnNum) + local newReload = w.reload/moddedReloadSpeedFactor + local nextReload = gameFrame+(reloadState-gameFrame)*newReload/reloadTime + -- Add HALF_FRAME to round reloadTime to the closest discrete frame (multiple of 1/30), since the the engine rounds DOWN + if moddedBurstRate then + spSetUnitWeaponState(unitId, wpnNum, {reloadTime = newReload + HALF_FRAME, reloadState = nextReload + 0.5, burstRate = moddedBurstRate + HALF_FRAME}) + else + spSetUnitWeaponState(unitId, wpnNum, {reloadTime = newReload + HALF_FRAME, reloadState = nextReload + 0.5}) + end + end + + local sprayAngle = math.max(w.sprayAngle, minSpray) + spSetUnitWeaponState(unitId, wpnNum, "sprayAngle", sprayAngle) + + spSetUnitWeaponState(unitId, wpnNum, "projectiles", moddedProjectiles) + + if moddedBurst then + spSetUnitWeaponState(unitId,wpnNum,"burst",moddedBurst) + end + + + if rangeUpdateRequired then + if w.projectileSpeed and not projectileSpeedLock[unitId] then + -- Changing projectile speed without subsequently setting range causes some weapons to go to zero range. Eg Scorcher + local moddedProjSpeed = w.projectileSpeed*moddedProjSpeedFactor + spSetUnitWeaponState(unitId, wpnNum, "projectileSpeed", moddedProjSpeed) + end + if not rangeUpdater[unitId] then + spSetUnitWeaponState(unitId, wpnNum, "range", moddedRange) + spSetUnitWeaponDamages(unitId, wpnNum, "dynDamageRange", moddedRange) + if maxRangeModified < moddedRange then + maxRangeModified = moddedRange + end + end + end + if GG.ATT_ENABLE_DAMAGE then + local did = 0 + local data = state.weapon[wpnNum].damages + local toSet = {} + while data[did] do + toSet[did] = data[did] * damageFactor + did = did + 1 + end + spSetUnitWeaponDamages(unitId, wpnNum, toSet) + end + end + if rangeUpdateRequired then + if rangeUpdater[unitId] and rangeUpdater[unitId] ~= true then + local mods = {} + for i = 1, state.weaponCount do + mods[i] = weaponMods and weaponMods[i] and weaponMods[i].rangeMult or 1 + end + rangeUpdater[unitId](rangeFactor, mods) + else + Spring.SetUnitMaxRange(unitId, maxRangeModified) + end + end +end + +local function LoadState(unitDefID) + local ud = UnitDefs[unitDefID] + local state = { + weapon = {}, + weaponCount = #ud.weapons, + maxWeaponRange = ud.maxWeaponRange, + } + + origUnitWeapons[unitDefID] = state + + for i = 1, state.weaponCount do + local wd = WeaponDefs[ud.weapons[i].weaponDef] + local reload = wd.reload + state.weapon[i] = { + reload = reload, + projectiles = wd.projectiles or 1, + oldReloadFrames = floor(reload*Game.gameSpeed), + range = wd.range, + sprayAngle = wd.sprayAngle or 0, + burst=wd.salvoSize or 1, + burstRate = (wd.salvoDelay or (1/30)), + damages = GG.ATT_ENABLE_DAMAGE and {}, + } + if GG.ATT_ENABLE_DAMAGE then + local armorType = 0 + local data = state.weapon[i].damages + while wd.damages[armorType] do + data[armorType] = wd.damages[armorType] + armorType = armorType + 1 + end + end + if wd.type == "LaserCannon" or wd.type == "Cannon" then + -- Barely works for missiles, and might break their burnblow and prediction + state.weapon[i].projectileSpeed = wd.projectilespeed + end + if wd.type == "BeamLaser" then + -- beamlasers go screwy if you mess with their burst length + state.weapon[i].burstRate = false + state.weapon[i].burst=false + end + end + return state +end + +local function list_to_set(list,value) + if value==nil then + value=true + end + local set={} + for _, k in pairs(list) do + set[k]=value + end + return set +end + + + +GG.att_ProjSpeed = {} +GG.att_ProjMult = {} +GG.att_DamageMult = {} +GG.att_ReloadChange = {} +GG.att_RangeChange={} +return{ + ---@type AttributesHandlerFactory + Weapons={ + handledAttributeNames=list_to_set({ + "weaponNum", + "reload", + "range", + "projSpeed", + "projectiles", + "damage", + "burst", + "burstRate", + "sprayAngle", + "minSpray", + }), + new=function (unitID, unitDefID) + local state = origUnitWeapons[unitDefID] + + if not state then + state=LoadState(unitDefID) + end + + ---@type AttributesHandler + return{ + newDataHandler=function (gameFrame) + + local weaponMods = { + def=defaultWeaponSpecificMod() + } + local minSpray=0 + ---@type AttributesDataHandler + return{ + fold=function (data) + local wepNum=data.weaponNum or "def" + local wepData=weaponMods[wepNum] + if wepData==nil then + wepData=defaultWeaponSpecificMod() + weaponMods[wepNum]=wepData + end + wepData.reloadMult = wepData.reloadMult*(data.reload or 1) + wepData.rangeMult = wepData.rangeMult*(data.range or 1) + wepData.projSpeedMult = wepData.projSpeedMult*(data.projSpeed or 1) + wepData.projectilesMult = wepData.projectilesMult*(data.projectiles or 1) + wepData.damageMult=wepData.damageMult*(data.damage or 1) + + wepData.burstMult=wepData.burstMult*(data.burst or 1) + wepData.burstRateMult=wepData.burstRateMult*(data.burstRate or 1) + wepData.sprayAngleAdd=wepData.sprayAngleAdd+(data.sprayAngle or 0) + minSpray = math.max(minSpray, data.minSpray or 0) + + end, + apply=function () + local wmdef=weaponMods.def + spSetUnitRulesParam(unitID, "projectilesMult", wmdef.projectilesMult, INLOS_ACCESS) + spSetUnitRulesParam(unitID, "projectileSpeedMult", wmdef.projSpeedMult, INLOS_ACCESS) + spSetUnitRulesParam(unitID, "damageMult", wmdef.damageMult, INLOS_ACCESS) + spSetUnitRulesParam(unitID, "rangeMult", wmdef.rangeMult, INLOS_ACCESS) + spSetUnitRulesParam(unitID, "totalReloadSpeedChange", wmdef.reloadMult, INLOS_ACCESS) + GG.att_ProjSpeed[unitID] = wmdef.projSpeedMult -- Ignores weapon mods + GG.att_ProjMult[unitID] = wmdef.projectilesMult + GG.att_DamageMult[unitID] = wmdef.damageMult + GG.att_ReloadChange[unitID] = wmdef.reloadMult + GG.att_RangeChange[unitID]=wmdef.rangeMult + ApplyWeaponMods(unitID,state,weaponMods,gameFrame,minSpray + + ) + + end + } + end, + clear=function () + projectileSpeedLock[unitID]=nil + rangeUpdater[unitID] = nil + GG.att_ProjSpeed[unitID] = nil -- Ignores weapon mods + GG.att_ProjMult[unitID] = nil + GG.att_DamageMult[unitID] = nil + end + } + end, + + initialize=function () + UnitReloadPause=UnitReloadPause or GG.UnitReloadPause + local Attributes=GG.Attributes + + function Attributes.SetProjectileSpeedLock(unitID, lockState) + projectileSpeedLock[unitID] = lockState + end + + function Attributes.SetRangeUpdater(unitID, updateFunc) + rangeUpdater[unitID] = updateFunc + end + end, + } +} \ No newline at end of file diff --git a/LuaRules/Configs/unit_attributes_generic_handlers.lua b/LuaRules/Configs/unit_attributes_generic_handlers.lua new file mode 100644 index 0000000000..7b6103066f --- /dev/null +++ b/LuaRules/Configs/unit_attributes_generic_handlers.lua @@ -0,0 +1,47 @@ +local Attributes=GG.Attributes +if not Attributes then + Attributes={} + GG.Attributes=Attributes +end + + +---@class AttributesDataHandler +---@field fold fun(domainData:table) +---@field apply fun() + +---@class AttributesHandler +---@field newDataHandler fun(frame:number):AttributesDataHandler +---@field clear fun() + +---@class AttributesHandlerFactory +---@field new fun(unitID:UnitId,unitDefID:UnitDefId):AttributesHandler +---@field initialize nil|fun() +---@field handledAttributeNames table + +---@type list +local HandlersFactory=Attributes.HandlersFactory +if not HandlersFactory then + HandlersFactory={} + local HandlersFile=VFS.DirList("LuaRules/Configs/UnitAttributeHandlers", "*.lua") or {} + for i = 1, #HandlersFile do + + Spring.Echo("unit_attributes_generic_handlers.lua: including " .. HandlersFile[i]) + + ---@type {[string|number]:AttributesHandlerFactory} + local HandlersDefs = VFS.Include(HandlersFile[i]) + if not HandlersDefs then + Spring.Echo("UnitAttributeHandlers file "..HandlersFile[i].." return nil") + else + for key, value in pairs(HandlersDefs) do + if type(key)=="number" then + HandlersFactory[#HandlersFactory+1]=value + else + HandlersFactory[key]=value + end + end + end + end + Attributes.HandlersFactory=HandlersFactory +end + +return HandlersFactory diff --git a/LuaRules/Gadgets/unit_attributes_generic.lua b/LuaRules/Gadgets/unit_attributes_generic.lua index 2a9d2af63a..4941982224 100644 --- a/LuaRules/Gadgets/unit_attributes_generic.lua +++ b/LuaRules/Gadgets/unit_attributes_generic.lua @@ -9,1013 +9,137 @@ function gadget:GetInfo() return { name = "Attributes Generic", desc = "Handles UnitRulesParam attributes in a generic way.", - author = "GoogleFrog", -- v1 CarReparier & GoogleFrog - date = "2018-11-30", -- v1 2009-11-27 + author = "XNTEABDSC", -- v2 GoogleFrog, v1 CarReparier & GoogleFrog + date = "2025", -- v2 2018-11-30 v1 2009-11-27 license = "GNU GPL, v2 or later", layer = -1, enabled = true, } end --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - -local UPDATE_PERIOD = 3 - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- -local floor = math.floor -local spValidUnitID = Spring.ValidUnitID local spGetUnitDefID = Spring.GetUnitDefID -local spGetGameFrame = Spring.GetGameFrame -local spSetUnitRulesParam = Spring.SetUnitRulesParam -local spGetUnitRulesParam = Spring.GetUnitRulesParam - -local spSetUnitBuildSpeed = Spring.SetUnitBuildSpeed -local spSetUnitWeaponState = Spring.SetUnitWeaponState -local spGetUnitWeaponState = Spring.GetUnitWeaponState -local spSetUnitWeaponDamages = Spring.SetUnitWeaponDamages - -local spGetUnitMoveTypeData = Spring.GetUnitMoveTypeData -local spMoveCtrlGetTag = Spring.MoveCtrl.GetTag -local spSetAirMoveTypeData = Spring.MoveCtrl.SetAirMoveTypeData -local spSetGunshipMoveTypeData = Spring.MoveCtrl.SetGunshipMoveTypeData -local spSetGroundMoveTypeData = Spring.MoveCtrl.SetGroundMoveTypeData - -local spSetUnitCOBValue = Spring.SetUnitCOBValue - -local ALLY_ACCESS = {allied = true} local INLOS_ACCESS = {inlos = true} -local WACKY_CONVERSION_FACTOR_1 = 2184.53 - -local getMovetype = Spring.Utilities.getMovetype -local Vector = Spring.Utilities.Vector - -local IterableMap = VFS.Include("LuaRules/Gadgets/Include/IterableMap.lua") - -local HALF_FRAME = 1 / (2 * Game.gameSpeed) - -local workingGroundMoveType = true -- not ((Spring.GetModOptions() and (Spring.GetModOptions().pathfinder == "classic") and true) or false) - -local function GetMass(health, cost) - return (((cost/2) + (health/8))^0.6)*6.5 -end - -local projectileSpeedLock = {} -local rangeUpdater = {} - -GG.att_LastChangeFrame = {} -GG.att_CostMult = {} -GG.att_HealthMult = {} -GG.att_EconomyChange = {} -GG.att_ReloadChange = {} -GG.att_MoveChange = {} -GG.att_RangeChange = {} -GG.att_JumpRangeChange = {} -GG.att_DeathExplodeMult = {} -GG.att_ProjSpeed = {} -GG.att_ProjMult = {} -GG.att_DamageMult = {} -GG.att_RegenChange = {} -GG.att_ShieldRegenChange = {} -GG.att_ShieldMaxMult = {} -GG.att_StaticBuildRateMult = {} -GG.attRaw_BuildSpeed = {} -- A build speed value rather than a multiplier - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- --- UnitDefs caching - -local shieldWeaponDef = {} -local buildSpeedDef = {} - -for i = 1, #UnitDefs do - local ud = UnitDefs[i] - if ud.shieldWeaponDef then - shieldWeaponDef[i] = true - end - if (ud.buildSpeed or 0) ~= 0 then - buildSpeedDef[i] = ud.buildSpeed - end -end - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- --- Sensor Handling - -local origUnitSight = {} -local radarUnitDef = {} -local sonarUnitDef = {} -local jammerUnitDef = {} - -local function UpdateSensorAndJamm(unitID, unitDefID, multiplier, enabled, radarOverride, sonarOverride, jammerOverride, sightOverride) - if not origUnitSight[unitDefID] then - local ud = UnitDefs[unitDefID] - origUnitSight[unitDefID] = ud.sightDistance - if ud.radarDistance > 0 then - radarUnitDef[unitDefID] = ud.radarDistance - end - if ud.sonarDistance > 0 and tobool(ud.customParams.sonar_can_be_disabled) then - sonarUnitDef[unitDefID] = ud.sonarDistance - end - if ud.radarDistanceJam > 0 then - jammerUnitDef[unitDefID] = ud.radarDistanceJam - end - end - local orig = origUnitSight[unitDefID] - local abilityMult = (enabled and 1) or 0 - - if radarUnitDef[unitDefID] or radarOverride then - Spring.SetUnitSensorRadius(unitID, "radar", abilityMult*(radarOverride or radarUnitDef[unitDefID])*multiplier) - end - if sonarUnitDef[unitDefID] or sonarOverride then - Spring.SetUnitSensorRadius(unitID, "sonar", abilityMult*(sonarOverride or sonarUnitDef[unitDefID])*multiplier) - end - if jammerUnitDef[unitDefID] or jammerOverride then - Spring.SetUnitSensorRadius(unitID, "radarJammer", abilityMult*(jammerOverride or jammerUnitDef[unitDefID])*multiplier) - end - Spring.SetUnitSensorRadius(unitID, "los", (sightOverride or origUnitSight[unitDefID])*multiplier) - Spring.SetUnitSensorRadius(unitID, "airLos", (sightOverride or origUnitSight[unitDefID])*multiplier) -end - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- --- Build Speed Handling - -local REPAIR_ENERGY_COST_FACTOR = Game.repairEnergyCostFactor - -local function UpdateBuildSpeed(unitID, unitDefID, speedFactor) - local buildSpeed = (buildSpeedDef[unitDefID] or 0) - if buildSpeed == 0 then - return - end - GG.attRaw_BuildSpeed[unitID] = buildSpeed*speedFactor - spSetUnitBuildSpeed(unitID, - buildSpeed*speedFactor, -- build - buildSpeed*speedFactor / REPAIR_ENERGY_COST_FACTOR, -- repair - buildSpeed*speedFactor, -- reclaim - 0.5*buildSpeed*speedFactor) -- rezz -end - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- --- Economy Handling - -local function UpdateEconomy(unitID, unitDefID, factor, energyFactor) - spSetUnitRulesParam(unitID,"metalGenerationFactor", factor, INLOS_ACCESS) - spSetUnitRulesParam(unitID,"energyGenerationFactor", factor*energyFactor, INLOS_ACCESS) -end - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- --- Health Cost and Mass Handling - -local origUnitHealth = {} -local origUnitCost = {} - -local function UpdateHealthCostMass(unitID, unitDefID, healthAdd, healthMult, costMult, massMult) - if not origUnitHealth[unitDefID] then - local ud = UnitDefs[unitDefID] - origUnitHealth[unitDefID] = ud.health - end - if not origUnitCost[unitDefID] then - local ud = UnitDefs[unitDefID] - origUnitCost[unitDefID] = ud.buildTime - end - local newMaxHealth = (origUnitHealth[unitDefID] + healthAdd) * healthMult - local oldHealth, oldMaxHealth = Spring.GetUnitHealth(unitID) - Spring.SetUnitMaxHealth(unitID, newMaxHealth) - Spring.SetUnitHealth(unitID, oldHealth * newMaxHealth / oldMaxHealth) - - local origCost = origUnitCost[unitDefID] - local cost = origCost*costMult - Spring.SetUnitCosts(unitID, { - metalCost = cost, - energyCost = cost, - buildTime = cost, - }) - - if massMult == 1 then - -- Default to update mass based on new stats, if a multiplier is not set. - local mass = GetMass(newMaxHealth, cost) - Spring.SetUnitMass(unitID, mass) - Spring.SetUnitRulesParam(unitID, "massOverride", mass, INLOS_ACCESS) - else - local mass = GetMass(origUnitHealth[unitDefID], origCost) * massMult - Spring.SetUnitMass(unitID, mass) - Spring.SetUnitRulesParam(unitID, "massOverride", mass, INLOS_ACCESS) - end -end - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- --- Reload Time Handling +-- under my test, IterableMap is slower than pairs +--local IterableMap = VFS.Include("LuaRules/Gadgets/Include/IterableMap.lua") -local origUnitWeapons = {} -local unitReloadPaused = {} -local function UpdatePausedReload(unitID, unitDefID, gameFrame) - local state = origUnitWeapons[unitDefID] - - for i = 1, state.weaponCount do - local w = state.weapon[i] - local reloadState = spGetUnitWeaponState(unitID, i , 'reloadState') - if reloadState then - local reloadTime = spGetUnitWeaponState(unitID, i , 'reloadTime') - local newReload = 100000 -- set a high reload time so healthbars don't judder. NOTE: math.huge is TOO LARGE - if reloadState < 0 then -- unit is already reloaded, so set unit to almost reloaded - spSetUnitWeaponState(unitID, i, {reloadTime = newReload, reloadState = gameFrame+UPDATE_PERIOD+1}) - else - local nextReload = gameFrame+(reloadState-gameFrame)*newReload/reloadTime - spSetUnitWeaponState(unitID, i, {reloadTime = newReload, reloadState = nextReload+UPDATE_PERIOD}) - end - end - end +local Attributes=GG.Attributes +if not Attributes then + Attributes={} + GG.Attributes=Attributes end -local function UpdateWeapons(unitID, unitDefID, weaponMods, speedFactor, rangeUpdateRequired, rangeFactor, projSpeedFactor, projectilesFactor, damageFactor, minSpray, gameFrame) - if not origUnitWeapons[unitDefID] then - local ud = UnitDefs[unitDefID] - - origUnitWeapons[unitDefID] = { - weapon = {}, - weaponCount = #ud.weapons, - maxWeaponRange = ud.maxWeaponRange, - } - local state = origUnitWeapons[unitDefID] - - for i = 1, state.weaponCount do - local wd = WeaponDefs[ud.weapons[i].weaponDef] - local reload = wd.reload - state.weapon[i] = { - reload = reload, - burstRate = wd.salvoDelay, - projectiles = wd.projectiles, - oldReloadFrames = floor(reload*Game.gameSpeed), - range = wd.range, - sprayAngle = wd.sprayAngle, - damages = GG.ATT_ENABLE_DAMAGE and {}, - } - if GG.ATT_ENABLE_DAMAGE then - local did = 0 - local data = state.weapon[i].damages - while wd.damages[did] do - data[did] = wd.damages[did] - did = did + 1 - end - end - - if wd.type == "LaserCannon" or wd.type == "Cannon" then - -- Barely works for missiles, and might break their burnblow and prediction - state.weapon[i].projectileSpeed = wd.projectilespeed - end - if wd.type == "BeamLaser" then - -- beamlasers go screwy if you mess with their burst length - state.weapon[i].burstRate = false - end - end - end - if damageFactor ~= 1 and not GG.ATT_ENABLE_DAMAGE then - Spring.Utilities.UnitEcho(unitID, "damage attribute requires GG.ATT_ENABLE_DAMAGE") - end - - local state = origUnitWeapons[unitDefID] - local maxRangeModified = state.maxWeaponRange*rangeFactor +--[=[ +---@type +local AllAttributes=IterableMap.New() +]=] - for i = 1, state.weaponCount do - local w = state.weapon[i] - local reloadState = spGetUnitWeaponState(unitID, i , 'reloadState') - local reloadTime = spGetUnitWeaponState(unitID, i , 'reloadTime') - local moddedSpeed = ((weaponMods and weaponMods[i] and weaponMods[i].reloadMult) or 1)*speedFactor - if moddedSpeed <= 0 then - if not unitReloadPaused[unitID] then - local newReload = 100000 -- set a high reload time so healthbars don't judder. NOTE: math.huge is TOO LARGE - unitReloadPaused[unitID] = unitDefID - spSetUnitRulesParam(unitID, "reloadPaused", 1, INLOS_ACCESS) - if reloadState < gameFrame then -- unit is already reloaded, so set unit to almost reloaded - spSetUnitWeaponState(unitID, i, {reloadTime = newReload, reloadState = gameFrame+UPDATE_PERIOD+1}) - else - local nextReload = gameFrame+(reloadState-gameFrame)*newReload/reloadTime - spSetUnitWeaponState(unitID, i, {reloadTime = newReload, reloadState = nextReload+UPDATE_PERIOD}) - end - -- add UPDATE_PERIOD so that the reload time never advances past what it is now - end - else - if unitReloadPaused[unitID] then - unitReloadPaused[unitID] = nil - spSetUnitRulesParam(unitID, "reloadPaused", 0, INLOS_ACCESS) - end - local newReload = w.reload/moddedSpeed - local nextReload = gameFrame+(reloadState-gameFrame)*newReload/reloadTime - -- Add HALF_FRAME to round reloadTime to the closest discrete frame (multiple of 1/30), since the the engine rounds DOWN - if w.burstRate then - spSetUnitWeaponState(unitID, i, {reloadTime = newReload + HALF_FRAME, reloadState = nextReload + 0.5, burstRate = w.burstRate/moddedSpeed + HALF_FRAME}) - else - spSetUnitWeaponState(unitID, i, {reloadTime = newReload + HALF_FRAME, reloadState = nextReload + 0.5}) - end - end - local moddedProjectiles = w.projectiles*((weaponMods and weaponMods[i] and weaponMods[i].projectilesMult) or 1)*projectilesFactor - - local sprayAngle = math.max(w.sprayAngle, minSpray) - spSetUnitWeaponState(unitID, i, "sprayAngle", sprayAngle) - - spSetUnitWeaponState(unitID, i, "projectiles", moddedProjectiles) - if rangeUpdateRequired then - if w.projectileSpeed and not projectileSpeedLock[unitID] then - -- Changing projectile speed without subsequently setting range causes some weapons to go to zero range. Eg Scorcher - local moddedSpeed = w.projectileSpeed*((weaponMods and weaponMods[i] and weaponMods[i].projSpeedMult) or 1)*projSpeedFactor - spSetUnitWeaponState(unitID, i, "projectileSpeed", moddedSpeed) - end - if not rangeUpdater[unitID] then - local moddedRange = w.range*((weaponMods and weaponMods[i] and weaponMods[i].rangeMult) or 1)*rangeFactor - spSetUnitWeaponState(unitID, i, "range", moddedRange) - spSetUnitWeaponDamages(unitID, i, "dynDamageRange", moddedRange) - if maxRangeModified < moddedRange then - maxRangeModified = moddedRange - end - end - end - if GG.ATT_ENABLE_DAMAGE then - local did = 0 - local data = state.weapon[i].damages - local toSet = {} - while data[did] do - toSet[did] = data[did] * damageFactor - did = did + 1 - end - spSetUnitWeaponDamages(unitID, i, toSet) - end - end - - if rangeUpdateRequired then - if rangeUpdater[unitID] and rangeUpdater[unitID] ~= true then - local mods = {} - for i = 1, state.weaponCount do - mods[i] = weaponMods and weaponMods[i] and weaponMods[i].rangeMult or 1 - end - rangeUpdater[unitID](rangeFactor, mods) - else - Spring.SetUnitMaxRange(unitID, maxRangeModified) - end - end -end +---UnitAttributes=Attributes[UnitId] +---UnitAttributesDomain=UnitAttributes[domain] +---UnitAttributesDomain[AttType]=value +---@type {[UnitId]:{[string]:{[string]:any}}} +local UnitsAttributes={} --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- --- Movement Speed Handling +---@type {[UnitId]:{[string|number]:AttributesHandler}} +local UnitAttributesHandlers={} -local origUnitSpeed = {} +Spring.Echo("DEBUG: unit_attributes_generic.lua") -local function UpdateMovementSpeed(unitID, unitDefID, speedFactor, turnAccelFactor, maxAccelerationFactor) - if spMoveCtrlGetTag(unitID) ~= nil then - return - end - - if not origUnitSpeed[unitDefID] then - local ud = UnitDefs[unitDefID] - local moveData = spGetUnitMoveTypeData(unitID) +---@type {[string|number]:AttributesHandlerFactory} +local AttributesHandlerFactorys=VFS.Include("LuaRules/Configs/unit_attributes_generic_handlers.lua") - origUnitSpeed[unitDefID] = { - origSpeed = ud.speed, - origReverseSpeed = (moveData.name == "ground") and moveData.maxReverseSpeed or ud.speed, - origTurnRate = ud.turnRate, - origTurnAccel = (ud.turnRate or 1) * (ud.customParams.turn_accel_factor or 1), - origMaxRudder = ud.maxRudder, - origMaxAcc = ud.maxAcc, - origMaxDec = ud.maxDec, - movetype = -1, - } - - local state = origUnitSpeed[unitDefID] - state.movetype = getMovetype(ud) - end - - local state = origUnitSpeed[unitDefID] - local decFactor = maxAccelerationFactor - local isSlowed = (speedFactor < 1) - if isSlowed then - -- increase brake rate to cause units to slow down to their new max speed correctly. - decFactor = 1000 - end - if speedFactor <= 0 then - speedFactor = 0 - - -- Set the units velocity to zero if it is attached to the ground. - local x, y, z = Spring.GetUnitPosition(unitID) - if x then - local h = Spring.GetGroundHeight(x, z) - if h and h >= y then - Spring.SetUnitVelocity(unitID, 0,0,0) - - -- Perhaps attributes should do this: - --local env = Spring.UnitScript.GetScriptEnv(unitID) - --if env and env.script.StopMoving then - -- Spring.UnitScript.CallAsUnit(unitID,env.script.StopMoving, hx, hy, hz) - --end - end - end - end - if turnAccelFactor <= 0 then - turnAccelFactor = 0 - end - local turnFactor = turnAccelFactor - if turnFactor <= 0.001 then - turnFactor = 0.001 - end - if maxAccelerationFactor <= 0 then - maxAccelerationFactor = 0.001 - end - - if state.movetype == 0 then - if speedFactor > 0 then - -- Only modify turning that goes beyond the turn factor that units get alongside - -- speed factor. This makes sense as planes turn via speed already. - turnFactor = turnFactor / speedFactor - else - turnFactor = 1 - end - local attribute = { - maxSpeed = state.origSpeed *speedFactor, - maxAcc = state.origMaxAcc *maxAccelerationFactor, --(speedFactor > 0.001 and speedFactor or 0.001) - maxRudder = state.origMaxRudder *turnFactor, - } - spSetAirMoveTypeData (unitID, attribute) - spSetAirMoveTypeData (unitID, attribute) - elseif state.movetype == 1 then - local attribute = { - maxSpeed = state.origSpeed *speedFactor, - --maxReverseSpeed = state.origReverseSpeed*speedFactor, - turnRate = state.origTurnRate *turnFactor, - accRate = state.origMaxAcc *maxAccelerationFactor, - decRate = state.origMaxDec *maxAccelerationFactor - } - spSetGunshipMoveTypeData (unitID, attribute) - GG.ForceUpdateWantedMaxSpeed(unitID, unitDefID) - elseif state.movetype == 2 then - if workingGroundMoveType then - local accRate = state.origMaxAcc*maxAccelerationFactor - if isSlowed and accRate > speedFactor then - -- Clamp acceleration to mitigate prevent brief speedup when executing new order - -- 1 is here as an arbitary factor, there is no nice conversion which means that 1 is a good value. - accRate = speedFactor - end - local attribute = { - maxSpeed = state.origSpeed *speedFactor, - maxReverseSpeed = (isSlowed and 0) or state.origReverseSpeed, --disallow reverse while slowed - turnRate = state.origTurnRate *turnFactor, - accRate = accRate, - decRate = state.origMaxDec *decFactor, - turnAccel = state.origTurnAccel *turnAccelFactor, - } - spSetGroundMoveTypeData (unitID, attribute) - GG.ForceUpdateWantedMaxSpeed(unitID, unitDefID) - else - --Spring.Echo(state.origSpeed*speedFactor*WACKY_CONVERSION_FACTOR_1) - --Spring.Echo(Spring.GetUnitCOBValue(unitID, COB.MAX_SPEED)) - spSetUnitCOBValue(unitID, COB.MAX_SPEED, math.ceil(state.origSpeed*speedFactor*WACKY_CONVERSION_FACTOR_1)) - end - end +---@param unitId UnitId +local function ClearAttributesHandlers(unitId) + local AttributesHandlers=UnitAttributesHandlers[unitId] + if AttributesHandlers then + for key, value in pairs(AttributesHandlers) do + value.clear() + end + UnitAttributesHandlers[unitId]=nil + end end --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- --- Death Explosion Handling - -local explosionDefID = {} -local explosionRadius = {} -local function AddExplosions(unitID, unitDefID, teamID, expMult) - if expMult <= 1 then -- Unsupported - return - end - local extraExplosions = math.max(1, math.floor(expMult - 0.5)) - local explosionDamageMult = extraExplosions / (expMult - 1) - if not explosionDefID[unitDefID] then - local wd = WeaponDefNames[UnitDefs[unitDefID].deathExplosion] - explosionDefID[unitDefID] = wd.id - explosionRadius[unitDefID] = wd.damageAreaOfEffect or 0 - end - local _, _, _, ux, uy, uz = Spring.GetUnitPosition(unitID, true) - local projectileParams = { - pos = {ux, uy, uz}, - ["end"] = {ux, uy - 1, uz}, - owner = unitID, - team = teamID, - ttl = 0, - } - local expLevel = 1 + math.log(expMult) / math.log(2) - local radius = (5 + 15*expLevel)*(50 + math.pow(explosionRadius[unitDefID], 0.8))/100 - for i = 1, extraExplosions do - local rand = Vector.RandomPointInCircle(radius) - projectileParams.pos[1] = ux + rand[1] - projectileParams.pos[3] = uz + rand[2] - local proID = Spring.SpawnProjectile(explosionDefID[unitDefID], projectileParams) - -- TODO: Handle explosionDamageMult ~= 1 with SetProjectileDamages - if proID then - Spring.SetProjectileCollision(proID) - end - end +---@param unitId UnitId +local function InitAttributesHandlers(unitId) + local AttributesHandlers=UnitAttributesHandlers[unitId] + if not AttributesHandlers then + AttributesHandlers={} + UnitAttributesHandlers[unitId]=AttributesHandlers + local udid=spGetUnitDefID(unitId) + ---@cast udid -nil + for key, value in pairs(AttributesHandlerFactorys) do + AttributesHandlers[key]=value.new(unitId,udid) + end + end + return AttributesHandlers end --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- --- Attribute Updating - -local attributesTypes = IterableMap.New() -local unitSlowed = {} -local unitHasAttributes = {} - - --- Internal tracking to avoid unnecessary updates -local currentHealthAdd = {} -local currentHealthMult = {} -local currentMove = {} -local currentTurn = {} -local currentAccel = {} -local currentReload = {} -local currentRange = {} -local currentProjSpeed = {} -local currentEcon = {} -local currentMass = {} -local currentEnergy = {} -local currentBuildpower = {} -local currentCost = {} -local currentProjectiles = {} -local currentDamage = {} -local currentMinSpray = {} -local currentShieldDisabled = {} -local currentAbilityDisabled = {} - -local currentSense = {} -local currentSetRadar = {} -local currentSetSonar = {} -local currentSetJammer = {} -local currentSetSight = {} +local spGetGameFrame = Spring.GetGameFrame -local function CleanupAttributeDataForUnit(unitID) - unitHasAttributes[unitID] = nil - unitReloadPaused[unitID] = nil -- defined earlier - - currentHealthAdd[unitID] = nil - currentHealthMult[unitID] = nil - currentMove[unitID] = nil - currentTurn[unitID] = nil - currentAccel[unitID] = nil - currentReload[unitID] = nil - currentRange[unitID] = nil - currentProjSpeed[unitID] = nil - currentEcon[unitID] = nil - currentMass[unitID] = nil - currentEnergy[unitID] = nil - currentBuildpower[unitID] = nil - currentCost[unitID] = nil - currentProjectiles[unitID] = nil - currentDamage[unitID] = nil - currentMinSpray[unitID] = nil - currentShieldDisabled[unitID] = nil - currentAbilityDisabled[unitID] = nil - - currentSense[unitID] = nil - currentSetRadar[unitID] = nil - currentSetSonar[unitID] = nil - currentSetJammer[unitID] = nil - currentSetSight[unitID] = nil - - GG.att_LastChangeFrame[unitID] = nil - GG.att_CostMult[unitID] = nil - GG.att_HealthMult[unitID] = nil - GG.att_EconomyChange[unitID] = nil - GG.att_ReloadChange[unitID] = nil - GG.att_MoveChange[unitID] = nil - GG.att_RangeChange[unitID] = nil - GG.att_JumpRangeChange[unitID] = nil - GG.att_DeathExplodeMult[unitID] = nil - GG.att_ProjSpeed[unitID] = nil - GG.att_ProjMult[unitID] = nil - GG.att_DamageMult[unitID] = nil - GG.att_RegenChange[unitID] = nil - GG.att_ShieldRegenChange[unitID] = nil - GG.att_StaticBuildRateMult[unitID] = nil - GG.attRaw_BuildSpeed[unitID] = nil -end +---@param unitID UnitId +---@param datas {[string]:{[string]:any}} -- {[domain]:{[attType]:value}} +local function UpdateUnitAttributes(unitID, datas) + if not datas or not next(datas) then + ClearAttributesHandlers(unitID) + return + end -local function UpdateUnitAttributes(unitID, attTypeMap) - if not spValidUnitID(unitID) then - return true - end - - local unitDefID = spGetUnitDefID(unitID) - if not unitDefID then - return true - end - local frame = spGetGameFrame() - GG.att_LastChangeFrame[unitID] = frame - - local healthAdd = 0 - local healthMult = 1 - local moveMult = 1 - local turnMult = 1 - local accelMult = 1 - local reloadMult = 1 - local rangeMult = 1 - local jumpRangeMult = 1 - local deathExplodeMult = 1 - local projSpeedMult = 1 - local econMult = 1 - local massMult = 1 - local energyMult = 1 - local shieldRegen = 1 - local shieldMaxMult = 1 - local healthRegen = 1 - local costMult = 1 - local buildMult = 1 - local senseMult = 1 - local projectilesMult = 1 - local damageMult = 1 - local minSpray = 0 - local abilityDisabled = false - local shieldDisabled = false - local weaponSpecificMods = false - local setRadar = false - local setSonar = false - local setJammer = false - local setSight = false - - local staticBuildpowerMult = 1 - local staticMetalMult = 1 - local staticEnergyMult = 1 - local staticShieldRegen = 1 - local staticHealthRegen = 1 - local staticMoveMult = 1 - - local hasAttributes = false - for key, data in IterableMap.Iterator(attTypeMap) do - if data.includedUnits[unitID] then - hasAttributes = true - - healthAdd = healthAdd + (data.healthAdd and data.healthAdd[unitID] or 0) - healthMult = healthMult*(data.healthMult and data.healthMult[unitID] or 1) - healthRegen = healthRegen*(data.healthRegen and data.healthRegen[unitID] or 1) - costMult = costMult*(data.cost and data.cost[unitID] or 1) - massMult = massMult*(data.mass and data.mass[unitID] or 1) - - moveMult = moveMult*(data.move and data.move[unitID] or 1) - turnMult = turnMult*(data.turn and data.turn[unitID] or (data.move and data.move[unitID]) or 1) - accelMult = accelMult*(data.accel and data.accel[unitID] or (data.move and data.move[unitID]) or 1) - jumpRangeMult = jumpRangeMult*(data.jumpRange and data.jumpRange[unitID] or 1) - deathExplodeMult = deathExplodeMult*(data.deathExplode and data.deathExplode[unitID] or 1) - - shieldRegen = shieldRegen*(data.shieldRegen and data.shieldRegen[unitID] or 1) - shieldMaxMult = shieldMaxMult*(data.shieldMax and data.shieldMax[unitID] or 1) - - energyMult = energyMult*(data.energy and data.energy[unitID] or 1) - econMult = econMult*(data.econ and data.econ[unitID] or 1) - buildMult = buildMult*(data.build and data.build[unitID] or 1) - - abilityDisabled = abilityDisabled or data.abilityDisabled and data.abilityDisabled[unitID] - shieldDisabled = shieldDisabled or data.shieldDisabled and data.shieldDisabled[unitID] - minSpray = math.max(minSpray, data.minSpray and data.minSpray[unitID] or 0) - senseMult = senseMult*(data.sense and data.sense[unitID] or 1) - setRadar = data.setRadar and data.setRadar[unitID] or setRadar - setJammer = data.setJammer and data.setJammer[unitID] or setJammer - setSonar = data.setSonar and data.setSonar[unitID] or setSonar - setSight = data.setSight and data.setSight[unitID] or setSight - - if data.static then - staticBuildpowerMult = staticBuildpowerMult*(data.build and data.build[unitID] or 1) - staticMetalMult = staticMetalMult*(data.econ and data.econ[unitID] or 1) - staticEnergyMult = staticEnergyMult*(data.econ and data.econ[unitID] or 1)*(data.energy and data.energy[unitID] or 1) - staticShieldRegen = staticShieldRegen*(data.shieldRegen and data.shieldRegen[unitID] or 1) - staticHealthRegen = staticHealthRegen*(data.healthRegen and data.healthRegen[unitID] or 1) - staticMoveMult = staticMoveMult*(data.move and data.move[unitID] or 1) - end - - if data.weaponNum and data.weaponNum[unitID] then - local weaponNum = data.weaponNum[unitID] - weaponSpecificMods = weaponSpecificMods or {} - weaponSpecificMods[weaponNum] = weaponSpecificMods[weaponNum] or { - reloadMult = 1, - rangeMult = 1, - projSpeedMult = 1, - projectilesMult = 1, - damageMult = 1, - } - local wepData = weaponSpecificMods[weaponNum] - wepData.reloadMult = wepData.reloadMult*(data.reload and data.reload[unitID] or 1) - wepData.rangeMult = wepData.rangeMult*(data.range and data.range[unitID] or 1) - wepData.projSpeedMult = wepData.projSpeedMult*(data.projSpeed and data.projSpeed[unitID] or 1) - wepData.projectilesMult = wepData.projectilesMult*(data.projectiles and data.projectiles[unitID] or 1) - wepData.damageMult = wepData.damageMult*(data.damage and data.damage[unitID] or 1) - else - reloadMult = reloadMult*(data.reload and data.reload[unitID] or 1) - rangeMult = rangeMult*(data.range and data.range[unitID] or 1) - projSpeedMult = projSpeedMult*(data.projSpeed and data.projSpeed[unitID] or 1) - projectilesMult = projectilesMult*(data.projectiles and data.projectiles[unitID] or 1) - damageMult = damageMult*(data.damage and data.damage[unitID] or 1) - end - end - end - - spSetUnitRulesParam(unitID, "totalReloadSpeedChange", reloadMult, INLOS_ACCESS) - spSetUnitRulesParam(unitID, "totalEconomyChange", econMult, INLOS_ACCESS) - spSetUnitRulesParam(unitID, "totalBuildPowerChange", buildMult, INLOS_ACCESS) - spSetUnitRulesParam(unitID, "totalMoveSpeedChange", moveMult, INLOS_ACCESS) - spSetUnitRulesParam(unitID, "costMult", costMult, INLOS_ACCESS) - spSetUnitRulesParam(unitID, "projectilesMult", projectilesMult, INLOS_ACCESS) - spSetUnitRulesParam(unitID, "projectileSpeedMult", projSpeedMult, INLOS_ACCESS) - spSetUnitRulesParam(unitID, "damageMult", damageMult, INLOS_ACCESS) - spSetUnitRulesParam(unitID, "rangeMult", rangeMult, INLOS_ACCESS) - spSetUnitRulesParam(unitID, "senseMult", senseMult, INLOS_ACCESS) - spSetUnitRulesParam(unitID, "jumpRangeMult", jumpRangeMult, INLOS_ACCESS) - spSetUnitRulesParam(unitID, "deathExplodeMult", deathExplodeMult, INLOS_ACCESS) - - spSetUnitRulesParam(unitID, "totalStaticBuildpowerMult", staticBuildpowerMult, INLOS_ACCESS) - spSetUnitRulesParam(unitID, "totalStaticMetalMult", staticMetalMult, INLOS_ACCESS) - spSetUnitRulesParam(unitID, "totalStaticEnergyMult", staticEnergyMult, INLOS_ACCESS) - spSetUnitRulesParam(unitID, "totalShieldMaxMult", shieldMaxMult, INLOS_ACCESS) - - spSetUnitRulesParam(unitID, "totalStaticShieldRegen", staticShieldRegen, INLOS_ACCESS) - spSetUnitRulesParam(unitID, "totalStaticHealthRegen", staticHealthRegen, INLOS_ACCESS) - spSetUnitRulesParam(unitID, "totalStaticMoveSpeedChange", staticMoveMult, INLOS_ACCESS) - - -- GG is faster (but gadget-only). - GG.att_CostMult[unitID] = costMult - GG.att_HealthMult[unitID] = healthMult - GG.att_EconomyChange[unitID] = econMult - GG.att_ReloadChange[unitID] = reloadMult - GG.att_MoveChange[unitID] = moveMult - GG.att_RangeChange[unitID] = rangeMult - GG.att_JumpRangeChange[unitID] = jumpRangeMult - GG.att_DeathExplodeMult[unitID] = deathExplodeMult - GG.att_RegenChange[unitID] = healthRegen - GG.att_ShieldRegenChange[unitID] = shieldRegen - GG.att_ShieldMaxMult[unitID] = shieldMaxMult - GG.att_StaticBuildRateMult[unitID] = staticBuildpowerMult - GG.att_ProjSpeed[unitID] = projSpeedMult -- Ignores weapon mods - GG.att_ProjMult[unitID] = projectilesMult - GG.att_DamageMult[unitID] = damageMult - - unitSlowed[unitID] = moveMult < 1 - - local healthChanges = (currentHealthAdd[unitID] or 0) ~= healthAdd - or (currentHealthMult[unitID] or 1) ~= healthMult - - local rangeUpdateRequired = (currentRange[unitID] or 1) ~= rangeMult or (currentProjectiles[unitID] or 1) ~= projectilesMult - local weaponChanges = (currentReload[unitID] or 1) ~= reloadMult - or (currentRange[unitID] or 1) ~= rangeMult - or (currentProjectiles[unitID] or 1) ~= projectilesMult - or (currentDamage[unitID] or 1) ~= damageMult - or (currentMinSpray[unitID] or 0) ~= minSpray - - local moveChanges = (currentMove[unitID] or 1) ~= moveMult - or (currentTurn[unitID] or 1) ~= turnMult - or (currentAccel[unitID] or 1) ~= accelMult - - local senseChanges = (currentSense[unitID] ~= senseMult) - or (abilityDisabled ~= currentAbilityDisabled[unitID]) - or (setRadar ~= (currentSetRadar[unitID] or false)) - or (setSonar ~= (currentSetSonar[unitID] or false)) - or (setJammer ~= (currentSetJammer[unitID] or false)) - or (setSight ~= (currentSetSight[unitID] or false)) - - local actualMassMult = 1 - if healthChanges or (currentCost[unitID] or 1) ~= costMult or (currentMass[unitID] or 1) ~= massMult then - actualMassMult = UpdateHealthCostMass(unitID, unitDefID, healthAdd, healthMult, costMult, massMult) - currentHealthAdd[unitID] = healthAdd - currentHealthMult[unitID] = healthMult - currentCost[unitID] = costMult - currentMass[unitID] = massMult - end - - if moveChanges then - UpdateMovementSpeed(unitID, unitDefID, moveMult, turnMult, accelMult) - currentMove[unitID] = moveMult - currentTurn[unitID] = turnMult - currentAccel[unitID] = accelMult - end - - if weaponSpecificMods or weaponChanges then - UpdateWeapons(unitID, unitDefID, weaponSpecificMods, reloadMult, rangeUpdateRequired, rangeMult, projSpeedMult, projectilesMult, damageMult, minSpray, frame) - currentReload[unitID] = reloadMult - currentRange[unitID] = rangeMult - currentProjSpeed[unitID] = projSpeedMult - currentProjectiles[unitID] = projectilesMult - currentDamage[unitID] = damageMult - currentMinSpray[unitID] = minSpray - end - - if buildMult ~= currentBuildpower[unitID] then - UpdateBuildSpeed(unitID, unitDefID, buildMult) - currentBuildpower[unitID] = buildMult - end - - if econMult ~= currentEcon[unitID] then - UpdateEconomy(unitID, unitDefID, econMult, energyMult) - currentEcon[unitID] = econMult - end - - if abilityDisabled ~= currentAbilityDisabled[unitID] then - spSetUnitRulesParam(unitID, "att_abilityDisabled", abilityDisabled and 1 or 0) - currentAbilityDisabled[unitID] = abilityDisabled - end - - if shieldDisabled ~= currentShieldDisabled[unitID] then - spSetUnitRulesParam(unitID, "att_shieldDisabled", shieldDisabled and 1 or 0) - if shieldDisabled then - Spring.SetUnitShieldState(unitID, -1, 0) - end - if spGetUnitRulesParam(unitID, "comm_shield_max") ~= 0 then - if shieldDisabled then - Spring.SetUnitShieldState(unitID, spGetUnitRulesParam(unitID, "comm_shield_num") or -1, false) - else - Spring.SetUnitShieldState(unitID, spGetUnitRulesParam(unitID, "comm_shield_num") or -1, true) - end - end - currentShieldDisabled[unitID] = shieldDisabled - end - - if senseChanges then - UpdateSensorAndJamm(unitID, unitDefID, senseMult, not abilityDisabled, setRadar, setSonar, setJammer, setSight) - currentSense[unitID] = senseMult - currentSetRadar[unitID] = setRadar - currentSetSonar[unitID] = setSonar - currentSetJammer[unitID] = setJammer - currentSetSight[unitID] = setSight - end - - if not hasAttributes then - CleanupAttributeDataForUnit(unitID) - end + local AttributesHandlers=InitAttributesHandlers(unitID) + ---@type {[string|number]:AttributesDataHandler} + local AttributesDataHandlers={} + for key, value in pairs(AttributesHandlers) do + AttributesDataHandlers[key]=value.newDataHandler(frame) + end + for domain, data in pairs(datas) do + for key, dataHandlers in pairs(AttributesDataHandlers) do + dataHandlers.fold(data) + end + end + for key, value in pairs(AttributesDataHandlers) do + value.apply() + end end --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - --- Gadgets can send these to Attributes.AddEffect -local attributeNames = { - "healthAdd", - "healthMult", - "healthRegen", - "move", - "turn", - "accel", - "reload", - "range", - "jumpRange", - "deathExplode", - "projSpeed", - "econ", - "energy", - "shieldMax", - "shieldRegen", - "cost", - "mass", - "build", - "sense", - "setRadar", - "setSonar", - "setJammer", - "setSight", - "projectiles", - "damage", -- Enabled by setting GG.ATT_ENABLE_DAMAGE = true - "weaponNum", - "minSpray", - "abilityDisabled", - "shieldDisabled", - - -- Whether the attribute change is intended to ba baked into the unit, or temporary. - -- This helps the UI display stats, and helps things like morph work correctly. - "static", -} - -local function InitAttributeType() - local attType = { - includedUnits = {}, - } - return attType -end - -local function AddToAttributeType(attType, unitID, effectTable, attName) - if not effectTable[attName] and not attType[attName] then - return false - end - if not attType[attName] then - attType[attName] = {} - end - attType[attName][unitID] = effectTable[attName] -end - -local function RemoveUnitFromAttributeType(attType, unitID) - if not attType.includedUnits[unitID] then - return - end - attType.includedUnits[unitID] = nil - for i = 1, #attributeNames do - if attType[attName] and attType[attName][unitID] ~= nil then - attType[attName][unitID] = nil - end - end -end - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- --- External Interface - -local Attributes = {} -function Attributes.SetProjectileSpeedLock(unitID, lockState) - projectileSpeedLock[unitID] = lockState -end - -function Attributes.SetRangeUpdater(unitID, updateFunc) - rangeUpdater[unitID] = updateFunc -end function Attributes.RemoveUnit(unitID) - if not unitHasAttributes[unitID] then - return - end - CleanupAttributeDataForUnit(unitID) - for _, attType in IterableMap.Iterator(attributesTypes) do - RemoveUnitFromAttributeType(attType, unitID) - end - projectileSpeedLock[unitID] = nil - rangeUpdater[unitID] = nil + ClearAttributesHandlers(unitID) end -function Attributes.AddEffect(unitID, key, effect) - -- See attributeNames above for value effect keys - local attType = IterableMap.Get(attributesTypes, key) - if not attType then - attType = InitAttributeType() - end - - unitHasAttributes[unitID] = true - attType.includedUnits[unitID] = true - for i = 1, #attributeNames do - AddToAttributeType(attType, unitID, effect, attributeNames[i]) - end - - IterableMap.Add(attributesTypes, key, attType) -- Overwrites existing key if it exists - if UpdateUnitAttributes(unitID, attributesTypes) then - Attributes.RemoveUnit(unitID) - end + +---@param unitID UnitId +---@param domain string +---@param effects {[string]:any} +function Attributes.AddEffect(unitID, domain, effects) + local UnitAttributes=UnitsAttributes[unitID] + if not UnitAttributes then + UnitAttributes={} + UnitsAttributes[unitID]=UnitAttributes + end + UnitAttributes[domain]=effects + UpdateUnitAttributes(unitID, UnitAttributes) end function Attributes.RemoveEffect(unitID, key) - if not unitHasAttributes[unitID] then - return - end - local attType = IterableMap.Get(attributesTypes, key) - if attType then - RemoveUnitFromAttributeType(attType, unitID) - end - if UpdateUnitAttributes(unitID, attributesTypes) then - Attributes.RemoveUnit(unitID) - end + local UnitAttributes=UnitsAttributes[unitID] + if not UnitAttributes then + return + end + UnitAttributes[key]=nil + UpdateUnitAttributes(unitID, UnitAttributes) end --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- --- Updates and tweaks - function gadget:Initialize() - GG.Attributes = Attributes + for key, fac in pairs(AttributesHandlerFactorys) do + if fac.initialize then + fac.initialize() + end + end end -function gadget:GameFrame(f) - if f % UPDATE_PERIOD == 1 then - for unitID, unitDefID in pairs(unitReloadPaused) do - UpdatePausedReload(unitID, unitDefID, f) - end - end -end function gadget:UnitDestroyed(unitID, unitDefID, teamID) - if (GG.att_DeathExplodeMult[unitID] or 1) ~= 1 then - if GG.MorphDestroy ~= unitID then - local _,_,_,_,build = Spring.GetUnitHealth(unitID) - if build and build >= 0.8 then - AddExplosions(unitID, unitDefID, teamID, GG.att_DeathExplodeMult[unitID]) - end - end - end Attributes.RemoveUnit(unitID) -end - -function gadget:AllowCommand_GetWantedCommand() - return true -end - -function gadget:AllowCommand_GetWantedUnitDefID() - return true -end - -function gadget:AllowCommand(unitID, unitDefID, teamID, cmdID, cmdParams, cmdOptions) - if (cmdID == 70 and unitSlowed[unitID]) then - return false - else - return true - end end \ No newline at end of file diff --git a/LuaRules/Gadgets/unit_death_explosion.lua b/LuaRules/Gadgets/unit_death_explosion.lua new file mode 100644 index 0000000000..1dcbd68046 --- /dev/null +++ b/LuaRules/Gadgets/unit_death_explosion.lua @@ -0,0 +1,69 @@ + +if not gadgetHandler:IsSyncedCode() then + return +end + +function gadget:GetInfo() + return { + name = "Unit Death Explosion", + desc = "Handles Unit Death Explosion", + author = "XNTEABDSC", -- v1 CarReparier & GoogleFrog + date = "2025", -- v1 2009-11-27 + license = "GNU GPL, v2 or later", + layer = 1, + enabled = true, + } +end + +local spGetUnitRulesParam=Spring.GetUnitRulesParam +local spSpawnProjectile=Spring.SpawnProjectile +local spSetProjectileCollision=Spring.SetProjectileCollision +local Vector = Spring.Utilities.Vector + +local explosionDefID = {} +local explosionRadius = {} +local function AddExplosions(unitID, unitDefID, teamID, expMult) + if expMult <= 1 then -- Unsupported + return + end + local extraExplosions = math.max(1, math.floor(expMult - 0.5)) + local explosionDamageMult = extraExplosions / (expMult - 1) + if not explosionDefID[unitDefID] then + local wd = WeaponDefNames[UnitDefs[unitDefID].deathExplosion] + explosionDefID[unitDefID] = wd.id + explosionRadius[unitDefID] = wd.damageAreaOfEffect or 0 + end + local _, _, _, ux, uy, uz = Spring.GetUnitPosition(unitID, true) + local projectileParams = { + pos = {ux, uy, uz}, + ["end"] = {ux, uy - 1, uz}, + owner = unitID, + team = teamID, + ttl = 0, + } + local expLevel = 1 + math.log(expMult) / math.log(2) + local radius = (5 + 15*expLevel)*(50 + math.pow(explosionRadius[unitDefID], 0.8))/100 + for i = 1, extraExplosions do + local rand = Vector.RandomPointInCircle(radius) + projectileParams.pos[1] = ux + rand[1] + projectileParams.pos[3] = uz + rand[2] + local proID = spSpawnProjectile(explosionDefID[unitDefID], projectileParams) + -- TODO: Handle explosionDamageMult ~= 1 with SetProjectileDamages + if proID then + spSetProjectileCollision(proID) + end + end +end + + +function gadget:UnitDestroyed(unitID, unitDefID, teamID) + local deathExplodeMult=spGetUnitRulesParam(unitID, "deathExplodeMult") + if deathExplodeMult and deathExplodeMult ~= 1 then + if GG.MorphDestroy ~= unitID then + local _,_,_,_,build = Spring.GetUnitHealth(unitID) + if build and build >= 0.8 then + AddExplosions(unitID, unitDefID, teamID, deathExplodeMult) + end + end + end +end \ No newline at end of file diff --git a/LuaRules/Gadgets/unit_reload_pause.lua b/LuaRules/Gadgets/unit_reload_pause.lua new file mode 100644 index 0000000000..58de897924 --- /dev/null +++ b/LuaRules/Gadgets/unit_reload_pause.lua @@ -0,0 +1,91 @@ + +if not gadgetHandler:IsSyncedCode() then + return +end + +function gadget:GetInfo() + return { + name = "Unit Reload Pause", + desc = "Handles Unit Reload Pause", + author = "XNTEABDSC", -- v1 CarReparier & GoogleFrog + date = "2025", -- v1 2009-11-27 + license = "GNU GPL, v2 or later", + layer = 0, + enabled = true, + } +end + +local INLOS_ACCESS = {inlos = true} + +local spSetUnitWeaponState=Spring.SetUnitWeaponState +local spSetUnitRulesParam=Spring.SetUnitRulesParam +local spGetUnitWeaponState = Spring.GetUnitWeaponState +local spGetGameFrame = Spring.GetGameFrame + + + +---@type table> +local UnitsWeaponsReloadPaused={} + +local UPDATE_PERIOD=3 + +local function UpdateUnitReloadPause(unitId,weaponNum,reloadState,reloadTime,gameFrame) + reloadState = reloadState or spGetUnitWeaponState(unitID, weaponNum , 'reloadState') + reloadTime = reloadTime or spGetUnitWeaponState(unitID, weaponNum , 'reloadTime') + gameFrame = gameFrame or spGetGameFrame() + + local newReload = 100000 -- set a high reload time so healthbars don't judder. NOTE: math.huge is TOO LARGE + if reloadState < gameFrame then -- unit is already reloaded, so set unit to almost reloaded + spSetUnitWeaponState(unitId, weaponNum, {reloadTime = newReload, reloadState = gameFrame+UPDATE_PERIOD+1}) + else + local nextReload = gameFrame+(reloadState-gameFrame)*newReload/reloadTime + spSetUnitWeaponState(unitId, weaponNum, {reloadTime = newReload, reloadState = nextReload+UPDATE_PERIOD}) + end +end + +local function UnitReloadUnpause(unitId,weaponNum) + local UnitWeaponsReloadPaused=UnitsWeaponsReloadPaused[unitId] + if not UnitWeaponsReloadPaused then + return + end + local UnitWeaponReloadPaused=UnitWeaponsReloadPaused[weaponNum] + if not UnitWeaponReloadPaused then + return + end + UnitWeaponsReloadPaused[weaponNum]=nil + if not next(UnitWeaponsReloadPaused) then + spSetUnitRulesParam(unitId, "reloadPaused", 0, INLOS_ACCESS) + end +end + +local function UnitReloadPause(unitId,weaponNum,reloadState,reloadTime,gameFrame) + local UnitWeaponsReloadPaused=UnitsWeaponsReloadPaused[unitId] + if not UnitWeaponsReloadPaused then + UnitWeaponsReloadPaused={} + UnitsWeaponsReloadPaused[unitId]=UnitWeaponsReloadPaused + end + local UnitWeaponReloadPaused=UnitWeaponsReloadPaused[weaponNum] + if UnitWeaponReloadPaused then + return + end + UnitWeaponsReloadPaused[weaponNum]=true + spSetUnitRulesParam(unitID, "reloadPaused", 1, INLOS_ACCESS) + UpdateUnitReloadPause(unitId,weaponNum,reloadState,reloadTime,gameFrame) + +end + +GG.UnitReloadPause={ + UnitReloadPause=UnitReloadPause, + UnitReloadUnpause=UnitReloadUnpause, + UpdateUnitReloadPause=UpdateUnitReloadPause +} + +function gadget:GameFrame(f) + if f % UPDATE_PERIOD == 1 then + for unitID, weapons in pairs(UnitsWeaponsReloadPaused) do + for weaponNum,_ in pairs(UnitsWeaponsReloadPaused) do + UpdateUnitReloadPause(unitID,weaponNum,nil,nil,f) + end + end + end +end \ No newline at end of file