Calculating Combat Power

Freeform discussion about anything related to modding Transcendence.
Post Reply
User avatar
0xABCDEF
Militia Lieutenant
Militia Lieutenant
Posts: 124
Joined: Thu May 19, 2016 12:58 am
Location: Was destroyed by a Phobos-class dreadnought in the Eridani system

The function objGetCombatPower is not very useful. It may be able to give you a clue about how strong a Phobos-class dreadnought is and the number it returns may reflect how much you should fear the capital ship's plasma and ion DPS. However, objGetCombatPower will not tell you how what might happen if that ship were to encounter another ship that just somehow happens to be completely immune to both plasma and ion damage. It does not tell you how many Aquilas the Commonwealth would need to destroy one Phobos.

Now there is no such thing as "absolute" combat power because the complexity of all the game mechanics leaves everything up to context. High maneuverability does not always help against a ship armed with many omnidirectional weapons. Shield buster weapons do nothing extra when you face a enemy with strong armor and no shields.

The best that we can do in terms of calculating combat strength is by comparing ships to each other. In order to do this, I have created a function called shpGetMatchup. It compares two ships based on their damage output by type, total hp, and resistances by type. Although it leaves out everything besides those two factors, it is relatively simple Here is the procedure:
  • We get the total hp of each ship, including all the interior compartments, armor segments and shield generators.
  • We average out the damage adjustments of every armor segment and shield generator for each ship.
  • For both ships, we take the maximum damage output per 180 ticks for each damage type of all the ship's non-launcher, non-linked, primary weapons. Only one weapon can be used at a time, so these values are "competitive." If a ship has two blast weapons and one of them is stronger, then that weapon's output becomes the returned value in the "blast" struct field.
  • For both ships, we take the total damage output per 180 ticks for each damage type of all the ship's launcher/linked weapons. Given that all of these weapons can often be fired together, we consider their damage outputs to be "additive." If a ship has two linked ion weapons, then the returned value is both of the ion weapons' outputs added together.
  • We take each value of a ship's primary-damage-output-per-type and adjust it to the other ship's resistances.
  • We take each value of a ship's launcher/linked-damage-output-per-type and adjust it to the other ship's resistances. Then we add all the values together.
  • We then take the minimum survival time of each ship, assuming full fire rate and 100% hit rate. We calculate the time it would take a ship to destroy the other using each primary weapon exclusively (we assume that the ships never change primary weapons and have infinite ammo) along with the launcher/linked weapons.
  • If the minimum survival time for one ship is Nil, that means that the ship is either immune to the other ship's attacks or the other ship is unarmed. If both ships have Nil survival time, we have a "Definite Stalemate." If one ship has Nil survival time, the other ship gets a "Definite Win." Otherwise, we return the ratio of the first ship's survival time to the second ship's survival time.
Here is the above procedure implemented in code. Edit: Sorry, it seems that the code formatting broke. To see the original, right-click and use View Page Source then use Ctrl+F with the string "shpGetShieldHP"

Code: Select all

(block Nil
	(setq shpGetShieldHP (lambda (ship)
		(- (shpGetShieldMaxHitPoints ship) (shpGetShieldDamage ship))
		))
	;Struct containing average damage adjustment percentage of each damage type for all installed armor segments and shield generators on the ship
	(setq shpGetAverageDamageAdjByType (lambda (ship)
		(block ((defense (objGetItems ship "asI")) (defenseCount (count defense)) result)
			(enum defense theDefense
				(block	((damageAdj (itmGetProperty theDefense 'damageAdj)))
					(enum dmgNameList theDamageType
						(setq result (inc@ result theDamageType (@ damageAdj theDamageType)))
						)
					)
				)
			(enum dmgNameList theDamageType
				(set@ result theDamageType (/ (@ result theDamageType) defenseCount))
				)
			result
			)
		))
	;Converts average damage adjustment percentage for each type to a multiplier
	(setq shpGetAverageDamageMultiplierByType (lambda (ship)
		(block ((result (shpGetAverageDamageAdjByType ship)))
			(enum dmgNameList theDamageType
				(set@ result theDamageType (/ (@ result theDamageType) 100))
				)
			result
			)
		))
	;Returns unadjusted hp
	(setq shpGetTotalHP (lambda (ship)
		(+ (apply add (map (objGetItems ship "aI") theArmor (itmGetProperty theArmor 'hp))) (shpGetShieldHP ship))
		))
	;Returns a struct with the total adjusted hp for each damage type
	(setq shpGetTotalHPByType (lambda (ship)
		(block	(
					(interiorHP (objGetProperty ship 'interiorHP))
					result
					)
			(objEnumItems ship "asI" theDefense
				(block	(
							(adjustedHPByType (itmGetAdjustedHPByType theDefense))
							)
					(enum
						dmgNameList
						theDamageType
						(setq result
							(inc@ result theDamageType
								(@ adjustedHPByType theDamageType)
								)
							)
						)
					)
				)
			(enum dmgNameList theDamageType (setq result (inc@ result theDamageType interiorHP)))
			result
			)
		))
	;Primary weapon damage is "competitive" (i.e. only one primary is used at a time) 
	(setq shpGetPrimaryDamageByType (lambda (ship)
		(block	(
					result
					)
			(enum
				(filter (objGetItems ship "pI") thePrimary (not (itmGetProperty thePrimary 'linkedFireOptions)))
				thePrimary
				(block ((damageName (itmGetDamageName thePrimary)))
					(setq result (set@ result damageName
						(max
							(itmGetProperty thePrimary 'damage)
							(@ result damageName)
							)
						))
					)
				)
			result
			)
		))
	;Secondary weapon damage is additive (i.e. all secondary weapons are used together)
	(setq shpGetNonPrimaryDamageByType (lambda (ship)
		(block	(
					(linked (shpGetLinkedDamageByType ship))
					(launcher (shpGetLauncherDamageByType ship))
					result
					)
			(enum dmgNameList theDamageType
				(setq result (set@ result theDamageType (+ (@ linked theDamageType) (@ launcher theDamageType))))
				)
			)
		))
	;Linked damage is additive
	(setq shpGetLinkedDamageByType (lambda (ship)
		(block (result)
			(enum (filter (objGetItems ship "pI") thePrimary (itmGetProperty thePrimary 'linkedFireOptions)) thePrimary
				(setq result (inc@ result (itmGetDamageName thePrimary) (itmGetProperty thePrimary 'damage)))
				)
			result
			)
		))
	;Launcher damage is additive
	(setq shpGetLauncherDamageByType (lambda (ship)
		(block (result)
			(objEnumItems ship "lI" theLauncher
				(setq result (set@ result (itmGetDamageName theLauncher) (itmGetProperty theLauncher 'damage)))
				)
			result
			)
		))
	(setq shpGetMatchup (lambda (ship1 ship2)
		(block	(
					(totalHP_ship1 (shpGetTotalHP ship1))
					(totalHP_ship2 (shpGetTotalHP ship2))
					(averageDamageMultiplierByType_ship1 (shpGetAverageDamageMultiplierByType ship1))
					(averageDamageMultiplierByType_ship2 (shpGetAverageDamageMultiplierByType ship2))
					
					;Ships can only use one non-linked primary at any time. Therefore, primary weapons compete with each other for the shortest destruction time.
					(primaryDamageByType_ship1 (shpGetPrimaryDamageByType ship1))
					(primaryDamageByType_ship2 (shpGetPrimaryDamageByType ship2))
					
					
					(nonPrimaryDamageByType_ship1 (shpGetNonPrimaryDamageByType ship1))
					(nonPrimaryDamageByType_ship2 (shpGetNonPrimaryDamageByType ship2))
					
					
					adjustedPrimaryDamageByType_ship1
					adjustedPrimaryDamageByType_ship2
					
					;Add all secondary dps values together by adjusting all of them to the other ship's damageAdj
					adjustedNonPrimaryDamage_ship1
					adjustedNonPrimaryDamage_ship2
					
					;Minimum time to destroy the ship, assuming maximum fire rate and accuracy.
					minSurvivalTime_ship1
					minSurvivalTime_ship2
					)
			(enum dmgNameList theDamageType
				(block Nil
					;Multiply the damage by each type with the enemy's damage multiplier by each type.
					(set@ adjustedPrimaryDamageByType_ship1 theDamageType
						(*
							(@ primaryDamageByType_ship1 theDamageType)
							(@ averageDamageMultiplierByType_ship2 theDamageType)
							)
						)
					(set@ adjustedPrimaryDamageByType_ship2 theDamageType
						(*
							(@ primaryDamageByType_ship2 theDamageType)
							(@ averageDamageMultiplierByType_ship1 theDamageType)
							)
						)
					;Linked or launcher damage is always additive because linked/launcher weapons do not compete for the quickest destruction time. Instead, their dps values add together
					(setq adjustedNonPrimaryDamage_ship1 (+ adjustedNonPrimaryDamage_ship1
						(*
							(@ nonPrimaryDamageByType_ship1 theDamageType)
							(@ averageDamageMultiplierByType_ship2 theDamageType)
							)
						))
					(setq adjustedNonPrimaryDamage_ship2 (+ adjustedNonPrimaryDamage_ship2
						(*
							(@ nonPrimaryDamageByType_ship2 theDamageType)
							(@ averageDamageMultiplierByType_ship1 theDamageType)
							)
						))
					)
				)
			(setq minSurvivalTime_ship1 (apply2 min
				(map dmgNameList 'excludeNil theDamageType
					(block	(
								(adjustedPrimaryDamage_ship2 (@ adjustedPrimaryDamageByType_ship2 theDamageType))
								(totalDamage_ship2 (+ adjustedPrimaryDamage_ship2 adjustedNonPrimaryDamage_ship2))
								)
						(if (gr totalDamage_ship2 0)
							(/ totalHP_ship1 totalDamage_ship2)
							)
						)
					)
				))
			(setq minSurvivalTime_ship2 (apply2 min
				(map dmgNameList 'excludeNil theDamageType
					(block	(
								(adjustedPrimaryDamage_ship1 (@ adjustedPrimaryDamageByType_ship1 theDamageType))
								(totalDamage_ship1 (+ adjustedPrimaryDamage_ship1 adjustedNonPrimaryDamage_ship1))
								)
						(if (gr totalDamage_ship1 0)
							(/ totalHP_ship2 totalDamage_ship1)
							)
						)
					)
				))
			(switch
				(not (or minSurvivalTime_ship1 minSurvivalTime_ship2)) ;Both ships are either unarmed or unable to deal damage to each other because they are immune to each other
					"Definite Stalemate"
				(not minSurvivalTime_ship1) ;If ship2 cannot deal any damage (either because it is unarmed or because ship2 is immune to ship1's damage types)
					"Definite Win"
				(not minSurvivalTime_ship2) ;If ship1 cannot deal any damage (either because it is unarmed or because ship2 is immune to ship2's damage types)
					"Definite Loss"
				(/ minSurvivalTime_ship1 minSurvivalTime_ship2)
				)
			)
		))
	(setq unvGenerateAllMatchups (lambda Nil
		(block	(
					result
					(ships (map (typFind "s") theClass (sysCreateShip theClass Nil &svPlayer;)))
					(classCount (count classes))
					)
			(enum ships ship1
				(enum ships ship2
					(setq result (cat result (subst "%1% VS %2% -> %3%:1\r\n" (objGetName ship1) (objGetName ship2) (shpGetMatchup ship1 ship2))))
					)
				)
			(enum ships theShip
				(objDestroy theShip)
				)
			(printTo 'log result)
			result
			)
		))
	(setq unvGenerateShipMatchupTable (lambda (standard)
		(block	(
					result
					(theStandard (sysCreateShip (or standard &scCenturion;) Nil &svPlayer;))
					(theStandardName (objGetName theStandard))
					)
			(enum (typFind "s") theShipClass
				(block	(
							(theShip (sysCreateShip theShipClass Nil &svPlayer;))
							(theShipName (objGetName theShip))
							(matchup (shpGetMatchup theShip theStandard))
							)
					(setq result (cat result (subst "%1% VS %2% -> %3%:1\r\n" theShipName theStandardName matchup)))
					)
				)
			(printTo 'log result)
			result
			)
		))
	)

Here is a complete ship matchup table that I generated using the code above

The ratio between the estimated minimum survival times of a ship and another ship can be useful in identifying matchups. A 1:1 ratio means that two ships are evenly matched. A 3:1 ratio means that the first ship will likely survive fighting three clones of the second ship before falling. A 0.2:1 ratio means that it would take five clones of the first ship to destroy the second ship. To determine whether a squadron of small ships would be able to take on a large ship, just add up all the matchup values for each small ship against the large ship and if the total value is greater than one, there is a good chance that the squadron will win. [I have not yet found a way to determine the matchup of two squadrons using this function.]

I believe that this algorithm is fairly effective at calculating relative combat power. What do you think?
RPC
Fleet Admiral
Fleet Admiral
Posts: 2876
Joined: Thu Feb 03, 2011 5:21 am
Location: Hmm... I'm confused. Anybody have a starmap to the Core?

The only use I can see for this at the moment is for having ships check their survivability in a certain area. This would be totally great because then we could use a generic <OnAIUpdate> function to have ships not suicide themselves 24/7 and add a layer of realism to the game.
Tutorial List on the Wiki and Installing Mods
Get on Discord for mod help and general chat
Image
Image
Der Tod ist der zeitlose Frieden und das leben ist der Krieg
Wir müssen wissen — wir werden wissen!
I don't want any sort of copyright on my Transcendence mods. Feel free to take/modify whatever you want.
Post Reply