diff --git a/CREDITS.md b/CREDITS.md
index 743e4b9ca9..ae78a3e1d8 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -747,6 +747,9 @@ This page lists all the individual contributions to the project by their author.
- Fix the issue where the sidebar would not refresh when an unit dies in limbo
- Enable playing ingame movie in non-campaign modes (i.e. trigger action 100 and 117)
- Allow replacing vanilla repairing with togglable auto repairing
+ - Forced displacement locomotor implementation
+ - Knock-up warhead
+ - Traction warhead
- **solar-III (凤九歌)**
- Target scanning delay customization (documentation)
- Skip target scanning function calling for unarmed technos (documentation)
diff --git a/Phobos.vcxproj b/Phobos.vcxproj
index e1aa9e9c96..90a085b2be 100644
--- a/Phobos.vcxproj
+++ b/Phobos.vcxproj
@@ -22,6 +22,7 @@
+
@@ -33,6 +34,7 @@
+
@@ -223,9 +225,11 @@
+
+
diff --git a/YRpp b/YRpp
index 97f03d8bdc..4faf00803d 160000
--- a/YRpp
+++ b/YRpp
@@ -1 +1 @@
-Subproject commit 97f03d8bdc7ebeda1a6e0848a7bd81a4662c52fc
+Subproject commit 4faf00803dbcab04eca19078d8657c3fd0038c26
diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md
index 653ee7c5c6..cba2909868 100644
--- a/docs/New-or-Enhanced-Logics.md
+++ b/docs/New-or-Enhanced-Logics.md
@@ -2688,6 +2688,24 @@ PenetratesIronCurtain=false ; boolean
PenetratesForceShield= ; boolean
```
+### Knock-up warhead
+
+- You can now launch targets in an arc with projectiles. The target will be knocked up in the opposite direction of its current facing.
+ - `KnockUp` controls whether to enable this logic.
+ - `KnockUp.Range` defines the knock-up distance.
+ - `KnockUp.Speed` defines the horizontal knock-up velocity.
+ - `KnockUp.Angle` defines the knock-up angle.
+
+In `rulesmd.ini`:
+
+```ini
+[SOMEWARHEAD] ; WarheadType
+KnockUp=false ; boolean
+KnockUp.Range=0.0 ; floating point, cells
+KnockUp.Speed=0.0 ; floating point, cells/frame (horizontal component)
+KnockUp.Angle=45.0 ; floating point, degrees
+```
+
### Launch superweapons on impact
- Superweapons can now be launched when a warhead is detonated.
@@ -2917,6 +2935,22 @@ ApplyPerTargetEffectsOnDetonate= ; boolean, default to [CombatDamage] -> Ap
- Ares' warhead effect controllers, such as `EffectsRequireDamage`, only affect Ares' effects. So they have nothing to do with this.
```
+### Traction warhead
+
+- `Traction` pulls affected targets toward the explosion point.
+ - `Traction` controls whether to enable this logic.
+ - `Traction.Range` is treated as a movement "budget" (floating point, in cells) used to limit how far the pull can progress.
+ - `Traction.Speed` controls the linear movement speed applied when the pull occurs (cells/frame).
+
+In `rulesmd.ini`:
+
+```ini
+[SOMEWARHEAD] ; WarheadType
+Traction=false ; boolean
+Traction.Range=0.0 ; floating point, cells
+Traction.Speed=0.0 ; floating point, cells/frame
+```
+
### Trigger specific NotHuman infantry Death anim sequence
- Warheads are now able to trigger specific `NotHuman=yes` infantry `Death` anim sequence using the corresponding tag. It's value represents sequences from `Die1` to `Die5`.
diff --git a/docs/Whats-New.md b/docs/Whats-New.md
index 44a2513c77..c9bb1a8155 100644
--- a/docs/Whats-New.md
+++ b/docs/Whats-New.md
@@ -571,6 +571,9 @@ New:
- [Updateable firing anim](Fixed-or-Improved-Logics.md#updateable-firing-anim) (by TaranDahl)
- [Additional customizations for `Splits` concerning target selection](Fixed-or-Improved-Logics.md#airburst--splits) (by Starkku)
- [Allow replacing vanilla repairing with togglable auto repairing](User-Interface.md#allow-replacing-vanilla-repairing-with-togglable-auto-repairing) (by TaranDahl)
+- Forced displacement locomotor implementation (by TaranDahl)
+- [Knock-up warhead](New-or-Enhanced-Logics.md#knock-up-warhead) (by TaranDahl)
+- [Traction warhead](New-or-Enhanced-Logics.md#traction-warhead) (by TaranDahl)
Vanilla fixes:
- Fixed sidebar not updating queued unit numbers when adding or removing units when the production is on hold (by CrimRecya)
diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp
index a1a5987a1a..d16c8bd7b0 100644
--- a/src/Ext/Techno/Body.cpp
+++ b/src/Ext/Techno/Body.cpp
@@ -1225,12 +1225,17 @@ void TechnoExt::ExtData::Serialize(T& Stm)
.Process(this->HoverShutdown)
.Process(this->LastTargetCrd)
.Process(this->LastTargetCrdClearTimer)
+ /*.Process(this->QueuedShift)*/ // Always set and reset in one function
+ .Process(this->ShiftApplier)
+ .Process(this->ShiftApplierHouse)
;
}
void TechnoExt::ExtData::InvalidatePointer(void* ptr, bool bRemoved)
{
AnnounceInvalidPointer(this->AirstrikeTargetingMe, ptr);
+ AnnounceInvalidPointer(this->ShiftApplier, ptr);
+ AnnounceInvalidPointer(this->ShiftApplierHouse, ptr);
}
void TechnoExt::ExtData::LoadFromStream(PhobosStreamReader& Stm)
@@ -1264,6 +1269,23 @@ TechnoExt::ExtContainer::ExtContainer() : Container("TechnoClass") { }
TechnoExt::ExtContainer::~ExtContainer() = default;
+bool TechnoExt::ExtContainer::InvalidateExtDataIgnorable(void* const ptr) const
+{
+ auto const abs = static_cast(ptr)->WhatAmI();
+
+ switch (abs)
+ {
+ case AbstractType::Airstrike:
+ case AbstractType::Aircraft:
+ case AbstractType::Building:
+ case AbstractType::Infantry:
+ case AbstractType::Unit:
+ case AbstractType::House:
+ return false;
+ }
+
+ return true;
+}
// =============================
// container hooks
diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h
index bfc21c09d1..8e7d69f5ad 100644
--- a/src/Ext/Techno/Body.h
+++ b/src/Ext/Techno/Body.h
@@ -6,8 +6,10 @@
#include
#include
#include
+#include
class BulletClass;
+struct ShiftSchedule;
class TechnoExt
{
@@ -106,6 +108,10 @@ class TechnoExt
CoordStruct LastTargetCrd;
CDTimerClass LastTargetCrdClearTimer;
+ std::unique_ptr QueuedShift;
+ TechnoClass* ShiftApplier;
+ HouseClass* ShiftApplierHouse;
+
ExtData(TechnoClass* OwnerObject) : Extension(OwnerObject)
, TypeExtData { nullptr }
, Shield {}
@@ -177,6 +183,9 @@ class TechnoExt
, HoverShutdown { false }
, LastTargetCrd { CoordStruct::Empty }
, LastTargetCrdClearTimer {}
+ , QueuedShift {}
+ , ShiftApplier { nullptr }
+ , ShiftApplierHouse { nullptr }
{ }
void OnEarlyUpdate();
@@ -235,18 +244,7 @@ class TechnoExt
ExtContainer();
~ExtContainer();
- virtual bool InvalidateExtDataIgnorable(void* const ptr) const override
- {
- auto const abs = static_cast(ptr)->WhatAmI();
-
- switch (abs)
- {
- case AbstractType::Airstrike:
- return false;
- default:
- return true;
- }
- }
+ virtual bool InvalidateExtDataIgnorable(void* const ptr) const override;
};
static ExtContainer ExtMap;
diff --git a/src/Ext/WarheadType/Body.cpp b/src/Ext/WarheadType/Body.cpp
index 82a1bbd41d..81dad65668 100644
--- a/src/Ext/WarheadType/Body.cpp
+++ b/src/Ext/WarheadType/Body.cpp
@@ -405,6 +405,15 @@ void WarheadTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI)
this->Taunt.Read(exINI, pSection, "Taunt");
+ this->KnockUp.Read(exINI, pSection, "KnockUp");
+ this->KnockUp_Range.Read(exINI, pSection, "KnockUp.Range");
+ this->KnockUp_Speed.Read(exINI, pSection, "KnockUp.Speed");
+ this->KnockUp_Angle.Read(exINI, pSection, "KnockUp.Angle");
+
+ this->Traction.Read(exINI, pSection, "Traction");
+ this->Traction_Range.Read(exINI, pSection, "Traction.Range");
+ this->Traction_Speed.Read(exINI, pSection, "Traction.Speed");
+
// Convert.From & Convert.To
TypeConvertGroup::Parse(this->Convert_Pairs, exINI, pSection, AffectedHouse::All);
@@ -465,6 +474,8 @@ void WarheadTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI)
|| this->ReturnWarhead
|| this->PenetratesTransport_Level > 0
|| this->Taunt
+ || this->KnockUp
+ || this->Traction
);
char tempBuffer[32];
@@ -737,6 +748,15 @@ void WarheadTypeExt::ExtData::Serialize(T& Stm)
.Process(this->Taunt)
+ .Process(this->KnockUp)
+ .Process(this->KnockUp_Range)
+ .Process(this->KnockUp_Speed)
+ .Process(this->KnockUp_Angle)
+
+ .Process(this->Traction)
+ .Process(this->Traction_Range)
+ .Process(this->Traction_Speed)
+
// Ares tags
.Process(this->AffectsEnemies)
.Process(this->AffectsOwner)
diff --git a/src/Ext/WarheadType/Body.h b/src/Ext/WarheadType/Body.h
index 88be830a6d..086c092d05 100644
--- a/src/Ext/WarheadType/Body.h
+++ b/src/Ext/WarheadType/Body.h
@@ -247,6 +247,15 @@ class WarheadTypeExt
Valueable Taunt;
+ Valueable KnockUp;
+ Valueable KnockUp_Range;
+ Valueable KnockUp_Speed;
+ Valueable KnockUp_Angle;
+
+ Valueable Traction;
+ Valueable Traction_Range;
+ Valueable Traction_Speed;
+
// Ares tags
// http://ares-developers.github.io/Ares-docs/new/warheads/general.html
Valueable AffectsEnemies;
@@ -522,6 +531,15 @@ class WarheadTypeExt
, ApplyPerTargetEffectsOnDetonate {}
, Taunt { false }
+
+ , KnockUp { false }
+ , KnockUp_Range { Leptons(0) }
+ , KnockUp_Speed { Leptons(0) }
+ , KnockUp_Angle { 45.0 }
+
+ , Traction { false }
+ , Traction_Range { Leptons(0) }
+ , Traction_Speed { Leptons(0) }
{ }
void ApplyConvert(HouseClass* pHouse, TechnoClass* pTarget);
@@ -561,6 +579,8 @@ class WarheadTypeExt
void ApplyReverseEngineer(HouseClass* pHouse, TechnoClass* pTarget);
void ApplyReturnWarhead(HouseClass* pHouse, TechnoClass* pTarget, TechnoClass* Owner);
void ApplyPenetratesTransport(TechnoClass* pTarget, TechnoClass* pInvoker, HouseClass* pInvokerHouse, const CoordStruct& coords, int damage, int distance);
+ void ApplyKnockUp(TechnoClass* pTarget);
+ void ApplyTraction(TechnoClass* pTarget, const CoordStruct& coords);
double GetCritChance(TechnoClass* pFirer) const;
};
diff --git a/src/Ext/WarheadType/Detonate.cpp b/src/Ext/WarheadType/Detonate.cpp
index ac3036e5bc..66adba34b4 100644
--- a/src/Ext/WarheadType/Detonate.cpp
+++ b/src/Ext/WarheadType/Detonate.cpp
@@ -5,6 +5,11 @@
#include
#include
#include
+#include
+#include
+#include
+#include
+#include
#pragma region CreateGap Calls
@@ -157,8 +162,23 @@ void WarheadTypeExt::ExtData::Detonate(TechnoClass* pOwner, HouseClass* pHouse,
auto const items = Helpers::Alex::getCellSpreadItems(coords, cellSpread, true);
Helpers::Alex::GetCellSpreadItems::ResetParams();
- for (auto const pTarget : items)
- this->DetonateOnOneUnit(pHouse, pTarget, coords, damage, pOwner, bulletWasIntercepted);
+ if (this->Traction)
+ {
+ // Convert to vector for sorting (std::sort requires random access iterators)
+ std::vector sortedItems(items.begin(), items.end());
+ std::sort(sortedItems.begin(), sortedItems.end(), [&coords](TechnoClass* a, TechnoClass* b)
+ {
+ return a->GetCoords().DistanceFromSquared(coords) < b->GetCoords().DistanceFromSquared(coords);
+ });
+
+ for (auto const pTarget : sortedItems)
+ this->DetonateOnOneUnit(pHouse, pTarget, coords, damage, pOwner, bulletWasIntercepted);
+ }
+ else
+ {
+ for (auto const pTarget : items)
+ this->DetonateOnOneUnit(pHouse, pTarget, coords, damage, pOwner, bulletWasIntercepted);
+ }
}
else if (pBullet)
{
@@ -213,6 +233,13 @@ void WarheadTypeExt::ExtData::DetonateOnOneUnit(HouseClass* pHouse, TechnoClass*
if (this->Taunt && pOwner)
pTarget->Override_Mission(Mission::Attack, pOwner, nullptr);
+ // Apply knockup and traction effects
+ if (this->KnockUp)
+ this->ApplyKnockUp(pTarget);
+
+ if (this->Traction)
+ this->ApplyTraction(pTarget, coords);
+
// This might change the target's armor type
this->ApplyShieldModifiers(pTarget);
@@ -845,3 +872,228 @@ void WarheadTypeExt::ExtData::ApplyPenetratesTransport(TechnoClass* pTarget, Tec
VocClass::PlayAt(cleanSound, transporterCoords);
}
}
+
+void WarheadTypeExt::ExtData::ApplyKnockUp(TechnoClass* pTarget)
+{
+ if (!this->KnockUp)
+ return;
+
+ auto pTargetFoot = abstract_cast(pTarget);
+
+ if (!pTargetFoot)
+ return;
+
+ // same locomotor? no point to change
+ CLSID targetCLSID { };
+ CLSID inflictCLSID = __uuidof(ShiftLocomotionClass);
+ auto pLoco = pTargetFoot->Locomotor;
+ IPersistPtr pLocoPersist = pLoco;
+ if (SUCCEEDED(pLocoPersist->GetClassID(&targetCLSID)) && targetCLSID == inflictCLSID)
+ return;
+
+ // prevent endless piggyback
+ IPiggybackPtr pTargetPiggy = pTargetFoot->Locomotor;
+ if (pTargetPiggy != nullptr && pTargetPiggy->Is_Piggybacking())
+ return;
+
+ bool isAirUnit = ShiftLocomotionClass::IsAirLoco(pLoco);
+
+ // Calculate the reverse direction of PrimaryFacing
+ DirStruct facing = pTargetFoot->PrimaryFacing.Current();
+
+ // Convert facing to a unit vector
+ double angleRad = -facing.GetRadian<65536>();
+ double dx = Math::cos(angleRad);
+ double dy = Math::sin(angleRad);
+
+ // Opposite direction
+ dx = -dx;
+ dy = -dy;
+
+ // Calculate end position based on range
+ int range = this->KnockUp_Range.Get();
+ CoordStruct knockUpOffset;
+ knockUpOffset.X = static_cast(range * dx);
+ knockUpOffset.Y = static_cast(range * dy);
+ knockUpOffset.Z = 0;
+
+ // Check bridge at destination - if there's a bridge, use bridge height
+ CoordStruct destCoords;
+
+ if (isAirUnit)
+ {
+ destCoords = pTargetFoot->GetCoords() + knockUpOffset;
+ destCoords.Z = pTargetFoot->GetHeight() + MapClass::Instance.GetCellAt(destCoords)->GetCoordsWithBridge().Z;
+ }
+ else
+ {
+ destCoords = pTargetFoot->GetCoords() + knockUpOffset;
+ auto destCellCoords = MapClass::Instance.GetCellAt(destCoords)->GetCoords();
+ auto currentCellCoords = pTargetFoot->GetCell()->GetCoords();
+ auto currentCrd = pTargetFoot->GetCoords();
+ destCoords = destCellCoords + (currentCrd - currentCellCoords);
+ destCoords.Z = pTargetFoot->GetHeight() + MapClass::Instance.GetCellAt(destCoords)->GetCoordsWithBridge().Z;
+ }
+
+ destCoords = ShiftLocomotionClass::FindShiftDestination(pTargetFoot, destCoords);
+
+ if (destCoords != CoordStruct::Empty)
+ {
+ // Create shift schedule
+ auto sampleStart = ShiftSchedule::Sample(
+ pTargetFoot->GetCoords(),
+ pTargetFoot->PrimaryFacing.Current(),
+ 0.0f, 0.0f, false);
+
+ auto sampleEnd = ShiftSchedule::Sample(
+ destCoords,
+ pTargetFoot->PrimaryFacing.Current(),
+ 0.0f, 0.0f, true);
+
+ auto params = ParabolaParams(this->KnockUp_Angle.Get(), this->KnockUp_Speed.Get());
+ auto schedule = ParabolaShiftSchedule(sampleStart, sampleEnd, ¶ms);
+
+ // Queue the shift
+ auto pExt = TechnoExt::ExtMap.Find(pTargetFoot);
+ pExt->QueuedShift = std::make_unique(schedule);
+
+ // Change locomotor
+ LocomotionClass::ChangeLocomotorTo(pTargetFoot, inflictCLSID);
+ }
+}
+
+void WarheadTypeExt::ExtData::ApplyTraction(TechnoClass* pTarget, const CoordStruct& coords)
+{
+ if (!this->Traction)
+ return;
+
+ auto pTargetFoot = abstract_cast(pTarget);
+
+ if (!pTargetFoot)
+ return;
+
+ if (coords == CoordStruct::Empty)
+ return;
+
+ // same locomotor? no point to change
+ CLSID targetCLSID { };
+ CLSID inflictCLSID = __uuidof(ShiftLocomotionClass);
+ IPersistPtr pLocoPersist = pTargetFoot->Locomotor;
+ if (SUCCEEDED(pLocoPersist->GetClassID(&targetCLSID)) && targetCLSID == inflictCLSID)
+ return;
+
+ // prevent endless piggyback
+ IPiggybackPtr pTargetPiggy = pTargetFoot->Locomotor;
+ if (pTargetPiggy != nullptr && pTargetPiggy->Is_Piggybacking())
+ return;
+
+ int tractionSpeed = this->Traction_Speed.Get();
+ int tractionRange = this->Traction_Range.Get();
+
+ if (tractionSpeed <= 0 || tractionRange <= 0)
+ return;
+
+ CoordStruct startCoords = pTargetFoot->GetCoords();
+ auto startCell = MapClass::Instance.GetCellAt(startCoords);
+ auto offset = startCoords - startCell->GetCoordsWithBridge();
+
+ CoordStruct destCoords = startCoords;
+ auto destMapCrd = startCell->MapCoords;
+ int tractionRangeUnused = tractionRange;
+
+ for (; tractionRangeUnused > 0;)
+ {
+ double bestCos = 0;
+ double bestCost = std::numeric_limits::max();
+ std::vector bestDirs;
+
+ for (size_t i = 0; i < 8; ++i)
+ {
+ auto calcStepResult = [&](CellStruct currentMapCrd) -> std::pair
+ {
+ auto dirOffset = CellSpread::GetNeighbourOffset(i);
+ auto nextMapCrd = currentMapCrd + dirOffset;
+ auto nextCell = MapClass::Instance.GetCellAt(nextMapCrd);
+
+ if (pTarget->IsCellOccupied(nextCell, FacingType::None, -1, nullptr, true) != Move::OK)
+ return { 0.0, std::numeric_limits::max() };
+
+ auto dirOffsetCrd = Point2D(dirOffset.X * Unsorted::LeptonsPerCell, dirOffset.Y * Unsorted::LeptonsPerCell);
+ auto dirDelta = dirOffsetCrd.Magnitude();
+ auto tractionVector = coords - CellClass::Cell2Coord(currentMapCrd);
+ auto tractionVector2D = Point2D(tractionVector.X, tractionVector.Y);
+ auto angleCos = Point2D(dirOffsetCrd.X, dirOffsetCrd.Y).AngleCosTo(tractionVector2D);
+
+ if (angleCos <= 0)
+ return { angleCos, std::numeric_limits::max() };
+
+ return { angleCos, dirDelta / angleCos };
+ };
+
+ auto stepResult = calcStepResult(destMapCrd);
+ auto angleCos = stepResult.first;
+ auto cost = stepResult.second;
+
+ if (cost > tractionRangeUnused)
+ continue;
+
+ if (angleCos > bestCos)
+ {
+ bestCos = angleCos;
+ bestCost = cost;
+ bestDirs.clear();
+ bestDirs.push_back(i);
+ }
+ else if (angleCos == bestCos)
+ {
+ if (cost > bestCost)
+ {
+ bestCost = cost;
+ bestDirs.clear();
+ bestDirs.push_back(i);
+ }
+ else if (cost == bestCost)
+ {
+ bestDirs.push_back(i);
+ }
+ }
+ }
+
+ // If no valid direction found, exit the loop
+ if (bestDirs.empty())
+ break;
+
+ // Randomly select one of the best directions if multiple exist
+ size_t selectedDir = bestDirs[ScenarioClass::Instance->Random.RandomRanged(0, bestDirs.size() - 1)];
+ // Move one step in the selected direction
+ auto dirOffset = CellSpread::GetNeighbourOffset(selectedDir);
+ destMapCrd += dirOffset;
+ tractionRangeUnused -= static_cast(bestCost);
+ }
+
+ if (destMapCrd != startCell->MapCoords)
+ {
+ // Convert final map coordinates back to world coordinates
+ destCoords = MapClass::Instance.GetCellAt(destMapCrd)->GetCoordsWithBridge() + offset;
+
+ // Create linear shift schedule for traction
+ auto sampleStart = ShiftSchedule::Sample(
+ startCoords,
+ pTargetFoot->PrimaryFacing.Current(),
+ 0.0f, 0.0f, false);
+
+ auto sampleEnd = ShiftSchedule::Sample(
+ destCoords,
+ pTargetFoot->PrimaryFacing.Current(),
+ 0.0f, 0.0f, true);
+
+ auto params = LinearParams { tractionSpeed };
+ auto schedule = LinearShiftSchedule(sampleStart, sampleEnd, ¶ms);
+
+ auto pExt = TechnoExt::ExtMap.Find(pTargetFoot);
+ pExt->QueuedShift = std::make_unique(schedule);
+
+ // Change locomotor
+ LocomotionClass::ChangeLocomotorTo(pTargetFoot, inflictCLSID);
+ }
+}
diff --git a/src/Locomotion/ShiftLocomotionClass.cpp b/src/Locomotion/ShiftLocomotionClass.cpp
new file mode 100644
index 0000000000..e80657c98a
--- /dev/null
+++ b/src/Locomotion/ShiftLocomotionClass.cpp
@@ -0,0 +1,648 @@
+#include "ShiftLocomotionClass.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#pragma region Helpers
+
+enum TrackerType
+{
+ Air,
+ Ground,
+ Underground
+};
+
+TrackerType GetTrackerTypeForHeight(int height)
+{
+ if (height >= 208)
+ return TrackerType::Air;
+ else if (height >= 0)
+ return TrackerType::Ground;
+ else
+ return TrackerType::Underground;
+}
+
+bool ShiftLocomotionClass::IsAirLoco(ILocomotion* pLoco)
+{
+ return pLoco->Is_Moving() && (locomotion_cast(pLoco) || locomotion_cast(pLoco));
+}
+
+CoordStruct ShiftLocomotionClass::FindShiftDestination(FootClass* pTechno, CoordStruct idealDest, double searchRange, bool pathReachable)
+{
+ if (IsAirLoco(pTechno->Locomotor))
+ return idealDest;
+
+ auto checkMapCrd = [&](CellStruct mapCrd)
+ {
+ auto pCell = MapClass::Instance.GetCellAt(mapCrd);
+ bool clear = pTechno->IsCellOccupied(pCell, FacingType::None, -1, nullptr, true) == Move::OK;
+
+ if (!clear)
+ return false;
+
+ auto currentMapCrd = pTechno->GetMapCoords();
+ bool reachable = !pathReachable || AStarClass::Instance.AttemptPath(¤tMapCrd, &mapCrd, pTechno, pTechno->OnBridge, pCell->ContainsBridge()) != INT_MAX;
+
+ if (!reachable)
+ return false;
+
+ return true;
+ };
+
+ auto cells = GeneralUtils::AdjacentCellsInRange((unsigned int)(std::floor(searchRange)));
+ auto idealMapCrd = CellClass::Coord2Cell(idealDest);
+ auto offset = idealDest - MapClass::Instance.GetCellAt(idealMapCrd)->GetCoordsWithBridge();
+
+ double bestDistSq = std::numeric_limits::max();
+ std::vector bestMapCrds;
+
+ for (auto cell : cells)
+ {
+ auto mapCrd = idealMapCrd + cell;
+
+ if (!checkMapCrd(mapCrd))
+ continue;
+
+ double distSq = (mapCrd - idealMapCrd).MagnitudeSquared();
+
+ if (distSq < bestDistSq)
+ {
+ bestDistSq = distSq;
+ bestMapCrds.clear();
+ bestMapCrds.push_back(mapCrd);
+ }
+ else if (distSq == bestDistSq)
+ {
+ bestMapCrds.push_back(mapCrd);
+ }
+ }
+
+ if (bestMapCrds.empty())
+ {
+ return CoordStruct::Empty;
+ }
+
+ CellStruct finalMapCrd = bestMapCrds[ScenarioClass::Instance->Random.RandomRanged(0, static_cast(bestMapCrds.size() - 1))];
+
+ return MapClass::Instance.GetCellAt(finalMapCrd)->GetCoordsWithBridge() + offset;
+}
+
+#pragma endregion
+
+void ShiftLocomotionClass::BeginShift(std::unique_ptr schedule)
+{
+ if (!this->LinkedTo || !schedule)
+ return;
+
+ this->Schedule = std::move(schedule);
+ this->Elapsed = 0;
+ this->IsShifting = true;
+
+ // Process
+ {
+ this->Schedule->BeginShiftProcess(this->LinkedTo);
+ }
+
+ // Remove from trackers
+ {
+ bool isOnMap = this->LinkedTo->IsOnMap;
+ this->LinkedTo->IsOnMap = false;
+ this->LinkedTo->Mark(MarkType::Up);
+ this->LinkedTo->IsOnMap = isOnMap;
+ auto frozenStill = this->LinkedTo->FrozenStill;
+ this->LinkedTo->FrozenStill = false;
+ this->LinkedTo->GetCell()->RemoveContent(this->LinkedTo, this->LinkedTo->OnBridge);
+ this->LinkedTo->FrozenStill = frozenStill;
+
+ AircraftTrackerClass::Instance.Remove(this->LinkedTo);
+ ScenarioExt::Global()->UndergroundTracker.Remove(this->LinkedTo);
+ //auto const ext = TechnoExt::ExtMap.Find(this->LinkedTo);
+ //if (ext) ext->SpecialTracked = true;
+ }
+
+ // Occupy the target cell
+ {
+ this->LinkedTo->UnmarkAllOccupationBits(this->LinkedTo->GetCoords());
+
+ auto finalSample = this->Schedule->End;
+ CoordStruct dest = finalSample.Position;
+ int destCellZ = MapClass::Instance.GetCellFloorHeight(dest);
+ int destCellHeight = destCellZ - dest.Z;
+
+ if (destCellHeight < 208 && destCellHeight >= 0)
+ {
+ this->LinkedTo->MarkAllOccupationBits(dest);
+ }
+ }
+}
+
+void ShiftLocomotionClass::FinishShift(bool normal)
+{
+ if (this->Schedule)
+ {
+ // Remove occupation bits
+ {
+ auto finalSample = this->Schedule->End;
+ CoordStruct dest = finalSample.Position;
+ int destCellZ = MapClass::Instance.GetCellFloorHeight(dest);
+ int destCellHeight = destCellZ - dest.Z;
+ if (destCellHeight < 208 && destCellHeight >= 0)
+ {
+ this->LinkedTo->UnmarkAllOccupationBits(dest);
+ }
+ }
+
+ // Add to trackers
+ if (normal)
+ {
+ auto sample = this->Schedule->End;
+ auto oldMapCrd = this->LinkedTo->GetMapCoords();
+ auto oldHeight = this->LinkedTo->GetHeight();
+ auto oldTrackerType = GetTrackerTypeForHeight(oldHeight);
+ auto oldFlightMapCrd = this->LinkedTo->GetLastFlightMapCoords();
+ bool oldAlt = this->LinkedTo->OnBridge;
+
+ bool isOnMap = this->LinkedTo->IsOnMap;
+ this->LinkedTo->IsOnMap = false;
+ this->LinkedTo->SetLocation(sample.Position);
+ this->LinkedTo->IsOnMap = isOnMap;
+ this->LinkedTo->OnBridge = this->LinkedTo->GetCell()->ContainsBridge() && this->LinkedTo->Location.Z >= MapClass::Instance.GetCellFloorHeight(this->LinkedTo->Location) + CellClass::BridgeHeight;
+
+ this->LinkedTo->PrimaryFacing.SetCurrent(sample.Facing);
+ auto newMapCrd = this->LinkedTo->GetMapCoords();
+ auto newHeight = this->LinkedTo->GetHeight();
+ auto newTrackerType = GetTrackerTypeForHeight(newHeight);
+ bool newAlt = this->LinkedTo->OnBridge;
+
+ if (oldTrackerType != newTrackerType)
+ {
+ switch (oldTrackerType)
+ {
+ case TrackerType::Air:
+ AircraftTrackerClass::Instance.Remove(this->LinkedTo);
+ break;
+ case TrackerType::Ground:
+ {
+ auto oldCell = MapClass::Instance.GetCellAt(oldMapCrd);
+ auto frozenStill = this->LinkedTo->FrozenStill;
+ this->LinkedTo->FrozenStill = false;
+ oldCell->RemoveContent(this->LinkedTo, oldAlt);
+ this->LinkedTo->FrozenStill = frozenStill;
+ break;
+ }
+ case TrackerType::Underground:
+ ScenarioExt::Global()->UndergroundTracker.Remove(this->LinkedTo);
+ TechnoExt::ExtMap.Find(this->LinkedTo)->UndergroundTracked = false;
+ break;
+ default:
+ break;
+ }
+ switch (newTrackerType)
+ {
+ case TrackerType::Air:
+ AircraftTrackerClass::Instance.Add(this->LinkedTo);
+ break;
+ case TrackerType::Ground:
+ {
+ auto cell = MapClass::Instance.GetCellAt(newMapCrd);
+ auto frozenStill = this->LinkedTo->FrozenStill;
+ this->LinkedTo->FrozenStill = false;
+ cell->AddContent(this->LinkedTo, newAlt);
+ this->LinkedTo->FrozenStill = frozenStill;
+ }
+ break;
+ case TrackerType::Underground:
+ ScenarioExt::Global()->UndergroundTracker.AddUnique(this->LinkedTo);
+ TechnoExt::ExtMap.Find(this->LinkedTo)->UndergroundTracked = true;
+ break;
+ default:
+ break;
+ }
+ }
+ else
+ {
+ switch (newTrackerType)
+ {
+ case Air:
+ if (oldFlightMapCrd != newMapCrd)
+ {
+ AircraftTrackerClass::Instance.Update(this->LinkedTo, oldFlightMapCrd, newMapCrd);
+ }
+ break;
+ case Ground:
+ if (oldMapCrd != newMapCrd)
+ {
+ auto oldCell = MapClass::Instance.GetCellAt(oldMapCrd);
+ auto newCell = MapClass::Instance.GetCellAt(newMapCrd);
+ auto frozenStill = this->LinkedTo->FrozenStill;
+ this->LinkedTo->FrozenStill = false;
+ oldCell->RemoveContent(this->LinkedTo, oldAlt);
+ this->LinkedTo->FrozenStill = frozenStill;
+ frozenStill = this->LinkedTo->FrozenStill;
+ this->LinkedTo->FrozenStill = false;
+ newCell->AddContent(this->LinkedTo, newAlt);
+ this->LinkedTo->FrozenStill = frozenStill;
+ }
+ break;
+ case Underground:
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (newTrackerType == TrackerType::Ground)
+ {
+ this->LinkedTo->UpdatePosition(PCPType::End);
+ }
+ else if (newTrackerType == TrackerType::Air)
+ {
+ if (this->Piggybacker && IsAirLoco(this->Piggybacker))
+ this->LinkedTo->OnBridge = false; // Air units is always marked as not on bridge.
+ }
+
+ //auto const ext = TechnoExt::ExtMap.Find(this->LinkedTo);
+ //if (ext) ext->SpecialTracked = false;
+ }
+ else
+ {
+ auto oldMapCrd = this->LinkedTo->GetMapCoords();
+ auto oldHeight = this->LinkedTo->GetHeight();
+ auto oldTrackerType = GetTrackerTypeForHeight(oldHeight);
+ auto oldFlightMapCrd = this->LinkedTo->GetLastFlightMapCoords();
+ bool oldAlt = this->LinkedTo->OnBridge;
+
+ switch (oldTrackerType)
+ {
+ case TrackerType::Air:
+ AircraftTrackerClass::Instance.Remove(this->LinkedTo);
+ break;
+ case TrackerType::Ground:
+ {
+ auto oldCell = MapClass::Instance.GetCellAt(oldMapCrd);
+ auto frozenStill = this->LinkedTo->FrozenStill;
+ this->LinkedTo->FrozenStill = false;
+ oldCell->RemoveContent(this->LinkedTo, oldAlt);
+ this->LinkedTo->FrozenStill = frozenStill;
+ break;
+ }
+ case TrackerType::Underground:
+ ScenarioExt::Global()->UndergroundTracker.Remove(this->LinkedTo);
+ TechnoExt::ExtMap.Find(this->LinkedTo)->UndergroundTracked = false;
+ break;
+ default:
+ break;
+ }
+ }
+
+ // Process
+ {
+ this->Schedule->FinishShiftProcess(this->LinkedTo);
+ }
+
+ // End piggyback
+ {
+ auto pExt = TechnoExt::ExtMap.Find(this->LinkedTo);
+ pExt->ShiftApplier = nullptr;
+ pExt->ShiftApplierHouse = nullptr;
+ this->LinkedTo->EnterIdleMode(false, false);
+ }
+ }
+
+ this->IsShifting = false;
+ this->Elapsed = 0;
+}
+
+bool ShiftLocomotionClass::Is_Moving()
+{
+ return this->IsShifting;
+}
+
+CoordStruct ShiftLocomotionClass::Destination()
+{
+ if (this->Schedule)
+ {
+ return this->Schedule->End.Position;
+ }
+ return this->LinkedTo ? this->LinkedTo->GetCenterCoords() : CoordStruct::Empty;
+}
+
+CoordStruct ShiftLocomotionClass::Head_To_Coord()
+{
+ if (this->Schedule)
+ {
+ return this->Schedule->End.Position;
+ }
+ return this->LinkedTo ? this->LinkedTo->GetCenterCoords() : CoordStruct::Empty;
+}
+
+bool ShiftLocomotionClass::Process()
+{
+ if (!this->IsShifting || !this->LinkedTo || !this->Schedule)
+ return false;
+
+ auto sample = this->Schedule->SampleAt(this->Elapsed);
+
+ // Move to a location outside of the map is not allowed could lead to Fatal Errors.
+ // Might need more investigation.
+ if (!MapClass::Instance.IsWithinUsableArea(sample.Position))
+ {
+ this->FinishShift(false);
+ this->LinkedTo->ReceiveDamage(&(this->LinkedTo->Health), 0, RulesClass::Instance->C4Warhead, nullptr, true, true, nullptr);
+ return false;
+ }
+
+ auto oldMapCrd = this->LinkedTo->GetMapCoords();
+ auto oldHeight = this->LinkedTo->GetHeight();
+ auto oldTrackerType = GetTrackerTypeForHeight(oldHeight);
+ auto oldFlightMapCrd = this->LinkedTo->GetLastFlightMapCoords();
+ bool oldAlt = this->LinkedTo->OnBridge;
+
+ bool isOnMap = this->LinkedTo->IsOnMap;
+ this->LinkedTo->IsOnMap = false;
+ this->LinkedTo->SetLocation(sample.Position);
+ this->LinkedTo->IsOnMap = isOnMap;
+ this->LinkedTo->OnBridge = this->LinkedTo->GetCell()->ContainsBridge() && this->LinkedTo->Location.Z >= MapClass::Instance.GetCellFloorHeight(this->LinkedTo->Location) + CellClass::BridgeHeight;
+
+ this->LinkedTo->PrimaryFacing.SetCurrent(sample.Facing);
+ auto newMapCrd = this->LinkedTo->GetMapCoords();
+ auto newHeight = this->LinkedTo->GetHeight();
+ auto newTrackerType = GetTrackerTypeForHeight(newHeight);
+ bool newAlt = this->LinkedTo->OnBridge;
+
+ if (oldTrackerType != newTrackerType)
+ {
+ switch (oldTrackerType)
+ {
+ case TrackerType::Air:
+ AircraftTrackerClass::Instance.Remove(this->LinkedTo);
+ break;
+ case TrackerType::Ground:
+ {
+ auto oldCell = MapClass::Instance.GetCellAt(oldMapCrd);
+ auto frozenStill = this->LinkedTo->FrozenStill;
+ this->LinkedTo->FrozenStill = false;
+ oldCell->RemoveContent(this->LinkedTo, oldAlt);
+ this->LinkedTo->FrozenStill = frozenStill;
+ break;
+ }
+ case TrackerType::Underground:
+ ScenarioExt::Global()->UndergroundTracker.Remove(this->LinkedTo);
+ TechnoExt::ExtMap.Find(this->LinkedTo)->UndergroundTracked = false;
+ break;
+ default:
+ break;
+ }
+ switch (newTrackerType)
+ {
+ case TrackerType::Air:
+ AircraftTrackerClass::Instance.Add(this->LinkedTo);
+ break;
+ case TrackerType::Ground:
+ {
+ auto cell = MapClass::Instance.GetCellAt(newMapCrd);
+ auto frozenStill = this->LinkedTo->FrozenStill;
+ this->LinkedTo->FrozenStill = false;
+ cell->AddContent(this->LinkedTo, newAlt);
+ this->LinkedTo->FrozenStill = frozenStill;
+ }
+ break;
+ case TrackerType::Underground:
+ ScenarioExt::Global()->UndergroundTracker.AddUnique(this->LinkedTo);
+ TechnoExt::ExtMap.Find(this->LinkedTo)->UndergroundTracked = true;
+ break;
+ default:
+ break;
+ }
+ }
+ else
+ {
+ switch (newTrackerType)
+ {
+ case Air:
+ if (oldFlightMapCrd != newMapCrd)
+ {
+ AircraftTrackerClass::Instance.Update(this->LinkedTo, oldFlightMapCrd, newMapCrd);
+ }
+ break;
+ case Ground:
+ if (oldMapCrd != newMapCrd)
+ {
+ auto oldCell = MapClass::Instance.GetCellAt(oldMapCrd);
+ auto newCell = MapClass::Instance.GetCellAt(newMapCrd);
+ auto frozenStill = this->LinkedTo->FrozenStill;
+ this->LinkedTo->FrozenStill = false;
+ oldCell->RemoveContent(this->LinkedTo, oldAlt);
+ this->LinkedTo->FrozenStill = frozenStill;
+ frozenStill = this->LinkedTo->FrozenStill;
+ this->LinkedTo->FrozenStill = false;
+ newCell->AddContent(this->LinkedTo, newAlt);
+ this->LinkedTo->FrozenStill = frozenStill;
+ }
+ break;
+ case Underground:
+ break;
+ default:
+ break;
+ }
+ }
+
+ this->Schedule->DuringShiftProcess(this->LinkedTo);
+
+ this->Elapsed++;
+
+ if (sample.Finished)
+ {
+ this->FinishShift(true);
+ return false;
+ }
+
+ return true;
+}
+
+void _stdcall ShiftLocomotionClass::Move_To(CoordStruct /*to*/)
+{
+ if (this->Schedule && this->Schedule->End.Position != CoordStruct::Empty)
+ this->IsShifting = true;
+}
+
+HRESULT __stdcall ShiftLocomotionClass::Link_To_Object(void* pointer)
+{
+ auto result = LocomotionClass::Link_To_Object(pointer);
+
+ if (SUCCEEDED(result))
+ {
+ auto ext = TechnoExt::ExtMap.Find(this->LinkedTo);
+
+ if (ext && ext->QueuedShift)
+ {
+ BeginShift(std::move(ext->QueuedShift));
+ ext->QueuedShift = nullptr;
+ }
+ else
+ {
+ Debug::Log("ShiftLocomotionClass linked to object without queued shift schedule.");
+ }
+ }
+
+ return result;
+}
+
+// IPiggyback
+HRESULT __stdcall ShiftLocomotionClass::Begin_Piggyback(ILocomotion* pointer)
+{
+ if (!pointer)
+ return E_POINTER;
+
+ if (this->Piggybacker)
+ return E_FAIL;
+
+ pointer->Mark_All_Occupation_Bits(MarkType::Up);
+
+ this->Piggybacker = pointer;
+ pointer->AddRef();
+
+ return S_OK;
+}
+
+HRESULT __stdcall ShiftLocomotionClass::End_Piggyback(ILocomotion** pointer)
+{
+ if (!pointer)
+ return E_POINTER;
+
+ if (!this->Piggybacker)
+ return S_FALSE;
+
+ *pointer = this->Piggybacker.Detach();
+
+ const auto pLinkedTo = this->LinkedTo;
+
+ if (!pLinkedTo->Deactivated && !pLinkedTo->IsUnderEMP())
+ this->Power_On();
+ else
+ this->Power_Off();
+
+ // Handle HeadToCrd
+ {
+ (*pointer)->Force_Immediate_Destination(CoordStruct::Empty); // For mech and walk
+
+ if ((*pointer)->Is_Moving())
+ {
+ auto oldHeadToCrd = (*pointer)->Head_To_Coord();
+ auto offset = oldHeadToCrd - this->Schedule->Start.Position;
+ (*pointer)->Force_Track((*pointer)->Get_Track_Number(), this->Schedule->End.Position + offset); // For drive and ship
+ }
+
+ if (auto dest = pLinkedTo->Destination)
+ {
+ pLinkedTo->SetDestination(nullptr, false);
+ pLinkedTo->SetDestination(dest, false);
+ }
+ }
+
+ return S_OK;
+}
+
+bool __stdcall ShiftLocomotionClass::Is_Ok_To_End()
+{
+ return !this->Is_Moving() && this->Piggybacker;
+}
+
+HRESULT __stdcall ShiftLocomotionClass::Piggyback_CLSID(GUID* classid)
+{
+ if (classid == nullptr)
+ return E_POINTER;
+
+ if (this->Piggybacker)
+ {
+ IPersistStreamPtr piggyAsPersist(this->Piggybacker);
+ return piggyAsPersist->GetClassID(classid);
+ }
+
+ if (reinterpret_cast(this) == nullptr)
+ return E_FAIL;
+
+ IPersistStreamPtr thisAsPersist(this);
+
+ if (thisAsPersist == nullptr)
+ return E_FAIL;
+
+ return thisAsPersist->GetClassID(classid);
+}
+
+bool __stdcall ShiftLocomotionClass::Is_Piggybacking()
+{
+ return this->Piggybacker != nullptr;
+}
+
+static ShiftSchedule::Sample MakeSampleFromObject(FootClass* obj)
+{
+ ShiftSchedule::Sample s;
+ if (obj)
+ {
+ s.Position = obj->GetCoords();
+ s.Facing = obj->PrimaryFacing.Current();
+ s.Pitch = 0.0f;
+ s.Roll = 0.0f;
+ s.Finished = false;
+ }
+ else
+ {
+ s.Position = CoordStruct::Empty;
+ s.Facing = DirStruct{ DirType::North };
+ s.Pitch = 0.0f;
+ s.Roll = 0.0f;
+ s.Finished = false;
+ }
+ return s;
+}
+
+Layer ShiftLocomotionClass::In_Which_Layer()
+{
+ auto height = this->LinkedTo->GetHeight();
+
+ if (height >= 208)
+ return Layer::Air;
+ else if (height >= 0)
+ return Layer::Ground;
+ else
+ return Layer::Underground;
+}
+
+FireError ShiftLocomotionClass::Can_Fire()
+{
+ if (this->Schedule)
+ {
+ if (auto cb = this->Schedule->CanFireCallback)
+ return cb(this->LinkedTo);
+ }
+
+ return FireError::OK;
+}
+
+void ShiftLocomotionClass::Mark_All_Occupation_Bits(MarkType mark)
+{
+ if (!this->LinkedTo)
+ return;
+
+ if (this->Schedule)
+ {
+ CoordStruct head = this->Schedule->End.Position;
+
+ if (mark != MarkType::Up)
+ this->LinkedTo->MarkAllOccupationBits(head);
+ else
+ this->LinkedTo->UnmarkAllOccupationBits(head);
+ }
+}
+
+void ShiftLocomotionClass::Limbo()
+{
+ this->FinishShift(false);
+}
diff --git a/src/Locomotion/ShiftLocomotionClass.h b/src/Locomotion/ShiftLocomotionClass.h
new file mode 100644
index 0000000000..67c4a88142
--- /dev/null
+++ b/src/Locomotion/ShiftLocomotionClass.h
@@ -0,0 +1,179 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+#include
+
+class __declspec(uuid("8A3F6C0F-5E2C-4A6F-9C6D-6E9B3F4A1B2E"))
+ ShiftLocomotionClass : public LocomotionClass, public IPiggyback
+{
+public:
+ // IUnknown
+ virtual HRESULT __stdcall QueryInterface(REFIID iid, LPVOID* ppvObject) override
+ {
+ HRESULT hr = this->LocomotionClass::QueryInterface(iid, ppvObject);
+
+ if (hr != E_NOINTERFACE)
+ return hr;
+
+ if (iid == __uuidof(IPiggyback))
+ {
+ *ppvObject = static_cast(this);
+ }
+
+ if (*ppvObject)
+ {
+ this->AddRef();
+
+ return S_OK;
+ }
+
+ return E_NOINTERFACE;
+ }
+ virtual ULONG __stdcall AddRef() override { return this->LocomotionClass::AddRef(); }
+ virtual ULONG __stdcall Release() override { return this->LocomotionClass::Release(); }
+
+ // IPersist
+ virtual HRESULT __stdcall GetClassID(CLSID* pClassID) override
+ {
+ if (pClassID == nullptr)
+ return E_POINTER;
+
+ *pClassID = __uuidof(this);
+
+ return S_OK;
+ }
+
+ // IPersistStream
+// virtual HRESULT __stdcall IsDirty() override { return !this->Dirty; }
+ virtual HRESULT __stdcall Load(IStream* pStm) override
+ {
+ HRESULT hr = this->LocomotionClass::Load(pStm);
+
+ if (FAILED(hr))
+ return hr;
+
+ if (this)
+ {
+ this->Piggybacker.Detach();
+ new (this) ShiftLocomotionClass(noinit_t());
+ }
+
+ bool piggybackerPresent = false;
+ hr = pStm->Read(&piggybackerPresent, sizeof(piggybackerPresent), nullptr);
+
+ if (!piggybackerPresent)
+ return hr;
+
+ hr = OleLoadFromStream(pStm, __uuidof(ILocomotion), reinterpret_cast(&this->Piggybacker));
+
+ return hr;
+ }
+ virtual HRESULT __stdcall Save(IStream* pStm, BOOL fClearDirty) override
+ {
+ HRESULT hr = this->LocomotionClass::Save(pStm, fClearDirty);
+
+ if (FAILED(hr))
+ return hr;
+
+ bool piggybackerPresent = this->Piggybacker != nullptr;
+ hr = pStm->Write(&piggybackerPresent, sizeof(piggybackerPresent), nullptr);
+
+ if (!piggybackerPresent)
+ return hr;
+
+ IPersistStreamPtr piggyPersist(this->Piggybacker);
+ hr = OleSaveToStream(piggyPersist, pStm);
+
+ return hr;
+ }
+
+ virtual HRESULT __stdcall Begin_Piggyback(ILocomotion* pointer) override;
+ virtual HRESULT __stdcall End_Piggyback(ILocomotion** pointer) override;
+ virtual bool __stdcall Is_Ok_To_End() override;
+ virtual HRESULT __stdcall Piggyback_CLSID(GUID* classid) override;
+ virtual bool __stdcall Is_Piggybacking() override;
+
+ virtual bool __stdcall Is_Moving() override;
+ virtual CoordStruct __stdcall Destination() override;
+ virtual CoordStruct __stdcall Head_To_Coord() override;
+ virtual bool __stdcall Process() override;
+ virtual void __stdcall Move_To(CoordStruct to) override; // Not allowed
+ // virtual void __stdcall Stop_Moving() override; // Not allowed
+ virtual HRESULT __stdcall Link_To_Object(void* pointer) override;
+ // virtual Move __stdcall Can_Enter_Cell(CellStruct cell) override;
+ // virtual bool __stdcall Is_To_Have_Shadow() override;
+ // virtual Matrix3D __stdcall Draw_Matrix(VoxelIndexKey* pIndex) override; // TODO
+ // virtual Matrix3D __stdcall Shadow_Matrix(VoxelIndexKey* pIndex) override; // TODO
+ // virtual Point2D __stdcall Draw_Point() override;
+ // virtual Point2D __stdcall Shadow_Point() override;
+ // virtual VisualType __stdcall Visual_Character(bool raw) override;
+ // virtual int __stdcall Z_Adjust() override;
+ // virtual ZGradient __stdcall Z_Gradient() override;
+ // virtual void __stdcall Do_Turn(DirStruct coord) override;
+ // virtual void __stdcall Unlimbo() override;
+ // virtual void __stdcall Tilt_Pitch_AI() override; // TODO
+ // virtual bool __stdcall Power_On() override;
+ // virtual bool __stdcall Power_Off() override;
+ // virtual bool __stdcall Is_Powered() override;
+ // virtual bool __stdcall Is_Ion_Sensitive() override;
+ // virtual bool __stdcall Push(DirStruct dir) override; // Not allowed
+ // virtual bool __stdcall Shove(DirStruct dir) override;
+ // virtual void __stdcall Force_Track(int track, CoordStruct coord) override; // Not allowed
+ virtual Layer __stdcall In_Which_Layer() override;
+ // virtual void __stdcall Force_Immediate_Destination(CoordStruct coord) override; // Not allowed
+ // virtual void __stdcall Force_New_Slope(int ramp) override; // Not allowed
+ // virtual bool __stdcall Is_Moving_Now() override;
+ // virtual int __stdcall Apparent_Speed() override; // TODO
+ // virtual int __stdcall Drawing_Code() override;
+ virtual FireError __stdcall Can_Fire() override;
+ // virtual int __stdcall Get_Status() override;
+ // virtual void __stdcall Acquire_Hunter_Seeker_Target() override; // Not allowed
+ // virtual bool __stdcall Is_Surfacing() override;
+ virtual void __stdcall Mark_All_Occupation_Bits(MarkType mark) override;
+ // virtual bool __stdcall Is_Moving_Here(CoordStruct to) override;
+ // virtual bool __stdcall Will_Jump_Tracks() override;
+ // virtual bool __stdcall Is_Really_Moving_Now() override;
+ // virtual void __stdcall Stop_Movement_Animation() override;
+ virtual void __stdcall Limbo() override;
+ // virtual void __stdcall Lock() override;
+ // virtual void __stdcall Unlock() override;
+ // virtual int __stdcall Get_Track_Number() override;
+ // virtual int __stdcall Get_Track_Index() override;
+ // virtual int __stdcall Get_Speed_Accum() override;
+
+ virtual int Size() override { return sizeof(*this); }
+
+public:
+ inline ShiftLocomotionClass() : LocomotionClass{}
+ , Elapsed{ 0 }
+ , IsShifting{ false }
+ , Piggybacker{ nullptr }
+ { }
+
+ inline ShiftLocomotionClass(noinit_t) : LocomotionClass{ noinit_t() } { }
+
+ // begin the shift using the schedule (takes ownership)
+ void BeginShift(std::unique_ptr schedule);
+
+ // finish current shift (internal helper)
+ void FinishShift(bool normal = true);
+
+ static bool IsAirLoco(ILocomotion* pLoco);
+ static CoordStruct FindShiftDestination(FootClass* pTechno, CoordStruct idealDest, double searchRange = 5.0, bool pathReachable = false);
+
+public:
+
+ std::unique_ptr Schedule;
+ int Elapsed; // elapsed frames since shift started
+ bool IsShifting;
+ ILocomotionPtr Piggybacker;
+};
diff --git a/src/New/Entity/ShiftSchedule.cpp b/src/New/Entity/ShiftSchedule.cpp
new file mode 100644
index 0000000000..fe20075b40
--- /dev/null
+++ b/src/New/Entity/ShiftSchedule.cpp
@@ -0,0 +1,373 @@
+#include "ShiftSchedule.h"
+
+void ShiftSchedule::BeginShiftProcess(TechnoClass* pThis)
+{
+ auto pExt = TechnoExt::ExtMap.Find(pThis);
+ for (auto const& cb : BeginCallbacks)
+ {
+ if (cb)
+ cb(pThis, pExt->ShiftApplier, pExt->ShiftApplierHouse);
+ }
+}
+
+void ShiftSchedule::DuringShiftProcess(TechnoClass* pThis)
+{
+ auto pExt = TechnoExt::ExtMap.Find(pThis);
+ for (auto const& cb : DuringCallbacks)
+ {
+ if (cb)
+ cb(pThis, pExt->ShiftApplier, pExt->ShiftApplierHouse);
+ }
+}
+
+void ShiftSchedule::FinishShiftProcess(TechnoClass* pThis)
+{
+ auto pExt = TechnoExt::ExtMap.Find(pThis);
+ for (auto const& cb : FinishCallbacks)
+ {
+ if (cb)
+ cb(pThis, pExt->ShiftApplier, pExt->ShiftApplierHouse);
+ }
+}
+
+InstantShiftSchedule::InstantShiftSchedule(const Sample& start, const Sample& end,
+ void* /*params*/,
+ const std::vector& beginCallbacks,
+ const std::vector& finishCallbacks) noexcept
+ : ShiftSchedule(start, end, beginCallbacks, finishCallbacks)
+{
+ // Nothing to do here.
+}
+
+ShiftSchedule::Sample InstantShiftSchedule::SampleAt(int elapsedFrames) const
+{
+ ShiftSchedule::Sample s;
+
+ if (elapsedFrames <= 0)
+ {
+ s = Start;
+ s.Finished = false;
+ return s;
+ }
+
+ s = End;
+ s.Finished = true;
+ return s;
+}
+
+LinearShiftSchedule::LinearShiftSchedule(const Sample& start, const Sample& end,
+ void* params,
+ const std::vector& beginCallbacks,
+ const std::vector& duringCallbacks,
+ const std::vector& finishCallbacks,
+ const ShiftCanFireCallback canFireCallback) noexcept
+ : ShiftSchedule(start, end, beginCallbacks, duringCallbacks, finishCallbacks, canFireCallback)
+{
+ if (params)
+ {
+ auto p = static_cast(params);
+ Speed = p->Speed;
+ }
+}
+
+ShiftSchedule::Sample LinearShiftSchedule::SampleAt(int elapsedFrames) const
+{
+ ShiftSchedule::Sample s;
+
+ if (Speed <= 0)
+ {
+ if (elapsedFrames <= 0)
+ return Start;
+ s = End;
+ s.Finished = true;
+ return s;
+ }
+
+ // Compute horizontal distance (XY plane)
+ const double dx = static_cast(End.Position.X - Start.Position.X);
+ const double dy = static_cast(End.Position.Y - Start.Position.Y);
+ const double dist = std::hypot(dx, dy);
+
+ // If distance is 0, only consider Z or complete immediately
+ if (dist <= 0.0)
+ {
+ if (elapsedFrames <= 0)
+ return Start;
+ s = End;
+ s.Finished = true;
+ return s;
+ }
+
+ // Distance covered (leptons) is given by speed * elapsedFrames
+ double covered = static_cast(Speed) * static_cast(elapsedFrames);
+ double frac = covered / dist;
+ if (frac >= 1.0)
+ {
+ s = End;
+ s.Finished = true;
+ return s;
+ }
+
+ // Use frac to interpolate
+ auto lerp = [](int a, int b, double tt) -> int
+ {
+ return static_cast(std::llround(a + (b - a) * tt));
+ };
+
+ s.Position.X = lerp(Start.Position.X, End.Position.X, frac);
+ s.Position.Y = lerp(Start.Position.Y, End.Position.Y, frac);
+ s.Position.Z = lerp(Start.Position.Z, End.Position.Z, frac);
+
+ s.Facing = Start.Facing;
+
+ s.Pitch = Start.Pitch + static_cast((End.Pitch - Start.Pitch) * frac);
+ s.Roll = Start.Roll + static_cast((End.Roll - Start.Roll) * frac);
+
+ s.Finished = false;
+ return s;
+}
+
+ParabolaShiftSchedule::ParabolaShiftSchedule(const Sample& start, const Sample& end,
+ void* params,
+ const std::vector& beginCallbacks,
+ const std::vector& duringCallbacks,
+ const std::vector& finishCallbacks,
+ const ShiftCanFireCallback canFireCallback) noexcept
+ : ShiftSchedule(start, end, beginCallbacks, duringCallbacks, finishCallbacks, canFireCallback)
+{
+ if (params)
+ {
+ auto p = static_cast(params);
+ InitialAngleDeg = p->InitialAngle;
+ InitialHorizSpeed = p->InitialHorizSpeed;
+ }
+ else
+ {
+ InitialAngleDeg = 0.0;
+ InitialHorizSpeed = 0;
+ }
+
+ const double angleRad = Math::deg2rad(InitialAngleDeg);
+ const double v_h = static_cast(InitialHorizSpeed);
+
+ const double dx = static_cast(End.Position.X - Start.Position.X);
+ const double dy = static_cast(End.Position.Y - Start.Position.Y);
+ const double horizDist = std::hypot(dx, dy);
+
+ DurationFrames = 0;
+ Gravity = 1.0;
+
+ if (v_h > 0.0 && horizDist > 0.0)
+ {
+ const double time = horizDist / v_h;
+ DurationFrames = static_cast(std::max(1, static_cast(std::ceil(time))));
+ const double v_v = v_h * std::tan(angleRad);
+ const double z0 = static_cast(Start.Position.Z);
+ const double z1 = static_cast(End.Position.Z);
+ const double T = time;
+ if (T > 0.0)
+ {
+ Gravity = 2.0 * (v_v * T + z0 - z1) / (T * T);
+ }
+ }
+ else
+ {
+ if (InitialHorizSpeed != 0)
+ DurationFrames = 1;
+ else
+ DurationFrames = 0;
+ Gravity = 1.0;
+ }
+}
+
+ShiftSchedule::Sample ParabolaShiftSchedule::SampleAt(int elapsedFrames) const
+{
+ ShiftSchedule::Sample s;
+
+ if (DurationFrames <= 0)
+ {
+ if (elapsedFrames <= 0)
+ return Start;
+ return End;
+ }
+
+ if (elapsedFrames <= 0) elapsedFrames = 0;
+ if (elapsedFrames >= DurationFrames) elapsedFrames = DurationFrames;
+
+ double t = static_cast(elapsedFrames);
+
+ const double dx = static_cast(End.Position.X - Start.Position.X);
+ const double dy = static_cast(End.Position.Y - Start.Position.Y);
+ const double horizDist = std::hypot(dx, dy);
+ double dirX = 0.0;
+ double dirY = 0.0;
+ if (horizDist > 0.0)
+ {
+ dirX = dx / horizDist;
+ dirY = dy / horizDist;
+ }
+
+ const double angleRad = Math::deg2rad(InitialAngleDeg);
+ const double v_h = static_cast(InitialHorizSpeed);
+ const double v_v = v_h * std::tan(angleRad);
+
+ s.Position.X = static_cast(std::llround(Start.Position.X + dirX * v_h * t));
+ s.Position.Y = static_cast(std::llround(Start.Position.Y + dirY * v_h * t));
+
+ double z = static_cast(Start.Position.Z) + v_v * t - 0.5 * Gravity * t * t;
+ s.Position.Z = static_cast(std::llround(z));
+
+ s.Facing = Start.Facing;
+
+ const double normalized = static_cast(elapsedFrames) / static_cast(DurationFrames);
+ s.Pitch = Start.Pitch + static_cast((End.Pitch - Start.Pitch) * normalized);
+ s.Roll = Start.Roll + static_cast((End.Roll - Start.Roll) * normalized);
+
+ s.Finished = (elapsedFrames >= DurationFrames);
+ return s;
+}
+
+PathShiftSchedule::PathShiftSchedule(const Sample& start, const Sample& end,
+ void* params,
+ const std::vector& beginCallbacks,
+ const std::vector& duringCallbacks,
+ const std::vector& finishCallbacks,
+ const ShiftCanFireCallback canFireCallback) noexcept
+ : ShiftSchedule(start, end, beginCallbacks, duringCallbacks, finishCallbacks, canFireCallback)
+{
+ if (params)
+ {
+ auto p = static_cast(params);
+ PathDirections = p->PathDirections;
+ Speed = p->Speed;
+ Height = p->Height;
+
+ auto currentCrd = start.Position;
+
+ for (auto dir : PathDirections)
+ {
+ auto offset = CellClass::Cell2Coord(CellSpread::GetNeighbourOffset(static_cast(dir)));
+ currentCrd += offset;
+ PathCoords.push_back(currentCrd);
+ }
+ }
+}
+
+ShiftSchedule::Sample PathShiftSchedule::SampleAt(int elapsedFrames) const
+{
+ ShiftSchedule::Sample s;
+
+ // If speed not set or no precomputed path, fallback to instant behavior
+ if (Speed <= 0 || PathCoords.empty())
+ {
+ if (elapsedFrames <= 0)
+ return Start;
+ s = End;
+ s.Finished = true;
+ return s;
+ }
+
+ if (elapsedFrames <= 0)
+ return Start;
+
+ // Total distance along the path (sum of segment lengths)
+ double totalPathLength = 0.0;
+ {
+ double prevX = static_cast(Start.Position.X);
+ double prevY = static_cast(Start.Position.Y);
+ for (const auto& node : PathCoords)
+ {
+ const double nx = static_cast(node.X);
+ const double ny = static_cast(node.Y);
+ totalPathLength += std::hypot(nx - prevX, ny - prevY);
+ prevX = nx;
+ prevY = ny;
+ }
+
+ // Add final segment from last path node to End.Position
+ const double endX = static_cast(End.Position.X);
+ const double endY = static_cast(End.Position.Y);
+ totalPathLength += std::hypot(endX - prevX, endY - prevY);
+ }
+
+ // Distance covered along path in leptons
+ double distanceCovered = static_cast(Speed) * static_cast(elapsedFrames);
+
+ // If we've reached or exceeded the whole path
+ if (distanceCovered >= totalPathLength)
+ {
+ s = End;
+ s.Finished = true;
+ return s;
+ }
+
+ // Walk segments to find current segment and ratio inside it
+ double traversed = 0.0;
+ double currX = static_cast(Start.Position.X);
+ double currY = static_cast(Start.Position.Y);
+
+ // iterate through path nodes, then final segment to End.Position
+ for (size_t i = 0; i <= PathCoords.size(); ++i)
+ {
+ double nodeX;
+ double nodeY;
+
+ if (i < PathCoords.size())
+ {
+ const auto& node = PathCoords[i];
+ nodeX = static_cast(node.X);
+ nodeY = static_cast(node.Y);
+ }
+ else
+ {
+ // final segment target is End.Position
+ nodeX = static_cast(End.Position.X);
+ nodeY = static_cast(End.Position.Y);
+ }
+
+ const double segDx = nodeX - currX;
+ const double segDy = nodeY - currY;
+ const double segLen = std::hypot(segDx, segDy);
+
+ if (segLen <= 0.0)
+ {
+ // zero-length segment, advance
+ currX = nodeX;
+ currY = nodeY;
+ continue;
+ }
+
+ if (traversed + segLen >= distanceCovered)
+ {
+ const double remain = distanceCovered - traversed;
+ const double ratio = segLen > 0.0 ? (remain / segLen) : 0.0;
+
+ const double posX = currX + segDx * ratio;
+ const double posY = currY + segDy * ratio;
+
+ s.Position.X = static_cast(std::llround(posX));
+ s.Position.Y = static_cast(std::llround(posY));
+ // Preserve original Z behaviour (path computed in XY plane)
+ s.Position.Z = Start.Position.Z;
+
+ // Interpolate pitch/roll along full path proportionally
+ const double normalized = distanceCovered / totalPathLength;
+ s.Pitch = Start.Pitch + static_cast((End.Pitch - Start.Pitch) * normalized);
+ s.Roll = Start.Roll + static_cast((End.Roll - Start.Roll) * normalized);
+
+ s.Facing = Start.Facing;
+ s.Finished = false;
+ return s;
+ }
+
+ // advance to next segment
+ traversed += segLen;
+ currX = nodeX;
+ currY = nodeY;
+ }
+
+ // Fallback: if something odd happened, return End
+ s = End;
+ s.Finished = true;
+ return s;
+}
diff --git a/src/New/Entity/ShiftSchedule.h b/src/New/Entity/ShiftSchedule.h
new file mode 100644
index 0000000000..8a9dc5e9c6
--- /dev/null
+++ b/src/New/Entity/ShiftSchedule.h
@@ -0,0 +1,186 @@
+#pragma once
+
+#include
+#include
+
+#include
+
+#include
+#include
+
+// Position shifting schedule
+struct ShiftSchedule
+{
+public:
+ struct Sample
+ {
+ CoordStruct Position;
+ DirStruct Facing;
+ float Pitch; // forward/back tilt in degrees
+ float Roll; // sideways tilt in degrees
+ bool Finished;
+
+ Sample()
+ : Position { }
+ , Facing { }
+ , Pitch { 0.0f }
+ , Roll { 0.0f }
+ , Finished { false }
+ { }
+
+ Sample(CoordStruct position, DirStruct facing, float pitch = 0.0f, float roll = 0.0f, bool finished = false)
+ {
+ Position = position;
+ Facing = facing;
+ Pitch = pitch;
+ Roll = roll;
+ Finished = finished;
+ }
+ };
+
+ typedef std::function ShiftProcessCallback;
+ typedef std::function ShiftCanFireCallback;
+
+ ShiftSchedule() noexcept = default;
+
+ ShiftSchedule(const Sample& start, const Sample& end,
+ const std::vector& beginCallbacks = {},
+ const std::vector& duringCallbacks = {},
+ const std::vector& finishCallbacks = {},
+ const ShiftCanFireCallback canFireCallback = nullptr) noexcept
+ : Start(start)
+ , End(end)
+ , BeginCallbacks(beginCallbacks)
+ , DuringCallbacks(duringCallbacks)
+ , FinishCallbacks(finishCallbacks)
+ , CanFireCallback(canFireCallback)
+ { }
+
+ virtual ~ShiftSchedule() = default;
+
+ // Decide the location and other status of the techno by frame.
+ virtual Sample SampleAt(int elapsedFrames) const { return Sample(); };
+
+ // Process callbacks.
+ void BeginShiftProcess(TechnoClass* pThis);
+ void DuringShiftProcess(TechnoClass* pThis);
+ void FinishShiftProcess(TechnoClass* pThis);
+
+ std::vector BeginCallbacks;
+ std::vector DuringCallbacks;
+ std::vector FinishCallbacks;
+ ShiftCanFireCallback CanFireCallback;
+
+ Sample Start {};
+ Sample End {};
+};
+
+// Instant schedule: teleport — moves to destination on the next frame (elapsed >= 1)
+class InstantShiftSchedule : public ShiftSchedule
+{
+public:
+ InstantShiftSchedule() noexcept = default;
+ virtual ~InstantShiftSchedule() override = default;
+
+ InstantShiftSchedule(const Sample& start, const Sample& end,
+ void* params = nullptr,
+ const std::vector& beginCallbacks = {},
+ const std::vector& finishCallbacks = {}) noexcept;
+
+ Sample SampleAt(int elapsedFrames) const override;
+};
+
+struct LinearParams
+{
+ int Speed;
+};
+
+class LinearShiftSchedule : public ShiftSchedule
+{
+public:
+ LinearShiftSchedule() noexcept = default;
+ virtual ~LinearShiftSchedule() override = default;
+
+ LinearShiftSchedule(const Sample& start, const Sample& end,
+ void* params = nullptr,
+ const std::vector& beginCallbacks = {},
+ const std::vector& duringCallbacks = {},
+ const std::vector& finishCallbacks = {},
+ const ShiftCanFireCallback canFireCallback = nullptr) noexcept;
+
+ Sample SampleAt(int elapsedFrames) const override;
+
+private:
+ int Speed { 0 }; // leptons / frame
+};
+
+struct ParabolaParams
+{
+public:
+
+ double InitialAngle; // degrees
+ int InitialHorizSpeed; // leptons / frame (horizontal component, constant during flight)
+
+ ParabolaParams(double initialAngleDeg, int initialHorizSpeed)
+ {
+ InitialAngle = initialAngleDeg;
+ InitialHorizSpeed = initialHorizSpeed;
+ }
+};
+
+class ParabolaShiftSchedule : public ShiftSchedule
+{
+public:
+ ParabolaShiftSchedule() noexcept = default;
+ virtual ~ParabolaShiftSchedule() override = default;
+
+ ParabolaShiftSchedule(const Sample& start, const Sample& end,
+ void* params = nullptr,
+ const std::vector& beginCallbacks = {},
+ const std::vector& duringCallbacks = {},
+ const std::vector& finishCallbacks = {},
+ const ShiftCanFireCallback canFireCallback = nullptr) noexcept;
+
+ Sample SampleAt(int elapsedFrames) const override;
+
+private:
+ int DurationFrames { 0 };
+ double InitialAngleDeg { 0.0 };
+ int InitialHorizSpeed { 0 };
+
+ double Gravity { 1.0 };
+};
+
+struct PathParams
+{
+public:
+ std::vector PathDirections; // The path to follow
+ int Speed; // leptons / frame
+ int Height;
+
+ PathParams() : Speed(0), Height(0) { }
+ PathParams(const std::vector& pathDirections, int speed, int height)
+ : PathDirections(pathDirections), Speed(speed), Height(height) { }
+};
+
+class PathShiftSchedule : public ShiftSchedule
+{
+public:
+ PathShiftSchedule() noexcept = default;
+ virtual ~PathShiftSchedule() override = default;
+
+ PathShiftSchedule(const Sample& start, const Sample& end,
+ void* params = nullptr,
+ const std::vector& beginCallbacks = {},
+ const std::vector& duringCallbacks = {},
+ const std::vector& finishCallbacks = {},
+ const ShiftCanFireCallback canFireCallback = nullptr) noexcept;
+
+ Sample SampleAt(int elapsedFrames) const override;
+
+private:
+ std::vector PathDirections { };
+ int Speed { 0 }; // leptons / frame
+ int Height { 0 };
+ std::vector PathCoords { };
+};
diff --git a/src/Phobos.COM.cpp b/src/Phobos.COM.cpp
index b31c953dfd..8325140c6b 100644
--- a/src/Phobos.COM.cpp
+++ b/src/Phobos.COM.cpp
@@ -3,18 +3,16 @@
#include
#include
+#include
-#ifdef CUSTOM_LOCO_EXAMPLE_ENABLED // Register the loco
DEFINE_HOOK(0x6BD68D, WinMain_PhobosRegistrations, 0x6)
{
Debug::Log("Starting COM registration...\n");
- // Add new classes to be COM-registered below
- RegisterFactoryForClass();
+ RegisterFactoryForClass();
Debug::Log("COM registration done!\n");
return 0;
}
-#endif
diff --git a/src/Phobos.COM.h b/src/Phobos.COM.h
index ef98e297fe..6838b4a3cf 100644
--- a/src/Phobos.COM.h
+++ b/src/Phobos.COM.h
@@ -13,7 +13,7 @@ void RegisterFactoryForClass(IClassFactory* pFactory)
else
Debug::Log("Class factory for %s registered.\n", typeid(T).name());
- Game::COMClasses->AddItem((ULONG)dwRegister);
+ Game::COMClasses.AddItem((ULONG)dwRegister);
}
// Registers an automatically created factory for a class.