From 7538fe19f422a9cbad47fa1949fae705048e5362 Mon Sep 17 00:00:00 2001 From: Basilisk3 <126026384+Basilisk3@users.noreply.github.com> Date: Sat, 19 Oct 2024 12:36:29 +0200 Subject: [PATCH 01/13] Update the balance section of `2024-07-07-3810.md` to match that of `3810.md` (#6478) --- docs/_posts/2024-07-07-3810.md | 200 ++++++++++++++++++++++----------- 1 file changed, 134 insertions(+), 66 deletions(-) diff --git a/docs/_posts/2024-07-07-3810.md b/docs/_posts/2024-07-07-3810.md index 0fb791cfa9..dfcb9ed2d8 100644 --- a/docs/_posts/2024-07-07-3810.md +++ b/docs/_posts/2024-07-07-3810.md @@ -15,89 +15,157 @@ permalink: changelog/3810 ## Balance -- Make shields absorb ACU explosions. +- (#6073, #6094) Allow Mongoose to fire from transports. - Both mobile and static shields absorb full damage from ACU explosions. For example, this prevents ACU explosions from killing all engineers inside shielded bases/firebases or killing all units in mobile-shielded T2 armies. - The structure part of static shields still takes reduced structure damage from ACU explosions. +- (#5895) After receiving several buffs, the Seraphim Tech 3 Submarine Hunter has become too oppressive, especially in larger formations. These changes aim to bring it back in line, while preserving its identity. -- Allow Mongoose to fire from transports. + - Yathsou: T3 Submarine Hunter (XSS0304): + - Damage: 290 --> 380 + - RateOfFire: 0.25 --> 0.22 + - MuzzleSalvoDelay: 0.7 --> 1 + - MuzzleSalvoSize: 5 --> 4 + - DPS: 363 --> 338 + - Torpedo Defense RateOfFire (x2): 0.15 --> 0.1 -- Increase UEF T3 MAA's DPS and damage, and swap its speed with Aeon's T3 MAA. +- (#5883) Allow Chrono Dampener to fire immediately when a unit comes into range. - - Cougar: DPS: 176 -> 206, Damage per volley: 1200 -> 1400, Speed: 3.3 -> 3.5 - - Redeemer: Speed: 3.5 -> 3.3 + Previously, Chrono was synced to game time to prevent stun locking. Now, units have a cooldown before they can be stunned by any Chrono, which prevents stun locking while allowing the new Chrono to be more responsive. - This brings its DPS closer in line with other factions, lets it 3 shot strategic bombers instead of 4 shot, and lets it better escort Titans/Parashields. - Redeemer's extra speed wasn't very necessary for its role within Aeon, so it was chosen to be swapped with. +- (#5901) Increase UEF T3 MAA's damage and DPS, and swap its speed with Aeon's T3 MAA. -- Fine tune the balance of the Tech 2 Navy stage. + - Cougar: T3 Mobile Rapid-fire AA Cannon (DELK002): + - Damage per volley: 1200 --> 1400 + - DPS: 176 --> 206 + - Speed: 3.3 -> 3.5 + - Redeemer: T3 Mobile Missile Anti-Air (DALK003): + - Speed: 3.5 -> 3.3 - - Uashavoh: T2 Destroyer (XSS0201): - - AntiTorpedo RateOfFire: 0.26 -> 0.3 - - Salem Class: T2 Destroyer (URS0201): - - BackUpDistance: 5 -> 10 - - Cooper: T2 Torpedo Boat (XES0102): - - Health: 1750 -> 2000 + This brings its DPS closer in line with other factions. It can now take out a strategic bomber in 3 salvo's instead of 4. And with the increased movement speed it can better keep up to Titans and Parashields. Swapping the speed with Redeemer maintains a semblance of faction diversity. Redeemer was chosen because it doesn't really need its speed due to asylums being far too quick (4.0) to keep up with, and harbingers being relatively slow at 3.0 speed. -- After receiving several buffs, the Seraphim Tech 3 Submarine Hunter has become too oppressive, especially in larger formations. These changes aim to bring it back in line, while keeping its identity. +- (#6060, #6296) Make static shields absorb ACU explosions. - - Yathsou: T3 Submarine Hunter (XSS0304): - - Damage: 290 -> 380 - - RateOfFire: 0.25 -> 0.22 - - MuzzleSalvoDelay: 0.7 -> 1 - - MuzzleSalvoSize: 5 -> 4 - - DPS: 363 -> 338 - - Torpedo Defense RateOfFire (x2): 0.15 -> 0.1 - -- The `BuildRate` and `BuildTime` stats of all Radars and Sonars are updated to be more streamlined. The build time of Tech 2 Sonars is corrected to ensure it is not longer than that of Tech 3 Sonars. Tech 3 Sonars get their very low build time increased, to compensate for their Tech 2 counterparts being much quicker to build and providing more build power. - - - Tech 1 Radars - - BuildRate: 14.08 -> 13 - - Tech 2 Radars - - BuildRate: 21.46 -> 20 - - BuildTime: 845 -> 780 - - Tech 3 Radars - - BuildTime: 2575 -> 2400 - - Tech 1 Sonars - - BuildRate: 14.08 -> 13 - - BuildTime: 127.5 -> 125 - - BuildCostEnergy: 1275 -> 1250 - - Tech 2 Sonars - - BuildRate: 15 -> 20 - - BuildTime: 1680 -> 780 - - BuildTime: 2120 -> 1040 (Seraphim) - - Tech 3 Sonars - - BuildTime: 750 -> 1200 - - BuildTime: 900 -> 1400 (Cybran) - -- The `BuildRate` and `BuildTime` stats of several Shield Generators are updated to be more streamlined. The Seraphim Tech 2 Shield Generator gains build power, as it previously had a very low amount. The Aeon Tech 2 Shield Generator loses its build power, as it cannot be upgraded. - - - Tech 2 Shield Generators - - BuildRate: 13.66 -> 0 (Aeon) - - BuildRate: 12.98 -> 20 (Seraphim) - - BuildRate: 19.95 -> 20 (UEF) - - Tech 3 Shield Generators - - BuildTime: 5841 -> 5800 (Seraphim) - - BuildTime: 4988 -> 5000 (UEF) - -- Reduce TML HP and add a Death Weapon - - - HP: 1500 -> 900 + Static shields absorb full damage from ACU explosions. For example, this prevents ACU explosions from killing all engineers inside shielded bases/firebases. + Mobile shields do not absorb damage from ACU explosions because the ACU explosion is an anti-snowball mechanic and it would be very unfair for Aeon and UEF T2 to ignore it. + + The structure part of static shields still takes reduced structure damage from ACU explosions. + +- (#6104) Reduce the hit points of stationary tactical missile launchers (TML) and make them volatile. + + - HP: 1500 --> 900 - Death Weapon - Damage: 750 - AoE (UEF/Aeon/Sera): 2 - AoE (Cybran): 3 -- Increase the hitpoints of the Aeon Tech 1 MAA so that it can survive a single bomb from a Seraphim Tech 1 bomber. +- (#6103) Greatly increase Othuum's maneuverability and slightly increase the range of its short range guns to make it better in early T3 rushes and T3 raids. + + - Othuum: T3 Siege Tank (XSL0303): + - TurnRate: 75 -> 90 + - MaxBrake: 2.85 -> 3.55 + - Bolters' range: 20 -> 22 + - Bolters' yaw speed: 120 -> 170 + - Thau cannon yaw speed: 90 -> 125 + +- (#6179) Increase Ilshavoh's max turn rate from 75 to 90 and on the spot turn rate from 45 to 90. + +- (#6107) Absolver can no longer damage shields while they are disabled. + + Shields can now recharge as usual. + +- (#6125) Increase the hitpoints of the Aeon Tech 1 MAA so that it can survive the first salvo of a Seraphim Tech 1 bomber. - Thistle: T1 Mobile Anti-Air Gun (UAL0104): - - MaxHealth: 250 -> 265 + - MaxHealth: 250 --> 265 + +- (#5874) Reduce the costs of the UEF Bubble Shield Upgrade. + + - Energy Cost: 70000 --> 45000 + - Mass Cost: 2000 --> 1400 + - Build Time: 1700 --> 1500 + - Shield + - HP: 7000 --> 5000 + - Recharge Time: 60 --> 45 + - Regen Rate: 67 --> 60 + +- (#6140) Slightly buff the Seraphim Tech 2 gunship to better align it with its intended role as a 'Tech 2.5-unit'. + + - Vulthoo: T2 Gunship (XSA0203): + - Damage (x2): 19.2 --> 20 + - MaxRadius (x2): 22 --> 24 + +- (#6141) Slightly buff the Hoplite by increasing the muzzle velocity of its rockets. + + - Hoplite: T2 Rocket Bot (DRL0204): + - MuzzleVelocity: 20 --> 25 -- Allow the Selen to activate its cloak more quickly and increase the vision and radar radius it retains after cloaking itself. - - Selen: T1 Combat Scout (XSL0101): - - StealthWaitTime: 3 -> 1.5 - - VisionRadius while stealthed/cloaked: 0.6 -> 0.8 - - RadarRadius while stealthed/cloaked: 0.6 -> 0.8 +- (#6150) Reduce Salvation's maximum reload discount adjacency bonus from 42% to 20%. + + After being fixed a long time ago, the adjacency bonus made Salvation more effective than Mavor as a game ending artillery as recently discovered. Since Salvation's purpose is to be spammed en masse after a Paragon, and not to be built independently as a superior artillery piece, the adjacency bonus that makes building Salvation without Paragon overpowered is nerfed. + +- (#5874) Fine tune the balance of the Tech 2 naval stage. + + - Uashavoh: T2 Destroyer (XSS0201): + - AntiTorpedo RateOfFire: 0.26 --> 0.3 + - Salem Class: T2 Destroyer (URS0201): + - BackUpDistance: 5 --> 10 + - Cooper: T2 Torpedo Boat (XES0102): + - Health: 1750 --> 2000 + +- (#6235) Tone down the Tempest, as it has become too oppressive and snowballed too quickly. Most importantly, its extremely powerful depth charges now fire once every 6 seconds instead of once every 5 seconds and have a reduced range. Additionally, the amount of mass required for it to attain one level of veterancy is increased from 12k to 18k. + + - Tempest: Experimental Battleship (UAS0401): + - Oblivion Cannon Damage: 8000 --> 10000 + - Oblivion Cannon RateOfFire: 0.1 --> 0.08 + - Harmonic Depth Charge MaxRadius: 80 --> 65 + - Harmonic Depth Charge RateOfFire: 0.2 --> 0.1667 + - Harmonic Depth Charge DPS: 420 --> 350 + - VeteranMassMult: 0.5 --> 0.75 + - Atlantis: Experimental Aircraft Carrier (UES0401): + - TurnRate: 12 --> 18 + - VeteranMassMult: 0.5 --> 0.75 + +- (#6252) A bug fix reverts a 1.2 -> 1.3 second reload time increase for the following unit weapons: + + - Megalith proton cannons, CZAR AA missiles, Pillar cannons, and Cybran cruiser AA. + +- (#6202) Increase the energy maintenance cost of the Barracuda (Cybran T2 Sub) from 30 to 45. This makes it more of a choice to enable the stealth, similar to how it is for the Cybran ASF. + +- (#6211) Increase the velocity and lifetime of the Wailer's AA-missiles, since they often expired before they were able to reach their target. + + - Wailer: T3 Heavy Gunship (XRA0305): + - MuzzleVelocity: 13 --> 22 + - ProjectileLifetime: 2.0 --> 2.4 + +- (#6043) The `BuildRate` and `BuildTime` stats of all Radars and Sonars are updated to be more streamlined. The build time of Tech 2 Sonars is corrected to ensure it is not longer than that of Tech 3 Sonars. Tech 3 Sonars get their very low build time increased, to compensate for their Tech 2 counterparts being much quicker to build and providing more build power. + + - Tech 1 Radars: + - BuildRate: 14.08 --> 13 + - Tech 2 Radars: + - BuildRate: 21.46 --> 20 + - BuildTime: 845 --> 780 + - Tech 3 Radars: + - BuildTime: 2575 --> 2400 + - Tech 1 Sonars: + - BuildRate: 14.08 --> 13 + - BuildTime: 127.5 --> 125 + - BuildCostEnergy: 1275 --> 1250 + - Tech 2 Sonars: + - BuildRate: 15 --> 20 + - BuildTime: 1680 --> 780 + - BuildTime: 2120 --> 1040 (Seraphim) + - Tech 3 Sonars: + - BuildTime: 750 --> 1200 + - BuildTime: 900 --> 1400 (Cybran) + +- (#6082) The `BuildRate` and `BuildTime` statistics of several shield generators are updated to be more streamlined. The Seraphim Tech 2 Shield Generator gains build power, as it previously had a very low amount. The Aeon Tech 2 Shield Generator loses its build power, as it cannot be upgraded. + + - Tech 2 Shield Generators: + - BuildRate: 13.66 --> 0 (Aeon) + - BuildRate: 12.98 --> 20 (Seraphim) + - BuildRate: 19.95 --> 20 (UEF) + - Tech 3 Shield Generators: + - BuildTime: 5841 --> 5800 (Seraphim) + - BuildTime: 4988 --> 5000 (UEF) ## Features From abb0e12a9b574fa8b3511baa286a495bfc4a922b Mon Sep 17 00:00:00 2001 From: Basilisk3 <126026384+Basilisk3@users.noreply.github.com> Date: Sat, 19 Oct 2024 16:22:02 +0200 Subject: [PATCH 02/13] UnitviewDetail.lua: Introduce total DPS calculations (#6424) ## Description of the proposed changes Shows a DPS total for all weapons not categorized as 'Anti Air' or 'Anti Navy' in the additional unit details displayed when `Show Armament Detail in Build Menu` is enabled in the settings. - Useful for units with multiple weapons of this type, such as the Uashavoh (Seraphim Destroyer), Ythotha or the Mongoose. - Accounts for multiple instances of the same weapon being present. Example: the Fatboy is correctly displayed as having a total DPS of 4000. - ~~Also introduce a DPS/M calculation; I do not believe this is calulcated anywhere in th UI or the unit databases.~~ ## Checklist - [x] Changes are annotated, including comments where useful. - [x] Changes are documented in the changelog for the next game version. - [x] Remove the entry if the unit only has 'Anti Air' or 'Anti Navy' weapons, or no weapons at all. - [x] Improve the naming of the stats. - [x] Split the ground DPS stat into direct fire and indirect fire DPS. --- changelog/snippets/features.6424.md | 5 +++ loc/US/strings_db.lua | 4 +++ lua/ui/game/unitviewDetail.lua | 52 ++++++++++++++++++++++++++--- units/URL0101/URL0101_unit.bp | 1 - units/XSL0303/XSL0303_unit.bp | 2 +- 5 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 changelog/snippets/features.6424.md diff --git a/changelog/snippets/features.6424.md b/changelog/snippets/features.6424.md new file mode 100644 index 0000000000..5faf285a74 --- /dev/null +++ b/changelog/snippets/features.6424.md @@ -0,0 +1,5 @@ +- (#6424) Calculate DPS totals in the additional unit details displayed when `Show Armament Detail in Build Menu` is enabled in the settings. This is useful for summarizing or comparing the stats of units with multiple instances of the same weapon, or multiple weapons of a similar type. If the unit only possesses one valid weapon, the total is not calculated. + + - For example, the Ythotha (Seraphim Experimental Assault Bot), which has multiple weapons, is now displayed as possessing a combined total of `3794` direct fire DPS. This is displayed in an additional field in the UI, the stats of the individual weapons of the unit remain accessible in the same locations as before. + +- (#6424) Do not display the Mole's Target Tracker in the additional unit details displayed when `Show Armament Detail in Build Menu` is enabled. diff --git a/loc/US/strings_db.lua b/loc/US/strings_db.lua index 9841cf163c..f19efe6f6e 100644 --- a/loc/US/strings_db.lua +++ b/loc/US/strings_db.lua @@ -6767,6 +6767,10 @@ uvd_0014="Damage: %.8g - %.8g, Splash: %.3g - %.3g" uvd_0015="Damage: %.8g x%d, Splash: %.3g" uvd_0016="Enhancements: %d" uvd_0017="Transport Speed Reduction: %.3g" +uvd_0018="Total Direct Fire DPS: %d" +uvd_0019="Total Indirect Fire DPS: %d" +uvd_0020="Total Anti Navy DPS: %d" +uvd_0021="Total Anti Air DPS: %d" uvd_DPS="(DPS: %d)" uvd_ManualFire="(Manual Fire)" diff --git a/lua/ui/game/unitviewDetail.lua b/lua/ui/game/unitviewDetail.lua index a62dce3816..a71bb861a5 100644 --- a/lua/ui/game/unitviewDetail.lua +++ b/lua/ui/game/unitviewDetail.lua @@ -547,6 +547,7 @@ function WrapAndPlaceText(bp, builder, descID, control) if not table.empty(bp.Weapon) then local weapons = {upgrades = {normal = {}, death = {}}, basic = {normal = {}, death = {}}} + local totalWeaponCount = 0 for _, weapon in bp.Weapon do if not weapon.WeaponCategory then continue end local dest = weapons.basic @@ -563,11 +564,18 @@ function WrapAndPlaceText(bp, builder, descID, control) else dest[weapon.DisplayName] = {info = weapon, count = 1} end + if not dest.death then + totalWeaponCount = totalWeaponCount + 1 + end end for k, v in weapons do if not table.empty(v.normal) or not table.empty(v.death) then table.insert(blocks, {color = UIUtil.fontColor, lines = {LOC('')..':'}}) end + local totalDirectFireDPS = 0 + local totalIndirectFireDPS = 0 + local totalNavalDPS = 0 + local totalAADPS = 0 for name, weapon in v.normal do local info = weapon.info local weaponDetails1 = LOCStr(name)..' ('..LOCStr(info.WeaponCategory)..') ' @@ -653,10 +661,23 @@ function WrapAndPlaceText(bp, builder, descID, control) CycleTime = CycleTime + FiringCooldown end + local DPS = 0 if not info.ManualFire and info.WeaponCategory ~= 'Kamikaze' and info.WeaponCategory ~= 'Defense' then --Round DPS, or else it gets floored in string.format. - local DPS = MATH_IRound(Damage * CycleProjs / CycleTime) + DPS = MATH_IRound(Damage * CycleProjs / CycleTime) weaponDetails1 = weaponDetails1..LOCF('', DPS) + -- Do not calulcate the DPS total if the unit only has one valid weapon. + if totalWeaponCount > 1 then + if (info.WeaponCategory == 'Direct Fire' or info.WeaponCategory == 'Direct Fire Naval' or info.WeaponCategory == 'Direct Fire Experimental') and not info.IgnoreIfDisabled then + totalDirectFireDPS = totalDirectFireDPS + DPS * weapon.count + elseif info.WeaponCategory == 'Indirect Fire' or info.WeaponCategory == 'Missile' or info.WeaponCategory == 'Artillery' or info.WeaponCategory == 'Bomb' then + totalIndirectFireDPS = totalIndirectFireDPS + DPS * weapon.count + elseif info.WeaponCategory == 'Anti Navy' then + totalNavalDPS = totalNavalDPS + DPS * weapon.count + elseif info.WeaponCategory == 'Anti Air' then + totalAADPS = totalAADPS + DPS * weapon.count + end + end end -- Avoid saying a unit fires a salvo when it in fact has a constant rate of fire @@ -681,7 +702,6 @@ function WrapAndPlaceText(bp, builder, descID, control) Damage, info.DamageRadius, info.MinRadius, info.MaxRadius, CycleTime) end - end if weapon.count > 1 then weaponDetails1 = weaponDetails1..' x'..weapon.count @@ -723,11 +743,33 @@ function WrapAndPlaceText(bp, builder, descID, control) weaponDetails = weaponDetails..' x'..weapon.count end table.insert(lines, weaponDetails) + table.insert(blocks, {color = 'FFFF0000', lines = lines}) end - if not table.empty(v.normal) or not table.empty(v.death) then - table.insert(lines, '') + + -- Only display the totalDPS stats if they are greater than 0. + -- Prevent the totalDPS stats from being displayed under the 'Upgrades' tab and avoid the doubling of empty lines. + local upgradesAvailable = not table.empty(weapons.upgrades.normal) or not table.empty(weapons.upgrades.death) + if k == 'basic' then + if totalDirectFireDPS > 0 then + table.insert(blocks, {color = 'FFA600', lines = {LOCF('', totalDirectFireDPS)}}) + end + if totalIndirectFireDPS > 0 then + table.insert(blocks, {color = 'FFA600', lines = {LOCF('', totalIndirectFireDPS)}}) + end + if totalNavalDPS > 0 then + table.insert(blocks, {color = 'FFA600', lines = {LOCF('', totalNavalDPS)}}) + end + if totalAADPS > 0 then + table.insert(blocks, {color = 'FFA600', lines = {LOCF('', totalAADPS)}}) + end + if not upgradesAvailable then + table.insert(blocks, {color = UIUtil.fontColor, lines = {''}}) -- Empty line + end + end + -- Avoid the doubling of empty lines when the unit has upgrades. + if upgradesAvailable then + table.insert(blocks, {color = UIUtil.fontColor, lines = {''}}) -- Empty line end - table.insert(blocks, {color = 'FFFF0000', lines = lines}) end end end diff --git a/units/URL0101/URL0101_unit.bp b/units/URL0101/URL0101_unit.bp index 1fdc30d2ab..671c7692d3 100644 --- a/units/URL0101/URL0101_unit.bp +++ b/units/URL0101/URL0101_unit.bp @@ -146,7 +146,6 @@ UnitBlueprint{ TargetPriorities = { "ALLUNITS" }, TargetRestrictDisallow = "UNTARGETABLE", Turreted = false, - WeaponCategory = "Missile", }, }, Wreckage = { diff --git a/units/XSL0303/XSL0303_unit.bp b/units/XSL0303/XSL0303_unit.bp index 60c5cb2eb0..643ea273b1 100644 --- a/units/XSL0303/XSL0303_unit.bp +++ b/units/XSL0303/XSL0303_unit.bp @@ -394,7 +394,7 @@ UnitBlueprint{ TurretYawRange = 180, TurretYawSpeed = 125, Turreted = true, - WeaponCategory = "Indirect Fire", + WeaponCategory = "Direct Fire", }, { Audio = { From fcbe00cdfe1682fe38e00abdb2846cfdefc62a7f Mon Sep 17 00:00:00 2001 From: Basilisk3 <126026384+Basilisk3@users.noreply.github.com> Date: Sun, 20 Oct 2024 21:10:17 +0200 Subject: [PATCH 03/13] Improve the visual representation of multiple projectiles by increasing their `StrategicIconSize` (#6465) ## Description of the proposed changes **Usha-Ah: Seraphim T3 Sniper Bot (secondary firing mode):** The current `StrategicIconSize` of this projectile is set to `1`. This is too small, because the projectile is very fast and thereby easy to miss. A `StrategicIconSize` of `1` also does not represent the actual strength of the projectile well (2000 damage). I changed the `StrategicIconSize` to `4` in this PR. Alternative and possibly preferable options could be: - Only increase it to `3` since `4` is only used by TML missiles. - Decrease the `StrategicIconSize` of the first firing mode to `2` and and only increase the size of the second firing mode to `3`. **Othuum: T3 Siege Tank (heavy cannon); Yenzyne: T2 Hover Tank:** StrategicIconSize: 1 --> 2 **Percival: T3 Armored Assault Bot:** StrategicIconSize: 2 --> 3 ## Checklist - [x] Changes are documented in the changelog for the next game version --- changelog/snippets/features.6465.md | 4 ++++ projectiles/SDFSniperShot02/SDFSniperShot02_proj.bp | 2 +- projectiles/SDFTauCannon01/SDFTauCannon01_proj.bp | 2 +- .../TDFIonizedPlasmaGatlingCannon01_proj.bp | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 changelog/snippets/features.6465.md diff --git a/changelog/snippets/features.6465.md b/changelog/snippets/features.6465.md new file mode 100644 index 0000000000..d4e482c3fb --- /dev/null +++ b/changelog/snippets/features.6465.md @@ -0,0 +1,4 @@ +- (#6465) Improve the visual representation of multiple projectiles by increasing their `StrategicIconSize`. + - Usha-Ah: Seraphim T3 Sniper Bot: Increase the `StrategicIconSize` of its secondary firing mode's projectile from `1` to `4`. + - Othuum: T3 Siege Tank and Yenzyne: T2 Hover Tank: Increase the `StrategicIconSize` of their Thau Cannon's projectile from `1` to `2`. + - Percival: T3 Armored Assault Bot: Increase the `StrategicIconSize` of its cannon's projectile from `2` to `3`. diff --git a/projectiles/SDFSniperShot02/SDFSniperShot02_proj.bp b/projectiles/SDFSniperShot02/SDFSniperShot02_proj.bp index 3c7ba87a77..0198ded45f 100644 --- a/projectiles/SDFSniperShot02/SDFSniperShot02_proj.bp +++ b/projectiles/SDFSniperShot02/SDFSniperShot02_proj.bp @@ -12,7 +12,7 @@ ProjectileBlueprint{ Display = { CameraFollowsProjectile = true, ImpactEffects = { Type = "Medium01" }, - StrategicIconSize = 1, + StrategicIconSize = 4, }, General = { Category = "Direct Fire", diff --git a/projectiles/SDFTauCannon01/SDFTauCannon01_proj.bp b/projectiles/SDFTauCannon01/SDFTauCannon01_proj.bp index 37a563f11f..49420f8d8c 100644 --- a/projectiles/SDFTauCannon01/SDFTauCannon01_proj.bp +++ b/projectiles/SDFTauCannon01/SDFTauCannon01_proj.bp @@ -11,7 +11,7 @@ ProjectileBlueprint{ }, Display = { ImpactEffects = { Type = "Small01" }, - StrategicIconSize = 1, + StrategicIconSize = 2, }, General = { Category = "Direct Fire", diff --git a/projectiles/TDFIonizedPlasmaGatlingCannon01/TDFIonizedPlasmaGatlingCannon01_proj.bp b/projectiles/TDFIonizedPlasmaGatlingCannon01/TDFIonizedPlasmaGatlingCannon01_proj.bp index 8e4ce3bfde..68144e7e81 100644 --- a/projectiles/TDFIonizedPlasmaGatlingCannon01/TDFIonizedPlasmaGatlingCannon01_proj.bp +++ b/projectiles/TDFIonizedPlasmaGatlingCannon01/TDFIonizedPlasmaGatlingCannon01_proj.bp @@ -11,7 +11,7 @@ ProjectileBlueprint{ }, Display = { ImpactEffects = { Type = "Medium03" }, - StrategicIconSize = 2, + StrategicIconSize = 3, }, General = { Category = "Direct Fire", From afa3c44a5e27595254f610c0b077278dc120f9d2 Mon Sep 17 00:00:00 2001 From: BlackYps <52536103+BlackYps@users.noreply.github.com> Date: Mon, 21 Oct 2024 10:44:09 +0200 Subject: [PATCH 04/13] Shader cleanup (#6452) I had a look at the shaders again for integration in the map editor. During this I noticed some improvements that should be made to make the code more readable --- effects/terrain.fx | 96 +++++++++++++++++++++++----------------------- effects/water2.fx | 33 ++++++---------- 2 files changed, 59 insertions(+), 70 deletions(-) diff --git a/effects/terrain.fx b/effects/terrain.fx index 95ccd4a7fe..44f65662a3 100644 --- a/effects/terrain.fx +++ b/effects/terrain.fx @@ -34,10 +34,10 @@ float WaterElevation; float WaterElevationDeep; float WaterElevationAbyss; -// masks of stratum layers 1 - 4 +// masks of stratum layers 0 - 3 (lower stratum has no mask) texture UtilityTextureA; -// masks of stratum layers 5 - 8 +// masks of stratum layers 4 - 7 texture UtilityTextureB; // red: wave normal strength @@ -389,16 +389,16 @@ float ComputeShadow( float4 vShadowCoord ) } // apply the water color -float3 ApplyWaterColor(float terrainHeight, float depth, float3 inColor) +float3 ApplyWaterColor(float terrainHeight, float waterDepth, float3 color) { - if (depth > 0) { + if (waterDepth > 0) { // With this extra check we get rid of unwanted coloration on steep cliffs when zoomed in, // but we prevent that terrain tesselation swallows too much of the water when zoomed out float opacity = saturate(smoothstep(10, 200, CameraPosition.y - WaterElevation) + step(terrainHeight, WaterElevation)); - float4 wcolor = tex1D(WaterRampSampler, depth); - inColor = lerp(inColor.xyz, wcolor.xyz, wcolor.w * opacity); + float4 waterColor = tex1D(WaterRampSampler, waterDepth); + color = lerp(color.xyz, waterColor.rgb, waterColor.a * opacity); } - return inColor; + return color; } float3 ApplyWaterColorExponentially(float3 viewDirection, float terrainHeight, float waterDepth, float3 color) @@ -422,19 +422,19 @@ float3 ApplyWaterColorExponentially(float3 viewDirection, float terrainHeight, f } // calculate the lit pixels -float4 CalculateLighting( float3 inNormal, float3 inViewPosition, float3 inAlbedo, float specAmount, float waterDepth, float4 inShadow, uniform bool inShadows) +float4 CalculateLighting( float3 inNormal, float3 worldTerrain, float3 inAlbedo, float specAmount, float waterDepth, float4 shadowCoords, uniform bool inShadows) { float4 color = float4( 0, 0, 0, 0 ); - float shadow = ( inShadows && ( 1 == ShadowsEnabled ) ) ? ComputeShadow( inShadow ) : 1; + float shadow = ( inShadows && ( 1 == ShadowsEnabled ) ) ? ComputeShadow(shadowCoords) : 1; if (IsExperimentalShader()) { - float3 position = TerrainScale * inViewPosition; + float3 position = TerrainScale * worldTerrain; float mapShadow = tex2D(UpperAlbedoSampler, position.xy).w; shadow = shadow * mapShadow; } // calculate some specular - float3 viewDirection = normalize(inViewPosition.xzy-CameraPosition); + float3 viewDirection = normalize(worldTerrain.xzy-CameraPosition); float SunDotNormal = dot( SunDirection, inNormal); float3 R = SunDirection - 2.0f * SunDotNormal * inNormal; @@ -445,9 +445,9 @@ float4 CalculateLighting( float3 inNormal, float3 inViewPosition, float3 inAlbed color.rgb = light * inAlbedo; if (IsExperimentalShader()) { - color.rgb = ApplyWaterColorExponentially(-viewDirection, inViewPosition.z, waterDepth, color); + color.rgb = ApplyWaterColorExponentially(-viewDirection, worldTerrain.z, waterDepth, color); } else { - color.rgb = ApplyWaterColor(inViewPosition.z, waterDepth, color); + color.rgb = ApplyWaterColor(worldTerrain.z, waterDepth, color); } color.a = 0.01f + (specular*SpecularColor.w); @@ -782,11 +782,11 @@ float4 TerrainNormalsPS( VS_OUTPUT inV ) : COLOR // sample all the textures we'll need float4 mask = saturate(tex2D( UtilitySamplerA, inV.mTexWT * TerrainScale)); - float4 lowerNormal = normalize(tex2D( LowerNormalSampler, inV.mTexWT * TerrainScale * LowerNormalTile ) * 2 - 1); - float4 stratum0Normal = normalize(tex2D( Stratum0NormalSampler, inV.mTexWT * TerrainScale * Stratum0NormalTile ) * 2 - 1); - float4 stratum1Normal = normalize(tex2D( Stratum1NormalSampler, inV.mTexWT * TerrainScale * Stratum1NormalTile ) * 2 - 1); - float4 stratum2Normal = normalize(tex2D( Stratum2NormalSampler, inV.mTexWT * TerrainScale * Stratum2NormalTile ) * 2 - 1); - float4 stratum3Normal = normalize(tex2D( Stratum3NormalSampler, inV.mTexWT * TerrainScale * Stratum3NormalTile ) * 2 - 1); + float4 lowerNormal = normalize(tex2D( LowerNormalSampler, inV.mTexWT * TerrainScale * LowerNormalTile ) * 2 - 1); + float4 stratum0Normal = normalize(tex2D(Stratum0NormalSampler, inV.mTexWT * TerrainScale * Stratum0NormalTile) * 2 - 1); + float4 stratum1Normal = normalize(tex2D(Stratum1NormalSampler, inV.mTexWT * TerrainScale * Stratum1NormalTile) * 2 - 1); + float4 stratum2Normal = normalize(tex2D(Stratum2NormalSampler, inV.mTexWT * TerrainScale * Stratum2NormalTile) * 2 - 1); + float4 stratum3Normal = normalize(tex2D(Stratum3NormalSampler, inV.mTexWT * TerrainScale * Stratum3NormalTile) * 2 - 1); // blend all normals together float4 normal = lowerNormal; @@ -890,13 +890,13 @@ float4 TerrainBasisPSBiCubic( VS_OUTPUT inV ) : COLOR float4 TerrainPS( VS_OUTPUT inV, uniform bool inShadows ) : COLOR { // sample all the textures we'll need - float4 mask = saturate(tex2Dproj( UtilitySamplerA, inV.mTexWT * TerrainScale)* 2 - 1 ); - float4 upperAlbedo = tex2Dproj( UpperAlbedoSampler, inV.mTexWT * TerrainScale* UpperAlbedoTile ); - float4 lowerAlbedo = tex2Dproj( LowerAlbedoSampler, inV.mTexWT * TerrainScale* LowerAlbedoTile ); - float4 stratum0Albedo = tex2Dproj( Stratum0AlbedoSampler, inV.mTexWT * TerrainScale* Stratum0AlbedoTile ); - float4 stratum1Albedo = tex2Dproj( Stratum1AlbedoSampler, inV.mTexWT * TerrainScale* Stratum1AlbedoTile ); - float4 stratum2Albedo = tex2Dproj( Stratum2AlbedoSampler, inV.mTexWT * TerrainScale* Stratum2AlbedoTile ); - float4 stratum3Albedo = tex2Dproj( Stratum3AlbedoSampler, inV.mTexWT * TerrainScale* Stratum3AlbedoTile ); + float4 mask = saturate(tex2D( UtilitySamplerA, inV.mTexWT * TerrainScale) * 2 - 1); + float4 upperAlbedo = tex2D( UpperAlbedoSampler, inV.mTexWT * TerrainScale * UpperAlbedoTile); + float4 lowerAlbedo = tex2D( LowerAlbedoSampler, inV.mTexWT * TerrainScale * LowerAlbedoTile); + float4 stratum0Albedo = tex2D(Stratum0AlbedoSampler, inV.mTexWT * TerrainScale * Stratum0AlbedoTile); + float4 stratum1Albedo = tex2D(Stratum1AlbedoSampler, inV.mTexWT * TerrainScale * Stratum1AlbedoTile); + float4 stratum2Albedo = tex2D(Stratum2AlbedoSampler, inV.mTexWT * TerrainScale * Stratum2AlbedoTile); + float4 stratum3Albedo = tex2D(Stratum3AlbedoSampler, inV.mTexWT * TerrainScale * Stratum3AlbedoTile); float3 normal = SampleScreen(NormalSampler, inV.mTexSS).xyz*2-1; @@ -909,7 +909,7 @@ float4 TerrainPS( VS_OUTPUT inV, uniform bool inShadows ) : COLOR albedo.xyz = lerp( albedo.xyz, upperAlbedo.xyz, upperAlbedo.w ); // get the water depth - float waterDepth = tex2Dproj( UtilitySamplerC, inV.mTexWT * TerrainScale).g; + float waterDepth = tex2D( UtilitySamplerC, inV.mTexWT * TerrainScale).g; // calculate the lit pixel float4 outColor = CalculateLighting( normal, inV.mTexWT.xyz, albedo.xyz, 1-albedo.w, waterDepth, inV.mShadow, inShadows); @@ -921,19 +921,19 @@ float4 TerrainAlbedoXP( VS_OUTPUT pixel) : COLOR { float4 position = TerrainScale*pixel.mTexWT; - float4 mask0 = saturate(tex2Dproj(UtilitySamplerA,position)*2-1); - float4 mask1 = saturate(tex2Dproj(UtilitySamplerB,position)*2-1); + float4 mask0 = saturate(tex2D(UtilitySamplerA,position)*2-1); + float4 mask1 = saturate(tex2D(UtilitySamplerB,position)*2-1); - float4 lowerAlbedo = tex2Dproj(LowerAlbedoSampler,position*LowerAlbedoTile); - float4 stratum0Albedo = tex2Dproj(Stratum0AlbedoSampler,position*Stratum0AlbedoTile); - float4 stratum1Albedo = tex2Dproj(Stratum1AlbedoSampler,position*Stratum1AlbedoTile); - float4 stratum2Albedo = tex2Dproj(Stratum2AlbedoSampler,position*Stratum2AlbedoTile); - float4 stratum3Albedo = tex2Dproj(Stratum3AlbedoSampler,position*Stratum3AlbedoTile); - float4 stratum4Albedo = tex2Dproj(Stratum4AlbedoSampler,position*Stratum4AlbedoTile); - float4 stratum5Albedo = tex2Dproj(Stratum5AlbedoSampler,position*Stratum5AlbedoTile); - float4 stratum6Albedo = tex2Dproj(Stratum6AlbedoSampler,position*Stratum6AlbedoTile); - float4 stratum7Albedo = tex2Dproj(Stratum7AlbedoSampler,position*Stratum7AlbedoTile); - float4 upperAlbedo = tex2Dproj(UpperAlbedoSampler,position*UpperAlbedoTile); + float4 lowerAlbedo = tex2D(LowerAlbedoSampler,position*LowerAlbedoTile); + float4 stratum0Albedo = tex2D(Stratum0AlbedoSampler,position*Stratum0AlbedoTile); + float4 stratum1Albedo = tex2D(Stratum1AlbedoSampler,position*Stratum1AlbedoTile); + float4 stratum2Albedo = tex2D(Stratum2AlbedoSampler,position*Stratum2AlbedoTile); + float4 stratum3Albedo = tex2D(Stratum3AlbedoSampler,position*Stratum3AlbedoTile); + float4 stratum4Albedo = tex2D(Stratum4AlbedoSampler,position*Stratum4AlbedoTile); + float4 stratum5Albedo = tex2D(Stratum5AlbedoSampler,position*Stratum5AlbedoTile); + float4 stratum6Albedo = tex2D(Stratum6AlbedoSampler,position*Stratum6AlbedoTile); + float4 stratum7Albedo = tex2D(Stratum7AlbedoSampler,position*Stratum7AlbedoTile); + float4 upperAlbedo = tex2D(UpperAlbedoSampler,position*UpperAlbedoTile); float4 albedo = lowerAlbedo; albedo = lerp(albedo,stratum0Albedo,mask0.x); @@ -958,7 +958,7 @@ float4 TerrainAlbedoXP( VS_OUTPUT pixel) : COLOR light = LightingMultiplier*light + ShadowFillColor*(1-light); albedo.rgb = light * ( albedo.rgb + specular.rgb ); - float waterDepth = tex2Dproj(UtilitySamplerC,pixel.mTexWT*TerrainScale).g; + float waterDepth = tex2D(UtilitySamplerC,pixel.mTexWT*TerrainScale).g; albedo.rgb = ApplyWaterColor(pixel.mTexWT.z, waterDepth, albedo.rgb); return float4(albedo.rgb, 0.01f); @@ -2075,17 +2075,17 @@ float4 TerrainPBRAlbedoPS ( VS_OUTPUT inV) : COLOR // Layer| Albedo stratum | Normal stratum // | R | G | B | A | R | G | B | A | // ---- --- --- --- --- --- --- --- --- -// | L | R G B unused | X Y Z unused | +// | L | R G B specularity | X Y Z unused | // ---- --- --- --- --- --- --- --- --- -// | S0 | R G B unused | X Y Z unused | -// | S1 | R G B unused | X Y Z unused | -// | S2 | R G B unused | X Y Z unused | -// | S3 | R G B unused | X Y Z unused | +// | S0 | R G B specularity | X Y Z unused | +// | S1 | R G B specularity | X Y Z unused | +// | S2 | R G B specularity | X Y Z unused | +// | S3 | R G B specularity | X Y Z unused | // ---- --- --- --- --- --- --- --- --- -// | S4 | R G B unused | X Y Z unused | -// | S5 | R G B unused | X Y Z unused | -// | S6 | R G B unused | X Y Z unused | -// | S7 | R G B unused | X Y Z unused | +// | S4 | R G B specularity | X Y Z unused | +// | S5 | R G B specularity | X Y Z unused | +// | S6 | R G B specularity | X Y Z unused | +// | S7 | R G B specularity | X Y Z unused | // ---- --- --- --- --- --- --- --- --- // | U | normal.x normal.z unused shadow | diff --git a/effects/water2.fx b/effects/water2.fx index 251925991e..7be6b78bc8 100644 --- a/effects/water2.fx +++ b/effects/water2.fx @@ -131,9 +131,7 @@ sampler2D MaskSampler = sampler_state AddressV = CLAMP; }; -// // surface water color -// float3 waterColor = float3(0,0.7,1.5); float3 waterColorLowFi = float3(0.7647,0.8784,0.9647); @@ -141,10 +139,8 @@ float3 waterColorLowFi = float3(0.7647,0.8784,0.9647); float2 waterLerp = 0.3; float refractionScale = 0.015; -// // fresnel parameters // both are unused by the shaders -// float fresnelBias = 0.1; float fresnelPower = 1.5; @@ -152,14 +148,9 @@ float fresnelPower = 1.5; float unitreflectionAmount = 0.5; float skyreflectionAmount = 1.5; - -// // 4 repeat rates for the 4 wave texture layers -// float4 normalRepeatRate = float4(0.0009, 0.009, 0.05, 0.5); - -// // 4 vectors of normal movements float2 normal1Movement = float2(0.5, -0.95); float2 normal2Movement = float2(0.05, -0.095); @@ -176,9 +167,6 @@ float sunReflectionAmount = 5; // unused by the shaders float SunGlow; -/// -/// -/// struct LOWFIDELITY_VERTEX { @@ -432,19 +420,19 @@ float4 HighFidelityPS( VS_OUTPUT inV, // we need to exclude the unit refraction above the water line. This creats small areas with // no refraction, but the water color in the next step will make this mostly unnoticeable - refractedPixels.xyz = lerp(refractedPixels, backGroundPixels, saturate(refractedPixels.a * 255)).xyz; + refractedPixels.rgb = lerp(refractedPixels, backGroundPixels, saturate(refractedPixels.a * 255)).rgb; // we want to lerp in the water color based on depth, but clamped - float waterLerp = clamp(waterDepth, waterLerp.x, waterLerp.y); - refractedPixels.xyz = lerp(refractedPixels.xyz, waterColor, waterLerp); + float factor = clamp(waterDepth, waterLerp.x, waterLerp.y); + refractedPixels.rgb = lerp(refractedPixels.rgb, waterColor, factor); // We can't compute wich part of the unit we would hit with our reflection vector, // so we have to resort to an approximation using the refractionPos - float4 reflectedPixels = tex2D(ReflectionSampler, refractionPos); + float4 unitReflections = tex2D(ReflectionSampler, refractionPos); float4 skyReflection = texCUBE(SkySampler, R); // The alpha channel acts as a mask for unit parts above the water and probably // uses unitReflectionAmount as the positive value of the mask - reflectedPixels.xyz = lerp(skyReflection.xyz, reflectedPixels.xyz, saturate(reflectedPixels.a)); + float3 reflections = lerp(skyReflection.rgb, unitReflections.rgb, saturate(unitReflections.a)); // Schlick approximation for fresnel float NDotV = saturate(dot(viewVector, N)); @@ -453,21 +441,22 @@ float4 HighFidelityPS( VS_OUTPUT inV, // the default value of 1.5 is way to high, but we want to preserve manually set values in existing maps if (skyreflectionAmount == 1.5) skyreflectionAmount = 1.0; - refractedPixels = lerp(refractedPixels, reflectedPixels, saturate(fresnel * skyreflectionAmount)); + float3 water = lerp(refractedPixels, reflections, saturate(fresnel * skyreflectionAmount)); // add in the sun reflection float3 sunReflection = calculateSunReflection(R, viewVector, N); // the sun shouldn't be visible where a unit reflection is - sunReflection *= (1 - saturate(reflectedPixels.a * 4)); + sunReflection *= (1 - saturate(unitReflections.a * 4)); // we can control this value to have terrain cast a shadow on the water surface sunReflection *= waterTexture.r; - refractedPixels.xyz += sunReflection; + water += sunReflection; // Lerp in the wave crests - refractedPixels.xyz = lerp(refractedPixels.xyz, waveCrestColor, (1 - waterTexture.a) * (1 - waterTexture.b) * waveCrest); + water = lerp(water, waveCrestColor, (1 - waterTexture.a) * (1 - waterTexture.b) * waveCrest); // return the pixels masked out by the water mask - float4 returnPixels = refractedPixels; + float4 returnPixels; + returnPixels.rgb = water; returnPixels.a = 1 - mask; return returnPixels; } From d1e6a39af71c5499acd88404b880d88871a2cfe4 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Mon, 21 Oct 2024 18:35:22 -0700 Subject: [PATCH 05/13] Implement a Dual Yaw turret controller so that Loyalist can fully aim its secondary (#6428) The secondary used to be unable to aim the body. Also fix the secondary getting stuck on T1 units when the primary is aiming at a higher tech unit in a different direction. --- changelog/snippets/features.6428.md | 4 ++ engine/Core/Blueprints/WeaponBlueprint.lua | 8 ++++ lua/sim/weapon.lua | 47 ++++++++++++++++++---- units/URL0303/URL0303_unit.bp | 5 +++ 4 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 changelog/snippets/features.6428.md diff --git a/changelog/snippets/features.6428.md b/changelog/snippets/features.6428.md new file mode 100644 index 0000000000..b55eeb2893 --- /dev/null +++ b/changelog/snippets/features.6428.md @@ -0,0 +1,4 @@ +- (#6428) Implement a Dual Yaw turret controller so that Loyalist can fully aim its secondary. + + Previously, the Loyalist was unable to aim its secondary weapon in all directions because the secondary weapon could not rotate the torso independently. + With this change, a weapon can have a secondary yaw bone to aim with by defining `TurretBoneDualYaw` in its blueprint. The secondary yaw's angle, range, and speed can be set with `TurretDualYaw`, `TurretDualYawRange`, and `TurretDualYawSpeed`, but they also default to the primary yaw's angle, range, and speed if not specified. diff --git a/engine/Core/Blueprints/WeaponBlueprint.lua b/engine/Core/Blueprints/WeaponBlueprint.lua index fee33642f0..e39a10afe3 100644 --- a/engine/Core/Blueprints/WeaponBlueprint.lua +++ b/engine/Core/Blueprints/WeaponBlueprint.lua @@ -319,6 +319,8 @@ ---@field TurretBoneDualMuzzle? Bone --- the second pitch bone for a turret, used for arms on bots as weapons ---@field TurretBoneDualPitch? Bone +--- The second yaw bone for a turret, used for the torso of the Loyalist's secondary weapon that is on a turret connected to the torso. +---@field TurretBoneDualYaw? Bone --- The bone used as the muzzle bone for turrets. This is used for aiming as where the projectile --- would come out ---@field TurretBoneMuzzle? Bone @@ -342,6 +344,12 @@ ---@field TurretYawRange number --- the speed at which the turret can turn in its yaw direction ---@field TurretYawSpeed number +--- the center angle for determining secondary yaw, based off the rest pose of the model +---@field TurretDualYaw number +--- the angle +/- off the secondary yaw that is a valid angle to turn to +---@field TurretDualYawRange number +--- the speed at which the secondary turret can turn in its yaw direction +---@field TurretDualYawSpeed number --- if this weapon uses the recent firing solution to create projectile instead of the --- aim bone transform ---@field UseFiringSolutionInsteadOfAimBone? boolean diff --git a/lua/sim/weapon.lua b/lua/sim/weapon.lua index f2b546b952..bf12a20414 100644 --- a/lua/sim/weapon.lua +++ b/lua/sim/weapon.lua @@ -137,7 +137,7 @@ Weapon = ClassWeapon(WeaponMethods, DebugWeaponComponent) { local pitchBone = bp.TurretBonePitch local muzzleBone = bp.TurretBoneMuzzle local precedence = bp.AimControlPrecedence or 10 - local pitchBone2, muzzleBone2 + local pitchBone2, muzzleBone2, yawBone2 local boneDualPitch = bp.TurretBoneDualPitch if boneDualPitch and boneDualPitch ~= '' then @@ -147,6 +147,11 @@ Weapon = ClassWeapon(WeaponMethods, DebugWeaponComponent) { if boneDualMuzzle and boneDualMuzzle ~= '' then muzzleBone2 = boneDualMuzzle end + local boneDualYaw = bp.TurretBoneDualYaw + if boneDualYaw and boneDualYaw ~= '' then + yawBone2 = boneDualYaw + end + local unit = self.unit if not (unit:ValidateBone(yawBone) and unit:ValidateBone(pitchBone) and unit:ValidateBone(muzzleBone)) then error('*ERROR: Bone aborting turret setup due to bone issues.', 2) @@ -156,10 +161,15 @@ Weapon = ClassWeapon(WeaponMethods, DebugWeaponComponent) { error('*ERROR: Bone aborting turret setup due to pitch/muzzle bone2 issues.', 2) return end + elseif yawBone2 then + if not unit:ValidateBone(yawBone2) then + error('*ERROR: Bone aborting turret setup due to yaw bone2 issues.', 2) + return + end end - local aimControl, aimRight, aimLeft + local aimControl, aimRight, aimLeft, aimYaw2 if yawBone and pitchBone and muzzleBone then - local trashManipulators = self.Trash + local selfTrash = self.Trash if bp.TurretDualManipulators then aimControl = CreateAimController(self, 'Torso', yawBone) aimRight = CreateAimController(self, 'Right', pitchBone, pitchBone, muzzleBone) @@ -173,15 +183,15 @@ Weapon = ClassWeapon(WeaponMethods, DebugWeaponComponent) { aimControl:SetResetPoseTime(9999999) end self:SetFireControl('Right') - trashManipulators:Add(aimControl) - trashManipulators:Add(aimRight) - trashManipulators:Add(aimLeft) + selfTrash:Add(aimControl) + selfTrash:Add(aimRight) + selfTrash:Add(aimLeft) else aimControl = CreateAimController(self, 'Default', yawBone, pitchBone, muzzleBone) if EntityCategoryContains(categories.STRUCTURE, unit) then aimControl:SetResetPoseTime(9999999) end - trashManipulators:Add(aimControl) + selfTrash:Add(aimControl) aimControl:SetPrecedence(precedence) if bp.RackSlavedToTurret and not table.empty(bp.RackBones) then for _, v in bp.RackBones do @@ -189,11 +199,20 @@ Weapon = ClassWeapon(WeaponMethods, DebugWeaponComponent) { if rackBone ~= pitchBone then local slaver = CreateSlaver(unit, rackBone, pitchBone) slaver:SetPrecedence(precedence - 1) - trashManipulators:Add(slaver) + selfTrash:Add(slaver) end end end end + + if yawBone2 then + aimYaw2 = CreateAimController(self, 'Yaw2', yawBone2, yawBone2) + aimYaw2:SetPrecedence(precedence-1) + if EntityCategoryContains(categories.STRUCTURE, unit) then + aimYaw2:SetResetPoseTime(9999999) + end + selfTrash:Add(aimYaw2) + end else error('*ERROR: Trying to setup a turreted weapon but there are yaw bones, pitch bones or muzzle bones missing from the blueprint.', 2) end @@ -232,6 +251,18 @@ Weapon = ClassWeapon(WeaponMethods, DebugWeaponComponent) { aimRight:SetFiringArc(turretyawmin, turretyawmax, turretyawspeed, turretpitchmin, turretpitchmax, turretpitchspeed) aimLeft:SetFiringArc(turretyawmin, turretyawmax, turretyawspeed, turretpitchmin, turretpitchmax, turretpitchspeed) end + + if aimYaw2 then + local turretYawMin2 = turretyawmin + local turretYawMax2 = turretyawmax + if bp.TurretDualYaw and bp.TurretDualYawRange then + turretYawMin2 = bp.TurretDualYaw - bp.TurretDualYawRange + turretYawMax2 = bp.TurretDualYaw + bp.TurretDualYawRange + end + + local turretYawSpeed2 = bp.TurretDualYawSpeed or turretyawspeed + aimYaw2:SetFiringArc(turretYawMin2, turretYawMax2, turretYawSpeed2, turretpitchmin, turretpitchmax, turretpitchspeed) + end else local strg = '*ERROR: TRYING TO SETUP A TURRET WITHOUT ALL TURRET NUMBERS IN BLUEPRINT, ABORTING TURRET SETUP. WEAPON: ' .. bp.Label .. ' UNIT: '.. unit.UnitId error(strg, 2) diff --git a/units/URL0303/URL0303_unit.bp b/units/URL0303/URL0303_unit.bp index 70391e7d74..f2a8826eb6 100644 --- a/units/URL0303/URL0303_unit.bp +++ b/units/URL0303/URL0303_unit.bp @@ -277,6 +277,7 @@ UnitBlueprint{ MuzzleSalvoSize = 1, MuzzleVelocity = 35, NotExclusive = true, + PrefersPrimaryWeaponTarget = true, ProjectileId = "/projectiles/CDFBolter02/CDFBolter02_proj.bp", ProjectileLifetimeUsesMultiplier = 1.15, ProjectilesPerOnFire = 1, @@ -306,6 +307,7 @@ UnitBlueprint{ TurretBoneMuzzle = "Muzzle_Right", TurretBonePitch = "Barrel_R", TurretBoneYaw = "Barrel_R", + TurretBoneDualYaw = "Torso", TurretDualManipulators = false, TurretPitch = 0, TurretPitchRange = 45, @@ -313,6 +315,9 @@ UnitBlueprint{ TurretYaw = 0, TurretYawRange = 35, TurretYawSpeed = 120, + TurretDualYaw = 0, + TurretDualYawRange = 180, + TurretDualYawSpeed = 120, Turreted = true, UseFiringSolutionInsteadOfAimBone = true, WeaponCategory = "Direct Fire", From c490f7c9e5427f9d8d2cae7ded9a5bcf68e7a76d Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Wed, 23 Oct 2024 00:57:04 -0700 Subject: [PATCH 06/13] Update vector processing (#6438) Add metamethods and utility functions for Vectors and Quaternions to simplify and clean up the code involving operations with them. Co-authored-by: G C <37224614+Hdt80bro@users.noreply.github.com> --- changelog/snippets/fix.6438.md | 1 + changelog/snippets/other.6438.md | 14 + engine/Core.lua | 73 ++-- engine/Sim.lua | 3 +- lua/benchmarking/benchmarks/VDist2.lua | 86 +++++ lua/benchmarking/benchmarks/VDist3.lua | 150 ++++++++ lua/benchmarking/controller.lua | 78 ++-- lua/defaultexplosions.lua | 34 +- lua/defaultunits.lua | 1 - lua/shared/quaternions.lua | 367 ------------------ lua/sim/Projectile.lua | 22 +- lua/sim/ScenarioUtilities.lua | 26 +- lua/sim/TerrainUtils.lua | 100 ++--- lua/sim/Unit.lua | 27 +- lua/sim/units/StructureUnit.lua | 16 +- lua/system/utils.lua | 192 ++++++++++ lua/utilities.lua | 490 ++++++++++++++++++------- tests/utility/string.spec.lua | 44 ++- units/UAL0401/UAL0401_Script.lua | 31 +- units/URL0402/URL0402_script.lua | 7 +- units/XRL0403/XRL0403_script.lua | 7 +- 21 files changed, 1047 insertions(+), 722 deletions(-) create mode 100644 changelog/snippets/fix.6438.md create mode 100644 changelog/snippets/other.6438.md create mode 100644 lua/benchmarking/benchmarks/VDist2.lua create mode 100644 lua/benchmarking/benchmarks/VDist3.lua delete mode 100644 lua/shared/quaternions.lua diff --git a/changelog/snippets/fix.6438.md b/changelog/snippets/fix.6438.md new file mode 100644 index 0000000000..fcf04ee0ee --- /dev/null +++ b/changelog/snippets/fix.6438.md @@ -0,0 +1 @@ +- (#6438) Fix `TerrainUtils.GetTerrainSlopeAngles` returning roll on the -Z axis instead of the +Z axis. Previously, this required the roll to be negated when used to orient objects such as units with the terrain. Now roll no longer needs to be negated. diff --git a/changelog/snippets/other.6438.md b/changelog/snippets/other.6438.md new file mode 100644 index 0000000000..711be31cd4 --- /dev/null +++ b/changelog/snippets/other.6438.md @@ -0,0 +1,14 @@ +- (#5061, #6438) Add metamethods and utility functions for Vectors and Quaternions to simplify and clean up the code involving operations with them. + - This **removes** the file `/lua/shared/quaternions.lua`, which was added in #4768 (Mar 4, 2023), so mods that use that file will have to be updated. + - The metamethods (defined globally in `/lua/system/utils.lua`) include: + - Vector/Vector2 addition/subtraction/negation + - Vector/Vector2 * Scalar multiplication + - Quaternion/Vector * Vector/Quaternion multiplication + - Vector * Vector multiplication (cross product) + - Since these are metamethods, they work on all instances of Vector/Vector2/Quaternion, without having to import anything. + - The utility functions (have to be imported from `/lua/utilities.lua`) include: + - Faster Lua versions of VDist2, VDist2Sq, VDot + - `QuatFromRotation`: Creates a quaternion from an orientation axis and rotation angle. + - `QuatFromXZDirection`: Returns the orientation quaternion given an XZ direction + - `TranslateInXZDirection`: Translates the XZ coordinates of a position by a length in a given quaternion orientation. + - `RotateVectorByQuat`: Rotates a vector representing a 3D point by a quaternion rotation. diff --git a/engine/Core.lua b/engine/Core.lua index 991cff96ac..19d1c390a2 100644 --- a/engine/Core.lua +++ b/engine/Core.lua @@ -4,23 +4,33 @@ ---@class FileName: string, stringlib ---@operator concat(FileName | string): FileName ----@class Quaternion ----@field [1] number # y ----@field [2] number # z ----@field [3] number # x ----@field [4] number # w - ----@class Vector +---@class VectorBase ---@field [1] number # x ----@field [2] number # y (up) +---@field [2] number # y +---@field x number +---@field y number + +---@class Quaternion : VectorBase +---@operator mul(Quaternion): Quaternion +---@operator mul(Vector): Quaternion +---@operator mul(number): Quaternion +---@field [3] number # z +---@field [4] number # w +---@field z number + +---@class Vector : VectorBase +---@operator add(Vector): Vector +---@operator mul(Quaternion): Quaternion +---@operator mul(Vector): Vector +---@operator mul(number): Vector +---@operator unm: Vector ---@field [3] number # z ----@field x number # east/west ----@field y number # up/down ----@field z number # north/south +---@field z number ----@class Vector2 ----@field [1] number # x ----@field [2] number # y +---@class Vector2 : VectorBase +---@operator add(Vector2): Vector2 +---@operator mul(number): Vector2 +---@operator unm: Vector2 --- A point-to-point based rectangle, where the first point is usually in the top left corner ---@class Rectangle @@ -142,6 +152,12 @@ end ---@return Quaternion function EulerToQuaternion(roll, pitch, yaw) end +-- { +-- sin(roll/2) * cos(pitch/2) * cos(yaw/2) - cos(roll/2) * sin(pitch/2) * sin(yaw/2), -- x +-- cos(roll/2) * sin(pitch/2) * cos(yaw/2) + sin(roll/2) * cos(pitch/2) * sin(yaw/2), -- y +-- cos(roll/2) * cos(pitch/2) * sin(yaw/2) - sin(roll/2) * sin(pitch/2) * cos(yaw/2), -- z +-- cos(roll/2) * cos(pitch/2) * cos(yaw/2) + sin(roll/2) * sin(pitch/2) * sin(yaw/2) -- w +-- } --- collapses all relative `/./` or `/../` directory names from a path ---@param fullPath FileName @@ -434,6 +450,7 @@ function Trace(enable) end --- Adds vector `b` to vector `a` +---@deprecated It is faster to compute it in Lua with `Vector` ---@param a Vector ---@param b Vector ---@return Vector @@ -441,13 +458,15 @@ function VAdd(a, b) end --- Subtracts vector `b` from vector `a` +---@deprecated It is faster to compute it in Lua with `Vector` ---@param a Vector ---@param b Vector ---@return Vector function VDiff(a, b) end ---- Computes the distance between two points +--- Computes the distance between two points. +---@deprecated It is faster to compute it in Lua. ---@param x1 number ---@param y1 number ---@param x2 number @@ -456,7 +475,8 @@ end function VDist2(x1, y1, x2, y2) end ---- Computes the squared distance between two points +--- Computes the squared distance between two points. +---@deprecated It is faster to compute it in Lua. ---@param x1 number ---@param y1 number ---@param x2 number @@ -465,14 +485,14 @@ end function VDist2Sq(x1, y1, x2, y2) end ---- Computes the distance between the vectors `a` and `b` +--- Computes the distance between vectors `a` and `b` ---@param a Vector ---@param b Vector ---@return number function VDist3(a, b) end ---- Computes the squared distance between the vectors `a` and `b` +--- Computes the squared distance between vectors `a` and `b` ---@deprecated It is faster to compute it in Lua ---@param a Vector ---@param b Vector @@ -480,28 +500,33 @@ end function VDist3Sq(a, b) end ---- Computes the dot product between the vectors `a` and `b` +--- Computes the dot product between vectors `a` and `b`, +--- or `a[1]*b[1] + a[2]*b[2] + a[3]*b[3]` +---@deprecated It is faster to compute it in Lua ---@param a Vector ---@param b Vector ---@return number function VDot(a, b) end ---- Scales the vector `v` with the scalar `s` +--- Scales the vector `v` by the scalar `s` +---@deprecated It is faster to compute it in Lua with `Vector` ---@param v Vector ---@param s number ---@return Vector function VMult(v, s) end ---- Computes the vector perpendicular to the plane described by the vectors `a` and `b` +--- Computes the component perpendicular to the XZ plane in the cross product of `a` across `b`, +--- or `a[3] * b[1] - a[1] * b[3]` +---@deprecated It is faster to compute it in Lua ---@param a Vector ---@param b Vector ----@return Vector +---@return number function VPerpDot(a, b) end ---- Populates a new table with the corresponding meta table +--- Creates a new 3-vector with the corresponding metatable ---@param x number ---@param y number ---@param z number @@ -509,7 +534,7 @@ end function Vector(x, y, z) end ---- Populates a new table with the corresponding meta table +--- Creates a new 2-vector with the corresponding metatable ---@param x number ---@param y number ---@return Vector2 diff --git a/engine/Sim.lua b/engine/Sim.lua index 7ea935bd12..d62b533a9f 100644 --- a/engine/Sim.lua +++ b/engine/Sim.lua @@ -444,7 +444,7 @@ end ---@see CreateUnitHPR() # heading-pitch-roll version ---@param blueprintId string ---@param army Army ----@param layer? number +---@param layer Layer ---@param x number ---@param z number ---@param heading number @@ -670,6 +670,7 @@ end function GetUnitBlueprintByName(bpName) end +---@overload fun(x1: number, z1: number, x2: number, z2: number): Unit[] | nil --- retrieves all units in a rectangle ---@param rectangle Rectangle ---@return Unit[] | nil diff --git a/lua/benchmarking/benchmarks/VDist2.lua b/lua/benchmarking/benchmarks/VDist2.lua new file mode 100644 index 0000000000..1b7d6e6b39 --- /dev/null +++ b/lua/benchmarking/benchmarks/VDist2.lua @@ -0,0 +1,86 @@ +-- UpvaluedEngineVDist2OnLocals: 0.043102264404297 +-- UpvaluedEngineVDist2SqOnLocals: 0.040153503417969 +-- UpvaluedLuaVDist2OnLocals: 0.029483795166016 +-- UpvaluedLuaVDist2SqOnLocals: 0.014518737792969 + +-- Conclusion: Lua is much faster, especially when skipping the square root (useful in distance comparisons). + +local outerLoop = 1000000 + +function UpvaluedEngineVDist2OnLocals() + local p1 = Random()*1000 + local p2 = Random()*1000 + local p3 = Random()*1000 + local p4 = Random()*1000 + + -- upvalue + local VDist2 = VDist2 + + local start = GetSystemTimeSecondsOnlyForProfileUse() + + for i=1, outerLoop do + local dist = VDist2(p1, p2, p3, p4) + end + + local final = GetSystemTimeSecondsOnlyForProfileUse() + + return final - start +end +function UpvaluedEngineVDist2SqOnLocals() + local p1 = Random()*1000 + local p2 = Random()*1000 + local p3 = Random()*1000 + local p4 = Random()*1000 + + -- upvalue + local VDist2Sq = VDist2Sq + + local start = GetSystemTimeSecondsOnlyForProfileUse() + + for i=1, outerLoop do + local dist = VDist2Sq(p1, p2, p3, p4) + end + + local final = GetSystemTimeSecondsOnlyForProfileUse() + + return final - start +end +function UpvaluedLuaVDist2OnLocals() + local p1 = Random()*1000 + local p2 = Random()*1000 + local p3 = Random()*1000 + local p4 = Random()*1000 + + -- upvalue + local MathSqrt = math.sqrt + + local start = GetSystemTimeSecondsOnlyForProfileUse() + + for i=1, outerLoop do + local d1 = p3 - p1 + local d2 = p4 - p2 + local dist = MathSqrt(d1 * d1 + d2 * d2) + end + + local final = GetSystemTimeSecondsOnlyForProfileUse() + + return final - start +end +function UpvaluedLuaVDist2SqOnLocals() + local p1 = Random()*1000 + local p2 = Random()*1000 + local p3 = Random()*1000 + local p4 = Random()*1000 + + local start = GetSystemTimeSecondsOnlyForProfileUse() + + for i=1, outerLoop do + local d1 = p3 - p1 + local d2 = p4 - p2 + local dist = d1 * d1 + d2 * d2 + end + + local final = GetSystemTimeSecondsOnlyForProfileUse() + + return final - start +end diff --git a/lua/benchmarking/benchmarks/VDist3.lua b/lua/benchmarking/benchmarks/VDist3.lua new file mode 100644 index 0000000000..edc0f11e16 --- /dev/null +++ b/lua/benchmarking/benchmarks/VDist3.lua @@ -0,0 +1,150 @@ +-- GlobalEngineVDist3OnGlobals: 0.064697265625 +-- GlobalEngineVDist3OnLocals: 0.06640625 +-- GlobalLuaVDist3OnGlobals: 0.072998046875 +-- GlobalLuaVDist3OnLocals: 0.067138671875 +-- UpvaluedEngineVDist3OnGlobals: 0.059814453125 +-- UpvaluedEngineVDist3OnLocals: 0.059814453125 +-- UpvaluedLuaVDist3OnGlobals: 0.068359375 +-- UpvaluedLuaVDist3OnLocals: 0.0615234375 # Sometimes this can be faster than `UpvaluedEngineVDist3OnLocals` in this benchmark but not often enough + +-- Conclusion: Using VDist3 instead of calculating in Lua is faster due to an engine patch: https://github.com/FAForever/FA-Binary-Patches/pull/54 +-- Make sure to upvalue VDist3 for a significant performance boost + +local outerLoop = 1000000 + +local pos1 = Vector(Random()*1000, Random()*1000, Random()*1000) +local pos2 = Vector(Random()*1000, Random()*1000, Random()*1000) + +function GlobalLuaVDist3OnGlobals() + local start = GetSystemTimeSecondsOnlyForProfileUse() + + for i=1, outerLoop do + local dx, dy, dz = pos1[1] - pos2[1] + , pos1[2] - pos2[2] + , pos1[3] - pos2[3] + local dist = math.sqrt(dx*dx + dy*dy + dz*dz) + end + + local final = GetSystemTimeSecondsOnlyForProfileUse() + + return final - start +end + +function GlobalEngineVDist3OnGlobals() + local start = GetSystemTimeSecondsOnlyForProfileUse() + + for i=1, outerLoop do + local dist = VDist3(pos1, pos2) + end + + local final = GetSystemTimeSecondsOnlyForProfileUse() + + return final - start +end + +function UpvaluedLuaVDist3OnGlobals() + -- upvalue + local MathSqrt = math.sqrt + + local start = GetSystemTimeSecondsOnlyForProfileUse() + + for i=1, outerLoop do + local dx, dy, dz = pos1[1] - pos2[1] + , pos1[2] - pos2[2] + , pos1[3] - pos2[3] + local dist = MathSqrt(dx*dx + dy*dy + dz*dz) + end + + local final = GetSystemTimeSecondsOnlyForProfileUse() + + return final - start +end + +function UpvaluedEngineVDist3OnGlobals() + -- upvalue + local VDist3 = VDist3 + + local start = GetSystemTimeSecondsOnlyForProfileUse() + + for i=1, outerLoop do + local dist = VDist3(pos1, pos2) + end + + local final = GetSystemTimeSecondsOnlyForProfileUse() + + return final - start +end + +-- Operating on local vectors + +function GlobalLuaVDist3OnLocals() + local pos1 = Vector(Random()*1000, Random()*1000, Random()*1000) + local pos2 = Vector(Random()*1000, Random()*1000, Random()*1000) + + local start = GetSystemTimeSecondsOnlyForProfileUse() + + for i=1, outerLoop do + local dx, dy, dz = pos1[1] - pos2[1] + , pos1[2] - pos2[2] + , pos1[3] - pos2[3] + local dist = math.sqrt(dx*dx + dy*dy + dz*dz) + end + + local final = GetSystemTimeSecondsOnlyForProfileUse() + + return final - start +end + +function GlobalEngineVDist3OnLocals() + local pos1 = Vector(Random()*1000, Random()*1000, Random()*1000) + local pos2 = Vector(Random()*1000, Random()*1000, Random()*1000) + + local start = GetSystemTimeSecondsOnlyForProfileUse() + + for i=1, outerLoop do + local dist = VDist3(pos1, pos2) + end + + local final = GetSystemTimeSecondsOnlyForProfileUse() + + return final - start +end + +function UpvaluedLuaVDist3OnLocals() + local pos1 = Vector(Random()*1000, Random()*1000, Random()*1000) + local pos2 = Vector(Random()*1000, Random()*1000, Random()*1000) + + -- upvalue + local MathSqrt = math.sqrt + + local start = GetSystemTimeSecondsOnlyForProfileUse() + + for i=1, outerLoop do + local dx, dy, dz = pos1[1] - pos2[1] + , pos1[2] - pos2[2] + , pos1[3] - pos2[3] + local dist = MathSqrt(dx*dx + dy*dy + dz*dz) + end + + local final = GetSystemTimeSecondsOnlyForProfileUse() + + return final - start +end + +function UpvaluedEngineVDist3OnLocals() + local pos1 = Vector(Random()*1000, Random()*1000, Random()*1000) + local pos2 = Vector(Random()*1000, Random()*1000, Random()*1000) + + -- upvalue + local VDist3 = VDist3 + + local start = GetSystemTimeSecondsOnlyForProfileUse() + + for i=1, outerLoop do + local dist = VDist3(pos1, pos2) + end + + local final = GetSystemTimeSecondsOnlyForProfileUse() + + return final - start +end diff --git a/lua/benchmarking/controller.lua b/lua/benchmarking/controller.lua index b49d586de9..a3aeb3ea7e 100644 --- a/lua/benchmarking/controller.lua +++ b/lua/benchmarking/controller.lua @@ -1,11 +1,13 @@ -local directory = "/lua/profiler/benchmarks" +local directory = "/lua/benchmarking/benchmarks" local FunctionsToExclude = { "import" + , "lazyimport" , "ComputePoint" } +---@param log boolean # Log to the game log if the profiler isn't enabled function RunAllBenchmarks(log) -- capture in local scope @@ -51,7 +53,7 @@ function RunAllBenchmarks(log) , Function = e } - LOG("Done running " .. func .. " of file " .. file) + LOG("Done running " .. e .. " of file " .. file) end end end @@ -65,47 +67,59 @@ function RunAllBenchmarks(log) -- if we do not have the profiler enabled if log then - LOG(repr(results)) + LOG(repr(results, {depth = 4})) end end -function RunIndividualBenchmark(file, func) +---@param pattern FileName # pattern inside "/lua/benchmarking/benchmarks" to look for and run 1 file +---@param log boolean # Log to the game log if the profiler isn't enabled +function RunIndividualBenchmark(pattern, log) - local results = { } - results[file] = { } - - LOG("Running " .. func .. " of file " .. file) - - -- import the benchmark - local benchmark = import(file)[func] + local file = DiskFindFiles(directory, pattern) - -- retrieve code and information - local code = debug.listcode(benchmark) - local numparams = code.numparams - local maxstack = code.maxstack - - -- keep byte code clean - code.numparams = nil - code.maxstack = nil - - -- add information for UI / interaction - results[file][func] = { - Time = benchmark() - , Code = code - , NumParams = numparams - , MaxStack = maxstack - , File = file - , Function = func - } + if not file[1] then + error("Could not find any benchmark using the pattern " .. tostring(pattern)) + end - LOG("Done running " .. func .. " of file " .. file) + local results = { } + results[file] = { } + -- load in the benchmark and run them + local benchmark = import(file[1]) + for e, element in benchmark do + if not table.find(FunctionsToExclude, e) then + if type(element) == "function" then + LOG("Running " .. e .. " of file " .. file[1]) + + -- retrieve code and information + local code = debug.listcode(element) + local numparams = code.numparams + local maxstack = code.maxstack + + -- keep byte code clean + code.numparams = nil + code.maxstack = nil + + -- add information for UI / interaction + results[file][e] = { + Time = element() + , Code = code + , NumParams = numparams + , MaxStack = maxstack + , File = file[1] + , Function = e + } + + LOG("Done running " .. e .. " of file " .. file[1]) + end + end + end -- send it to the ui Sync.Profiler = Sync.Profiler or { } Sync.Profiler.Benchmarks = results -- if we do not have the profiler enabled - if log then - LOG(repr(results)) + if log then + LOG(repr(results, {depth = 4})) end end \ No newline at end of file diff --git a/lua/defaultexplosions.lua b/lua/defaultexplosions.lua index aaa11e081a..0e3290f1fc 100644 --- a/lua/defaultexplosions.lua +++ b/lua/defaultexplosions.lua @@ -161,7 +161,9 @@ function GetAverageBoundingXYZRadiusCorrect(unit) return ((bp.SizeX or 1) + (bp.SizeZ or 1) + (bp.SizeY or 1)) * 0.166 end ---- ??, what is the mathematics here exactly? +---@overload fun(rotation: number): number, number, number, number +--- Creates a quaternion from an orientation axis and rotation angle. The orientation axis defaults +--- to up (the y-axis). ---@param rotation number ---@param x number ---@param y number @@ -171,14 +173,7 @@ end ---@return number qz ---@return number qw function QuatFromRotation(rotation, x, y, z) - local angleRot, qw, qx, qy, qz, angle - angle = 0.00872664625 * rotation - angleRot = MathSin(angle) - qw = MathCos(angle) - qx = x * angleRot - qy = y * angleRot - qz = z * angleRot - return qx, qy, qz, qw + return unpack(util.QuatFromRotation(rotation, x, y, z)) end -------------------------------------- @@ -866,23 +861,22 @@ end ---@param posZ number ---@param scale number ---@param scaleVelocity number ----@param Lifetime number +---@param lifetime number ---@param velX number ---@param velY number ----@param VelZ number ----@param orientRot number +---@param velZ number +---@param orientRot number # degrees ---@param orientX number ---@param orientY number ---@param orientZ number ---@return Projectile -function CreateExplosionMesh(object, projBP, posX, posY, posZ, scale, scaleVelocity, Lifetime, velX, velY, VelZ, orientRot, orientX, orientY, orientZ) - - local proj = object:CreateProjectile(projBP, posX, posY, posZ, nil, nil, nil) - proj:SetScale(scale,scale,scale):SetScaleVelocity(scaleVelocity):SetLifetime(Lifetime):SetVelocity(velX, velY, VelZ) - - local orient = {0, 0, 0, 0} - orient[1], orient[2], orient[3], orient[4] = QuatFromRotation(orientRot, orientX, orientY, orientZ) - proj:SetOrientation(orient, true) +function CreateExplosionMesh(object, projBP, posX, posY, posZ, scale, scaleVelocity, lifetime, velX, velY, velZ, orientRot, orientX, orientY, orientZ) + local proj = object:CreateProjectile(projBP, posX, posY, posZ) + proj:SetScale(scale,scale,scale) + :SetScaleVelocity(scaleVelocity) + :SetLifetime(lifetime) + :SetVelocity(velX, velY, velZ) + :SetOrientation(util.QuatFromRotation(orientRot, orientX, orientY, orientZ), true) CreateEmitterAtEntity(proj, proj.Army, '/effects/emitters/destruction_explosion_smoke_10_emit.bp') return proj diff --git a/lua/defaultunits.lua b/lua/defaultunits.lua index 2c7cd78187..121a0a339f 100644 --- a/lua/defaultunits.lua +++ b/lua/defaultunits.lua @@ -66,7 +66,6 @@ local Buff = import("/lua/sim/buff.lua") local AdjacencyBuffs = import("/lua/sim/adjacencybuffs.lua") local FireState = import("/lua/game.lua").FireState local ScenarioFramework = import("/lua/scenarioframework.lua") -local Quaternion = import("/lua/shared/quaternions.lua").Quaternion local MathAbs = math.abs diff --git a/lua/shared/quaternions.lua b/lua/shared/quaternions.lua deleted file mode 100644 index 9acee7b37a..0000000000 --- a/lua/shared/quaternions.lua +++ /dev/null @@ -1,367 +0,0 @@ --- from: https://gist.github.com/ColonelThirtyTwo/1735522#file-quaternions-lua - --- faster access to some math library functions -local abs = math.abs --- local Round = math.Round -local sqrt = math.sqrt -local exp = math.exp -local log = math.log -local sin = math.sin -local cos = math.cos -local sinh = math.sinh -local cosh = math.cosh -local acos = math.acos - -local deg2rad = math.pi/180 -local rad2deg = 180/math.pi - -local delta = 0.0000001000000 - -Quaternion = {} -Quaternion.__index = Quaternion - ----@param q number ----@param r number ----@param s number ----@param t number ----@return Quaternion -function Quaternion.new(q,r,s,t) - return setmetatable({q,r,s,t},Quaternion) -end -local quat_new = Quaternion.new - ----@param lhs Quaternion ----@param rhs Quaternion ----@return Quaternion -local function qmul(lhs, rhs) - local lhs1, lhs2, lhs3, lhs4 = lhs[1], lhs[2], lhs[3], lhs[4] - local rhs1, rhs2, rhs3, rhs4 = rhs[1], rhs[2], rhs[3], rhs[4] - return quat_new( - lhs1 * rhs1 - lhs2 * rhs2 - lhs3 * rhs3 - lhs4 * rhs4, - lhs1 * rhs2 + lhs2 * rhs1 + lhs3 * rhs4 - lhs4 * rhs3, - lhs1 * rhs3 + lhs3 * rhs1 + lhs4 * rhs2 - lhs2 * rhs4, - lhs1 * rhs4 + lhs4 * rhs1 + lhs2 * rhs3 - lhs3 * rhs2 - ) -end -Quaternion.__mul = qmul - ----@param q Quaternion ----@return Quaternion -local function qexp(q) - local m = sqrt(q[2]*q[2] + q[3]*q[3] + q[4]*q[4]) - local u1, u2, u3 = 0, 0, 0 - if m ~= 0 then - u1 = q[2]*sin(m)/m - u2 = q[3]*sin(m)/m - u3 = q[4]*sin(m)/m - end - local r = exp(q[1]) - return quat_new(r*cos(m), r*u1, r*u2, r*u3) -end - ----@param q Quaternion ----@return Quaternion -local function qlog(q) - local l = sqrt(q[1]*q[1] + q[2]*q[2] + q[3]*q[3] + q[4]*q[4]) - if l == 0 then return { -1e+100, 0, 0, 0 } end - local u2, u3, u4 = q[2]/l, q[3]/l, q[4]/l -- u1 is never used - local a = acos(u[1]) - local m = sqrt(u2*u2 + u3*u3 + u4*u4) - if abs(m) > delta then - return quat_new( log(l), a*u2/m, a*u3/m, a*u4/m ) - else - return quat_new( log(l), 0, 0, 0 ) --when m is 0, u[2], u[3] and u[4] are 0 too - end -end -Quaternion.log = qlog - ---- Converts to a quaternion ----@param pitch number ----@param yaw number ----@param roll number ----@return Quaternion -function Quaternion.fromAngle(pitch, yaw, roll) - pitch = pitch*deg2rad*0.5 - yaw = yaw*deg2rad*0.5 - roll = roll*deg2rad*0.5 - local qr = {cos(roll), sin(roll), 0, 0} - local qp = {cos(pitch), 0, sin(pitch), 0} - local qy = {cos(yaw), 0, 0, sin(yaw)} - return qmul(qy,qmul(qp,qr)) -end - --- ---@param forward Vector --- ---@param up Vector --- ---@return Quaternion --- function Quaternion.fromVectors(forward, up) --- local x = forward --- local z = up --- local y = z:Cross(x):GetNormalized() --up x forward = left - --- local ang = x:Angle() --- if ang.p > 180 then ang.p = ang.p - 360 end --- if ang.y > 180 then ang.y = ang.y - 360 end - --- local yyaw = Vector(0,1,0) --- yyaw:Rotate(Angle(0,ang.y,0)) - --- local roll = acos(y:Dot(yyaw))*rad2deg - --- local dot = y:Dot(z) --- if dot < 0 then roll = -roll end - --- local p, y, r = ang.p, ang.y, roll --- p = p*deg2rad*0.5 --- y = y*deg2rad*0.5 --- r = r*deg2rad*0.5 --- local qr = {cos(r), sin(r), 0, 0} --- local qp = {cos(p), 0, sin(p), 0} --- local qy = {cos(y), 0, 0, sin(y)} --- return qmul(qy,qmul(qp,qr)) --- end - ---- Returns quaternion for rotation about axis by angle . If ang is left out, then it is computed as the magnitude of --- function Quaternion.fromRotation(axis, ang) --- -- if ang then --- -- axis:Normalize() --- -- local ang2 = ang*deg2rad*0.5 --- -- return quat_new( cos(ang2), axis.x*sin(ang2), axis.y*sin(ang2), axis.z*sin(ang2) ) --- -- else --- -- local angSquared = axis:LengthSqr() --- -- if angSquared == 0 then return quat_new( 1, 0, 0, 0 ) end --- -- local len = sqrt(angSquared) --- -- local ang = (len + 180) % 360 - 180 --- -- local ang2 = ang*deg2rad*0.5 --- -- local sang2len = sin(ang2) / len --- -- return quat_new( cos(ang2), rv1[1] * sang2len , rv1[2] * sang2len, rv1[3] * sang2len ) --- -- end --- end - ----@return Quaternion -function Quaternion:__neg() - return quat_new( -self[1], -self[2], -self[3], -self[4] ) -end - ----@param lhs Quaternion ----@param rhs Quaternion ----@return Quaternion -function Quaternion.__add(lhs, rhs) - return quat_new( lhs[1] + rhs[1], lhs[2] + rhs[2], lhs[3] + rhs[3], lhs[4] + rhs[4] ) -end - ----@param lhs Quaternion ----@param rhs Quaternion ----@return Quaternion -function Quaternion.__sub(lhs, rhs) - return quat_new( lhs[1] - rhs[1], lhs[2] - rhs[2], lhs[3] - rhs[3], lhs[4] - rhs[4] ) -end - ----@param lhs Quaternion ----@param rhs Quaternion ----@return Quaternion -function Quaternion.__mul(lhs, rhs) - if type(rhs) == "number" then - return quat_new( rhs * lhs[1], rhs * lhs[2], rhs * lhs[3], rhs * lhs[4] ) - elseif type(rhs) == "Vector" then - local lhs1, lhs2, lhs3, lhs4 = lhs[1], lhs[2], lhs[3], lhs[4] - local rhs2, rhs3, rhs4 = rhs.x, rhs.y, rhs.z - return quat_new( - -lhs2 * rhs2 - lhs3 * rhs3 - lhs4 * rhs4, - lhs1 * rhs2 + lhs3 * rhs4 - lhs4 * rhs3, - lhs1 * rhs3 + lhs4 * rhs2 - lhs2 * rhs4, - lhs1 * rhs4 + lhs2 * rhs3 - lhs3 * rhs2 - ) - else - local lhs1, lhs2, lhs3, lhs4 = lhs[1], lhs[2], lhs[3], lhs[4] - local rhs1, rhs2, rhs3, rhs4 = rhs[1], rhs[2], rhs[3], rhs[4] - return quat_new( - lhs1 * rhs1 - lhs2 * rhs2 - lhs3 * rhs3 - lhs4 * rhs4, - lhs1 * rhs2 + lhs2 * rhs1 + lhs3 * rhs4 - lhs4 * rhs3, - lhs1 * rhs3 + lhs3 * rhs1 + lhs4 * rhs2 - lhs2 * rhs4, - lhs1 * rhs4 + lhs4 * rhs1 + lhs2 * rhs3 - lhs3 * rhs2 - ) - end -end - ----@param lhs Quaternion ----@param rhs Quaternion ----@return Quaternion -function Quaternion.__div(lhs, rhs) - local lhs1, lhs2, lhs3, lhs4 = lhs[1], lhs[2], lhs[3], lhs[4] - return quat_new( - lhs1/rhs, - lhs2/rhs, - lhs3/rhs, - lhs4/rhs - ) -end - ----@param lhs Quaternion ----@param rhs Quaternion ----@return Quaternion -function Quaternion.__pow(lhs, rhs) - local l = qlog(lhs) - return qexp({ l[1]*rhs, l[2]*rhs, l[3]*rhs, l[4]*rhs }) -end - ----@param lhs Quaternion ----@param rhs Quaternion ----@return boolean -function Quaternion.__eq(lhs, rhs) - if getmetatable(lhs) ~= Quaternion or getmetatable(lhs) ~= getmetatable(rhs) then return false end - local rvd1, rvd2, rvd3, rvd4 = lhs[1] - rhs[1], lhs[2] - rhs[2], lhs[3] - rhs[3], lhs[4] - rhs[4] - return rvd1 <= delta and rvd1 >= -delta and - rvd2 <= delta and rvd2 >= -delta and - rvd3 <= delta and rvd3 >= -delta and - rvd4 <= delta and rvd4 >= -delta -end - ---- Returns absolute value of self ----@return number -function Quaternion:abs() - return sqrt(self[1]*self[1] + self[2]*self[2] + self[3]*self[3] + self[4]*self[4]) -end - ---- Returns the conjugate of self ----@return Quaternion -function Quaternion:conj() - return quat_new(self[1], -self[2], -self[3], -self[4]) -end - ---- Returns the inverse of self ----@return Quaternion -function Quaternion:inv() - local l = self[1]*self[1] + self[2]*self[2] + self[3]*self[3] + self[4]*self[4] - return quat_new( self[1]/l, -self[2]/l, -self[3]/l, -self[4]/l ) -end - - ---- Raises Euler's constant e to the power self ----@return Quaternion -function Quaternion:exp() - return qexp(self) -end - ---- Calculates natural logarithm of self ----@return Quaternion -function Quaternion:log() - return qlog(self) -end - ---- Changes quaternion so that the represented rotation is by an angle between 0 and 180 degrees (by coder0xff) ----@return Quaternion? -function Quaternion:mod() - if self[1]<0 then return quat_new(-self[1], -self[2], -self[3], -self[4]) else return quat_new(self[1], self[2], self[3], self[4]) end -end - ---- Performs spherical linear interpolation between and . Returns for =0, for =1 ----@param q0 Quaternion ----@param q1 Quaternion ----@param t number ----@return Quaternion -function Quaternion.slerp(q0, q1, t) - local dot = q0[1]*q1[1] + q0[2]*q1[2] + q0[3]*q1[3] + q0[4]*q1[4] - local q11 - if dot<0 then - q11 = {-q1[1], -q1[2], -q1[3], -q1[4]} - else - q11 = { q1[1], q1[2], q1[3], q1[4] } -- dunno if just q11 = q1 works - end - - local l = q0[1]*q0[1] + q0[2]*q0[2] + q0[3]*q0[3] + q0[4]*q0[4] - if l==0 then return { 0, 0, 0, 0 } end - local invq0 = { q0[1]/l, -q0[2]/l, -q0[3]/l, -q0[4]/l } - local logq = qlog(qmul(invq0,q11)) - local q = qexp( { logq[1]*t, logq[2]*t, logq[3]*t, logq[4]*t } ) - return qmul(q0,q) -end - ---- Returns vector pointing forward for ----@return Vector -function Quaternion:forward() - local this1, this2, this3, this4 = self[1], self[2], self[3], self[4] - local t2, t3, t4 = this2 * 2, this3 * 2, this4 * 2 - return Vector ( - this1 * this1 + this2 * this2 - this3 * this3 - this4 * this4, - t3 * this2 + t4 * this1, - t4 * this2 - t3 * this1 - ) -end - ---- Returns vector pointing right for ----@return Vector -function Quaternion:right() - local this1, this2, this3, this4 = self[1], self[2], self[3], self[4] - local t2, t3, t4 = this2 * 2, this3 * 2, this4 * 2 - return Vector( - t4 * this1 - t2 * this3, - this2 * this2 - this1 * this1 + this4 * this4 - this3 * this3, - - t2 * this1 - t3 * this4 - ) -end - ---- Returns vector pointing up for ----@return Vector -function Quaternion:up() - local this1, this2, this3, this4 = self[1], self[2], self[3], self[4] - local t2, t3, t4 = this2 * 2, this3 * 2, this4 * 2 - return Vector( - t3 * this1 + t2 * this4, - t3 * this4 - t2 * this1, - this1 * this1 - this2 * this2 - this3 * this3 + this4 * this4 - ) -end - ---- Returns the angle of rotation in degrees ----@return number -function Quaternion:rotationAngle() - local l2 = self[1]*self[1] + self[2]*self[2] + self[3]*self[3] + self[4]*self[4] - if l2 == 0 then return 0 end - local l = sqrt(l2) - local ang = 2*acos(self[1]/l)*rad2deg -- this returns angle from 0 to 360 - if ang > 180 then ang = ang - 360 end -- make it -180 - 180 - return ang -end - ---- Returns the axis of rotation ----@return Vector -function Quaternion:rotationAxis() - local m2 = self[2] * self[2] + self[3] * self[3] + self[4] * self[4] - if m2 == 0 then return Vector( 0, 0, 1 ) end - local m = sqrt(m2) - return Vector( self[2] / m, self[3] / m, self[4] / m) -end - ---- Returns angle represented by ----@return unknown --- function Quaternion:toAngle() --- local l = sqrt(self[1]*self[1]+self[2]*self[2]+self[3]*self[3]+self[4]*self[4]) --- local q1, q2, q3, q4 = self[1]/l, self[2]/l, self[3]/l, self[4]/l - --- local x = Vector(q1*q1 + q2*q2 - q3*q3 - q4*q4, --- 2*q3*q2 + 2*q4*q1, --- 2*q4*q2 - 2*q3*q1) - --- local y = Vector(2*q2*q3 - 2*q4*q1, --- q1*q1 - q2*q2 + q3*q3 - q4*q4, --- 2*q2*q1 + 2*q3*q4) - --- local ang = x:Angle() --- if ang.p > 180 then ang.p = ang.p - 360 end --- if ang.y > 180 then ang.y = ang.y - 360 end - --- local yyaw = Vector(0,1,0) --- yyaw:Rotate(Angle(0,ang.y,0)) - --- local roll = acos(y:Dot(yyaw))*rad2deg - --- local dot = q2*q1 + q3*q4 --- if dot < 0 then roll = -roll end - --- return Angle(ang.p, ang.y, roll) --- end - ----@return string -function Quaternion:__tostring() - return string.format("<%d,%d,%d,%d>",self[1],self[2],self[3],self[4]) -end \ No newline at end of file diff --git a/lua/sim/Projectile.lua b/lua/sim/Projectile.lua index c481e8aca9..df7c2dc62f 100644 --- a/lua/sim/Projectile.lua +++ b/lua/sim/Projectile.lua @@ -11,6 +11,8 @@ local AreaDoTThread = DefaultDamage.AreaDoTThread local Flare = import("/lua/defaultantiprojectile.lua").Flare local DepthCharge = import("/lua/defaultantiprojectile.lua").DepthCharge +local RotateVectorXYZByQuat = import("/lua/utilities.lua").RotateVectorXYZByQuat + -- upvalue scope for performance local unpack = unpack local Damage = Damage @@ -183,25 +185,11 @@ Projectile = ClassProjectile(ProjectileMethods, DebugProjectileComponent) { local dx = sx * (Random() - 0.5) * fuzziness local dy = (sy + offset) * (Random() - 0.5) * fuzziness + sy / 2 + cy local dz = sz * (Random() - 0.5) * fuzziness + cz - local dw - - -- Rotate a vector by a quaternion: q * v * conjugate(q) - -- Supreme Commander quaternions use y,z,x,w! - local ty, tz, tx, tw = unpack(EntityGetOrientation(target)) - - -- compute the product in a single assignment to not have to use temporary, single-use variables. - dw, dx, dy, dz = -tx * dx - tz * dy - ty * dz, - tw * dx + tz * dz - ty * dy, - tw * dy + ty * dx - tx * dz, - tw * dz + tx * dy - tz * dx - - tx, tz, ty = -tx, -tz, -ty - -- compute the product in a single assignment to not have to use temporary, single-use variables. - dx, dy, dz = dw * tx + dx * tw + dy * ty - dz * tz, - dw * tz + dy * tw + dz * tx - dx * ty, - dw * ty + dz * tw + dx * tz - dy * tx + local orientation = EntityGetOrientation(target) + local dx, dy, dz = RotateVectorXYZByQuat(dx, dy, dz, orientation) + self:SetNewTargetGroundXYZ(px + dx, py + dy, pz + dz) else local px, _, pz = self:GetCurrentTargetPositionXYZ() diff --git a/lua/sim/ScenarioUtilities.lua b/lua/sim/ScenarioUtilities.lua index 493f2b814e..bc7bd1c3d7 100644 --- a/lua/sim/ScenarioUtilities.lua +++ b/lua/sim/ScenarioUtilities.lua @@ -503,30 +503,12 @@ end ---@param unit Unit ---@param needToRotate number function CreateWreckage(unit, needToRotate) - prop = unit:CreateWreckageProp(0) + local prop = unit:CreateWreckageProp(0) if needToRotate then -- Some units like naval and air need to rotate for effect like after death in game - local roll = 0.5 + Random() - 2 * Random(0, 1) -- Random angle +-(0.5->1.5) radian - local pitch = 0.5 + Random() - 2 * Random(0, 1) + local roll = 0.5 + Random() - 2 * Random() -- Random angle +-(0.5->1.5) radian + local pitch = 0.5 + Random() - 2 * Random() local yaw = 0 - - local unitRotation = unit:GetOrientation() - local rotation = EulerToQuaternion(roll, pitch, yaw) - local newOrientation = {} - -- mmm I`m love quaternions... =3 - newOrientation[1] = unitRotation[4] * rotation[1] + unitRotation[1] * rotation[4] + - unitRotation[2] * rotation[3 - ] - unitRotation[3] * rotation[2] - newOrientation[2] = unitRotation[4] * rotation[2] + unitRotation[2] * rotation[4] + - unitRotation[3] * rotation[1 - ] - unitRotation[1] * rotation[3] - newOrientation[3] = unitRotation[4] * rotation[3] + unitRotation[3] * rotation[4] + - unitRotation[1] * rotation[2 - ] - unitRotation[2] * rotation[1] - newOrientation[4] = unitRotation[4] * rotation[4] - unitRotation[1] * rotation[1] - - unitRotation[2] * rotation[2 - ] - unitRotation[3] * rotation[3] - - prop:SetOrientation(newOrientation, true) + prop:SetOrientation(EulerToQuaternion(roll, pitch, yaw) * unit:GetOrientation(), true) end unit:Destroy() end diff --git a/lua/sim/TerrainUtils.lua b/lua/sim/TerrainUtils.lua index e4882a9783..9f152ebf9c 100644 --- a/lua/sim/TerrainUtils.lua +++ b/lua/sim/TerrainUtils.lua @@ -1,88 +1,66 @@ - local GetTerrainHeight = GetTerrainHeight local FlattenMapRect = FlattenMapRect -local MathMax = math.max -local MathAtan = math.atan +local MathAtan2 = math.atan2 local MathDeg = math.deg -local CacheBox = { 0, 0 } -local CacheAngles = { 0, 0, 0, 0 } -local CacheHeights = { 0, 0, 0, 0 } - ---- Compute two angles in radians to match the terrain gradient +--- Compute two angles in radians to match the terrain gradient. --- Copyright 2018-2022 Sean 'Balthazar' Wheeldon ---@param pos Vector ----@param bx number ----@param bz number ----@return number ----@return number -function GetTerrainSlopeAngles(pos, bx, bz) - - local box = CacheBox - box[1], box[2] = 0.5 * bx, 0.5 * bz - - --Get heights - local heights = CacheHeights - heights[1] = GetTerrainHeight(pos[1]-box[1],pos[3]) - heights[2] = GetTerrainHeight(pos[1],pos[3]-box[2]) +---@param sizeX number +---@param sizeZ number +---@return number angleX # roll +---@return number angleZ # pitch +function GetTerrainSlopeAngles(pos, sizeX, sizeZ) + local posX, posY, posZ = pos[1], pos[2], pos[3] + local lenX, lenZ = sizeX * 0.5, sizeZ * 0.5 - --Get averages if its 2 squares or bigger, bearing in mind the number was halved. - local requireFourPoints = MathMax(box[1],box[2]) >= 1 - if requireFourPoints then - heights[3] = GetTerrainHeight(pos[1]+box[1],pos[3]) - heights[4] = GetTerrainHeight(pos[1],pos[3]+box[2]) - end + -- Get angles, starting from the upper-right edges + local angleX = MathAtan2(GetTerrainHeight(posX + lenX, posZ) - posY, lenX) + local angleZ = MathAtan2(GetTerrainHeight(posX, posZ - lenZ) - posY, lenZ) - --Subtract center height - for i, v in heights do - heights[i] = v - pos[2] + -- If it has the other edges to sample, average those + if sizeX >= 2 then + -- negate rightX when adding in average since its axis of rotation is -z + local rightX = MathAtan2(GetTerrainHeight(posX - lenX, posZ) - posY, lenX) + angleX = (angleX - rightX) * 0.5 end - - --Calculate angles - local angles = CacheAngles - for i, v in heights do - angles[i] = MathAtan(heights[i] / box[math.mod(i-1,2)+1]) + if sizeZ >= 2 then + -- negate lowerZ when adding in average since its axis of rotation is -x + local lowerZ = MathAtan2(GetTerrainHeight(posX, posZ + lenZ) - posY, lenZ) + angleZ = (angleZ - lowerZ) * 0.5 end - --Condence down to average if they were calculated - if requireFourPoints then - return 0.5 * (angles[1]-angles[3]), 0.5 * (angles[2]-angles[4]) - else - return angles[1], angles[2] - end + return angleX, angleZ end ---- Compute two angles in degrees to match the terrain gradient +--- Compute two angles in degrees to match the terrain gradient. --- Copyright 2018-2022 Sean 'Balthazar' Wheeldon ---@param pos Vector Center of area ----@param bx number width ----@param bz number height ----@return number ----@return number -function GetTerrainSlopeAnglesDegrees(pos, bx, bz) - local a1, a2 = GetTerrainSlopeAngles(pos, bx, bz) - return MathDeg(a1), MathDeg(a2) +---@param sizeX number +---@param sizeZ number +---@return number angleX +---@return number angleZ +function GetTerrainSlopeAnglesDegrees(pos, sizeX, sizeZ) + local angleX, angleZ = GetTerrainSlopeAngles(pos, sizeX, sizeZ) + return MathDeg(angleX), MathDeg(angleZ) end ---- Flattens the terrain by interpolating between +--- Flattens the terrain by bilinearly interpolating between the rectangle. --- Copyright 2018-2022 Sean 'Balthazar' Wheeldon ---@param x number Top-left coordinate ---@param z number Top-left coordinate ---@param w number ---@param h number -FlattenGradientMapRect = function(x,z,w,h) - --a1,a2 - --b1,b2 - local a1, a2, b1, b2 = GetTerrainHeight(x,z), GetTerrainHeight(x+w,z), GetTerrainHeight(x,z+h), GetTerrainHeight(x+w,z+h) +function FlattenGradientMapRect(x, z, w, h) + local start, xHeight = GetTerrainHeight(x, z), GetTerrainHeight(x + w, z) + local zHeight, corner = GetTerrainHeight(x, z + h), GetTerrainHeight(x + w, z + h) + local xGrad = (xHeight - start) / w + local zGrad = (zHeight - start) / h + local diagGrad = (start + corner - xHeight - zHeight) / (w * h) for i = 0, w do for j = 0, h do - FlattenMapRect(x+i,z+j,0,0, - ( - ((a1*(w-i)+a2*(i))/w)*(h-j) + - ((b1*(w-i)+b2*(i))/w)*(j) - )/h - ) + FlattenMapRect(x + i, z + j, 0, 0, start + i*xGrad + j*zGrad + i*j*diagGrad) end end -end \ No newline at end of file +end diff --git a/lua/sim/Unit.lua b/lua/sim/Unit.lua index 8f4b89150b..e7b625e219 100644 --- a/lua/sim/Unit.lua +++ b/lua/sim/Unit.lua @@ -2244,31 +2244,30 @@ Unit = ClassUnit(moho.unit_methods, IntelComponent, VeterancyComponent, DebugUni self.Brain:OnUnitBeingBuiltProgress(self, builder, oldProg, newProg) end, + --- Set the unit's Yaw rotation and reset its roll/pitch ---@param self Unit - ---@param angle number - SetRotation = function(self, angle) - local qx, qy, qz, qw = Explosion.QuatFromRotation(angle, 0, 1, 0) - self:SetOrientation({qx, qy, qz, qw}, true) + ---@param degrees number + SetRotation = function(self, degrees) + self:SetOrientation(utilities.QuatFromRotation(degrees), true) end, + --- Rotate the unit on its Yaw axis ---@param self Unit - ---@param angle number - Rotate = function(self, angle) - local qx, qy, qz, qw = unpack(self:GetOrientation()) - local a = math.atan2(2.0 * (qx * qz + qw * qy), qw * qw + qx * qx - qz * qz - qy * qy) - local current_yaw = math.floor(math.abs(a) * (180 / math.pi) + 0.5) - - self:SetRotation(angle + current_yaw) + ---@param degrees number + Rotate = function(self, degrees) + self:SetOrientation(utilities.QuatFromRotation(degrees) * self:GetOrientation(), true) end, + --- Set the unit's Yaw rotation towards a point and reset its roll/pitch ---@param self Unit - ---@param tpos number + ---@param tpos Vector RotateTowards = function(self, tpos) local pos = self:GetPosition() - local rad = math.atan2(tpos[1] - pos[1], tpos[3] - pos[3]) - self:SetRotation(rad * (180 / math.pi)) + local dx, dz = tpos[1] - pos[1], tpos[3] - pos[3] + self:SetOrientation(utilities.QuatFromXZDirection(dx, dz), true) end, + --- Set the unit's Yaw rotation towards the middle of the map (not playable area) and reset its roll/pitch ---@param self Unit RotateTowardsMid = function(self) local x, y = GetMapSize() diff --git a/lua/sim/units/StructureUnit.lua b/lua/sim/units/StructureUnit.lua index 1f1507a951..2feb297192 100644 --- a/lua/sim/units/StructureUnit.lua +++ b/lua/sim/units/StructureUnit.lua @@ -19,7 +19,6 @@ local EffectTemplate = import("/lua/effecttemplates.lua") local TerrainUtils = import("/lua/sim/terrainutils.lua") local Buff = import("/lua/sim/buff.lua") local AdjacencyBuffs = import("/lua/sim/adjacencybuffs.lua") -local Quaternion = import("/lua/shared/quaternions.lua").Quaternion local FactionToTarmacIndex = { UEF = 1, @@ -34,7 +33,7 @@ local Rect = Rect local WaitTicks = WaitTicks local ForkThread = ForkThread local FlattenMapRect = FlattenMapRect -local FlattenGradientMapRect = import('/lua/sim/terrainutils.lua').FlattenGradientMapRect +local FlattenGradientMapRect = import("/lua/sim/terrainutils.lua").FlattenGradientMapRect local GetTerrainHeight = GetTerrainHeight local GetReclaimablesInRect = GetReclaimablesInRect local EntityCategoryContains = EntityCategoryContains @@ -119,20 +118,17 @@ StructureUnit = ClassUnit(Unit, BlinkingLightsUnitComponent) { and (layer == 'Land' or layer == 'Seabed') then -- rotate structure to match terrain gradient - local a1, a2 = TerrainUtils.GetTerrainSlopeAnglesDegrees( + local roll, pitch = TerrainUtils.GetTerrainSlopeAngles( self:GetPosition(), blueprint.Footprint.SizeX or physicsBlueprint.SkirtSizeX, blueprint.Footprint.SizeZ or physicsBlueprint.SkirtSizeZ ) -- do not orientate structures that are on flat ground - if a1 != 0 or a2 != 0 then - -- quaternion magic incoming, be prepared! Note that the yaw axis is inverted, but then - -- re-inverted again by multiplying it with the original orientation - local quatSlope = Quaternion.fromAngle(0, 0 - a2,-1 * a1) - local quatOrient = setmetatable(self:GetOrientation(), Quaternion) - local quat = quatOrient * quatSlope - self:SetOrientation(quat, true) + if roll ~= 0 or pitch ~= 0 then + -- "q′ = q2 * q1 in which q′ corresponds to the rotation q1 followed by the rotation q2" (wikipedia) + -- the unit's orientation comes first, and then is rotated by the terrain angle + self:SetOrientation(EulerToQuaternion(roll, pitch, 0) * self:GetOrientation(), true) -- technically obsolete, but as this is part of an integration we don't want to break -- the mod package that it originates from. Originates from the BrewLan mod suite diff --git a/lua/system/utils.lua b/lua/system/utils.lua index 8db7ebcd2a..09bb5acbab 100644 --- a/lua/system/utils.lua +++ b/lua/system/utils.lua @@ -11,6 +11,9 @@ local type = type local pcall = pcall local unpack = unpack local next = next +local getmetatable = getmetatable +local rawget = rawget +local setmetatable = setmetatable -- local Random = Random @@ -884,3 +887,192 @@ function CreateTimer() end } end + + + +local vector_metatable = getmetatable(Vector2(0, 0)) + +function UnsafeQuaternion(x, y, z, w) + return setmetatable({x, y, z, w}, vector_metatable) +end + +--- Constructs a quaternion with the given components. Absent ones default to `0`. +--- If the quaternion isn't of unit length, then it is normalized. +---@param x? number +---@param y? number +---@param z? number +---@param w? number +---@return Quaternion +function Quaternion(x, y, z, w) + x, y, z, w = x or 0, y or 0, z or 0, w or 0 + local lenSq = x*x + y*y + z*z + w*w + local dif = 1 - lenSq + -- maximum residual to unit length from `EulerToQuaternion` magnitude squared appears to be about 3.5e-7 + -- squaring this is easier than using `math.abs` + if dif*dif > 1.6e-13 then + -- normalize any quaternions that break the unit length invariant + lenSq = math.sqrt(lenSq) + x, y, z, w = x / lenSq, y / lenSq, z / lenSq, w / lenSq -- let any divide-by-zero fail + end + return UnsafeQuaternion(x, y, z, w) +end + + +-- In these functions, we use `z` instead of `3` when we don't know which type of vector it is +-- because the builtin `__index` method throws a fit if it can't find the field for a Vector2 +-- (but it handles `z` fine as it knows that it maps to `3`, which happens to be nil). +-- Similarly, we `rawget` index 4 so that non-quaternions don't crash the game. + +---@overload fun(self: Vector2): Vector2 +---@param self Vector +---@return Vector +function vector_metatable.__unm(self) + if rawget(self, 4) then + -- negative orientation??? + error("incompatible vector type for negation: quaternion") + end + + local newX, newY, z = -self[1], -self[2], self.z + if z then + return Vector(newX, newY, -z) + end + + ---@diagnostic disable-next-line: return-type-mismatch + return Vector2(newX, newY) +end + +---@overload fun(a: Vector2, b: Vector2): Vector2 +---@param a Vector +---@param b Vector +---@return Vector +function vector_metatable.__add(a, b) + if getmetatable(a) ~= getmetatable(b) then + error("invalid argument for vector addition") + end + + if rawget(a, 4) or rawget(b, 4) then + --- translating orientation doesn't make sense + error("incompatible vector type for addition: quaternion") + end + + local a3, b3 = a.z, b.z + if a3 then + if b3 then + return setmetatable({b[1] + a[1], b[2] + a[2], b3 + a3}, vector_metatable) + else + error("incompatible vector types for addition: Vector + Vector2") + end + else + if b3 then + error("incompatible vector types for addition: Vector2 + Vector") + end + + return setmetatable({b[1] + a[1], b[2] + a[2]}, vector_metatable) + end +end + +---@overload fun(a: Vector2, b: Vector2): Vector2 +---@param a Vector +---@param b Vector +---@return Vector +function vector_metatable.__sub(a, b) + if getmetatable(a) ~= getmetatable(b) then + error("invalid argument for vector subtraction") + end + + if rawget(a, 4) or rawget(b, 4) then + --- translating orientation doesn't make sense + error("incompatible vector type for subtraction: quaternion") + end + + local a3, b3 = a.z, b.z + if a3 then + if b3 then + return setmetatable({b[1] - a[1], b[2] - a[2], b3 - a3}, vector_metatable) + else + error("incompatible vector types for subtraction: Vector - Vector2") + end + else + if b3 then + error("incompatible vector types for subtraction: Vector2 - Vector") + end + + return setmetatable({b[1] - a[1], b[2] - a[2]}, vector_metatable) + end +end + +---@overload fun(a: Quaternion, b: Vector): Quaternion rotation composition +---@overload fun(a: Quaternion, b: number): Quaternion scaling +---@overload fun(a: Vector, b: Quaternion): Quaternion rotation composition +---@overload fun(a: Vector, b: Vector): Vector cross product +---@overload fun(a: Vector, b: number): Vector scaling +---@overload fun(a: Vector2, b: number): Vector2 scaling +---@param a Quaternion +---@param b Quaternion +---@return Quaternion +function vector_metatable.__mul(a, b) + local a3 = a.z + if type(b) == "number" then + if rawget(a, 4) then + -- Even though we could multiply each component by the scalar, scaling an orientation + -- doesn't make sense and would break the unit length invariant + error("incompatible vector type for scalar multiplication: quaternion") + end + if a3 then + -- Vector * Scalar + ---@diagnostic disable-next-line: return-type-mismatch + return Vector(a[1] * b, a[2] * b, a3 * b) + end + -- Vector2 * Scalar + ---@diagnostic disable-next-line: return-type-mismatch + return Vector2(a[1] * b, a[2] * b) + end + + if getmetatable(a) ~= getmetatable(b) then + error("invalid argument for vector multiplication") + end + local b3 = b.z + if not a3 or not b3 then + error("incompatible vector type for multiplication: Vector2") + end + + local a1, a2, a4 = a[1], a[2], rawget(a, 4) + local b1, b2, b4 = b[1], b[2], rawget(b, 4) + if a4 then + if b4 then + -- Quaternion * Quaternion + return UnsafeQuaternion( + a1 * b4 + a4 * b1 - a3 * b2 + a2 * b3, + a2 * b4 + a3 * b1 + a4 * b2 - a1 * b3, + a3 * b4 - a2 * b1 + a1 * b2 + a4 * b3, + a4 * b4 - a1 * b1 - a2 * b2 - a3 * b3 + ) + end + + -- Quaternion * Vector (Quaternion with no spin) + return UnsafeQuaternion( + a4 * b1 - a3 * b2 + a2 * b3, + a3 * b1 + a4 * b2 - a1 * b3, + -a2 * b1 + a1 * b2 + a4 * b3, + -a1 * b1 - a2 * b2 - a3 * b3 + ) + end + + if b4 then + -- Vector (Quaternion with no spin) * Quaternion + return UnsafeQuaternion( + a1 * b4 - a3 * b2 + a2 * b3, + a2 * b4 + a3 * b1 - a1 * b3, + a3 * b4 - a2 * b1 + a1 * b2, + -a1 * b1 - a2 * b2 - a3 * b3 + ) + end + + -- Vector x Vector (cross-product) + ---@diagnostic disable-next-line: return-type-mismatch + return Vector( + a2 * b3 - a3 * b2, + a3 * b1 - a1 * b3, + a1 * b2 - a2 * b1 + ) +end \ No newline at end of file diff --git a/lua/utilities.lua b/lua/utilities.lua index 61597253c9..43f5eecd53 100644 --- a/lua/utilities.lua +++ b/lua/utilities.lua @@ -5,10 +5,19 @@ -- Copyright © 2005 Gas Powered Games, Inc. All rights reserved. ----------------------------------------------------------------- +local MathACos = math.acos +local MathCos = math.cos local MathMod = math.mod +local MathSin = math.sin +local MathSqrt = math.sqrt + +local VDist3 = VDist3 function GetDistanceBetweenTwoEntities(entity1, entity2) - return VDist3(entity1:GetPosition(), entity2:GetPosition()) + local pos1x, pos1y, pos1z = entity1:GetPositionXYZ() + local pos2x, pos2y, pos2z = entity2:GetPositionXYZ() + local dx, dy, dz = pos2x - pos1x, pos2y - pos1y, pos2z - pos1z + return MathSqrt(dx*dx + dy*dy + dz*dz) end local powersOf2 = {[0] = 1, @@ -56,217 +65,354 @@ function GetBits(number) end -- Function originally created to check if a Mass Storage can be queued in a location without overlapping +---@param originUnit Unit +---@param unitId UnitId +---@param pos Vector +---@return boolean function CanBuildInSpot(originUnit, unitId, pos) - local bp = __blueprints[unitId] - local mySkirtX = bp.Physics.SkirtSizeX / 2 - local mySkirtZ = bp.Physics.SkirtSizeZ / 2 + local posX, posZ = pos[1], pos[3] + local bpPhysics = __blueprints[unitId].Physics + local mySkirtX = bpPhysics.SkirtSizeX / 2 + local mySkirtZ = bpPhysics.SkirtSizeZ / 2 -- Find the distance between my skirt and the skirt of a potential Quantum Gateway - local xDiff = mySkirtX + 5 -- Using 5 because that's half the size of a Quantum Gateway, the largest stock structure + -- Using 5 because that's half the size of a Quantum Gateway, the largest stock structure + local xDiff = mySkirtX + 5 local zDiff = mySkirtZ + 5 -- Full extent of search rectangle - local x1 = pos.x - xDiff - local z1 = pos.z - zDiff - local x2 = pos.x + xDiff - local z2 = pos.z + zDiff + local x1 = posX - xDiff + local z1 = posZ - zDiff + local x2 = posX + xDiff + local z2 = posZ + zDiff -- Find all the units in that rectangle local units = GetUnitsInRect(x1, z1, x2, z2) + if not units then + return false + end -- Filter it down to structures and experimentals only units = EntityCategoryFilterDown(categories.STRUCTURE + categories.EXPERIMENTAL, units) -- Bail if there's nothing in range - if not units[1] then return false end + if not units[1] then + return false + end for _, struct in units do - if struct ~= originUnit then - local structPhysics = struct:GetBlueprint().Physics - local structPos = struct:GetPosition() + if struct == originUnit then + continue + end + local structPhysics = struct:GetBlueprint().Physics + local structPos = struct:GetPosition() - -- These can be positive or negative, so we need to make them positive using math.abs - local xDist = math.abs(pos.x - structPos.x) - local zDist = math.abs(pos.z - structPos.z) + -- These can be positive or negative, so we need to make them positive using math.abs + local xDist = math.abs(posX - structPos[1]) + local zDist = math.abs(posZ - structPos[3]) - local skirtDiffx = mySkirtX + (structPhysics.SkirtSizeX / 2) - local skirtDiffz = mySkirtZ + (structPhysics.SkirtSizeZ / 2) + local skirtDiffx = mySkirtX + structPhysics.SkirtSizeX * 0.5 + local skirtDiffz = mySkirtZ + structPhysics.SkirtSizeZ * 0.5 - -- Check if the axis difference is smaller than the combined skirt distance - -- If it is, we overlap, and can't build here - if xDist < skirtDiffx and zDist < skirtDiffz then - return false - end + -- Check if the axis difference is smaller than the combined skirt distance + -- If it is, we overlap, and can't build here + if xDist < skirtDiffx and zDist < skirtDiffz then + return false end end return true end --- Note: Includes allied units in selection!! +--- Gets all units in a sphere that have a different army from a given unit. Note that, despite the +--- name, this also includes ally and civilian units. +---@see GetTrueEnemyUnitsInSphere(unit, position, radius, categories) # to truly only get enemy units +---@param unit Unit +---@param position Vector +---@param radius number +---@return Unit[] | nil function GetEnemyUnitsInSphere(unit, position, radius) - local x1 = position.x - radius - local y1 = position.y - radius - local z1 = position.z - radius - local x2 = position.x + radius - local y2 = position.y + radius - local z2 = position.z + radius - local UnitsinRec = GetUnitsInRect(x1, z1, x2, z2) - + local posX, posZ = position[1], position[3] + local unitsInRec = GetUnitsInRect(posX - radius, posZ - radius, posX + radius, posZ + radius) -- Check for empty rectangle - if not UnitsinRec then - return UnitsinRec + if not unitsInRec then + return unitsInRec end - local RadEntities = {} - for _, v in UnitsinRec do - local dist = VDist3(position, v:GetPosition()) - if unit.Army ~= v.Army and dist <= radius then - table.insert(RadEntities, v) + local posY = position[2] + local army = unit.Army + local radiusSq = radius*radius + local k = 1 + local unitsInRadius = {} + for _, unit in unitsInRec do + if army ~= unit.Army then + continue + end + local unitPos = unit:GetPosition() + local dx, dy, dz = posX - unitPos[1], posY - unitPos[2], posZ - unitPos[3] + if dx*dx + dy*dy + dz*dz <= radiusSq then + unitsInRadius[k] = unit + k = k + 1 end end - return RadEntities + return unitsInRadius end --- This function is like the one above, but filters out Allied units -function GetTrueEnemyUnitsInSphere(unit, position, radius, categories) - local x1 = position.x - radius - local y1 = position.y - radius - local z1 = position.z - radius - local x2 = position.x + radius - local y2 = position.y + radius - local z2 = position.z + radius - local UnitsinRec = GetUnitsInRect(x1, z1, x2, z2) - +--- Gets all units in a sphere that are an enemy to a given unit and contained in a category. +---@param allyUnit Unit +---@param position Vector +---@param radius number +---@param categories? EntityCategory +---@return Unit[] | nil +function GetTrueEnemyUnitsInSphere(allyUnit, position, radius, categories) + local posX, posZ = position[1], position[3] + local unitsInRec = GetUnitsInRect(posX - radius, posZ - radius, posX + radius, posZ + radius) -- Check for empty rectangle - if not UnitsinRec then - return UnitsinRec + if not unitsInRec then + return unitsInRec end - local RadEntities = {} - for _, v in UnitsinRec do - local dist = VDist3(position, v:GetPosition()) - local vArmy = v.Army - if unit.Army ~= vArmy and not IsAlly(unit.Army, vArmy) and dist <= radius and EntityCategoryContains(categories or categories.ALLUNITS, v) then - table.insert(RadEntities, v) + local posY = position[2] + local army = allyUnit.Army + local radiusSq = radius*radius + local k = 1 + local unitsInRadius = {} + for _, unit in unitsInRec do + local unitArmy = unit.Army + if army == unitArmy or IsAlly(army, unitArmy) + or categories and not EntityCategoryContains(categories, unit) + then + continue + end + local unitPosX, unitPosY, unitPosZ = unit:GetPositionXYZ() + local dx, dy, dz = posX - unitPosX, posY - unitPosY, posZ - unitPosZ + if dx*dx + dy*dy + dz*dz <= radiusSq then + unitsInRadius[k] = unit + k = k + 1 end end - return RadEntities + return unitsInRadius end +---@see GetDistanceBetweenTwoPoints2(x1, z1, x2, z2) # for a 2D option +---@param x1 number +---@param y1 number +---@param z1 number +---@param x2 number +---@param y2 number +---@param z2 number +---@return number function GetDistanceBetweenTwoPoints(x1, y1, z1, x2, y2, z2) - local dx = (x1-x2) - local dy = (y1-y2) - local dz = (z1-z2) - return (math.sqrt(dx * dx + dy * dy + dz * dz)) + local dx = x2 - x1 + local dy = y2 - y1 + local dz = z2 - z1 + return MathSqrt(dx*dx + dy*dy + dz*dz) end -function GetDistanceBetweenTwoVectors(v1, v2) - return VDist3(v1, v2) +---@see GetDistanceBetweenTwoPoints(x1, z1, x2, z2) # for a 3D option +---@see XZDistanceTwoVectors(v1, v2) # for a vector option +---@param x1 number +---@param z1 number +---@param x2 number +---@param z2 number +---@return number +function GetDistanceBetweenTwoPoints2(x1, z1, x2, z2) + local dx, dz = x2 - x1, z2 - z1 + return MathSqrt(dx*dx + dz*dz) end +-- engine implementation is optimized to be faster than lua, see /lua/benchmarking/benchmarks/VDist3.lua +GetDistanceBetweenTwoVectors = VDist3 + +--- Returns the squared distance between two vectors +---@see XZDistanceTwoVectorsSquared(v1, v2) # for horizontal distance option +---@param v1 Vector +---@param v2 Vector +function GetDistanceBetweenTwoVectorsSquared(v1, v2) + local dx, dy, dz = v2[1] - v1[1], v2[2] - v1[2], v2[3] - v1[3] + return dx*dx + dy*dy + dz*dz +end + + +--- Returns the horizontal distance between two vectors without considering the y-axis +---@param v1 Vector +---@param v2 Vector +---@return number function XZDistanceTwoVectors(v1, v2) - return VDist2(v1[1], v1[3], v2[1], v2[3]) + local dx, dz = v2[1] - v1[1], v2[3] - v1[3] + return MathSqrt(dx*dx + dz*dz) end +--- Returns the horizontal distance between two vectors without considering the y-axis +---@see GetDistanceBetweenTwoVectorsSquared(v1, v2) # for 3D option +---@param v1 Vector +---@param v2 Vector +---@return number +function XZDistanceTwoVectorsSquared(v1, v2) + local dx, dz = v2[1] - v1[1], v2[3] - v1[3] + return dx*dx + dz*dz +end + +--- Returns the vector length +---@param v Vector +---@return number function GetVectorLength(v) - return math.sqrt(math.pow(v.x, 2) + math.pow(v.y, 2) + math.pow(v.z, 2)) + local x, y, z = v[1], v[2], v[3] + return MathSqrt(x*x + y*y + z*z) end +--- Returns the vector with the same direction, but unit length. The zero-vector returns itself. +---@param v Vector +---@return Vector function NormalizeVector(v) - local length = GetVectorLength(v) + local x, y, z = v[1], v[2], v[3] + local length = MathSqrt(x*x + y*y + z*z) + if length > 0 then local invlength = 1 / length - return Vector(v.x * invlength, v.y * invlength, v.z * invlength) + return Vector(x * invlength, y * invlength, z * invlength) else - return Vector(0,0,0) + return Vector(0, 0, 0) end end -function GetDifferenceVector(v1, v2) - return Vector(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z) -end +GetDifferenceVector = VDiff +--- Gets the direction vector from `v1` to `v2` +---@see GetScaledDirectionVector(v1, v2, len) # for a vector given length +---@param v1 Vector +---@param v2 Vector +---@return Vector function GetDirectionVector(v1, v2) - return NormalizeVector(Vector(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z)) + return NormalizeVector(Vector(v1[1] - v2[1], v1[2] - v2[2], v1[3] - v2[3])) end -function GetScaledDirectionVector(v1, v2, scale) +--- Gets the vector `v1` to `v2` with a certain length +---@see GetDirectionVector(v1, v2) # for unit length +---@param v1 Vector +---@param v2 Vector +---@param len number +---@return Vector +function GetScaledDirectionVector(v1, v2, len) local vec = GetDirectionVector(v1, v2) - return Vector(vec.x * scale, vec.y * scale, vec.z * scale) + return Vector(vec[1] * len, vec[2] * len, vec[3] * len) end +--- Gets the vector halfway between `v1` and `v2` +---@param v1 Vector +---@param v2 Vector +---@return Vector function GetMidPoint(v1, v2) - return Vector((v1.x + v2.x) * 0.5, (v1.y + v2.y) * 0.5, (v1.z + v2.z) * 0.5) + return Vector((v1[1] + v2[1]) * 0.5, (v1[2] + v2[2]) * 0.5, (v1[3] + v2[3]) * 0.5) end +--- Gets a random float between `nmin` and `nmax` +---@param nmin number +---@param nmax number +---@return number function GetRandomFloat(nmin, nmax) return Random() * (nmax - nmin) + nmin end -function GetRandomInt(nmin, nmax) - return Random(nmin, nmax) -end - +GetRandomInt = Random + +--- Returns a random offset given size variables where each component is in the range `[-v/2, v/2)`, +--- multiplied by the scalar. +---@param sx number +---@param sy number +---@param sz number +---@param scalar number +---@return number xOffset +---@return number yOffset +---@return number zOffset function GetRandomOffset(sx, sy, sz, scalar) - sx = sx * scalar - sy = sy * scalar - sz = sz * scalar - local x = Random() * sx - (sx * 0.5) - local y = Random() * sy - (sy * 0.5) - local z = Random() * sz - (sz * 0.5) - + local x = scalar * sx * (Random() - 0.5) + local y = scalar * sy * (Random() - 0.5) + local z = scalar * sz * (Random() - 0.5) return x, y, z end +--- Returns a random offset given bounds where the X and Z components are in the range +--- `[-1.5*v, 0.5*v)`, and Y is in `[-v, v)`. Everything is multiplied by the scalar. +---@see GetRandomOffset(sx, sy, sz, scalar) # for a size version +---@param sx number +---@param sy number +---@param sz number +---@param scalar number +---@return number xOffset +---@return number yOffset +---@return number zOffset function GetRandomOffset2(sx, sy, sz, scalar) - sx = sx * scalar - sy = sy * scalar - sz = sz * scalar - local x = (Random() * 2 - 1) * sx - (sx * 0.5) - local y = (Random() * 2 - 1) * sy - local z = (Random() * 2 - 1) * sz - (sz * 0.5) - + local x = scalar * sx * (Random() * 2 - 1.5) + local y = scalar * sy * (Random() * 2 - 1) + local z = scalar * sz * (Random() * 2 - 1.5) return x, y, z end -function GetClosestVector(vFrom, vToList) - local dist, cDist, retVec = 0 - if vToList then - dist = GetDistanceBetweenTwoVectors(vFrom, vToList[1]) - retVec = vToList[1] - end - for kTo, vTo in vToList do - cDist = GetDistanceBetweenTwoVectors(vFrom, vTo) - if dist > cDist then - dist = cDist - retVec = vTo +--- Returns the vector from a list that is closest to a given vector. Returns nil if empty. +---@param from Vector +---@param list? Vector[] +---@return Vector | nil +function GetClosestVector(from, list) + if not list or not list[1] then + return nil + end + local closest = list[1] + local closestDist = GetDistanceBetweenTwoVectorsSquared(from, closest) + for _, vec in list do + local dist = GetDistanceBetweenTwoVectorsSquared(from, vec) + if dist < closestDist then + closestDist = dist + closest = vec end end - - return retVec + return closest end +--- Returns the cross product of `a` across `b` +---@param v1 Vector +---@param v2 Vector +---@return Vector function Cross(v1, v2) - return Vector((v1.y * v2.z) - (v1.z * v2.y), (v1.z * v2.x) - (v1.x * v2.z), (v1.x * v2.y) - (v1.y - v2.x)) + local x1, y1, z1 = v1[1], v1[2], v1[3] + local x2, y2, z2 = v2[1], v2[2], v2[3] + return Vector( + y1 * z2 - z1 * y2, + z1 * x2 - x1 * z2, + x1 * y2 - y1 * x2 + ) end +--- Returns the dot product of two vectors +---@param v1 Vector +---@param v2 Vector +---@return number function DotP(v1, v2) - return (v1.x * v2.x) + (v1.y * v2.y) + (v1.z * v2.z) + return v1[1] * v2[1] + v1[2] * v[2] + v[3] * v[3] end -function GetAngleInBetween(v1, v2) - -- Normalize the vectors - local vec1 = {} - local vec2 = {} - vec1 = NormalizeVector(v1) - vec2 = NormalizeVector(v2) - local dotp = DotP(vec1, vec2) +--- Returns the conjugate of a quaternion +---@param q Quaternion +---@return Quaternion +function ConjugateQuat(q) + return UnsafeQuaternion({-q[1], -q[2], -q[3], q[4]}) +end - return math.acos(dotp) * (360 / (math.pi * 2)) +--- Gets the angle between two vectors in degrees +---@param v1 Vector +---@param v2 Vector +---@return number +function GetAngleInBetween(v1, v2) + local x1, y1, z2 = v1[1], v1[2], v1[3] + local x2, y2, z2 = v2[1], v2[2], v2[3] + -- arccos((v1 . v2) / (|v1| |v2|)) + local z2Sq = z2 * z2 + local dot = x1*x2 + y1*y2 + z2Sq + local len2 = MathSqrt((x1*x1 + y1*y1 + z2Sq) * (x2*x2 + y2*y2 + z2Sq)) + return MathACos(dot / len2) * 180 / math.pi end --- Computes the full angle between the two vectors in two dimensions: the y dimension is not taken into account. Angle @@ -274,7 +420,6 @@ end -- @param base The base direction from which the angle will be computed in a counter clockwise fashion. -- @param direction The direction from which we want to compute the angle given a base. function GetAngleCCW(base, direction) - local bn = NormalizeVector(base) local dn = NormalizeVector(direction) @@ -282,7 +427,7 @@ function GetAngleCCW(base, direction) local ort = { bn[3], 0, -bn[1] } -- compute the radians, correct it accordingly - local rads = math.acos(bn[1] * dn[1] + bn[3] * dn[3]) + local rads = MathACos(bn[1] * dn[1] + bn[3] * dn[3]) if ort[1] * dn[1] + ort[3] * dn[3] < 0 then rads = 2 * math.pi - rads end @@ -291,6 +436,8 @@ function GetAngleCCW(base, direction) return (180 / math.pi) * rads end +--- adds a string to `Sync.UserConRequests` +---@param string string function UserConRequest(string) if not Sync.UserConRequests then Sync.UserConRequests = {} @@ -298,18 +445,109 @@ function UserConRequest(string) table.insert(Sync.UserConRequests, string) end ------------------------------------------------------------------ --- TableCat - Concatenates multiple tables into one single table ------------------------------------------------------------------ + +--- takes multiple tables and constructs a new table with all of the values in it +---@param ... table +---@return table function TableCat(...) local ret = {} - for index = 1, table.getn(arg) do + for index = 1, arg.n do if arg[index] ~= nil then - for k, v in arg[index] do + for _, v in arg[index] do table.insert(ret, v) end end end - return ret end + + +---@overload fun(degrees: number): Quaternion +--- Creates a quaternion from an orientation axis and rotation angle. The orientation axis defaults +--- to up (the y-axis). +---@param degrees number +---@param x number +---@param y number +---@param z number +---@return Quaternion +function QuatFromRotation(degrees, x, y, z) + local halfAngle = 0.00872664625997 * degrees -- math.rad(rotation / 2) + local angleRot = MathSin(halfAngle) + local qw = MathCos(halfAngle) + if not x then + return UnsafeQuaternion(0, angleRot, 0, qw) + end + local qx = x * angleRot + local qy = y * angleRot + local qz = z * angleRot + return Quaternion(qx, qy, qz, qw) +end + +--- Returns the orientation quaternion given an XZ direction +---@param dx number +---@param dz number +---@return Quaternion +function QuatFromXZDirection(dx, dz) + -- ang = atan2(dx, dz) -- `dz` is adjacent + -- {0, sin(ang/2), 0, cos(ang/2)} + local hypot = MathSqrt(dx*dx + dz*dz) + -- use the half-angle formulas + local halfCosA = dz / (2 * hypot) + local sinHalfA = MathSqrt(0.5 - halfCosA) + local cosHalfA = MathSqrt(0.5 + halfCosA) + return UnsafeQuaternion(0, sinHalfA, 0, cosHalfA) +end + +--- Translates the XZ coordinates of a position by a length in a given quaternion orientation. +---@param pos Vector +---@param orientation Quaternion +---@param length number +function TranslateInXZDirection(pos, orientation, length) + local qx, qy, qz, qw = orientation[1], orientation[2], orientation[3], orientation[4] + local dirX = 2 * (qx * qz + qw * qy) + local dirZ = qw*qw + qx*qx - qz*qz - qy*qy + length = length / MathSqrt(dirX*dirX + dirZ*dirZ) + return Vector( + pos[1] + length * dirX, + pos[2], + pos[3] + length * dirZ + ) +end + +--- Rotates a vector representing a 3D point by a quaternion rotation +---@param v Vector +---@param q Quaternion +---@return Vector +function RotateVectorByQuat(v, q) + -- Formula: v' = q * v * conjugate(q) + v = q * v * UnsafeQuaternion(-q[1], -q[2], -q[3], q[4]) + return Vector(v[1], v[2], v[3]) +end + +--- Rotates the components of a vector representing a 3D point by a quaternion rotation +---@param vX number +---@param vY number +---@param vZ number +---@param q Quaternion +---@return number # v'X +---@return number # v'Y +---@return number # v'Z +function RotateVectorXYZByQuat(vX, vY, vZ, q) + local vW + local qX, qY, qZ, qW = unpack(q) + + -- q * v + vX, vY, vZ, vW = + qW * vX - qZ * vY + qY * vZ, + qZ * vX + qW * vY - qX * vZ, + -qY * vX + qX * vY + qW * vZ, + -qX * vX - qY * vY - qZ * vZ + + -- q -> conjugate(q) + qX, qY, qZ = -qX, -qY, -qZ + + -- v * conjugate(q) -- discard W component -- return without setting variables + return vX * qW + vW * qX - vZ * qY + vY * qZ, + vY * qW + vZ * qX + vW * qY - vX * qZ, + vZ * qW - vY * qX + vX * qY + vW * qZ +end diff --git a/tests/utility/string.spec.lua b/tests/utility/string.spec.lua index 64d5db5ce7..07c361d047 100644 --- a/tests/utility/string.spec.lua +++ b/tests/utility/string.spec.lua @@ -1,7 +1,49 @@ -- Test framework local luft = require "./tests/packages/luft" --- Functions are imported to the global scope... +-- Vector2 is needed in utils but not provided outside the game, so it has to be created here +local Vector2Meta = { + __index = function(t, k) + if k == 'x' then + return t[1] + elseif k == 'y' then + return t[2] + elseif k == 'z' then + return t[3] + else + error("bad argument #2 to `?' ('x', 'y', or 'z' expected)", 1) + end + end, + + __newindex = function(t, k, v) + if k == 'x' then + t[1] = v + elseif k == 'y' then + t[2] = v + elseif k == 'z' then + t[3] = v + else + error("bad argument #2 to `?' ('x', 'y', or 'z' expected)", 1) + end + end, +} +Vector2 = function(...) + if arg.n ~= 2 then + error("expected 2 args, but got " .. arg.n) + end + if not type(arg[1]) == "number" then + error("number expected but got " .. type(arg[1])) + end + if not type(arg[2]) == "number" then + error("number expected but got " .. type(arg[2])) + end + + local newVector2 = {arg[1], arg[2]} + setmetatable(newVector2, Vector2Meta) + return newVector2 +end + +-- These functions are imported to the global scope in globalInit and RuleInit require "./lua/system/utils.lua" luft.describe("Utils", function() diff --git a/units/UAL0401/UAL0401_Script.lua b/units/UAL0401/UAL0401_Script.lua index 3264d8f86f..e65f9e8189 100644 --- a/units/UAL0401/UAL0401_Script.lua +++ b/units/UAL0401/UAL0401_Script.lua @@ -11,6 +11,7 @@ local ADFPhasonLaser = WeaponsFile.ADFPhasonLaser local ADFTractorClaw = WeaponsFile.ADFTractorClaw local explosion = import("/lua/defaultexplosions.lua") local CreateAeonColossusBuildingEffects = import("/lua/effectutilities.lua").CreateAeonColossusBuildingEffects +local Utilities = import("/lua/utilities.lua") -- upvalue for performance local MathSqrt = math.sqrt @@ -125,26 +126,32 @@ UAL0401 = ClassUnit(AWalkingLandUnit) { end, DeathThread = function(self, overkillRatio, instigator) + local bp = self.Blueprint self:PlayUnitSound('Destroyed') explosion.CreateDefaultHitExplosionAtBone(self, 'Torso', 4.0) explosion.CreateDebrisProjectiles(self, explosion.GetAverageBoundingXYZRadius(self), - { self.Blueprint.SizeX, self.Blueprint.SizeY, self.Blueprint.SizeZ }) + {bp.SizeX, bp.SizeY, bp.SizeZ}) WaitTicks(1) + explosion.CreateDefaultHitExplosionAtBone(self, 'Right_Leg_B02', 1.0) WaitTicks(1) + explosion.CreateDefaultHitExplosionAtBone(self, 'Right_Leg_B01', 1.0) WaitTicks(1) + explosion.CreateDefaultHitExplosionAtBone(self, 'Left_Arm_B02', 1.0) WaitTicks(3) + explosion.CreateDefaultHitExplosionAtBone(self, 'Right_Arm_B01', 1.0) explosion.CreateDefaultHitExplosionAtBone(self, 'Right_Leg_B01', 1.0) - WaitTicks(15) + explosion.CreateDefaultHitExplosionAtBone(self, 'Right_Leg_B01', 1.0) explosion.CreateDefaultHitExplosionAtBone(self, 'Right_Leg_B02', 1.0) explosion.CreateDefaultHitExplosionAtBone(self, 'Left_Leg_B01', 1.0) explosion.CreateDefaultHitExplosionAtBone(self, 'Left_Leg_B02', 1.0) WaitTicks(38) + explosion.CreateDefaultHitExplosionAtBone(self, 'Torso', 5.0) explosion.CreateDefaultHitExplosionAtBone(self, 'Left_Arm_B02', 1.0) explosion.CreateDefaultHitExplosionAtBone(self, 'Right_Arm_B01', 1.0) @@ -153,19 +160,14 @@ UAL0401 = ClassUnit(AWalkingLandUnit) { end -- only apply death damage when the unit is sufficiently build - local bp = self.Blueprint local FractionThreshold = bp.General.FractionThreshold or 0.5 if self:GetFractionComplete() >= FractionThreshold then - local bp = self.Blueprint - local position = self:GetPosition() - local qx, qy, qz, qw = unpack(self:GetOrientation()) - local a = math.atan2(2.0 * (qx * qz + qw * qy), qw * qw + qx * qx - qz * qz - qy * qy) - for i, numWeapons in bp.Weapon do - if bp.Weapon[i].Label == 'CollossusDeath' then - position[3] = position[3] + 5 * math.cos(a) - position[1] = position[1] + 5 * math.sin(a) - DamageArea(self, position, bp.Weapon[i].DamageRadius, bp.Weapon[i].Damage, bp.Weapon[i].DamageType, - bp.Weapon[i].DamageFriendly) + for _, weapon in bp.Weapon do + if weapon.Label == 'CollossusDeath' then + -- move the death damage away from its feet towards where the colossus will fall + local position = Utilities.TranslateInXZDirection(self:GetPosition(), self:GetOrientation(), 5) + DamageArea(self, position, weapon.DamageRadius, weapon.Damage, + weapon.DamageType, weapon.DamageFriendly) break end end @@ -194,6 +196,3 @@ UAL0401 = ClassUnit(AWalkingLandUnit) { end, } TypeClass = UAL0401 - --- Kept for Mod Backwards Compatability -local Utilities = import("/lua/utilities.lua") diff --git a/units/URL0402/URL0402_script.lua b/units/URL0402/URL0402_script.lua index 2ed3fd5476..59ce429651 100644 --- a/units/URL0402/URL0402_script.lua +++ b/units/URL0402/URL0402_script.lua @@ -18,6 +18,7 @@ local utilities = import("/lua/utilities.lua") local EffectUtil = import("/lua/effectutilities.lua") local CANTorpedoLauncherWeapon = CybranWeaponsFile.CANTorpedoLauncherWeapon local Entity = import("/lua/sim/entity.lua").Entity +local TranslateInXZDirection = import("/lua/utilities.lua").TranslateInXZDirection ---@class URL0402 : CWalkingLandUnit ---@field AmbientExhaustEffectsBag TrashBag @@ -201,13 +202,9 @@ URL0402 = ClassUnit(CWalkingLandUnit) { local FractionThreshold = bp.General.FractionThreshold or 0.99 if self:GetFractionComplete() >= FractionThreshold then local bp = self.Blueprint - local position = self:GetPosition() - local qx, qy, qz, qw = unpack(self:GetOrientation()) - local a = math.atan2(2.0 * (qx * qz + qw * qy), qw * qw + qx * qx - qz * qz - qy * qy) for i, numWeapons in bp.Weapon do if bp.Weapon[i].Label == 'SpiderDeath' then - position[3] = position[3] + 3 * math.cos(a) - position[1] = position[1] + 3 * math.sin(a) + local position = TranslateInXZDirection(self:GetPosition(), self:GetOrientation(), 3) DamageArea(self, position, bp.Weapon[i].DamageRadius, bp.Weapon[i].Damage, bp.Weapon[i].DamageType, bp.Weapon[i].DamageFriendly) break diff --git a/units/XRL0403/XRL0403_script.lua b/units/XRL0403/XRL0403_script.lua index 8365f1c2d1..c205bbf6ff 100644 --- a/units/XRL0403/XRL0403_script.lua +++ b/units/XRL0403/XRL0403_script.lua @@ -14,6 +14,7 @@ local CANNaniteTorpedoWeapon = CybranWeaponsFile.CANNaniteTorpedoWeapon local CIFSmartCharge = CybranWeaponsFile.CIFSmartCharge local CAABurstCloudFlakArtilleryWeapon = CybranWeaponsFile.CAABurstCloudFlakArtilleryWeapon local CDFBrackmanCrabHackPegLauncherWeapon = CybranWeaponsFile.CDFBrackmanCrabHackPegLauncherWeapon +local TranslateInXZDirection = import("/lua/utilities.lua").TranslateInXZDirection local CConstructionTemplate = import("/lua/cybranunits.lua").CConstructionTemplate @@ -208,13 +209,9 @@ XRL0403 = ClassUnit(CWalkingLandUnit, CConstructionTemplate) { local FractionThreshold = bp.General.FractionThreshold or 0.99 if self:GetFractionComplete() >= FractionThreshold then local bp = self.Blueprint - local position = self:GetPosition() - local qx, qy, qz, qw = unpack(self:GetOrientation()) - local a = math.atan2(2.0 * (qx * qz + qw * qy), qw * qw + qx * qx - qz * qz - qy * qy) for i, numWeapons in bp.Weapon do if (bp.Weapon[i].Label == 'MegalithDeath') then - position[3] = position[3] + 2.5 * math.cos(a) - position[1] = position[1] + 2.5 * math.sin(a) + local position = TranslateInXZDirection(self:GetPosition(), self:GetOrientation(), 2.5) DamageArea(self, position, bp.Weapon[i].DamageRadius, bp.Weapon[i].Damage, bp.Weapon[i].DamageType, bp.Weapon[i].DamageFriendly) break From 1442dad631ec1c960e0ad7265aaddc2c023be8d8 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Wed, 23 Oct 2024 05:28:12 -0700 Subject: [PATCH 07/13] Annotate the damage data passed from weapons to projectiles (#6458) This is the table passed from weapons to projectiles so that the projectiles know their damage-dealing behavior. --- changelog/snippets/sections/other.6458.md | 1 + lua/sim/CollisionBeam.lua | 3 +++ lua/sim/Projectile.lua | 10 ++++----- lua/sim/weapon.lua | 26 ++++++++++++++++++++--- 4 files changed, 32 insertions(+), 8 deletions(-) create mode 100644 changelog/snippets/sections/other.6458.md diff --git a/changelog/snippets/sections/other.6458.md b/changelog/snippets/sections/other.6458.md new file mode 100644 index 0000000000..9dc52be74f --- /dev/null +++ b/changelog/snippets/sections/other.6458.md @@ -0,0 +1 @@ +- (#6458) Annotate the damage data table passed from weapons to projectiles. diff --git a/lua/sim/CollisionBeam.lua b/lua/sim/CollisionBeam.lua index e02acbe725..0bf3e84b39 100644 --- a/lua/sim/CollisionBeam.lua +++ b/lua/sim/CollisionBeam.lua @@ -19,6 +19,7 @@ local GetTerrainType = GetTerrainType local DefaultTerrainType = GetTerrainType(-1, -1) ---@class CollisionBeam : moho.CollisionBeamEntity +---@field DamageTable WeaponDamageTable ---@field Trash TrashBag CollisionBeam = Class(moho.CollisionBeamEntity) { @@ -358,9 +359,11 @@ CollisionBeam = Class(moho.CollisionBeamEntity) { return self.CollideFriendly end, + --- Creates a new `WeaponDamageTable` in `self.DamageTable` using the weapon blueprint ---@param self CollisionBeam SetDamageTable = function(self) local weaponBlueprint = self.Weapon:GetBlueprint() + ---@type WeaponDamageTable self.DamageTable = {} self.DamageTable.DamageRadius = weaponBlueprint.DamageRadius self.DamageTable.DamageAmount = weaponBlueprint.Damage diff --git a/lua/sim/Projectile.lua b/lua/sim/Projectile.lua index df7c2dc62f..bd5aa5519c 100644 --- a/lua/sim/Projectile.lua +++ b/lua/sim/Projectile.lua @@ -93,7 +93,7 @@ local VectorCached = Vector(0, 0, 0) ---@field Trash TrashBag ---@field Launcher Unit ---@field OriginalTarget? Unit ----@field DamageData table +---@field DamageData WeaponDamageTable ---@field CreatedByWeapon Weapon ---@field IsRedirected? boolean ---@field InnerRing? NukeAOE @@ -605,7 +605,7 @@ Projectile = ClassProjectile(ProjectileMethods, DebugProjectileComponent) { --- Called by Lua to pass the damage data as a metatable ---@param self Projectile - ---@param data table + ---@param data WeaponDamageTable PassMetaDamage = function(self, data) self.DamageData = {} setmetatable(self.DamageData, data) @@ -619,7 +619,7 @@ Projectile = ClassProjectile(ProjectileMethods, DebugProjectileComponent) { -- @param cachedPosition A cached position that is passed to prevent table allocations, can not be used in fork threads and / or after a yield statement ---@param self Projectile ---@param instigator Unit - ---@param DamageData table + ---@param DamageData WeaponDamageTable # passed by the weapon ---@param targetEntity Unit | Prop | nil ---@param cachedPosition Vector DoDamage = function(self, instigator, DamageData, targetEntity, cachedPosition) @@ -1033,7 +1033,7 @@ Projectile = ClassProjectile(ProjectileMethods, DebugProjectileComponent) { ---@deprecated ---@param self Projectile - ---@param DamageData table + ---@param DamageData WeaponDamageTable PassDamageData = function(self, DamageData) self.DamageData = {} self.DamageData.DamageRadius = DamageData.DamageRadius @@ -1046,7 +1046,7 @@ Projectile = ClassProjectile(ProjectileMethods, DebugProjectileComponent) { self.DamageData.Buffs = DamageData.Buffs self.DamageData.ArtilleryShieldBlocks = DamageData.ArtilleryShieldBlocks self.DamageData.InitialDamageAmount = DamageData.InitialDamageAmount - self.CollideFriendly = self.DamageData.CollideFriendly + self.CollideFriendly = DamageData.CollideFriendly end, ---root of all performance evil diff --git a/lua/sim/weapon.lua b/lua/sim/weapon.lua index bf12a20414..a2dceee73b 100644 --- a/lua/sim/weapon.lua +++ b/lua/sim/weapon.lua @@ -16,6 +16,24 @@ local RecycledPriTable = {} local DebugWeaponComponent = import("/lua/sim/weapons/components/DebugWeaponComponent.lua").DebugWeaponComponent +--- Table of damage information passed from the weapon to the projectile +--- Can be assigned as a meta table to the projectile's damage table to reduce memory usage for unchanged values +---@class WeaponDamageTable +---@field DamageToShields number # weaponBlueprint.DamageToShields +---@field InitialDamageAmount number # weaponBlueprint.InitialDamage or 0 +---@field DamageRadius number # weaponBlueprint.DamageRadius + Weapon.DamageRadiusMod +---@field DamageAmount number # weaponBlueprint.Damage + Weapon.DamageMod +---@field DamageType DamageType # weaponBlueprint.DamageType +---@field DamageFriendly boolean # weaponBlueprint.DamageFriendly or true +---@field CollideFriendly boolean # weaponBlueprint.CollideFriendly or false +---@field DoTTime number # weaponBlueprint.DoTTime +---@field DoTPulses number # weaponBlueprint.DoTPulses +---@field MetaImpactAmount any # weaponBlueprint.MetaImpactAmount +---@field MetaImpactRadius any # weaponBlueprint.MetaImpactRadius +---@field ArtilleryShieldBlocks boolean # weaponBlueprint.ArtilleryShieldBlocks +---@field Buffs BlueprintBuff[] # Active buffs for the weapon +---@field __index WeaponDamageTable + local function ParsePriorities() local idlist = EntityCategoryGetUnitList(categories.ALLUNITS) local finalPriorities = {} @@ -59,6 +77,7 @@ local WeaponMethods = moho.weapon_methods ---@field CollideFriendly boolean ---@field DamageMod number ---@field DamageRadiusMod number +---@field damageTableCache WeaponDamageTable | false # Set to false when the weapon's damage is modified ---@field DisabledBuffs table ---@field EnergyRequired? number ---@field EnergyDrainPerSecond? number @@ -441,8 +460,9 @@ Weapon = ClassWeapon(WeaponMethods, DebugWeaponComponent) { ---@param self Weapon GetDamageTableInternal = function(self) local weaponBlueprint = self.Blueprint + ---@type WeaponDamageTable local damageTable = {} - + damageTable.DamageToShields = weaponBlueprint.DamageToShields damageTable.InitialDamageAmount = weaponBlueprint.InitialDamage or 0 damageTable.DamageRadius = weaponBlueprint.DamageRadius + self.DamageRadiusMod @@ -476,12 +496,12 @@ Weapon = ClassWeapon(WeaponMethods, DebugWeaponComponent) { damageTableCache = false, ---@param self Weapon - ---@return table | boolean + ---@return WeaponDamageTable GetDamageTable = function(self) if not self.damageTableCache then self.damageTableCache = self:GetDamageTableInternal() end - return self.damageTableCache + return self.damageTableCache --[[@as WeaponDamageTable]] end, ---@param self Weapon From ba693b0c071ff0afc317661a0cdc3b4f5614935c Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Wed, 23 Oct 2024 05:33:35 -0700 Subject: [PATCH 08/13] Fix movement effects warning spam due to contrails (#6480) --- changelog/snippets/fix.6436.md | 2 +- lua/sim/Unit.lua | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/changelog/snippets/fix.6436.md b/changelog/snippets/fix.6436.md index 1afff09c70..17e12d25bb 100644 --- a/changelog/snippets/fix.6436.md +++ b/changelog/snippets/fix.6436.md @@ -1 +1 @@ -- (#6436) Prevent the logging of an unecessary warning when certain units make landfall. +- (#6436, #6480) Prevent the logging of an unecessary warning when certain units make landfall. diff --git a/lua/sim/Unit.lua b/lua/sim/Unit.lua index e7b625e219..a77a1a191c 100644 --- a/lua/sim/Unit.lua +++ b/lua/sim/Unit.lua @@ -3634,7 +3634,7 @@ Unit = ClassUnit(moho.unit_methods, IntelComponent, VeterancyComponent, DebugUni ---@overload fun(self: Unit, effectTypeGroups: UnitBlueprintEffect[], fxBlockType: "FXImpact", impactType: ImpactType, suffix?: string, bag?: TrashBag, terrainType?: TerrainType) ---@overload fun(self: Unit, effectTypeGroups: UnitBlueprintEffect[], fxBlockType: "FXMotionChange", motionChange: MotionChangeType, suffix?: string, bag?: TrashBag, terrainType?: TerrainType) ---@overload fun(self: Unit, effectTypeGroups: UnitBlueprintEffect[], fxBlockType: "FXLayerChange", layerChange: LayerChangeType, suffix?: string, bag?: TrashBag, terrainType?: TerrainType) - --- + --- Creates effects defined in `TerrainTypes.lua` based on the terrain that a movement is moving on. ---@param self Unit ---@param effectTypeGroups UnitBlueprintEffect[] ---@param fxBlockType LayerTerrainEffectType @@ -3707,11 +3707,12 @@ Unit = ClassUnit(moho.unit_methods, IntelComponent, VeterancyComponent, DebugUni end end, + --- Creates camera shake effects and also movement effects for the terrain a unit is moving on. ---@param self Unit ---@param EffectsBag TrashBag ---@param TypeSuffix string - ---@param TerrainType string - ---@return boolean + ---@param TerrainType TerrainType? + ---@return boolean? CreateMovementEffects = function(self, EffectsBag, TypeSuffix, TerrainType) local layer = self.Layer local bpTable = self.Blueprint.Display.MovementEffects @@ -3726,8 +3727,8 @@ Unit = ClassUnit(moho.unit_methods, IntelComponent, VeterancyComponent, DebugUni local effectTypeGroups = bpTable.Effects if not effectTypeGroups or (effectTypeGroups and (table.empty(effectTypeGroups))) then - -- warning isn't needed if this layer's table is used for Footfall without terrain effects - if not bpTable.Footfall then + -- warning isn't needed if this layer's table is used for Footfall or Contrails without terrain effects + if not bpTable.Footfall and not bpTable.Contrails then WARN('*No movement effect groups defined for unit ', repr(self.UnitId), ', Effect groups with bone lists must be defined to play movement effects. Add these to the Display.MovementEffects.', layer, '.Effects table in unit blueprint.') end return false From 7e4d12dfe5da4623ac80ce4d35381f38d4f047d5 Mon Sep 17 00:00:00 2001 From: "(Jip) Willem Wijnia" Date: Sun, 27 Oct 2024 06:56:25 +0100 Subject: [PATCH 09/13] Refactor of `maputils.lua` (#6495) ## Description of the proposed changes Updates the annotations surrounding the map utilities module and decouples some of the functionality. This allows the logic in other modules, such as the (auto) lobby, to be simplified. Removes the map blacklist module. It has not been updated for a decade and these days we do have decent version control of maps in the vault. ## Testing done on the proposed changes Tested in both the regular and the coop lobby. --- changelog/snippets/other.6495.md | 3 + engine/User.lua | 2 +- etc/faf/mapblacklist.lua | 1907 ------------------------------ lua/ui/dialogs/mapselect.lua | 3 - lua/ui/maputil.lua | 274 +++-- 5 files changed, 209 insertions(+), 1980 deletions(-) create mode 100644 changelog/snippets/other.6495.md delete mode 100644 etc/faf/mapblacklist.lua diff --git a/changelog/snippets/other.6495.md b/changelog/snippets/other.6495.md new file mode 100644 index 0000000000..65d8a95284 --- /dev/null +++ b/changelog/snippets/other.6495.md @@ -0,0 +1,3 @@ +- (#6495) Refactor the map utilities module + +Chunks up some larger functions into smaller functions to allow them to be re-used in other modules. diff --git a/engine/User.lua b/engine/User.lua index 63c82e050a..b68f8f6564 100644 --- a/engine/User.lua +++ b/engine/User.lua @@ -1004,7 +1004,7 @@ end --- Return the table of scenario info that was originally passed to the sim on launch --- Unlike other engine functions that return tables, this function returns the same table each time it is called. ----@return UIScenarioInfo +---@return UISessionSenarioInfo function SessionGetScenarioInfo() end diff --git a/etc/faf/mapblacklist.lua b/etc/faf/mapblacklist.lua deleted file mode 100644 index 631a00dd70..0000000000 --- a/etc/faf/mapblacklist.lua +++ /dev/null @@ -1,1907 +0,0 @@ --- Someday, we'll have a versioning system that makes sense. --- --- This is not that day. --- --- Today, we have a huge pile of regexes, a cronjob, and this blacklist of out-of-date maps. --- --- To make things even _more_ exciting, the ZeP-insanity-version-number (.v[0-9]{4}) is excluded --- from scenario file names, so we need to be slightly magical to even _use_ this fucking blacklist. --- It is quite hard to imagine how the existing versioning system could be any less effective at its --- job without its replacement with a PRNG. -MapBlacklist = { - ['000t2'] = true, - ['100_acre_wood'] = true, - ['100_aw teamplay_ai'] = true, - ["1035 drake's ravine v1.0 3v3.v0001"] = true, - ["1035 Drake's Ravine v1.0 3v3.v0001"] = true, - ["1035 seton's clutch v1.1 3v3.v0001"] = true, - ["1035 Seton's Clutch v1.1 3v3.v0001"] = true, - ['10 nuke the elk'] = true, - ['10 seton'] = true, - ['10 the plain'] = true, - ['10x10 desertpass'] = true, - ['10x10 desertpass.v0002'] = true, - ['10x10test.v0001'] = true, - ['12-citadel small'] = true, - ['12-citadel small v2'] = true, - ['12-desert warefare'] = true, - ['12-isis'] = true, - ['12 the pass'] = true, - ['1v1 bigfmap'] = true, - ['1v1 hot box'] = true, - ['1vs1 killers.v0001'] = true, - ['1vs1 killers.v0002'] = true, - ['1vs1 killers.v0003'] = true, - ['1vs1 killers.v0004'] = true, - ['20nr2x_hero_islands.v0001'] = true, - ['2 side crater'] = true, - ['2v2 boogie-woogie'] = true, - ['2v2tag_voi_vittu'] = true, - ['2v4 bad cage v1'] = true, - ['2way'] = true, - ['3bridges'] = true, - ['3lake'] = true, - ['3lake greenv2'] = true, - ['3v3_ballin'] = true, - ['3v3carnagefest'] = true, - ['3v3 engagement'] = true, - ['3v3 hot box'] = true, - ['3v3 paradise'] = true, - ['3v3 rx30.v0001'] = true, - ['3v3 sand box.v0001'] = true, - ['3v3 sand box.v0001 - copy'] = true, - ['3v3 sand box v2a'] = true, - ['3v3 v3.v0001'] = true, - ['3vs3 fun.v0001'] = true, - ['3vs3 fun.v0002'] = true, - ['3vs3 fun.v0003'] = true, - ['3way'] = true, - ['4 double island v2'] = true, - ['4v4 battle box v2.v0001'] = true, - ['4v4 passage.v0001'] = true, - ['4v4 team tournament map one'] = true, - ['4v4 team tournament map one.v0002'] = true, - ['4v4 the violence'] = true, - ['5vs5 tabula rasa'] = true, - ['666.v0001'] = true, - ['666.v0003'] = true, - ['6 - badlands_fa.v0001'] = true, - ['6 - badlands.v0001'] = true, - ['6 - badlands.v0002'] = true, - ['6castles.v0001'] = true, - ['6fortcastles'] = true, - ['6fortcastlesv1'] = true, - ['6fortcastlesv2'] = true, - ['6fortcastlesv3'] = true, - ['6-icelands_v1.v0001'] = true, - ['6-icelands_v2.v0001'] = true, - ['6-icelands_v3.v0001'] = true, - ['6-icelands_v4.v0001'] = true, - ['6 point.v0001'] = true, - ['6v6 team tournament map two'] = true, - ['7impstar.v0001'] = true, - ['8 - badlands.v0001'] = true, - ['8 - badlandsv1.v0001'] = true, - ['8 - badlands_v2.v0001'] = true, - ['8 - badlands_v3.v0001'] = true, - ['8 castles v3'] = true, - ['8plfosv1.v0001'] = true, - ['action_tug_of_war.v0001'] = true, - ['action_tug_of_war.v0002'] = true, - ['adaptive_adrastea.v0001'] = true, - ['adaptive_adrastea.v0002'] = true, - ['adaptive_corona.v0001'] = true, - ['adaptive_hrungdaks_canyon.v0001'] = true, - ['adaptive_zeta_wonder.v0001'] = true, - ['adaptive_twin_rivers.v0001'] = true, - ['adaptive_diversity.v0001'] = true, - ['adaptive_flooded_tabula_rasa.v0001'] = true, - ['adaptive_hilly_plateau.v0001'] = true, - ['adaptive_regor_highlands.v0001'] = true, - ['adaptive_canis_river.v0001'] = true, - ['adaptive_argon.v0001'] = true, - ['adaptive_new_canis.v0001'] = true, - ['adaptive_flooded_corona.v0001'] = true, - ['adaptive_point_of_reason.v0001'] = true, - ['adaptive_fields_of_great_phoenix.v0001'] = true, - ['adaptive_ians_cross.v0001'] = true, - ['adaptive_setons_clutch.v0001'] = true, - ['adaptive_twilight_hill.v0001'] = true, - ['adaptive_point_of_reach.v0001'] = true, - ['adaptive_waters_of_random_noobs.v0001'] = true, - ['adaptive_waters_of_random_noobs.v0002'] = true, - ['adaptive_waters_of_random_noobs.v0003'] = true, - ['adaptive_waters_of_random_noobs.v0004'] = true, - ['adaptive_waters_of_random_noobs.v0005'] = true, - ['adaptive_waters_of_random_noobs.v0006'] = true, - ['adaptive_wonder_open.v0001'] = true, - ['adaptive_neptune.v0001'] = true, - ['adaptive_flooded_argon.v0001'] = true, - ['aeon afterworld gw.v0001'] = true, - ['airbase.v0001'] = true, - ['airbasev2.v0001'] = true, - ['ai wars'] = true, - ['alien desert'] = true, - ['aliendesert v2'] = true, - ['aliendesert v4'] = true, - ['alphawarv2'] = true, - ['alphawarv4'] = true, - ['alphawarv5'] = true, - ['alphawarv6'] = true, - ['alphawarv7'] = true, - ['alphawarv8'] = true, - ['americore.v0001'] = true, - ['ancient crater.v0001'] = true, - ['anti_002'] = true, - ['anti_002.v0002'] = true, - ['anti_003'] = true, - ['aperture.v0001'] = true, - ['archipelago.v0002'] = true, - ['archipelago.v0003'] = true, - ['archipelago.v0004'] = true, - ['arctic surprise.v0001'] = true, - ['arctic surprise.v0002'] = true, - ['area death.v0001'] = true, - ['arena of shinzon.v0001'] = true, - ['arena of shinzon v2.v0001'] = true, - ['argon v1'] = true, - ['armed and dangerous.v0001'] = true, - ['artys paradise.v0001'] = true, - ['assassin circles v1.v0001'] = true, - ['assassin circles v2.v0001'] = true, - ['assassin ice circles v1.v0001'] = true, - ['assassin ice circles v2.v0001'] = true, - ['asterion.v0001'] = true, - ['astro crater battles v3'] = true, - ['astro guy war'] = true, - ['astro silar pass'] = true, - ['asymmetric'] = true, - ['asymmetric.v0002'] = true, - ['atlantis.v0001'] = true, - ['attack of the intrepid 3 player.v0001'] = true, - ['attack of the intrepid 3 player.v0002'] = true, - ['attack of the intrepid 3 player.v0003'] = true, - ['attack of the intrepid 3 vs 1.v0001'] = true, - ['attack of the intrepid 3 vs 1.v0002'] = true, - ['australia.v0001'] = true, - ['avangiss'] = true, - ['avangiss v3'] = true, - ['back at back.v0001'] = true, - ['back at back.v0002'] = true, - ['back to back.v0001'] = true, - ['back_to_back.v0001'] = true, - ['baja.v0001'] = true, - ['baked.v0001'] = true, - ['balvery mountains.v0001'] = true, - ['balvery mountains v2.v0001'] = true, - ['balvery mountains v3.v0001'] = true, - ['balvery mountains v4.v0001'] = true, - ['barren big.v0001'] = true, - ['barren.v0001'] = true, - ['barrier_islands.v0001'] = true, - ['barrier_islands.v0002'] = true, - ['barrier_islands.v0003'] = true, - ['basicblocks 1 - v1'] = true, - ['basicblocks 1 - v2'] = true, - ['battle_arena'] = true, - ['battle for inland ffa by licomfx v1.v0001'] = true, - ['battle for the land 3vs3'] = true, - ['battle for the land.v0001'] = true, - ['battle for the land.v0002'] = true, - ['battle line'] = true, - ['battle of midway.v0001'] = true, - ['battle of rohan'] = true, - ['battle of tactics v5.v0001'] = true, - ['battle of thermopylae official'] = true, - ['battle of thermopylae.v0001'] = true, - ['battle of thermopylae v2.v0001'] = true, - ['battle of thermopylae v3'] = true, - ['beachhead'] = true, - ['beachhead.v0001'] = true, - ['beltway.v0001'] = true, - ['bgh'] = true, - ['big black papy2.v0001'] = true, - ['big game hunter.v0001'] = true, - ['big sentry point'] = true, - ['black_sun'] = true, - ["blitzphil's duel.v0001"] = true, - ["blitzphil's duel.v0002"] = true, - ['block wars'] = true, - ['bloodbath.v0001'] = true, - ['bloodbath.v0002'] = true, - ['bloodbath.v0003'] = true, - ['blood sacrifice.v0001'] = true, - ['bloody archpeligo.v0001'] = true, - ['bloody archpeligo.v0002'] = true, - ['blue divide.v0001'] = true, - ['blue divide.v0002'] = true, - ['bossie07'] = true, - ['braincakes thermo small v1.v0001'] = true, - ['bridge two far.v0001'] = true, - ['bridge two far.v0002'] = true, - ['bridge two short.v0001'] = true, - ['brutal mounds'] = true, - ['bullfish island.v0001'] = true, - ['bullfish island.v0002'] = true, - ['bullfish island.v0003'] = true, - ['bullfish island.v0004'] = true, - ['bullfish island.v0005'] = true, - ['bunnyroanoke.v0001'] = true, - ['bunnyroanokev10.v0001'] = true, - ['bunnyroanokev2.v0001'] = true, - ['bunnyroanokev3.v0001'] = true, - ['bunnyroanokev4.v0001'] = true, - ['bunnyroanokev5.v0001'] = true, - ['bunnyroanokev6.v0001'] = true, - ['bunnyroanokev7.v0001'] = true, - ['bunnyroanokev8.v0001'] = true, - ['bunnyroanokev9.v0001'] = true, - ['butterfly.v0001'] = true, - ['butterfly.v0002'] = true, - ['butterfly.v0003'] = true, - ['butterfly.v0004'] = true, - ['canalbedlam'] = true, - ['canalbedlamv2'] = true, - ['canis 4v4 spezial edition.v0001'] = true, - ['canyonwarversion2.v0001'] = true, - ['carnage canyon.v0001'] = true, - ['cathare cross resized.v0002'] = true, - ['cauldron_small_ep'] = true, - ['central fortress.v0001'] = true, - ['central fortress.v0002'] = true, - ['central fortress.v0003'] = true, - ['central mound'] = true, - ['central mound2'] = true, - ['centura.v0001'] = true, - ['centura.v0002'] = true, - ['cgm-353838578'] = true, - ['cgm-61893250'] = true, - ['cgm-61893250.v0001'] = true, - ['champions last stand.v0001'] = true, - ['chantry hills.v0001'] = true, - ['chartresfire.v0001'] = true, - ['cheese slice v1.v0001'] = true, - ['chiznila zone.v0001'] = true, - ['choked.v0001'] = true, - ['chokequest.v0003'] = true, - ['chokequest.v0004'] = true, - ['chokequest.v0005'] = true, - ['ch_td01v10.v0001'] = true, - ['ch_td01v11.v0001'] = true, - ['ch_td01v12.v0001'] = true, - ['city in the sky.v0001'] = true, - ['clash of the titans.v0001'] = true, - ['clash of the titans.v0002'] = true, - ['clash of the titans.v0003'] = true, - ['cliff_defense_v1'] = true, - ['closest enemies'] = true, - ['close to home.v0001'] = true, - ['cloved island.v0001'] = true, - ['cloved island.v0002'] = true, - ['cloved island.v0003'] = true, - ['cloved island.v0004'] = true, - ['clover'] = true, - ['clover.v0001'] = true, - ['cm 2.v0001'] = true, - ['cm 2.v0002'] = true, - ['cm 2.v0003'] = true, - ['cmpdd_cw002.v0001'] = true, - ['coastal bridge'] = true, - ['coastalchaos'] = true, - ['coastal siege small.v0001'] = true, - ['coastal wars.v0001'] = true, - ['coastal wars.v0002'] = true, - ['coast to coast.v0001'] = true, - ['cobalt_canyon_v2'] = true, - ['cobalt valley - alpha'] = true, - ['cobalt valley - alpha v2'] = true, - ['cockroachcanyon'] = true, - ['cockroachcanyon.v0001'] = true, - ['cold conflict'] = true, - ['cold conflict 1v1'] = true, - ['colosseum.v0001'] = true, - ['colusseum.v0001'] = true, - ['comet catcher'] = true, - ['comet catcher mirrored'] = true, - ['comet catcher mirrored v2'] = true, - ['comet catcher mirrored v3'] = true, - ['comet catcher mirrored v4'] = true, - ['comet catcher mirrored v5'] = true, - ['comet catcher.v0001'] = true, - ['compstomp_isolatesystem_v3'] = true, - ['compstomp_isolatesystem_v4'] = true, - ['compstomp unleashed'] = true, - ['cool maze.v0001'] = true, - ['coreprimecanyonfort.v0001'] = true, - ['coreprimecanyonfort.v0002'] = true, - ['core prime industrial area.v0001'] = true, - ['core prime industrial area.v0002'] = true, - ['core prime industrial area.v0003'] = true, - ['core prime industrial area.v0004'] = true, - ['core prime industrial area.v0005'] = true, - ['corner posts v1'] = true, - ['corner posts v2'] = true, - ['corsicafuss.v0001'] = true, - ['corsicafuss.v0002'] = true, - ['corsicafuss.v0003'] = true, - ['corsicafuss.v0005'] = true, - ['corvana chasm.v0001'] = true, - ['crabs.v0005'] = true, - ['crater war'] = true, - ['crazy phantom.v0001'] = true, - ['crazyrush.v0002'] = true, - ['crop circle v002'] = true, - ['crop circle v1'] = true, - ['crossfire drought'] = true, - ['cross fire.v0001'] = true, - ['crystal canyons b v2'] = true, - ['crystal canyons b v3'] = true, - ['crystal sludge.v0001'] = true, - ['cyclone.v0001'] = true, - ['cyclone v2.v0001'] = true, - ['czar prime.v0001'] = true, - ['czar prime.v0002'] = true, - ['_czr_battlefield.v0001'] = true, - ['dangerousrock.v0002'] = true, - ['daring_pursuit.v0001'] = true, - ['daring_pursuit.v0002'] = true, - ['daring_pursuit.v0003'] = true, - ['daring_pursuit.v0004'] = true, - ['daring_pursuit.v0005'] = true, - ['dark gate.v0001'] = true, - ['dawn'] = true, - ['dawn_v2'] = true, - ['deafstarassult.v0001'] = true, - ['deathmagnet.v0001'] = true, - ['deathmagnet.v0002'] = true, - ['death pass.v0004'] = true, - ['deat point'] = true, - ['deltasiegedry'] = true, - ['deltasiegedry v3'] = true, - ['deltasiegedry v4'] = true, - ['deltasiegedry v5'] = true, - ['deltasiegedry v6'] = true, - ['deltasiegedry v7'] = true, - ['denialation.v0001'] = true, - ['denmark3.v0001'] = true, - ['denmark3.v0002'] = true, - ['denmark3.v0003'] = true, - ['depths v6'] = true, - ['desert arena v2.v0002'] = true, - ['desert conflict.v0001'] = true, - ['desert fox.v0001'] = true, - ['desert isles.v0001'] = true, - ['desert isles.v0002'] = true, - ['desert of pillars'] = true, - ['desert of war.v0001'] = true, - ['desert planet ii.v0001'] = true, - ['desert planet ii.v0002'] = true, - ['desert planet.v0001'] = true, - ['desert rock'] = true, - ['desert sunset'] = true, - ['desert throne'] = true, - ['desert throne v2'] = true, - ['desert throne v3'] = true, - ['desert throne v4'] = true, - ['desert throne v5'] = true, - ['desert throne v6'] = true, - ['desert throne v8 (4v4)'] = true, - ['desert warplane'] = true, - ['desert warplane v2'] = true, - ['deset of desolation'] = true, - ['destiny.v0001'] = true, - ['destiny.v0002'] = true, - ['deutaria.v0001'] = true, - ['deutaria.v0002'] = true, - ['devils landings.v0001'] = true, - ['dhl road to perdition.v0001'] = true, - ['dievalleyii.v0001'] = true, - ['dirr.v0002'] = true, - ['divided oasis.v0001'] = true, - ['divided oasis.v0002'] = true, - ['docks.v0001'] = true, - ['domain of the teenage mutant ninja turtles.v0001'] = true, - ['donners pass.v0001'] = true, - ['dragon_beach'] = true, - ['dragon_island.v0001'] = true, - ['dragon_river'] = true, - ['dragon_river.v0003'] = true, - ['dragon_river_v4'] = true, - ['dragoons'] = true, - ['drakes_ravine_4v4_v2'] = true, - ['dry canyon.v0003'] = true, - ['dry canyon v2'] = true, - ['dry desert world.v0002'] = true, - ['dry river.v0001'] = true, - ['dry waterways.v0001'] = true, - ['dual_arches.v0001'] = true, - ['dual_arches.v0004'] = true, - ['dustbowlin'] = true, - ['dusty grounds.v0001'] = true, - ['e-3 alessius v1.v0001'] = true, - ['ea the see.v0001'] = true, - ['elementofthreat.v0001'] = true, - ['elenin.v0001'] = true, - ['elephantgraveyard'] = true, - ['elysion_40k'] = true, - ['emerald crater large.v0001'] = true, - ['emerald island.v0001'] = true, - ['ephemeral'] = true, - ['ephemeral.v0002'] = true, - ['eraphim technology.v0001'] = true, - ['erfect.v0001'] = true, - ['evad.v0001'] = true, - ['evil is here'] = true, - ['evil waters'] = true, - ['field of thunder.v0001'] = true, - ['fields of grass.v0001'] = true, - ['fields of grass.v0002'] = true, - ['fields of isis 3v3'] = true, - ['fields of isis 3v3.v0001'] = true, - ['fields_of_isis_remix_4x4-t.v0001'] = true, - ['fields_of_isis_remix-t.v0001'] = true, - ['fields_of_isis_remix-t.v0002'] = true, - ['fields_of_isis_remix-t.v0003'] = true, - ['fields_of_isis_rock_v1'] = true, - ['fields_of_isis_rock_v3'] = true, - ['fields_of_isis_rock_v6'] = true, - ['fields of thunder.v0001'] = true, - ['fight of the giants.v0001'] = true, - ['fight of the giants v2.v0001'] = true, - ['filds of training.v0001'] = true, - ['final rush pro2 4v4.v0001'] = true, - ['final rush pro2 4v4.v0002'] = true, - ['final rush pro3 4v4.v0001'] = true, - ['final rush pro3 4v4.v0002'] = true, - ['final rush pro3 4v4.v0003'] = true, - ['final rush pro3 4v4.v0004'] = true, - ['final rush pro3 4v4.v0005'] = true, - ['final rush pro3 4v4.v0006'] = true, - ['final rush pro3 4v4.v0007'] = true, - ['final rush pro3 4v4.v0008'] = true, - ['final rush pro3 4v4.v0009'] = true, - ['final rush pro3 4v4.v0010'] = true, - ['final rush pro3 4v4.v0011'] = true, - ['final rush survival.v0001'] = true, - ['final rush survival.v0002'] = true, - ['final rush survival.v0003'] = true, - ['final rush survival.v0004'] = true, - ['final rush survival.v0005'] = true, - ['final rush survival.v0006'] = true, - ['final with handicap.v0001'] = true, - ['final with handicap.v0002'] = true, - ['final with handicap.v0003'] = true, - ['final with handicap.v0005'] = true, - ['final with handicap.v0006'] = true, - ['final with handicap.v0007'] = true, - ['final with handicap.v0008'] = true, - ["finn's fury.v0001"] = true, - ['finns of trololol'] = true, - ['firefox.v0001'] = true, - ['firefox.v0002'] = true, - ['fireline 2 v1.v0001'] = true, - ['fireline 4 v1.v0001'] = true, - ['firstmap'] = true, - ['flat'] = true, - ['flat (1mexeach)'] = true, - ['florence is mad v1'] = true, - ['florence is mad v2'] = true, - ['forbidden pass.v0001'] = true, - ['forbidden passv2'] = true, - ['forbidden passv3'] = true, - ['forestplanet fa v2a.v0001'] = true, - ['forgotten_corridors.v0001'] = true, - ['fort_1mex.v0001'] = true, - ['fort major.v0001'] = true, - ['fortress micro remix 2.v0001'] = true, - ['fortress micro remix.v0001'] = true, - ['fortress wars.v0001'] = true, - ['fourpointer.v0001'] = true, - ['fourpointer.v0002'] = true, - ['four way setons.v0001'] = true, - ['frost bite'] = true, - ['frozenblood_for_ai'] = true, - ['frozen isis.v0003'] = true, - ['frozen pass.v0001'] = true, - ['frozen wasteland.v0001'] = true, - ['fudge.v0001'] = true, - ['fudge.v0002'] = true, - ['fun 3vs3.v0001'] = true, - ['fundamental.v0001'] = true, - ['funffa3 battle phantom island.v0001'] = true, - ['fusion.v0001'] = true, - ['f_xl_isis_fa.v0001'] = true, - ['galms naval grid'] = true, - ['gap of genesis'] = true, - ['gap of rhonda teamplay'] = true, - ['gap of rhonda teamplay.v0004'] = true, - ['gap of rhonda teamplay.v0005'] = true, - ['gates to paradise'] = true, - ['gates to paradise.v0001'] = true, - ['gbc.v0001'] = true, - ['gd21.v0001'] = true, - ['genisis01v'] = true, - ['gfi.v0002'] = true, - ['ghost channel.v0001'] = true, - ['gods domain.v0001'] = true, - ['gods of war.v0001'] = true, - ['gods of war.v0002'] = true, - ['gold rush'] = true, - ['goodlands_8p.v0001'] = true, - ['goodlands_8p.v0002'] = true, - ['goodlands_8p.v0003'] = true, - ['goodlands_8p.v0004'] = true, - ['goodlands_8p.v0005'] = true, - ['gords_islands'] = true, - ['great wall - v1'] = true, - ['great wall - v2'] = true, - ['greenfields.v0001'] = true, - ['greenfields.v0002'] = true, - ['green_island_v1'] = true, - ['green_island_v1b'] = true, - ['green_island_v1c'] = true, - ['green_island_v1d'] = true, - ['green_island_v1e'] = true, - ['green_island_v1f'] = true, - ['green_island_v1g'] = true, - ['green_island_v1g.v0002'] = true, - ['green_island_v1h'] = true, - ['green_island_v1i'] = true, - ['green_island_v1k'] = true, - ['greenlight4v4(7)'] = true, - ['greenlight desert war (4v4)'] = true, - ['green_lua_war'] = true, - ['green volcano'] = true, - ['green volcano v1'] = true, - ['griffin_iv'] = true, - ['ground zero.v0001'] = true, - ['halogyn.v0001'] = true, - ['handicapable_faf'] = true, - ['handicapable_faf_v2'] = true, - ['handicapable_faf_v2.v0002'] = true, - ['hantompalms.v0001'] = true, - ['havenofwar'] = true, - ['havenofwar.v0001'] = true, - ['heimat'] = true, - ['heimat.v0002'] = true, - ['hellfire'] = true, - ['helms klamm.v0001'] = true, - ['helms klamm.v0002'] = true, - ['hibou.v0001'] = true, - ['hidden fortress.v0001'] = true, - ['hidden fortress.v0002'] = true, - ['high noon 3vs3 v1'] = true, - ['hills of endless empires.v0005'] = true, - ['hills of endless empires.v0006'] = true, - ['hilly jungle'] = true, - ['hilly jungle 2'] = true, - ['hilly_plateau.v0001'] = true, - ['hipu1.v0001'] = true, - ['hrungdaks canyon rage.v0001'] = true, - ['human_normal.v0002'] = true, - ['human_normal.v0003'] = true, - ['human_normal.v0004'] = true, - ['human_normal.v0005'] = true, - ['human_normal.v0006'] = true, - ['human_normal.v0007'] = true, - ['human_normal.v0008'] = true, - ['human.v0001'] = true, - ['human.v0002'] = true, - ['human.v0003'] = true, - ['human.v0004'] = true, - ['human.v0005'] = true, - ['human.v0006'] = true, - ['hunters'] = true, - ['huntersv1'] = true, - ['huntersv2'] = true, - ['hypothermia.v0001'] = true, - ['hypothermia.v0002'] = true, - ['ians cross 4vs4 v2'] = true, - ["ian's river.v0001"] = true, - ['illars of creation.v0001'] = true, - ['iniustus.v0001'] = true, - ['iniustus.v0002'] = true, - ['iniustus.v0003'] = true, - ['iniustus.v0004'] = true, - ['ino'] = true, - ['interior knife fight.v0001'] = true, - ['into the trees.v0001'] = true, - ['island zero 3vs3.v0001'] = true, - ['islehopping.v0001'] = true, - ['islehopping.v0002'] = true, - ['iwojima.v0001'] = true, - ['jackarena256a.v0001'] = true, - ['jip jip cacauette.v0001'] = true, - ['jip jip cacauette v2.v0001'] = true, - ['joejas bloody hole'] = true, - ['joejas bloody hole v2'] = true, - ['judgement.v0001'] = true, - ['julia.v0002'] = true, - ['junction'] = true, - ['jungle v2 teamplayai'] = true, - ['jungle valley'] = true, - ['jungle valley 2vs2'] = true, - ['jungle valley 2vs2 v4'] = true, - ['jungle valley.v0003'] = true, - ['jungle valley.v0004'] = true, - ['kaasmap.v0001'] = true, - ['karte 2.v0001'] = true, - ['kattara.v0001'] = true, - ['kazam'] = true, - ['kazam.v0001'] = true, - ['keepvogel_map.v0001'] = true, - ['keepvogel_map.v0002'] = true, - ['keepvogel_map.v0003'] = true, - ['killamjo'] = true, - ['kill box alpha.v0001'] = true, - ['kill box alpha.v0002'] = true, - ['kingofthehill.v0001'] = true, - ['kings dominion.v0001'] = true, - ['kings dominion.v0002'] = true, - ['kings dominion.v0004'] = true, - ['kings dominion.v0005'] = true, - ['kings dominion.v0007'] = true, - ['kmb lost paradise.v0001'] = true, - ['kmb scrapyard.v0001'] = true, - ['koth v3.v0001'] = true, - ['lab wars'] = true, - ['labyrinth'] = true, - ['labyrinth-rightway.v0001'] = true, - ['labyrinth v2'] = true, - ['last exile.v0001'] = true, - ['lej_nair'] = true, - ['lej_nair.v0001'] = true, - ['lets settle this.v0001'] = true, - ['liberation.v0001'] = true, - ['liberation.v0002'] = true, - ['liberation.v0003'] = true, - ['limited resources.v0001'] = true, - ['limited resources.v0002'] = true, - ['lms 8 player.v0001'] = true, - ['lms 8 player v3.v0001'] = true, - ['lms 8 player v4.v0001'] = true, - ['loki.v0001'] = true, - ['loki.v0002'] = true, - ['london_38152ad.v0001'] = true, - ['lost island ii.v0001'] = true, - ['lost island ii.v0002'] = true, - ['lost paradise ii.v0001'] = true, - ['lost paradise ii.v0002'] = true, - ['lost paradise ii v4'] = true, - ['lost paradise ii v4.v0002'] = true, - ['lost paradise ii v5'] = true, - ['losttemple.v0001'] = true, - ['losttemple.v0002'] = true, - ['low tide'] = true, - ['luna.v0001'] = true, - ['madness -1-'] = true, - ['madness -2-'] = true, - ['madness -3-'] = true, - ['madness -5-'] = true, - ['madness -5- v2'] = true, - ['maelstrom.v0001'] = true, - ['maelstrom.v0002'] = true, - ['maelstrom.v0003'] = true, - ['malazgirt.v0001'] = true, - ['mangrove swamps'] = true, - ['mars_nilokeras'] = true, - ['mass is allmighty v3.v0001'] = true, - ['massless'] = true, - ['massless v2'] = true, - ['mdlmdl.v0001'] = true, - ['mesaii_v1_sli-fox'] = true, - ['mesa plains3v3'] = true, - ['mesa plains3v3.v0001'] = true, - ['mesa plains3v3.v0002'] = true, - ['mesa plains.v0002'] = true, - ['messy.v0001'] = true, - ['messy.v0002'] = true, - ['metal heck.v0001'] = true, - ['metal heck.v0002'] = true, - ['metal heck.v0003'] = true, - ['metal_wartime'] = true, - ['metal_wartimev2'] = true, - ['metal_wartimev3'] = true, - ['meteorfa.v0001'] = true, - ['meteorfa_v2.v0001'] = true, - ['mex island v1'] = true, - ['middle ground.v0001'] = true, - ['midgar'] = true, - ['military camp'] = true, - ['military camp v2'] = true, - ['military camp v4'] = true, - ['minitabula v2'] = true, - ['miraflores.v0001'] = true, - ['mirafloresv2.v0001'] = true, - ['mond reno v1.v0001'] = true, - ['mordor'] = true, - ['mordor_v2'] = true, - ['mountainbreakpass'] = true, - ['mountainbreakpassv2'] = true, - ['mountain clearing v1'] = true, - ['mountain clearing v2'] = true, - ['mountain colony'] = true, - ['mountain colony v2'] = true, - ['mountain colony v3'] = true, - ['mountain_lake_small.v0001'] = true, - ['mountain_lakes.v0001'] = true, - ['mountain pass'] = true, - ['mountain range'] = true, - ['mountains of mass.v0001'] = true, - ['multimum.v0001'] = true, - ['mururoa'] = true, - ['mururoa 2v2v2'] = true, - ['naval is pretty good'] = true, - ['navarone.v0001'] = true, - ['navy wars'] = true, - ['navy wars v2'] = true, - ['neptuna'] = true, - ['neptuna v2'] = true, - ['new mexiko.v0001'] = true, - ['new wonder'] = true, - ['new wonder.v0002'] = true, - ['nfbbs-islands.v0001'] = true, - ['nfbbs-islands.v0002'] = true, - ['nfbbs-islands.v0003'] = true, - ['nfbbs-islands.v0004'] = true, - ['northandsouth.v0001'] = true, - ['o8o_road_to_vega.v0001'] = true, - ['oblivionmountain'] = true, - ['oblivionmountain.v0001'] = true, - ['oblivionmountain.v0002'] = true, - ['ocean divides.v0001'] = true, - ['ocean divides_v2.v0001'] = true, - ['ocean skirmish.v0001'] = true, - ['ocean skirmish.v0002'] = true, - ['ocean skirmish.v0003'] = true, - ['ocean skirmish.v0004'] = true, - ['ocean skirmish.v0005'] = true, - ['ocean wars1.v0001'] = true, - ['ocean wars1.v0002'] = true, - ['octal.v0001'] = true, - ['octiclops.v0001'] = true, - ['octiclops.v0002'] = true, - ['octoclops.v0001'] = true, - ['octopus_hill.v0001'] = true, - ['ofdagam'] = true, - ['ointless dirt.v0001'] = true, - ['okers_hideout.v0001'] = true, - ['omax against the world.v0001'] = true, - ['omax against the world.v0002'] = true, - ['omax total war.v0001'] = true, - ['omega 3 v2.v0001'] = true, - ['omega 3 v2.v0002'] = true, - ['omfgyeti.v0001'] = true, - ['omfgyeti.v0002'] = true, - ['omfgyeti.v0003'] = true, - ['on melancholy hill.v0001'] = true, - ['on melancholy hillv2'] = true, - ['open combat.v0001'] = true, - ['open combat.v0002'] = true, - ['opener_palms'] = true, - ['opener_palmsv2'] = true, - ['opener_palmsv3'] = true, - ['opener_palmsv4'] = true, - ['open waters.v0001'] = true, - ['open zeta.v0001'] = true, - ['option zero dfns ntwrk'] = true, - ['os2.v0001'] = true, - ['os2.v0002'] = true, - ['os2.v0003'] = true, - ['otomar.v0001'] = true, - ['ousai mountains'] = true, - ['ousai mountains v2'] = true, - ['outpost 7.v0002'] = true, - ['ozone_islands_v4'] = true, - ['ozon plans.v0001'] = true, - ['ozon plans v2.v0001'] = true, - ['painted desert.v0001'] = true, - ['palacedefensexd_v1'] = true, - ['paradise lost.v0001'] = true, - ['paraphet'] = true, - ['paths of betrayal.v0001'] = true, - ['pelsedu ffa'] = true, - ['permafrost pass.v0001'] = true, - ['phantom chasma.v0001'] = true, - ['phantom chasma.v0002'] = true, - ['phantom island.v0001'] = true, - ['phantom island.v0002'] = true, - ['phantom island.v0004'] = true, - ['phantom roanoke abyss v1.v0001'] = true, - ['phantoms-hideout.v0001'] = true, - ['phantom_sung_island.v0001'] = true, - ['phantom_sung_island.v0002'] = true, - ['phenom spartiate'] = true, - ["phil's duel.v0001"] = true, - ['pizza.v0001'] = true, - ['plains and passes.v0001'] = true, - ['plains_monkeh.v0001'] = true, - ['plateau_small.v0001'] = true, - ['plateaus_small.v0001'] = true, - ['platoon'] = true, - ['point of reach.v0001'] = true, - ['point of reach v2.v0001'] = true, - ['point of reach v3.v0001'] = true, - ['pond.v0001'] = true, - ['pond.v0002'] = true, - ['potato smasher two'] = true, - ['prime sea'] = true, - ['projekt_i.v0001'] = true, - ['python'] = true, - ['pythonv1'] = true, - ['pythonv2'] = true, - ['pythonv3'] = true, - ['quantumsea.v0001'] = true, - ['rap of gohan v3'] = true, - ['red canyon.v0001'] = true, - ['reds 3 v 3.v0001'] = true, - ['regicide'] = true, - ['regicide.v0002'] = true, - ['regicide.v0003'] = true, - ['regicide.v0004'] = true, - ['regor_vi_highlands.v0001'] = true, - ['regor_vi_highlands.v0002'] = true, - ['regor_vi_highlands.v0003'] = true, - ['regor_vi_highlands.v0004'] = true, - ['regor_vi_highlands.v0005'] = true, - ['relics of time'] = true, - ['resistance.v0001'] = true, - ['resistancev2.v0001'] = true, - ['resistancev3m.v0001'] = true, - ['resistancev3.v0001'] = true, - ['resistancev4.v0001'] = true, - ['revenge of the cybrans.v0001'] = true, - ['revenge of the cybrans.v0002'] = true, - ['revenge of the cybrans.v0003'] = true, - ['ridge iii.v0001'] = true, - ['ridge iii.v0002'] = true, - ['ridge iii.v0003'] = true, - ['riptide.v0001'] = true, - ['riptide.v0002'] = true, - ['riptide.v0003'] = true, - ['riptide.v0004'] = true, - ['river of death.v0001'] = true, - ['riverside'] = true, - ['riverside v2'] = true, - ['rockstone_mine.v0001'] = true, - ['rockstone_mine.v0002'] = true, - ['roject 4.v0001'] = true, - ['rushgo'] = true, - ['rush islandv4beta.v0001'] = true, - ['rush islandv4beta.v0002'] = true, - ['rush islandv4beta.v0003'] = true, - ['rush me green'] = true, - ['rush me green v2'] = true, - ['rush me if u can.v0001'] = true, - ['salt_v2.v0002'] = true, - ['sands of ablicka.v0001'] = true, - ['sands of war'] = true, - ['sands of war v2'] = true, - ['sands of war v3'] = true, - ['sands of war v4'] = true, - ['sands of war v5'] = true, - ['saracuse.v0001'] = true, - ['saracuse.v0002'] = true, - ['sathira beach'] = true, - ['scattered land.v0001'] = true, - ['scattered land.v0002'] = true, - ['scattered redhills.v0001'] = true, - ['scca_coop_a01.v0004'] = true, - ['scca_coop_a01.v0005'] = true, - ['scca_coop_a01.v0006'] = true, - ['scca_coop_a01.v0007'] = true, - ['scca_coop_a01.v0008'] = true, - ['scca_coop_a01.v0009'] = true, - ['scca_coop_a01.v0010'] = true, - ['scca_coop_a01_v01'] = true, - ['scca_coop_a01_v02'] = true, - ['scca_coop_a02.v0004'] = true, - ['scca_coop_a02.v0005'] = true, - ['scca_coop_a02.v0006'] = true, - ['scca_coop_a02.v0007'] = true, - ['scca_coop_a02.v0008'] = true, - ['scca_coop_a02.v0009'] = true, - ['scca_coop_a02.v0010'] = true, - ['scca_coop_a02.v0011'] = true, - ['scca_coop_a02.v0012'] = true, - ['scca_coop_a02.v0013'] = true, - ['scca_coop_a02.v0014'] = true, - ['scca_coop_a02_v01'] = true, - ['scca_coop_a02_v02'] = true, - ['scca_coop_a03.v0004'] = true, - ['scca_coop_a03.v0005'] = true, - ['scca_coop_a03.v0006'] = true, - ['scca_coop_a03.v0007'] = true, - ['scca_coop_a03.v0008'] = true, - ['scca_coop_a03.v0009'] = true, - ['scca_coop_a03.v0010'] = true, - ['scca_coop_a03.v0011'] = true, - ['scca_coop_a03.v0012'] = true, - ['scca_coop_a03.v0013'] = true, - ['scca_coop_a03.v0014'] = true, - ['scca_coop_a03.v0015'] = true, - ['scca_coop_a03_v01'] = true, - ['scca_coop_a03_v02'] = true, - ['scca_coop_a04.v0005'] = true, - ['scca_coop_a04.v0006'] = true, - ['scca_coop_a04.v0007'] = true, - ['scca_coop_a04.v0008'] = true, - ['scca_coop_a04.v0009'] = true, - ['scca_coop_a04.v0010'] = true, - ['scca_coop_a04.v0011'] = true, - ['scca_coop_a04.v0012'] = true, - ['scca_coop_a04.v0013'] = true, - ['scca_coop_a04.v0014'] = true, - ['scca_coop_a04_v01'] = true, - ['scca_coop_a04_v02'] = true, - ['scca_coop_a04_v03'] = true, - ['scca_coop_a05.v0004'] = true, - ['scca_coop_a05.v0005'] = true, - ['scca_coop_a05.v0006'] = true, - ['scca_coop_a05.v0007'] = true, - ['scca_coop_a05.v0008'] = true, - ['scca_coop_a05.v0009'] = true, - ['scca_coop_a05.v0010'] = true, - ['scca_coop_a05.v0011'] = true, - ['scca_coop_a05.v0012'] = true, - ['scca_coop_a05.v0013'] = true, - ['scca_coop_a05.v0014'] = true, - ['scca_coop_a05.v0015'] = true, - ['scca_coop_a05_v01'] = true, - ['scca_coop_a05_v02'] = true, - ['scca_coop_a06.v0004'] = true, - ['scca_coop_a06.v0005'] = true, - ['scca_coop_a06.v0006'] = true, - ['scca_coop_a06.v0007'] = true, - ['scca_coop_a06.v0008'] = true, - ['scca_coop_a06.v0009'] = true, - ['scca_coop_a06.v0010'] = true, - ['scca_coop_a06.v0011'] = true, - ['scca_coop_a06.v0012'] = true, - ['scca_coop_a06.v0013'] = true, - ['scca_coop_a06.v0014'] = true, - ['scca_coop_a06.v0015'] = true, - ['scca_coop_a06_v01'] = true, - ['scca_coop_a06_v02'] = true, - ['scca_coop_e01.v0005'] = true, - ['scca_coop_e01.v0006'] = true, - ['scca_coop_e01.v0007'] = true, - ['scca_coop_e01.v0008'] = true, - ['scca_coop_e01.v0009'] = true, - ['scca_coop_e01.v0010'] = true, - ['scca_coop_e01.v0011'] = true, - ['scca_coop_e01.v0012'] = true, - ['scca_coop_e01.v0013'] = true, - ['scca_coop_e01.v0014'] = true, - ['scca_coop_e01.v0015'] = true, - ['scca_coop_e01.v0016'] = true, - ['scca_coop_e01_v01'] = true, - ['scca_coop_e01_v02'] = true, - ['scca_coop_e01_v03'] = true, - ['scca_coop_e02.v0003'] = true, - ['scca_coop_e02.v0004'] = true, - ['scca_coop_e02.v0005'] = true, - ['scca_coop_e02.v0006'] = true, - ['scca_coop_e02.v0007'] = true, - ['scca_coop_e02.v0008'] = true, - ['scca_coop_e02.v0009'] = true, - ['scca_coop_e02.v0010'] = true, - ['scca_coop_e02.v0011'] = true, - ['scca_coop_e02.v0012'] = true, - ['scca_coop_e02.v0013'] = true, - ['scca_coop_e02_v01'] = true, - ['scca_coop_e03.v0004'] = true, - ['scca_coop_e03.v0005'] = true, - ['scca_coop_e03.v0006'] = true, - ['scca_coop_e03.v0007'] = true, - ['scca_coop_e03.v0008'] = true, - ['scca_coop_e03.v0009'] = true, - ['scca_coop_e03.v0010'] = true, - ['scca_coop_e03.v0011'] = true, - ['scca_coop_e03.v0012'] = true, - ['scca_coop_e03.v0013'] = true, - ['scca_coop_e03.v0014'] = true, - ['scca_coop_e03.v0015'] = true, - ['scca_coop_e03_v01'] = true, - ['scca_coop_e03_v02'] = true, - ['scca_coop_e04.v0004'] = true, - ['scca_coop_e04.v0005'] = true, - ['scca_coop_e04.v0006'] = true, - ['scca_coop_e04.v0007'] = true, - ['scca_coop_e04.v0008'] = true, - ['scca_coop_e04.v0009'] = true, - ['scca_coop_e04.v0010'] = true, - ['scca_coop_e04.v0011'] = true, - ['scca_coop_e04.v0012'] = true, - ['scca_coop_e04.v0013'] = true, - ['scca_coop_e04.v0014'] = true, - ['scca_coop_e04_v01'] = true, - ['scca_coop_e04_v02'] = true, - ['scca_coop_e05.v0002'] = true, - ['scca_coop_e05.v0003'] = true, - ['scca_coop_e05.v0004'] = true, - ['scca_coop_e05.v0005'] = true, - ['scca_coop_e05.v0006'] = true, - ['scca_coop_e05.v0007'] = true, - ['scca_coop_e05.v0008'] = true, - ['scca_coop_e05.v0009'] = true, - ['scca_coop_e05.v0010'] = true, - ['scca_coop_e05.v0011'] = true, - ['scca_coop_e05.v0012'] = true, - ['scca_coop_e05.v0013'] = true, - ['scca_coop_e06.v0002'] = true, - ['scca_coop_e06.v0003'] = true, - ['scca_coop_e06.v0004'] = true, - ['scca_coop_e06.v0005'] = true, - ['scca_coop_e06.v0006'] = true, - ['scca_coop_e06.v0007'] = true, - ['scca_coop_e06.v0008'] = true, - ['scca_coop_e06.v0009'] = true, - ['scca_coop_e06.v0010'] = true, - ['scca_coop_e06.v0011'] = true, - ['scca_coop_e06.v0012'] = true, - ['scca_coop_e06.v0013'] = true, - ['scca_coop_e06.v0014'] = true, - ['scca_coop_e06.v0015'] = true, - ['scca_coop_r01.v0002'] = true, - ['scca_coop_r01.v0003'] = true, - ['scca_coop_r01.v0004'] = true, - ['scca_coop_r01.v0005'] = true, - ['scca_coop_r01.v0006'] = true, - ['scca_coop_r01.v0007'] = true, - ['scca_coop_r01.v0008'] = true, - ['scca_coop_r01.v0009'] = true, - ['scca_coop_r01.v0010'] = true, - ['scca_coop_r01.v0011'] = true, - ['scca_coop_r01.v0012'] = true, - ['scca_coop_r02.v0002'] = true, - ['scca_coop_r02.v0003'] = true, - ['scca_coop_r02.v0004'] = true, - ['scca_coop_r02.v0005'] = true, - ['scca_coop_r02.v0006'] = true, - ['scca_coop_r02.v0007'] = true, - ['scca_coop_r02.v0008'] = true, - ['scca_coop_r02.v0009'] = true, - ['scca_coop_r02.v0010'] = true, - ['scca_coop_r02.v0011'] = true, - ['scca_coop_r02.v0012'] = true, - ['scca_coop_r03.v0002'] = true, - ['scca_coop_r03.v0003'] = true, - ['scca_coop_r03.v0004'] = true, - ['scca_coop_r03.v0005'] = true, - ['scca_coop_r03.v0006'] = true, - ['scca_coop_r03.v0007'] = true, - ['scca_coop_r03.v0008'] = true, - ['scca_coop_r03.v0009'] = true, - ['scca_coop_r03.v0010'] = true, - ['scca_coop_r03.v0011'] = true, - ['scca_coop_r03.v0012'] = true, - ['scca_coop_r04.v0001'] = true, - ['scca_coop_r04.v0002'] = true, - ['scca_coop_r04.v0003'] = true, - ['scca_coop_r04.v0004'] = true, - ['scca_coop_r04.v0005'] = true, - ['scca_coop_r04.v0006'] = true, - ['scca_coop_r04.v0007'] = true, - ['scca_coop_r04.v0008'] = true, - ['scca_coop_r04.v0009'] = true, - ['scca_coop_r04.v0010'] = true, - ['scca_coop_r04.v0011'] = true, - ['scca_coop_r04.v0012'] = true, - ['scca_coop_r05.v0001'] = true, - ['scca_coop_r05.v0002'] = true, - ['scca_coop_r05.v0003'] = true, - ['scca_coop_r05.v0004'] = true, - ['scca_coop_r05.v0005'] = true, - ['scca_coop_r05.v0006'] = true, - ['scca_coop_r05.v0007'] = true, - ['scca_coop_r05.v0008'] = true, - ['scca_coop_r05.v0009'] = true, - ['scca_coop_r05.v0010'] = true, - ['scca_coop_r05.v0011'] = true, - ['scca_coop_r05.v0012'] = true, - ['scca_coop_r06.v0001'] = true, - ['scca_coop_r06.v0002'] = true, - ['scca_coop_r06.v0003'] = true, - ['scca_coop_r06.v0004'] = true, - ['scca_coop_r06.v0005'] = true, - ['scca_coop_r06.v0006'] = true, - ['scca_coop_r06.v0007'] = true, - ['scca_coop_r06.v0008'] = true, - ['scca_coop_r06.v0009'] = true, - ['scca_coop_r06.v0010'] = true, - ['scca_coop_r06.v0011'] = true, - ['scca_coop_r06.v0012'] = true, - ['scfa_highlands lake'] = true, - ['scfa_highlands lake v2'] = true, - ['scfa_highlands lake v3'] = true, - ['scfa_highlands lake v4'] = true, - ['scfa_highlands lake v5'] = true, - ['scfa_highlands lake v6'] = true, - ['scmp-00123'] = true, - ['scmp_002593.v0001'] = true, - ['scmp_002593.v0002'] = true, - ['scmp_0025- cursed island'] = true, - ['scmp_0054.v0001'] = true, - ['scmp_0058.v0001'] = true, - ['scmp_012_ai.v0001'] = true, - ['scmp_015expand_ai.v0001'] = true, - ['scmp_015.v0001'] = true, - ['scmp_017_3v3.v0001'] = true, - ['scmp_019_3v3.v0001'] = true, - ['scmp_041'] = true, - ['scmp_044 - buttes of war.v0003'] = true, - ['scmp_044 - buttes of war.v0004'] = true, - ['scmp_088_87.v0001'] = true, - ['scmp_099.v0001'] = true, - ['scmp_33_667.v0001'] = true, - ['scmp_equinox'] = true, - ['scmp_haz09.v0001'] = true, - ['scmp_ichi01.v0001'] = true, - ['scmp_ichi01.v0002'] = true, - ['scmp_ir009.v0001'] = true, - ['scmp_libra3.v0001'] = true, - ['scmp_lvwtwinridge.v0001'] = true, - ['scmp_mp004 - defence_6p.v0001'] = true, - ['scmp_mvcx_03.v0001'] = true, - ['scmp_mvcx_05'] = true, - ['scmp_white and mountain.v0001'] = true, - ['scmp_wild_finn3v3_2.v0001'] = true, - ['scoobysnacks'] = true, - ['scoobysnacksv01'] = true, - ['scoobysnacksv02'] = true, - ['scrap yard.v0001'] = true, - ['scrap yard.v0002'] = true, - ['sc v1.v0001'] = true, - ['seas of orion.v0001'] = true, - ['sector x-54.v0001'] = true, - ['sedongs.v0001'] = true, - ["segfaulthunter's bridge.v0001"] = true, - ['sentina'] = true, - ['sentina beta v1.v0001'] = true, - ['sentina betav1.v0001'] = true, - ['sentina betav1.v0002'] = true, - ['sentry 2v2 point.v0001'] = true, - ['seraphim mines.v0001'] = true, - ['seraphim ruins.v0001'] = true, - ['serenity desert.v0001'] = true, - ['serenity desert.v0002'] = true, - ['serenity isles.v0001'] = true, - ['setonmetzel.v0001'] = true, - ['setons_1v1'] = true, - ["seton's archipelago 2.v0003"] = true, - ['setons_cauldron_small_v2.v0001'] = true, - ['setons_islands.v0001'] = true, - ['setons shrunken.v0001'] = true, - ['setons shrunken.v0002'] = true, - ['setons shrunken.v0003'] = true, - ['setons shrunken.v0004'] = true, - ['seven islands.v0001'] = true, - ['sh1r3_mycelium2'] = true, - ['shadow of darkness.v0001'] = true, - ['shiftingalliances'] = true, - ['shiftingalliancesv2'] = true, - ['shiftingalliancesv4'] = true, - ['shiftingalliancesv5'] = true, - ['shiftingalliancesv6'] = true, - ['shkebab.v0001'] = true, - ['shloob1.v0001'] = true, - ['shloob1.v0002'] = true, - ['shloob3.v0001'] = true, - ['show down.v0001'] = true, - ['sinai.v0001'] = true, - ['s islands'] = true, - ['s islands 3v3.v0001'] = true, - ['s islands 3v3.v0002'] = true, - ['s islands 3v3.v0003'] = true, - ['s islands 3v3.v0004'] = true, - ['s islands 3v3.v0005'] = true, - ['s islands 4v4.v00010'] = true, - ['s islands 4v4.v00011'] = true, - ['s islands 4v4.v00012'] = true, - ['s islands 4v4.v00013'] = true, - ['s islands 4v4.v00014'] = true, - ['s islands 4v4.v00015'] = true, - ['s islands 4v4.v00016'] = true, - ['s islands 4v4.v00017'] = true, - ['s islands 4v4.v00018'] = true, - ['s islands 4v4.v00019'] = true, - ['s islands 4v4.v0008'] = true, - ['s islands 4v4.v0009'] = true, - ['s islands.v0001'] = true, - ['s islands.v00010'] = true, - ['s islands.v00011'] = true, - ['s islands.v00012'] = true, - ['s islands.v00013'] = true, - ['s islands.v00014'] = true, - ['s islands.v00015'] = true, - ['s islands.v00016'] = true, - ['s islands.v00017'] = true, - ['s islands.v00018'] = true, - ['s islands.v00019'] = true, - ['s islands.v0002'] = true, - ['s islands.v00020'] = true, - ['s islands.v00021'] = true, - ['s islands.v00022'] = true, - ['s islands.v00023'] = true, - ['s islands.v00024'] = true, - ['s islands.v00025'] = true, - ['s islands.v00026'] = true, - ['s islands.v00027'] = true, - ['s islands.v00028'] = true, - ['s islands.v00029'] = true, - ['s islands.v0003'] = true, - ['s islands.v00030'] = true, - ['s islands.v00031'] = true, - ['s islands.v00032'] = true, - ['s islands.v00033'] = true, - ['s islands.v0004'] = true, - ['s islands.v0005'] = true, - ['s islands.v0007'] = true, - ['s islands.v0008'] = true, - ['s islands.v0009'] = true, - ['sixth stone.v0001'] = true, - ['sleipnir'] = true, - ['sleipnir.v0001'] = true, - ['sleipnir.v0002'] = true, - ['sliny1.v0001'] = true, - ["slithkiller's revenge.v0001"] = true, - ["slithkiller's revenge.v0002"] = true, - ["slithkiller's revenge.v0003"] = true, - ["slithkiller's revenge.v0004"] = true, - ["slithkiller's revenge.v0005"] = true, - ['sludge2'] = true, - ['sludge beta'] = true, - ['sludge beta v1'] = true, - ['snow ffa'] = true, - ['snow ffa v2'] = true, - ['snowy mountains2.v0001'] = true, - ['snowy mountains.v0001'] = true, - ['snowy mountains v2.v0001'] = true, - ['solo acu.v0001'] = true, - ['solo acu.v0002'] = true, - ['solo acu.v0003'] = true, - ['solo acu.v0004'] = true, - ['souvenir'] = true, - ['springfield.v0001'] = true, - ['stairs'] = true, - ['stella maris.v0001'] = true, - ['straitofsorrows.v0001'] = true, - ['strategic islands.v0001'] = true, - ['strock valley.v0001'] = true, - ['sudden stop.v0001'] = true, - ['sulphur mounds.v0001'] = true, - ['summer duel.v0001'] = true, - ['summer of thermopylae'] = true, - ['summervacation'] = true, - ['summervswinter'] = true, - ['summervswinter v2'] = true, - ['summervswinter v3'] = true, - ['supreme_aode.v0001'] = true, - ['supreme_aod.v0001'] = true, - ['supreme_aod.v0002'] = true, - ['supreme_aod.v0003'] = true, - ['supreme defence.v0001'] = true, - ['supreme defence.v0002'] = true, - ['supreme defence.v0003'] = true, - ['supreme defence.v0004'] = true, - ['supreme defence.v0005'] = true, - ['supreme_firefly.v0001'] = true, - ['supreme_firefly.v0002'] = true, - ['survival_5thdimension.v0001'] = true, - ['survival_campaign_v1c.v0001'] = true, - ['survival_campaign_v1c.v0002'] = true, - ['survival_campaign_v1e.v0001'] = true, - ['survival_campaign_v1.v0001'] = true, - ['survival_escape.v0001'] = true, - ['survival_escape_v2.v0001'] = true, - ['survival_escape_v3.v0001'] = true, - ['survival_extremedesertpatrick10.v0001'] = true, - ['survival_extremedesert.v0001'] = true, - ['survival_extremedesertv10.v0001'] = true, - ['survival_extremedesertv10.v0002'] = true, - ['survival_extremedesertv10.v0003'] = true, - ['survival_extremedesertv2.v0001'] = true, - ['survival_extremenavyv2.v0001'] = true, - ['survival_northsouth_barrens.v0001'] = true, - ['survival_northsouth_barrens.v0002'] = true, - ['survival run.v0001'] = true, - ['survival run v2.v0001'] = true, - ['survival run v2.v0002'] = true, - ['survival run v3.v0001'] = true, - ['survival run v4.v0003'] = true, - ['survival run v6.v0001'] = true, - ['survival run v7.v0001'] = true, - ['tabula rasa.v0001'] = true, - ['tabula rasa v3.v0001'] = true, - ['tactical barricade.v0001'] = true, - ['tactical barricade.v0002'] = true, - ['tactical barricade.v0003'] = true, - ['take it all.v0001'] = true, - ['talrasi.v0001'] = true, - ['talrasi.v0002'] = true, - ['talrasiv2.v0001'] = true, - ['tamp gods of war.v0012'] = true, - ['ta naval arena.v0001'] = true, - ['tar isle.v0001'] = true, - ['tar of death.v0001'] = true, - ['team_wars.v0001'] = true, - ['templemental.v0001'] = true, - ['termitesvsfourmis.v0001'] = true, - ['test1'] = true, - ['test1.v0001'] = true, - ['test3.v0001'] = true, - ['test3.v0002'] = true, - ['test4.v0001'] = true, - ['test4.v0002'] = true, - ['test4.v0003'] = true, - ['teste bata.v0001'] = true, - ['teste bata.v0002'] = true, - ['testicle.v0001'] = true, - ['thargorum2.v0001'] = true, - ['the alps.v0001'] = true, - ['the alpsv2.v0001'] = true, - ['the broken bridge'] = true, - ['the broken bridge v2'] = true, - ['thecapeofgoodhope'] = true, - ['thecapeofgoodhopev2'] = true, - ['the drunken pirates dance'] = true, - ['the fall of dawn.v0001'] = true, - ['the fall of dawn_v3.v0001'] = true, - ['the fall of dawn_v4.v0001'] = true, - ['the fall of dawn_v4.v0002'] = true, - ['the fall of dawn_v6.v0002'] = true, - ['the fall of dawn_v8.v0001'] = true, - ['the gathering.v0001'] = true, - ['the great plateau.v0001'] = true, - ['thehighroad.v0001'] = true, - ['the hunters.v0001'] = true, - ['the islands lost v1.v0001'] = true, - ['the longest way.v0001'] = true, - ['the longest way.v0002'] = true, - ['the longest way.v0003'] = true, - ['the maze'] = true, - ['themeteorv2'] = true, - ['the_netherlands_v2'] = true, - ['the new rush me more.v0001'] = true, - ['the old battle zone 1.0.v0001'] = true, - ['the old battle zone 1.0.v0002'] = true, - ['the pass.v0001'] = true, - ['the pass v3.v0001'] = true, - ['the pass v4.v0001'] = true, - ['the pit.v0001'] = true, - ['the potato smasher'] = true, - ['the river.v0001'] = true, - ['thermopylae v3.v0003'] = true, - ['theta passage 2.v0001'] = true, - ['the trench.v0001'] = true, - ['the trench.v0002'] = true, - ['the trench.v0003'] = true, - ['throne'] = true, - ['throne v2'] = true, - ['throne v3'] = true, - ['tidra.v0002'] = true, - ['tidra.v0003'] = true, - ['tinysteps.v0001'] = true, - ['tinysteps.v0002'] = true, - ['titan'] = true, - ['titan final'] = true, - ['titan final.v0004'] = true, - ['titan v2'] = true, - ['titan v2b'] = true, - ['tkp_fusion'] = true, - ['tkp_mountain_peaks'] = true, - ['tkp_rings'] = true, - ['tkp_team_game'] = true, - ['tokota.v0001'] = true, - ['to pass'] = true, - ['tpwmp_01.v0001'] = true, - ['trade wars.v0001'] = true, - ['trashed_paradise'] = true, - ['trash land.v0003'] = true, - ['treachery.v0001'] = true, - ['trech.v0001'] = true, - ['trespasser.v0001'] = true, - ['triangle death.v0001'] = true, - ['tributaries.v0001'] = true, - ['tributaries.v0002'] = true, - ['tributaries.v0003'] = true, - ['tributaries.v0004'] = true, - ['tributaries.v0005'] = true, - ['tributaries.v0006'] = true, - ['tributaries.v0007'] = true, - ['triple t.v0001'] = true, - ['triple t v2.v0001'] = true, - ['triple t v4.v0001'] = true, - ['triple t v5.v0001'] = true, - ['triple t v6.v0001'] = true, - ['triple t v7.v0001'] = true, - ['triumvirate v1.02.v0001'] = true, - ['tropical dunes.v0001'] = true, - ['tropical lake'] = true, - ['tropical lake.v0001'] = true, - ['tropical lake.v0002'] = true, - ['tropical lake.v0003'] = true, - ['tropical lake.v0004'] = true, - ['tropical lake.v0005'] = true, - ['tropical lake.v0006'] = true, - ['tropical lake.v0007'] = true, - ['tropical paradise.v0001'] = true, - ['trufflesfastest.v0001'] = true, - ['tsg'] = true, - ['t sharp quadrent.v0001'] = true, - ['t sharp quadrent.v0002'] = true, - ['tucon.v0001'] = true, - ['tucon.v0002'] = true, - ['tuggo'] = true, - ['tuggo v1'] = true, - ['tuggo v2'] = true, - ['tug_of_war.v0003'] = true, - ['tug_of_war.v0004'] = true, - ['tug_of_war.v0005'] = true, - ['tug_of_war.v0006'] = true, - ['tug_of_war.v0007'] = true, - ['tundra test.v0001'] = true, - ['turtlefest.v0001'] = true, - ['turtlers fight.v0001'] = true, - ['twin inlets.v0001'] = true, - ['twin inlets.v0002'] = true, - ['twin inlets.v0003'] = true, - ['twin inlets.v0004'] = true, - ['twin lakes'] = true, - ['twisted fate.v0001'] = true, - ['twisted fate.v0002'] = true, - ['twisted fate.v0003'] = true, - ['twocorners.v0001'] = true, - ['twolakes.v0004'] = true, - ['two sides.v0001'] = true, - ['twospikes.v0001'] = true, - ['two towers.v0001'] = true, - ['uef-airport'] = true, - ['umbrapassage'] = true, - ['upreme_carnage.v0001'] = true, - ['upreme_carnage.v0002'] = true, - ['upremerisk.v0001'] = true, - ['upremerisk.v0002'] = true, - ['upremerisk.v0003'] = true, - ['upremerisk.v0004'] = true, - ['upremerisk.v0005'] = true, - ['upremerisk.v0006'] = true, - ['upremerisk.v0007'] = true, - ['urvival_vs_4player.v0001'] = true, - ['valley of death and dark.v0001'] = true, - ['valley of death and dark.v0002'] = true, - ['valley of sorrow.v0001'] = true, - ['vally.v0001'] = true, - ['veil of stars'] = true, - ['veil of stars v2'] = true, - ['venom.v0001'] = true, - ['venom.v0002'] = true, - ['venom.v0003'] = true, - ['venom.v0004'] = true, - ['venom.v0005'] = true, - ['virgo pass'] = true, - ['virgo pass v2'] = true, - ['virgo pass v3'] = true, - ['virgo pass v4'] = true, - ['virgo pass v5'] = true, - ['volcano 3v3 +.v0001'] = true, - ['volcano 3v3 +.v0002'] = true, - ['volcano 3v3 +.v0003'] = true, - ['volcano islands.v0001'] = true, - ['volcanov1.v0001'] = true, - ['volcanov2.v0001'] = true, - ['voodoo acu battle v1'] = true, - ['voodoo acu battle v1b'] = true, - ['voodoo acu battle v2'] = true, - ['voodoo base v1'] = true, - ['voodoo base v2'] = true, - ['voodoo base v3'] = true, - ['voodoo base v4'] = true, - ['voodoo base v5'] = true, - ['voodoo base v6'] = true, - ['voodoo base v7'] = true, - ['voodoo base v7b'] = true, - ['voodoo basketball v1'] = true, - ['voodoo battle v1'] = true, - ['voodoo battle v10'] = true, - ['voodoo battle v11'] = true, - ['voodoo battle v12'] = true, - ['voodoo battle v13'] = true, - ['voodoo battle v14'] = true, - ['voodoo battle v2'] = true, - ['voodoo battle v3'] = true, - ['voodoo battle v4'] = true, - ['voodoo battle v5'] = true, - ['voodoo battle v6'] = true, - ['voodoo battle v6a'] = true, - ['voodoo battle v7'] = true, - ['voodoo battle v8'] = true, - ['voodoo battle v9a'] = true, - ['voodoo_castle_ai_v1'] = true, - ['voodoo_castle_v1'] = true, - ['voodoo castle v10'] = true, - ['voodoo castle v11'] = true, - ['voodoo castle v12'] = true, - ['voodoo castle v13'] = true, - ['voodoo castle v13b'] = true, - ['voodoo castle v14'] = true, - ['voodoo castle v15b'] = true, - ['voodoo castle v3'] = true, - ['voodoo castle v4'] = true, - ['voodoo castle v4b'] = true, - ['voodoo castle v4c'] = true, - ['voodoo castle v5'] = true, - ['voodoo castle v6'] = true, - ['voodoo castle v6a'] = true, - ['voodoo castle v7'] = true, - ['voodoo castle v8'] = true, - ['voodoo castle v9'] = true, - ['voodoo dead arena v1'] = true, - ['voodoo dead arena v2'] = true, - ['voodoo dead arena v2a'] = true, - ['voodoo dead zone v2'] = true, - ['voodoo dead zone v2a'] = true, - ['voodoo dead zone v3'] = true, - ['voodoo desert v1'] = true, - ['voodoo doppel thermo v1'] = true, - ['voodoo doppel thermo v2b'] = true, - ['voodoo drakes ravine v1'] = true, - ['voodoo drakes ravine v2b'] = true, - ['voodoo eye v1'] = true, - ['voodoo eye v2'] = true, - ['voodoo eye v2b'] = true, - ['voodoo eye v3'] = true, - ['voodoo football v2'] = true, - ['voodoo hell v1'] = true, - ['voodoo hell v2a'] = true, - ['voodoo hell v3'] = true, - ['voodoo_hurricane v1'] = true, - ['voodoo ian s cross v1'] = true, - ['voodoo ian s cross v2'] = true, - ['voodoo inferno v1'] = true, - ['voodoo inferno v2'] = true, - ['voodoo inferno v3'] = true, - ['voodoo inferno v4'] = true, - ['voodoo isis v1'] = true, - ['voodoo isis v2'] = true, - ['voodoo isis v3'] = true, - ['voodoo isis v4a'] = true, - ['voodoo isis v5'] = true, - ['voodoo isis v6'] = true, - ['voodoo mountains v1'] = true, - ['voodoo mountains v2'] = true, - ['voodoo mountains v3'] = true, - ['voodoo mountains v3b'] = true, - ['voodoo mountains v3c'] = true, - ['voodoo mountains v4'] = true, - ['voodoo mountains v5a'] = true, - ['voodoo mountains v6'] = true, - ['voodoo mountains v6b'] = true, - ['voodoo ocean v1'] = true, - ['voodoo ocean v2a'] = true, - ['voodoo olymp v1b'] = true, - ['voodoo planet v1'] = true, - ['voodoo planet v2b'] = true, - ['voodoo pyramids v1'] = true, - ['voodoo pyramids v2'] = true, - ['voodoo scorpion v1'] = true, - ['voodoo scorpion v2'] = true, - ['voodoo scorpion v2b'] = true, - ['voodoo scorpion v3'] = true, - ['voodoo scorpion v4'] = true, - ['voodoo scorpion v4a'] = true, - ['voodoo temple v1'] = true, - ['voodoo tournament v1'] = true, - ['voodoo tournament v1b'] = true, - ['voodoo tournament v2'] = true, - ['voodoo tournament v3'] = true, - ['voodoo tournament v3b'] = true, - ['voodoo tournament v4'] = true, - ['voodoo tournament v5'] = true, - ['voodoo tournament v5b'] = true, - ['voodoo tournament v6'] = true, - ['voodoo tournament v7'] = true, - ['voodoo_vulcan_v1'] = true, - ['voodoo vulcan v1b'] = true, - ['voodoo_vulcan_v2'] = true, - ['voodoo_vulcan_v3'] = true, - ['voodoo_vulcan_v4'] = true, - ['voodoo vulcan v6'] = true, - ['voodoo water hole v1'] = true, - ['vulcan island.v0001'] = true, - ['vulcan island.v0002'] = true, - ['vulcan island.v0003'] = true, - ['waffle tower defense.v0001'] = true, - ['war at sea.v0001'] = true, - ['war at sea.v0002'] = true, - ['war zone.v0001'] = true, - ['war zone.v0002'] = true, - ['waterearth.v0001'] = true, - ['waters of isis 5v5 v1'] = true, - ['waters of isis 5v5 v2'] = true, - ['waters of isis r001.v0001'] = true, - ['wave_of_death.v0001'] = true, - ['wave_of_death.v0002'] = true, - ['wave_of_death.v0003'] = true, - ['wave_of_death.v0004'] = true, - ['wave_of_death.v0005'] = true, - ['wave_of_death.v0006'] = true, - ['wave_of_death.v0007'] = true, - ['wave_of_death.v0008'] = true, - ['wave_of_death.v0009'] = true, - ['wave_of_death.v0010'] = true, - ['wave_of_death.v0011'] = true, - ['what where and why.v0001'] = true, - ['whitefireislands'] = true, - ['wonder 4v4 v3b'] = true, - ['wonder open 5v5 v2'] = true, - ['wonder.v0001'] = true, - ['wonder.v0002'] = true, - ['wonder.v0004'] = true, - ['wonder water 4v4 v1a'] = true, - ['x1ca_coop_001_.v0016'] = true, - ['x1ca_coop_001.v0018'] = true, - ['x1ca_coop_001.v0019'] = true, - ['x1ca_coop_001_v02'] = true, - ['x1ca_coop_001_v03'] = true, - ['x1ca_coop_001_v04'] = true, - ['x1ca_coop_001_v05'] = true, - ['x1ca_coop_001_v06'] = true, - ['x1ca_coop_001_v07'] = true, - ['x1ca_coop_001_v10'] = true, - ['x1ca_coop_001_v11'] = true, - ['x1ca_coop_001_v12'] = true, - ['x1ca_coop_001_v13'] = true, - ['x1ca_coop_001_v14'] = true, - ['x1ca_coop_002.v0004'] = true, - ['x1ca_coop_002.v0005'] = true, - ['x1ca_coop_002.v0006'] = true, - ['x1ca_coop_002.v0007'] = true, - ['x1ca_coop_002.v0008'] = true, - ['x1ca_coop_002.v0009'] = true, - ['x1ca_coop_002_v03'] = true, - ['x1ca_coop_003.v0003'] = true, - ['x1ca_coop_003.v0004'] = true, - ['x1ca_coop_003.v0005'] = true, - ['x1ca_coop_003.v0006'] = true, - ['x1ca_coop_003.v0007'] = true, - ['x1ca_coop_003.v0008'] = true, - ['x1ca_coop_003.v0009'] = true, - ['x1ca_coop_003.v0010'] = true, - ['x1ca_coop_003.v0011'] = true, - ['x1ca_coop_003_v01'] = true, - ['x1ca_coop_004.v0005'] = true, - ['x1ca_coop_004.v0006'] = true, - ['x1ca_coop_004.v0007'] = true, - ['x1ca_coop_004.v0008'] = true, - ['x1ca_coop_004.v0009'] = true, - ['x1ca_coop_004.v0010'] = true, - ['x1ca_coop_004.v0011'] = true, - ['x1ca_coop_004_v01'] = true, - ['x1ca_coop_004_v02'] = true, - ['x1ca_coop_004_v03'] = true, - ['x1ca_coop_005.v0003'] = true, - ['x1ca_coop_005.v0005'] = true, - ['x1ca_coop_005.v0007'] = true, - ['x1ca_coop_005.v0009'] = true, - ['x1ca_coop_005.v0011'] = true, - ['x1ca_coop_005.v0013'] = true, - ['x1ca_coop_005.v0015'] = true, - ['x1ca_coop_005_v01'] = true, - ['x1ca_coop_006.v0003'] = true, - ['x1ca_coop_006.v0004'] = true, - ['x1ca_coop_006.v0005'] = true, - ['x1ca_coop_006.v0006'] = true, - ['x1ca_coop_006.v0008'] = true, - ['x1ca_coop_006.v0010'] = true, - ['x1ca_coop_006.v0012'] = true, - ['x1ca_coop_006_v01'] = true, - ['x6.v0001'] = true, - ['x6.v0002'] = true, - ['x marks the spot'] = true, - ['x marks the spot v2'] = true, - ['zbalv10.v0001'] = true, - ['zbalv2g.v0001'] = true, - ['zbalv2.v0001'] = true, - ['zbalv2w.v0001'] = true, - ['zbffav2.v0001'] = true, - ['zbffav4.v0001'] = true, - ['zbfrv2.v0001'] = true, - ['zbsm 40x5v4.v0001'] = true, - ['zero point.v0001'] = true, - ['zero point.v0002'] = true, - ['zero point.v0003'] = true, - ['zone control extended.v0001'] = true, - ['zone control for faf 8p'] = true, - ['zone control.v0002'] = true, - ['zone control.v0004'] = true, - ['zone control.v0005'] = true, - ['zone control.v06'] = true, - ['zorg_outpost.v0001'] = true, - ['zorgvalleys'] = true, - ['wonder_open_ultimate.v0013'] = true, - ['wonder_open_ultimate.v0012'] = true, - ['wonder_open_ultimate.v0011'] = true, - ['wonder_open_ultimate.v0010'] = true, - ['wonder_open_ultimate.v0009'] = true, - ['wonder_open_ultimate.v0008'] = true, - ['wonder_open_ultimate.v0007'] = true, - ['wonder_open_ultimate.v0006'] = true, - ['wonder_open_ultimate.v0005'] = true, - ['wonder_open_ultimate.v0004'] = true, - ['wonder_open_ultimate.v0003'] = true, - ['wonder_open_ultimate.v0002'] = true, - ['wonder_open_ultimate.v0001'] = true, - ['flooded_tabula_rasa_ultimate.v0012'] = true, - ['flooded_tabula_rasa_ultimate.v0011'] = true, - ['flooded_tabula_rasa_ultimate.v0010'] = true, - ['flooded_tabula_rasa_ultimate.v0009'] = true, - ['flooded_tabula_rasa_ultimate.v0008'] = true, - ['flooded_tabula_rasa_ultimate.v0007'] = true, - ['flooded_tabula_rasa_ultimate.v0006'] = true, - ['flooded_tabula_rasa_ultimate.v0005'] = true, - ['flooded_tabula_rasa_ultimate.v0004'] = true, - ['flooded_tabula_rasa_ultimate.v0003'] = true, - ['flooded_tabula_rasa_ultimate.v0002'] = true, - ['flooded_tabula_rasa_ultimate.v0001'] = true, - ['canis_ultimate.v0017'] = true, - ['canis_ultimate.v0016'] = true, - ['canis_ultimate.v0015'] = true, - ['canis_ultimate.v0014'] = true, - ['canis_ultimate.v0013'] = true, - ['canis_ultimate.v0012'] = true, - ['canis_ultimate.v0011'] = true, - ['canis_ultimate.v0010'] = true, - ['canis_ultimate.v0009'] = true, - ['canis_ultimate.v0008'] = true, - ['canis_ultimate.v0007'] = true, - ['canis_ultimate.v0006'] = true, - ['canis_ultimate.v0005'] = true, - ['canis_ultimate.v0004'] = true, - ['canis_ultimate.v0003'] = true, - ['canis_ultimate.v0002'] = true, - ['canis_ultimate.v0001'] = true, - ['canis_cross_ultimate.v0001'] = true, - ['syrtis_major_ultimate.v0007'] = true, - ['syrtis_major_ultimate.v0006'] = true, - ['syrtis_major_ultimate.v0005'] = true, - ['syrtis_major_ultimate.v0004'] = true, - ['syrtis_major_ultimate.v0003'] = true, - ['syrtis_major_ultimate.v0002'] = true, - ['syrtis_major_ultimate.v0001'] = true, - ['open_palms_ultimate.v0001'] = true, - ['flooded_argon_ultimate.v0013'] = true, - ['flooded_argon_ultimate.v0012'] = true, - ['flooded_argon_ultimate.v0011'] = true, - ['flooded_argon_ultimate.v0010'] = true, - ['flooded_argon_ultimate.v0009'] = true, - ['flooded_argon_ultimate.v0008'] = true, - ['flooded_argon_ultimate.v0007'] = true, - ['flooded_argon_ultimate.v0006'] = true, - ['flooded_argon_ultimate.v0005'] = true, - ['flooded_argon_ultimate.v0004'] = true, - ['flooded_argon_ultimate.v0003'] = true, - ['flooded_argon_ultimate.v0002'] = true, - ['flooded_argon_ultimate.v0001'] = true, - ['flooded_corona_ultimate.v0004'] = true, - ['flooded_corona_ultimate.v0003'] = true, - ['flooded_corona_ultimate.v0002'] = true, - ['flooded_corona_ultimate.v0001'] = true, - ['point_of_reach_ultimate.v0009'] = true, - ['point_of_reach_ultimate.v0008'] = true, - ['point_of_reach_ultimate.v0007'] = true, - ['point_of_reach_ultimate.v0006'] = true, - ['point_of_reach_ultimate.v0005'] = true, - ['point_of_reach_ultimate.v0004'] = true, - ['point_of_reach_ultimate.v0003'] = true, - ['point_of_reach_ultimate.v0002'] = true, - ['point_of_reach_ultimate.v0001'] = true, - ['hilly_plateau_ultimate.v0008'] = true, - ['hilly_plateau_ultimate.v0007'] = true, - ['hilly_plateau_ultimate.v0006'] = true, - ['hilly_plateau_ultimate.v0005'] = true, - ['hilly_plateau_ultimate.v0004'] = true, - ['hilly_plateau_ultimate.v0003'] = true, - ['hilly_plateau_ultimate.v0002'] = true, - ['hilly_plateau_ultimate.v0001'] = true, - ['the_pyramid_ultimate.v0001'] = true, - ['argon_ultimate.v0013'] = true, - ['argon_ultimate.v0012'] = true, - ['argon_ultimate.v0011'] = true, - ['argon_ultimate.v0010'] = true, - ['argon_ultimate.v0009'] = true, - ['argon_ultimate.v0008'] = true, - ['argon_ultimate.v0007'] = true, - ['argon_ultimate.v0006'] = true, - ['argon_ultimate.v0005'] = true, - ['argon_ultimate.v0004'] = true, - ['argon_ultimate.v0003'] = true, - ['argon_ultimate.v0002'] = true, - ['argon_ultimate.v0001'] = true, - ['corona_ultimate.v0004'] = true, - ['corona_ultimate.v0003'] = true, - ['corona_ultimate.v0002'] = true, - ['corona_ultimate.v0001'] = true, - ['anchor_ultimate.v0001'] = true, - ['cauldron_small_ultimate.v0002'] = true, - ['cauldron_small_ultimate.v0001'] = true, - ['zeta_wonder_ultimate.v0010'] = true, - ['zeta_wonder_ultimate.v0009'] = true, - ['zeta_wonder_ultimate.v0008'] = true, - ['zeta_wonder_ultimate.v0007'] = true, - ['zeta_wonder_ultimate.v0006'] = true, - ['zeta_wonder_ultimate.v0005'] = true, - ['zeta_wonder_ultimate.v0004'] = true, - ['zeta_wonder_ultimate.v0003'] = true, - ['zeta_wonder_ultimate.v0002'] = true, - ['zeta_wonder_ultimate.v0001'] = true, - ['africa_ultimate.v0001'] = true, - ['twin_rivers_ultimate.v0005'] = true, - ['twin_rivers_ultimate.v0004'] = true, - ['twin_rivers_ultimate.v0003'] = true, - ['twin_rivers_ultimate.v0002'] = true, - ['twin_rivers_ultimate.v0001'] = true, - ['setons_clutch_ultimate.v0009'] = true, - ['setons_clutch_ultimate.v0008'] = true, - ['setons_clutch_ultimate.v0007'] = true, - ['setons_clutch_ultimate.v0006'] = true, - ['setons_clutch_ultimate.v0005'] = true, - ['setons_clutch_ultimate.v0004'] = true, - ['setons_clutch_ultimate.v0003'] = true, - ['setons_clutch_ultimate.v0002'] = true, - ['setons_clutch_ultimate.v0001'] = true, - ['new_canis_ultimate.v0003'] = true, - ['new_canis_ultimate.v0002'] = true, - ['new_canis_ultimate.v0001'] = true, - ['neptune_ultimate.v0003'] = true, - ['neptune_ultimate.v0002'] = true, - ['neptune_ultimate.v0001'] = true, - ['ians_cross_ultimate.v0002'] = true, - ['ians_cross_ultimate.v0001'] = true, - ['hrungdaks_canyon_ultimate.v0011'] = true, - ['hrungdaks_canyon_ultimate.v0010'] = true, - ['hrungdaks_canyon_ultimate.v0009'] = true, - ['hrungdaks_canyon_ultimate.v0008'] = true, - ['hrungdaks_canyon_ultimate.v0007'] = true, - ['hrungdaks_canyon_ultimate.v0006'] = true, - ['hrungdaks_canyon_ultimate.v0005'] = true, - ['hrungdaks_canyon_ultimate.v0004'] = true, - ['hrungdaks_canyon_ultimate.v0003'] = true, - ['hrungdaks_canyon_ultimate.v0002'] = true, - ['hrungdaks_canyon_ultimate.v0001'] = true, - ['regor_ultimate.v0003'] = true, - ['regor_ultimate.v0002'] = true, - ['regor_ultimate.v0001'] = true, - ['fields_of_great_phoenix_ultimate.v0006'] = true, - ['fields_of_great_phoenix_ultimate.v0005'] = true, - ['fields_of_great_phoenix_ultimate.v0004'] = true, - ['fields_of_great_phoenix_ultimate.v0003'] = true, - ['fields_of_great_phoenix_ultimate.v0002'] = true, - ['fields_of_great_phoenix_ultimate.v0001'] = true, - ['diversity_ultimate.v0007'] = true, - ['diversity_ultimate.v0006'] = true, - ['diversity_ultimate.v0005'] = true, - ['diversity_ultimate.v0004'] = true, - ['diversity_ultimate.v0003'] = true, - ['diversity_ultimate.v0002'] = true, - ['diversity_ultimate.v0001'] = true, - ['paradisebay.v0001'] = true, - ['paradisebay.v0002'] = true, - ['paradisebay.v0003'] = true, - ['paradisebay.v0004'] = true, - ['paradisebay.v0005'] = true, - ['paradisebay.v0006'] = true, - ['paradise_bay_v4.v0001'] = true, - ['paradise_bay_v4.v0002'] = true, - ['paradise_bay_v4.v0003'] = true, - ['paradise_bay_v4.v0004'] = true, - ['paradisebayv2.v0001'] = true, - ['paradisebayv2.v0002'] = true, - ['paradisebayv2.v0003'] = true, --- Maps that are hidden in the Vault - ['aspro_crater.v0001'] = true, - ['aspro_crater_remastered.v0001'] = true, - ['12_ultinate_you_shall_not_pass.v0001.zip'] = true, - ['fun setons'] = true, -} diff --git a/lua/ui/dialogs/mapselect.lua b/lua/ui/dialogs/mapselect.lua index af0bc68ee0..9cf88a766a 100644 --- a/lua/ui/dialogs/mapselect.lua +++ b/lua/ui/dialogs/mapselect.lua @@ -226,9 +226,6 @@ mapFilters = { SelectedKey = 1, Filters = { function(scenInfo) - if scenInfo.Outdated then - return false - end if CheckMapIsOfficial(scenInfo) then return true end diff --git a/lua/ui/maputil.lua b/lua/ui/maputil.lua index 8479588e52..e74974a92a 100644 --- a/lua/ui/maputil.lua +++ b/lua/ui/maputil.lua @@ -6,15 +6,60 @@ --* Copyright © 2006 Gas Powered Games, Inc. All rights reserved. --***************************************************************************** +--- A basic area defined in the scenario. +---@class UIScenarioArea +---@field [1] number # x0 +---@field [2] number # z0 +---@field [3] number # x1 +---@field [4] number # z1 +---@field type 'RECTANGLE' + +--- A marker defined in the scenario. +---@class UIScenarioMarker +---@field color string +---@field type string +---@field prop BlueprintId # path to blueprint +---@field orientation Vector +---@field position Vector + +--- A chain of markers defined in the scenario. +---@class UIScenarioChain +---@field Markers string[] # key of marker in the master chain + +--- An army defined in the scenario. +---@class UIScenarioArmy +---@field personality string +---@field plans string +---@field color number +---@field faction number +---@field Economy { mass: number, energy: number } +---@field Alliances table +---@field PlatoonBuilders { Builders: table } + +--- Scenario entities of a map that defines all areas, (resource) markers, marker chains and armies as defined in the average _save file. +---@class UIScenarioSaveFile +---@field Props table # Unknown +---@field Areas table +---@field MasterChain { _MASTERCHAIN_ : table } +---@field Chains table +---@field Orders table # Unknown +---@field Platoons table # Unknown +---@field Armies table + +--- Scenario options of a map, as defined in the average _options.lua file. +---@class UIScenarioOptionsFile +---@field options ScenarioOption[] + ---@class UIScenarioConfiguration ---@field teams {name: string, armies: string[]} ---@field customprops table ----@class UIScenarioInfo +--- The scenario information as defined in the average _scenario file. +---@class UIScenarioInfoFile ---@field AdaptiveMap boolean ---@field description string ---@field map string ----@field map_version number +---@field map_version? number ---@field name string ---@field norushradius number ---@field norushoffsetX_ARMY_1? number @@ -49,19 +94,36 @@ ---@field norushoffsetY_ARMY_15? number ---@field norushoffsetX_ARMY_16? number ---@field norushoffsetY_ARMY_16? number ----@field preview string ----@field save string ----@field script string +---@field preview? FileName +---@field save FileName +---@field script FileName ---@field size {[1]: number, [2]: number} +---@field reclaim? {[1]: number, [2]: number } # mass/energy ---@field starts boolean ----@field type string +---@field type 'skirmish' | 'campaign_coop' ---@field Configurations table ----@field file string ----@field Outdated boolean ---- Additional map options supplied by the map `options.lua` file, if it exists. ---- These are the lobby option-factory type of options, not the actual `` pairs ---- that the lobby ends up defining. ----@field options? ScenarioOption[] + +---@class UIScenarioBriefingData +---@field text table +---@field movies string[] +---@field voice table +---@field bgsound table +---@field style string + +--- The scenario strings as defined in the average _strings file. +---@class UIScenarioStringsFile: table +---@field BriefingData? UIScenarioBriefingData +---@field OPERATION_NAME? string +---@field OPERATION_DESCRIPTION? string + +--- The full scenario information with additional fields +---@class UILobbyScenarioInfo: UIScenarioInfoFile +---@field file FileName # reference to the _scenario.lua file +---@field options ScenarioOption[] # options from optional _options.lua file +---@field hasBriefing boolean # flag whether the _strings.lua file has briefing data in it + +--- The scenario information with additional fields, as defined once in a session +---@class UISessionSenarioInfo : UIScenarioInfoFile --- These are the actual `` pairs that the lobby defines, not the option-factory type --- objects the lobby uses ---@field Options? GameOptions @@ -70,69 +132,128 @@ ---@field PlayableAreaHeight number Syncs when the playable area changes ---@field PlayableRect { [1]: number, [2]: number, [3]: number, [4]: number } Coordinates `{x0, y0, x1, y1}` of the playable area Rectangle. Syncs when the playable area changes. +--- Given the path to a scenario info file, returns the path to the scenario options file. The reference to this file is not stored in the _scenario.lua file. +---@param pathToScenarioInfo FileName +---@return FileName +function GetPathToScenarioOptions(pathToScenarioInfo) + return string.sub(pathToScenarioInfo, 1, string.len(pathToScenarioInfo) - string.len("scenario.lua")) .. + "options.lua" --[[@as FileName]] +end + +--- Given the path to a scenario info file, returns the path to the scenario strings file. The reference to this file is not stored in the _scenario.lua file. +---@param pathToScenarioInfo FileName +---@return FileName +function GetPathToScenarioStrings(pathToScenarioInfo) + return string.sub(pathToScenarioInfo, 1, string.len(pathToScenarioInfo) - string.len("scenario.lua")) .. + "strings.lua" --[[@as FileName]] +end + +--- Loads in the scenario save. +---@param pathToScenarioSave FileName +---@return UIScenarioSaveFile | nil +function LoadScenarioSaveFile(pathToScenarioSave) + if not DiskGetFileInfo(pathToScenarioSave) then + return nil + end -local OutdatedMaps = import("/etc/faf/mapblacklist.lua").MapBlacklist -local Utils = import("/lua/system/utils.lua") + local data = {} + doscript('/lua/dataInit.lua', data) + doscript(pathToScenarioSave, data) --- load a scenario based on a scenario file name ----@param scenName string ----@return UIScenarioInfo -function LoadScenario(scenName) - -- TODO - expose FILE_IsAbsolute and if it's not, add the path and the _scenario.lua + return data.Scenario +end - if not DiskGetFileInfo(scenName) then +--- Loads in the scenario options. +---@param pathToScenarioOptions FileName +---@return UIScenarioOptionsFile | nil +function LoadScenarioOptionsFile(pathToScenarioOptions) + if not DiskGetFileInfo(pathToScenarioOptions) then return nil end - local env = {} - doscript('/lua/dataInit.lua', env) - doscript(scenName, env) + local data = {} + doscript('/lua/dataInit.lua', data) + doscript(pathToScenarioOptions, data) + + return data.options +end - if not env.ScenarioInfo then +--- Loads in the scenario strings. +---@param pathToScenarioStrings FileName +---@return UIScenarioStringsFile | nil +function LoadScenarioStringsFile(pathToScenarioStrings) + if not DiskGetFileInfo(pathToScenarioStrings) then return nil end + local data = {} + doscript('/lua/dataInit.lua', data) + doscript(pathToScenarioStrings, data) + + return data +end + +--- Loads in the scenario information. +---@param pathToScenarioInfo FileName +---@return UIScenarioInfoFile | nil +function LoadScenarioInfoFile(pathToScenarioInfo) + if not DiskGetFileInfo(pathToScenarioInfo) then + return nil + end + + local data = {} + doscript('/lua/dataInit.lua', data) + doscript(pathToScenarioInfo, data) + -- Backward compatibility - if env.version == 1 then - local temp = env - env = { + if data.version == 1 then + local temp = data + data = { ScenarioInfo = temp, } end - local optionsFileName = string.sub(scenName, 1, string.len(scenName) - string.len("scenario.lua")) .. "options.lua" - if DiskGetFileInfo(optionsFileName) then - local optionsEnv = {} - doscript(optionsFileName, optionsEnv) - if optionsEnv.options ~= nil then - env.ScenarioInfo.options = optionsEnv.options - end + return data.ScenarioInfo +end + +--- Loads in the entire scenario including the save and optional files such as _options.lua and _strings.lua. +---@param pathToScenarioInfo FileName +---@return UILobbyScenarioInfo? +function LoadScenario(pathToScenarioInfo) + local scenarioInfo = LoadScenarioInfoFile(pathToScenarioInfo) + if not scenarioInfo then + return nil end - -- Check if the map has mission briefing data - local stringsFileName = string.sub(scenName, 1, string.len(scenName) - string.len("scenario.lua")) .. "strings.lua" - if DiskGetFileInfo(stringsFileName) then - local stringsEnv = {} - doscript(stringsFileName, stringsEnv) - if stringsEnv.BriefingData ~= nil then - env.ScenarioInfo.hasBriefing = true - end + -- optionally, add in the options + local ok, msg, scenarioOptions = pcall(LoadScenarioOptionsFile, GetPathToScenarioOptions(pathToScenarioInfo)) --[[@as UIScenarioOptionsFile | nil]] + if scenarioOptions then + scenarioInfo.options = scenarioOptions end - -- Is this map flagged out of date? *CACKLES INSANELY* - local pathBits = Utils.StringSplit(scenName, '/') - env.ScenarioInfo.Outdated = OutdatedMaps[pathBits[2]] + -- optionally, add in briefing data flag + local ok, msg, scenarioStrings = pcall(LoadScenarioStringsFile, GetPathToScenarioStrings(pathToScenarioInfo)) --[[@as UIScenarioStringsFile | nil]] + if scenarioStrings then + if scenarioStrings.BriefingData then + scenarioInfo.hasBriefing = true + end + end - env.ScenarioInfo.file = scenName -- stuff the file name in so we have that - return env.ScenarioInfo + scenarioInfo.file = pathToScenarioInfo + return scenarioInfo --[[@as UILobbyScenarioInfo]] end --- the default scenario enumerator sort method +--- the default scenario enumerator sort method +---@param compa string +---@param compb string +---@return boolean local function DefaultScenarioSorter(compa, compb) return string.upper(compa) < string.upper(compb) end --- given a scenario, determines if it can be played in skirmish mode +--- given a scenario, determines if it can be played in skirmish mode +---@param scenario UIScenarioInfoFile +---@return boolean function IsScenarioPlayable(scenario) if not scenario.Configurations.standard.teams[1].armies then return false @@ -141,12 +262,15 @@ function IsScenarioPlayable(scenario) return true end --- EnumerateScenarios returns an array of scenario names --- nameFilter can be passed in to narrow the enumaration, for example --- EnumerateScenarios("SCMP*") will find all maps that start with SCMP --- if nameFilter is nil, all maps will be returned --- sortFunc is a function which, given two scenario names, will return true for the file name to come first --- if no sortFunc is defined the default sorter will be used +--- EnumerateScenarios returns an array of scenario names +--- nameFilter can be passed in to narrow the enumaration, for example +--- EnumerateScenarios("SCMP*") will find all maps that start with SCMP +--- if nameFilter is nil, all maps will be returned +--- sortFunc is a function which, given two scenario names, will return true for the file name to come first +--- if no sortFunc is defined the default sorter will be used +---@param nameFilter? string # defaults to '*' +---@param sortFunc? fun(a, b): boolean # defaults to alphabetical on name of map +---@return table function EnumerateSkirmishScenarios(nameFilter, sortFunc) nameFilter = nameFilter or '*' sortFunc = sortFunc or DefaultScenarioSorter @@ -172,6 +296,8 @@ end -- given a scenario table, loads the save file and puts all the start positions in a table -- I've made this function so it works with the old data format and the new -- Returning an empty table means scenario data was ill formed +---@param scenario UIScenarioInfoFile +---@return Vector2[] function GetStartPositions(scenario) local saveData = {} doscript('/lua/dataInit.lua', saveData) @@ -229,7 +355,9 @@ function GetStartPositions(scenario) return armyPositions end --- enumerates and returns to key name for all the armies for this map +-- Retrieves all of the playable armies for a scenario +---@param scenario UIScenarioInfoFile +---@return string[] | nil function GetArmies(scenario) local retArmies = nil @@ -249,11 +377,14 @@ function GetArmies(scenario) return retArmies end + +---@param scenario UIScenarioInfoFile +---@return string[] function GetExtraArmies(scenario) if scenario.Configurations.standard and scenario.Configurations.standard.teams then local teams = scenario.Configurations.standard.teams if teams.ExtraArmies then - local armies = STR_GetTokens(teams.ExtraArmies,' ') + local armies = STR_GetTokens(teams.ExtraArmies, ' ') return armies end end @@ -261,6 +392,8 @@ end --- Validate options provided by the scenario file. -- This function prints warnings about any defects and attempts to correct them with sane defaults. +---@param scenarioOptions ScenarioOption[] +---@return boolean function ValidateScenarioOptions(scenarioOptions) -- Most maps just don't have any options. if not scenarioOptions then @@ -276,8 +409,8 @@ function ValidateScenarioOptions(scenarioOptions) table.print(optData) passed = false elseif type(optData.default) ~= "number" or - optData.default <= 0 or - optData.default > table.getn(optData.values) then + optData.default <= 0 or + optData.default > table.getn(optData.values) then WARN("Invalid default option value " .. tostring(optData.default)) WARN("Remember: option defaults are 1-based indices into the `values' table, not values themselves") WARN("Offending option table:") @@ -306,6 +439,8 @@ end -- -- @param scenario Scenario info -- @return true if the map has Land Path nodes, false otherwise. +---@param scenario UIScenarioInfoFile +---@return boolean function CheckMapHasMarkers(scenario) if not DiskGetFileInfo(scenario.save) then return false @@ -316,19 +451,20 @@ function CheckMapHasMarkers(scenario) local markers = saveData and - saveData.Scenario and - saveData.Scenario.MasterChain and - saveData.Scenario.MasterChain['_MASTERCHAIN_'] and - saveData.Scenario.MasterChain['_MASTERCHAIN_'].Markers or false + saveData.Scenario and + saveData.Scenario.MasterChain and + saveData.Scenario.MasterChain['_MASTERCHAIN_'] and + saveData.Scenario.MasterChain['_MASTERCHAIN_'].Markers or false if not markers then - WARN('Map '.. scenario.name..' has no markers') return false + WARN('Map ' .. scenario.name .. ' has no markers') + return false else - for marker, data in markers do - if data.adjacentTo and string.find(data.adjacentTo, ' ') then - return true - end - end + for marker, data in markers do + if data.adjacentTo and string.find(data.adjacentTo, ' ') then + return true + end + end end return false end From 337185dbb3b707c768ad4b440d36ceaf3767f145 Mon Sep 17 00:00:00 2001 From: Alexander <84857900+4z0t@users.noreply.github.com> Date: Thu, 31 Oct 2024 13:11:11 +0300 Subject: [PATCH 10/13] Document CMauiBitmap:SetColorMask (#6500) --- engine/User/CMauiBitmap.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/engine/User/CMauiBitmap.lua b/engine/User/CMauiBitmap.lua index 278b891143..8a5f7f851b 100644 --- a/engine/User/CMauiBitmap.lua +++ b/engine/User/CMauiBitmap.lua @@ -33,6 +33,11 @@ end function CMauiBitmap:SetForwardPattern() end +---Sets color mask applied to bitmap during rendering (white images will get this color for example) +---@param color Color +function CMauiBitmap:SetColorMask(color) +end + --- ---@param frame number function CMauiBitmap:SetFrame(frame) From 641d59814ba2d53d536edbc03157af0b0592ce78 Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 31 Oct 2024 03:19:41 -0700 Subject: [PATCH 11/13] Improve maintainability of UEF ACU enhancements (#6498) --- changelog/snippets/other.6498.md | 1 + units/UEL0001/UEL0001_script.lua | 597 +++++++++++++++++++------------ 2 files changed, 376 insertions(+), 222 deletions(-) create mode 100644 changelog/snippets/other.6498.md diff --git a/changelog/snippets/other.6498.md b/changelog/snippets/other.6498.md new file mode 100644 index 0000000000..6387b72138 --- /dev/null +++ b/changelog/snippets/other.6498.md @@ -0,0 +1 @@ +- (#6498) Refactor the Enhancements section in the Unit script file to replace the long if/else chain with a more modular design. Each enhancement should have its own dedicated function, enabling cleaner code organization and easier maintenance. The CreateEnhancement function will then call the appropriate enhancement function based on specific enhancement criteria. diff --git a/units/UEL0001/UEL0001_script.lua b/units/UEL0001/UEL0001_script.lua index 80dfcab0bc..3be7b2711a 100644 --- a/units/UEL0001/UEL0001_script.lua +++ b/units/UEL0001/UEL0001_script.lua @@ -302,239 +302,392 @@ UEL0001 = ClassUnit(ACUUnit) { attachee:SetDoNotTarget(false) end, + --------------------------------------------------------------------------- + --#region Enhancements + + -- Drone Upgrades + ---@param self UEL0001 - ---@param enh string - CreateEnhancement = function(self, enh) - ACUUnit.CreateEnhancement(self, enh) + ---@param bp Blueprint unused + ProcessEnhancementLeftPod = function(self, bp) + local location = self:GetPosition('AttachSpecial02') + local pod = CreateUnitHPR('UEA0001', self.Army, location[1], location[2], location[3], 0, 0, 0) + pod:SetParent(self, 'LeftPod') + pod:SetCreator(self) + self.Trash:Add(pod) + self.HasLeftPod = true + self.LeftPod = pod + end, - local bp = self:GetBlueprint().Enhancements[enh] - if not bp then return end - if enh == 'LeftPod' then - local location = self:GetPosition('AttachSpecial02') - local pod = CreateUnitHPR('UEA0001', self.Army, location[1], location[2], location[3], 0, 0, 0) - pod:SetParent(self, 'LeftPod') - pod:SetCreator(self) - self.Trash:Add(pod) - self.HasLeftPod = true - self.LeftPod = pod - elseif enh == 'RightPod' then - local location = self:GetPosition('AttachSpecial01') - local pod = CreateUnitHPR('UEA0001', self.Army, location[1], location[2], location[3], 0, 0, 0) - pod:SetParent(self, 'RightPod') - pod:SetCreator(self) - self.Trash:Add(pod) - self.HasRightPod = true - self.RightPod = pod - elseif enh == 'LeftPodRemove' or enh == 'RightPodRemove' then - if self.HasLeftPod == true then - self.HasLeftPod = false - if self.LeftPod and not self.LeftPod.Dead then - self.LeftPod:Kill() - self.LeftPod = nil - end - if self.RebuildingPod ~= nil then - RemoveEconomyEvent(self, self.RebuildingPod) - self.RebuildingPod = nil - end - end - if self.HasRightPod == true then - self.HasRightPod = false - if self.RightPod and not self.RightPod.Dead then - self.RightPod:Kill() - self.RightPod = nil - end - if self.RebuildingPod2 ~= nil then - RemoveEconomyEvent(self, self.RebuildingPod2) - self.RebuildingPod2 = nil - end + ---@param self UEL0001 + ---@param bp Blueprint unused + ProcessEnhancementLeftPodRemove = function(self, bp) + if self.HasLeftPod == true then + self.HasLeftPod = false + if self.LeftPod and not self.LeftPod.Dead then + self.LeftPod:Kill() + self.LeftPod = nil end - KillThread(self.RebuildThread) - KillThread(self.RebuildThread2) - elseif enh == 'Teleporter' then - self:AddCommandCap('RULEUCC_Teleport') - elseif enh == 'TeleporterRemove' then - self:RemoveCommandCap('RULEUCC_Teleport') - elseif enh == 'Shield' then - self:AddToggleCap('RULEUTC_ShieldToggle') - self:CreateShield(bp) - self:SetEnergyMaintenanceConsumptionOverride(bp.MaintenanceConsumptionPerSecondEnergy or 0) - self:SetMaintenanceConsumptionActive() - elseif enh == 'ShieldRemove' then - self:DestroyShield() - self:SetMaintenanceConsumptionInactive() - RemoveUnitEnhancement(self, 'ShieldRemove') - self:RemoveToggleCap('RULEUTC_ShieldToggle') - elseif enh == 'ShieldGeneratorField' then - self:AddToggleCap('RULEUTC_ShieldToggle') - self:DestroyShield() - self:ForkThread( - function() - WaitTicks(1) - self:CreateShield(bp) - self:SetEnergyMaintenanceConsumptionOverride(bp.MaintenanceConsumptionPerSecondEnergy or 0) - self:SetMaintenanceConsumptionActive() - end - ) - elseif enh == 'ShieldGeneratorFieldRemove' then - self:DestroyShield() - self:SetMaintenanceConsumptionInactive() - self:RemoveToggleCap('RULEUTC_ShieldToggle') - elseif enh == 'AdvancedEngineering' then - local cat = ParseEntityCategory(bp.BuildableCategoryAdds) - self:RemoveBuildRestriction(cat) - if not Buffs['UEFACUT2BuildRate'] then - BuffBlueprint { - Name = 'UEFACUT2BuildRate', - DisplayName = 'UEFACUT2BuildRate', - BuffType = 'ACUBUILDRATE', - Stacks = 'REPLACE', - Duration = -1, - Affects = { - BuildRate = { - Add = bp.NewBuildRate - self:GetBlueprint().Economy.BuildRate, - Mult = 1, - }, - MaxHealth = { - Add = bp.NewHealth, - Mult = 1.0, - }, - Regen = { - Add = bp.NewRegenRate, - Mult = 1.0, - }, - }, - } + if self.RebuildingPod ~= nil then + RemoveEconomyEvent(self, self.RebuildingPod) + self.RebuildingPod = nil end - Buff.ApplyBuff(self, 'UEFACUT2BuildRate') - elseif enh == 'AdvancedEngineeringRemove' then - local bp = self:GetBlueprint().Economy.BuildRate - if not bp then return end - self:RestoreBuildRestrictions() - self:AddBuildRestriction(categories.UEF * - (categories.BUILTBYTIER2COMMANDER + categories.BUILTBYTIER3COMMANDER)) - self:AddBuildRestriction(categories.UEF * - (categories.BUILTBYTIER2COMMANDER + categories.BUILTBYTIER3COMMANDER)) - if Buff.HasBuff(self, 'UEFACUT2BuildRate') then - Buff.RemoveBuff(self, 'UEFACUT2BuildRate') + end + KillThread(self.RebuildThread) + end, + + ---@param self UEL0001 + ---@param bp Blueprint unused + ProcessEnhancementRightPod = function(self, bp) + local location = self:GetPosition('AttachSpecial01') + local pod = CreateUnitHPR('UEA0001', self.Army, location[1], location[2], location[3], 0, 0, 0) + pod:SetParent(self, 'RightPod') + pod:SetCreator(self) + self.Trash:Add(pod) + self.HasRightPod = true + self.RightPod = pod + end, + + ---@param self UEL0001 + ---@param bp Blueprint unused + ProcessEnhancementRightPodRemove = function(self, bp) + if self.HasLeftPod == true then + self.HasLeftPod = false + if self.LeftPod and not self.LeftPod.Dead then + self.LeftPod:Kill() + self.LeftPod = nil end - elseif enh == 'T3Engineering' then - local cat = ParseEntityCategory(bp.BuildableCategoryAdds) - self:RemoveBuildRestriction(cat) - if not Buffs['UEFACUT3BuildRate'] then - BuffBlueprint { - Name = 'UEFACUT3BuildRate', - DisplayName = 'UEFCUT3BuildRate', - BuffType = 'ACUBUILDRATE', - Stacks = 'REPLACE', - Duration = -1, - Affects = { - BuildRate = { - Add = bp.NewBuildRate - self:GetBlueprint().Economy.BuildRate, - Mult = 1, - }, - MaxHealth = { - Add = bp.NewHealth, - Mult = 1.0, - }, - Regen = { - Add = bp.NewRegenRate, - Mult = 1.0, - }, - }, - } + if self.RebuildingPod ~= nil then + RemoveEconomyEvent(self, self.RebuildingPod) + self.RebuildingPod = nil end - Buff.ApplyBuff(self, 'UEFACUT3BuildRate') - elseif enh == 'T3EngineeringRemove' then - local bp = self:GetBlueprint().Economy.BuildRate - if not bp then return end - self:RestoreBuildRestrictions() - if Buff.HasBuff(self, 'UEFACUT3BuildRate') then - Buff.RemoveBuff(self, 'UEFACUT3BuildRate') + end + if self.HasRightPod == true then + self.HasRightPod = false + if self.RightPod and not self.RightPod.Dead then + self.RightPod:Kill() + self.RightPod = nil end - self:AddBuildRestriction(categories.UEF * - (categories.BUILTBYTIER2COMMANDER + categories.BUILTBYTIER3COMMANDER)) - elseif enh == 'DamageStabilization' then - if not Buffs['UEFACUDamageStabilization'] then - BuffBlueprint { - Name = 'UEFACUDamageStabilization', - DisplayName = 'UEFACUDamageStabilization', - BuffType = 'DamageStabilization', - Stacks = 'REPLACE', - Duration = -1, - Affects = { - MaxHealth = { - Add = bp.NewHealth, - Mult = 1.0, - }, - Regen = { - Add = bp.NewRegenRate, - Mult = 1.0, - }, - }, - } + if self.RebuildingPod2 ~= nil then + RemoveEconomyEvent(self, self.RebuildingPod2) + self.RebuildingPod2 = nil end - Buff.ApplyBuff(self, 'UEFACUDamageStabilization') - elseif enh == 'DamageStabilizationRemove' then - if Buff.HasBuff(self, 'UEFACUDamageStabilization') then - Buff.RemoveBuff(self, 'UEFACUDamageStabilization') + end + KillThread(self.RebuildThread) + KillThread(self.RebuildThread2) + end, + + -- Teleport Upgrade + + ---@param self UEL0001 + ---@param bp Blueprint unused + ProcessEnhancementTeleporter = function(self, bp) + self:AddCommandCap('RULEUCC_Teleport') + end, + + ---@param self UEL0001 + ---@param bp Blueprint unused + ProcessEnhancementTeleporterRemove = function(self, bp) + self:RemoveCommandCap('RULEUCC_Teleport') + end, + + -- Personal Shield + + ---@param self UEL0001 + ---@param bp Blueprint + ProcessEnhancementShield = function(self, bp) + self:AddToggleCap('RULEUTC_ShieldToggle') + self:CreateShield(bp) + self:SetEnergyMaintenanceConsumptionOverride(bp.MaintenanceConsumptionPerSecondEnergy or 0) + self:SetMaintenanceConsumptionActive() + end, + + ---@param self UEL0001 + ---@param bp Blueprint unused + ProcessEnhancementShieldRemove = function(self, bp) + self:DestroyShield() + self:SetMaintenanceConsumptionInactive() + RemoveUnitEnhancement(self, 'ShieldRemove') + self:RemoveToggleCap('RULEUTC_ShieldToggle') + end, + + -- Bubble Shield + + ---@param self UEL0001 + ---@param bp Blueprint + ProcessEnhancementShieldGeneratorField = function(self, bp) + self:AddToggleCap('RULEUTC_ShieldToggle') + self:DestroyShield() + self:ForkThread( + function() + WaitTicks(1) + self:CreateShield(bp) + self:SetEnergyMaintenanceConsumptionOverride(bp.MaintenanceConsumptionPerSecondEnergy or 0) + self:SetMaintenanceConsumptionActive() end - elseif enh == 'HeavyAntiMatterCannon' then - local wep = self:GetWeaponByLabel('RightZephyr') - wep:AddDamageMod(bp.ZephyrDamageMod) - wep:ChangeMaxRadius(bp.NewMaxRadius or 44) - local oc = self:GetWeaponByLabel('OverCharge') - oc:ChangeMaxRadius(bp.NewMaxRadius or 44) - local aoc = self:GetWeaponByLabel('AutoOverCharge') - aoc:ChangeMaxRadius(bp.NewMaxRadius or 44) - elseif enh == 'HeavyAntiMatterCannonRemove' then - local bp = self:GetBlueprint().Enhancements['HeavyAntiMatterCannon'] - if not bp then return end - local wep = self:GetWeaponByLabel('RightZephyr') - wep:AddDamageMod(-bp.ZephyrDamageMod) - local bpDisrupt = self:GetBlueprint().Weapon[1].MaxRadius - wep:ChangeMaxRadius(bpDisrupt or 22) - local oc = self:GetWeaponByLabel('OverCharge') - oc:ChangeMaxRadius(bpDisrupt or 22) - local aoc = self:GetWeaponByLabel('AutoOverCharge') - aoc:ChangeMaxRadius(bpDisrupt or 22) - elseif enh == 'ResourceAllocation' then - local bp = self:GetBlueprint().Enhancements[enh] - local bpEcon = self:GetBlueprint().Economy - if not bp then return end - self:SetProductionPerSecondEnergy((bp.ProductionPerSecondEnergy + bpEcon.ProductionPerSecondEnergy) or 0) - self:SetProductionPerSecondMass((bp.ProductionPerSecondMass + bpEcon.ProductionPerSecondMass) or 0) - elseif enh == 'ResourceAllocationRemove' then - local bpEcon = self:GetBlueprint().Economy - self:SetProductionPerSecondEnergy(bpEcon.ProductionPerSecondEnergy or 0) - self:SetProductionPerSecondMass(bpEcon.ProductionPerSecondMass or 0) - elseif enh == 'TacticalMissile' then - self:AddCommandCap('RULEUCC_Tactical') - self:AddCommandCap('RULEUCC_SiloBuildTactical') - self:SetWeaponEnabledByLabel('TacMissile', true) - elseif enh == 'TacticalNukeMissile' then - self:RemoveCommandCap('RULEUCC_Tactical') - self:RemoveCommandCap('RULEUCC_SiloBuildTactical') - self:AddCommandCap('RULEUCC_Nuke') - self:AddCommandCap('RULEUCC_SiloBuildNuke') - self:SetWeaponEnabledByLabel('TacMissile', false) - self:SetWeaponEnabledByLabel('TacNukeMissile', true) - local amt = self:GetTacticalSiloAmmoCount() - self:RemoveTacticalSiloAmmo(amt or 0) - self:StopSiloBuild() - elseif enh == 'TacticalMissileRemove' or enh == 'TacticalNukeMissileRemove' then - self:RemoveCommandCap('RULEUCC_Nuke') - self:RemoveCommandCap('RULEUCC_SiloBuildNuke') - self:RemoveCommandCap('RULEUCC_Tactical') - self:RemoveCommandCap('RULEUCC_SiloBuildTactical') - self:SetWeaponEnabledByLabel('TacMissile', false) - self:SetWeaponEnabledByLabel('TacNukeMissile', false) - local amt = self:GetTacticalSiloAmmoCount() - self:RemoveTacticalSiloAmmo(amt or 0) - local amt = self:GetNukeSiloAmmoCount() - self:RemoveNukeSiloAmmo(amt or 0) - self:StopSiloBuild() + ) + end, + + ---@param self UEL0001 + ---@param bp Blueprint unused + ProcessEnhancementShieldGeneratorFieldRemove = function(self, bp) + self:DestroyShield() + self:SetMaintenanceConsumptionInactive() + self:RemoveToggleCap('RULEUTC_ShieldToggle') + end, + + -- T2 Engineering Suite + + ---@param self UEL0001 + ---@param bp Blueprint + ProcessEnhancementAdvancedEngineering = function(self, bp) + local cat = ParseEntityCategory(bp.BuildableCategoryAdds) + self:RemoveBuildRestriction(cat) + if not Buffs['UEFACUT2BuildRate'] then + BuffBlueprint { + Name = 'UEFACUT2BuildRate', + DisplayName = 'UEFACUT2BuildRate', + BuffType = 'ACUBUILDRATE', + Stacks = 'REPLACE', + Duration = -1, + Affects = { + BuildRate = { + Add = bp.NewBuildRate - self:GetBlueprint().Economy.BuildRate, + Mult = 1, + }, + MaxHealth = { + Add = bp.NewHealth, + Mult = 1.0, + }, + Regen = { + Add = bp.NewRegenRate, + Mult = 1.0, + }, + }, + } + end + Buff.ApplyBuff(self, 'UEFACUT2BuildRate') + + end, + + ---@param self UEL0001 + ---@param bp Blueprint unused + ProcessEnhancementAdvancedEngineeringRemove = function(self, bp) + local bp = self:GetBlueprint().Economy.BuildRate + if not bp then return end + self:RestoreBuildRestrictions() + self:AddBuildRestriction(categories.UEF * + (categories.BUILTBYTIER2COMMANDER + categories.BUILTBYTIER3COMMANDER)) + self:AddBuildRestriction(categories.UEF * + (categories.BUILTBYTIER2COMMANDER + categories.BUILTBYTIER3COMMANDER)) + if Buff.HasBuff(self, 'UEFACUT2BuildRate') then + Buff.RemoveBuff(self, 'UEFACUT2BuildRate') + end + end, + + -- T3 Engineering Suite + + ---@param self UEL0001 + ---@param bp Blueprint + ProcessEnhancementT3Engineering = function(self, bp) + local cat = ParseEntityCategory(bp.BuildableCategoryAdds) + self:RemoveBuildRestriction(cat) + if not Buffs['UEFACUT3BuildRate'] then + BuffBlueprint { + Name = 'UEFACUT3BuildRate', + DisplayName = 'UEFCUT3BuildRate', + BuffType = 'ACUBUILDRATE', + Stacks = 'REPLACE', + Duration = -1, + Affects = { + BuildRate = { + Add = bp.NewBuildRate - self:GetBlueprint().Economy.BuildRate, + Mult = 1, + }, + MaxHealth = { + Add = bp.NewHealth, + Mult = 1.0, + }, + Regen = { + Add = bp.NewRegenRate, + Mult = 1.0, + }, + }, + } + end + Buff.ApplyBuff(self, 'UEFACUT3BuildRate') + + end, + + ---@param self UEL0001 + ---@param bp Blueprint unused + ProcessEnhancementT3EngineeringRemove = function(self, bp) + local bp = self:GetBlueprint().Economy.BuildRate + if not bp then return end + self:RestoreBuildRestrictions() + if Buff.HasBuff(self, 'UEFACUT3BuildRate') then + Buff.RemoveBuff(self, 'UEFACUT3BuildRate') + end + self:AddBuildRestriction(categories.UEF * (categories.BUILTBYTIER2COMMANDER + categories.BUILTBYTIER3COMMANDER)) + end, + + -- Nano Repair System + + ---@param self UEL0001 + ---@param bp Blueprint + ProcessEnhancementDamageStabilization = function(self, bp) + if not Buffs['UEFACUDamageStabilization'] then + BuffBlueprint { + Name = 'UEFACUDamageStabilization', + DisplayName = 'UEFACUDamageStabilization', + BuffType = 'DamageStabilization', + Stacks = 'REPLACE', + Duration = -1, + Affects = { + MaxHealth = { + Add = bp.NewHealth, + Mult = 1.0, + }, + Regen = { + Add = bp.NewRegenRate, + Mult = 1.0, + }, + }, + } + end + Buff.ApplyBuff(self, 'UEFACUDamageStabilization') + + end, + + ---@param self UEL0001 + ---@param bp Blueprint unused + ProcessEnhancementDamageStabilizationRemove = function(self, bp) + if Buff.HasBuff(self, 'UEFACUDamageStabilization') then + Buff.RemoveBuff(self, 'UEFACUDamageStabilization') + end + end, + + -- Gun Upgrade + + ---@param self UEL0001 + ---@param bp Blueprint + ProcessEnhancementHeavyAntiMatterCannon = function(self, bp) + local wep = self:GetWeaponByLabel('RightZephyr') + wep:AddDamageMod(bp.ZephyrDamageMod) + wep:ChangeMaxRadius(bp.NewMaxRadius or 44) + local oc = self:GetWeaponByLabel('OverCharge') + oc:ChangeMaxRadius(bp.NewMaxRadius or 44) + local aoc = self:GetWeaponByLabel('AutoOverCharge') + aoc:ChangeMaxRadius(bp.NewMaxRadius or 44) + end, + + ---@param self UEL0001 + ---@param bp Blueprint unused + ProcessEnhancementHeavyAntiMatterCannonRemove = function(self, bp) + local bp = self:GetBlueprint().Enhancements['HeavyAntiMatterCannon'] + if not bp then return end + local wep = self:GetWeaponByLabel('RightZephyr') + wep:AddDamageMod(-bp.ZephyrDamageMod) + local bpDisrupt = self:GetBlueprint().Weapon[1].MaxRadius + wep:ChangeMaxRadius(bpDisrupt or 22) + local oc = self:GetWeaponByLabel('OverCharge') + oc:ChangeMaxRadius(bpDisrupt or 22) + local aoc = self:GetWeaponByLabel('AutoOverCharge') + aoc:ChangeMaxRadius(bpDisrupt or 22) + end, + + -- RAS + + ---@param self UEL0001 + ---@param bp Blueprint + ProcessEnhancementResourceAllocation = function(self, bp) + local bpEcon = self:GetBlueprint().Economy + if not bp then return end + self:SetProductionPerSecondEnergy((bp.ProductionPerSecondEnergy + bpEcon.ProductionPerSecondEnergy) or 0) + self:SetProductionPerSecondMass((bp.ProductionPerSecondMass + bpEcon.ProductionPerSecondMass) or 0) + + end, + + ---@param self UEL0001 + ---@param bp Blueprint unused + ProcessEnhancementResourceAllocationRemove = function(self, bp) + local bpEcon = self:GetBlueprint().Economy + self:SetProductionPerSecondEnergy(bpEcon.ProductionPerSecondEnergy or 0) + self:SetProductionPerSecondMass(bpEcon.ProductionPerSecondMass or 0) + end, + + -- Tactical Missile Launcher + + ---@param self UEL0001 + ---@param bp Blueprint unused + ProcessEnhancementTacticalMissile = function(self, bp) + self:AddCommandCap('RULEUCC_Tactical') + self:AddCommandCap('RULEUCC_SiloBuildTactical') + self:SetWeaponEnabledByLabel('TacMissile', true) + end, + + ---@param self UEL0001 + ---@param bp Blueprint unused + ProcessEnhancementTacticalMissileRemove = function(self, bp) + self:RemoveCommandCap('RULEUCC_Tactical') + self:RemoveCommandCap('RULEUCC_SiloBuildTactical') + self:SetWeaponEnabledByLabel('TacMissile', false) + local amt = self:GetTacticalSiloAmmoCount() + self:RemoveTacticalSiloAmmo(amt or 0) + self:StopSiloBuild() + end, + + -- Billy Nuke + + ---@param self UEL0001 + ---@param bp Blueprint unused + ProcessEnhancementTacticalNukeMissile = function(self, bp) + self:RemoveCommandCap('RULEUCC_Tactical') + self:RemoveCommandCap('RULEUCC_SiloBuildTactical') + self:AddCommandCap('RULEUCC_Nuke') + self:AddCommandCap('RULEUCC_SiloBuildNuke') + self:SetWeaponEnabledByLabel('TacMissile', false) + self:SetWeaponEnabledByLabel('TacNukeMissile', true) + local amt = self:GetTacticalSiloAmmoCount() + self:RemoveTacticalSiloAmmo(amt or 0) + self:StopSiloBuild() + end, + + ---@param self UEL0001 + ---@param bp Blueprint unused + ProcessEnhancementTacticalNukeMissileRemove = function(self, bp) + self:RemoveCommandCap('RULEUCC_Nuke') + self:RemoveCommandCap('RULEUCC_SiloBuildNuke') + self:RemoveCommandCap('RULEUCC_Tactical') + self:RemoveCommandCap('RULEUCC_SiloBuildTactical') + self:SetWeaponEnabledByLabel('TacMissile', false) + self:SetWeaponEnabledByLabel('TacNukeMissile', false) + local amt = self:GetTacticalSiloAmmoCount() + self:RemoveTacticalSiloAmmo(amt or 0) + local amt = self:GetNukeSiloAmmoCount() + self:RemoveNukeSiloAmmo(amt or 0) + self:StopSiloBuild() + end, + + + ---@param self UEL0001 + ---@param enh string + CreateEnhancement = function(self, enh) + ACUUnit.CreateEnhancement(self, enh) + + local bp = self:GetBlueprint().Enhancements[enh] + if not bp then return end + + local ref = 'ProcessEnhancement' .. enh + local handler = self[ref] + if handler then + handler(self, bp) + else + WARN("Missing enhancement: ", enh, " for unit: ", self:GetUnitId(), " note that the function name should be called: ", ref) end end, + + --#endregion } TypeClass = UEL0001 From 3f034d13505450b406994746590d6668ed322826 Mon Sep 17 00:00:00 2001 From: Basilisk3 <126026384+Basilisk3@users.noreply.github.com> Date: Thu, 31 Oct 2024 11:30:57 +0100 Subject: [PATCH 12/13] Fix a crash when the `EnableDiskWatch` command line argument is enabled (#6494) --- changelog/snippets/fix.6494.md | 1 + lua/system/blueprints-units.lua | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog/snippets/fix.6494.md diff --git a/changelog/snippets/fix.6494.md b/changelog/snippets/fix.6494.md new file mode 100644 index 0000000000..89b2cdbff1 --- /dev/null +++ b/changelog/snippets/fix.6494.md @@ -0,0 +1 @@ +- (#6494) Fix a crash when the `EnableDiskWatch` command line argument is enabled. diff --git a/lua/system/blueprints-units.lua b/lua/system/blueprints-units.lua index 8b311fb23a..0c9fd3bdd4 100644 --- a/lua/system/blueprints-units.lua +++ b/lua/system/blueprints-units.lua @@ -397,7 +397,7 @@ local function PostProcessUnit(unit) status.AllIntel = {} if intelBlueprint then for name, value in pairs(intelBlueprint) do - + -- may contain tables, such as `JamRadius` if type(value) ~= 'table' then if value == true or value > 0 then @@ -555,7 +555,7 @@ local function PostProcessUnit(unit) -- Define a specific TransportSpeedReduction for all land and naval units. -- Experimentals have a TransportSpeedReduction of 1 due to transports gaining 1 speed and some survival maps loading experimentals into transports. -- Naval units also gain a TransportSpeedReduction of 1 to ensure mod compatibility. - if not unit.Physics.TransportSpeedReduction and not isStructure then + if unit.Physics and not unit.Physics.TransportSpeedReduction and not isStructure then if isLand and isTech1 then unit.Physics.TransportSpeedReduction = 0.15 elseif isLand and isTech2 then From 1491ad6fec3b570533153771ccabbb32f066a534 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 6 Nov 2024 02:46:18 -0800 Subject: [PATCH 13/13] Refactor Aeon ACU Enhancements Script (#6502) Continuing the refactor of #6498 --------- Co-authored-by: lL1l1 <82986251+lL1l1@users.noreply.github.com> --- changelog/snippets/other.6498.md | 2 +- units/UAL0001/UAL0001_script.lua | 498 ++++++++++++++++++------------- 2 files changed, 299 insertions(+), 201 deletions(-) diff --git a/changelog/snippets/other.6498.md b/changelog/snippets/other.6498.md index 6387b72138..237a2857b1 100644 --- a/changelog/snippets/other.6498.md +++ b/changelog/snippets/other.6498.md @@ -1 +1 @@ -- (#6498) Refactor the Enhancements section in the Unit script file to replace the long if/else chain with a more modular design. Each enhancement should have its own dedicated function, enabling cleaner code organization and easier maintenance. The CreateEnhancement function will then call the appropriate enhancement function based on specific enhancement criteria. +- (#6498, #6502) Refactor the Enhancements section in the ACU/SACU scripts to replace the long if/else chain with a more modular design that is easier to maintain and hook. Each enhancement has its own dedicated function, named with the format `ProcessEnhancement[EnhancementName]`. The CreateEnhancement function now calls the appropriate enhancement function automatically by that name format. diff --git a/units/UAL0001/UAL0001_script.lua b/units/UAL0001/UAL0001_script.lua index fcbdc7e5cd..8070c43ae8 100644 --- a/units/UAL0001/UAL0001_script.lua +++ b/units/UAL0001/UAL0001_script.lua @@ -49,7 +49,7 @@ UAL0001 = ClassUnit(ACUUnit) { self:HideBone('Right_Upgrade', true) self:HideBone('Left_Upgrade', true) -- Set initial range of Chrono here so that max range can be displayed in the UI - local bpDisrupt = self:GetBlueprint().Weapon[1].MaxRadius + local bpDisrupt = self.Blueprint.Weapon[1].MaxRadius local cd = self:GetWeaponByLabel('ChronoDampener') cd:ChangeMaxRadius(bpDisrupt) -- Restrict what enhancements will enable later @@ -67,218 +67,316 @@ UAL0001 = ClassUnit(ACUUnit) { EffectUtil.CreateAeonCommanderBuildingEffects(self, unitBeingBuilt, self.BuildEffectBones, self.BuildEffectsBag) end, - CreateEnhancement = function(self, enh) - ACUUnit.CreateEnhancement(self, enh) - local bp = self:GetBlueprint().Enhancements[enh] - -- Resource Allocation - if enh == 'ResourceAllocation' then - local bp = self:GetBlueprint().Enhancements[enh] - local bpEcon = self:GetBlueprint().Economy - if not bp then return end - self:SetProductionPerSecondEnergy((bp.ProductionPerSecondEnergy + bpEcon.ProductionPerSecondEnergy) or 0) - self:SetProductionPerSecondMass((bp.ProductionPerSecondMass + bpEcon.ProductionPerSecondMass) or 0) - elseif enh == 'ResourceAllocationRemove' then - local bpEcon = self:GetBlueprint().Economy - self:SetProductionPerSecondEnergy(bpEcon.ProductionPerSecondEnergy or 0) - self:SetProductionPerSecondMass(bpEcon.ProductionPerSecondMass or 0) - elseif enh == 'ResourceAllocationAdvanced' then - local bp = self:GetBlueprint().Enhancements[enh] - local bpEcon = self:GetBlueprint().Economy - if not bp then return end - self:SetProductionPerSecondEnergy((bp.ProductionPerSecondEnergy + bpEcon.ProductionPerSecondEnergy) or 0) - self:SetProductionPerSecondMass((bp.ProductionPerSecondMass + bpEcon.ProductionPerSecondMass) or 0) - elseif enh == 'ResourceAllocationAdvancedRemove' then - local bpEcon = self:GetBlueprint().Economy - self:SetProductionPerSecondEnergy(bpEcon.ProductionPerSecondEnergy or 0) - self:SetProductionPerSecondMass(bpEcon.ProductionPerSecondMass or 0) - -- Shields - elseif enh == 'Shield' then - self:AddToggleCap('RULEUTC_ShieldToggle') - self:SetEnergyMaintenanceConsumptionOverride(bp.MaintenanceConsumptionPerSecondEnergy or 0) - self:SetMaintenanceConsumptionActive() - self:CreateShield(bp) - elseif enh == 'ShieldRemove' then - self:DestroyShield() - self:SetMaintenanceConsumptionInactive() - self:RemoveToggleCap('RULEUTC_ShieldToggle') - elseif enh == 'ShieldHeavy' then - self:AddToggleCap('RULEUTC_ShieldToggle') - self:ForkThread(self.CreateHeavyShield, bp) - elseif enh == 'ShieldHeavyRemove' then - self:DestroyShield() - self:SetMaintenanceConsumptionInactive() - self:RemoveToggleCap('RULEUTC_ShieldToggle') - -- Teleporter - elseif enh == 'Teleporter' then - self:AddCommandCap('RULEUCC_Teleport') - elseif enh == 'TeleporterRemove' then - self:RemoveCommandCap('RULEUCC_Teleport') - -- Chrono Dampener - elseif enh == 'ChronoDampener' then - self:SetWeaponEnabledByLabel('ChronoDampener', true) - if not Buffs['AeonACUChronoDampener'] then - BuffBlueprint { - Name = 'AeonACUChronoDampener', - DisplayName = 'AeonACUChronoDampener', - BuffType = 'DamageStabilization', - Stacks = 'REPLACE', - Duration = -1, - Affects = { - MaxHealth = { - Add = bp.NewHealth, - Mult = 1.0, - }, + + --------------------------------------------------------------------------- + --#region Enhancements + + ---@param self UAL0001 + ---@param bp UnitBlueprintEnhancement + ProcessEnhancementResourceAllocation = function(self, bp) + if not bp then return end + local bpEcon = self.Blueprint.Economy + self:SetProductionPerSecondEnergy((bp.ProductionPerSecondEnergy + bpEcon.ProductionPerSecondEnergy) or 0) + self:SetProductionPerSecondMass((bp.ProductionPerSecondMass + bpEcon.ProductionPerSecondMass) or 0) + end, + + ---@param self UAL0001 + ---@param bp UnitBlueprintEnhancement + ProcessEnhancementResourceAllocationRemove = function(self, bp) + local bpEcon = self.Blueprint.Economy + self:SetProductionPerSecondEnergy(bpEcon.ProductionPerSecondEnergy or 0) + self:SetProductionPerSecondMass(bpEcon.ProductionPerSecondMass or 0) + end, + + ---@param self UAL0001 + ---@param bp UnitBlueprintEnhancement + ProcessEnhancementResourceAllocationAdvanced = function(self, bp) + if not bp then return end + local bpEcon = self.Blueprint.Economy + self:SetProductionPerSecondEnergy((bp.ProductionPerSecondEnergy + bpEcon.ProductionPerSecondEnergy) or 0) + self:SetProductionPerSecondMass((bp.ProductionPerSecondMass + bpEcon.ProductionPerSecondMass) or 0) + end, + + ---@param self UAL0001 + ---@param bp UnitBlueprintEnhancement + ProcessEnhancementResourceAllocationAdvancedRemove = function(self, bp) + local bpEcon = self.Blueprint.Economy + self:SetProductionPerSecondEnergy(bpEcon.ProductionPerSecondEnergy or 0) + self:SetProductionPerSecondMass(bpEcon.ProductionPerSecondMass or 0) + end, + + ---@param self UAL0001 + ---@param bp UnitBlueprintEnhancement # This enhancement blueprint also includes the fields from `UnitBlueprintDefenseShield` + ProcessEnhancementShield = function(self, bp) + self:AddToggleCap('RULEUTC_ShieldToggle') + self:SetEnergyMaintenanceConsumptionOverride(bp.MaintenanceConsumptionPerSecondEnergy or 0) + self:SetMaintenanceConsumptionActive() + self:CreateShield(bp) + end, + + ---@param self UAL0001 + ---@param bp UnitBlueprintEnhancement + ProcessEnhancementShieldRemove = function(self, bp) + self:DestroyShield() + self:SetMaintenanceConsumptionInactive() + self:RemoveToggleCap('RULEUTC_ShieldToggle') + end, + + ---@param self UAL0001 + ---@param bp UnitBlueprintEnhancement # This enhancement blueprint also includes the fields from `UnitBlueprintDefenseShield` + ProcessEnhancementShieldHeavy = function(self, bp) + self:AddToggleCap('RULEUTC_ShieldToggle') + self:ForkThread(self.CreateHeavyShield, bp) + end, + + ---@param self UAL0001 + ---@param bp UnitBlueprintEnhancement + ProcessEnhancementShieldHeavyRemove = function(self, bp) + self:DestroyShield() + self:SetMaintenanceConsumptionInactive() + self:RemoveToggleCap('RULEUTC_ShieldToggle') + end, + + ---@param self UAL0001 + ---@param bp UnitBlueprintEnhancement + ProcessEnhancementTeleporter = function(self, bp) + self:AddCommandCap('RULEUCC_Teleport') + end, + + ---@param self UAL0001 + ---@param bp UnitBlueprintEnhancement + ProcessEnhancementTeleporterRemove = function(self, bp) + self:RemoveCommandCap('RULEUCC_Teleport') + end, + + ---@param self UAL0001 + ---@param bp UnitBlueprintEnhancement + ProcessEnhancementChronoDampener = function(self, bp) + self:SetWeaponEnabledByLabel('ChronoDampener', true) + if not Buffs['AeonACUChronoDampener'] then + BuffBlueprint { + Name = 'AeonACUChronoDampener', + DisplayName = 'AeonACUChronoDampener', + BuffType = 'DamageStabilization', + Stacks = 'REPLACE', + Duration = -1, + Affects = { + MaxHealth = { + Add = bp.NewHealth, + Mult = 1.0, }, - } - end - Buff.ApplyBuff(self, 'AeonACUChronoDampener') - elseif enh == 'ChronoDampenerRemove' then - if Buff.HasBuff(self, 'AeonACUChronoDampener') then - Buff.RemoveBuff(self, 'AeonACUChronoDampener') - end - self:SetWeaponEnabledByLabel('ChronoDampener', false) - -- T2 Engineering - elseif enh =='AdvancedEngineering' then - local bp = self:GetBlueprint().Enhancements[enh] - if not bp then return end - local cat = ParseEntityCategory(bp.BuildableCategoryAdds) - self:RemoveBuildRestriction(cat) + }, + } + end + Buff.ApplyBuff(self, 'AeonACUChronoDampener') + end, + + ---@param self UAL0001 + ---@param bp UnitBlueprintEnhancement + ProcessEnhancementChronoDampenerRemove = function(self, bp) + if Buff.HasBuff(self, 'AeonACUChronoDampener') then + Buff.RemoveBuff(self, 'AeonACUChronoDampener') + end + self:SetWeaponEnabledByLabel('ChronoDampener', false) + end, + + ---@param self UAL0001 + ---@param bp UnitBlueprintEnhancement + ProcessEnhancementAdvancedEngineering = function(self, bp) + if not bp then return end + local cat = ParseEntityCategory(bp.BuildableCategoryAdds) + self:RemoveBuildRestriction(cat) if not Buffs['AeonACUT2BuildRate'] then - BuffBlueprint { - Name = 'AeonACUT2BuildRate', - DisplayName = 'AeonACUT2BuildRate', - BuffType = 'ACUBUILDRATE', - Stacks = 'REPLACE', - Duration = -1, - Affects = { - BuildRate = { - Add = bp.NewBuildRate - self:GetBlueprint().Economy.BuildRate, - Mult = 1, - }, - MaxHealth = { - Add = bp.NewHealth, - Mult = 1.0, - }, - Regen = { - Add = bp.NewRegenRate, - Mult = 1.0, - }, + BuffBlueprint { + Name = 'AeonACUT2BuildRate', + DisplayName = 'AeonACUT2BuildRate', + BuffType = 'ACUBUILDRATE', + Stacks = 'REPLACE', + Duration = -1, + Affects = { + BuildRate = { + Add = bp.NewBuildRate - self.Blueprint.Economy.BuildRate, + Mult = 1, + }, + MaxHealth = { + Add = bp.NewHealth, + Mult = 1.0, + }, + Regen = { + Add = bp.NewRegenRate, + Mult = 1.0, + }, + }, + } + end + Buff.ApplyBuff(self, 'AeonACUT2BuildRate') + end, + + ---@param self UAL0001 + ---@param bp UnitBlueprintEnhancement + ProcessEnhancementAdvancedEngineeringRemove = function(self, bp) + self:RestoreBuildRestrictions() + self:AddBuildRestriction(categories.AEON * (categories.BUILTBYTIER2COMMANDER + categories.BUILTBYTIER3COMMANDER)) + if Buff.HasBuff(self, 'AeonACUT2BuildRate') then + Buff.RemoveBuff(self, 'AeonACUT2BuildRate') + end + end, + + ---@param self UAL0001 + ---@param bp UnitBlueprintEnhancement + ProcessEnhancementT3Engineering = function(self, bp) + if not bp then return end + local cat = ParseEntityCategory(bp.BuildableCategoryAdds) + self:RemoveBuildRestriction(cat) + if not Buffs['AeonACUT3BuildRate'] then + BuffBlueprint { + Name = 'AeonACUT3BuildRate', + DisplayName = 'AeonCUT3BuildRate', + BuffType = 'ACUBUILDRATE', + Stacks = 'REPLACE', + Duration = -1, + Affects = { + BuildRate = { + Add = bp.NewBuildRate - self.Blueprint.Economy.BuildRate, + Mult = 1, + }, + MaxHealth = { + Add = bp.NewHealth, + Mult = 1.0, }, - } - end - Buff.ApplyBuff(self, 'AeonACUT2BuildRate') - elseif enh =='AdvancedEngineeringRemove' then - local bp = self:GetBlueprint().Economy.BuildRate - if not bp then return end - self:RestoreBuildRestrictions() - self:AddBuildRestriction(categories.AEON * (categories.BUILTBYTIER2COMMANDER + categories.BUILTBYTIER3COMMANDER)) - if Buff.HasBuff(self, 'AeonACUT2BuildRate') then - Buff.RemoveBuff(self, 'AeonACUT2BuildRate') - end - -- T3 Engineering - elseif enh =='T3Engineering' then - local bp = self:GetBlueprint().Enhancements[enh] - if not bp then return end - local cat = ParseEntityCategory(bp.BuildableCategoryAdds) - self:RemoveBuildRestriction(cat) - if not Buffs['AeonACUT3BuildRate'] then - BuffBlueprint { - Name = 'AeonACUT3BuildRate', - DisplayName = 'AeonCUT3BuildRate', - BuffType = 'ACUBUILDRATE', - Stacks = 'REPLACE', - Duration = -1, - Affects = { - BuildRate = { - Add = bp.NewBuildRate - self:GetBlueprint().Economy.BuildRate, - Mult = 1, - }, - MaxHealth = { - Add = bp.NewHealth, - Mult = 1.0, - }, - Regen = { - Add = bp.NewRegenRate, - Mult = 1.0, - }, + Regen = { + Add = bp.NewRegenRate, + Mult = 1.0, }, - } - end - Buff.ApplyBuff(self, 'AeonACUT3BuildRate') - elseif enh =='T3EngineeringRemove' then - local bp = self:GetBlueprint().Economy.BuildRate - if not bp then return end - self:RestoreBuildRestrictions() - self:AddBuildRestriction(categories.AEON * (categories.BUILTBYTIER2COMMANDER + categories.BUILTBYTIER3COMMANDER)) - if Buff.HasBuff(self, 'AeonACUT3BuildRate') then - Buff.RemoveBuff(self, 'AeonACUT3BuildRate') - end - -- Crysalis Beam - elseif enh == 'CrysalisBeam' then - local wep = self:GetWeaponByLabel('RightDisruptor') - wep:ChangeMaxRadius(bp.NewMaxRadius or 30) - local oc = self:GetWeaponByLabel('OverCharge') - oc:ChangeMaxRadius(bp.NewMaxRadius or 30) - local aoc = self:GetWeaponByLabel('AutoOverCharge') - aoc:ChangeMaxRadius(bp.NewMaxRadius or 30) - local cd = self:GetWeaponByLabel('ChronoDampener') - cd:ChangeMaxRadius(bp.NewMaxRadius or 30) - elseif enh == 'CrysalisBeamRemove' then - local wep = self:GetWeaponByLabel('RightDisruptor') - local bpDisrupt = self:GetBlueprint().Weapon[1].MaxRadius - wep:ChangeMaxRadius(bpDisrupt or 22) - local oc = self:GetWeaponByLabel('OverCharge') - oc:ChangeMaxRadius(bpDisrupt or 22) - local aoc = self:GetWeaponByLabel('AutoOverCharge') - aoc:ChangeMaxRadius(bpDisrupt or 22) - local cd = self:GetWeaponByLabel('ChronoDampener') - cd:ChangeMaxRadius(bpDisrupt or 22) - -- Advanced Cryslised Beam - elseif enh == 'FAF_CrysalisBeamAdvanced' then - local wep = self:GetWeaponByLabel('RightDisruptor') - wep:ChangeMaxRadius(bp.NewMaxRadius or 35) - local oc = self:GetWeaponByLabel('OverCharge') - oc:ChangeMaxRadius(bp.NewMaxRadius or 35) - local aoc = self:GetWeaponByLabel('AutoOverCharge') - aoc:ChangeMaxRadius(bp.NewMaxRadius or 35) - local cd = self:GetWeaponByLabel('ChronoDampener') - cd:ChangeMaxRadius(bp.NewMaxRadius or 35) - elseif enh == 'FAF_CrysalisBeamAdvancedRemove' then - local wep = self:GetWeaponByLabel('RightDisruptor') - local bpDisrupt = self:GetBlueprint().Weapon[1].MaxRadius - wep:ChangeMaxRadius(bpDisrupt or 22) - local oc = self:GetWeaponByLabel('OverCharge') - oc:ChangeMaxRadius(bpDisrupt or 22) - local aoc = self:GetWeaponByLabel('AutoOverCharge') - aoc:ChangeMaxRadius(bpDisrupt or 22) - local cd = self:GetWeaponByLabel('ChronoDampener') - cd:ChangeMaxRadius(bpDisrupt or 22) - -- Heat Sink Augmentation - elseif enh == 'HeatSink' then - local wep = self:GetWeaponByLabel('RightDisruptor') - wep:ChangeRateOfFire(bp.NewRateOfFire or 2) - elseif enh == 'HeatSinkRemove' then - local wep = self:GetWeaponByLabel('RightDisruptor') - local bpDisrupt = self:GetBlueprint().Weapon[1].RateOfFire - wep:ChangeRateOfFire(bpDisrupt or 1) - -- Enhanced Sensor Systems - elseif enh == 'EnhancedSensors' then - self:SetIntelRadius('Vision', bp.NewVisionRadius or 104) - self:SetIntelRadius('Omni', bp.NewOmniRadius or 104) - elseif enh == 'EnhancedSensorsRemove' then - local bpIntel = self:GetBlueprint().Intel - self:SetIntelRadius('Vision', bpIntel.VisionRadius or 26) - self:SetIntelRadius('Omni', bpIntel.OmniRadius or 26) - end + }, + } + end + Buff.ApplyBuff(self, 'AeonACUT3BuildRate') + end, + + ---@param self UAL0001 + ---@param bp UnitBlueprintEnhancement + ProcessEnhancementT3EngineeringRemove = function(self, bp) + self:RestoreBuildRestrictions() + self:AddBuildRestriction(categories.AEON * (categories.BUILTBYTIER2COMMANDER + categories.BUILTBYTIER3COMMANDER)) + if Buff.HasBuff(self, 'AeonACUT3BuildRate') then + Buff.RemoveBuff(self, 'AeonACUT3BuildRate') + end + end, + + ---@param self UAL0001 + ---@param bp UnitBlueprintEnhancement + ProcessEnhancementCrysalisBeam = function(self, bp) + local wep = self:GetWeaponByLabel('RightDisruptor') + wep:ChangeMaxRadius(bp.NewMaxRadius or 30) + local oc = self:GetWeaponByLabel('OverCharge') + oc:ChangeMaxRadius(bp.NewMaxRadius or 30) + local aoc = self:GetWeaponByLabel('AutoOverCharge') + aoc:ChangeMaxRadius(bp.NewMaxRadius or 30) + local cd = self:GetWeaponByLabel('ChronoDampener') + cd:ChangeMaxRadius(bp.NewMaxRadius or 30) + end, + + ---@param self UAL0001 + ---@param bp UnitBlueprintEnhancement + ProcessEnhancementCrysalisBeamRemove = function(self, bp) + local wep = self:GetWeaponByLabel('RightDisruptor') + local bpDisrupt = self.Blueprint.Weapon[1].MaxRadius + wep:ChangeMaxRadius(bpDisrupt or 22) + local oc = self:GetWeaponByLabel('OverCharge') + oc:ChangeMaxRadius(bpDisrupt or 22) + local aoc = self:GetWeaponByLabel('AutoOverCharge') + aoc:ChangeMaxRadius(bpDisrupt or 22) + local cd = self:GetWeaponByLabel('ChronoDampener') + cd:ChangeMaxRadius(bpDisrupt or 22) + end, + + ---@param self UAL0001 + ---@param bp UnitBlueprintEnhancement + ProcessEnhancementFAF_CrysalisBeamAdvanced = function(self, bp) + local wep = self:GetWeaponByLabel('RightDisruptor') + wep:ChangeMaxRadius(bp.NewMaxRadius or 35) + local oc = self:GetWeaponByLabel('OverCharge') + oc:ChangeMaxRadius(bp.NewMaxRadius or 35) + local aoc = self:GetWeaponByLabel('AutoOverCharge') + aoc:ChangeMaxRadius(bp.NewMaxRadius or 35) + local cd = self:GetWeaponByLabel('ChronoDampener') + cd:ChangeMaxRadius(bp.NewMaxRadius or 35) + end, + + ---@param self UAL0001 + ---@param bp UnitBlueprintEnhancement + ProcessEnhancementFAF_CrysalisBeamAdvancedRemove = function(self, bp) + local wep = self:GetWeaponByLabel('RightDisruptor') + local bpDisrupt = self.Blueprint.Weapon[1].MaxRadius + wep:ChangeMaxRadius(bpDisrupt or 22) + local oc = self:GetWeaponByLabel('OverCharge') + oc:ChangeMaxRadius(bpDisrupt or 22) + local aoc = self:GetWeaponByLabel('AutoOverCharge') + aoc:ChangeMaxRadius(bpDisrupt or 22) + local cd = self:GetWeaponByLabel('ChronoDampener') + cd:ChangeMaxRadius(bpDisrupt or 22) + end, + + ---@param self UAL0001 + ---@param bp UnitBlueprintEnhancement + ProcessEnhancementHeatSink = function(self, bp) + local wep = self:GetWeaponByLabel('RightDisruptor') + wep:ChangeRateOfFire(bp.NewRateOfFire or 2) + end, + + ---@param self UAL0001 + ---@param bp UnitBlueprintEnhancement + ProcessEnhancementHeatSinkRemove = function(self, bp) + local wep = self:GetWeaponByLabel('RightDisruptor') + local bpDisrupt = self.Blueprint.Weapon[1].RateOfFire + wep:ChangeRateOfFire(bpDisrupt or 1) + end, + + ---@param self UAL0001 + ---@param bp UnitBlueprintEnhancement + ProcessEnhancementEnhancedSensors = function(self, bp) + self:SetIntelRadius('Vision', bp.NewVisionRadius or 104) + self:SetIntelRadius('Omni', bp.NewOmniRadius or 104) + end, + + ---@param self UAL0001 + ---@param bp UnitBlueprintEnhancement + ProcessEnhancementEnhancedSensorsRemove = function(self, bp) + local bpIntel = self.Blueprint.Intel + self:SetIntelRadius('Vision', bpIntel.VisionRadius or 26) + self:SetIntelRadius('Omni', bpIntel.OmniRadius or 26) end, + ---@param self UAL0001 + ---@param enh Enhancement + CreateEnhancement = function(self, enh) + ACUUnit.CreateEnhancement(self, enh) + + local bp = self.Blueprint.Enhancements[enh] + + if not bp then return end + + local ref = 'ProcessEnhancement' .. enh + local handler = self[ref] + if handler then + handler(self, bp) + else + WARN("Missing enhancement: ", enh, " for unit: ", self:GetUnitId(), " note that the function name should be called: ", ref) + end + end, + + ---@param self UAL0001 + ---@param bp UnitBlueprintEnhancement # This enhancement blueprint also includes the fields from `UnitBlueprintDefenseShield` CreateHeavyShield = function(self, bp) WaitTicks(1) self:CreateShield(bp) self:SetEnergyMaintenanceConsumptionOverride(bp.MaintenanceConsumptionPerSecondEnergy or 0) self:SetMaintenanceConsumptionActive() end + + --#endregion } TypeClass = UAL0001