From a1815f52a5f6aa89d4100098e5a569a7f8d6764d Mon Sep 17 00:00:00 2001 From: FlufflesTheMicrosaur <68170519+FlufflesTheMicrosaur@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:32:22 -0800 Subject: [PATCH 01/14] fix: 105915: incorrect severity assignment and failing to populate all of the fields in the error message (#312) (cherry picked from commit 00a7af388daee5b360b03c28f787f3477b1e7611) --- Alchemy/Include/KernelExceptions.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Alchemy/Include/KernelExceptions.h b/Alchemy/Include/KernelExceptions.h index c45912d8a..433007aec 100644 --- a/Alchemy/Include/KernelExceptions.h +++ b/Alchemy/Include/KernelExceptions.h @@ -219,30 +219,30 @@ class CException { case (0b00): { - CONSTLIT("SUCCESS"); + sSeverity = CONSTLIT("SUCCESS"); break; } case (0b01): { - CONSTLIT("INFO"); + sSeverity = CONSTLIT("INFO"); break; } case (0b10): { - CONSTLIT("WARNING"); + sSeverity = CONSTLIT("WARNING"); break; } case (0b11): { - CONSTLIT("ERROR"); + sSeverity = CONSTLIT("ERROR"); break; } default: - CONSTLIT("INVALID"); + sSeverity = CONSTLIT("INVALID"); } CString sCustomer = byCustomer ? CONSTLIT("Customer") : CONSTLIT("Microsoft"); CString sBad = byReserved ? CONSTLIT("Invalid") : CONSTLIT(""); - m_sMsg = strCat(m_sMsg, strPatternSubst(CONSTLIT(" - NTSTATUS: %x %s (Code by: %s Severity: %s Facility: %x Code: %x)"), dwInfo)); + m_sMsg = strCat(m_sMsg, strPatternSubst(CONSTLIT(" - NTSTATUS: %x (Code by: %s Severity: %s Facility: %x Code: %x)"), dwInfo, sCustomer, sSeverity, wFacility, wCode)); } // everything after this if in the array is junk From 14c9d81d5938c230b6fcd65a632d8942b01f69f9 Mon Sep 17 00:00:00 2001 From: FlufflesTheMicrosaur <68170519+FlufflesTheMicrosaur@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:36:22 -0800 Subject: [PATCH 02/14] Fix: 105978: fix strToDouble using atof (#325) Atof is horrifically out of date and does not handle failures properly, uses strtod instead which also eliminates the need for checking for hexadecimal (cherry picked from commit e5ae747a44641926046b3b383e2433c0602eb88c) --- Alchemy/Kernel/CString.cpp | 66 ++++++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 17 deletions(-) diff --git a/Alchemy/Kernel/CString.cpp b/Alchemy/Kernel/CString.cpp index b093c1dec..2b7ba1d8b 100644 --- a/Alchemy/Kernel/CString.cpp +++ b/Alchemy/Kernel/CString.cpp @@ -2856,40 +2856,72 @@ CString Kernel::strRomanNumeral (int i) } } -double Kernel::strToDouble (const CString &sString, double rFailResult, bool *retbFailed) - // strToDouble // // Converts a string to a double +// Handles Hexadecimal ints as well +// +// Special cases: +// Invalid conversion: +// returns: rFailResult +// retbFailed: true +// Overflow: (ex, 1e500) +// returns: R_INF or R_NINF +// retbFailed: true +// Underflow: (ex, 1e-500) +// returns: 0 +// retbFailed: true +// Explicity infinity: (Ex, "-infinity") +// returns: R_INF or R_NINF +// retbFailed: false +// Explicit NaN: (Ex, "nan" or "nan(\"SpecificNaN\")") +// returns: R_NAN +// retbFailed: false +// +// See strtod for more information +// +double Kernel::strToDouble (const CString &sString, double rFailResult, bool *retbFailed) { - // Check to see if this is a hex integer + // strtod handles hexadecimal natively as well char *pPos = sString.GetASCIIZPointer(); - if (sString.GetLength() > 2 - && pPos[0] == '0' - && (pPos[1] == 'x' || pPos[1] == 'X')) + + char *pStop = NULL; + errno = 0; + + double rResult = ::strtod(pPos, &pStop); + + // Handle fail case: invalid conversion + + if (pPos == pStop || *pStop != '\0') { - bool bFailed; - DWORD dwValue = strToInt(sString, 0, &bFailed); - if (retbFailed) *retbFailed = bFailed; - return (bFailed ? rFailResult : (double)dwValue); + if (retbFailed) + *retbFailed = true; + return rFailResult; } - // Assume a float + // It correctly handles underflows by returning INFINITIES or 0 + // We return these but set retbFailed because it was NOT an explicit + // infinity or 0 - double rResult = ::atof(sString.GetASCIIZPointer()); - if (_isnan(rResult)) + else if (errno == ERANGE) { if (retbFailed) *retbFailed = true; - return rFailResult; + return rResult; } - if (retbFailed) - *retbFailed = false; + // Otherwise we succeeded. If we get a nan or infinity + // its because someone explicitly set that in the string. + + else + { + if (retbFailed) + *retbFailed = false; - return rResult; + return rResult; + } } CString Kernel::strToFilename (const CString &sString) From a720cf56b970d97fa10ec0067b953df554dfd34d Mon Sep 17 00:00:00 2001 From: FlufflesTheMicrosaur <68170519+FlufflesTheMicrosaur@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:42:42 -0800 Subject: [PATCH 03/14] fix: 105767: fix some conditionals so they are more robust against sloppy script inputs or when a player has used scripts or debugtools to overfill their cargo (#329) (cherry picked from commit 091932534c67597845341677f5d6003f09f9a83b) --- Transcendence/TransCore/Compatibility10.xml | 2 +- Transcendence/TransCore/HSCompatibility.xml | 2 +- Transcendence/TransCore/RPGCommoditiesExchange.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Transcendence/TransCore/Compatibility10.xml b/Transcendence/TransCore/Compatibility10.xml index ab9d2a357..c8ae725bc 100644 --- a/Transcendence/TransCore/Compatibility10.xml +++ b/Transcendence/TransCore/Compatibility10.xml @@ -78,7 +78,7 @@ (setq desc (cat desc "This item is not for sale.")) (eq affordCount 0) (setq desc (cat desc "You do not have enough rin to purchase any.")) - (eq fitCount 0) + (leq fitCount 0) (setq desc (cat desc "Unfortunately, you cannot fit any in your cargo hold.")) (and (gr availCount affordCount) (gr fitCount affordCount)) (setq desc (cat desc "You have enough rin to buy " affordCount ".")) diff --git a/Transcendence/TransCore/HSCompatibility.xml b/Transcendence/TransCore/HSCompatibility.xml index 2ca8db7b3..a7b8d8abe 100644 --- a/Transcendence/TransCore/HSCompatibility.xml +++ b/Transcendence/TransCore/HSCompatibility.xml @@ -455,7 +455,7 @@ (setq desc (cat desc "This item is not for sale.")) (eq affordCount 0) (setq desc (cat desc "You do not have enough " (fmtCurrency currencyUsed True) " to purchase any.")) - (eq fitCount 0) + (leq fitCount 0) (setq desc (cat desc "Unfortunately, you cannot fit any in your cargo hold.")) (and (gr availCount affordCount) (gr fitCount affordCount)) (setq desc (cat desc "You have enough " (fmtCurrency currencyUsed True) " to buy " affordCount ".")) diff --git a/Transcendence/TransCore/RPGCommoditiesExchange.xml b/Transcendence/TransCore/RPGCommoditiesExchange.xml index a9a07ff3a..83ae62b10 100644 --- a/Transcendence/TransCore/RPGCommoditiesExchange.xml +++ b/Transcendence/TransCore/RPGCommoditiesExchange.xml @@ -307,7 +307,7 @@ (eq affordCount 0) (scrTranslate gScreen 'descNotEnoughMoney { currency: (fmtCurrency currencyUsed True) }) - (eq fitCount 0) + (leq fitCount 0) (scrTranslate gScreen 'descNotEnoughRoom) (and (gr availCount affordCount) (gr fitCount affordCount)) From eda3f004e6cdfd74a043d30755863a280de2e84d Mon Sep 17 00:00:00 2001 From: FlufflesTheMicrosaur <68170519+FlufflesTheMicrosaur@users.noreply.github.com> Date: Mon, 19 Jan 2026 13:50:57 -0800 Subject: [PATCH 04/14] Chore/106023 rename customer errcode (#330) * chore: 106023: improve error code to use terminology relevant to us rather than to microsoft * chore: 106023: add more comments (cherry picked from commit 7b27169715bea07267763655b086ec4dc67f6a60) --- Alchemy/Include/KernelExceptions.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Alchemy/Include/KernelExceptions.h b/Alchemy/Include/KernelExceptions.h index 433007aec..565406a81 100644 --- a/Alchemy/Include/KernelExceptions.h +++ b/Alchemy/Include/KernelExceptions.h @@ -191,14 +191,14 @@ class CException // Just manually look it up on this page: // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55 // - // If the code doesnt exist there, it may be customer defined. + // If the code doesnt exist there, it may be customer defined. (Customer = non-microsoft code, usually drivers) // We can output some basic metadata about the code too based on the code structure. // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/87fba13e-bf06-450e-83b1-9241dc81e781 // // (Note that this doesnt care about endianness, it is always in this exact bit order // Field masks: // 0b 1100 0000 0000 0000 0000 0000 0000 0000 - severity 00 = success, 01 = info, 10 = warning, 11 = error - // 0b 0010 0000 0000 0000 0000 0000 0000 0000 - 0 = microsoft 1 = customer + // 0b 0010 0000 0000 0000 0000 0000 0000 0000 - 0 = microsoft 1 = customer (3rd party, usually drivers) // 0b 0001 0000 0000 0000 0000 0000 0000 0000 - reserved (should always be 0!) // 0b 0000 1111 1111 1111 0000 0000 0000 0000 - facility - indicates numbering space for the code field // 0b 0000 0000 0000 0000 1111 1111 1111 1111 - code @@ -209,7 +209,7 @@ class CException else if (i == 2) { BYTE bySeverity = dwInfo >> 30 & 0x03; - BYTE byCustomer = dwInfo >> 29 & 0x01; + BYTE byCustomer = dwInfo >> 29 & 0x01; // False if microsoft code, True if 3rd party code (ex, drivers) BYTE byReserved = dwInfo >> 28 & 0x01; // we check this bit in case of invalid customer-set codes or memory corruption WORD wFacility = dwInfo >> 16 & 0x0FFF; WORD wCode = dwInfo & 0x0000FFFF; @@ -240,9 +240,9 @@ class CException default: sSeverity = CONSTLIT("INVALID"); } - CString sCustomer = byCustomer ? CONSTLIT("Customer") : CONSTLIT("Microsoft"); + CString sSource = byCustomer ? CONSTLIT("3rd-Party") : CONSTLIT("Microsoft"); CString sBad = byReserved ? CONSTLIT("Invalid") : CONSTLIT(""); - m_sMsg = strCat(m_sMsg, strPatternSubst(CONSTLIT(" - NTSTATUS: %x (Code by: %s Severity: %s Facility: %x Code: %x)"), dwInfo, sCustomer, sSeverity, wFacility, wCode)); + m_sMsg = strCat(m_sMsg, strPatternSubst(CONSTLIT(" - NTSTATUS: %x (Code by: %s Severity: %s Facility: %x Code: %x)"), dwInfo, sSource, sSeverity, wFacility, wCode)); } // everything after this if in the array is junk From e7d4ed993a79c5e8b0bd75f9d539cbefc8cbb23e Mon Sep 17 00:00:00 2001 From: George Moromisato Date: Mon, 19 Jan 2026 17:34:29 -0800 Subject: [PATCH 05/14] Fix bug with auton maneuvering when attacking a stationary target (cherry picked from commit 46759fcc893bacda353e4f5d25b53bac6bbf15e9) --- Mammoth/Include/TSEShipAI.h | 6 ++++++ Mammoth/TSE/AIManeuvers.cpp | 17 +++-------------- Mammoth/TSE/CShipAIHelper.cpp | 14 ++++++++++++++ .../Transcendence/CManeuverController.cpp | 3 +-- 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/Mammoth/Include/TSEShipAI.h b/Mammoth/Include/TSEShipAI.h index 3fe73892c..61486052b 100644 --- a/Mammoth/Include/TSEShipAI.h +++ b/Mammoth/Include/TSEShipAI.h @@ -527,5 +527,11 @@ class COrderDesc class CShipAIHelper { public: + // When adjusting formation positions, we cheat by this factor if we're + // really close. + + static constexpr Metric CHEAT_FORMATION_FACTOR = 0.2; + + static void ApplyFormationAccel (CShip& Ship, const CVector& vVel, Metric rCheatThrustFactor = CHEAT_FORMATION_FACTOR); static bool CalcFormationParams (CShip *pShip, const CVector &vDestPos, const CVector &vDestVel, CVector *retvRecommendedVel, Metric *retrDeltaPos2 = NULL, Metric *retrDeltaVel2 = NULL); }; diff --git a/Mammoth/TSE/AIManeuvers.cpp b/Mammoth/TSE/AIManeuvers.cpp index 8a97a82b8..9e178015f 100644 --- a/Mammoth/TSE/AIManeuvers.cpp +++ b/Mammoth/TSE/AIManeuvers.cpp @@ -434,15 +434,7 @@ CVector CAIBehaviorCtx::CalcManeuverFormation (CShip *pShip, const CVector vDest if (rCheatThrustFactor && bCloseEnough) { - if (!pShip->IsParalyzed()) - { - pShip->AddForce(vDeltaV * rCheatThrustFactor * max(1.0, pShip->GetMass() / 2000.0)); - -#ifdef DEBUG_ATTACK_TARGET_MANEUVERS - pShip->SetDebugVector(vDeltaV.Normal() * 100. * g_KlicksPerPixel); -#endif - } - + CShipAIHelper::ApplyFormationAccel(*pShip, vDeltaV, rCheatThrustFactor); return CVector(); } @@ -1883,9 +1875,7 @@ void CAIBehaviorCtx::ImplementFormationManeuver (CShip *pShip, const CVector vDe if (bCloseEnough) { - if (!pShip->IsParalyzed()) - pShip->AddForce(vDeltaV * pShip->GetMass() / 2000.0); - + CShipAIHelper::ApplyFormationAccel(*pShip, vDeltaV); ImplementTurnTo(pShip, iDestFacing); } @@ -2262,8 +2252,7 @@ void CAIBehaviorCtx::ImplementStop (CShip *pShip) } else { - if (!pShip->IsParalyzed()) - pShip->AddForce(-pShip->GetVel() * pShip->GetMass() / 2000.0); + CShipAIHelper::ApplyFormationAccel(*pShip, -pShip->GetVel()); } } diff --git a/Mammoth/TSE/CShipAIHelper.cpp b/Mammoth/TSE/CShipAIHelper.cpp index ea917b5a1..f51c329e2 100644 --- a/Mammoth/TSE/CShipAIHelper.cpp +++ b/Mammoth/TSE/CShipAIHelper.cpp @@ -8,6 +8,20 @@ const Metric MAX_DISTANCE = (400 * KLICKS_PER_PIXEL); const Metric CLOSE_DELTA_V_RATIO = 4.0; +void CShipAIHelper::ApplyFormationAccel (CShip& Ship, const CVector& vVel, Metric rCheatThrustFactor) + { + if (!Ship.IsParalyzed()) + { + // Converts from a velocity to a force, taking mass into account. + + Ship.AddForceFromDeltaV(rCheatThrustFactor * vVel); + +#ifdef DEBUG_ATTACK_TARGET_MANEUVERS + Ship.SetDebugVector(vVel * g_KlicksPerPixel * rCheatThrustFactor * Ship.GetMass() / 1000.0); +#endif + } + } + bool CShipAIHelper::CalcFormationParams (CShip *pShip, const CVector &vDestPos, const CVector &vDestVel, diff --git a/Transcendence/Transcendence/CManeuverController.cpp b/Transcendence/Transcendence/CManeuverController.cpp index 4e7e8eefa..b25c952c4 100644 --- a/Transcendence/Transcendence/CManeuverController.cpp +++ b/Transcendence/Transcendence/CManeuverController.cpp @@ -187,8 +187,7 @@ void CManeuverController::UpdateMoveTo (SUpdateCtx &Ctx, CShip *pShip) if (bCloseEnough) { - if (!pShip->IsParalyzed()) - pShip->AddForce(vDeltaV * pShip->GetMass() / 2000.0); + CShipAIHelper::ApplyFormationAccel(*pShip, vDeltaV); m_iManeuver = EManeuver::None; m_bThrust = false; From 1d99cb2842bb82e09ca2d5e9a23a131b00f94fa3 Mon Sep 17 00:00:00 2001 From: George Moromisato Date: Mon, 2 Feb 2026 11:37:55 -0800 Subject: [PATCH 06/14] Adjust accuracy algorithm (cherry picked from commit 0b816e413aca8c5ea75cd1afd3dbe7ad25d8fbdc) --- Mammoth/TSE/AIManeuvers.cpp | 48 +----------------------------------- Mammoth/TSE/CWeaponClass.cpp | 18 ++++++++++++++ 2 files changed, 19 insertions(+), 47 deletions(-) diff --git a/Mammoth/TSE/AIManeuvers.cpp b/Mammoth/TSE/AIManeuvers.cpp index 9e178015f..04f2533ee 100644 --- a/Mammoth/TSE/AIManeuvers.cpp +++ b/Mammoth/TSE/AIManeuvers.cpp @@ -1499,52 +1499,8 @@ void CAIBehaviorCtx::FireWeaponIfOnTarget(CShip *pShip, CSpaceObject *pTarget, C &iAimAngle, &iFireAngle, retiFacingAngle); - bool bAimError = false; *retiAngleToTarget = iAimAngle; - // iAimAngle is the direction that we should fire in order to hit - // the target. - // - // iFireAngle is the direction in which the weapon will fire. - // - // iFacingAngle is the direction in which the ship should face - // in order for the weapon to hit the target. - - // There is a chance of missing - - if (pWeaponToFire->IsReady()) - { - if (bAligned) - { - if (mathRandom(1, 100) > GetFireAccuracy()) - { - bAligned = false; - - // In this case, we happen to be aligned, but because of inaccuracy - // reason we think we're not. We clear the aim angle because for - // omnidirectional weapons, we don't want to try to turn towards - // the new aim point. - - iAimAngle = -1; - bAimError = true; - DebugAIOutput(pShip, "Aim error: hold fire when aligned"); - } - } - else if (iAimAngle != -1) - { - if (mathRandom(1, 100) <= m_iPrematureFireChance) - { - int iAimOffset = AngleOffset(iFireAngle, iAimAngle); - if (iAimOffset < 20) - { - bAligned = true; - bAimError = true; - DebugAIOutput(pShip, "Aim error: fire when not aligned"); - } - } - } - } - // Fire if (bAligned) @@ -1552,9 +1508,7 @@ void CAIBehaviorCtx::FireWeaponIfOnTarget(CShip *pShip, CSpaceObject *pTarget, C #ifdef DEBUG { char szDebug[1024]; - if (bAimError) - wsprintf(szDebug, "%s: false positive iAim=%d iFireAngle=%d", pWeaponToFire->GetName().GetASCIIZPointer(), iAimAngle, iFireAngle); - else if (!pWeaponToFire->IsReady()) + if (!pWeaponToFire->IsReady()) wsprintf(szDebug, "%s: aligned; NOT READY", pWeaponToFire->GetName().GetASCIIZPointer()); else if (rTargetDist2 > (rWeaponRange * rWeaponRange)) wsprintf(szDebug, "%s: aligned; TARGET OUT OF RANGE", pWeaponToFire->GetName().GetASCIIZPointer()); diff --git a/Mammoth/TSE/CWeaponClass.cpp b/Mammoth/TSE/CWeaponClass.cpp index 0f9003c58..ecf80d3fd 100644 --- a/Mammoth/TSE/CWeaponClass.cpp +++ b/Mammoth/TSE/CWeaponClass.cpp @@ -4667,6 +4667,24 @@ bool CWeaponClass::IsTargetReachable (const CInstalledDevice &Device, CSpaceObje { int iAimTolerance = GetConfiguration(*pShotDesc).GetAimTolerance(GetFireDelay(*pShotDesc)); + // Adjust fire angle based on accuracy. At 100% accuracy, no adjustment. + // We increase the adjustment at 1 std deviation up to 45 degrees for + // 0% accuracy. + + const CShip* pSourceShip = pSource->AsShip(); + if (pSourceShip) + { + int iAccuracy = pSourceShip->GetClass().GetAISettings().GetFireAccuracy(); + if (iAccuracy < 100) + { + // At 0% accuracy, +/- 45 degrees at 1 std deviation + + int iMaxAdjustment = (45 * (100 - iAccuracy)) / 100; + int iAdjustment = (int)(mathRandomGaussian() * iMaxAdjustment); + iFireAngle = AngleMod(iFireAngle + iAdjustment); + } + } + // Area weapons have 60 degree aim tolerance if (pShotDesc->GetType() == CWeaponFireDesc::ftArea) From 1a52436879a82a817eefeff33fc8c4832152f5d2 Mon Sep 17 00:00:00 2001 From: FlufflesTheMicrosaur <68170519+FlufflesTheMicrosaur@users.noreply.github.com> Date: Mon, 9 Feb 2026 11:48:23 -0800 Subject: [PATCH 07/14] chore: 106244: remove empty WeaponClassImpl.h (real header is TSEWeaponClassImpl.h) (#366) (cherry picked from commit 1fa24bb48b99e56fd08061c0f235d2c85b912ae5) --- Mammoth/TSE/CWeaponClass.cpp | 1 - Mammoth/TSE/TSE.vcxproj | 1 - Mammoth/TSE/TSE.vcxproj.filters | 3 --- Mammoth/TSE/WeaponClassImpl.h | 7 ------- 4 files changed, 12 deletions(-) delete mode 100644 Mammoth/TSE/WeaponClassImpl.h diff --git a/Mammoth/TSE/CWeaponClass.cpp b/Mammoth/TSE/CWeaponClass.cpp index ecf80d3fd..94c71309c 100644 --- a/Mammoth/TSE/CWeaponClass.cpp +++ b/Mammoth/TSE/CWeaponClass.cpp @@ -4,7 +4,6 @@ // Copyright (c) 2019 Kronosaur Productions, LLC. All Rights Reserved. #include "PreComp.h" -#include "WeaponClassImpl.h" #define DAMAGE_FAILURE_TAG CONSTLIT("DamageFailure") #define MISSILES_TAG CONSTLIT("Missiles") diff --git a/Mammoth/TSE/TSE.vcxproj b/Mammoth/TSE/TSE.vcxproj index 2be67ec26..de6ffa5df 100644 --- a/Mammoth/TSE/TSE.vcxproj +++ b/Mammoth/TSE/TSE.vcxproj @@ -359,7 +359,6 @@ - diff --git a/Mammoth/TSE/TSE.vcxproj.filters b/Mammoth/TSE/TSE.vcxproj.filters index f069cd3b8..7ba4ee3f9 100644 --- a/Mammoth/TSE/TSE.vcxproj.filters +++ b/Mammoth/TSE/TSE.vcxproj.filters @@ -344,9 +344,6 @@ Header Files - - Header Files - Header Files diff --git a/Mammoth/TSE/WeaponClassImpl.h b/Mammoth/TSE/WeaponClassImpl.h deleted file mode 100644 index a0837ba24..000000000 --- a/Mammoth/TSE/WeaponClassImpl.h +++ /dev/null @@ -1,7 +0,0 @@ -// WeaponClassImpl.h -// -// Implementation classes for CWeaponClass -// Copyright (c) 20202 Kronosaur Productions, LLC. All Rights Reserved. - -#pragma once - From 1b513851ec6595bda49677959e0e2bc6cf325405 Mon Sep 17 00:00:00 2001 From: gcabbage Date: Mon, 9 Feb 2026 19:49:14 +0000 Subject: [PATCH 08/14] Fix failureOnReturnToSystem (#361) (cherry picked from commit c243e3f0b2d9fae8730fec17b501466781f4c62e) --- Mammoth/TSE/CMission.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mammoth/TSE/CMission.cpp b/Mammoth/TSE/CMission.cpp index 06fc0a62a..a75d7e45c 100644 --- a/Mammoth/TSE/CMission.cpp +++ b/Mammoth/TSE/CMission.cpp @@ -681,7 +681,7 @@ void CMission::OnNewSystem (CSystem *pSystem) if (m_dwLeftSystemOn != 0 || !m_fInMissionSystem) { - const DWORD dwTimeAway = sysGetTicksElapsed(m_dwLeftSystemOn); + const DWORD dwTimeAway = GetUniverse().GetTicks() - m_dwLeftSystemOn; // Back in our system From 9a33b925ca0bfc7f5eb573aa9c9885c4b39200ba Mon Sep 17 00:00:00 2001 From: gcabbage Date: Mon, 23 Feb 2026 21:36:27 +0000 Subject: [PATCH 09/14] Do not recharge disabled shields (#377) (cherry picked from commit 479d1966e82be214d33150c67684aceae44df37d) --- Mammoth/TSE/CCExtensions.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mammoth/TSE/CCExtensions.cpp b/Mammoth/TSE/CCExtensions.cpp index e6a64da28..6d6619508 100644 --- a/Mammoth/TSE/CCExtensions.cpp +++ b/Mammoth/TSE/CCExtensions.cpp @@ -11325,7 +11325,7 @@ ICCItem *fnShipSet (CEvalContext *pEvalCtx, ICCItem *pArgs, DWORD dwData) case FN_SHIP_RECHARGE_SHIELD: { CInstalledDevice *pShield = pShip->GetNamedDevice(devShields); - if (pShield == NULL) + if (pShield == NULL || !pShield->IsEnabled()) return pCC->CreateNil(); CItemCtx ItemCtx(pShip, pShield); From 050690d141a8d52d41a157e648be3d5eb43b46cc Mon Sep 17 00:00:00 2001 From: FlufflesTheMicrosaur <68170519+FlufflesTheMicrosaur@users.noreply.github.com> Date: Mon, 12 Jan 2026 16:00:37 -0800 Subject: [PATCH 10/14] =?UTF-8?q?fix:=20105872:=20fixes=20"speed"=20also?= =?UTF-8?q?=20adjusting=20energy=20per=20activation=20to=20ma=E2=80=A6=20(?= =?UTF-8?q?#306)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 105872: fixes "speed" also adjusting energy per activation to make something more efficient, causing the buff to be more potent by the square of its buff. This fixes the power per activation issue by scaling power consumption with the speed change. * fix: 105872: speed should only impact active power, not idle power. Logic for universal power editing enhancements and active-only power editing enhancements added. (cherry picked from commit f811517062d497025f24a0001310b79a26e065f9) --- Mammoth/Include/TSEItemEnhancements.h | 2 ++ Mammoth/TSE/CItemEnhancement.cpp | 46 ++++++++++++++++++++++-- Mammoth/TSE/CItemEnhancementStack.cpp | 50 ++++++++++++++++++++++----- Mammoth/TSE/CShieldClass.cpp | 11 ++++-- Mammoth/TSE/CWeaponClass.cpp | 11 ++++-- 5 files changed, 102 insertions(+), 18 deletions(-) diff --git a/Mammoth/Include/TSEItemEnhancements.h b/Mammoth/Include/TSEItemEnhancements.h index 0121df760..ae08a15b2 100644 --- a/Mammoth/Include/TSEItemEnhancements.h +++ b/Mammoth/Include/TSEItemEnhancements.h @@ -151,6 +151,7 @@ class CItemEnhancement DWORD GetModCode (void) const { return m_dwMods; } int GetPerceptionAdj () const; int GetPowerAdj (void) const; + int GetActivePowerAdj (void) const; int GetReflectChance (DamageTypes iDamage) const; Metric GetRegen180 (CItemCtx &Ctx, int iTicksPerUpdate) const; int GetResistEnergyAdj (void) const { return (GetType() == etResistEnergy ? Level2DamageAdj(GetLevel(), IsDisadvantage()) : 100); } @@ -273,6 +274,7 @@ class CItemEnhancementStack int GetManeuverRate (void) const; int GetPerceptionAdj () const; int GetPowerAdj (void) const; + int GetActivePowerAdj (void) const; int GetResistDamageAdj (DamageTypes iDamage) const; int GetResistEnergyAdj (void) const; int GetResistMatterAdj (void) const; diff --git a/Mammoth/TSE/CItemEnhancement.cpp b/Mammoth/TSE/CItemEnhancement.cpp index 65dce2a5f..b0fec0375 100644 --- a/Mammoth/TSE/CItemEnhancement.cpp +++ b/Mammoth/TSE/CItemEnhancement.cpp @@ -1554,11 +1554,13 @@ int CItemEnhancement::GetPerceptionAdj () const } } -int CItemEnhancement::GetPowerAdj (void) const - // GetPowerAdj // -// Get the increase/decrease in power usage +// Get the increase/decrease in all power usage +// Only includes power use modifiers that affect both +// active and standby/idle power use +// +int CItemEnhancement::GetPowerAdj (void) const { switch (GetType()) @@ -1583,6 +1585,44 @@ int CItemEnhancement::GetPowerAdj (void) const } } +// GetPowerAdj +// +// Get the increases/decreases in power usage +// that apply when the device is activated +// +int CItemEnhancement::GetActivePowerAdj (void) const + + { + switch (GetType()) + { + case etPowerEfficiency: + { + int iLevel = GetLevel(); + + if (IsDisadvantage()) + return 100 + (10 * iLevel); + else + { + if (iLevel >= 0 && iLevel <= 9) + return 100 - (10 * iLevel); + else + return 10; + } + } + + case etSpeed: + case etSpeedOld: + { + int iDelayAdj = max(1, GetActivateRateAdj()); + // We multiply by 100 since it has to be returned as a % + return mathRound(100 * 100.0 / iDelayAdj); + } + + default: + return 100; + } + } + int CItemEnhancement::GetReflectChance (DamageTypes iDamage) const // GetReflectChance diff --git a/Mammoth/TSE/CItemEnhancementStack.cpp b/Mammoth/TSE/CItemEnhancementStack.cpp index f79cb6586..647340282 100644 --- a/Mammoth/TSE/CItemEnhancementStack.cpp +++ b/Mammoth/TSE/CItemEnhancementStack.cpp @@ -63,6 +63,14 @@ void CItemEnhancementStack::AccumulateAttributes (const CItem &Item, TArray 100) + retList->Insert(SDisplayAttribute(attribNegative, CONSTLIT("-slow"), true)); + else if (iFireAdj < 100) + retList->Insert(SDisplayAttribute(attribPositive, CONSTLIT("+fast"), true)); + // Power adjustment int iPowerAdj = GetPowerAdj(); @@ -70,14 +78,18 @@ void CItemEnhancementStack::AccumulateAttributes (const CItem &Item, TArrayInsert(SDisplayAttribute(attribNegative, CONSTLIT("-power drain"), true)); else if (iPowerAdj < 100) retList->Insert(SDisplayAttribute(attribPositive, CONSTLIT("+power save"), true)); - - // Enhancements to fire rate - - int iFireAdj = GetActivateDelayAdj(); - if (iFireAdj > 100) - retList->Insert(SDisplayAttribute(attribNegative, CONSTLIT("-slow"), true)); - else if (iFireAdj < 100) - retList->Insert(SDisplayAttribute(attribPositive, CONSTLIT("+fast"), true)); + else + { + // We need to check if our activation power is different + // This is relative to the inverse activate delay adjustment + // This way we can represent the power consumed per activation accurately + int iInverseDelayAdj = mathRound(100 * 100.0 / iFireAdj); + int iActivePowerAdj = GetActivePowerAdj(); + if (iActivePowerAdj > iInverseDelayAdj) + retList->Insert(SDisplayAttribute(attribNegative, CONSTLIT("-power drain"), true)); + else if (iActivePowerAdj < iInverseDelayAdj) + retList->Insert(SDisplayAttribute(attribPositive, CONSTLIT("+power save"), true)); + } // Add bonus @@ -434,11 +446,31 @@ int CItemEnhancementStack::GetPerceptionAdj () const return iAdj; } +// GetPowerAdj +// +// Returns the power consumption adjustment +// int CItemEnhancementStack::GetPowerAdj (void) const + { + int i; + + Metric rValue = 100.0; + for (i = 0; i < m_Stack.GetCount(); i++) + { + int iAdj = m_Stack[i].GetPowerAdj(); + if (iAdj != 100) + rValue = iAdj * rValue / 100.0; + } + + return mathRound(rValue); + } + // GetPowerAdj // // Returns the power consumption adjustment +// +int CItemEnhancementStack::GetActivePowerAdj (void) const { int i; @@ -446,7 +478,7 @@ int CItemEnhancementStack::GetPowerAdj (void) const Metric rValue = 100.0; for (i = 0; i < m_Stack.GetCount(); i++) { - int iAdj = m_Stack[i].GetPowerAdj(); + int iAdj = m_Stack[i].GetActivePowerAdj(); if (iAdj != 100) rValue = iAdj * rValue / 100.0; } diff --git a/Mammoth/TSE/CShieldClass.cpp b/Mammoth/TSE/CShieldClass.cpp index 62797781e..c8f8fc1ff 100644 --- a/Mammoth/TSE/CShieldClass.cpp +++ b/Mammoth/TSE/CShieldClass.cpp @@ -1420,9 +1420,14 @@ int CShieldClass::GetPowerRating (CItemCtx &Ctx, int *retiIdlePowerUse) const const CItemEnhancementStack *pEnhancements = Ctx.GetEnhancementStack(); if (pEnhancements) { - int iAdj = pEnhancements->GetPowerAdj(); - iPower = iPower * iAdj / 100; - iIdlePower = iIdlePower * iAdj / 100; + int iActiveAdj = pEnhancements->GetActivePowerAdj(); + iPower = iPower * iActiveAdj / 100; + + if (retiIdlePowerUse) + { + int iAdj = pEnhancements->GetPowerAdj(); + iIdlePower = iIdlePower * iAdj / 100; + } } if (retiIdlePowerUse) diff --git a/Mammoth/TSE/CWeaponClass.cpp b/Mammoth/TSE/CWeaponClass.cpp index 94c71309c..a60c2e68c 100644 --- a/Mammoth/TSE/CWeaponClass.cpp +++ b/Mammoth/TSE/CWeaponClass.cpp @@ -3323,9 +3323,14 @@ int CWeaponClass::GetPowerRating (CItemCtx &Ctx, int *retiIdlePowerUse) const TSharedPtr pEnhancements = Ctx.GetEnhancementStack(); if (pEnhancements) { - int iAdj = pEnhancements->GetPowerAdj(); - iPower = iPower * iAdj / 100; - iIdlePower = iIdlePower * iAdj / 100; + int iActiveAdj = pEnhancements->GetActivePowerAdj(); + iPower = iPower * iActiveAdj / 100; + + if (retiIdlePowerUse) + { + int iAdj = pEnhancements->GetPowerAdj(); + iIdlePower = iIdlePower * iAdj / 100; + } } if (retiIdlePowerUse) From 0f79981f99467688bb8efc00858ca69804dfaf60 Mon Sep 17 00:00:00 2001 From: FlufflesTheMicrosaur <68170519+FlufflesTheMicrosaur@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:52:04 -0800 Subject: [PATCH 11/14] fix 105937: a load of AI fixes and some AI improvements: ... (#320) * fix 105937: a load of AI fixes and some AI improvements: Significantly reduces oscillation without bringing back full assist. Reduces assist cheat still throwing low mass ships around. Improves AI debug output. Improves AI combat movement logic. Fixes AI "turning off" if primary weapon is broken. * fix: 105937: ships will now only avoid the threat of a large object exploding on death if the object is about to die. Also add an evasive version of calc spiral away to avoid ships consistently presenting the same armor segment to a pursuer. Its not perfect - they still present their aft corner segments but it now alternates. Will add a smarter version of standOff style because that would be an API change. * chore: 105937: tweak the 'imminent death' cautiousness parameter for explosion avoidance to be more cautious (because lategame heavy munitions can sometimes take out that last chunk of hitpoints all at once) * Chore: 105937: remove now-unnecessary mass adjustment to cheat factor because george fixed the bug with the cheat force application in commit 46759FC (cherry picked from commit c9e7e42eaf66469f596c88af18993a696283234c) --- Mammoth/Include/TSE.h | 3 +- Mammoth/Include/TSEPhysics.h | 2 +- Mammoth/Include/TSEShipSystems.h | 24 +- Mammoth/Include/TSESpaceObjectsImpl.h | 7 + Mammoth/Include/TSEStationImpl.h | 1 + Mammoth/TSE/AIManeuvers.cpp | 360 +++++++++++++++++++++----- Mammoth/TSE/CAIBehaviorCtx.cpp | 28 +- Mammoth/TSE/CIntegralRotationDesc.cpp | 46 +++- Mammoth/TSE/CShip.cpp | 61 ++++- Mammoth/TSE/CStation.cpp | 21 ++ Mammoth/TSE/ShipAIImpl.h | 4 +- 11 files changed, 458 insertions(+), 99 deletions(-) diff --git a/Mammoth/Include/TSE.h b/Mammoth/Include/TSE.h index 9cb37a8aa..42aa91609 100644 --- a/Mammoth/Include/TSE.h +++ b/Mammoth/Include/TSE.h @@ -1246,7 +1246,7 @@ class CSpaceObject virtual CString GetObjClassName (void) const { return CONSTLIT("unknown"); } virtual Metric GetParallaxDist (void) { return 0.0; } virtual EDamageResults GetPassthroughDefault (void) { return damageNoDamage; } - virtual int GetPlanetarySize (void) const { return 0; } + virtual int GetPlanetarySize (void) const { return 0; } virtual ScaleTypes GetScale (void) const { return scaleFlotsam; } virtual CSovereign *GetSovereign (void) const { return NULL; } virtual Metric GetStellarMass (void) const { return 0.0; } @@ -1307,6 +1307,7 @@ class CSpaceObject virtual int GetMaxLightDistance (void) const { return 0; } virtual Metric GetMaxWeaponRange (void) const { return 0.0; } virtual int GetPerception (void) const { return perceptNormal; } + virtual int GetRelativeHealth () const { return INT_MAX; } virtual int GetScore (void) { return 0; } virtual CG32bitPixel GetSpaceColor (void) { return 0; } virtual int GetStealth (void) const { return stealthNormal; } diff --git a/Mammoth/Include/TSEPhysics.h b/Mammoth/Include/TSEPhysics.h index be2762c34..3da6ba05b 100644 --- a/Mammoth/Include/TSEPhysics.h +++ b/Mammoth/Include/TSEPhysics.h @@ -88,7 +88,7 @@ class CPhysicsForceResolver struct SForceDesc { CSpaceObject *pObj = NULL; - CVector vForce; // Absolute acceleration + CVector vForce; // Force to apply CVector vLimitedForce; // Accelerate only if below max speed. Metric rDragFactor = 1.0; // Final velocity gets multiplied by this. }; diff --git a/Mammoth/Include/TSEShipSystems.h b/Mammoth/Include/TSEShipSystems.h index d1af2be55..a5f1cc70b 100644 --- a/Mammoth/Include/TSEShipSystems.h +++ b/Mammoth/Include/TSEShipSystems.h @@ -514,21 +514,25 @@ class CIntegralRotationDesc static constexpr int ROTATION_FRACTION = 1080; - CIntegralRotationDesc (void) { } + CIntegralRotationDesc () { } explicit CIntegralRotationDesc (const CRotationDesc &Desc) { InitFromDesc(Desc); } int AlignToRotationAngle (int iAngle) const { return GetRotationAngle(GetFrameIndex(iAngle)); } int CalcFinalRotationFrame (int iRotationFrame, int iRotationSpeed) const; - int GetFrameAngle (void) const { return (m_iCount > 0 ? mathRound(360.0 / m_iCount) : 0); } - int GetFrameCount (void) const { return m_iCount; } + int GetFrameAngle () const { return (m_iCount > 0 ? mathRound(360.0 / m_iCount) : 0); } + int GetFrameCount () const { return m_iCount; } int GetFrameIndex (int iAngle) const { return (m_iCount > 0 ? (m_FacingsData[m_iCount].AngleToFrameIndex[AngleMod(iAngle)]) : 0); } - int GetManeuverDelay (void) const; - Metric GetManeuverRatio (void) const { return (Metric)m_iMaxRotationRate / ROTATION_FRACTION; } - int GetMaxRotationSpeed (void) const { return m_iMaxRotationRate; } - Metric GetMaxRotationSpeedDegrees (void) const; - int GetMaxRotationTimeTicks (void) const { Metric rSpeed = GetMaxRotationSpeedDegrees(); return (rSpeed > 0.0 ? (int)(360.0 / rSpeed) : 0); } - int GetRotationAccel (void) const { return m_iRotationAccel; } - int GetRotationAccelStop (void) const { return m_iRotationAccelStop; } + int GetManeuverDelay () const; + Metric GetManeuverRatio () const { return (Metric)m_iMaxRotationRate / ROTATION_FRACTION; } + int GetMaxRotationSpeed () const { return m_iMaxRotationRate; } + Metric GetMaxRotationSpeedDegrees () const; + int GetMaxRotationTimeTicks () const { Metric rSpeed = GetMaxRotationSpeedDegrees(); return (rSpeed > 0.0 ? (int)(360.0 / rSpeed) : 0); } + int GetRotationAccel () const { return m_iRotationAccel; } + Metric GetRotationAccelDegrees () const; + int GetRotationAccelStop () const { return m_iRotationAccelStop; } + Metric GetRotationAccelStopDegrees () const; + int GetRotationResponsiveness () const { return min(m_iMaxRotationRate, min(m_iRotationAccel, m_iRotationAccelStop)); } + Metric GetRotationResponsivenessDegrees () const; int GetRotationAngle (int iIndex) const { return (m_iCount > 0 ? m_FacingsData[m_iCount].FrameIndexToAngle[iIndex % m_iCount] : 0); } int GetRotationAngleExact (int iRotationFrameExact) const { return (m_iCount > 0 ? GetRotationAngleExact(m_iCount, iRotationFrameExact) : 0); } int GetRotationFrameExact (int iAngle) const { return (m_iCount > 0 ? GetRotationFrameExact(m_iCount, iAngle) : 0); } diff --git a/Mammoth/Include/TSESpaceObjectsImpl.h b/Mammoth/Include/TSESpaceObjectsImpl.h index 2a98a1d34..a98ceb1a7 100644 --- a/Mammoth/Include/TSESpaceObjectsImpl.h +++ b/Mammoth/Include/TSESpaceObjectsImpl.h @@ -34,6 +34,7 @@ class CAreaDamage : public TSpaceObjectImpl virtual CString GetNamePattern (DWORD dwNounPhraseFlags = 0, DWORD *retdwFlags = NULL) const override; virtual CString GetObjClassName (void) const override { return CONSTLIT("CAreaDamage"); } virtual CSystem::LayerEnum GetPaintLayer (void) const override { return CSystem::layerEffects; } + virtual int GetRelativeHealth () const override { return IsDestroyed() ? -1 : INT_MAX; } virtual CSpaceObject *GetSecondarySource (void) const override { return m_Source.GetSecondaryObj(); } virtual CSovereign *GetSovereign (void) const override { return m_pSovereign; } virtual CDesignType *GetType (void) const override { return m_pDesc->GetWeaponType(); } @@ -80,6 +81,7 @@ class CBeam : public TSpaceObjectImpl virtual CString GetNamePattern (DWORD dwNounPhraseFlags = 0, DWORD *retdwFlags = NULL) const override; virtual CString GetObjClassName (void) const override { return CONSTLIT("CBeam"); } virtual CSystem::LayerEnum GetPaintLayer (void) const override { return CSystem::layerStations; } + virtual int GetRelativeHealth () const override { return IsDestroyed() ? -1 : 100; } virtual CSpaceObject *GetSecondarySource (void) const override { return m_Source.GetSecondaryObj(); } virtual CSovereign *GetSovereign (void) const override { return m_pSovereign; } virtual const CWeaponFireDesc *GetWeaponFireDesc (void) const override { return m_pDesc; } @@ -169,6 +171,7 @@ class CContinuousBeam : public TSpaceObjectImpl virtual CString GetNamePattern (DWORD dwNounPhraseFlags = 0, DWORD *retdwFlags = NULL) const override; virtual CString GetObjClassName (void) const override { return CONSTLIT("CContinuousBeam"); } virtual CSystem::LayerEnum GetPaintLayer (void) const override { return CSystem::layerEffects; } + virtual int GetRelativeHealth () const override { return IsDestroyed() ? -1 : INT_MAX; } virtual int GetRotation (void) const override { return m_iLastDirection; } virtual CSpaceObject *GetSecondarySource (void) const override { return m_Source.GetSecondaryObj(); } virtual CSovereign *GetSovereign (void) const override { return m_pSovereign; } @@ -548,6 +551,7 @@ class CMissile : public TSpaceObjectImpl virtual COverlayList *GetOverlays (void) override { return &m_Overlays; } virtual const COverlayList *GetOverlays (void) const override { return &m_Overlays; } virtual CSystem::LayerEnum GetPaintLayer (void) const override { return (m_pDesc->GetPassthrough() > 0 ? CSystem::layerEffects : CSystem::layerStations); } + virtual int GetRelativeHealth () const override { return IsDestroyed() ? -1 : (m_pDesc->GetHitPoints() ? min(100, mathRound((Metric)m_iHitPoints / m_pDesc->GetHitPoints())) : 100); } virtual int GetRotation (void) const override { return m_iRotation; } virtual CSpaceObject *GetSecondarySource (void) const override { return m_Source.GetSecondaryObj(); } virtual CSovereign *GetSovereign (void) const override { return m_pSovereign; } @@ -651,6 +655,7 @@ class CParticleDamage : public TSpaceObjectImpl virtual CString GetNamePattern (DWORD dwNounPhraseFlags = 0, DWORD *retdwFlags = NULL) const override; virtual CString GetObjClassName (void) const override { return CONSTLIT("CParticleDamage"); } virtual CSystem::LayerEnum GetPaintLayer (void) const override { return CSystem::layerEffects; } + virtual int GetRelativeHealth () const override { return IsDestroyed() ? -1 : INT_MAX; } virtual CSpaceObject *GetSecondarySource (void) const override { return m_Source.GetSecondaryObj(); } virtual CSovereign *GetSovereign (void) const override { return m_pSovereign; } virtual CDesignType *GetType (void) const override { return m_pDesc->GetWeaponType(); } @@ -934,6 +939,7 @@ class CRadiusDamage : public TSpaceObjectImpl virtual CString GetNamePattern (DWORD dwNounPhraseFlags = 0, DWORD *retdwFlags = NULL) const override; virtual CString GetObjClassName (void) const override { return CONSTLIT("CRadiusDamage"); } virtual CSystem::LayerEnum GetPaintLayer (void) const override { return CSystem::layerEffects; } + virtual int GetRelativeHealth () const override { return IsDestroyed() ? -1 : INT_MAX; } virtual CSpaceObject *GetSecondarySource (void) const override { return m_Source.GetSecondaryObj(); } virtual CSovereign *GetSovereign (void) const override { return m_pSovereign; } virtual CDesignType *GetType (void) const override { return m_pDesc->GetWeaponType(); } @@ -1206,6 +1212,7 @@ class CShip : public TSpaceObjectImpl virtual CSystem::LayerEnum GetPaintLayer (void) const override { return (m_fShipCompartment ? CSystem::layerOverhang : CSystem::layerShips); } virtual int GetPerception (void) const override; virtual ICCItem *GetPropertyCompatible (CCodeChainCtx &Ctx, const CString &sName) const override; + virtual int GetRelativeHealth () const override; virtual int GetRotation (void) const override { return m_Rotation.GetRotationAngle(m_Perf.GetIntegralRotationDesc()); } virtual int GetRotationFrameIndex (void) const override { return m_Rotation.GetFrameIndex(); } virtual ScaleTypes GetScale (void) const override { return scaleShip; } diff --git a/Mammoth/Include/TSEStationImpl.h b/Mammoth/Include/TSEStationImpl.h index 33e08d8d7..684277091 100644 --- a/Mammoth/Include/TSEStationImpl.h +++ b/Mammoth/Include/TSEStationImpl.h @@ -176,6 +176,7 @@ class CStation : public TSpaceObjectImpl virtual int GetPlanetarySize (void) const override { return (GetScale() == scaleWorld ? m_pType->GetSize() : 0); } virtual ICCItem *GetPropertyCompatible (CCodeChainCtx &Ctx, const CString &sName) const override; virtual IShipGenerator *GetRandomEncounterTable (int *retiFrequency = NULL) const override; + virtual int GetRelativeHealth () const override; virtual int GetRotation (void) const override; virtual ScaleTypes GetScale (void) const override { return m_Scale; } virtual CXMLElement *GetScreen (const CString &sName) override { return m_pType->GetScreen(sName); } diff --git a/Mammoth/TSE/AIManeuvers.cpp b/Mammoth/TSE/AIManeuvers.cpp index 04f2533ee..b181b074a 100644 --- a/Mammoth/TSE/AIManeuvers.cpp +++ b/Mammoth/TSE/AIManeuvers.cpp @@ -464,11 +464,11 @@ CVector CAIBehaviorCtx::CalcManeuverSpiralIn (CShip *pShip, const CVector &vTarg return PolarToVector(iAngle + 360 - iTrajectory, rRadius); } -CVector CAIBehaviorCtx::CalcManeuverSpiralOut (CShip *pShip, const CVector &vTarget, int iTrajectory) - // CalcManeuverSpiralOut // // Returns the vector that the ship should move in for a spiral-out maneuver +// +CVector CAIBehaviorCtx::CalcManeuverSpiralOut (CShip *pShip, const CVector &vTarget, int iTrajectory) { CVector vTangent = (vTarget.Perpendicular()).Normal() * pShip->GetMaxSpeed() * g_SecondsPerUpdate * 8; @@ -486,17 +486,65 @@ CVector CAIBehaviorCtx::CalcManeuverSpiralOut (CShip *pShip, const CVector &vTar return PolarToVector(iAngle + iTrajectory, rRadius); } -void CAIBehaviorCtx::DebugAIOutput (CShip *pShip, LPCSTR pText) +// CalcManeuverSpiralOutEvasive +// +// Returns the vector that the ship should move in for a spiral-out maneuver +// May flip the sign on iTrajectory at a frequency specific to that ship +// Randomize iTrajectory by an amount specific to that ship +// +// This makes it harder for an enemy to repeatedly attack and damage the same segments of armor +// While trying to flee a faster enemy +// +CVector CAIBehaviorCtx::CalcManeuverSpiralOutEvasive (CShip *pShip, const CVector &vTarget, int iTrajectory) + + { + int iTrajectoryPreference = pShip->GetDestiny() % 31 - 15; + int iTurnTime = pShip->GetDestiny() - 180 + 300; + int iTurnTimeEven = (iTurnTime + 1) % 2; + iTurnTime = iTurnTime + iTurnTimeEven; + bool bFlip = pShip->GetSystem()->GetTick() % (iTurnTime) < iTurnTime / 2; + int iAdjustedTrajectory = iTrajectoryPreference + iTrajectory; + + if (bFlip) + iAdjustedTrajectory = 180 - iAdjustedTrajectory; + + CVector vTangent = (vTarget.Perpendicular()).Normal() * pShip->GetMaxSpeed() * g_SecondsPerUpdate * 8; + + Metric rRadius; + int iAngle = VectorToPolar(vTangent, &rRadius); + + // Handle the case where we vTangent is 0 (i.e., we are on top of the enemy) + + if (rRadius == 0.0) + rRadius = g_KlicksPerPixel; + + // Curve out + + return PolarToVector(iAngle + iAdjustedTrajectory, rRadius); + } // DebugAIOutput // // Output AI state +// +void CAIBehaviorCtx::DebugAIOutput (CShip *pShip, LPCSTR pText) { if (pShip->GetUniverse().GetDebugOptions().IsShowAIDebugEnbled()) pShip->HighlightAppend(strPatternSubst(CONSTLIT("%d: %s"), pShip->GetID(), CString(pText))); } +// DebugAIOutput +// +// Output AI state (accepts CString) +// +void CAIBehaviorCtx::DebugAIOutput (CShip *pShip, CString sText) + + { + if (pShip->GetUniverse().GetDebugOptions().IsShowAIDebugEnbled()) + pShip->HighlightAppend(strPatternSubst(CONSTLIT("%d: %s"), pShip->GetID(), sText)); + } + void CAIBehaviorCtx::ImplementAttackNearestTarget (CShip *pShip, Metric rMaxRange, CSpaceObject **iopTarget, CSpaceObject *pExcludeObj, bool bTurn) // ImplementAttackNearestTarget @@ -582,7 +630,7 @@ void CAIBehaviorCtx::ImplementAttackTarget (CShip *pShip, CSpaceObject *pTarget, if (bMaintainCourse || IsImmobile()) NULL; - // If we're flocking, then implement flocking maneuverses + // If we're flocking, then implement flocking maneuvers else if (m_AISettings.IsFlocker() && rTargetDist2 > FLOCK_COMBAT_RANGE2 @@ -643,14 +691,14 @@ void CAIBehaviorCtx::ImplementAttackTarget (CShip *pShip, CSpaceObject *pTarget, DEBUG_CATCH } -bool CAIBehaviorCtx::ImplementAttackTargetManeuver (CShip *pShip, CSpaceObject *pTarget, const CVector &vTarget, Metric rTargetDist2) - // ImplementAttackTargetManeuver // // Implements maneuvers to attack the target based on the ship's combat style // and taking into account the hazard potential vector. // // Returns TRUE if a maneuver was made (if FALSE, then ship is free to maneuver to align weapon) +// +bool CAIBehaviorCtx::ImplementAttackTargetManeuver (CShip *pShip, CSpaceObject *pTarget, const CVector &vTarget, Metric rTargetDist2) { CVector vDirection; @@ -665,13 +713,28 @@ bool CAIBehaviorCtx::ImplementAttackTargetManeuver (CShip *pShip, CSpaceObject * { bool bFaster = (pShip->GetMaxSpeed() > pTarget->GetMaxSpeed()); + // Do we need to avoid getting too close? + // + // Check its health. We dont start avoiding until an explosion is imminent + // + // If we do so much damage that it dies instantly we probably are big or high + // level enough to entirely tank the explosion + + int iTargetRelHP = pTarget->GetRelativeHealth(); + + bool bAvoidExplodingTarget = m_fAvoidExplodingStations + && rTargetDist2 < MIN_STATION_TARGET_DIST2 + && iTargetRelHP < 15 + && iTargetRelHP >= 0 + && pTarget->GetMass() > 5000.0; + // If we're waiting for shields to regenerate, then // spiral away if (IsWaitingForShieldsToRegen() && bFaster) { - DebugAIOutput(pShip, "Wait for shields"); - vDirection = CombinePotential(CalcManeuverSpiralOut(pShip, vTarget, 75)); + DebugAIOutput(pShip, "Std: Wait for shields"); + vDirection = CombinePotential(CalcManeuverSpiralOutEvasive(pShip, vTarget, 75)); } // If we're not well in range of our primary weapon then @@ -679,7 +742,7 @@ bool CAIBehaviorCtx::ImplementAttackTargetManeuver (CShip *pShip, CSpaceObject * else if (rTargetDist2 > GetPrimaryAimRange2()) { - DebugAIOutput(pShip, "Close on target"); + DebugAIOutput(pShip, "Std: Close on target"); // Try to flank our target, if we are faster @@ -708,17 +771,19 @@ bool CAIBehaviorCtx::ImplementAttackTargetManeuver (CShip *pShip, CSpaceObject * // it above). // We cheat a little to dampen our movements against stationary targets. - vDirection = CalcManeuverFormation(pShip, vPos, CVector(), iAngle, 0.2); + Metric rCheatFactor = CHEAT_FORMATION_FACTOR; + + DebugAIOutput(pShip, strPatternSubst(CONSTLIT("Std: Close on static fire point: Assist %r"), rCheatFactor)); + + vDirection = CalcManeuverFormation(pShip, vPos, CVector(), iAngle, rCheatFactor); } - // If we're attacking a station, then keep our distance so that + // If we're attacking a station/capship, then keep our distance so that // we don't get caught in the explosion - else if (m_fAvoidExplodingStations - && rTargetDist2 < MIN_STATION_TARGET_DIST2 - && pTarget->GetMass() > 5000.0) + else if (bAvoidExplodingTarget) { - DebugAIOutput(pShip, "Spiral away to avoid explosion"); + DebugAIOutput(pShip, "Std: Spiral away: avoid explosion"); vDirection = CombinePotential(CalcManeuverSpiralOut(pShip, vTarget)); } @@ -726,7 +791,7 @@ bool CAIBehaviorCtx::ImplementAttackTargetManeuver (CShip *pShip, CSpaceObject * else if (rTargetDist2 < MIN_TARGET_DIST2) { - DebugAIOutput(pShip, "Spiral away"); + DebugAIOutput(pShip, "Std: Spiral away: too close"); vDirection = CombinePotential(CalcManeuverSpiralOut(pShip, vTarget)); } @@ -736,7 +801,7 @@ bool CAIBehaviorCtx::ImplementAttackTargetManeuver (CShip *pShip, CSpaceObject * && (pShip->GetVel().Length2() < (0.01 * 0.01 * LIGHT_SPEED * LIGHT_SPEED))) { DebugAIOutput(pShip, "Speed away"); - vDirection = CombinePotential(CalcManeuverSpiralOut(pShip, vTarget)); + vDirection = CombinePotential(CalcManeuverSpiralOutEvasive(pShip, vTarget)); } // Otherwise, hazard avoidance only @@ -763,9 +828,18 @@ bool CAIBehaviorCtx::ImplementAttackTargetManeuver (CShip *pShip, CSpaceObject * const Metric rBravery = pow((Metric)iLastHit / (Metric)MAX_BRAVERY_TICKS, BRAVERY_DECAY_POWER); // Do we need to avoid getting too close? + // + // Check its health. We dont start avoiding until an explosion is imminent + // + // If we do so much damage that it dies instantly we probably are big or high + // level enough to entirely tank the explosion + + int iTargetRelHP = pTarget->GetRelativeHealth(); bool bAvoidExplodingTarget = m_fAvoidExplodingStations - && rTargetDist2 < MIN_STATION_TARGET_DIST2 + && rTargetDist2 < MIN_STATION_TARGET_DIST2 + && iTargetRelHP < 15 + && iTargetRelHP >= 0 && pTarget->GetMass() > 5000.0; // Compute the maximum distance at which we'll start firing. If we're feeling brave, @@ -793,8 +867,25 @@ bool CAIBehaviorCtx::ImplementAttackTargetManeuver (CShip *pShip, CSpaceObject * && bWeAreFaster && pShip->GetCurrentOrderDesc().GetOrder() != IShipController::orderEscort) { - DebugAIOutput(pShip, "Wait for shields"); - vDirection = CombinePotential(CalcManeuverSpiralOut(pShip, vTarget, 75)); + DebugAIOutput(pShip, "Adv: Wait for shields"); + vDirection = CombinePotential(CalcManeuverSpiralOutEvasive(pShip, vTarget, 75)); + } + + // If we're attacking a station/capship, then keep our distance so that + // we don't get caught in the explosion + + else if (bAvoidExplodingTarget) + { + DebugAIOutput(pShip, "Adv: Spiral away: avoid explosion"); + vDirection = CombinePotential(CalcManeuverSpiralOut(pShip, vTarget)); + } + + // If we're too close to our target, spiral away + + else if (rTargetDist2 < rMinDist2) + { + DebugAIOutput(pShip, "Adv: Spiral away: too close to target"); + vDirection = CombinePotential(CalcManeuverSpiralOut(pShip, vTarget)); } // If we're attacking a static target then find a good spot @@ -822,7 +913,20 @@ bool CAIBehaviorCtx::ImplementAttackTargetManeuver (CShip *pShip, CSpaceObject * // it above). // We cheat a little to dampen our movements against stationary targets. - vDirection = CalcManeuverFormation(pShip, vPos, CVector(), iAngle, 0.2); + Metric rCheatFactor = CHEAT_FORMATION_FACTOR; + + DebugAIOutput(pShip, strPatternSubst(CONSTLIT("Adv: Close on static fire point: Assist %r"), rCheatFactor)); + + vDirection = CalcManeuverFormation(pShip, vPos, CVector(), iAngle, rCheatFactor); + } + + // If we are way too far away (over ~50% of our aim distance off) + // just close on the target + + else if (rTargetDist2 > rMaxAimDist2 * 2) + { + DebugAIOutput(pShip, "Adv: Close on target"); + vDirection = CombinePotential(CalcManeuverCloseOnTarget(pShip, pTarget, vTarget, rTargetDist2)); } // If we're not well in range of our primary weapon then @@ -841,13 +945,13 @@ bool CAIBehaviorCtx::ImplementAttackTargetManeuver (CShip *pShip, CSpaceObject * CVector vPos; if (m_fHasAvoidPotential) { - DebugAIOutput(pShip, "Avoid hazards"); + DebugAIOutput(pShip, "Adv: Avoid hazards"); vPos = pShip->GetPos() + (POTENTIAL_TO_POS_ADJ * GetPotential()); } else { - DebugAIOutput(pShip, "Close to aim point"); + DebugAIOutput(pShip, "Adv: Close to aim point"); // Pick a position at the flank distance between us and the target @@ -859,37 +963,63 @@ bool CAIBehaviorCtx::ImplementAttackTargetManeuver (CShip *pShip, CSpaceObject * CVector vVel = pTarget->GetVel() + (vToTargetN * (1.0 - Min(0.7, rBravery)) * pShip->GetMaxSpeed()); - // Maneuver to that point - cheat a little if the target is stationary to dampen our movement. + // We already handled stationary targets in the previous check + // so we can assume that our target is moving + // + // If we have bad maneuverability we need to cheat a little to avoid severe oversteer oscillation + // + // If we are close enough, we dont care and start shooting + + Metric rPosRadius = LIGHT_SECOND * 3; + Metric rPosRadius2 = rPosRadius * rPosRadius; + Metric rPosOffset2 = pShip->GetPos().Distance2(vPos); + + if (rPosRadius2 > rPosOffset2) + { + DebugAIOutput(pShip, "Adv: Close enough to fire point, Aiming..."); + // good enough, just drift while the aim code shoots + vDirection = GetPotential(); + } + else if (m_fLowManeuverability) + { + Metric rCheatFactor = CHEAT_FORMATION_FACTOR; + + // We need to be facing the correct direction for this. Formations can + // cheat backwards because they look ok moving as a unit, however + // individual attackers getting cheated backwards/sideways is ugly and obvious + // We might still need a little backwards dampening but we need to minimize it + + CVector vToPos = vPos - pShip->GetPos(); + + // How far off we are in radians from the force being applied + Metric rAngleDiff = abs(vToPos.Polar() - mathDegreesToRadians(pShip->GetRotationAngle())); + Metric rCos = max(cos(rAngleDiff), 0.0); - vDirection = CalcManeuverFormation(pShip, vPos, vVel, 0, pTarget->GetVel().Length2() ? 0.0 : 0.2); + // We get the square to bias against moving at wrong angles + Metric rCos2 = rCos * rCos; + + rCheatFactor *= rCos2; + + DebugAIOutput(pShip, strPatternSubst(CONSTLIT("Adv: Close on fire point: Assist %r"), rCheatFactor)); + + vDirection = CalcManeuverFormation(pShip, vPos, vVel, 0, rCheatFactor); + } + else + { + DebugAIOutput(pShip, "Adv: Close on fire point"); + vDirection = CalcManeuverFormation(pShip, vPos, vVel, 0, 0.0); + } } // Otherwise, we just try to close as best as possible else { - DebugAIOutput(pShip, "Close on target"); + DebugAIOutput(pShip, "Adv: Close on target"); vDirection = CombinePotential(CalcManeuverCloseOnTarget(pShip, pTarget, vTarget, rTargetDist2)); } } - // If we're attacking a station, then keep our distance so that - // we don't get caught in the explosion - - else if (bAvoidExplodingTarget) - { - DebugAIOutput(pShip, "Spiral away to avoid explosion"); - vDirection = CombinePotential(CalcManeuverSpiralOut(pShip, vTarget)); - } - - // If we're too close to our target, spiral away - - else if (rTargetDist2 < rMinDist2) - { - DebugAIOutput(pShip, "Spiral away"); - vDirection = CombinePotential(CalcManeuverSpiralOut(pShip, vTarget)); - } - // Otherwise, hazard avoidance only. else @@ -907,6 +1037,7 @@ bool CAIBehaviorCtx::ImplementAttackTargetManeuver (CShip *pShip, CSpaceObject * if (rTargetDist2 > rMaxRange2) { + DebugAIOutput(pShip, "StandOff: Close on target"); vDirection = CombinePotential(CalcManeuverCloseOnTarget(pShip, pTarget, vTarget, rTargetDist2)); } @@ -914,7 +1045,8 @@ bool CAIBehaviorCtx::ImplementAttackTargetManeuver (CShip *pShip, CSpaceObject * else if (rTargetDist2 < rIdealRange2) { - vDirection = CombinePotential(CalcManeuverSpiralOut(pShip, vTarget, 45)); + DebugAIOutput(pShip, "StandOff: Spiral away: Too close"); + vDirection = CombinePotential(CalcManeuverSpiralOutEvasive(pShip, vTarget, 45)); } // Otherwise, hazard avoidance only @@ -929,37 +1061,104 @@ bool CAIBehaviorCtx::ImplementAttackTargetManeuver (CShip *pShip, CSpaceObject * { Metric rMaxRange2 = m_rBestWeaponRange * m_rBestWeaponRange; - // Compute the angle line along the target's motion (and make sure - // it is aligned on a rotation angle, so we can get a shot in) + // Pick an aggressively close range + // Avoid being unnecessarily close if our range is so overwhelming though + // Because it makes maneuvering harder + + Metric rRange = Max(0.125 * m_rBestWeaponRange, Min(0.5 * m_rBestWeaponRange, 10.0 * LIGHT_SECOND)); - int iTargetMotion = (pTarget->CanThrust() ? + // If we are way too far away (over ~100% of our desired distance off) + // just close on the target. This saves on compute and reduces oversteer. + + if (rTargetDist2 > rRange * rRange * 4) + { + DebugAIOutput(pShip, "Chase: Close on target"); + vDirection = CombinePotential(CalcManeuverCloseOnTarget(pShip, pTarget, vTarget, rTargetDist2)); + } + else + { + + // Compute the angle line along the target's motion (and make sure + // it is aligned on a rotation angle, so we can get a shot in) + + int iTargetMotion = (pTarget->CanThrust() ? pShip->AlignToRotationAngle(VectorToPolar(pTarget->GetVel())) : pShip->AlignToRotationAngle(pShip->GetDestiny())); - // Compute the target's angle with respect to us. We want to end up facing - // directly towards the target + // Compute the target's angle with respect to us. We want to end up facing + // directly towards the target - int iTargetAngle = VectorToPolar(vTarget); + int iTargetAngle = VectorToPolar(vTarget); - // Pick a point behind the target (and add hazard potential) + // Pick a point behind the target (and add hazard potential) - Metric rRange = Min(0.5 * m_rBestWeaponRange, 10.0 * LIGHT_SECOND); - CVector vPos; - if (m_fHasAvoidPotential) - vPos = pShip->GetPos() + (POTENTIAL_TO_POS_ADJ * GetPotential()); - else - vPos = pTarget->GetPos() + PolarToVector(iTargetMotion + 180, rRange); + CVector vPos; + if (m_fHasAvoidPotential) + vPos = pShip->GetPos() + (POTENTIAL_TO_POS_ADJ * GetPotential()); + else + vPos = pTarget->GetPos() + PolarToVector(iTargetMotion + 180, rRange); - // Figure out which way we need to move to end up where we want - // (Note that we don't combine the potential because we've already accounted for - // it above). - // Cheat a little if the target cant move or is stationary to dampen our movements + // Figure out which way we need to move to end up where we want + // (Note that we don't combine the potential because we've already accounted for + // it above). + // Cheat a little if the target cant move or is stationary to dampen our movements + // We also need to cheat if our handling is really poor to avoid oscillations + // + // If we are close enough, we dont care and start shooting + + Metric rPosRadius = LIGHT_SECOND * 3; + Metric rPosRadius2 = rPosRadius * rPosRadius; + Metric rPosOffset2 = pShip->GetPos().Distance2(vPos); + + bool bTargetMobile = (pTarget->CanThrust() && pTarget->GetVel().Length2() > g_Epsilon); + bool bCheat = !bTargetMobile || m_fLowManeuverability; + + if (rPosOffset2 > rPosRadius2) + { + // need to get closer to our desired pos + + bool bTargetMobile = (pTarget->CanThrust() && pTarget->GetVel().Length2() > g_Epsilon); + bool bCheat = !bTargetMobile || m_fLowManeuverability; + + if (bCheat) + { + Metric rCheatFactor = CHEAT_FORMATION_FACTOR; + + // We need to be facing the correct direction for this. Formations can + // cheat backwards because they look ok moving as a unit, however + // individual attackers getting cheated backwards/sideways is ugly and obvious + // We might still need a little backwards dampening but we need to minimize it - vDirection = CalcManeuverFormation(pShip, vPos, pTarget->GetVel(), iTargetAngle, pTarget->CanThrust() && pTarget->GetVel().Length2() ? 0.0 : 0.2); + CVector vToPos = vPos - pShip->GetPos(); - // We don't want to thrust unless we're in position + // How far off we are in radians from the force being applied + Metric rAngleDiff = abs(vToPos.Polar() - mathDegreesToRadians(pShip->GetRotationAngle())); + Metric rCos = max(cos(rAngleDiff), 0.0); - bNoThrustThroughTurn = true; + // We get the square to bias against moving at wrong angles + Metric rCos2 = rCos * rCos; + + rCheatFactor *= rCos2; + + DebugAIOutput(pShip, strPatternSubst(CONSTLIT("Chase: Close on static fire point: Assist %r"), rCheatFactor)); + vDirection = CalcManeuverFormation(pShip, vPos, pTarget->GetVel(), iTargetAngle, rCheatFactor); + } + else + { + DebugAIOutput(pShip, "Chase: Close on fire point"); + vDirection = CalcManeuverFormation(pShip, vPos, pTarget->GetVel(), iTargetAngle, 0.0); + } + } + else + { + DebugAIOutput(pShip, "Chase: Close enough to fire point, aiming..."); + vDirection = GetPotential(); + } + + // We don't want to thrust unless we're in position + + bNoThrustThroughTurn = true; + } // Debug @@ -993,6 +1192,9 @@ bool CAIBehaviorCtx::ImplementAttackTargetManeuver (CShip *pShip, CSpaceObject * if (rTargetDist2 < rCloseRange2) { + // TODO: this needs investigation, the implemented behavior seems + // contrary to the code comments + if (AreAnglesAligned(VectorToPolar(-vTarget), pTarget->GetRotation(), 90)) { vDirection = CombinePotential(CalcManeuverCloseOnTarget(pShip, pTarget, vTarget, rTargetDist2)); @@ -1000,7 +1202,7 @@ bool CAIBehaviorCtx::ImplementAttackTargetManeuver (CShip *pShip, CSpaceObject * } else { - vDirection = CombinePotential(CalcManeuverSpiralOut(pShip, vTarget)); + vDirection = CombinePotential(CalcManeuverSpiralOutEvasive(pShip, vTarget)); DebugAIOutput(pShip, "Flyby: Target not facing us, spiral away"); } } @@ -1030,7 +1232,7 @@ bool CAIBehaviorCtx::ImplementAttackTargetManeuver (CShip *pShip, CSpaceObject * if (rTargetDist2 > GetPrimaryAimRange2()) { - DebugAIOutput(pShip, "Close on target"); + DebugAIOutput(pShip, "No Retreat: Close on target"); // Try to flank our target, if we are faster @@ -1043,8 +1245,8 @@ bool CAIBehaviorCtx::ImplementAttackTargetManeuver (CShip *pShip, CSpaceObject * else if (pTarget->CanThrust() && (pShip->GetVel().Length2() < (0.01 * 0.01 * LIGHT_SPEED * LIGHT_SPEED))) { - DebugAIOutput(pShip, "Speed away"); - vDirection = CombinePotential(CalcManeuverSpiralOut(pShip, vTarget)); + DebugAIOutput(pShip, "No Retreat: Speed away"); + vDirection = CombinePotential(CalcManeuverSpiralOutEvasive(pShip, vTarget)); } // No maneuver @@ -1054,6 +1256,13 @@ bool CAIBehaviorCtx::ImplementAttackTargetManeuver (CShip *pShip, CSpaceObject * break; } + + default: + { + DebugAIOutput(pShip, "Error: Combat style not found"); + vDirection = GetPotential(); + ASSERT(false); + } } // If our direction vector is not long enough, then it means that we @@ -1598,10 +1807,21 @@ void CAIBehaviorCtx::ImplementFireSingleWeaponOnTarget (CShip* pShip, ClearBestWeapon(); CalcBestWeapon(pShip, pTarget, rTargetDist2); - if (m_iBestWeapon == devNone) + + bool bBestWeaponDamaged = false; + + if (m_iBestWeapon != devNone) + bBestWeaponDamaged = pShip->GetDevice(m_iBestWeapon)->IsDamaged(); + + if (m_iBestWeapon == devNone || (m_fHasSecondaryWeapons && bBestWeaponDamaged)) { + // Turn to face the enemy so we dont just turn off and sit uselessly + // and get shot. + // Eventually we should intelligently figure the angle out based + // on secondary distribution around the ship + if (retiFireDir) - *retiFireDir = -1; + *retiFireDir = mathRound(mathRadiansToDegrees((pTarget->GetPos() - pShip->GetPos()).Polar())); DebugAIOutput(pShip, "Fire: No appropriate weapon found"); return; diff --git a/Mammoth/TSE/CAIBehaviorCtx.cpp b/Mammoth/TSE/CAIBehaviorCtx.cpp index 4ec1e213a..0a05704d9 100644 --- a/Mammoth/TSE/CAIBehaviorCtx.cpp +++ b/Mammoth/TSE/CAIBehaviorCtx.cpp @@ -197,14 +197,20 @@ void CAIBehaviorCtx::CalcBestWeapon (CShip *pShip, CSpaceObject *pTarget, Metric int iBestScore = 0; int iPrimaryCount = 0; int iBestNonLauncherLevel = 0; + bool bBestIsBroken = false; for (CDeviceItem DeviceItem : pShip->GetDeviceSystem()) { CInstalledDevice &Weapon = *DeviceItem.GetInstalledDevice(); - // If this weapon is not working, then skip it + // We still consider broken weapons, but only as a + // last resort - if (!Weapon.IsWorking()) + bool bIsBroken = !Weapon.IsWorking(); + + // Only skip if we havent found a better non-broken weapon already + + if (iBestWeapon != -1 && bIsBroken) continue; // See if this weapon shoots missiles. @@ -258,11 +264,13 @@ void CAIBehaviorCtx::CalcBestWeapon (CShip *pShip, CSpaceObject *pTarget, Metric case itemcatWeapon: { int iScore = CalcWeaponScore(pShip, pTarget, &Weapon, rTargetDist2); - if (iScore > iBestScore) + + if ((iScore > iBestScore && !bIsBroken) || (bBestIsBroken && !bIsBroken) || (bIsBroken && iBestWeapon == -1)) { iBestWeapon = Weapon.GetDeviceSlot(); iBestWeaponVariant = 0; iBestScore = iScore; + bBestIsBroken = bIsBroken; } Metric rMaxRange = DeviceItem.GetMaxEffectiveRange(); @@ -380,21 +388,25 @@ void CAIBehaviorCtx::CalcInvariants (CShip *pShip) // Primary aim range Metric rPrimaryRange = pShip->GetWeaponRange(devPrimaryWeapon); - Metric rAimRange = (GetFireRangeAdj() * rPrimaryRange) / (100.0 + ((pShip->GetDestiny() % 8) + 4)); + Metric rAimRange = (GetFireRangeAdj() * rPrimaryRange) / (100.0 + ((pShip->GetDestiny() % 11) + 1)); if (rAimRange < 1.5 * MIN_TARGET_DIST) rAimRange = 1.5 * MIN_TARGET_DIST; m_rPrimaryAimRange2 = rAimRange * rAimRange; // Maneuverability - Metric rMaxRotationSpeed = pShip->GetRotationDesc().GetMaxRotationSpeedDegrees(); - m_fLowManeuverability = (rMaxRotationSpeed <= 6.0); + CIntegralRotationDesc Rotation = pShip->GetRotationDesc(); + Metric rMaxRotationSpeed = Rotation.GetMaxRotationSpeedDegrees(); + Metric rMaxRotationAccel = min(Rotation.GetRotationAccelDegrees(), Rotation.GetRotationAccelStopDegrees()); + m_fLowManeuverability = (rMaxRotationSpeed <= 6.0) || (rMaxRotationSpeed > (rMaxRotationAccel + g_Epsilon) * 2); + Metric rRotationResponse = Rotation.GetRotationResponsivenessDegrees(); // Compute the minimum flanking distance. If we're very maneuverable, // can get in closer because we can turn faster to adjust for the target's - // motion. + // motion, but we need to account for our ability to alter rotation to + // avoid oversteering - Metric rDegreesPerTick = Max(1.0, Min(rMaxRotationSpeed, 60.0)); + Metric rDegreesPerTick = Max(1.0, Min(Min(rMaxRotationSpeed, rRotationResponse * 2), 60.0)); Metric rTanRot = tan(PI * rDegreesPerTick / 180.0); Metric rMinFlankDist = Max(MIN_TARGET_DIST, MAX_TARGET_SPEED / rTanRot); diff --git a/Mammoth/TSE/CIntegralRotationDesc.cpp b/Mammoth/TSE/CIntegralRotationDesc.cpp index 0bd9c22c1..f52f6dae0 100644 --- a/Mammoth/TSE/CIntegralRotationDesc.cpp +++ b/Mammoth/TSE/CIntegralRotationDesc.cpp @@ -41,22 +41,22 @@ int CIntegralRotationDesc::CalcFinalRotationFrame (int iRotationFrame, int iRota return iRotationFrame; } -int CIntegralRotationDesc::GetManeuverDelay (void) const - // GetManeuverDelay // // For compatibility we convert from our internal units to old style // maneuverability (ticks per rotation angle) +// +int CIntegralRotationDesc::GetManeuverDelay () const { return (m_iMaxRotationRate > 0 ? (int)(ROTATION_FRACTION / m_iMaxRotationRate) : 0); } -Metric CIntegralRotationDesc::GetMaxRotationSpeedDegrees (void) const - // GetMaxRotationSpeedDegrees // // Returns the max speed in degrees per tick. +// +Metric CIntegralRotationDesc::GetMaxRotationSpeedDegrees () const { if (m_iCount == 0) @@ -65,11 +65,47 @@ Metric CIntegralRotationDesc::GetMaxRotationSpeedDegrees (void) const return 360.0 * m_iMaxRotationRate / (ROTATION_FRACTION * m_iCount); } -void CIntegralRotationDesc::Init (int iFrameCount, Metric rMaxRotation, Metric rAccel, Metric rAccelStop) +// GetRotationAccelDegrees +// +// Returns the max rotation accel in degrees per tick^2. +// +Metric CIntegralRotationDesc::GetRotationAccelDegrees () const + { + if (m_iCount == 0) + return 0.0; + + return 360.0 * m_iRotationAccel / (ROTATION_FRACTION * m_iCount); + } + +// GetRotationAccelStopDegrees +// +// Returns the max rotation deccel in degrees per tick^2. +// +Metric CIntegralRotationDesc::GetRotationAccelStopDegrees () const + { + if (m_iCount == 0) + return 0.0; + + return 360.0 * m_iRotationAccelStop / (ROTATION_FRACTION * m_iCount); + } + +// GetRotationResponsivenessDegrees +// +// Returns the max possible change in rotation over a tick in degrees +// +Metric CIntegralRotationDesc::GetRotationResponsivenessDegrees () const + { + if (m_iCount == 0) + return 0.0; + + return min(GetMaxRotationSpeedDegrees(), min(GetRotationAccelDegrees(), GetRotationAccelStopDegrees())); + } // Init // // Initialize from constants +// +void CIntegralRotationDesc::Init (int iFrameCount, Metric rMaxRotation, Metric rAccel, Metric rAccelStop) { m_iCount = iFrameCount; diff --git a/Mammoth/TSE/CShip.cpp b/Mammoth/TSE/CShip.cpp index 895837f59..0fc441f1c 100644 --- a/Mammoth/TSE/CShip.cpp +++ b/Mammoth/TSE/CShip.cpp @@ -2963,11 +2963,11 @@ Metric CShip::GetInvMass (void) const return (1.0 / rMass); } -Metric CShip::GetMass (void) const - // GetMass // // Returns the mass of the object in metric tons +// +Metric CShip::GetMass (void) const { return m_pClass->GetHullDesc().GetMass() + GetItemMass(); @@ -3166,11 +3166,66 @@ void CShip::GetReactorStats (SReactorStats &Stats) const } } -int CShip::GetStealth (void) const +// GetRelativeHealth +// +// Returns an int 0-100 representing +// the relative health of this ship +// +// Values > 100 represent an indestructible or intangible object (suspended, gated, virtual, etc) +// Values < 0 represent a destroyed object +// +int CShip::GetRelativeHealth () const + { + if (IsDestroyed()) + return -1; + + if (IsImmutable() || IsIntangible()) + return INT_MAX; + + Metric rArmorHPRatio = 1.0; + + // Pick the most damaged segment + + for (int i = 0; i < m_Armor.GetSegmentCount(); i++) + { + CInstalledArmor Segment = m_Armor.GetSegment(i); + int iTotalArmorHP = Segment.GetHitPoints(); + int iTotalArmorMaxHP = Segment.GetMaxHP(this); + Metric rSegmentArmorHPRatio = iTotalArmorMaxHP ? (Metric)iTotalArmorHP / iTotalArmorMaxHP : 0.0; + + if (rSegmentArmorHPRatio < rArmorHPRatio) + rArmorHPRatio = rSegmentArmorHPRatio; + } + + // Handle ships with compartments + + if (m_fHasShipCompartments || m_Interior.GetCount()) + { + int iCompartmentHP; + int iCompartmentMaxHP; + + m_Interior.GetHitPoints(*this, m_pClass->GetInteriorDesc(), &iCompartmentHP, &iCompartmentMaxHP); + + Metric rCompartmentHPRatio = iCompartmentMaxHP ? (Metric)iCompartmentHP / iCompartmentMaxHP : 0; + + // Each counts for half of the total HP pool. This is designed to mirror the actual + // HP bar in the UI + + Metric rCombinedRatio = rArmorHPRatio * 0.5 + rCompartmentHPRatio * 0.5; + return min(100, mathRound(100 * rCombinedRatio + 0.5 - g_Epsilon)); //only return 0 if we are actually at 0 + } + + // Ships without compartments are just the most damaged armor ratio + + else + return min(100, mathRound(100 * rArmorHPRatio + 0.5 - g_Epsilon)); + } // GetStealth // // Returns the stealth of the ship +// +int CShip::GetStealth () const { int iStealth = m_Perf.GetStealth(); diff --git a/Mammoth/TSE/CStation.cpp b/Mammoth/TSE/CStation.cpp index 1fd037058..db8ff8961 100644 --- a/Mammoth/TSE/CStation.cpp +++ b/Mammoth/TSE/CStation.cpp @@ -2197,6 +2197,27 @@ IShipGenerator *CStation::GetRandomEncounterTable (int *retiFrequency) const return m_pType->GetEncountersTable(); } +// GetRelativeHealth +// +// Returns an int 0-100 representing +// the relative health of this station +// +// Values > 100 represent an indestructible or intangible object (suspended, gated, virtual, etc) +// Values < 0 represent a destroyed object +// +int CStation::GetRelativeHealth() const + { + if (IsDestroyed()) + return -1; + + if (IsImmutable() || IsIntangible() || !m_Hull.CanBeHit() || !m_Hull.GetMaxHitPoints()) + return INT_MAX; + + Metric rHPRatio = m_Hull.GetHitPoints() / m_Hull.GetMaxHitPoints(); + + return min(100, mathRound(rHPRatio * 100 + 0.5)); + } + int CStation::GetRotation (void) const // GetRotation diff --git a/Mammoth/TSE/ShipAIImpl.h b/Mammoth/TSE/ShipAIImpl.h index 7904e5b04..43869c2be 100644 --- a/Mammoth/TSE/ShipAIImpl.h +++ b/Mammoth/TSE/ShipAIImpl.h @@ -137,6 +137,7 @@ class CAIBehaviorCtx CVector CalcManeuverFormation (CShip *pShip, const CVector vDest, const CVector vDestVel, int iDestFacing, Metric rCheatThrustFactor = 0.0) const; CVector CalcManeuverSpiralIn (CShip *pShip, const CVector &vTarget, int iTrajectory = 30); CVector CalcManeuverSpiralOut (CShip *pShip, const CVector &vTarget, int iTrajectory = 30); + CVector CalcManeuverSpiralOutEvasive (CShip *pShip, const CVector &vTarget, int iTrajectory = 30); void ImplementAttackNearestTarget (CShip *pShip, Metric rMaxRange, CSpaceObject **iopTarget, CSpaceObject *pExcludeObj = NULL, bool bTurn = false); void ImplementAttackTarget (CShip *pShip, CSpaceObject *pTarget, bool bMaintainCourse = false, bool bDoNotShoot = false); void ImplementCloseOnImmobileTarget (CShip *pShip, CSpaceObject *pTarget, const CVector &vTarget, Metric rTargetDist2, Metric rTargetSpeed = 0.0); @@ -190,6 +191,7 @@ class CAIBehaviorCtx private: void DebugAIOutput (CShip *pShip, LPCSTR pText); + void DebugAIOutput (CShip *pShip, CString sText); void CalcEscortFormation (CShip *pShip, CSpaceObject *pLeader, CVector *retvPos, CVector *retvVel, int *retiFacing); bool CalcFlockingFormationCloud (CShip *pShip, CSpaceObject *pLeader, Metric rFOVRange, Metric rSeparationRange, CVector *retvPos, CVector *retvVel, int *retiFacing); bool CalcFlockingFormationRandom (CShip *pShip, CSpaceObject *pLeader, CVector *retvPos, CVector *retvVel, int *retiFacing); @@ -245,7 +247,7 @@ class CAIBehaviorCtx DWORD m_fHasAvoidPotential:1 = false; // TRUE if there is something to avoid DWORD m_fShootTargetableMissiles:1 = false; // TRUE if we try to hit targetable missiles with secondaries DWORD m_fShootAllMissiles:1 = false; // TRUE if we try to hit all missiles with secondaries - DWORD m_fLowManeuverability:1 = false; // TRUE if we maneuver at less than or equal to 12 degrees per second + DWORD m_fLowManeuverability:1 = false; // TRUE if we maneuver <=12 degrees per second or rotation accel*2 < maneuver DWORD m_fSpare7:1 = false; DWORD m_fSpare8:1 = false; From 92b7c3831d67922c10fd8477bbc188557d130416 Mon Sep 17 00:00:00 2001 From: FlufflesTheMicrosaur <68170519+FlufflesTheMicrosaur@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:52:44 -0800 Subject: [PATCH 12/14] fix: 106106: standard combat style read as standoff (#344) (cherry picked from commit b186dac47327c0e07c6760e0838b48117bc60452) --- Mammoth/TSE/CAISettings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mammoth/TSE/CAISettings.cpp b/Mammoth/TSE/CAISettings.cpp index 5abba4886..089772033 100644 --- a/Mammoth/TSE/CAISettings.cpp +++ b/Mammoth/TSE/CAISettings.cpp @@ -74,7 +74,7 @@ AICombatStyle CAISettings::ConvertToAICombatStyle (const CString &sValue) else if (strEquals(sValue, COMBAT_STYLE_NO_RETREAT)) return AICombatStyle::NoRetreat; else if (strEquals(sValue, COMBAT_STYLE_STANDARD)) - return AICombatStyle::StandOff; + return AICombatStyle::Standard; else if (strEquals(sValue, COMBAT_STYLE_STAND_OFF)) return AICombatStyle::StandOff; else From 850fc85f1bf7cbed9e3aa35cf847967a39355450 Mon Sep 17 00:00:00 2001 From: FlufflesTheMicrosaur <68170519+FlufflesTheMicrosaur@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:54:16 -0800 Subject: [PATCH 13/14] fix: 104610: fix the sound playing on repeat when too full of cargo to pick up ore (#345) (cherry picked from commit c9f8c27e9428338e6cd326d16573338b01f0495c) --- Mammoth/Include/TSEPlayer.h | 2 ++ Mammoth/TSE/CStation.cpp | 6 +++++- Transcendence/Transcendence/Transcendence.h | 9 +++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Mammoth/Include/TSEPlayer.h b/Mammoth/Include/TSEPlayer.h index 07f3d0e68..1427dbafd 100644 --- a/Mammoth/Include/TSEPlayer.h +++ b/Mammoth/Include/TSEPlayer.h @@ -213,6 +213,7 @@ class IPlayerController virtual ICCItem *CreateGlobalRef (CCodeChain &CC) { return CC.CreateInteger((int)this); } virtual CPlayerGameStats *GetGameStats (void) const { return NULL; } virtual GenomeTypes GetGenome (void) const { return genomeUnknown; } + virtual DWORD GetLastWarningTick () const { return 0; } virtual CString GetName (void) const { return NULL_STR; } virtual bool GetPropertyInteger (const CString &sProperty, int *retiValue) { return false; } virtual bool GetPropertyItemList (const CString &sProperty, CItemList *retItemList) { return false; } @@ -220,6 +221,7 @@ class IPlayerController virtual CSovereign *GetSovereign (void) const; virtual EUIMode GetUIMode (void) const { return uimodeUnknown; } virtual void OnMessageFromObj (const CSpaceObject *pSender, const CString &sMessage) { } + virtual void SetLastWarningTick (DWORD dwTick) { } virtual bool SetPropertyInteger (const CString &sProperty, int iValue) { return false; } virtual bool SetPropertyItemList (const CString &sProperty, const CItemList &ItemList) { return false; } virtual bool SetPropertyString (const CString &sProperty, const CString &sValue) { return false; } diff --git a/Mammoth/TSE/CStation.cpp b/Mammoth/TSE/CStation.cpp index db8ff8961..73ad4f5e5 100644 --- a/Mammoth/TSE/CStation.cpp +++ b/Mammoth/TSE/CStation.cpp @@ -4594,10 +4594,14 @@ void CStation::OnUpdate (SUpdateCtx &Ctx, Metric rSecondsPerTick) } // If they don't fit, we just beep + // + // Dont play the audio que faster than once per several seconds + // Ticks are DWORDs (unsigned) so we dont care if the universe ticks roll over to 0 - else + else if (GetUniverse().GetTicks() - GetUniverse().GetPlayer().GetLastWarningTick() > g_TicksPerSecond * 3) { GetUniverse().PlaySound(this, GetUniverse().FindSound(UNID_DEFAULT_CANT_DO_IT)); + GetUniverse().GetPlayer().SetLastWarningTick(GetUniverse().GetTicks()); } } diff --git a/Transcendence/Transcendence/Transcendence.h b/Transcendence/Transcendence/Transcendence.h index 38e4bbd8e..d92e7eff7 100644 --- a/Transcendence/Transcendence/Transcendence.h +++ b/Transcendence/Transcendence/Transcendence.h @@ -789,12 +789,21 @@ class CTranscendencePlayer : public IPlayerController virtual ICCItem *CreateGlobalRef (CCodeChain &CC) override { return CC.CreateInteger((int)m_pPlayer); } virtual CPlayerGameStats *GetGameStats (void) const override { return &m_pPlayer->GetGameStats(); } virtual GenomeTypes GetGenome (void) const override; + virtual DWORD GetLastWarningTick () const override { return m_EphemeralState.dwLastWarningTick; }; virtual CString GetName (void) const override; virtual EUIMode GetUIMode (void) const override; virtual void OnMessageFromObj (const CSpaceObject *pSender, const CString &sMessage) override; + virtual void SetLastWarningTick (DWORD dwTick) override { m_EphemeralState.dwLastWarningTick = dwTick; }; private: + + struct SEphemeralPlayerState + { + DWORD dwLastWarningTick = 0; // Last tick on which an audio warning has played + }; + CPlayerShipController *m_pPlayer; + SEphemeralPlayerState m_EphemeralState; }; class CTranscendenceModel From e598164db0ee84de1455163e73c4dd886a580e04 Mon Sep 17 00:00:00 2001 From: FlufflesTheMicrosaur <68170519+FlufflesTheMicrosaur@users.noreply.github.com> Date: Mon, 2 Feb 2026 11:08:54 -0800 Subject: [PATCH 14/14] chore: 106195: remove unecessary fp32 casts in 3dpoint conversion code (#357) (cherry picked from commit 851e62c3ce9dd719056749fd5a28c59f4c84aa95) --- Mammoth/TSE/C3DConversion.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Mammoth/TSE/C3DConversion.cpp b/Mammoth/TSE/C3DConversion.cpp index d9c67e7ff..b36ec57b1 100644 --- a/Mammoth/TSE/C3DConversion.cpp +++ b/Mammoth/TSE/C3DConversion.cpp @@ -5,7 +5,7 @@ #include "PreComp.h" -static Metric g_rViewAngle = 0.4636448f; // 26.56 degrees (z=12 x=6) +static Metric g_rViewAngle = 0.4636448; // 26.56 degrees (z=12 x=6) static Metric g_rK1 = sin(g_rViewAngle); static Metric g_rK2 = cos(g_rViewAngle); static Metric g_MinZg = 0.1; @@ -39,9 +39,9 @@ void C3DConversion::CalcCoord (int iScale, int iAngle, int iRadius, int iZ, int Metric rYg = rY * g_rK2 - rZ * g_rK1; Metric rZg = rY * g_rK1 + rZ * g_rK2; - rZg = Max(g_MinZg, rZg + 2.0f); + rZg = Max(g_MinZg, rZg + 2.0); - Metric rD = rScale * 2.0f; + Metric rD = rScale * 2.0; // Now convert to projection coordinates @@ -76,9 +76,9 @@ void C3DConversion::CalcCoord (int iScale, int iAngle, int iRadius, int iZ, CVec Metric rYg = rY * g_rK2 - rZ * g_rK1; Metric rZg = rY * g_rK1 + rZ * g_rK2; - rZg = Max(g_MinZg, rZg + 2.0f); + rZg = Max(g_MinZg, rZg + 2.0); - Metric rD = rScale * 2.0f; + Metric rD = rScale * 2.0; // Now convert to projection coordinates @@ -110,9 +110,9 @@ void C3DConversion::CalcCoord (Metric rScale, const CVector &vPos, Metric rPosZ, Metric rYg = rY * g_rK2 - rZ * g_rK1; Metric rZg = rY * g_rK1 + rZ * g_rK2; - rZg = Max(g_MinZg, rZg + 2.0f); + rZg = Max(g_MinZg, rZg + 2.0); - Metric rD = rScale * 2.0f; + Metric rD = rScale * 2.0; // Now convert to projection coordinates @@ -151,7 +151,7 @@ void C3DConversion::CalcPolar (int iScale, const CVector &vPos, int iZ, Metric * { Metric rScale = (Metric)iScale; Metric rZ = -(Metric)iZ / rScale; - Metric rD = rScale * 2.0f; + Metric rD = rScale * 2.0; Metric rXp = vPos.GetX() / g_KlicksPerPixel; Metric rYp = vPos.GetY() / g_KlicksPerPixel;