441 lines
18 KiB
Lua

if (aiAttackWaveG) then
return aiAttackWaveG
end
local aiAttackWave = {}
-- imports
local constants = require("Constants")
local mapUtils = require("MapUtils")
local chunkPropertyUtils = require("ChunkPropertyUtils")
local unitGroupUtils = require("UnitGroupUtils")
local movementUtils = require("MovementUtils")
local mathUtils = require("MathUtils")
local config = require("__RampantFixed__/config")
local scoreChunks = require("ScoreChunks")
-- constants
local BASE_PHEROMONE = constants.BASE_PHEROMONE
local BASE_DETECTION_PHEROMONE = constants.BASE_DETECTION_PHEROMONE
local PLAYER_PHEROMONE = constants.PLAYER_PHEROMONE
local RESOURCE_PHEROMONE = constants.RESOURCE_PHEROMONE
local MAX_BASE_DETECTION_PHEROMONES_IN_CHUNK = constants.MAX_BASE_DETECTION_PHEROMONES_IN_CHUNK
local AGGRESSIVE_CAN_ATTACK_WAIT_MAX_DURATION = constants.AGGRESSIVE_CAN_ATTACK_WAIT_MAX_DURATION
local AGGRESSIVE_CAN_ATTACK_WAIT_MIN_DURATION = constants.AGGRESSIVE_CAN_ATTACK_WAIT_MIN_DURATION
local AI_SQUAD_COST = constants.AI_SQUAD_COST
local AI_SETTLER_COST = constants.AI_SETTLER_COST
local AI_VENGENCE_SQUAD_COST = constants.AI_VENGENCE_SQUAD_COST
local AI_STATE_AGGRESSIVE = constants.AI_STATE_AGGRESSIVE
local COOLDOWN_RALLY = constants.COOLDOWN_RALLY
local CHUNK_ALL_DIRECTIONS = constants.CHUNK_ALL_DIRECTIONS
local CHUNK_SIZE = constants.CHUNK_SIZE
local RALLY_CRY_DISTANCE = constants.RALLY_CRY_DISTANCE
local AI_STATE_SIEGE = constants.AI_STATE_SIEGE
local AI_STATE_MIGRATING = constants.AI_STATE_MIGRATING
local AI_STATE_RAIDING = constants.AI_STATE_RAIDING
-- imported functions
local randomTickEvent = mathUtils.randomTickEvent
local calculateKamikazeThreshold = unitGroupUtils.calculateKamikazeThreshold
local mRandom = math.random
local mMax = math.max
local mMin = math.min
local positionFromDirectionAndChunk = mapUtils.positionFromDirectionAndChunk
local getPassable = chunkPropertyUtils.getPassable
local getNestCount = chunkPropertyUtils.getNestCount
local getRaidNestActiveness = chunkPropertyUtils.getRaidNestActiveness
local getNestActiveness = chunkPropertyUtils.getNestActiveness
local getRallyTick = chunkPropertyUtils.getRallyTick
local setRallyTick = chunkPropertyUtils.setRallyTick
local gaussianRandomRange = mathUtils.gaussianRandomRange
local getNeighborChunks = mapUtils.getNeighborChunks
local getChunkByXY = mapUtils.getChunkByXY
local scoreNeighborsForFormation = movementUtils.scoreNeighborsForFormation
local scoreNeighborsForResource = movementUtils.scoreNeighborsForResource
local createSquad = unitGroupUtils.createSquad
local attackWaveScaling = config.attackWaveScaling
local settlerWaveScaling = config.settlerWaveScaling
local getAttackWaveMaxSize = config.getAttackWaveMaxSize
local getDeathGenerator = chunkPropertyUtils.getDeathGenerator
local validSiegeLocation = scoreChunks.validSiegeLocation
local validSettlerLocation = scoreChunks.validSettlerLocation
local validUnitGroupLocation = scoreChunks.validUnitGroupLocation
local scoreUnitGroupLocation = scoreChunks.scoreUnitGroupLocation
local scoreSiegeLocation = scoreChunks.scoreSiegeLocation
local scoreSettlerLocation = scoreChunks.scoreSettlerLocation
-- module code
local function attackWaveValidCandidate(chunk, map)
local isValid = getNestActiveness(map, chunk)
if (map.state == AI_STATE_RAIDING) or (map.state == AI_STATE_SIEGE) or (map.state == AI_STATE_MIGRATING) then
isValid = isValid + getRaidNestActiveness(map, chunk)
end
return (isValid > 0)
end
-- + !КДА
local function getFreeNeighborChunks(neighborChunks, map)
local freeChunks = 0
for x=1,8 do
local neighborChunk = neighborChunks[x]
if (neighborChunk ~= -1)
and (getPassable(map, neighborChunk) == CHUNK_ALL_DIRECTIONS)
and (getNestCount(map, neighborChunk) == 0)
then
freeChunks = freeChunks + 1
end
end
return freeChunks
end
-- - !КДА
local function visitPattern(o, cX, cY, distance)
local startX
local endX
local stepX
local startY
local endY
local stepY
if (o == 0) then
startX = cX - distance
endX = cX + distance
stepX = 32
startY = cY - distance
endY = cY + distance
stepY = 32
elseif (o == 1) then
startX = cX + distance
endX = cX - distance
stepX = -32
startY = cY + distance
endY = cY - distance
stepY = -32
elseif (o == 2) then
startX = cX - distance
endX = cX + distance
stepX = 32
startY = cY + distance
endY = cY - distance
stepY = -32
elseif (o == 3) then
startX = cX + distance
endX = cX - distance
stepX = -32
startY = cY - distance
endY = cY + distance
stepY = 32
end
return startX, endX, stepX, startY, endY, stepY
end
function aiAttackWave.rallyUnits(chunk, map, tick)
if ((tick - getRallyTick(map, chunk) > COOLDOWN_RALLY) and (map.points >= AI_VENGENCE_SQUAD_COST)) then
setRallyTick(map, chunk, tick)
local cX = chunk.x
local cY = chunk.y
local startX, endX, stepX, startY, endY, stepY = visitPattern(tick % 4, cX, cY, RALLY_CRY_DISTANCE)
local vengenceQueue = map.vengenceQueue
for x=startX, endX, stepX do
for y=startY, endY, stepY do
if (x ~= cX) and (y ~= cY) then
local rallyChunk = getChunkByXY(map, x, y)
if (rallyChunk ~= -1) and (getNestCount(map, rallyChunk) > 0) then
local count = vengenceQueue[rallyChunk]
if not count then
count = 0
vengenceQueue[rallyChunk] = count
end
vengenceQueue[rallyChunk] = count + 1
end
end
end
end
return true
end
end
function aiAttackWave.formSettlers(map, chunk)
local universe = map.universe
local squadCreated = false
if (universe.builderCount < universe.AI_MAX_BUILDER_COUNT) and
(mRandom() < universe.formSquadThreshold) and
((map.points - AI_SETTLER_COST) > 0)
then
local surface = map.surface
local squadPath, squadDirection
local neighborChunks = getNeighborChunks(map, chunk.x, chunk.y)
-- single nests have priority
local FreeNeighborChunks = getFreeNeighborChunks(neighborChunks, map) -- number of non-settled chunks
local settleRoll= 1/(mMax(9-FreeNeighborChunks, 1)) -- 8 = 1, 7 - 50%, 6 - 25%, 5 = 12% ...
local settlerType = "Settler"
if (map.state == AI_STATE_SIEGE) or (universe.siegeAIToggle and (mRandom() <= 0.1)) then
settlerType = "Siege"
end
if (settlerType == "Siege") then
local siegeRoll
local pheromones = chunk[BASE_DETECTION_PHEROMONE]
if pheromones > universe.no_pollution_attack_threshold then
siegeRoll = 1
elseif pheromones < universe.raiding_minimum_base_threshold then
if map.activeNests < 5 then
siegeRoll = 1
else
siegeRoll = 0
end
else
siegeRoll = (pheromones - universe.raiding_minimum_base_threshold)/(universe.no_pollution_attack_threshold - universe.raiding_minimum_base_threshold)
end
settleRoll = settleRoll * siegeRoll
end
if (settleRoll==1) or (mRandom() < settleRoll) then
-- - !КДА
if (settlerType == "Siege") then
squadPath, squadDirection = scoreNeighborsForResource(chunk,
getNeighborChunks(map, chunk.x, chunk.y),
validSiegeLocation,
scoreSiegeLocation,
map)
else
squadPath, squadDirection = scoreNeighborsForResource(chunk,
getNeighborChunks(map, chunk.x, chunk.y),
validSettlerLocation,
scoreSettlerLocation,
map)
end
if (squadPath ~= -1) then
local squadPosition = surface.find_non_colliding_position("chunk-scanner-squad-rampant",
positionFromDirectionAndChunk(squadDirection,
chunk,
universe.position,
0.98),
CHUNK_SIZE,
4,
true)
if squadPosition then
local squad = createSquad(squadPosition, map, nil, true)
squad.maxDistance = gaussianRandomRange(universe.expansionMaxDistance * 0.5,
universe.expansionMaxDistanceDerivation,
10,
universe.expansionMaxDistance)
if settlerType == "Siege" then
squad.siege = true
end
local scaledWaveSize = settlerWaveScaling(universe)
universe.formGroupCommand.group = squad.group
universe.formCommand.unit_count = scaledWaveSize
local foundUnits = surface.set_multi_command(universe.formCommand)
if (foundUnits > 0) then
squad.kamikaze = mRandom() < calculateKamikazeThreshold(foundUnits, universe)
universe.builderCount = universe.builderCount + 1
local rangeKf = 1
if map.state == AI_STATE_MIGRATING then
rangeKf = 0.7 + mMin(chunk[BASE_DETECTION_PHEROMONE]/universe.no_pollution_attack_threshold, 0.3)
end
local settlerCost = math.floor(AI_SETTLER_COST * rangeKf)
map.points = map.points - settlerCost
if universe.aiPointsPrintSpendingToChat then
game.print(map.surface.name .. ": Points: -" .. settlerCost .. ". ["..settlerType.."] Total: " .. string.format("%.2f", map.points) .. " [gps=" .. squadPosition.x .. "," .. squadPosition.y .. "]")
end
universe.groupNumberToSquad[squad.groupNumber] = squad
map.vengenceLimiter = mMax(0, map.vengenceLimiter - 3)
squadCreated = true
else
if (squad.group.valid) then
squad.group.destroy()
end
end
end
end
end
end
return squadCreated
end
function aiAttackWave.formVengenceSquad(map, chunk)
local universe = map.universe
local finalSquadCost = universe.finalSquadCost
local finalVengenceSquadCost = universe.finalVengenceSquadCost
if (universe.squadCount < universe.AI_MAX_SQUAD_COUNT) and
((map.points - finalVengenceSquadCost) > 0) and
(mRandom() < universe.formSquadThreshold)
then
local surface = map.surface
local squadPath, squadDirection = scoreNeighborsForFormation(getNeighborChunks(map, chunk.x, chunk.y),
validUnitGroupLocation,
scoreUnitGroupLocation,
map)
if (squadPath ~= -1) then
local squadPosition = surface.find_non_colliding_position("chunk-scanner-squad-rampant",
positionFromDirectionAndChunk(squadDirection,
chunk,
universe.position,
0.98),
CHUNK_SIZE,
4,
true)
if squadPosition then
local squad = createSquad(squadPosition, map)
squad.vengence = true
squad.rabid = mRandom() < 0.03
local scaledWaveSize = mMin(attackWaveScaling(universe), getAttackWaveMaxSize(universe)*0.8)
universe.formGroupCommand.group = squad.group
universe.formCommand.unit_count = scaledWaveSize
local foundUnits = surface.set_multi_command(universe.formCommand)
if (foundUnits > 0) then
squad.kamikaze = mRandom() < calculateKamikazeThreshold(foundUnits, universe)
universe.groupNumberToSquad[squad.groupNumber] = squad
universe.squadCount = universe.squadCount + 1
if (map.points < finalSquadCost*4) or (universe.aiDifficulty~="Hard") then
map.vengenceLimiter = map.vengenceLimiter + 1
else
map.vengenceLimiter = map.vengenceLimiter + 0.5
end
map.points = map.points - finalVengenceSquadCost
if universe.aiPointsPrintSpendingToChat then
game.print(map.surface.name .. ": Points: -" .. finalVengenceSquadCost .. ". [Vengence] Total: " .. string.format("%.2f", map.points) .. " [gps=" .. squadPosition.x .. "," .. squadPosition.y .. "]")
end
else
if (squad.group.valid) then
squad.group.destroy()
end
end
end
end
end
end
function aiAttackWave.formSquads(map, chunk, tick, remoteInterfaceParameters)
local universe = map.universe
local squadCreated = false
local squad
local finalSquadCost = universe.finalSquadCost
local canCreateSquad = (remoteInterfaceParameters and remoteInterfaceParameters.ignoreSquadLimit) or (universe.squadCount < (universe.AI_MAX_SQUAD_COUNT*0.7))
if remoteInterfaceParameters then
finalSquadCost = mMin(finalSquadCost, map.points)
end
if canCreateSquad and
attackWaveValidCandidate(chunk, map) and
((map.points - finalSquadCost) >= 0) and
(remoteInterfaceParameters or (mRandom() < universe.formSquadThreshold))
then
local surface = map.surface
local squadPath, squadDirection = scoreNeighborsForFormation(getNeighborChunks(map, chunk.x, chunk.y),
validUnitGroupLocation,
scoreUnitGroupLocation,
map)
if (squadPath ~= -1) then
local squadPosition = surface.find_non_colliding_position("chunk-scanner-squad-rampant",
positionFromDirectionAndChunk(squadDirection,
chunk,
universe.position,
0.98),
CHUNK_SIZE,
4,
true)
if squadPosition then
squad = createSquad(squadPosition, map)
if universe.undergroundAttack and (not remoteInterfaceParameters) and (universe.undergroundAttackProbability > 0) and (mRandom() < universe.undergroundAttackProbability) then
-- if mRandom() < 0.2 then
-- squad.undergoundAttack = "randomTarget" -- look at undergroundAttack.createUndergroudAttack
-- else
squad.undergoundAttack = "common"
-- end
end
squad.rabid = mRandom() < 0.03
local scaledWaveSize = attackWaveScaling(universe)
universe.formGroupCommand.group = squad.group
if remoteInterfaceParameters and remoteInterfaceParameters.size and remoteInterfaceParameters.size > 0 then
universe.formCommand.unit_count = remoteInterfaceParameters.size
else
universe.formCommand.unit_count = scaledWaveSize
end
local foundUnits = surface.set_multi_command(universe.formCommand)
if (foundUnits > 0) then
squad.kamikaze = mRandom() < calculateKamikazeThreshold(foundUnits, universe)
map.points = map.points - finalSquadCost
map.vengenceLimiter = mMax(0, map.vengenceLimiter - 2)
squadCreated = true
universe.squadCount = universe.squadCount + 1
universe.groupNumberToSquad[squad.groupNumber] = squad
if tick and ((map.state == AI_STATE_AGGRESSIVE) or (map.state ==AI_STATE_GROWING)) then
map.squadsGenerated = (map.squadsGenerated or 0) + 1 -- this "attack-wave" only
chanceForAdditionalSquad = map.activeNests/15-map.squadsGenerated
if universe.aiPointsScaler > 1 then
chanceForAdditionalSquad = chanceForAdditionalSquad + universe.aiPointsScaler - 1
end
chanceForAdditionalSquad = chanceForAdditionalSquad + map.points / 5000
local needMoreSuads
if chanceForAdditionalSquad<=0 then
needMoreSuads = false
elseif chanceForAdditionalSquad>=1 then
needMoreSuads = true
else
needMoreSuads = (mRandom()<=chanceForAdditionalSquad)
end
if not needMoreSuads then
map.squadsGenerated = 0
map.canAttackTick = randomTickEvent(tick,
AGGRESSIVE_CAN_ATTACK_WAIT_MIN_DURATION,
AGGRESSIVE_CAN_ATTACK_WAIT_MAX_DURATION)
end
end
if universe.aiPointsPrintSpendingToChat then
game.print(map.surface.name .. ": Points: -" .. finalSquadCost .. ". [Squad] Total: " .. string.format("%.2f", map.points) .. " [gps=" .. squadPosition.x .. "," .. squadPosition.y .. "]")
end
else
if (squad.group.valid) then
squad.group.destroy()
end
end
end
end
end
if remoteInterfaceParameters then
if squadCreated then
remoteInterfaceParameters.squad = squad
end
end
return squadCreated
end
aiAttackWaveG = aiAttackWave
return aiAttackWave