diff --git a/CREDITS.md b/CREDITS.md index e3ab516ac..e8d9ca1c7 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -223,6 +223,7 @@ This page lists all the individual contributions to the project by their author. - Projectile return weapon - Aircraft landing / docking direction - `DeploysInto` cursor desync fix + - Minor crate logic improvements - **Morton (MortonPL)**: - `XDrawOffset` for animations - Shield passthrough & absorption diff --git a/Phobos.vcxproj b/Phobos.vcxproj index 1ff7abb8a..f30aa9d9b 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -63,6 +63,7 @@ + diff --git a/docs/Fixed-or-Improved-Logics.md b/docs/Fixed-or-Improved-Logics.md index 9d43e1308..7d32cdd5a 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -153,7 +153,7 @@ This page describes all ingame logics that are fixed or improved in Phobos witho - `PowerUpN` building animations can now use `Powered` & `PoweredLight/Effect/Special` keys. - Fixed a desync potentially caused by displaying of cursor over selected `DeploysInto` units. - Skipped drawing rally point line when undeploying a factory. - + ## Fixes / interactions with other extensions - All forms of type conversion (including Ares') now correctly update `OpenTopped` state of passengers in transport that is converted. @@ -1158,15 +1158,26 @@ In `rulesmd.ini`: RadialIndicatorVisibility=allies ; list of Affected House Enumeration (owner/self | allies/ally | enemies/enemy | all) ``` -## Crate generation +## Crate improvements -The statistic distribution of the randomly generated crates is now more uniform within the visible map region by using an optimized sampling procedure. -- You can now limit the crates' spawn region to land only. +There are some improvements on goodie crate logic: +- The statistic distribution of the randomly generated crates is now more uniform within the visible map region by using an optimized sampling procedure. +- You can now limit the crates' spawn region to land only by setting `[CrateRules]` -> `CreateOnlyOnLand` to true. +- The limit of vehicles a player can own before unit crates start giving money instead can now be customized by setting `UnitCrateVehicleCap`. Negative numbers disable the cap entirely. +- `FreeMCV` setting is now actually respected and can be used to disable the forced unit selected from `[General]` -> `BaseUnit` that is given if player picks a crate and has enough credits but no existing buildings or `BaseUnit` vehicles. + - The previously hardcoded credits threshold that must be passed can also now be customized via `FreeMCV.CreditsThreshold`. +- It is possible to influence weighting of units given from crates (`CrateGoodie=true`) via `CrateGoodie.RerollChance`, which determines the chance that if this type of unit is rolled, it will reroll again for another type of unit. In `rulesmd.ini`: ```ini [CrateRules] -CrateOnlyOnLand=no ; boolean +CrateOnlyOnLand=false ; boolean +UnitCrateVehicleCap=50 ; integer +FreeMCV=true ; boolean +FreeMCV.CreditsThreshold=1500 ; integer + +[SOMEVEHICLE] ; VehicleType +CrateGoodie.RerollChance=0.0 ; floating point value, percents or absolute (0.0-1.0) ``` ## DropPod diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index f2373407b..fee3c6800 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -1236,6 +1236,20 @@ In `rulesmd.ini`: BigGap=false ; boolean ``` +### Spawn powerup crate + +- Warheads can now spawn powerup crates of specified type(s) on their impact cells (if free, or nearby cells if occupied something other than a crate) akin to map trigger action 108 ('Create Crate'). + - `SpawnsCrateN` where N is a number starting from 0, parsed until no key is found can be used to define the type of crate spawned. + - `SpawnsCrateN.Weight` is a number that determines relative weighting of spawning corresponding crate type vs. other listed ones (0 is no chance, higher means higher probability) defaulting to 1 if not defined. + - `SpawnsCrate.Type/Weight` is an alias for `SpawnsCrate0.Type/Weight` if latter is not set. + +In `rulesmd.ini`: +```ini +[SOMEWARHEAD] ; Warhead +SpawnsCrate(N).Type= ; Powerup crate type enum (money|unit|healbase|cloak|explosion|napalm|squad|reveal|armor|speed|firepower|icbm|invulnerability|veteran|ionstorm|gas|tiberium|pod) +SpawnsCrate(N).Weight=1 ; integer +``` + ### Trigger specific NotHuman infantry Death anim sequence - Warheads are now able to trigger specific `NotHuman=yes` infantry `Death` anim sequence using the corresponding tag. It's value represents sequences from `Die1` to `Die5`. diff --git a/docs/Whats-New.md b/docs/Whats-New.md index c520dbb1d..9ed611e57 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -10,9 +10,10 @@ You can use the migration utility (can be found on [Phobos supplementaries repo] ### From vanilla +- `[CrateRules]` -> `FreeMCV` now controls whether or not player is forced to receive unit from `[General]` -> `BaseUnit` from goodie crate if they own no buildings or any existing `BaseUnit` vehicles and own more than `[CrateRules]` -> `FreeMCV.CreditsThreshold` (defaults to 1500) credits.- Iron Curtain status is now preserved by default when converting between TechnoTypes via `DeploysInto`/`UndeploysInto`. This behavior can be turned off per-TechnoType and global basis using `[SOMETECHNOTYPE]/[CombatDamage]->IronCurtain.KeptOnDeploy=no`. - Translucent RLE SHPs will now be drawn using a more precise and performant algorithm that has no green tint and banding. Can be disabled with `rulesmd.ini->[General]->FixTransparencyBlitters=no`. - Iron Curtain status is now preserved by default when converting between TechnoTypes via `DeploysInto`/`UndeploysInto`. This behavior can be turned off per-TechnoType and global basis using `[SOMETECHNOTYPE]/[CombatDamage]->IronCurtain.KeptOnDeploy=no`. -- The obsolete `[General] WarpIn` has been enabled for the default anim type when technos are warping in. If you want to restore the vanilla behavior, use the same anim type as `WarpOut`. +- - The obsolete `[General] WarpIn` has been enabled for the default anim type when technos are warping in. If you want to restore the vanilla behavior, use the same anim type as `WarpOut`. - Vehicles with `Crusher=true` + `OmniCrusher=true` / `MovementZone=CrusherAll` were hardcoded to tilt when crushing vehicles / walls respectively. This now obeys `TiltsWhenCrushes` but can be customized individually for these two scenarios using `TiltsWhenCrusher.Vehicles` and `TiltsWhenCrusher.Overlays`, which both default to `TiltsWhenCrushes`. ### From older Phobos versions @@ -394,6 +395,10 @@ New: - Toggleable height-based shadow scaling for voxel air units (by Trsdy & Starkku) - User setting toggles for harvester counter & power delta indicator (by Starkku) - Shrapnel weapon target filtering toggle (by Starkku) +- Restore functionality of `[CrateRules]` -> `FreeMCV` with customizable credits threshold (by Starkku) +- Allow customizing the number of vehicles required for unit crates to turn into money crates (by Starkku) +- Per-VehicleType reroll chance for `CrateGoodie=true` (by Starkku) +- Warheads spawning powerup crates (by Starkku) Vanilla fixes: - Allow AI to repair structures built from base nodes/trigger action 125/SW delivery in single player missions (by Trsdy) diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index 58bb324be..987b1d7aa 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -137,6 +137,8 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) this->IronCurtain_KillOrganicsWarhead.Read(exINI, GameStrings::CombatDamage, "IronCurtain.KillOrganicsWarhead"); this->CrateOnlyOnLand.Read(exINI, GameStrings::CrateRules, "CrateOnlyOnLand"); + this->UnitCrateVehicleCap.Read(exINI, GameStrings::CrateRules, "UnitCrateVehicleCap"); + this->FreeMCV_CreditsThreshold.Read(exINI, GameStrings::CrateRules, "FreeMCV.CreditsThreshold"); this->ROF_RandomDelay.Read(exINI, GameStrings::CombatDamage, "ROF.RandomDelay"); @@ -287,6 +289,8 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->DisplayIncome_AllowAI) .Process(this->DisplayIncome_Houses) .Process(this->CrateOnlyOnLand) + .Process(this->UnitCrateVehicleCap) + .Process(this->FreeMCV_CreditsThreshold) .Process(this->RadialIndicatorVisibility) .Process(this->DrawTurretShadow) .Process(this->IsVoiceCreatedGlobal) diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index c134e46ed..5773bdd14 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -103,6 +103,8 @@ class RulesExt Valueable ToolTip_Background_BlurSize; Valueable CrateOnlyOnLand; + Valueable UnitCrateVehicleCap; + Valueable FreeMCV_CreditsThreshold; Valueable RadialIndicatorVisibility; Valueable DrawTurretShadow; ValueableIdx AnimRemapDefaultColorScheme; @@ -185,6 +187,8 @@ class RulesExt , DisplayIncome_AllowAI { true } , DisplayIncome_Houses { AffectedHouse::All } , CrateOnlyOnLand { false } + , UnitCrateVehicleCap { 50 } + , FreeMCV_CreditsThreshold { 1500 } , RadialIndicatorVisibility { AffectedHouse::Allies } , DrawTurretShadow { false } , IsVoiceCreatedGlobal { false } diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index 28780eac4..b946295bc 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -279,6 +279,8 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->Convert_HumanToComputer.Read(exINI, pSection, "Convert.HumanToComputer"); this->Convert_ComputerToHuman.Read(exINI, pSection, "Convert.ComputerToHuman"); + this->CrateGoodie_RerollChance.Read(exINI, pSection, "CrateGoodie.RerollChance"); + // Ares 0.2 this->RadarJamRadius.Read(exINI, pSection, "RadarJamRadius"); @@ -605,6 +607,8 @@ void TechnoTypeExt::ExtData::Serialize(T& Stm) .Process(this->DroppodType) .Process(this->Convert_HumanToComputer) .Process(this->Convert_ComputerToHuman) + + .Process(this->CrateGoodie_RerollChance) ; } void TechnoTypeExt::ExtData::LoadFromStream(PhobosStreamReader& Stm) diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index 1e0c798b0..fea9f3424 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -191,6 +191,8 @@ class TechnoTypeExt Valueable Convert_HumanToComputer; Valueable Convert_ComputerToHuman; + Valueable CrateGoodie_RerollChance; + struct LaserTrailDataEntry { ValueableIdx idxType; @@ -377,6 +379,8 @@ class TechnoTypeExt , DroppodType {} , Convert_HumanToComputer { } , Convert_ComputerToHuman { } + + , CrateGoodie_RerollChance { 0.0 } { } virtual ~ExtData() = default; diff --git a/src/Ext/WarheadType/Body.cpp b/src/Ext/WarheadType/Body.cpp index bc177cb1d..4e0a73eb4 100644 --- a/src/Ext/WarheadType/Body.cpp +++ b/src/Ext/WarheadType/Body.cpp @@ -243,6 +243,44 @@ void WarheadTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) || this->InflictLocomotor || this->RemoveInflictedLocomotor ); + + char tempBuffer[32]; + Nullable crateType; + Nullable weight; + + for (size_t i = 0; ; i++) + { + crateType.Reset(); + weight.Reset(); + + _snprintf_s(tempBuffer, sizeof(tempBuffer), "SpawnsCrate%u.Type", i); + crateType.Read(exINI, pSection, tempBuffer); + + if (i == 0 && !crateType.isset()) + crateType.Read(exINI, pSection, "SpawnsCrate.Type"); + + if (!crateType.isset()) + break; + + if (this->SpawnsCrate_Types.size() < i) + this->SpawnsCrate_Types[i] = crateType; + else + this->SpawnsCrate_Types.push_back(crateType); + + _snprintf_s(tempBuffer, sizeof(tempBuffer), "SpawnsCrate%u.Weight", i); + weight.Read(exINI, pSection, tempBuffer); + + if (i == 0 && !weight.isset()) + weight.Read(exINI, pSection, "SpawnsCrate.Weight"); + + if (!weight.isset()) + weight = 1; + + if (this->SpawnsCrate_Weights.size() < i) + this->SpawnsCrate_Weights[i] = weight; + else + this->SpawnsCrate_Weights.push_back(weight); + } } template @@ -316,6 +354,9 @@ void WarheadTypeExt::ExtData::Serialize(T& Stm) .Process(this->Shield_Respawn_Types) .Process(this->Shield_SelfHealing_Types) + .Process(this->SpawnsCrate_Types) + .Process(this->SpawnsCrate_Weights) + .Process(this->NotHuman_DeathSequence) .Process(this->LaunchSW) .Process(this->LaunchSW_RealLaunch) diff --git a/src/Ext/WarheadType/Body.h b/src/Ext/WarheadType/Body.h index f8936a3fe..d52c3cbeb 100644 --- a/src/Ext/WarheadType/Body.h +++ b/src/Ext/WarheadType/Body.h @@ -78,6 +78,9 @@ class WarheadTypeExt Valueable Shield_SelfHealing_RestartInCombatDelay; Valueable Shield_SelfHealing_RestartTimer; + std::vector SpawnsCrate_Types; + std::vector SpawnsCrate_Weights; + ValueableVector Shield_AttachTypes; ValueableVector Shield_RemoveTypes; Valueable Shield_ReplaceOnly; @@ -115,7 +118,6 @@ class WarheadTypeExt Valueable InflictLocomotor; Valueable RemoveInflictedLocomotor; - // Ares tags // http://ares-developers.github.io/Ares-docs/new/warheads/general.html Valueable AffectsEnemies; @@ -204,6 +206,9 @@ class WarheadTypeExt , Shield_Respawn_Types {} , Shield_SelfHealing_Types {} + , SpawnsCrate_Types {} + , SpawnsCrate_Weights {} + , NotHuman_DeathSequence { -1 } , LaunchSW {} , LaunchSW_RealLaunch { true } diff --git a/src/Ext/WarheadType/Detonate.cpp b/src/Ext/WarheadType/Detonate.cpp index ba6993749..bdc589157 100644 --- a/src/Ext/WarheadType/Detonate.cpp +++ b/src/Ext/WarheadType/Detonate.cpp @@ -62,6 +62,14 @@ void WarheadTypeExt::ExtData::Detonate(TechnoClass* pOwner, HouseClass* pHouse, } } + if (this->SpawnsCrate_Types.size() > 0) + { + int index = GeneralUtils::ChooseOneWeighted(ScenarioClass::Instance->Random.RandomDouble(), &this->SpawnsCrate_Weights); + + if (index < static_cast(this->SpawnsCrate_Types.size())) + MapClass::Instance->PlacePowerupCrate(CellClass::Coord2Cell(coords), this->SpawnsCrate_Types.at(index)); + } + for (const int swIdx : this->LaunchSW) { if (const auto pSuper = pHouse->Supers.GetItem(swIdx)) @@ -212,12 +220,12 @@ void WarheadTypeExt::ExtData::ApplyShieldModifiers(TechnoClass* pTarget) if (pExt->Shield) { auto isShieldTypeEligible = [pExt](Iterator elements) -> bool - { - if (elements.size() > 0 && !elements.contains(pExt->Shield->GetType())) - return false; + { + if (elements.size() > 0 && !elements.contains(pExt->Shield->GetType())) + return false; - return true; - }; + return true; + }; if (this->Shield_Break && pExt->Shield->IsActive() && isShieldTypeEligible(this->Shield_Break_Types.GetElements(this->Shield_AffectTypes))) pExt->Shield->BreakShield(this->Shield_BreakAnim.Get(nullptr), this->Shield_BreakWeapon.Get(nullptr)); diff --git a/src/Misc/Hooks.BugFixes.cpp b/src/Misc/Hooks.BugFixes.cpp index 5617137d5..7feaa7206 100644 --- a/src/Misc/Hooks.BugFixes.cpp +++ b/src/Misc/Hooks.BugFixes.cpp @@ -569,37 +569,6 @@ DEFINE_HOOK(0x70BCE6, TechnoClass_GetTargetCoords_BuildingFix, 0x6) return 0; } -DEFINE_HOOK(0x56BD8B, MapClass_PlaceRandomCrate_Sampling, 0x5) -{ - enum { SpawnCrate = 0x56BE7B, SkipSpawn = 0x56BE91 }; - - int XP = 2 * MapClass::Instance->VisibleRect.X - MapClass::Instance->MapRect.Width - + ScenarioClass::Instance->Random.RandomRanged(0, 2 * MapClass::Instance->VisibleRect.Width); - int YP = 2 * MapClass::Instance->VisibleRect.Y + MapClass::Instance->MapRect.Width - + ScenarioClass::Instance->Random.RandomRanged(0, 2 * MapClass::Instance->VisibleRect.Height + 2); - CellStruct candidate { (short)((XP + YP) / 2),(short)((YP - XP) / 2) }; - - auto pCell = MapClass::Instance->TryGetCellAt(candidate); - if (!pCell) - return SkipSpawn; - - if (!MapClass::Instance->IsWithinUsableArea(pCell, true)) - return SkipSpawn; - - bool isWater = pCell->LandType == LandType::Water; - if (isWater && RulesExt::Global()->CrateOnlyOnLand.Get()) - return SkipSpawn; - - REF_STACK(CellStruct, cell, STACK_OFFSET(0x28, -0x18)); - cell = MapClass::Instance->NearByLocation(pCell->MapCoords, - isWater ? SpeedType::Float : SpeedType::Track, - -1, MovementZone::Normal, false, 1, 1, false, false, false, true, CellStruct::Empty, false, false); - - R->EAX(&cell); - - return SpawnCrate; -} - // Fixes C4=no amphibious infantry being killed in water if Chronoshifted/Paradropped there. DEFINE_HOOK(0x51A996, InfantryClass_PerCellProcess_KillOnImpassable, 0x5) { diff --git a/src/Misc/Hooks.Crates.cpp b/src/Misc/Hooks.Crates.cpp new file mode 100644 index 000000000..b4285a419 --- /dev/null +++ b/src/Misc/Hooks.Crates.cpp @@ -0,0 +1,94 @@ +#include +#include + +#include +#include + +#include + +DEFINE_HOOK(0x56BD8B, MapClass_PlaceRandomCrate_Sampling, 0x5) +{ + enum { SpawnCrate = 0x56BE7B, SkipSpawn = 0x56BE91 }; + + int XP = 2 * MapClass::Instance->VisibleRect.X - MapClass::Instance->MapRect.Width + + ScenarioClass::Instance->Random.RandomRanged(0, 2 * MapClass::Instance->VisibleRect.Width); + + int YP = 2 * MapClass::Instance->VisibleRect.Y + MapClass::Instance->MapRect.Width + + ScenarioClass::Instance->Random.RandomRanged(0, 2 * MapClass::Instance->VisibleRect.Height + 2); + + CellStruct candidate { (short)((XP + YP) / 2),(short)((YP - XP) / 2) }; + auto pCell = MapClass::Instance->TryGetCellAt(candidate); + + if (!pCell) + return SkipSpawn; + + if (!MapClass::Instance->IsWithinUsableArea(pCell, true)) + return SkipSpawn; + + bool isWater = pCell->LandType == LandType::Water; + + if (isWater && RulesExt::Global()->CrateOnlyOnLand.Get()) + return SkipSpawn; + + REF_STACK(CellStruct, cell, STACK_OFFSET(0x28, -0x18)); + + cell = MapClass::Instance->NearByLocation(pCell->MapCoords, + isWater ? SpeedType::Float : SpeedType::Track, + -1, MovementZone::Normal, false, 1, 1, false, false, false, true, CellStruct::Empty, false, false); + + R->EAX(&cell); + + return SpawnCrate; +} + +// Change RulesClass->FreeMCV default from 0 to 1. +DEFINE_PATCH(0x6656B3, 0x89, 0x4E); + +DEFINE_HOOK(0x481BB8, CellClass_GoodieCheck_FreeMCV, 0x6) +{ + enum { SkipForcedMCV = 0x481C03, EnableForcedMCV = 0x481BF6 }; + + GET(HouseClass*, pHouse, EDI); + GET_STACK(UnitTypeClass*, pBaseUnit, STACK_OFFSET(0x188, -0x138)); + + if (RulesClass::Instance->FreeMCV && pHouse->Available_Money() > RulesExt::Global()->FreeMCV_CreditsThreshold && + SessionClass::Instance->Config.Bases && !pHouse->OwnedBuildings && !pHouse->CountOwnedNow(pBaseUnit)) + { + return EnableForcedMCV; + } + + return SkipForcedMCV; +} + +DEFINE_HOOK(0x481C27, CellClass_GoodieCheck_UnitCrateVehicleCap, 0x5) +{ + enum { Capped = 0x481C44, NotCapped = 0x481C4A }; + + GET(HouseClass*, pHouse, EDX); + + if (RulesExt::Global()->UnitCrateVehicleCap < 0 || pHouse->OwnedUnits <= RulesExt::Global()->UnitCrateVehicleCap) + return NotCapped; + + return Capped; +} + +DEFINE_HOOK(0x4821BD, CellClass_GoodieCheck_CrateGoodie, 0x6) +{ + enum { SkipGameCode = 0x4821C3 }; + + GET(UnitTypeClass*, pUnitType, EDI); + + bool crateGoodie = pUnitType->CrateGoodie; + + if (crateGoodie) + { + auto const pTypeExt = TechnoTypeExt::ExtMap.Find(pUnitType); + + if (pTypeExt->CrateGoodie_RerollChance > 0.0) + crateGoodie = pTypeExt->CrateGoodie_RerollChance < ScenarioClass::Instance->Random.RandomDouble(); + } + + R->CL(crateGoodie); + + return SkipGameCode; +} diff --git a/src/Utilities/TemplateDef.h b/src/Utilities/TemplateDef.h index c158d0f05..c5e2202c7 100644 --- a/src/Utilities/TemplateDef.h +++ b/src/Utilities/TemplateDef.h @@ -47,6 +47,7 @@ #include #include #include +#include #include #include #include @@ -594,6 +595,39 @@ namespace detail return false; } + template <> + inline bool read(Powerup& value, INI_EX& parser, const char* pSection, const char* pKey) + { + if (parser.ReadString(pSection, pKey)) + { + auto const& powerupNames = Powerups::Effects; + int index = -1; + + for (size_t i = 0; i < powerupNames.size(); i++) + { + if (!_strcmpi(parser.value(), powerupNames[i])) + { + index = static_cast(i); + break; + } + } + + if (index >= 0) + { + value = Powerup(index); + } + else + { + Debug::INIParseFailed(pSection, pKey, parser.value(), "Expected a powerup crate type"); + return false; + } + + return true; + } + + return false; + } + template <> inline bool read(SuperWeaponAITargetingMode &value, INI_EX &parser, const char *pSection, const char *pKey) {