diff --git a/CREDITS.md b/CREDITS.md index 3ecead2d75..89a6234f61 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -159,6 +159,7 @@ This page lists all the individual contributions to the project by their author. - Event 606: AttachEffect is attaching to a Techno - Linked superweapons - Unit & infantry auto-conversion on ammo change + - Script action for repairing destroyed bridges - Restore the ScriptType action#24 `Play speech` from Tiberian Sun - Modify ammo on impact - **Starkku**: diff --git a/Phobos.vcxproj b/Phobos.vcxproj index 67dc28e6ee..2803d2d15a 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -132,6 +132,7 @@ + diff --git a/docs/AI-Scripting-and-Mapping.md b/docs/AI-Scripting-and-Mapping.md index 074d3917e2..e5323cf36c 100644 --- a/docs/AI-Scripting-and-Mapping.md +++ b/docs/AI-Scripting-and-Mapping.md @@ -335,6 +335,24 @@ In `aimd.ini`: x=10104,n ; integer, additional distance in cells ``` +##### `10105` Repair Destroyed Bridge + +- Picks a Bridge Repair Hut from the map that is linked with a bridge with destroyed sections and is reachable by engineers and then send the Taskforce against it. +- Puts nonengineers into Area Guard mode when they arrive near the Bridge Repair Hut location. + +In `aimd.ini`: +```ini +[SOMESCRIPTTYPE] ; ScriptType +x=10105,n ; integer, mode for selecting Bridge Repair Huts +``` +- The possible argument values are: + +| *Argument* | *Target priority* | +| :--------: | :----------------: | +| 0 | Pick the closest | +| 1 | Pick the Farthest | +| -1 | Pick Random | + ### `12000-12999` Suplementary/Setup Pre-actions #### `12000` Wait if No Target Found diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 68b6030d64..05573bfe4d 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -327,6 +327,7 @@ HideShakeEffects=false ; boolean 10102=Regroup Temporarily Around the Team Leader,20,0,1,[LONG DESC] 10103=Load Onto Transports,0,0,1,[LONG DESC] 10104=Chronoshift to Enemy Base,20,0,1,[LONG DESC] + 10105=Repair Destroyed Bridge,20,0,1,[LONG DESC] 14004=Force Global OnlyTargetHouseEnemy value in Teams,20,0,1,[LONG DESC] 18000=Local variable set,22,0,1,[LONG DESC] 18001=Local variable add,22,0,1,[LONG DESC] @@ -577,7 +578,7 @@ HideShakeEffects=false ; boolean - OpenTopped range bonus and damage multiplier customization for passengers (by Ollerus) - AutoDeath upon ownership change (by Ollerus) - [Script Action 14004 for forcing all new actions to target only the main owner's enemy](AI-Scripting-and-Mapping.md#force-global-onlytargethouseenemy-value-in-teams-for-new-attack-move-actions-introduced-by-phobos) (by FS-21) -- [Allow merging AOE damage to buildings into one](New-or-Enhanced-Logics.md#allow-merging-aoe-damage-to-buildings-into-one) (by CrimRecya) +- [Script action for repairing destroyed bridges](AI-Scripting-and-Mapping.md#repair-destroyed-bridge) (by FS-21) - [Allow customizing whether to synchronously change the owner of the RadioLink-linked units when the owner of a building changes](Fixed-or-Improved-Logics.md#custom-whether-to-synchronously-change-the-owner-of-the-radiolink-linked-units-when-the-owner-of-a-building-changes) (by TaranDahl) - [Toggle per-target warhead effects apply timing](New-or-Enhanced-Logics.md#toggle-per-target-warhead-effects-apply-timing) (by TaranDahl) - [Extra range for chasing and pre-firing](New-or-Enhanced-Logics.md#extra-range) (by TaranDahl) diff --git a/src/Ext/Script/Body.BridgeRepair.cpp b/src/Ext/Script/Body.BridgeRepair.cpp new file mode 100644 index 0000000000..178f865060 --- /dev/null +++ b/src/Ext/Script/Body.BridgeRepair.cpp @@ -0,0 +1,260 @@ +#include "Body.h" +#include + +void ScriptExt::RepairDestroyedBridge(TeamClass* pTeam, int mode = -1) +{ + auto pTeamData = TeamExt::Fetch(pTeam); + if (!pTeamData) + return; + + auto pScript = pTeam->CurrentScript; + int currentMission = pScript->CurrentMission; + + // The first time this team runs this kind of script the repair huts list will updated. The only reason of why it isn't stored in ScenarioClass is because always exists the possibility of a modder to make destroyable Repair Huts + if (pTeamData->BridgeRepairHuts.size() == 0) + { + for (auto pTechno : TechnoClass::Array) + { + if (pTechno->WhatAmI() != AbstractType::Building) + continue; + + const auto pBuilding = abstract_cast(pTechno); + if (!pBuilding) + continue; + + if (pBuilding->Type->BridgeRepairHut) + pTeamData->BridgeRepairHuts.push_back(pTechno); + } + + if (pTeamData->BridgeRepairHuts.size() == 0) + { + pTeam->StepCompleted = true; + ScriptExt::Log("AI Scripts - RepairDestroyedBridge: [%s][%s] (line: %d = %d,%d) Jump to next line: %d = %d,%d -> (Reason: No repair huts found).\n", + pTeam->Type->ID, + pScript->Type->ID, + currentMission, + pScript->Type->ScriptActions[currentMission].Action, + pScript->Type->ScriptActions[currentMission].Argument, + currentMission + 1, + pScript->Type->ScriptActions[currentMission + 1].Action, + pScript->Type->ScriptActions[currentMission + 1].Argument); + + return; + } + } + + // Reset Team's target if the current target isn't a repair hut + if (pTeam->Focus) + { + if (pTeam->Focus->WhatAmI() != AbstractType::Building) + { + pTeam->Focus = nullptr; + } + else + { + const auto pBuilding = static_cast(pTeam->Focus); + + if (!pBuilding->Type->BridgeRepairHut) + { + pTeam->Focus = nullptr; + } + else + { + CellStruct cell = pBuilding->GetCell()->MapCoords; + + // If the Bridge was repaired then the repair hut isn't valid anymore + if (!MapClass::Instance.IsLinkedBridgeDestroyed(cell)) + pTeam->Focus = nullptr; + } + } + } + + TechnoClass* selectedTarget = pTeam->Focus ? static_cast(pTeam->Focus) : nullptr; + bool isEngineerAmphibious = false; + std::vector engineers; + std::vector otherTeamMembers; + + // Check if there are no engineers + for (auto pUnit = pTeam->FirstUnit; pUnit; pUnit = pUnit->NextTeamMember) + { + if (!ScriptExt::IsUnitAvailable(pUnit, true)) + continue; + + if (!pTeam->Focus) + { + pUnit->SetTarget(nullptr); + pUnit->SetDestination(nullptr, false); + pUnit->ForceMission(Mission::Guard); + } + + if (pUnit->WhatAmI() == AbstractType::Infantry) + { + const auto pInf = static_cast(pUnit); + + if (pInf->IsEngineer()) + { + if (pUnit->GetTechnoType()->MovementZone == MovementZone::Amphibious + || pUnit->GetTechnoType()->MovementZone == MovementZone::AmphibiousCrusher + || pUnit->GetTechnoType()->MovementZone == MovementZone::AmphibiousDestroyer) + { + isEngineerAmphibious = true; + } + + engineers.push_back(pUnit); + continue; + } + } + + // Non-engineers will receive a different command + otherTeamMembers.push_back(pUnit); + } + + if (engineers.size() == 0) + { + pTeam->StepCompleted = true; + ScriptExt::Log("AI Scripts - RepairDestroyedBridge: [%s][%s] (line: %d = %d,%d) Jump to next line: %d = %d,%d -> (Reason: Team has no engineers).\n", + pTeam->Type->ID, + pScript->Type->ID, + currentMission, + pScript->Type->ScriptActions[currentMission].Action, + pScript->Type->ScriptActions[currentMission].Argument, + currentMission + 1, + pScript->Type->ScriptActions[currentMission + 1].Action, + pScript->Type->ScriptActions[currentMission + 1].Argument); + + return; + } + + std::vector validHuts; + + if (!selectedTarget) + { + for (const auto pTechno : pTeamData->BridgeRepairHuts) + { + CellStruct cell = pTechno->GetCell()->MapCoords; + + // Skip all huts linked to non-destroyed bridges + if (!MapClass::Instance.IsLinkedBridgeDestroyed(cell)) + continue; + + if (isEngineerAmphibious) + { + validHuts.push_back(pTechno); + } + else + { + CoordStruct coords = pTechno->GetCenterCoords(); + + // Only huts reachable by the (first) engineer are valid + if (engineers.at(0)->IsInSameZoneAsCoords(pTechno->GetCenterCoords())) + validHuts.push_back(pTechno); + } + } + + if (validHuts.size() == 0) + { + ScriptExt::Log("AI Scripts - RepairDestroyedBridge: [%s][%s] (line: %d = %d,%d) Jump to next line: %d = %d,%d (Reason: Can not select a bridge repair hut).\n", + pTeam->Type->ID, + pScript->Type->ID, + currentMission, + pScript->Type->ScriptActions[currentMission].Action, + pScript->Type->ScriptActions[currentMission].Argument, + currentMission + 1, + pScript->Type->ScriptActions[currentMission + 1].Action, + pScript->Type->ScriptActions[currentMission + 1].Argument); + + pTeam->StepCompleted = true; + return; + } + + // Find the best repair hut + int bestVal = -1; + + if (mode < 0) + mode = pTeam->CurrentScript->Type->ScriptActions[pTeam->CurrentScript->CurrentMission].Argument; + + if (mode < 0) // Pick a random bridge + { + selectedTarget = validHuts.at(ScenarioClass::Instance->Random.RandomRanged(0, validHuts.size() - 1)); + } + else + { + for (const auto pHut : validHuts) + { + int value = engineers.at(0)->DistanceFrom(pHut); // Note: distance is in leptons (*256) + bool isValidCandidate = false; + + if (mode == 0) + isValidCandidate = value < bestVal; // Pick the closest target + else + isValidCandidate = value >= bestVal; // Pick the farthest target + + if (isValidCandidate || bestVal < 0) + { + bestVal = value; + selectedTarget = pHut; + } + } + } + } + + validHuts.clear(); + + if (!selectedTarget) + { + ScriptExt::Log("AI Scripts - RepairDestroyedBridge: [%s][%s] (line: %d = %d,%d) Jump to next line: %d = %d,%d (Reason: Can not select a bridge repair hut).\n", + pTeam->Type->ID, + pScript->Type->ID, + currentMission, + pScript->Type->ScriptActions[currentMission].Action, + pScript->Type->ScriptActions[currentMission].Argument, + currentMission + 1, + pScript->Type->ScriptActions[currentMission + 1].Action, + pScript->Type->ScriptActions[currentMission + 1].Argument); + + pTeam->StepCompleted = true; + return; + } + + // Setting the team's target & mission + pTeam->Focus = selectedTarget; + + for (auto engineer : engineers) + { + if (engineer->Destination != selectedTarget) + { + engineer->SetTarget(selectedTarget); + engineer->QueueMission(Mission::Capture, true); + } + } + + if (otherTeamMembers.size() > 0) + { + double closeEnough = RulesClass::Instance->CloseEnough; // Note: this value is in leptons (*256) + + for (auto pFoot : otherTeamMembers) + { + if (pTeamData && pTeamData->CloseEnough > 0) + closeEnough = pTeamData->CloseEnough * 256.0; + + if (!pFoot->Destination + || (selectedTarget->DistanceFrom(pFoot->Destination) > closeEnough)) + { + // Reset previous command + pFoot->SetTarget(nullptr); + pFoot->SetDestination(nullptr, false); + pFoot->ForceMission(Mission::Guard); + + // Get a cell near the target + pFoot->QueueMission(Mission::Move, false); + CoordStruct coord = TechnoExt::PassengerKickOutLocation(selectedTarget, pFoot); + CellClass* pCellDestination = MapClass::Instance.TryGetCellAt(coord); + pFoot->SetDestination(pCellDestination, true); + } + + // Reached destination, stay in guard until next action + if (pFoot->DistanceFrom(pFoot->Destination) < closeEnough) + pFoot->QueueMission(Mission::Area_Guard, false); + } + } +} diff --git a/src/Ext/Script/Body.cpp b/src/Ext/Script/Body.cpp index 8380de3b5b..382309838c 100644 --- a/src/Ext/Script/Body.cpp +++ b/src/Ext/Script/Body.cpp @@ -216,6 +216,10 @@ void ScriptExt::ProcessAction(TeamClass* pTeam) // Chronoshift to enemy base, argument is additional distance modifier ScriptExt::ChronoshiftToEnemyBase(pTeam, argument); break; + case PhobosScripts::RepairDestroyedBridge: + // Start Timed Jump that jumps to the same line when the countdown finish (in frames) + ScriptExt::RepairDestroyedBridge(pTeam, -1); + break; case PhobosScripts::ForceGlobalOnlyTargetHouseEnemy: ScriptExt::ForceGlobalOnlyTargetHouseEnemy(pTeam, -1); break; diff --git a/src/Ext/Script/Body.h b/src/Ext/Script/Body.h index f1ce860587..dcebdec4c1 100644 --- a/src/Ext/Script/Body.h +++ b/src/Ext/Script/Body.h @@ -49,6 +49,7 @@ enum class PhobosScripts : unsigned int GatherAroundLeader = 10102, LoadIntoTransports = 10103, ChronoshiftToEnemyBase = 10104, + RepairDestroyedBridge = 10105, // Range 12000-12999 are suplementary/setup pre-actions WaitIfNoTarget = 12000, @@ -230,6 +231,7 @@ class ScriptExt final : public AbstractExt static void VariableBinaryOperationHandler(TeamClass* pTeam, int nVariable, int nVarToOperate); static bool IsUnitAvailable(TechnoClass* pTechno, bool checkIfInTransportOrAbsorbed); static void Log(const char* pFormat, ...); + static void RepairDestroyedBridge(TeamClass* pTeam, int mode); static void PlaySpeech(TeamClass* pTeam); // Mission.Attack.cpp diff --git a/src/Ext/Team/Body.cpp b/src/Ext/Team/Body.cpp index 1d8be2bc29..3b5300e16a 100644 --- a/src/Ext/Team/Body.cpp +++ b/src/Ext/Team/Body.cpp @@ -22,6 +22,7 @@ void TeamExt::Serialize(T& Stm) .Process(this->ForceJump_RepeatMode) .Process(this->TeamLeader) .Process(this->PreviousScriptList) + .Process(this->BridgeRepairHuts) ; } diff --git a/src/Ext/Team/Body.h b/src/Ext/Team/Body.h index 5d6b9916b5..a5e7b3ecbb 100644 --- a/src/Ext/Team/Body.h +++ b/src/Ext/Team/Body.h @@ -37,6 +37,7 @@ class TeamExt final : public AbstractExt, public Detach::Listener bool ForceJump_RepeatMode; FootClass* TeamLeader; std::vector PreviousScriptList; + std::vector BridgeRepairHuts; TeamExt(TeamClass* OwnerObject) : AbstractExt(OwnerObject) , WaitNoTargetAttempts { 0 } @@ -52,6 +53,7 @@ class TeamExt final : public AbstractExt, public Detach::Listener , ForceJump_RepeatMode { false } , TeamLeader { nullptr } , PreviousScriptList { } + , BridgeRepairHuts { } { } virtual ~TeamExt() = default; diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index 1615942f11..e721307cd6 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -242,7 +242,7 @@ double TechnoExt::GetCurrentArmorMultiplier(TechnoClass* pThis, TechnoTypeClass* (pThis->HasAbility(Ability::Stronger) ? RulesClass::Instance->VeteranArmor : 1.0); } -CoordStruct TechnoExt::PassengerKickOutLocation(TechnoClass* pThis, FootClass* pPassenger, int maxAttempts = 1) +CoordStruct TechnoExt::PassengerKickOutLocation(TechnoClass* pThis, FootClass* pPassenger, int maxAttempts) { if (!pThis || !pPassenger) return CoordStruct::Empty; diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h index 57a79aa69e..b25396710e 100644 --- a/src/Ext/Techno/Body.h +++ b/src/Ext/Techno/Body.h @@ -242,7 +242,7 @@ class TechnoExt : public RadioExt, public Detach::Listener static void DrawInsignia(TechnoClass* pThis, Point2D* pLocation, RectangleStruct* pBounds); static void ApplyGainedSelfHeal(TechnoClass* pThis); static void SyncInvulnerability(TechnoClass* pFrom, TechnoClass* pTo); - static CoordStruct PassengerKickOutLocation(TechnoClass* pThis, FootClass* pPassenger, int maxAttempts); + static CoordStruct PassengerKickOutLocation(TechnoClass* pThis, FootClass* pPassenger, int maxAttempts = 1); static bool AllowedTargetByZone(TechnoClass* pThis, TechnoClass* pTarget, TargetZoneScanType zoneScanType, WeaponTypeClass* pWeapon = nullptr, bool useZone = false, int zone = -1); static void UpdateAttachedAnimLayers(TechnoClass* pThis); static bool ConvertToType(FootClass* pThis, TechnoTypeClass* toType);