Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[New Feature] Crusher Level System #1455

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CREDITS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
55 changes: 55 additions & 0 deletions docs/New-or-Enhanced-Logics.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/Whats-New.md
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,7 @@ New:
- `<Player @ X>` 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)
Expand Down
15 changes: 15 additions & 0 deletions src/Ext/Rules/Body.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
;
}

Expand Down
16 changes: 16 additions & 0 deletions src/Ext/Rules/Body.h
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,14 @@ class RulesExt
Valueable<int> CombatLightDetailLevel;
Valueable<int> LightFlashAlphaImageDetailLevel;

Valueable<bool> CrusherLevelEnabled;
Valueable<bool> CrusherLevelEnabled_For1x1Buildings;
Valueable<int> CrusherLevel_Defaults_Crusher;
Valueable<int> CrusherLevel_Defaults_OmniCrusher;
Valueable<int> CrushableLevel_Defaults_Uncrushable_Infantry;
Valueable<int> CrushableLevel_Defaults_Uncrushable_Others;
Valueable<int> CrushableLevel_Defaults_OmniCrushResistant;

ExtData(RulesClass* OwnerObject) : Extension<RulesClass>(OwnerObject)
, Storage_TiberiumIndex { -1 }
, InfantryGainSelfHealCap {}
Expand Down Expand Up @@ -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;
Expand Down
84 changes: 84 additions & 0 deletions src/Ext/TechnoType/Body.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<InfantryClass*>(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<InfantryTypeClass*>(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
{
Expand Down Expand Up @@ -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");

Expand Down Expand Up @@ -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)

Expand Down
11 changes: 11 additions & 0 deletions src/Ext/TechnoType/Body.h
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,10 @@ class TechnoTypeExt
Valueable<double> CrushOverlayExtraForwardTilt;
Valueable<double> CrushSlowdownMultiplier;

Nullable<int> CrusherLevel;
Nullable<int> CrushableLevel;
Nullable<int> DeployedCrushableLevel;

Valueable<bool> DigitalDisplay_Disable;
ValueableVector<DigitalDisplayTypeClass*> DigitalDisplayTypes;

Expand Down Expand Up @@ -405,6 +409,10 @@ class TechnoTypeExt
, CrushForwardTiltPerFrame {}
, CrushOverlayExtraForwardTilt { 0.02 }

, CrusherLevel {}
, CrushableLevel {}
, DeployedCrushableLevel {}

, DigitalDisplay_Disable { false }
, DigitalDisplayTypes {}

Expand Down Expand Up @@ -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;

Expand Down
28 changes: 28 additions & 0 deletions src/Ext/Unit/Hooks.Crushing.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include <DriveLocomotionClass.h>
#include <ShipLocomotionClass.h>
#include <UnitClass.h>
#include <OverlayTypeClass.h>

#include <Ext/TechnoType/Body.h>
#include <Utilities/Macro.h>
Expand Down Expand Up @@ -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<BuildingTypeClass*>(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;
}
Loading