From 6c315a5c174b9931a4b081df081cc52ec6adcdde Mon Sep 17 00:00:00 2001 From: SethDGamre <165520713+SethDGamre@users.noreply.github.com> Date: Thu, 10 Oct 2024 14:51:32 -0500 Subject: [PATCH] Collision damage patch 4 (#3784) Fix some issues with collision and impulse handling --- gamedata/modrules.lua | 2 +- .../unit_collision_damage_behavior.lua | 70 ++++++++++++++----- 2 files changed, 54 insertions(+), 18 deletions(-) diff --git a/gamedata/modrules.lua b/gamedata/modrules.lua index 8d2efcbb449..c107556240a 100644 --- a/gamedata/modrules.lua +++ b/gamedata/modrules.lua @@ -68,7 +68,7 @@ local modrules = { }, movement = { - allowUnitCollisionDamage = false, -- default: true if using QTPFS pathfinder. Do unit-unit (skidding) collisions cause damage? + allowUnitCollisionDamage = true, -- default: true if using QTPFS pathfinder. Do unit-unit (skidding) collisions cause damage? allowUnitCollisionOverlap = false, -- can mobile units collision volumes overlap one another? Allows unit movement like this (video http://www.youtube.com/watch?v=mRtePUdVk2o ) at the cost of more 'clumping'. allowCrushingAlliedUnits = true, -- default: false. Can allied ground units crush each other during collisions? Units still have to be explicitly set as crushable using the crushable parameter of Spring.SetUnitBlocking. allowGroundUnitGravity = false, -- default: true. Allows fast moving mobile units to 'catch air' as they move over terrain. diff --git a/luarules/gadgets/unit_collision_damage_behavior.lua b/luarules/gadgets/unit_collision_damage_behavior.lua index 8a722786663..2835ab4698e 100644 --- a/luarules/gadgets/unit_collision_damage_behavior.lua +++ b/luarules/gadgets/unit_collision_damage_behavior.lua @@ -6,7 +6,7 @@ function gadget:GetInfo() date = "2024.8.29", license = "GNU GPL, v2 or later", layer = 0, - enabled = false + enabled = true } end @@ -24,12 +24,15 @@ local collisionVelocityThreshold = 99 / Game.gameSpeed local validCollisionAngleMultiplier = math.cos(math.rad(20)) --degrees -- Decrease this value to make units move less from impulse. This defines the maximum impulse allowed, which is (maxImpulseMultiplier * mass) of each unit. -local maxImpulseMultiplier = 165 / Game.gameSpeed +local maxImpulseMultiplier = 5.5 --- elmo/s, converted to elmo/frame. If a unit is launched via explosion faster than this, it is instantly slowed to the derivitive velLengthOffsetCap. +--to save performance and reduce unit hesitation from nominal impulse, impulse values below (minImpulseMultiplier * mass) returns 0 impulse. +local minImpulseMultiplier = 1 + +-- elmo/s, converted to elmo/frame. If a unit is launched via explosion faster than this, it is instantly slowed. If unit speed/gameSpeed is greater or canFly = true, speed/gameSpeed is used instead. local velocityCap = 330 / Game.gameSpeed ---measured in elmos per frame. If velocity is above this threshold, it will be slowed until below this threshold. +--measured in elmos per frame. If velocity is above this threshold, it will be slowed until below this threshold so long as its initial velocity was greater than velocityCap. local velocitySlowdownThreshold = 30 / Game.gameSpeed --any weapondef impulseFactor below this is ignored to save performance @@ -51,22 +54,39 @@ local mathAbs = math.abs local fallDamageMultipliers = {} local unitsMaxImpulse = {} +local unitsMinImpulse = {} local weaponDefIDImpulses = {} local transportedUnits = {} local unitMasses = {} +local unitDefData = {} local weaponDefIgnored = {} local unitInertiaCheckFlags = {} local fallingKillQueue = {} +local launchedUnits = {} local fallingUnits = {} local gameFrame = 0 local velocityWatchFrames = 300 / Game.gameSpeed -local velLengthOffsetCap = velocityCap * 0.75 --Empirically chosen. Ensures that the resultant velocity reduction is below the cap due to how Velocity Length is usually larger than any single XYZ velocity. for unitDefID, unitDef in ipairs(UnitDefs) do + unitDefData[unitDefID] = {} + if unitDef.canFly then + unitDefData[unitDefID].canFly = true + end + if unitDef.speed and unitDef.speed > 0 then + if unitDefData[unitDefID].canFly then + unitDefData[unitDefID].velocityCap = unitDef.speed / Game.gameSpeed + else + unitDefData[unitDefID].velocityCap = math.max(unitDef.speed / Game.gameSpeed, velocityCap) + end + else + unitDefData[unitDefID].velocityCap = velocityCap + end + local fallDamageMultiplier = unitDef.customParams.fall_damage_multiplier or 1.0 fallDamageMultipliers[unitDefID] = fallDamageMultiplier * fallDamageMagnificationFactor unitsMaxImpulse[unitDefID] = unitDef.mass * maxImpulseMultiplier + unitsMinImpulse[unitDefID] = unitDef.mass * minImpulseMultiplier unitMasses[unitDefID] = unitDef.mass end @@ -82,7 +102,6 @@ for weaponDefID, wDef in ipairs(WeaponDefs) do end end - local function maxDamage(damages) local damage = damages[0] for i = 1, #damages do @@ -106,8 +125,12 @@ local function getImpulseMultiplier(unitDefID, weaponDefID, damage) impulseFactor = weaponDefIDImpulses[weaponDefID].impulseFactor or 1 end local impulse = (damage + impulseBoost) * impulseFactor - local maxImpulse = unitsMaxImpulse[unitDefID] - local impulseMultiplier = mathMin(maxImpulse/impulse, 1) -- negative impulse values are not capped. + local impulseMultiplier + if impulse < unitsMinImpulse[unitDefID] then + impulseMultiplier = 0 + else + impulseMultiplier = mathMin(unitsMaxImpulse[unitDefID]/impulse, 1) -- negative impulse values are not capped. + end return impulseMultiplier end @@ -141,7 +164,9 @@ function gadget:UnitPreDamaged(unitID, unitDefID, unitTeam, damage, paralyzer, w if not weaponDefIgnored[weaponDefID] and weaponDefID >= 0 then --this section handles limiting maximum impulse local impulseMultiplier = 1 impulseMultiplier = getImpulseMultiplier(unitDefID, weaponDefID, damage) - unitInertiaCheckFlags[unitID] = gameFrame + velocityWatchFrames + if not unitInertiaCheckFlags[unitID] and impulseMultiplier ~= 0 then + unitInertiaCheckFlags[unitID] = {expirationFrame = gameFrame + velocityWatchFrames, velocityCap = unitDefData[unitDefID].velocityCap} + end return damage, impulseMultiplier elseif (weaponDefID == groundCollisionDefID or weaponDefID == objectCollisionDefID) and (isValidCollisionDirection(unitID) or fallingUnits[unitID]) then local healthRatioMultiplier, health = massToHealthRatioMultiplier(unitID, unitDefID) @@ -161,21 +186,29 @@ function gadget:UnitUnloaded(unitID, unitDefID, unitTeam, transportID, transpor fallingUnits[unitID] = true --units falling from transports should take collision damagee from any trajectory, including when bouncing off of other objects. end +function gadget:UnitEnteredAir(unitID, unitDefID, unitTeam) + if not transportedUnits[unitID] and not unitDefData[unitDefID].canFly then + launchedUnits[unitID] = true + end +end + function gadget:UnitLeftAir(unitID, unitDefID, unitTeam) fallingUnits[unitID] = nil + launchedUnits[unitID] = nil end function gadget:UnitDestroyed(unitID, unitDefID, unitTeam, attackerID, attackerDefID, attackerTeam) transportedUnits[unitID] = nil unitInertiaCheckFlags[unitID] = nil + launchedUnits[unitID] = nil + fallingUnits[unitID] = nil end function gadget:GameFrame(frame) - for unitID, expirationFrame in pairs(unitInertiaCheckFlags) do + for unitID, data in pairs(unitInertiaCheckFlags) do if not transportedUnits[unitID] and not spGetUnitIsDead(unitID) then local velX, velY, velZ, velocityLength = spGetUnitVelocity(unitID) - - if velocityLength > velocityCap then -- Sheer off extreme velocities within acceptable range + if not data.velocityReduced and velocityLength > data.velocityCap then local verticalVelocityCapThreshold = 0.07 --value derived from empirical testing to prevent fall damage and goofy trajectories from impulse local horizontalVelocity = math.sqrt(velX^2 + velZ^2) local newVelY = mathAbs(mathMin(horizontalVelocity * verticalVelocityCapThreshold, velY)) @@ -186,14 +219,15 @@ function gadget:GameFrame(frame) newVelYToOldVelYRatio = 1 end - local scale = velLengthOffsetCap / mathMax(mathAbs(velX), mathAbs(newVelY), mathAbs(velZ)) + local scale = data.velocityCap / mathMax(mathAbs(velX), mathAbs(newVelY), mathAbs(velZ)) velX = velX * scale * newVelYToOldVelYRatio velZ = velZ * scale * newVelYToOldVelYRatio spSetUnitVelocity(unitID, velX, newVelY, velZ) - expirationFrame = frame + velocityWatchFrames - elseif velocityLength > velocitySlowdownThreshold then + data.velocityReduced = true + data.expirationFrame = frame + velocityWatchFrames + elseif launchedUnits[unitID] and velocityLength > velocitySlowdownThreshold then local decelerateHorizontal = 0.98 --Number empirically tested to produce optimal deceleration without looking goofy. local decelerateVertical if velY < 0 then @@ -202,12 +236,14 @@ function gadget:GameFrame(frame) decelerateVertical = 0.92 --Number empirically tested to produce optimal deceleration without looking goofy. end spSetUnitVelocity(unitID, velX * decelerateHorizontal, velY * decelerateVertical, velZ * decelerateHorizontal) - expirationFrame = frame + velocityWatchFrames - elseif expirationFrame < frame then + data.expirationFrame = frame + velocityWatchFrames + elseif data.expirationFrame < frame then unitInertiaCheckFlags[unitID] = nil + launchedUnits[unitID] = nil end else unitInertiaCheckFlags[unitID] = nil + launchedUnits[unitID] = nil end end