Skip to content

Commit

Permalink
Damage frames & crumbling animation for TerrainTypes
Browse files Browse the repository at this point in the history
  • Loading branch information
Starkku committed May 2, 2024
1 parent 0888d51 commit ba9226a
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 11 deletions.
2 changes: 1 addition & 1 deletion YRpp
Submodule YRpp updated 2 files
+3 −0 MapClass.h
+1 −1 TerrainClass.h
33 changes: 32 additions & 1 deletion docs/Fixed-or-Improved-Logics.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ This page describes all ingame logics that are fixed or improved in Phobos witho
![Waving trees](_static/images/tree-shake.gif)
*Animated trees used in [Ion Shock](https://www.moddb.com/mods/tiberian-war-ionshock)*

- `IsAnimated`, `AnimationRate` and `AnimationProbability` now work on TerrainTypes without `SpawnsTiberium` set to true. Note that this might impact performance.
- Fixed transports recursively put into each other not having a correct killer set after second transport when being killed by something.

![image](_static/images/translucency-fix.png)
Expand Down Expand Up @@ -734,6 +733,17 @@ ForbidParallelAIQueues.Building=no ; boolean

## Terrains

### Animated TerrainTypes

- By default `IsAnimated`, `AnimationRate` and `AnimationProbability` only work on TerrainTypes with `SpawnsTiberium` set to true. This restriction has now been lifted.
- Length of the animation can now be customized by setting `AnimationLength` as well, defaulting to half (or quarter if [damaged frames](#damaged-frames-and-crumbling-animation) are enabled) the number of frames in TerrainType's image.

In `rulesmd.ini`:
```ini
[SOMETERRAINTYPE] ; TerrainType
AnimationLength= ; integer, number of frames
```

### Customizable ore spawners

![image](_static/images/ore-01.png)
Expand All @@ -752,6 +762,27 @@ SpawnsTiberium.GrowthStage=3 ; integer - single or comma-sep. range
SpawnsTiberium.CellsPerAnim=1 ; integer - single or comma-sep. range
```

### Damaged frames and crumbling animation

- By default game shows damage frame only for TerrainTypes alive at only 1 point of health left. Because none of the original game TerrainType assets were made with this in mind, the logic is now locked behind a new key `HasDamagedFrames`.
- Instead of showing at 1 point of HP left, TerrainTypes switch to damaged frames once their health reaches `[AudioVisual]` -> `ConditionYellow.Terrain` percentage of their maximum health. Defaults to `ConditionYellow` if not set.
- In addition, TerrainTypes can now show 'crumbling' animation after their health has reached zero and before they are deleted from the map by setting `HasCrumblingFrames` to true.
- Crumbling frames start from first frame after both regular & damaged frames and ends at halfway point of the frames in TerrainType's image.
- Note that the number of regular & damage frames considered for this depends on value of `HasDamagedFrames` and for `IsAnimated` TerrainTypes, `AnimationLength` (see [Animated TerrainTypes](#animated-terraintypes). Exercise caution and ensure there are correct amount of frames to display.
- Sound event from `CrumblingSound` (if set) is played when crumbling animation starts playing.
- [Destroy animation & sound](New-or-Enhanced-Logics.md#destroy-animation-sound) only play after crumbling animation has finished.

In `rulesmd.ini`:
```ini
[AudioVisual]
ConditionYellow.Terrain= ; floating-point value

[SOMETERRAINTYPE] ; TerrainType
HasDamagedFrames=false ; boolean
HasCrumblingFrames=false
CrumblingSound= ; Sound
```

### Minimap color customization

- TerrainTypes can now be made to display on minimap with different colors by setting `MinimapColor`.
Expand Down
1 change: 1 addition & 0 deletions docs/Whats-New.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ New:
- Allow upgrade animations to use `Powered` & `PoweredLight/Effect/Special` keys (by Starkku)
- Toggle for `Explodes=true` BuildingTypes to not explode during buildup or being sold (by Starkku)
- Toggleable height-based shadow scaling for voxel air units (by Trsdy & Starkku)
- Customizable damage & 'crumbling' (destruction) frames for TerrainTypes (by Starkku)
Vanilla fixes:
- Allow AI to repair structures built from base nodes/trigger action 125/SW delivery in single player missions (by Trsdy)
Expand Down
2 changes: 2 additions & 0 deletions src/Ext/Rules/Body.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI)
this->PlacementPreview.Read(exINI, GameStrings::AudioVisual, "PlacementPreview");
this->PlacementPreview_Translucency.Read(exINI, GameStrings::AudioVisual, "PlacementPreview.Translucency");

this->ConditionYellow_Terrain.Read(exINI, GameStrings::AudioVisual, "ConditionYellow.Terrain");
this->Shield_ConditionYellow.Read(exINI, GameStrings::AudioVisual, "Shield.ConditionYellow");
this->Shield_ConditionRed.Read(exINI, GameStrings::AudioVisual, "Shield.ConditionRed");
this->Pips_Shield.Read(exINI, GameStrings::AudioVisual, "Pips.Shield");
Expand Down Expand Up @@ -246,6 +247,7 @@ void RulesExt::ExtData::Serialize(T& Stm)
.Process(this->PlacementGrid_TranslucencyWithPreview)
.Process(this->PlacementPreview)
.Process(this->PlacementPreview_Translucency)
.Process(this->ConditionYellow_Terrain)
.Process(this->Shield_ConditionYellow)
.Process(this->Shield_ConditionRed)
.Process(this->Pips_Shield)
Expand Down
1 change: 1 addition & 0 deletions src/Ext/Rules/Body.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class RulesExt
Valueable<bool> PlacementPreview;
TranslucencyLevel PlacementPreview_Translucency;

Nullable<double> ConditionYellow_Terrain;
Nullable<double> Shield_ConditionYellow;
Nullable<double> Shield_ConditionRed;
Valueable<Vector3D<int>> Pips_Shield;
Expand Down
18 changes: 18 additions & 0 deletions src/Ext/TerrainType/Body.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "Body.h"

#include <AnimClass.h>
#include <TacticalClass.h>
#include <TerrainClass.h>
#include <TerrainTypeClass.h>
Expand All @@ -18,6 +19,14 @@ int TerrainTypeExt::ExtData::GetCellsPerAnim()
return GeneralUtils::GetRangedRandomOrSingleValue(this->SpawnsTiberium_CellsPerAnim.Get());
}

void TerrainTypeExt::ExtData::PlayDestroyEffects(CoordStruct coords)
{
VocClass::PlayIndexAtPos(this->DestroySound.Get(-1), coords);

if (auto const pAnimType = this->DestroyAnim.Get(nullptr))
GameCreate<AnimClass>(pAnimType, coords);
}

void TerrainTypeExt::Remove(TerrainClass* pTerrain)
{
if (!pTerrain)
Expand Down Expand Up @@ -46,6 +55,10 @@ void TerrainTypeExt::ExtData::Serialize(T& Stm)
.Process(this->MinimapColor)
.Process(this->IsPassable)
.Process(this->CanBeBuiltOn)
.Process(this->HasDamagedFrames)
.Process(this->HasCrumblingFrames)
.Process(this->CrumblingSound)
.Process(this->AnimationLength)
;
}

Expand All @@ -71,6 +84,11 @@ void TerrainTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI)
this->IsPassable.Read(exINI, pSection, "IsPassable");
this->CanBeBuiltOn.Read(exINI, pSection, "CanBeBuiltOn");

this->HasDamagedFrames.Read(exINI, pSection, "HasDamagedFrames");
this->HasCrumblingFrames.Read(exINI, pSection, "HasCrumblingFrames");
this->CrumblingSound.Read(exINI, pSection, "CrumblingSound");
this->AnimationLength.Read(exINI, pSection, "AnimationLength");

//Strength is already part of ObjecTypeClass::ReadIni Duh!
//this->TerrainStrength.Read(exINI, pSection, "Strength");
}
Expand Down
9 changes: 9 additions & 0 deletions src/Ext/TerrainType/Body.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ class TerrainTypeExt
Nullable<ColorStruct> MinimapColor;
Valueable<bool> IsPassable;
Valueable<bool> CanBeBuiltOn;
Valueable<bool> HasDamagedFrames;
Valueable<bool> HasCrumblingFrames;
NullableIdx<VocClass> CrumblingSound;
Nullable<int> AnimationLength;

ExtData(TerrainTypeClass* OwnerObject) : Extension<TerrainTypeClass>(OwnerObject)
, SpawnsTiberium_Type { 0 }
Expand All @@ -37,6 +41,10 @@ class TerrainTypeExt
, MinimapColor {}
, IsPassable { false }
, CanBeBuiltOn { false }
, HasDamagedFrames { false }
, HasCrumblingFrames { false }
, CrumblingSound {}
, AnimationLength {}
{ }

virtual ~ExtData() = default;
Expand All @@ -50,6 +58,7 @@ class TerrainTypeExt

int GetTiberiumGrowthStage();
int GetCellsPerAnim();
void PlayDestroyEffects(CoordStruct coords);

private:
template <typename T>
Expand Down
136 changes: 127 additions & 9 deletions src/Ext/TerrainType/Hooks.cpp
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
#include "Body.h"

#include <ScenarioClass.h>
#include <SpecificStructures.h>
#include <TacticalClass.h>
#include <TiberiumClass.h>
#include <TerrainClass.h>
#include <SpecificStructures.h>
#include <AnimClass.h>

#include <Ext/Rules/Body.h>
#include <Utilities/GeneralUtils.h>

namespace TerrainTypeTemp
{
TerrainTypeClass* pCurrentType = nullptr;
TerrainTypeExt::ExtData* pCurrentExt = nullptr;
double PriorHealthRatio = 0.0;
}

DEFINE_HOOK(0x71C84D, TerrainClass_AI_Animated, 0x6)
Expand All @@ -22,15 +24,16 @@ DEFINE_HOOK(0x71C84D, TerrainClass_AI_Animated, 0x6)

if (pThis->Type->IsAnimated)
{
if (pThis->Animation.Value == pThis->Type->GetImage()->Frames / 2)
auto const pTypeExt = TerrainTypeExt::ExtMap.Find(pThis->Type);

if (pThis->Animation.Value == pTypeExt->AnimationLength.Get(pThis->Type->GetImage()->Frames / (2 * (pTypeExt->HasDamagedFrames + 1))))
{
pThis->Animation.Value = 0;
pThis->Animation.Start(0);

// Spawn tiberium if enabled.
if (pThis->Type->SpawnsTiberium)
{
auto const pTypeExt = TerrainTypeExt::ExtMap.Find(pThis->Type);
auto pCell = pThis->GetCell();
int cellCount = pTypeExt->GetCellsPerAnim();

Expand All @@ -51,6 +54,68 @@ DEFINE_HOOK(0x71C84D, TerrainClass_AI_Animated, 0x6)
return SkipGameCode;
}

DEFINE_HOOK(0x71C812, TerrainClass_AI_Crumbling, 0x6)
{
enum { ReturnFromFunction = 0x71C839, SkipCheck = 0x71C7C2 };

GET(TerrainClass*, pThis, ESI);

auto const pTypeExt = TerrainTypeExt::ExtMap.Find(pThis->Type);

if (pTypeExt->HasDamagedFrames && pThis->Health > 0)
{
if (!pThis->Type->IsAnimated && !pThis->Type->IsFlammable)
MapClass::Instance->Logics.get().Remove(pThis);

pThis->IsCrumbling = false;

return SkipCheck;
}

int animationLength = pTypeExt->AnimationLength.Get(pThis->Type->GetImage()->Frames / (2 * (pTypeExt->HasDamagedFrames + 1)));
int currentStage = pThis->Animation.Value + (pThis->Type->IsAnimated ? animationLength * (pTypeExt->HasDamagedFrames + 1) : 0 + pTypeExt->HasDamagedFrames);

if (currentStage + 1 == pThis->Type->GetImage()->Frames / 2)
{
pTypeExt->PlayDestroyEffects(pThis->GetCoords());
TerrainTypeExt::Remove(pThis);
}

return ReturnFromFunction;
}

DEFINE_HOOK(0x71C1FE, TerrainClass_Draw_PickFrame, 0x6)
{
enum { SkipGameCode = 0x71C234 };

GET(int, frame, EBX);

GET(TerrainClass*, pThis, ESI);

auto const pTypeExt = TerrainTypeExt::ExtMap.Find(pThis->Type);
bool isDamaged = pTypeExt->HasDamagedFrames && pThis->GetHealthPercentage() <= RulesExt::Global()->ConditionYellow_Terrain.Get(RulesClass::Instance->ConditionYellow);

if (pThis->Type->IsAnimated)
{
int animLength = pTypeExt->AnimationLength.Get(pThis->Type->GetImage()->Frames / (2 * (pTypeExt->HasDamagedFrames + 1)));

if (pTypeExt->HasCrumblingFrames && pThis->IsCrumbling)
frame = (animLength * (pTypeExt->HasDamagedFrames + 1)) + 1 + pThis->Animation.Value;
else
frame = pThis->Animation.Value + (isDamaged * animLength);
}
else
{
if (pTypeExt->HasCrumblingFrames && pThis->IsCrumbling)
frame = 1 + pThis->Animation.Value;
else if (isDamaged)
frame = 1;
}

R->EBX(frame);
return SkipGameCode;
}

// Overrides Ares hook at 0x5F4FF9, required for animated terrain cause game & Ares check SpawnsTiberium instead of IsAnimated
DEFINE_HOOK(0x5F4FEF, ObjectClass_Unlimbo_UpdateTerrain, 0x6)
{
Expand Down Expand Up @@ -122,22 +187,74 @@ DEFINE_HOOK(0x48381D, CellClass_SpreadTiberium_CellSpread, 0x6)
return 0;
}

DEFINE_HOOK(0x71C6EE, TerrainClass_FireOut_Crumbling, 0x6)
{
enum { StartCrumbling = 0x71C6F8, Skip = 0x71C72B };

GET(TerrainClass*, pThis, ESI);

auto const pTypeExt = TerrainTypeExt::ExtMap.Find(pThis->Type);

if (!pThis->IsCrumbling && pTypeExt->HasCrumblingFrames)
{
// Needs to be added to the logic layer for the anim to work.
MapClass::Instance->Logics.get().AddObject(pThis, false);
VocClass::PlayIndexAtPos(pTypeExt->CrumblingSound.Get(-1), pThis->GetCoords());

return StartCrumbling;
}

return Skip;
}

DEFINE_HOOK(0x71B965, TerrainClass_TakeDamage_SetContext, 0x8)
{
GET(TerrainClass*, pThis, ESI);

TerrainTypeTemp::PriorHealthRatio = pThis->GetHealthPercentage();

return 0;
}

DEFINE_HOOK(0x71B98B, TerrainClass_TakeDamage_RefreshDamageFrame, 0x7)
{
GET(TerrainClass*, pThis, ESI);

auto const pTypeExt = TerrainTypeExt::ExtMap.Find(pThis->Type);
double condYellow = RulesExt::Global()->ConditionYellow_Terrain.Get(RulesClass::Instance->ConditionYellow);

if (!pThis->Type->IsAnimated && pTypeExt->HasDamagedFrames && TerrainTypeTemp::PriorHealthRatio > condYellow && pThis->GetHealthPercentage() <= condYellow)
{
pThis->IsCrumbling = true; // Dirty hack to get game to redraw the art reliably.
MapClass::Instance->Logics.get().AddObject(pThis, false);
}

return 0;
}

//This one on Very end of it , let everything play first
DEFINE_HOOK(0x71BB2C, TerrainClass_TakeDamage_NowDead_Add, 0x6)
{
GET(TerrainClass*, pThis, ESI);
//saved for later usage !
//REF_STACK(args_ReceiveDamage const, ReceiveDamageArgs, STACK_OFFSET(0x3C, 0x4));

if (auto const pTerrainExt = TerrainTypeExt::ExtMap.Find(pThis->Type))
auto const pTypeExt = TerrainTypeExt::ExtMap.Find(pThis->Type);

// Skip over the removal of the tree as well as destroy sound/anim (for now) if the tree has crumble animation.
if (pThis->IsCrumbling && pTypeExt->HasCrumblingFrames)
{
auto const nCoords = pThis->GetCoords();
VocClass::PlayIndexAtPos(pTerrainExt->DestroySound.Get(-1), nCoords);
// Needs to be added to the logic layer for the anim to work.
MapClass::Instance->Logics.get().AddObject(pThis, false);
VocClass::PlayIndexAtPos(pTypeExt->CrumblingSound.Get(-1), pThis->GetCoords());
pThis->Mark(MarkType::Change);
pThis->Disappear(true);

if (auto const pAnimType = pTerrainExt->DestroyAnim.Get(nullptr))
GameCreate<AnimClass>(pAnimType, nCoords);
return 0x71BB79;
}

pTypeExt->PlayDestroyEffects(pThis->GetCoords());

return 0;
}

Expand Down Expand Up @@ -195,3 +312,4 @@ DEFINE_HOOK(0x568432, MapClass_PlaceDown_0x0TerrainTypes, 0x8)

return 0;
}

0 comments on commit ba9226a

Please sign in to comment.