So, I just want to reiterate what you said earlier Keith and make sure I've got it square.
We're getting closer
CPAs (Cross Planet Attacks) are timer based assaults launched by the AIs. Their strength is determined by either AIP or by time since start of game, whichever gives the CPA the greatest strength. Note: Core CPA Guardposts are determined solely by AIP. (Formula: 100 * Difficulty * AIP/50 (or 1, if 1 is greater) * (0.67 + (0.33 * # of Human Homeworlds)) ) Once released, they act similarly to Cross Planet Waves, in that they are simply released threat.
If you're going to include the Core CPA formula may as well include the normal one (that's why I pointed to the logs, though obviously getting a natural CPA to be announced for observation purposes is nontrivial unless you have a handy save), so here's an example log entry from a fairly vanilla diff 7 game:
effectiveAIPForCPAPurposes = this.AIProgressionLevelEffective = 99
since diff > 7, numberOfSecondsPerPointOfMinimumAIPForCPAPurposes = 360 + ( (FInt)60 * ( (FInt)7 - this.AIDifficulty ) ).IntValue = 342
minimumAIPForCPAPurposes = Game.Instance.GameSecond / numberOfSecondsPerPointOfMinimumAIPForCPAPurposes = 43
effectiveAIPForCPAPurposes = Max(effectiveAIPForCPAPurposes,minimumAIPForCPAPurposes) = 99
difficultyFactor = ( this.AIDifficulty * this.GetHandicapMultiplier() ) / ( 13 - this.AIDifficulty ) : 1.28
simulateMaxTimeWaveFactor = Mat.One + ( ( this.AIDifficulty * 2 ) / ( ( 14 - this.AIDifficulty ) * 3 ) ) : 1.73
simulateDoubleWaveFactor = 2
simulateDifficultySpecificWaveSizeMultiplier = 2.5
humanHomeworldCountMultiplier = Mat.One + ( (FInt)( humanHomeworldCount - 1 ) * FInt.FromParts( 0, 330 ) ) + ( championCount * FInt.FromParts( 0, 066 ) ) = 1
numberOfShips (before applying cap-scale) = ( (FInt)effectiveAIPForCPAPurposes * difficultyFactor * simulateMaxTimeWaveFactor * simulateDoubleWaveFactor * simulateDifficultySpecificWaveSizeMultiplier * humanHomeworldCountMultiplier ).IntValue = 1094
And to break it down:
effectiveAIPForCPAPurposes = this.AIProgressionLevelEffective = 99
This is just the AIP.
since diff > 7, numberOfSecondsPerPointOfMinimumAIPForCPAPurposes = 360 + ( (FInt)60 * ( (FInt)7 - this.AIDifficulty ) ).IntValue = 342
This is what determines the scale of the "alternative minimum AIP". At diff 7 it's just 360, and below diff 7 it's "360 + ( (FInt)51 * ( (FInt)7 - this.AIDifficulty ) ).IntValue;"
minimumAIPForCPAPurposes = Game.Instance.GameSecond / numberOfSecondsPerPointOfMinimumAIPForCPAPurposes = 43
Here it's just computing the actual "alternative minimum AIP"
effectiveAIPForCPAPurposes = Max(effectiveAIPForCPAPurposes,minimumAIPForCPAPurposes) = 99
And here it picks either AIP or the time-based minimum. Here the real AIP is (substantially) higher, so the time-based minimum has no effect at all.
difficultyFactor = ( this.AIDifficulty * this.GetHandicapMultiplier() ) / ( 13 - this.AIDifficulty ) : 1.28
This is the diff/handicap multiplier adapted from wave-calc logic.
The "difficultyFactor" variable name is a bit imprecise, in that difficulty is factored in many times throughout this whole tour-de-pain. Its overall impact is quite explodential.
simulateMaxTimeWaveFactor = Mat.One + ( ( this.AIDifficulty * 2 ) / ( ( 14 - this.AIDifficulty ) * 3 ) ) : 1.73
This is simulating the max size increase a wave can get from simply having been a long time since the last wave. Iirc waves can have a higher multiplier than this now due to the ingress-point calculation thing (which does not apply here) but that's where this is originally from.
simulateDoubleWaveFactor = 2
This just simulates what it's like when both AIs wave you simultaneously.
simulateDifficultySpecificWaveSizeMultiplier = 2.5
This is also used by waves and is just a lookup really:
if diff <= 3, then 1
else if diff <= 4, then 1.5
else if diff <= 5, then 1.75
else if diff <= 6, then 2
else if diff <= 7, then 2.25
else if diff <= 9, then 2.5
else if diff <= 9.3, then 2.75
else if diff <= 9.6, then 3
else if diff <= 9.8, then 3.8
else (that is, diff 10), then 4.5
humanHomeworldCountMultiplier = Mat.One + ( (FInt)( humanHomeworldCount - 1 ) * FInt.FromParts( 0, 330 ) ) + ( championCount * FInt.FromParts( 0, 066 ) ) = 1
Just accounting for champs and multiple HWs.
numberOfShips (before applying cap-scale) = ( (FInt)effectiveAIPForCPAPurposes * difficultyFactor * simulateMaxTimeWaveFactor * simulateDoubleWaveFactor * simulateDifficultySpecificWaveSizeMultiplier * humanHomeworldCountMultiplier ).IntValue = 1094
Putting it all together. As Bognor pointed out, if the result would be less than 200 here it just doesn't bother and skips the CPA.
The next and final step in computing the actual number of ship is: on normal caps divide this by 2; on low it divides by 4, and on ultra low it divides by 8.
The first rule is tech limitations for the wave.
At under Difficulty 7 the tech limit is maxxed at 2.
At 7 -> 7.8 the max tech limit is either 2, or the AIP tech level if it is larger.
At 8+, the max tech limit is 5. AIP doesn't matter.
The numbers are right, but this is not a maximum tech level; it is the tech level at which it starts looking, nothing more. Long ago it was a max, but CPAs kept coming up short, etc, and they're just more fun when they can dump a load of mkIV on you if things are strange
The following process will be cycled through from Current AIP Tech level (or AI tech level, should they have an enforced level, such as a technologist) down through to 1, and then from +1 current level through to 5 if you're at 8+. Planets will only be considered if the humans are not in force on the planet (less than 1/3 the AI's military ships).
Just remove the "if you're at 8+" and that's correct. I'm not sure what's meant by the "(or AI tech level, should they have an enforced level, such as a technologist)" bit, though.
Examples:
Diff 6 and lower: If AIP is at tech level 3, it is maxxed at 2. So it will perform the planet search cycle for Tech II ships, then Tech I ships, then stop.
Diff 7 -> 7.8: If AIP is at tech level 3, it is maxxed at 3. So it will perform the planet search cycle for Tech III ships, then Tech II ships, then Tech I ships, then stop.
Diff 8+: If AIP is at tech level 3, it is uncapped. So it will perform the planet search cycle for Tech III ships, then Tech II ships, then Tech I ships, then Tech IV ships, then Tech V ships. Then stop.
The last example is correct, for the first two: instead of stopping they would go back to (target tech level + 1) and count up to 5, then stop.
One thing I'm not sure I mentioned: homeworlds are skipped in the planet loop
Guardians, Starships, and Carriers are not considered available, nor are ships under permanent forcefields (Spire Shield Guardpost, Forcefield III, etc.)
Just a quibble: it's not "permanent forcefields", it's "immobile forcefields". For that matter, it probably counts module forcefields since they have a move speed of zero.
Everything else looks right