From 230b7743b8c6dff78eeb814760cc77c736a87172 Mon Sep 17 00:00:00 2001 From: Trsdy <914137150@qq.com> Date: Sat, 2 Mar 2024 21:29:32 +0800 Subject: [PATCH] DropPod properties per InfantryType (#1196) --- CREDITS.md | 1 + Phobos.vcxproj | 2 + docs/Fixed-or-Improved-Logics.md | 27 +++ docs/Whats-New.md | 1 + src/Ext/Rules/Body.cpp | 9 + src/Ext/Rules/Body.h | 4 + src/Ext/Techno/Body.Update.cpp | 39 +++- src/Ext/Techno/Body.h | 2 + src/Ext/Techno/Hooks.cpp | 19 +- src/Ext/TechnoType/Body.cpp | 12 ++ src/Ext/TechnoType/Body.h | 3 + src/New/Type/Affiliated/DroppodTypeClass.cpp | 201 +++++++++++++++++++ src/New/Type/Affiliated/DroppodTypeClass.h | 31 +++ src/New/Type/LaserTrailTypeClass.cpp | 2 + src/New/Type/LaserTrailTypeClass.h | 2 + 15 files changed, 333 insertions(+), 22 deletions(-) create mode 100644 src/New/Type/Affiliated/DroppodTypeClass.cpp create mode 100644 src/New/Type/Affiliated/DroppodTypeClass.h diff --git a/CREDITS.md b/CREDITS.md index 1f4054da67..12b4d88669 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -292,6 +292,7 @@ This page lists all the individual contributions to the project by their author. - Teleport timer reset after load game fix - Teleport and Tunnel loco visual tilt fix - Skip units' turret rotation and jumpjets' wobbling under EMP + - Droppod properties dehardcode - Misc code refactor & maintenance, CN doc fixes, bugfixes - **FlyStar** - Campaign load screen PCX support diff --git a/Phobos.vcxproj b/Phobos.vcxproj index f51d06a1e9..8240b6cc6f 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -65,6 +65,7 @@ + @@ -200,6 +201,7 @@ + diff --git a/docs/Fixed-or-Improved-Logics.md b/docs/Fixed-or-Improved-Logics.md index 7738d13fc4..e34e437dfc 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -1038,3 +1038,30 @@ In `rulesmd.ini`: [AudioVisual] SelectionFlashDuration=0 ; integer, number of frames ``` + +## DropPod + +DropPod properties can now be customized on a per-InfantryType basis. +- Note that the DropPod is actually the infantry itself with a different shp image. +- If you want to attach the trailer animation to the pod, set `DropPod.Trailer.Attached` to yes. +- By default LaserTrails that are attached to the infantry will not be drawn if it's on DropPod. + - If you really want to use it, set `DropPodOnly` on the LaserTrail's type entry in art. +- If you want `DropPod.Weapon` to be fired only upon hard landing, set `DropPod.Weapon.HitLandOnly` to true. +- The landing speed is not smaller than it's current height /10 + 2 for unknown reason. A small `DropPod.Speed` value therefore results in exponential deceleration. + +In `rulesmd.ini` +```ini +[SOMEINFANTRY] +DropPod.Angle = ; double, default to [General]->DropPodAngle, measured in radians +DropPod.AtmosphereEntry = ; anim, default to [AudioVisual]->AtmosphereEntry +DropPod.GroundAnim = ; 2 anims, default to [General]->DropPod +DropPod.AirImage = ; SHP file, the pod's shape, default to POD +DropPod.Height = ; int, default to [General]->DropPodHeight +DropPod.Puff = ; anim, default to [General]->DropPodPuff +DropPod.Speed = ; int, default to [General]->DropPodSpeed +DropPod.Trailer = ; anim, default to [General]->DropPodTrailer, which by default is SMOKEY +DropPod.Trailer.Attached = ; boolean, default to no +DropPod.Trailer.SpawnDelay = ; int, number of frames between each spawn of DropPod.Trailer, default to 6 +DropPod.Weapon = ; weapon, default to [General]->DropPodWeapon +DropPod.Weapon.HitLandOnly = ; boolean, default to no +``` diff --git a/docs/Whats-New.md b/docs/Whats-New.md index a6e9c4fa86..5f876ab47f 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -356,6 +356,7 @@ New: - Allow toggling whether or not fire particle systems adjust target coordinates when firer rotates (by Starkku) - `AmbientDamage` warhead & main target ignore customization (by Starkku) - Flashing Technos on selecting (by Fryone) +- Customizable DropPod properties on a per-InfantryType basis (by Trsdy) - Projectile return weapon (by Starkku) - Allow customizing aircraft landing direction per aircraft or per dock (by Starkku) diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index d3c27b6903..97e1a97991 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -137,6 +137,13 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) this->IsVoiceCreatedGlobal.Read(exINI, GameStrings::AudioVisual, "IsVoiceCreatedGlobal"); this->SelectionFlashDuration.Read(exINI, GameStrings::AudioVisual, "SelectionFlashDuration"); + Nullable droppod_trailer {}; + droppod_trailer.Read(exINI, GameStrings::General, "DropPodTrailer"); + droppod_trailer = droppod_trailer.Get(AnimTypeClass::Find("SMOKEY")); // Ares convention + if (!droppod_trailer.Get()) + this->DropPodTrailer = droppod_trailer.Get(); + this->PodImage = FileSystem::LoadSHPFile("POD.SHP"); + this->Buildings_DefaultDigitalDisplayTypes.Read(exINI, GameStrings::AudioVisual, "Buildings.DefaultDigitalDisplayTypes"); this->Infantry_DefaultDigitalDisplayTypes.Read(exINI, GameStrings::AudioVisual, "Infantry.DefaultDigitalDisplayTypes"); this->Vehicles_DefaultDigitalDisplayTypes.Read(exINI, GameStrings::AudioVisual, "Vehicles.DefaultDigitalDisplayTypes"); @@ -277,6 +284,8 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->Vehicles_DefaultDigitalDisplayTypes) .Process(this->Aircraft_DefaultDigitalDisplayTypes) .Process(this->ShowDesignatorRange) + .Process(this->DropPodTrailer) + .Process(this->PodImage) ; } diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index dcbec69213..00ef1f8bb8 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -108,6 +108,8 @@ class RulesExt Valueable ShowDesignatorRange; Valueable IsVoiceCreatedGlobal; Valueable SelectionFlashDuration; + AnimTypeClass* DropPodTrailer; + SHPStruct* PodImage; ExtData(RulesClass* OwnerObject) : Extension(OwnerObject) , Storage_TiberiumIndex { -1 } @@ -178,6 +180,8 @@ class RulesExt , Vehicles_DefaultDigitalDisplayTypes {} , Aircraft_DefaultDigitalDisplayTypes {} , ShowDesignatorRange { true } + , DropPodTrailer { } + , PodImage { } { } virtual ~ExtData() = default; diff --git a/src/Ext/Techno/Body.Update.cpp b/src/Ext/Techno/Body.Update.cpp index 51bdc0bc39..33477166ee 100644 --- a/src/Ext/Techno/Body.Update.cpp +++ b/src/Ext/Techno/Body.Update.cpp @@ -1,3 +1,4 @@ +// methods used in TechnoClass_AI hooks or anything similar #include "Body.h" #include @@ -9,7 +10,31 @@ #include #include -// methods used in TechnoClass_AI hooks or anything similar + +// TechnoClass_AI_0x6F9E50 +// It's not recommended to do anything more here it could have a better place for performance consideration +void TechnoExt::ExtData::OnEarlyUpdate() +{ + auto pType = this->OwnerObject()->GetTechnoType(); + + // Set only if unset or type is changed + // Notice that Ares may handle type conversion in the same hook here, which is executed right before this one thankfully + if (!this->TypeExtData || this->TypeExtData->OwnerObject() != pType) + this->UpdateTypeData(pType); + + this->IsInTunnel = false; // TechnoClass::AI is only called when not in tunnel. + + if (this->CheckDeathConditions()) + return; + + this->ApplyInterceptor(); + this->EatPassengers(); + this->UpdateShield(); + this->ApplySpawnLimitRange(); + this->UpdateLaserTrails(); + this->DepletedAmmoActions(); +} + void TechnoExt::ExtData::ApplyInterceptor() { @@ -454,12 +479,18 @@ void TechnoExt::ExtData::UpdateTypeData(TechnoTypeClass* pCurrentType) void TechnoExt::ExtData::UpdateLaserTrails() { - auto const pThis = this->OwnerObject(); + auto const pThis = generic_cast(this->OwnerObject()); + if (!pThis) + return; - // LaserTrails update routine is in TechnoClass::AI hook because TechnoClass::Draw - // doesn't run when the object is off-screen which leads to visual bugs - Kerbiter + // LaserTrails update routine is in TechnoClass::AI hook because LaserDrawClass-es are updated in LogicClass::AI for (auto& trail : this->LaserTrails) { + // @Kerbiter if you want to limit it to certain locos you do it here + // with vtable check you can avoid the tedious process of Query IPersit/IUnknown Interface, GetClassID, compare with loco GUID, which is omnipresent in vanilla code + if (VTable::Get(pThis->Locomotor.GetInterfacePtr()) != 0x7E8278 && trail.Type->DroppodOnly) + continue; + if (pThis->CloakState == CloakState::Cloaked && !trail.Type->CloakVisible) continue; diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h index 84feb66b15..815fa37883 100644 --- a/src/Ext/Techno/Body.h +++ b/src/Ext/Techno/Body.h @@ -63,6 +63,8 @@ class TechnoExt , WHAnimRemainingCreationInterval { 0 } { } + void OnEarlyUpdate(); + void ApplyInterceptor(); bool CheckDeathConditions(bool isInLimbo = false); void DepletedAmmoActions(); diff --git a/src/Ext/Techno/Hooks.cpp b/src/Ext/Techno/Hooks.cpp index a3c77019e8..d130a2d1fc 100644 --- a/src/Ext/Techno/Hooks.cpp +++ b/src/Ext/Techno/Hooks.cpp @@ -13,24 +13,7 @@ DEFINE_HOOK(0x6F9E50, TechnoClass_AI, 0x5) // Do not search this up again in any functions called here because it is costly for performance - Starkku auto pExt = TechnoExt::ExtMap.Find(pThis); - auto pType = pThis->GetTechnoType(); - - // Set only if unset or type is changed - // Notice that Ares may handle type conversion in the same hook here, which is executed right before this one thankfully - if (!pExt->TypeExtData || pExt->TypeExtData->OwnerObject() != pType) - pExt->UpdateTypeData(pType); - - pExt->IsInTunnel = false; // TechnoClass::AI is only called when not in tunnel. - - if (pExt->CheckDeathConditions()) - return 0; - - pExt->ApplyInterceptor(); - pExt->EatPassengers(); - pExt->UpdateShield(); - pExt->ApplySpawnLimitRange(); - pExt->UpdateLaserTrails(); - pExt->DepletedAmmoActions(); + pExt->OnEarlyUpdate(); TechnoExt::ApplyMindControlRangeLimit(pThis); diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index 4785b96757..725c5e870d 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -405,6 +405,17 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) { this->InterceptorType.reset(); } + + if (this->OwnerObject()->WhatAmI() == AbstractType::InfantryType) + { + if (this->DroppodType == nullptr) + this->DroppodType = std::make_unique(); + this->DroppodType->LoadFromINI(pINI, pSection); + } + else + { + this->DroppodType.reset(); + } } template @@ -571,6 +582,7 @@ void TechnoTypeExt::ExtData::Serialize(T& Stm) .Process(this->SpawnDistanceFromTarget) .Process(this->SpawnHeight) .Process(this->LandingDir) + .Process(this->DroppodType) ; } void TechnoTypeExt::ExtData::LoadFromStream(PhobosStreamReader& Stm) diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index 2b9b5a2cee..384b7caf3c 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -10,6 +10,7 @@ #include #include #include +#include class Matrix3D; @@ -52,6 +53,7 @@ class TechnoTypeExt Valueable ShieldType; std::unique_ptr PassengerDeletionType; + std::unique_ptr DroppodType; Nullable Ammo_AddOnDeploy; Valueable Ammo_AutoDeployMinimumAmount; @@ -361,6 +363,7 @@ class TechnoTypeExt , SpawnDistanceFromTarget {} , SpawnHeight {} , LandingDir {} + , DroppodType {} { } virtual ~ExtData() = default; diff --git a/src/New/Type/Affiliated/DroppodTypeClass.cpp b/src/New/Type/Affiliated/DroppodTypeClass.cpp new file mode 100644 index 0000000000..82a6049c46 --- /dev/null +++ b/src/New/Type/Affiliated/DroppodTypeClass.cpp @@ -0,0 +1,201 @@ +#include "DroppodTypeClass.h" +#include + +#include +#include +#include + +void DroppodTypeClass::LoadFromINI(CCINIClass* pINI, const char* pSection) +{ + INI_EX exINI(pINI); + this->Angle.Read(exINI, pSection, "DropPod.Angle"); + this->AtmosphereEntry.Read(exINI, pSection, "DropPod.AtmosphereEntry"); + ValueableVector anims; + anims.Read(exINI, pSection, "DropPod.GroundAnim"); + if (!anims.empty()) + { + this->GroundAnim[0] = anims[0]; + this->GroundAnim[1] = anims[anims.size() > 1]; + } + this->AirImage.Read(exINI, pSection, "DropPod.AirImage"); + this->Height.Read(exINI, pSection, "DropPod.Height"); + this->Puff.Read(exINI, pSection, "DropPod.Puff"); + this->Speed.Read(exINI, pSection, "DropPod.Speed"); + this->Trailer.Read(exINI, pSection, "DropPod.Trailer"); + this->Trailer_SpawnDelay.Read(exINI, pSection, "DropPod.Trailer.SpawnDelay"); + this->Trailer_Attached.Read(exINI, pSection, "DropPod.Trailer.Attached"); + this->Weapon.Read(exINI, pSection, "DropPod.Weapon"); + this->Weapon_HitLandOnly.Read(exINI, pSection, "DropPod.Weapon.HitLandOnly"); +} + +#pragma region(save/load) + +template +bool DroppodTypeClass::Serialize(T& stm) +{ + return stm + .Process(this->Angle) + .Process(this->GroundAnim[0]) + .Process(this->GroundAnim[1]) + .Process(this->AtmosphereEntry) + .Process(this->Height) + .Process(this->Puff) + .Process(this->Speed) + .Process(this->Trailer) + .Process(this->Trailer_SpawnDelay) + .Process(this->Trailer_Attached) + .Process(this->Weapon) + .Process(this->Weapon_HitLandOnly) + .Process(this->AirImage) + .Success(); +} + +bool DroppodTypeClass::Load(PhobosStreamReader& stm, bool registerForChange) +{ + return this->Serialize(stm); +} + +bool DroppodTypeClass::Save(PhobosStreamWriter& stm) const +{ + return const_cast(this)->Serialize(stm); +} + +#pragma endregion(save/load) + +// DropPod loco is so far the easiest loco to rewrite completely + +DEFINE_HOOK(0x4B5B70, DroppodLocomotionClass_ILoco_Process, 0x5) +{ + GET_STACK(ILocomotion*, iloco, 0x4); + __assume(iloco != nullptr); + auto const lThis = static_cast(iloco); + auto const pLinked = lThis->LinkedTo; + auto const linkedExt = TechnoExt::ExtMap.Find(pLinked); + const auto podType = linkedExt->TypeExtData->DroppodType.get(); + + CoordStruct oldLoc = pLinked->Location; + + const double angle = podType->Angle.Get(RulesClass::Instance->DropPodAngle); + // exponentially decelerate + int speed = std::max(podType->Speed.Get(RulesClass::Instance->DropPodSpeed), pLinked->GetHeight() / 10 + 2); + + CoordStruct coords = pLinked->Location; + coords.X += int(Math::cos(angle) * speed * (lThis->OutOfMap ? 1 : -1)); + coords.Z -= int(Math::sin(angle) * speed); + + auto dWpn = podType->Weapon.Get(RulesClass::Instance->DropPodWeapon); + + if (pLinked->GetHeight() > 0) + { + pLinked->SetLocation(coords); + const int timeSinceCreation = Unsorted::CurrentFrame - pLinked->ParalysisTimer.StartTime;//or anything + + if (timeSinceCreation % podType->Trailer_SpawnDelay == 1) + { + if (auto trailerType = podType->Trailer.Get(RulesExt::Global()->DropPodTrailer)) + { + auto trailer = GameCreate(trailerType, oldLoc); + trailer->Owner = pLinked->Owner; + if (podType->Trailer_Attached) + trailer->SetOwnerObject(pLinked); + } + } + + if (dWpn && !podType->Weapon_HitLandOnly) + { + if (timeSinceCreation % std::max(dWpn->ROF, 3) == 1) + { + auto cell = MapClass::Instance->GetCellAt(lThis->DestinationCoords); + auto techno = cell->FindTechnoNearestTo({ 0,0 }, false); + if (!pLinked->Owner->IsAlliedWith(techno)) + { + // really? not firing for real? + auto locnear = MapClass::GetRandomCoordsNear(lThis->DestinationCoords, 85, false); + if (int count = dWpn->Report.Count) + VocClass::PlayAt(dWpn->Report[Randomizer::Global->Random() % count], coords); + MapClass::DamageArea(locnear, 2 * dWpn->Damage, pLinked, dWpn->Warhead, true, pLinked->Owner); + if (auto dmgAnim = MapClass::SelectDamageAnimation(2 * dWpn->Damage, dWpn->Warhead, LandType::Clear, locnear)) + GameCreate(dmgAnim, locnear, 0, 1, 0x2600, -15, 0)->Owner = pLinked->Owner; + } + } + } + } + else + { + pLinked->SetHeight(0); + pLinked->Limbo(); + lThis->AddRef(); + lThis->End_Piggyback(&pLinked->Locomotor); + + if (pLinked->Unlimbo(pLinked->Location, DirType::North)) + { + if (auto puff = podType->Puff.Get(RulesClass::Instance->DropPodPuff)) + GameCreate(puff, pLinked->Location)->Owner = pLinked->Owner; + + if (auto podAnim = podType->GroundAnim[lThis->OutOfMap].Get(RulesClass::Instance->DropPod[lThis->OutOfMap])) + GameCreate(podAnim, pLinked->Location)->Owner = pLinked->Owner; + + if (dWpn && podType->Weapon_HitLandOnly) + WeaponTypeExt::DetonateAt(dWpn, pLinked->Location, pLinked, pLinked->Owner); + + pLinked->Mark(MarkType::Down); + pLinked->SetHeight(0); + pLinked->EnterIdleMode(false, true); + pLinked->NextMission(); + pLinked->Scatter(CoordStruct::Empty, false, false); + } + else + { + MapClass::DamageArea(coords, 100, pLinked, RulesClass::Instance->C4Warhead, true, pLinked->Owner); + if (auto dmgAnim = MapClass::SelectDamageAnimation(100, RulesClass::Instance->C4Warhead, LandType::Clear, coords)) + GameCreate(dmgAnim, coords, 0, 1, 0x2600, -15, 0)->Owner = pLinked->Owner; + } + lThis->Release(); + } + + + R->AL(true); + return 0x4B6036; +} + +DEFINE_HOOK(0x4B607D, DroppodLocomotionClass_ILoco_MoveTo, 0x8) +{ + GET(ILocomotion*, iloco, EDI); + REF_STACK(CoordStruct, to, STACK_OFFSET(0x1C, 0x8)); + __assume(iloco != nullptr); + auto const lThis = static_cast(iloco); + auto const pLinked = lThis->LinkedTo; + + lThis->DestinationCoords = to; + lThis->DestinationCoords.Z = MapClass::Instance->GetCellFloorHeight(to); + + const auto podType = TechnoTypeExt::ExtMap.Find(pLinked->GetTechnoType())->DroppodType.get(); + const int height = podType->Height.Get(RulesClass::Instance->DropPodHeight); + const double angle = podType->Angle.Get(RulesClass::Instance->DropPodAngle); + + lThis->OutOfMap = !MapClass::Instance->IsWithinUsableArea(to); + to.X -= (lThis->OutOfMap ? 1 : -1) * int(height / Math::tan(angle)); + to.Z += height; + + pLinked->SetLocation(to); + + if (pLinked->Unlimbo(to, DirType::South)) + { + pLinked->PrimaryFacing.SetCurrent(DirStruct { DirType::South }); + if (auto entryAnim = podType->AtmosphereEntry.Get(RulesClass::Instance->AtmosphereEntry)) + GameCreate(entryAnim, to)->Owner = pLinked->Owner; + } + + return 0x4B61F0; +} + +DEFINE_HOOK(0x519168, InfantryClass_Draw_Droppod, 0x5) +{ + GET(InfantryClass*, pThis, EBP); + REF_STACK(SHPStruct*, shp, STACK_OFFSET(0x54, -0x34)); + + auto const podType = TechnoTypeExt::ExtMap.Find(pThis->Type)->DroppodType.get(); + shp = podType->AirImage.Get(RulesExt::Global()->PodImage); + + return 0x519176; +} diff --git a/src/New/Type/Affiliated/DroppodTypeClass.h b/src/New/Type/Affiliated/DroppodTypeClass.h new file mode 100644 index 0000000000..644d6ab623 --- /dev/null +++ b/src/New/Type/Affiliated/DroppodTypeClass.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +class DroppodTypeClass +{ +public: + Nullable Speed {}; + Nullable Angle {}; + Nullable Height {}; + Nullable Weapon {}; + Nullable GroundAnim[2] { {},{} }; + Nullable Puff {}; + Nullable Trailer {}; + Valueable Trailer_SpawnDelay { 6 }; + Nullable AtmosphereEntry {}; + Nullable AirImage { }; + Valueable Trailer_Attached { false }; + Valueable Weapon_HitLandOnly { false }; + + DroppodTypeClass() = default; + + void LoadFromINI(CCINIClass* pINI, const char* pSection); + bool Load(PhobosStreamReader& stm, bool registerForChange); + bool Save(PhobosStreamWriter& stm) const; + +private: + + template + bool Serialize(T& stm); +}; diff --git a/src/New/Type/LaserTrailTypeClass.cpp b/src/New/Type/LaserTrailTypeClass.cpp index c10d2deea3..8233630fc9 100644 --- a/src/New/Type/LaserTrailTypeClass.cpp +++ b/src/New/Type/LaserTrailTypeClass.cpp @@ -24,6 +24,7 @@ void LaserTrailTypeClass::LoadFromINI(CCINIClass* pINI) this->IgnoreVertical.Read(exINI, section, "IgnoreVertical"); this->IsIntense.Read(exINI, section, "IsIntense"); this->CloakVisible.Read(exINI, section, "CloakVisible"); + this->DroppodOnly.Read(exINI, section, "DropPodOnly"); } template @@ -38,6 +39,7 @@ void LaserTrailTypeClass::Serialize(T& Stm) .Process(this->IgnoreVertical) .Process(this->IsIntense) .Process(this->CloakVisible) + .Process(this->DroppodOnly) ; } diff --git a/src/New/Type/LaserTrailTypeClass.h b/src/New/Type/LaserTrailTypeClass.h index c5f5f64ce0..5de0dfa575 100644 --- a/src/New/Type/LaserTrailTypeClass.h +++ b/src/New/Type/LaserTrailTypeClass.h @@ -14,6 +14,7 @@ class LaserTrailTypeClass final : public Enumerable Valueable IgnoreVertical; Valueable IsIntense; Valueable CloakVisible; + Valueable DroppodOnly; LaserTrailTypeClass(const char* pTitle = NONE_STR) : Enumerable(pTitle) , IsHouseColor { false } @@ -24,6 +25,7 @@ class LaserTrailTypeClass final : public Enumerable , IgnoreVertical { false } , IsIntense { false } , CloakVisible { false } + , DroppodOnly { false } { } virtual ~LaserTrailTypeClass() override = default;