From 9b82335b3eed08c4035870f0ca91230b546957f2 Mon Sep 17 00:00:00 2001 From: Aephiex <34618932+Aephiex@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:13:49 +0800 Subject: [PATCH] crusher level system --- CREDITS.md | 1 + docs/New-or-Enhanced-Logics.md | 55 +++++++++++++++++++++ docs/Whats-New.md | 1 + src/Ext/Rules/Body.cpp | 15 ++++++ src/Ext/Rules/Body.h | 16 +++++++ src/Ext/TechnoType/Body.cpp | 84 +++++++++++++++++++++++++++++++++ src/Ext/TechnoType/Body.h | 11 +++++ src/Ext/Unit/Hooks.Crushing.cpp | 28 +++++++++++ 8 files changed, 211 insertions(+) diff --git a/CREDITS.md b/CREDITS.md index f977b2a3ed..098f22fd90 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -408,3 +408,4 @@ This page lists all the individual contributions to the project by their author. - **Damfoos** - extensive and thorough testing - **Dmitry Volkov** - extensive and thorough testing - **Rise of the East community** - extensive playtesting of in-dev features +- **Aephiex** - crusher level system \ No newline at end of file diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index b789131486..5d469c958e 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -1366,6 +1366,61 @@ Convert.HumanToComputer = ; TechnoType Convert.ComputerToHuman = ; TechnoType ``` +### Crusher level and crushable level + +- A techno can now be specified with a `CrusherLevel=` and `CrushableLevel=` akin to that of successing CNC titles. This feature completely takes over the crush check and must be turned on manually by `[General]â–ºCrusherLevelEnabled=true` before it can take any effect. +- A unit can crush something if its `CrusherLevel` is greater than the latter's `CrushableLevel`. If not set, the default value will be taken from `[General]` settings. The default values of the `[General]` settings themselves follow the convention of *[Command and Conquer 3: Tiberium Wars](https://cnc-central.fandom.com/wiki/Command_%26_Conquer_3:_Tiberium_Wars)* and *[Kane's Wrath](https://cnc-central.fandom.com/wiki/Command_%26_Conquer_3:_Kane%27s_Wrath)*. + - `CrusherLevel=`: + - 0 if `Crusher=no` + - 1 if `Crusher=yes` and `OmniCrusher=no` + - 3 if `Crusher=yes` and `OmniCrusher=yes` + - `CrushableLevel=`: + - 0 if `Crushable=yes` + - 1 if `Crushable=no` and `OmniCrushResistant=no` and is an Infantry + - 2 if `Crushable=no` and `OmniCrushResistant=no` and is NOT an Infantry + - 3 if `Crushable=no` and `OmniCrushResistant=yes` + - `DeployedCrushableLevel=`: + - The same value as `CrushableLevel` if it was set + - 0 if `Crushable=yes` and `DeployedCrushable=yes` + - 1 if `Crushable=yes`, `DeployedCrushable=no`, and `OmniCrushResistant=no` + - 3 if `Crushable=yes`, `DeployedCrushable=no`, and `OmniCrushResistant=yes` + - Here is a quick lookup of the default values of `CrusherLevel` and `CrushableLevel` for Yuri's Revenge units: + - Conscript: 0/0 + - Tesla Trooper: 0/1 + - Guardian G.I.: 0/0 when undeployed, 0/1 when deployed + - T-Rex: 0/3 + - IFV: 0/2 + - Rhino Tank: 1/2 + - Slave Miner: 1/3 + - Battle Fortress: 3/3 + - A few applications of the crusher level system: + - At 2/2, a vehicle can crush Tesla Troopers and deployed Guardian G.I.s, but it can't crush IFVs and is still crushable by Battle Fortresses, just like a [Scorpion Tank](https://cnc-central.fandom.com/wiki/Scorpion_tank_(Tiberium_Wars)) does with the Dozer blades upgrade. + - At 4/4, a vehicle can crush almost anything else, even Battle Fortresses, just like a [MARV](https://cnc-central.fandom.com/wiki/Mammoth_Armored_Reclamation_Vehicle) does. +- Other usage notes: + - A unit must has a locomotor that supports crushing before it can crush something. Most naval units don't, save for the amphibious transports. + - In an unmodded game, it doesn't even try to check if it can crush something if it has `Crusher=no`, meaning `OmniCrusher=yes` make no sense on a unit with `Crusher=no`. This behavior isn't changed by this feature, meaning you will still need `Crusher=yes` for a positive `CrushableLevel` to function. + - In an unmodded game, infantries can never crush anything regardless of `Crusher=yes` or locomotors. This behavior isn't changed by this feature, meaning a positive `CrusherLevel` makes no sense on an infantry type. + - If `CrushableLevel` is set, `Crushable`, `OmniCrushResistant`, and `DeployedCrushable` are redundant and ignored. Use `DeployedCrushableLevel` instead if you wish the infantry to have a different crushable level when deployed. + - If `CrushableLevel` is unset, `DeployedCrushableLevel` does not apply at all. +- A building with 1x1 foundation can be made crushable, however they have `Crushable=no` and `OmniCrushResistant=yes` by default, meaning they can't be crushed by normal means. The crusher level system does not apply to buildings by default, and it must be turned on manually by `[General]â–ºCrusherLevelEnabled.For1x1Buildings=true`. Note that crushing buildings may cause unexpected behavior of the game, such as crushing a Bridge Repair Hut can render the bridge irrepairable. + +In `rulesmd.ini` +```ini +[General] +CrusherLevelEnabled=false ; boolean +CrusherLevelEnabled.For1x1Buildings=false ; boolean +CrusherLevel.Defaults.Crusher=1 ; integer +CrusherLevel.Defaults.OmniCrusher=3 ; integer +CrushableLevel.Defaults.Uncrushable.Infantry=1 ; integer +CrushableLevel.Defaults.Uncrushable.Others=2 ; integer +CrushableLevel.Defaults.OmniCrushResistant=3 ; integer + +[SOMETECHNO] ; TechnoType +CrusherLevel= ; integer +CrushableLevel= ; integer +DeployedCrushableLevel= ; integer; this only works for [InfantryTypes] +``` + ## Terrain ### Destroy animation & sound diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 0b6f8c19c6..cdb4096960 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -470,6 +470,7 @@ New: - `` can now be used as owner for pre-placed objects on skirmish and multiplayer maps (by Starkku) - Allow customizing charge turret delays per burst on a weapon (by Starkku) - Unit `Speed` setting now accepts floating point values (by Starkku) +- Crusher level system (by Aephiex) Vanilla fixes: - Allow AI to repair structures built from base nodes/trigger action 125/SW delivery in single player missions (by Trsdy) diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index bc72f69570..a95e06fe45 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -207,6 +207,14 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) this->CombatLightDetailLevel.Read(exINI, GameStrings::AudioVisual, "CombatLightDetailLevel"); this->LightFlashAlphaImageDetailLevel.Read(exINI, GameStrings::AudioVisual, "LightFlashAlphaImageDetailLevel"); + this->CrusherLevelEnabled.Read(exINI, GameStrings::General, "CrusherLevelEnabled"); + this->CrusherLevelEnabled_For1x1Buildings.Read(exINI, GameStrings::General, "CrusherLevelEnabled.For1x1Buildings"); + this->CrusherLevel_Defaults_Crusher.Read(exINI, GameStrings::General, "CrusherLevel.Defaults.Crusher"); + this->CrusherLevel_Defaults_OmniCrusher.Read(exINI, GameStrings::General, "CrusherLevel.Defaults.OmniCrusher"); + this->CrushableLevel_Defaults_Uncrushable_Infantry.Read(exINI, GameStrings::General, "CrushableLevel.Defaults.Uncrushable.Infantry"); + this->CrushableLevel_Defaults_Uncrushable_Others.Read(exINI, GameStrings::General, "CrushableLevel.Defaults.Uncrushable.Others"); + this->CrushableLevel_Defaults_OmniCrushResistant.Read(exINI, GameStrings::General, "CrushableLevel.Defaults.OmniCrushResistant"); + // Section AITargetTypes int itemsCount = pINI->GetKeyCount("AITargetTypes"); for (int i = 0; i < itemsCount; ++i) @@ -389,6 +397,13 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->WarheadParticleAlphaImageIsLightFlash) .Process(this->CombatLightDetailLevel) .Process(this->LightFlashAlphaImageDetailLevel) + .Process(this->CrusherLevelEnabled) + .Process(this->CrusherLevelEnabled_For1x1Buildings) + .Process(this->CrusherLevel_Defaults_Crusher) + .Process(this->CrusherLevel_Defaults_OmniCrusher) + .Process(this->CrushableLevel_Defaults_Uncrushable_Infantry) + .Process(this->CrushableLevel_Defaults_Uncrushable_Others) + .Process(this->CrushableLevel_Defaults_OmniCrushResistant) ; } diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index b2cce2d4cd..ed5165a5a7 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -165,6 +165,14 @@ class RulesExt Valueable CombatLightDetailLevel; Valueable LightFlashAlphaImageDetailLevel; + Valueable CrusherLevelEnabled; + Valueable CrusherLevelEnabled_For1x1Buildings; + Valueable CrusherLevel_Defaults_Crusher; + Valueable CrusherLevel_Defaults_OmniCrusher; + Valueable CrushableLevel_Defaults_Uncrushable_Infantry; + Valueable CrushableLevel_Defaults_Uncrushable_Others; + Valueable CrushableLevel_Defaults_OmniCrushResistant; + ExtData(RulesClass* OwnerObject) : Extension(OwnerObject) , Storage_TiberiumIndex { -1 } , InfantryGainSelfHealCap {} @@ -284,6 +292,14 @@ class RulesExt , WarheadParticleAlphaImageIsLightFlash { false } , CombatLightDetailLevel { 0 } , LightFlashAlphaImageDetailLevel { 0 } + + , CrusherLevelEnabled { false } + , CrusherLevelEnabled_For1x1Buildings { false } + , CrusherLevel_Defaults_Crusher { 1 } + , CrusherLevel_Defaults_OmniCrusher { 3 } + , CrushableLevel_Defaults_Uncrushable_Infantry { 1 } + , CrushableLevel_Defaults_Uncrushable_Others { 2 } + , CrushableLevel_Defaults_OmniCrushResistant { 3 } { } virtual ~ExtData() = default; diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index 0091ceb22e..24cd2b8d15 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -32,6 +32,82 @@ void TechnoTypeExt::ExtData::ApplyTurretOffset(Matrix3D* mtx, double factor) mtx->Translate(x, y, z); } +// This function is called upon a potential crusher's perspective and returns the crusher level of it. +// This function is intended to use only on a unit with "Crusher=yes". +int TechnoTypeExt::ExtData::GetCrusherLevel(FootClass* pCrusher) +{ + // Returns the CrusherLevel if explictly set. + if (this->CrusherLevel.isset()) + { + return this->CrusherLevel.Get(0); + } + + // Otherwise, gets a default value for CrusherLevel. + return pCrusher->GetTechnoType()->OmniCrusher ? + RulesExt::Global()->CrusherLevel_Defaults_OmniCrusher : + RulesExt::Global()->CrusherLevel_Defaults_Crusher; +} + +// This function is called upon a potential crushing victim's perspective and returns the crushable level of it. +// This function is intended to use only on an infantry, a unit, or an overlay. +// Passing anything else such as terrain types into this function can cause a game crash. +int TechnoTypeExt::ExtData::GetCrushableLevel(FootClass* pVictim) +{ + // If this techno is infantry: + if (auto const pVictimInfantry = abstract_cast(pVictim)) + { + // Returns the CrushableLevel if explictly set. + // Respects the "DeployedCrushableLevel=" setting if the infantry is deployed. + // Unlike the unmodded game, where you cannot tell an infantry is "Crushable=no" and "DeployableCrushable=yes", + // here you may have an infantry come with a lower CrushableLevel when deployed. + if (this->CrushableLevel.isset()) + { + if (pVictimInfantry->IsDeployed()) + { + return this->DeployedCrushableLevel.Get(this->CrushableLevel.Get(0)); + } + else + { + return this->CrushableLevel.Get(0); + } + } + + // Otherwise, gets a default value for CrushableLevel. + // If the InfantryType has "Crushable=yes", and it doesn't have "DeployedCrushable=no" and is deployed, then it can always be crushed. + // Note that in base game logic, "OmniCrushResistant=yes" only prevents "OmniCrusher=yes", it does not prevent "Crusher=yes". + // There fore, "Crushable=yes" and "OmniCrushResistant=yes" can still be crushed by "Crusher=yes" and "OmniCrusher=yes". + // Plus the fact that "OmniCrusher=yes" requires "Crusher=yes" to function, + // I'm ignoring the "OmniCrushResistant=" entry if "Crushable=yes" in the first place. + auto const pVictimInfTypeClass = abstract_cast(pVictimInfantry->GetTechnoType()); + if (!pVictimInfTypeClass->Crushable || (!pVictimInfTypeClass->DeployedCrushable && pVictimInfantry->IsDeployed())) + { + return pVictimInfTypeClass->OmniCrushResistant ? + RulesExt::Global()->CrushableLevel_Defaults_OmniCrushResistant : + RulesExt::Global()->CrushableLevel_Defaults_Uncrushable_Infantry; + } + } + + // If this is something else: + else + { + // Returns the CrushableLevel if explictly set. + if (this->CrushableLevel.isset()) + { + return this->CrushableLevel.Get(0); + } + // Otherwise, gets a default value for CrushableLevel. + // If this is explictly set as "Crushable=yes" then regard it crushable. + if (!pVictim->GetTechnoType()->Crushable) + { + return pVictim->GetTechnoType()->OmniCrushResistant ? + RulesExt::Global()->CrushableLevel_Defaults_OmniCrushResistant : + RulesExt::Global()->CrushableLevel_Defaults_Uncrushable_Others; + } + } + + return 0; +} + // Ares 0.A source const char* TechnoTypeExt::ExtData::GetSelectionGroupID() const { @@ -414,6 +490,10 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->CrushOverlayExtraForwardTilt.Read(exINI, pSection, "CrushOverlayExtraForwardTilt"); this->CrushSlowdownMultiplier.Read(exINI, pSection, "CrushSlowdownMultiplier"); + this->CrusherLevel.Read(exINI, pSection, "CrusherLevel"); + this->CrushableLevel.Read(exINI, pSection, "CrushableLevel"); + this->DeployedCrushableLevel.Read(exINI, pSection, "DeployedCrushableLevel"); + this->DigitalDisplay_Disable.Read(exINI, pSection, "DigitalDisplay.Disable"); this->DigitalDisplayTypes.Read(exINI, pSection, "DigitalDisplayTypes"); @@ -781,6 +861,10 @@ void TechnoTypeExt::ExtData::Serialize(T& Stm) .Process(this->CrushOverlayExtraForwardTilt) .Process(this->CrushSlowdownMultiplier) + .Process(this->CrusherLevel) + .Process(this->CrushableLevel) + .Process(this->DeployedCrushableLevel) + .Process(this->DigitalDisplay_Disable) .Process(this->DigitalDisplayTypes) diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index 3335acac05..6679de8927 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -183,6 +183,10 @@ class TechnoTypeExt Valueable CrushOverlayExtraForwardTilt; Valueable CrushSlowdownMultiplier; + Nullable CrusherLevel; + Nullable CrushableLevel; + Nullable DeployedCrushableLevel; + Valueable DigitalDisplay_Disable; ValueableVector DigitalDisplayTypes; @@ -405,6 +409,10 @@ class TechnoTypeExt , CrushForwardTiltPerFrame {} , CrushOverlayExtraForwardTilt { 0.02 } + , CrusherLevel {} + , CrushableLevel {} + , DeployedCrushableLevel {} + , DigitalDisplay_Disable { false } , DigitalDisplayTypes {} @@ -465,6 +473,9 @@ class TechnoTypeExt void ApplyTurretOffset(Matrix3D* mtx, double factor = 1.0); + int GetCrusherLevel(FootClass* pCrusher); + int GetCrushableLevel(FootClass* pVictim); + // Ares 0.A const char* GetSelectionGroupID() const; diff --git a/src/Ext/Unit/Hooks.Crushing.cpp b/src/Ext/Unit/Hooks.Crushing.cpp index 978a4f8bb7..3b6e935d7c 100644 --- a/src/Ext/Unit/Hooks.Crushing.cpp +++ b/src/Ext/Unit/Hooks.Crushing.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include @@ -99,3 +100,30 @@ DEFINE_HOOK(0x6A108D, ShipLocomotionClass_WhileMoving_CrushTilt, 0xD) return SkipGameCode; } + +DEFINE_HOOK(0x5F6CE0, FootClass_CanGetCrushed_Hook, 6) +{ + enum { CanCrush = 0x5F6D85, CannotCrush = 0x5F6D8C }; + + GET(FootClass* const, pCrusher, EDI); + GET(FootClass* const, pVictim, ESI); + + // An eligible crusher must be a unit with "Crusher=yes". + // An eligible victim must be either an infantry, a unit, an overlay, or a building with 1x1 foundation. + if (RulesExt::Global()->CrusherLevelEnabled && + pCrusher && pCrusher->WhatAmI() == AbstractType::Unit && pCrusher->GetTechnoType()->Crusher && + pVictim && (pVictim->WhatAmI() == AbstractType::Infantry || + pVictim->WhatAmI() == AbstractType::Unit || + pVictim->WhatAmI() == AbstractType::Overlay || + (RulesExt::Global()->CrusherLevelEnabled_For1x1Buildings && pVictim->WhatAmI() == AbstractType::Building && + abstract_cast(pVictim->GetTechnoType())->Foundation == Foundation::_1x1))) + { + auto pCrusherExt = TechnoTypeExt::ExtMap.Find(pCrusher->GetTechnoType()); + auto pVictimExt = TechnoTypeExt::ExtMap.Find(pVictim->GetTechnoType()); + int crusherLevel = pCrusherExt->GetCrusherLevel(pCrusher); + int crushableLevel = pVictimExt->GetCrushableLevel(pVictim); + return crusherLevel > crushableLevel ? CanCrush : CannotCrush; + } + + return 0; +}