diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/DumbProjectileBehavior.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/DumbProjectileBehavior.h index 0a47aee50c2..d77a5cfefd4 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/DumbProjectileBehavior.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/DumbProjectileBehavior.h @@ -96,6 +96,7 @@ class DumbProjectileBehavior : public UpdateModule, public ProjectileUpdateInter virtual Bool projectileHandleCollision( Object *other ); virtual Bool projectileIsArmed() const { return true; } virtual ObjectID projectileGetLauncherID() const { return m_launcherID; } + virtual Bool projectileGetLaunchPos(Coord3D& pos) const { if (m_launcherID == INVALID_ID) return false; pos = m_flightPathStart; return true; } virtual void setFramesTillCountermeasureDiversionOccurs( UnsignedInt frames ) {} virtual void projectileNowJammed() {} virtual Object* getTargetObject(); diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/FreeFallProjectileBehavior.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/FreeFallProjectileBehavior.h index 8c9637c431b..670572bfede 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/FreeFallProjectileBehavior.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/FreeFallProjectileBehavior.h @@ -98,6 +98,7 @@ class FreeFallProjectileBehavior : public UpdateModule, public ProjectileUpdateI virtual Bool projectileHandleCollision( Object *other ); virtual Bool projectileIsArmed() const { return true; } virtual ObjectID projectileGetLauncherID() const { return m_launcherID; } + virtual Bool projectileGetLaunchPos(Coord3D& pos) const { if (m_launcherID == INVALID_ID) return false; pos = m_launchPos; return true; } virtual void setFramesTillCountermeasureDiversionOccurs( UnsignedInt frames ) {} virtual void projectileNowJammed() {} virtual Object* getTargetObject(); @@ -114,6 +115,7 @@ class FreeFallProjectileBehavior : public UpdateModule, public ProjectileUpdateI ObjectID m_launcherID; ///< ID of object that launched us (zero if not yet launched) ObjectID m_victimID; ///< ID of object we are targeting (zero if not yet launched) Coord3D m_targetPos; + Coord3D m_launchPos; ///< launcher's position at launch time (for DamageFactorAtMaxRange) const WeaponTemplate* m_detonationWeaponTmpl; ///< weapon to fire at end (or null) UnsignedInt m_lifespanFrame; ///< if we haven't collided by this frame, blow up anyway WeaponBonusConditionFlags m_extraBonusFlags; diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/MissileAIUpdate.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/MissileAIUpdate.h index 490468cda89..3c8b1a892a0 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/MissileAIUpdate.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/MissileAIUpdate.h @@ -107,6 +107,7 @@ class MissileAIUpdate : public AIUpdateInterface, public ProjectileUpdateInterfa virtual Bool projectileHandleCollision( Object *other ); virtual Bool projectileIsArmed() const { return m_isArmed; } virtual ObjectID projectileGetLauncherID() const { return m_launcherID; } + virtual Bool projectileGetLaunchPos(Coord3D& pos) const { if (m_launcherID == INVALID_ID) return false; pos = m_launchPos; return true; } virtual void setFramesTillCountermeasureDiversionOccurs( UnsignedInt frames ); ///< Number of frames till missile diverts to countermeasures. virtual void projectileNowJammed();///< We lose our Object target and scatter to the ground virtual Object* getTargetObject(); @@ -139,6 +140,7 @@ class MissileAIUpdate : public AIUpdateInterface, public ProjectileUpdateInterfa Real m_maxAccel; Coord3D m_originalTargetPos; ///< When firing uphill, we aim high to clear the brow of the hill. jba. Coord3D m_prevPos; + Coord3D m_launchPos; ///< launcher's position at launch time (for DamageFactorAtMaxRange) WeaponBonusConditionFlags m_extraBonusFlags; const WeaponTemplate* m_detonationWeaponTmpl; ///< weapon to fire at end (or null) const ParticleSystemTemplate* m_exhaustSysTmpl; diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/NeutronMissileUpdate.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/NeutronMissileUpdate.h index 4b2216dd252..e4b6b5de036 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/NeutronMissileUpdate.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/NeutronMissileUpdate.h @@ -99,6 +99,7 @@ class NeutronMissileUpdate : public UpdateModule, virtual void projectileFireAtObjectOrPosition( const Object *victim, const Coord3D *victimPos, const WeaponTemplate *detWeap, const ParticleSystemTemplate* exhaustSysOverride ); virtual Bool projectileIsArmed() const { return m_isArmed; } ///< return true if the missile is armed and ready to explode virtual ObjectID projectileGetLauncherID() const { return m_launcherID; } ///< Return firer of missile. Returns 0 if not yet fired. + virtual Bool projectileGetLaunchPos(Coord3D& pos) const { if (m_launcherID == INVALID_ID) return false; pos = m_launchPos; return true; } ///< launcher's position at launch time (for DamageFactorAtMaxRange) virtual Bool projectileHandleCollision( Object *other ); virtual const Coord3D *getVelocity() const { return &m_vel; } ///< get current velocity virtual void setFramesTillCountermeasureDiversionOccurs( UnsignedInt frames ) {} @@ -114,6 +115,7 @@ class NeutronMissileUpdate : public UpdateModule, MissileStateType m_state; ///< the behavior state of the missile Coord3D m_targetPos; ///< the position of the target Coord3D m_intermedPos; + Coord3D m_launchPos; ///< launcher's position at launch time (for DamageFactorAtMaxRange) ObjectID m_launcherID; ///< ID of object that launched us (zero if not yet launched) WeaponSlotType m_attach_wslot; ///< where to fire the missile from diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/UpdateModule.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/UpdateModule.h index c8988578b35..82af85017d3 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/UpdateModule.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/UpdateModule.h @@ -263,6 +263,7 @@ class ProjectileUpdateInterface virtual void projectileFireAtObjectOrPosition( const Object *victim, const Coord3D *victimPos, const WeaponTemplate *detWeap, const ParticleSystemTemplate* exhaustSysOverride ) = 0; virtual Bool projectileIsArmed() const = 0; ///< return true if the projectile is armed and ready to explode virtual ObjectID projectileGetLauncherID() const = 0; ///< All projectiles need to keep track of their firer + virtual Bool projectileGetLaunchPos(Coord3D& pos) const { return false; } ///< launcher's position at launch time (for DamageFactorAtMaxRange); returns false if unknown virtual Bool projectileHandleCollision(Object *other) = 0; virtual void setFramesTillCountermeasureDiversionOccurs( UnsignedInt frames ) = 0; ///< Number of frames till missile diverts to countermeasures. virtual void projectileNowJammed() = 0; diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Weapon.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Weapon.h index 915da9e0525..0ce75626134 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Weapon.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Weapon.h @@ -494,6 +494,10 @@ class WeaponTemplate : public MemoryPoolObject protected: + // compute the range-based scaling factor (1.0 at point-blank to factorAtMaxRange at attack range) + // for the engagement from 'source' to 'pos'. Uses launch-time position for projectile detonations. + Real computeRangeScaleFactor(const Object* source, const Coord3D* pos, const WeaponBonus& bonus, Real factorAtMaxRange, Bool isProjectileDetonation) const; + // actually deal out the damage. void dealDamageInternal(ObjectID sourceID, ObjectID victimID, const Coord3D *pos, const WeaponBonus& bonus, Bool isProjectileDetonation) const; void trimOldHistoricDamage() const; @@ -508,6 +512,8 @@ class WeaponTemplate : public MemoryPoolObject static void parseWeaponBonusSet( INI* ini, void *instance, void * /*store*/, const void* /*userData*/ ); static void parseScatterTarget( INI* ini, void *instance, void * /*store*/, const void* /*userData*/ ); static void parseShotDelay( INI* ini, void *instance, void * /*store*/, const void* /*userData*/ ); + static void parsePrimaryDamage( INI* ini, void *instance, void * /*store*/, const void* /*userData*/ ); + static void parseSecondaryDamage( INI* ini, void *instance, void * /*store*/, const void* /*userData*/ ); static const FieldParse TheWeaponTemplateFieldParseTable[]; ///< the parse table for INI definition @@ -516,10 +522,17 @@ class WeaponTemplate : public MemoryPoolObject AsciiString m_projectileStreamName; ///< Name of object that tracks are stream, if we have one AsciiString m_laserName; ///< Name of the laser object that persists. AsciiString m_laserBoneName; ///< Where to put the laser object - Real m_primaryDamage; ///< primary damage amount + Real m_primaryDamage; ///< primary damage amount (nominal/max when variance is used) + Real m_primaryDamageVariance; ///< if nonzero, actual primary damage is randomly reduced by up to this much (from Min:/Max: definition) Real m_primaryDamageRadius; ///< primary damage radius range - Real m_secondaryDamage; ///< secondary damage amount + Real m_secondaryDamage; ///< secondary damage amount (nominal/max when variance is used) + Real m_secondaryDamageVariance; ///< if nonzero, actual secondary damage is randomly reduced by up to this much (from Min:/Max: definition) Real m_secondaryDamageRadius; ///< secondary damage radius range + Real m_primaryDamageTaperOff; ///< factor of primary damage applied at the edge of the primary radius (1.0 = no taper) + Real m_secondaryDamageTaperOff; ///< factor of secondary damage applied at the edge of the secondary radius (1.0 = no taper) + Real m_damageFactorAtMaxRange; ///< scales damage based on engagement distance / attack range (1.0 = no scaling) + Real m_radiusFactorAtMaxRange; ///< scales damage radii based on engagement distance / attack range (1.0 = no scaling) + Real m_scatterRadiusFactorAtMaxRange; ///< scales ScatterRadius based on engagement distance / attack range (1.0 = no scaling) Real m_shockWaveAmount; ///( How much shockwave generated Real m_shockWaveRadius; ///( How far shockwave effect affects objects Real m_shockWaveTaperOff; ///( How much shockwave is left at the tip of the shockwave radius diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/FreeFallProjectileBehavior.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/FreeFallProjectileBehavior.cpp index 47401e6bc9a..1138d970ddc 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/FreeFallProjectileBehavior.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/FreeFallProjectileBehavior.cpp @@ -111,6 +111,7 @@ FreeFallProjectileBehavior::FreeFallProjectileBehavior(Thing* thing, const Modul m_launcherID = INVALID_ID; m_victimID = INVALID_ID; m_targetPos.zero(); + m_launchPos.zero(); m_detonationWeaponTmpl = NULL; m_lifespanFrame = 0; m_extraBonusFlags = 0; @@ -142,6 +143,8 @@ void FreeFallProjectileBehavior::projectileLaunchAtObjectOrPosition( DEBUG_ASSERTCRASH(specificBarrelToUse >= 0, ("specificBarrelToUse must now be explicit")); m_launcherID = launcher ? launcher->getID() : INVALID_ID; + if (launcher) + m_launchPos = *launcher->getPosition(); m_extraBonusFlags = launcher ? launcher->getWeaponBonusCondition() : 0; if (d->m_applyLauncherBonus && m_extraBonusFlags != 0) { @@ -428,7 +431,8 @@ void FreeFallProjectileBehavior::xfer(Xfer* xfer) { // version - XferVersion currentVersion = 1; + // 2: Added m_launchPos (for DamageFactorAtMaxRange) + XferVersion currentVersion = 2; XferVersion version = currentVersion; xfer->xferVersion(&version, currentVersion); @@ -444,6 +448,10 @@ void FreeFallProjectileBehavior::xfer(Xfer* xfer) // target pos xfer->xferCoord3D(&m_targetPos); + // launch pos + if (version >= 2) + xfer->xferCoord3D(&m_launchPos); + // weapon template AsciiString weaponTemplateName = AsciiString::TheEmptyString; if (m_detonationWeaponTmpl) diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/MissileAIUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/MissileAIUpdate.cpp index dae4e361093..40cc2cc196d 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/MissileAIUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/MissileAIUpdate.cpp @@ -164,6 +164,7 @@ MissileAIUpdate::MissileAIUpdate( Thing *thing, const ModuleData* moduleData ) : m_exhaustID = INVALID_PARTICLE_SYSTEM_ID; m_extraBonusFlags = 0; m_originalTargetPos.zero(); + m_launchPos.zero(); m_framesTillDecoyed = 0; m_noDamage = FALSE; m_isJammed = FALSE; @@ -225,6 +226,8 @@ void MissileAIUpdate::projectileLaunchAtObjectOrPosition( DEBUG_ASSERTCRASH(specificBarrelToUse>=0, ("specificBarrelToUse must now be explicit")); m_launcherID = launcher ? launcher->getID() : INVALID_ID; + if (launcher) + m_launchPos = *launcher->getPosition(); m_detonationWeaponTmpl = detWeap; m_extraBonusFlags = launcher ? launcher->getWeaponBonusCondition() : 0; @@ -1137,7 +1140,7 @@ void MissileAIUpdate::crc( Xfer *xfer ) void MissileAIUpdate::xfer( Xfer *xfer ) { // version - const XferVersion currentVersion = 7; + const XferVersion currentVersion = 8; XferVersion version = currentVersion; xfer->xferVersion( &version, currentVersion ); @@ -1207,6 +1210,11 @@ void MissileAIUpdate::xfer( Xfer *xfer ) { xfer->xferReal( &m_randomPathDistLeft); } + + if( version >= 8 ) + { + xfer->xferCoord3D( &m_launchPos ); + } } // end xfer // ------------------------------------------------------------------------------------------------ diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/NeutronMissileUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/NeutronMissileUpdate.cpp index 67d477cf4a2..89660dc6962 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/NeutronMissileUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/NeutronMissileUpdate.cpp @@ -111,6 +111,7 @@ NeutronMissileUpdate::NeutronMissileUpdate( Thing *thing, const ModuleData* modu m_targetPos.zero(); m_intermedPos.zero(); + m_launchPos.zero(); m_accel.zero(); m_vel.zero(); @@ -147,6 +148,8 @@ void NeutronMissileUpdate::projectileLaunchAtObjectOrPosition(const Object *vict DEBUG_ASSERTCRASH(specificBarrelToUse>=0, ("specificBarrelToUse must now be explicit")); m_launcherID = launcher ? launcher->getID() : INVALID_ID; + if (launcher) + m_launchPos = *launcher->getPosition(); m_attach_wslot = wslot; m_attach_specificBarrelToUse = specificBarrelToUse; @@ -554,7 +557,8 @@ void NeutronMissileUpdate::xfer( Xfer *xfer ) { // version - XferVersion currentVersion = 1; + // 2: Added m_launchPos (for DamageFactorAtMaxRange) + XferVersion currentVersion = 2; XferVersion version = currentVersion; xfer->xferVersion( &version, currentVersion ); @@ -606,6 +610,10 @@ void NeutronMissileUpdate::xfer( Xfer *xfer ) // height at launch xfer->xferReal( &m_heightAtLaunch ); + // launch pos + if( version >= 2 ) + xfer->xferCoord3D( &m_launchPos ); + // decal, if any m_deliveryDecal.xferRadiusDecal(xfer); diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp index 167a2b3d2c5..71b431001bb 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp @@ -164,10 +164,15 @@ WeaponStore *TheWeaponStore = nullptr; ///< the weapon store definition const FieldParse WeaponTemplate::TheWeaponTemplateFieldParseTable[] = { - { "PrimaryDamage", INI::parseReal, nullptr, offsetof(WeaponTemplate, m_primaryDamage) }, + { "PrimaryDamage", WeaponTemplate::parsePrimaryDamage, nullptr, 0 }, { "PrimaryDamageRadius", INI::parseReal, nullptr, offsetof(WeaponTemplate, m_primaryDamageRadius) }, - { "SecondaryDamage", INI::parseReal, nullptr, offsetof(WeaponTemplate, m_secondaryDamage) }, + { "SecondaryDamage", WeaponTemplate::parseSecondaryDamage, nullptr, 0 }, { "SecondaryDamageRadius", INI::parseReal, nullptr, offsetof(WeaponTemplate, m_secondaryDamageRadius) }, + { "PrimaryDamageTaperOff", INI::parseReal, nullptr, offsetof(WeaponTemplate, m_primaryDamageTaperOff) }, + { "SecondaryDamageTaperOff", INI::parseReal, nullptr, offsetof(WeaponTemplate, m_secondaryDamageTaperOff) }, + { "DamageFactorAtMaxRange", INI::parseReal, nullptr, offsetof(WeaponTemplate, m_damageFactorAtMaxRange) }, + { "RadiusFactorAtMaxRange", INI::parseReal, nullptr, offsetof(WeaponTemplate, m_radiusFactorAtMaxRange) }, + { "ScatterRadiusFactorAtMaxRange", INI::parseReal, nullptr, offsetof(WeaponTemplate, m_scatterRadiusFactorAtMaxRange) }, { "ShockWaveAmount", INI::parseReal, nullptr, offsetof(WeaponTemplate, m_shockWaveAmount) }, { "ShockWaveRadius", INI::parseReal, nullptr, offsetof(WeaponTemplate, m_shockWaveRadius) }, { "ShockWaveTaperOff", INI::parseReal, nullptr, offsetof(WeaponTemplate, m_shockWaveTaperOff) }, @@ -270,9 +275,16 @@ WeaponTemplate::WeaponTemplate() : m_nextTemplate(nullptr) m_name = "NoNameWeapon"; m_nameKey = NAMEKEY_INVALID; m_primaryDamage = 0.0f; + m_primaryDamageVariance = 0.0f; m_primaryDamageRadius = 0.0f; m_secondaryDamage = 0.0f; + m_secondaryDamageVariance = 0.0f; m_secondaryDamageRadius = 0.0f; + m_primaryDamageTaperOff = 1.0f; // no taper + m_secondaryDamageTaperOff = 1.0f; // no taper + m_damageFactorAtMaxRange = 1.0f; // no range scaling + m_radiusFactorAtMaxRange = 1.0f; // no range scaling + m_scatterRadiusFactorAtMaxRange = 1.0f; // no range scaling m_attackRange = 0.0f; m_minimumAttackRange = 0.0f; m_requestAssistRange = 0.0f; @@ -455,6 +467,60 @@ void WeaponTemplate::copy_from(const WeaponTemplate& other) { } +//------------------------------------------------------------------------------------------------- +/** Shared smart parser for PrimaryDamage/SecondaryDamage. Accepts a single number for traditional + fixed damage, or a labeled "Min:x Max:y" pair for a random damage range. The nominal damage is + stored as the max, and the spread (max-min) is stored as the variance, so that the actual damage + dealt is rolled as (max - random[0,variance]) == random[min,max]. Storing the max as nominal + keeps AI damage estimation and UI unchanged (optimistic). */ +//------------------------------------------------------------------------------------------------- +static void parseDamageMinMax( INI* ini, Real* nominal, Real* variance ) +{ + static const char *MIN_LABEL = "Min"; + static const char *MAX_LABEL = "Max"; + + const char* token = ini->getNextTokenOrNull(ini->getSepsColon()); + + if( token != nullptr && stricmp(token, MIN_LABEL) == 0 ) + { + // Two entry min/max + Real minVal = INI::scanReal(ini->getNextToken(ini->getSepsColon())); + Real maxVal = minVal; + token = ini->getNextTokenOrNull(ini->getSepsColon()); + if( token != nullptr && stricmp(token, MAX_LABEL) == 0 ) + maxVal = INI::scanReal(ini->getNextToken(ini->getSepsColon())); + + // guard against reversed Min/Max + if( maxVal < minVal ) + { + Real tmp = maxVal; maxVal = minVal; minVal = tmp; + } + + *nominal = maxVal; + *variance = maxVal - minVal; + } + else + { + // single entry, no label, so the first token is just a number + *nominal = INI::scanReal(token); + *variance = 0.0f; + } +} + +//------------------------------------------------------------------------------------------------- +/*static*/ void WeaponTemplate::parsePrimaryDamage( INI* ini, void *instance, void * /*store*/, const void* /*userData*/ ) +{ + WeaponTemplate* self = (WeaponTemplate*)instance; + parseDamageMinMax( ini, &self->m_primaryDamage, &self->m_primaryDamageVariance ); +} + +//------------------------------------------------------------------------------------------------- +/*static*/ void WeaponTemplate::parseSecondaryDamage( INI* ini, void *instance, void * /*store*/, const void* /*userData*/ ) +{ + WeaponTemplate* self = (WeaponTemplate*)instance; + parseDamageMinMax( ini, &self->m_secondaryDamage, &self->m_secondaryDamageVariance ); +} + //------------------------------------------------------------------------------------------------- void WeaponTemplate::postProcessLoad() { @@ -971,6 +1037,10 @@ UnsignedInt WeaponTemplate::fireWeaponTemplate Bool handled; + // The radius handed to UseCallersRadius FX must match the actual damage radius, so apply the same + // range-based scaling (RadiusFactorAtMaxRange) that dealDamageInternal uses. + Real fxRadius = getPrimaryDamageRadius(bonus) * computeRangeScaleFactor(sourceObj, &targetPos, bonus, m_radiusFactorAtMaxRange, isProjectileDetonation); + // TheSuperHackers @todo: Remove hardcoded KINDOF_MINE check and apply PlayFXWhenStealthed = Yes to the mine weapons instead. if (!sourceObj->isLogicallyVisible() // if user watching cannot see us @@ -989,7 +1059,7 @@ UnsignedInt WeaponTemplate::fireWeaponTemplate reAngle, reDir, &targetPos, - getPrimaryDamageRadius(bonus) + fxRadius ); } @@ -998,7 +1068,7 @@ UnsignedInt WeaponTemplate::fireWeaponTemplate // bah. just play it at the drawable's pos. //DEBUG_LOG(("*** WeaponFireFX not fully handled by the client")); const Coord3D* where = isContactWeapon() ? &targetPos : sourceObj->getDrawable()->getPosition(); - FXList::doFXPos(fx, where, sourceObj->getDrawable()->getTransformMatrix(), getWeaponSpeed(), &targetPos, getPrimaryDamageRadius(bonus)); + FXList::doFXPos(fx, where, sourceObj->getDrawable()->getTransformMatrix(), getWeaponSpeed(), &targetPos, fxRadius); } } @@ -1019,6 +1089,25 @@ UnsignedInt WeaponTemplate::fireWeaponTemplate // and find a random point within the radius to shoot at as victimPos scatterRadius = m_scatterRadius; + // Scale the scatter radius based on engagement distance / attack range. Scaled from 1.0 at + // point-blank to m_scatterRadiusFactorAtMaxRange at (or beyond) the weapon's attack range. + // Note: this scales ScatterRadius only, not the infantry-inaccuracy bonus added below. + if (m_scatterRadiusFactorAtMaxRange != 1.0f) + { + Real range = getAttackRange(bonus); + if (range > 0.0f) + { + Coord3D delta; + delta.x = victimPos->x - sourcePos->x; + delta.y = victimPos->y - sourcePos->y; + delta.z = victimPos->z - sourcePos->z; + Real t = delta.length() / range; + if (t < 0.0f) t = 0.0f; + if (t > 1.0f) t = 1.0f; + scatterRadius *= 1.0f + (m_scatterRadiusFactorAtMaxRange - 1.0f) * t; + } + } + // if it's an object, aim at the center, not the ground part (srj) PathfindLayerEnum targetLayer = LAYER_GROUND; if( victimObj ) @@ -1133,7 +1222,8 @@ UnsignedInt WeaponTemplate::fireWeaponTemplate Vector3 dir(v.x, v.y, v.z); dir.Normalize(); //This is fantastically crucial for calling buildTransformMatrix!!!!! laserMtx.buildTransformMatrix(pos, dir); - FXList::doFXPos(fx, &targetPos, &laserMtx, 0.0f, NULL, getPrimaryDamageRadius(bonus)); + Real fxRadius = getPrimaryDamageRadius(bonus) * computeRangeScaleFactor(sourceObj, &targetPos, bonus, m_radiusFactorAtMaxRange, isProjectileDetonation); + FXList::doFXPos(fx, &targetPos, &laserMtx, 0.0f, NULL, fxRadius); } if( inflictDamage ) @@ -1476,6 +1566,52 @@ void WeaponTemplate::processHistoricDamage(const Object* source, const Coord3D* } #endif +//------------------------------------------------------------------------------------------------- +// Compute the range-based scaling factor (1.0 at point-blank, factorAtMaxRange at/beyond attack range) +// for the engagement from 'source' to 'pos'. The origin is the firing source's position for direct and +// laser weapons; for projectile detonations the firing source is the projectile, so the launcher's +// position captured at launch time (projectileGetLaunchPos) is used instead. Returns 1.0 if the factor +// is unused, the origin is unknown, or the weapon has no attack range. +//------------------------------------------------------------------------------------------------- +Real WeaponTemplate::computeRangeScaleFactor(const Object* source, const Coord3D* pos, const WeaponBonus& bonus, Real factorAtMaxRange, Bool isProjectileDetonation) const +{ + if (factorAtMaxRange == 1.0f || pos == nullptr) + return 1.0f; + + Coord3D fromPos; + Bool haveFromPos = false; + if (isProjectileDetonation && source != nullptr && source->isKindOf(KINDOF_PROJECTILE)) + { + for (BehaviorModule** u = source->getBehaviorModules(); *u; ++u) + { + ProjectileUpdateInterface* pui = (*u)->getProjectileUpdateInterface(); + if (pui != nullptr) + { + haveFromPos = pui->projectileGetLaunchPos(fromPos); + break; + } + } + } + else if (source != nullptr) + { + fromPos = *source->getPosition(); + haveFromPos = true; + } + + Real range = getAttackRange(bonus); + if (!haveFromPos || range <= 0.0f) + return 1.0f; + + Coord3D delta; + delta.x = pos->x - fromPos.x; + delta.y = pos->y - fromPos.y; + delta.z = pos->z - fromPos.z; + Real t = delta.length() / range; + if (t < 0.0f) t = 0.0f; + if (t > 1.0f) t = 1.0f; + return 1.0f + (factorAtMaxRange - 1.0f) * t; +} + //------------------------------------------------------------------------------------------------- void WeaponTemplate::dealDamageInternal(ObjectID sourceID, ObjectID victimID, const Coord3D *pos, const WeaponBonus& bonus, Bool isProjectileDetonation) const { @@ -1513,6 +1649,30 @@ void WeaponTemplate::dealDamageInternal(ObjectID sourceID, ObjectID victimID, co Real secondaryDamage = getSecondaryDamage(bonus); Int affects = getAffectsMask(); + // Apply random damage variance (from Min:/Max: definition). Roll once per shot so that every + // victim caught in the blast takes the same rolled damage. Must use the synchronized game-logic + // RNG so multiplayer clients stay in sync. + const Real damageBonusScalar = bonus.getField(WeaponBonus::DAMAGE); + if (m_primaryDamageVariance > 0.0f) + primaryDamage -= GameLogicRandomValueReal(0.0f, m_primaryDamageVariance * damageBonusScalar); + if (m_secondaryDamageVariance > 0.0f) + secondaryDamage -= GameLogicRandomValueReal(0.0f, m_secondaryDamageVariance * damageBonusScalar); + + // Apply range-based scaling of damage and/or damage radii. Each is scaled from 1.0 at point-blank + // to its factor at (or beyond) the weapon's attack range, based on the engagement distance. + if (m_damageFactorAtMaxRange != 1.0f) + { + Real rangeDamageFactor = computeRangeScaleFactor(source, pos, bonus, m_damageFactorAtMaxRange, isProjectileDetonation); + primaryDamage *= rangeDamageFactor; + secondaryDamage *= rangeDamageFactor; + } + if (m_radiusFactorAtMaxRange != 1.0f) + { + Real rangeRadiusFactor = computeRangeScaleFactor(source, pos, bonus, m_radiusFactorAtMaxRange, isProjectileDetonation); + primaryRadius *= rangeRadiusFactor; + secondaryRadius *= rangeRadiusFactor; + } + DEBUG_ASSERTCRASH(secondaryRadius >= primaryRadius || secondaryRadius == 0.0f, ("secondary radius should be >= primary radius (or zero)")); Real primaryRadiusSqr = sqr(primaryRadius); @@ -1674,7 +1834,33 @@ void WeaponTemplate::dealDamageInternal(ObjectID sourceID, ObjectID victimID, co } // note, don't bother with damage multipliers here... // that's handled internally by the attemptDamage() method. - damageInfo.in.m_amount = (curVictimDistSqr <= primaryRadiusSqr) ? primaryDamage : secondaryDamage; + Real damageAmount; + if (curVictimDistSqr <= primaryRadiusSqr) + { + // inside the primary blast: taper from full damage at the center to + // m_primaryDamageTaperOff at the edge of the primary radius. + damageAmount = primaryDamage; + if (m_primaryDamageTaperOff != 1.0f && primaryRadius > 0.0f) + { + Real t = sqrtf(curVictimDistSqr) / primaryRadius; + if (t > 1.0f) t = 1.0f; + damageAmount *= 1.0f + (m_primaryDamageTaperOff - 1.0f) * t; + } + } + else + { + // in the secondary ring: taper from full secondary damage at the inner edge + // (primary radius) to m_secondaryDamageTaperOff at the outer edge (secondary radius). + damageAmount = secondaryDamage; + if (m_secondaryDamageTaperOff != 1.0f && secondaryRadius > primaryRadius) + { + Real t = (sqrtf(curVictimDistSqr) - primaryRadius) / (secondaryRadius - primaryRadius); + if (t < 0.0f) t = 0.0f; + if (t > 1.0f) t = 1.0f; + damageAmount *= 1.0f + (m_secondaryDamageTaperOff - 1.0f) * t; + } + } + damageInfo.in.m_amount = damageAmount; if( killSelf ) {