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.