2023-03-12 06:51:13 +02:00
|
|
|
-- Copyright (C) 2022 veden
|
|
|
|
|
|
|
|
-- This program is free software: you can redistribute it and/or modify
|
|
|
|
-- it under the terms of the GNU General Public License as published by
|
|
|
|
-- the Free Software Foundation, either version 3 of the License, or
|
|
|
|
-- (at your option) any later version.
|
|
|
|
|
|
|
|
-- This program is distributed in the hope that it will be useful,
|
|
|
|
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
-- GNU General Public License for more details.
|
|
|
|
|
|
|
|
-- You should have received a copy of the GNU General Public License
|
|
|
|
-- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
|
|
|
|
if SquadG then
|
|
|
|
return SquadG
|
|
|
|
end
|
|
|
|
local Squad = {}
|
|
|
|
|
|
|
|
--
|
|
|
|
|
|
|
|
local Universe
|
2023-03-13 03:31:46 +02:00
|
|
|
local TargetPosition
|
2023-03-26 23:04:24 +02:00
|
|
|
local SearchPath
|
2023-03-13 03:31:46 +02:00
|
|
|
local Queries
|
2023-03-12 06:51:13 +02:00
|
|
|
|
|
|
|
-- imports
|
|
|
|
|
|
|
|
local Constants = require("Constants")
|
|
|
|
local MapUtils = require("MapUtils")
|
|
|
|
local BaseUtils = require("BaseUtils")
|
|
|
|
local MathUtils = require("MathUtils")
|
|
|
|
local ChunkPropertyUtils = require("ChunkPropertyUtils")
|
|
|
|
local Utils = require("Utils")
|
|
|
|
|
|
|
|
-- Constants
|
|
|
|
|
|
|
|
local BASE_AI_STATE_ONSLAUGHT = Constants.BASE_AI_STATE_ONSLAUGHT
|
|
|
|
local BASE_AI_STATE_RAIDING = Constants.BASE_AI_STATE_RAIDING
|
|
|
|
local MAGIC_MAXIMUM_NUMBER = Constants.MAGIC_MAXIMUM_NUMBER
|
|
|
|
local MINIMUM_EXPANSION_DISTANCE = Constants.MINIMUM_EXPANSION_DISTANCE
|
|
|
|
local COMMAND_TIMEOUT = Constants.COMMAND_TIMEOUT
|
|
|
|
local PLAYER_PHEROMONE = Constants.PLAYER_PHEROMONE
|
|
|
|
local BASE_PHEROMONE = Constants.BASE_PHEROMONE
|
|
|
|
local ENEMY_PHEROMONE = Constants.ENEMY_PHEROMONE
|
|
|
|
local RESOURCE_PHEROMONE = Constants.RESOURCE_PHEROMONE
|
|
|
|
|
2023-03-13 03:31:46 +02:00
|
|
|
local HALF_CHUNK_SIZE = Constants.HALF_CHUNK_SIZE
|
|
|
|
|
2023-03-12 06:51:13 +02:00
|
|
|
local FIVE_DEATH_PHEROMONE_GENERATOR_AMOUNT = Constants.FIVE_DEATH_PHEROMONE_GENERATOR_AMOUNT
|
|
|
|
|
|
|
|
local SQUAD_BUILDING = Constants.SQUAD_BUILDING
|
|
|
|
|
|
|
|
local SQUAD_RAIDING = Constants.SQUAD_RAIDING
|
|
|
|
local SQUAD_SETTLING = Constants.SQUAD_SETTLING
|
|
|
|
local SQUAD_GUARDING = Constants.SQUAD_GUARDING
|
|
|
|
local SQUAD_RETREATING = Constants.SQUAD_RETREATING
|
|
|
|
|
|
|
|
local BASE_AI_STATE_SIEGE = Constants.BASE_AI_STATE_SIEGE
|
|
|
|
local BASE_AI_STATE_AGGRESSIVE = Constants.BASE_AI_STATE_AGGRESSIVE
|
|
|
|
|
|
|
|
local PLAYER_PHEROMONE_MULTIPLER = Constants.PLAYER_PHEROMONE_MULTIPLER
|
|
|
|
|
|
|
|
local DEFINES_DISTRACTION_NONE = defines.distraction.none
|
|
|
|
local DEFINES_DISTRACTION_BY_ENEMY = defines.distraction.by_enemy
|
|
|
|
local DEFINES_DISTRACTION_BY_ANYTHING = defines.distraction.by_anything
|
|
|
|
local COOLDOWN_RETREAT = Constants.COOLDOWN_RETREAT
|
|
|
|
|
|
|
|
local CHUNK_SIZE = Constants.CHUNK_SIZE
|
|
|
|
local COOLDOWN_RALLY = Constants.COOLDOWN_RALLY
|
|
|
|
local RALLY_CRY_DISTANCE = Constants.RALLY_CRY_DISTANCE
|
|
|
|
|
|
|
|
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
|
2023-03-13 01:13:32 +02:00
|
|
|
local AI_VENGENCE_SETTLER_COST = Constants.AI_VENGENCE_SETTLER_COST
|
2023-03-12 06:51:13 +02:00
|
|
|
local CHUNK_ALL_DIRECTIONS = Constants.CHUNK_ALL_DIRECTIONS
|
|
|
|
|
|
|
|
-- imported functions
|
|
|
|
|
2023-03-26 23:26:15 +02:00
|
|
|
local setPositionInQuery = Utils.setPositionInQuery
|
|
|
|
|
2023-03-12 06:51:13 +02:00
|
|
|
local getPassable = ChunkPropertyUtils.getPassable
|
|
|
|
local getRallyTick = ChunkPropertyUtils.getRallyTick
|
|
|
|
local setRallyTick = ChunkPropertyUtils.setRallyTick
|
|
|
|
local modifyBaseUnitPoints = BaseUtils.modifyBaseUnitPoints
|
|
|
|
|
|
|
|
local tableRemove = table.remove
|
|
|
|
local tableInsert = table.insert
|
|
|
|
local mCeil = math.ceil
|
|
|
|
|
|
|
|
local isActiveNest = ChunkPropertyUtils.isActiveNest
|
|
|
|
local isActiveRaidNest = ChunkPropertyUtils.isActiveRaidNest
|
|
|
|
|
|
|
|
local gaussianRandomRangeRG = MathUtils.gaussianRandomRangeRG
|
|
|
|
local getEnemyStructureCount = ChunkPropertyUtils.getEnemyStructureCount
|
|
|
|
local getRetreatTick = ChunkPropertyUtils.getRetreatTick
|
|
|
|
|
|
|
|
local findNearbyBase = ChunkPropertyUtils.findNearbyBase
|
|
|
|
local tableSize = table_size
|
|
|
|
local setPositionInCommand = Utils.setPositionInCommand
|
|
|
|
|
|
|
|
local euclideanDistancePoints = MathUtils.euclideanDistancePoints
|
|
|
|
local canMoveChunkDirection = MapUtils.canMoveChunkDirection
|
|
|
|
|
|
|
|
local setRetreatTick = ChunkPropertyUtils.setRetreatTick
|
|
|
|
|
|
|
|
local removeSquadFromChunk = ChunkPropertyUtils.removeSquadFromChunk
|
|
|
|
local addDeathGenerator = ChunkPropertyUtils.addDeathGenerator
|
|
|
|
|
|
|
|
local getNeighborChunks = MapUtils.getNeighborChunks
|
|
|
|
local addSquadToChunk = ChunkPropertyUtils.addSquadToChunk
|
|
|
|
local getChunkByXY = MapUtils.getChunkByXY
|
|
|
|
local positionToChunkXY = MapUtils.positionToChunkXY
|
2023-03-13 03:31:46 +02:00
|
|
|
local positionFromScaledDirections = MapUtils.positionFromScaledDirections
|
2023-03-26 23:04:24 +02:00
|
|
|
local positionFromScaledSearchPath = MapUtils.positionFromScaledSearchPath
|
2023-03-12 06:51:13 +02:00
|
|
|
|
|
|
|
local euclideanDistanceNamed = MathUtils.euclideanDistanceNamed
|
|
|
|
|
|
|
|
-- module code
|
2023-03-13 01:13:32 +02:00
|
|
|
local function scoreRetreatLocation(neighborChunk)
|
2023-03-12 06:51:13 +02:00
|
|
|
return (-neighborChunk[BASE_PHEROMONE] +
|
|
|
|
-(neighborChunk[PLAYER_PHEROMONE] * PLAYER_PHEROMONE_MULTIPLER) +
|
|
|
|
-((neighborChunk.playerBaseGenerator or 0) * 1000))
|
|
|
|
end
|
|
|
|
|
2023-03-13 01:13:32 +02:00
|
|
|
local function scoreResourceLocation(neighborChunk)
|
2023-03-26 23:04:24 +02:00
|
|
|
local preferred = false
|
|
|
|
if (
|
|
|
|
not neighborChunk.playerBaseGenerator
|
|
|
|
and not neighborChunk.playerGenerator
|
|
|
|
and neighborChunk.resourceGenerator
|
|
|
|
and getEnemyStructureCount(neighborChunk) == 0
|
|
|
|
)
|
|
|
|
then
|
|
|
|
preferred = true
|
|
|
|
end
|
|
|
|
local score = neighborChunk[RESOURCE_PHEROMONE]
|
2023-03-12 06:51:13 +02:00
|
|
|
- (neighborChunk[PLAYER_PHEROMONE] * PLAYER_PHEROMONE_MULTIPLER)
|
|
|
|
- neighborChunk[ENEMY_PHEROMONE]
|
2023-03-26 23:04:24 +02:00
|
|
|
return score, preferred
|
2023-03-12 06:51:13 +02:00
|
|
|
end
|
|
|
|
|
2023-03-13 01:13:32 +02:00
|
|
|
local function scoreSiegeLocation(neighborChunk)
|
2023-03-26 23:04:24 +02:00
|
|
|
local preferred = false
|
|
|
|
if (
|
|
|
|
not neighborChunk.playerBaseGenerator
|
|
|
|
and not neighborChunk.playerGenerator
|
|
|
|
)
|
|
|
|
then
|
|
|
|
preferred = true
|
|
|
|
end
|
2023-03-12 06:51:13 +02:00
|
|
|
local settle = neighborChunk[BASE_PHEROMONE]
|
|
|
|
+ neighborChunk[RESOURCE_PHEROMONE] * 0.5
|
|
|
|
+ (neighborChunk[PLAYER_PHEROMONE] * PLAYER_PHEROMONE_MULTIPLER)
|
|
|
|
|
2023-03-26 23:04:24 +02:00
|
|
|
local score = settle - neighborChunk[ENEMY_PHEROMONE]
|
|
|
|
|
|
|
|
return score, preferred
|
2023-03-12 06:51:13 +02:00
|
|
|
end
|
|
|
|
|
2023-03-13 01:13:32 +02:00
|
|
|
local function scoreAttackLocation(neighborChunk)
|
2023-03-26 23:04:24 +02:00
|
|
|
local preferred = false
|
|
|
|
if (
|
|
|
|
neighborChunk.playerBaseGenerator
|
|
|
|
or neighborChunk.playerGenerator
|
|
|
|
)
|
|
|
|
then
|
|
|
|
preferred = true
|
|
|
|
end
|
2023-03-12 06:51:13 +02:00
|
|
|
local damage = neighborChunk[BASE_PHEROMONE] +
|
|
|
|
(neighborChunk[PLAYER_PHEROMONE] * PLAYER_PHEROMONE_MULTIPLER)
|
2023-03-26 23:04:24 +02:00
|
|
|
if preferred then
|
|
|
|
damage = damage * 2
|
|
|
|
end
|
|
|
|
return damage, preferred
|
2023-03-12 06:51:13 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
local function findMovementPosition(surface, position)
|
2023-03-13 01:13:32 +02:00
|
|
|
return surface.find_non_colliding_position(
|
|
|
|
"behemoth-biter",
|
|
|
|
position,
|
|
|
|
10,
|
|
|
|
2,
|
|
|
|
false
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
local function findDeploymentPosition(surface, position)
|
|
|
|
return surface.find_non_colliding_position(
|
|
|
|
"biter-spawner",
|
|
|
|
position,
|
|
|
|
CHUNK_SIZE,
|
|
|
|
4,
|
|
|
|
true
|
|
|
|
)
|
2023-03-12 06:51:13 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
local function calculateSettlerMaxDistance()
|
|
|
|
local targetDistance
|
|
|
|
local distanceRoll = Universe.random()
|
|
|
|
if distanceRoll < 0.05 then
|
|
|
|
return 0
|
|
|
|
elseif distanceRoll < 0.30 then
|
|
|
|
targetDistance = Universe.expansionLowTargetDistance
|
|
|
|
elseif distanceRoll < 0.70 then
|
|
|
|
targetDistance = Universe.expansionMediumTargetDistance
|
|
|
|
elseif distanceRoll < 0.95 then
|
|
|
|
targetDistance = Universe.expansionHighTargetDistance
|
|
|
|
else
|
|
|
|
return Universe.expansionMaxDistance
|
|
|
|
end
|
|
|
|
return gaussianRandomRangeRG(targetDistance,
|
|
|
|
Universe.expansionDistanceDeviation,
|
|
|
|
MINIMUM_EXPANSION_DISTANCE,
|
|
|
|
Universe.expansionMaxDistance,
|
|
|
|
Universe.random)
|
|
|
|
end
|
|
|
|
|
|
|
|
local function addMovementPenalty(squad, chunk)
|
|
|
|
if (chunk == -1) then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
local penalties = squad.penalties
|
|
|
|
local penaltyCount = #penalties
|
|
|
|
for i=1,penaltyCount do
|
|
|
|
local penalty = penalties[i]
|
|
|
|
if (penalty.c.id == chunk.id) then
|
|
|
|
penalty.v = penalty.v + 1
|
|
|
|
if penalty.v >= 15 then
|
|
|
|
if Universe.enabledMigration and
|
|
|
|
(Universe.builderCount < Universe.AI_MAX_BUILDER_COUNT) then
|
|
|
|
squad.settler = true
|
|
|
|
squad.originPosition.x = squad.group.position.x
|
|
|
|
squad.originPosition.y = squad.group.position.y
|
|
|
|
squad.maxDistance = calculateSettlerMaxDistance()
|
|
|
|
|
|
|
|
squad.status = SQUAD_SETTLING
|
|
|
|
else
|
|
|
|
squad.group.destroy()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return
|
|
|
|
end
|
|
|
|
end
|
|
|
|
if (penaltyCount == 10) then
|
|
|
|
tableRemove(penalties, 10)
|
|
|
|
end
|
|
|
|
tableInsert(penalties,
|
|
|
|
1,
|
|
|
|
{ v = 1,
|
|
|
|
c = chunk })
|
|
|
|
end
|
|
|
|
|
2023-03-26 23:26:15 +02:00
|
|
|
function Squad.compressSquad(squad)
|
|
|
|
if not squad.canBeCompressed then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
local compressionSet = {}
|
|
|
|
local group = squad.group
|
|
|
|
local members = group.members
|
2023-04-01 18:48:28 +02:00
|
|
|
if (Universe.squadCompressionThreshold == -1)
|
|
|
|
or (#members <= Universe.squadCompressionThreshold)
|
|
|
|
then
|
2023-03-26 23:26:15 +02:00
|
|
|
squad.canBeCompressed = false
|
|
|
|
return
|
|
|
|
end
|
|
|
|
local compressedTotal = 0
|
|
|
|
local totalTypes = 0
|
|
|
|
for _, entity in pairs(members) do
|
|
|
|
local entityName = entity.name
|
|
|
|
local count = compressionSet[entityName]
|
|
|
|
if not count then
|
|
|
|
totalTypes = totalTypes + 1
|
|
|
|
if totalTypes > 6 then
|
|
|
|
entity.destroy()
|
|
|
|
compressionSet[entityName] = 1
|
|
|
|
else
|
|
|
|
compressionSet[entityName] = 0
|
|
|
|
end
|
|
|
|
else
|
|
|
|
compressionSet[entityName] = count + 1
|
|
|
|
compressedTotal = compressedTotal + 1
|
|
|
|
entity.destroy()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
local query = Queries.renderText
|
|
|
|
query.surface = group.surface
|
|
|
|
query.text = compressedTotal
|
|
|
|
query.target = members[1]
|
|
|
|
squad.compressionText = rendering.draw_text(query)
|
|
|
|
squad.compressionSet = compressionSet
|
|
|
|
squad.canBeCompressed = false
|
|
|
|
end
|
|
|
|
|
|
|
|
function Squad.decompressSquad(squad)
|
|
|
|
if not squad.compressionSet then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
local group = squad.group
|
|
|
|
local query = Queries.createEntityQuery
|
|
|
|
local add_member = group.add_member
|
|
|
|
local create_entity = squad.map.surface.create_entity
|
|
|
|
setPositionInQuery(
|
|
|
|
query,
|
|
|
|
group.position
|
|
|
|
)
|
|
|
|
for name,count in pairs(squad.compressionSet) do
|
|
|
|
query.name = name
|
|
|
|
for _ = 1, count do
|
|
|
|
add_member(create_entity(query))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
rendering.destroy(squad.compressionText)
|
|
|
|
squad.compressionSet = nil
|
|
|
|
end
|
|
|
|
|
2023-03-12 06:51:13 +02:00
|
|
|
--[[
|
|
|
|
Expects all neighbors adjacent to a chunk
|
|
|
|
--]]
|
2023-03-26 23:04:24 +02:00
|
|
|
local function scoreNeighbors(map, chunk, neighborDirectionChunks, scoreFunction)
|
|
|
|
local highest = SearchPath[1]
|
2023-03-12 06:51:13 +02:00
|
|
|
local highestScore = -MAGIC_MAXIMUM_NUMBER
|
2023-03-26 23:04:24 +02:00
|
|
|
local highestPreferred = false
|
|
|
|
|
|
|
|
highest.chunk = -1
|
2023-03-12 06:51:13 +02:00
|
|
|
|
|
|
|
for x=1,8 do
|
|
|
|
local neighborChunk = neighborDirectionChunks[x]
|
|
|
|
if (neighborChunk ~= -1) then
|
2023-03-13 01:13:32 +02:00
|
|
|
if (chunk == -1) or canMoveChunkDirection(x, chunk, neighborChunk) then
|
2023-03-26 23:04:24 +02:00
|
|
|
local score, preferred = scoreFunction(neighborChunk)
|
|
|
|
if (highestPreferred and preferred and (score > highestScore))
|
|
|
|
or (not highestPreferred and (score > highestScore))
|
|
|
|
or (not highestPreferred and preferred)
|
|
|
|
then
|
|
|
|
highest.chunk = neighborChunk
|
|
|
|
highest.direction = x
|
2023-03-12 06:51:13 +02:00
|
|
|
highestScore = score
|
2023-03-26 23:04:24 +02:00
|
|
|
highestPreferred = preferred
|
2023-03-12 06:51:13 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-03-26 23:04:24 +02:00
|
|
|
if (highest.chunk == -1) then
|
|
|
|
return SearchPath
|
|
|
|
end
|
|
|
|
|
|
|
|
if highestPreferred then
|
|
|
|
SearchPath[2].chunk = -1
|
|
|
|
return SearchPath
|
2023-03-12 06:51:13 +02:00
|
|
|
end
|
|
|
|
|
2023-03-26 23:04:24 +02:00
|
|
|
for i = 2, 4 do
|
|
|
|
local lastChunk = SearchPath[i-1].chunk
|
|
|
|
highest = SearchPath[i]
|
|
|
|
highest.chunk = -1
|
|
|
|
highestPreferred = false
|
2023-03-12 06:51:13 +02:00
|
|
|
|
2023-03-26 23:04:24 +02:00
|
|
|
neighborDirectionChunks = getNeighborChunks(map, lastChunk.x, lastChunk.y)
|
2023-03-12 06:51:13 +02:00
|
|
|
for x=1,8 do
|
|
|
|
local neighborChunk = neighborDirectionChunks[x]
|
2023-03-26 23:04:24 +02:00
|
|
|
if ((neighborChunk ~= -1) and (neighborChunk.id ~= lastChunk.id) and
|
|
|
|
canMoveChunkDirection(x, lastChunk, neighborChunk))
|
|
|
|
then
|
|
|
|
local score, preferred = scoreFunction(neighborChunk)
|
|
|
|
if (highestPreferred and preferred and (score > highestScore))
|
|
|
|
or (not highestPreferred and (score > highestScore))
|
|
|
|
or (not highestPreferred and preferred)
|
|
|
|
then
|
|
|
|
highestScore = score
|
|
|
|
highest.chunk = neighborChunk
|
|
|
|
highest.direction = x
|
|
|
|
highestPreferred = preferred
|
2023-03-12 06:51:13 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2023-03-26 23:04:24 +02:00
|
|
|
if highest.chunk == -1 then
|
|
|
|
return SearchPath
|
|
|
|
end
|
|
|
|
if highestPreferred and (i ~= 4) then
|
|
|
|
SearchPath[i+1].chunk = -1
|
|
|
|
return SearchPath
|
|
|
|
end
|
2023-03-12 06:51:13 +02:00
|
|
|
end
|
|
|
|
|
2023-03-26 23:04:24 +02:00
|
|
|
return SearchPath
|
2023-03-12 06:51:13 +02:00
|
|
|
end
|
|
|
|
|
2023-03-13 03:31:46 +02:00
|
|
|
local function settleMove(squad)
|
2023-03-12 06:51:13 +02:00
|
|
|
local group = squad.group
|
2023-03-13 03:31:46 +02:00
|
|
|
local map = squad.map
|
2023-03-12 06:51:13 +02:00
|
|
|
|
|
|
|
local groupPosition = group.position
|
|
|
|
local x, y = positionToChunkXY(groupPosition)
|
|
|
|
local chunk = getChunkByXY(map, x, y)
|
|
|
|
local scoreFunction = scoreResourceLocation
|
|
|
|
if (squad.type == BASE_AI_STATE_SIEGE) then
|
|
|
|
scoreFunction = scoreSiegeLocation
|
|
|
|
end
|
|
|
|
local squadChunk = squad.chunk
|
|
|
|
if squadChunk ~= -1 then
|
|
|
|
addDeathGenerator(squadChunk, -FIVE_DEATH_PHEROMONE_GENERATOR_AMOUNT)
|
|
|
|
end
|
|
|
|
if chunk ~= -1 then
|
|
|
|
addSquadToChunk(chunk, squad)
|
|
|
|
addMovementPenalty(squad, chunk)
|
|
|
|
if not squad.group.valid then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
end
|
2023-03-26 23:04:24 +02:00
|
|
|
local originPosition = squad.originPosition
|
2023-03-12 06:51:13 +02:00
|
|
|
local distance = euclideanDistancePoints(groupPosition.x,
|
|
|
|
groupPosition.y,
|
2023-03-26 23:04:24 +02:00
|
|
|
originPosition.x,
|
|
|
|
originPosition.y)
|
2023-03-12 06:51:13 +02:00
|
|
|
local cmd
|
|
|
|
local position
|
|
|
|
local surface = map.surface
|
|
|
|
|
|
|
|
if (chunk ~= -1) and
|
|
|
|
(
|
|
|
|
(distance >= squad.maxDistance) or
|
|
|
|
(
|
|
|
|
chunk.resourceGenerator and (not chunk.nestCount) and (not chunk.hiveCount)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
then
|
2023-03-26 23:26:15 +02:00
|
|
|
Squad.decompressSquad(squad)
|
|
|
|
|
2023-03-26 23:04:24 +02:00
|
|
|
position = findMovementPosition(surface, groupPosition) or groupPosition
|
2023-03-12 06:51:13 +02:00
|
|
|
|
2023-03-13 03:31:46 +02:00
|
|
|
cmd = Queries.settleCommand
|
2023-03-12 06:51:13 +02:00
|
|
|
if squad.kamikaze then
|
|
|
|
cmd.distraction = DEFINES_DISTRACTION_NONE
|
|
|
|
else
|
|
|
|
cmd.distraction = DEFINES_DISTRACTION_BY_ENEMY
|
|
|
|
end
|
|
|
|
|
|
|
|
setPositionInCommand(cmd, position)
|
|
|
|
|
|
|
|
squad.status = SQUAD_BUILDING
|
|
|
|
|
|
|
|
group.set_command(cmd)
|
2023-03-26 23:04:24 +02:00
|
|
|
return
|
|
|
|
end
|
2023-03-12 06:51:13 +02:00
|
|
|
|
2023-03-26 23:04:24 +02:00
|
|
|
local searchPath = scoreNeighbors(
|
|
|
|
map,
|
|
|
|
chunk,
|
|
|
|
getNeighborChunks(map, x, y),
|
|
|
|
scoreFunction
|
|
|
|
)
|
2023-03-12 06:51:13 +02:00
|
|
|
|
2023-03-26 23:04:24 +02:00
|
|
|
if (searchPath[1].chunk == -1) then
|
|
|
|
cmd = Queries.wanderCommand
|
|
|
|
group.set_command(cmd)
|
|
|
|
return
|
|
|
|
end
|
2023-03-12 06:51:13 +02:00
|
|
|
|
2023-03-26 23:04:24 +02:00
|
|
|
local lastChunk = 0
|
|
|
|
for i = 1, 4 do
|
|
|
|
local moveChunk = searchPath[i].chunk
|
|
|
|
if (moveChunk ~= -1) then
|
|
|
|
distance = euclideanDistancePoints(
|
|
|
|
moveChunk.x + HALF_CHUNK_SIZE,
|
|
|
|
moveChunk.y + HALF_CHUNK_SIZE,
|
|
|
|
originPosition.x,
|
|
|
|
originPosition.y
|
|
|
|
)
|
|
|
|
if distance >= squad.maxDistance then
|
|
|
|
searchPath[i].chunk = -1
|
|
|
|
if i < 4 then
|
|
|
|
searchPath[i+1].chunk = -1
|
2023-03-12 06:51:13 +02:00
|
|
|
end
|
|
|
|
else
|
2023-03-26 23:04:24 +02:00
|
|
|
lastChunk = i
|
|
|
|
addDeathGenerator(moveChunk, -FIVE_DEATH_PHEROMONE_GENERATOR_AMOUNT)
|
2023-03-12 06:51:13 +02:00
|
|
|
end
|
|
|
|
end
|
2023-03-26 23:04:24 +02:00
|
|
|
end
|
2023-03-12 06:51:13 +02:00
|
|
|
|
2023-03-26 23:04:24 +02:00
|
|
|
position = findMovementPosition(
|
|
|
|
surface,
|
|
|
|
positionFromScaledSearchPath(
|
|
|
|
groupPosition,
|
|
|
|
1,
|
|
|
|
searchPath
|
|
|
|
)
|
|
|
|
)
|
2023-03-12 06:51:13 +02:00
|
|
|
|
2023-03-26 23:04:24 +02:00
|
|
|
if not position then
|
|
|
|
cmd = Queries.wanderCommand
|
2023-03-12 06:51:13 +02:00
|
|
|
group.set_command(cmd)
|
2023-03-26 23:04:24 +02:00
|
|
|
return
|
2023-03-12 06:51:13 +02:00
|
|
|
end
|
|
|
|
|
2023-03-26 23:04:24 +02:00
|
|
|
TargetPosition.x = position.x
|
|
|
|
TargetPosition.y = position.y
|
2023-03-12 06:51:13 +02:00
|
|
|
|
2023-03-26 23:04:24 +02:00
|
|
|
if lastChunk ~= 4 then
|
|
|
|
cmd = Queries.settleCommand
|
|
|
|
squad.status = SQUAD_BUILDING
|
2023-03-26 23:26:15 +02:00
|
|
|
Squad.decompressSquad(squad)
|
2023-03-26 23:04:24 +02:00
|
|
|
if squad.kamikaze then
|
|
|
|
cmd.distraction = DEFINES_DISTRACTION_NONE
|
|
|
|
else
|
|
|
|
cmd.distraction = DEFINES_DISTRACTION_BY_ENEMY
|
2023-03-12 06:51:13 +02:00
|
|
|
end
|
2023-03-26 23:04:24 +02:00
|
|
|
else
|
|
|
|
cmd = Queries.moveCommand
|
|
|
|
if squad.rabid or squad.kamikaze then
|
|
|
|
cmd.distraction = DEFINES_DISTRACTION_NONE
|
|
|
|
else
|
|
|
|
cmd.distraction = DEFINES_DISTRACTION_BY_ENEMY
|
2023-03-12 06:51:13 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-03-26 23:04:24 +02:00
|
|
|
setPositionInCommand(cmd, TargetPosition)
|
|
|
|
|
|
|
|
group.set_command(cmd)
|
2023-03-12 06:51:13 +02:00
|
|
|
end
|
|
|
|
|
2023-03-13 03:31:46 +02:00
|
|
|
local function attackMove(squad)
|
2023-03-12 06:51:13 +02:00
|
|
|
local group = squad.group
|
|
|
|
|
|
|
|
local groupPosition = group.position
|
|
|
|
local x, y = positionToChunkXY(groupPosition)
|
2023-03-13 03:31:46 +02:00
|
|
|
local map = squad.map
|
2023-03-12 06:51:13 +02:00
|
|
|
local chunk = getChunkByXY(map, x, y)
|
|
|
|
local squadChunk = squad.chunk
|
|
|
|
if squadChunk ~= -1 then
|
|
|
|
addDeathGenerator(squadChunk, -FIVE_DEATH_PHEROMONE_GENERATOR_AMOUNT)
|
|
|
|
end
|
|
|
|
if chunk ~= -1 then
|
|
|
|
addSquadToChunk(chunk, squad)
|
|
|
|
addMovementPenalty(squad, chunk)
|
|
|
|
if not squad.group.valid then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
end
|
2023-03-13 03:31:46 +02:00
|
|
|
|
|
|
|
local attackScorer = scoreAttackLocation
|
|
|
|
|
2023-03-12 06:51:13 +02:00
|
|
|
squad.frenzy = (squad.frenzy and (euclideanDistanceNamed(groupPosition, squad.frenzyPosition) < 100))
|
2023-03-26 23:04:24 +02:00
|
|
|
local searchPath = scoreNeighbors(
|
|
|
|
map,
|
|
|
|
chunk,
|
|
|
|
getNeighborChunks(map, x, y),
|
|
|
|
attackScorer
|
|
|
|
)
|
2023-03-12 06:51:13 +02:00
|
|
|
local cmd
|
2023-03-26 23:04:24 +02:00
|
|
|
if (searchPath[1].chunk == -1) then
|
2023-03-13 03:31:46 +02:00
|
|
|
cmd = Queries.wanderCommand
|
2023-03-12 06:51:13 +02:00
|
|
|
group.set_command(cmd)
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
2023-03-26 23:04:24 +02:00
|
|
|
local position = findMovementPosition(
|
|
|
|
map.surface,
|
|
|
|
positionFromScaledSearchPath(
|
|
|
|
groupPosition,
|
|
|
|
1,
|
|
|
|
searchPath
|
2023-03-12 06:51:13 +02:00
|
|
|
)
|
2023-03-26 23:04:24 +02:00
|
|
|
)
|
2023-03-12 06:51:13 +02:00
|
|
|
|
|
|
|
if not position then
|
2023-03-13 03:31:46 +02:00
|
|
|
cmd = Queries.wanderCommand
|
2023-03-12 06:51:13 +02:00
|
|
|
group.set_command(cmd)
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
2023-03-26 23:04:24 +02:00
|
|
|
local attack = false
|
|
|
|
for i = 1, 4 do
|
|
|
|
local attackChunk = searchPath[i].chunk
|
|
|
|
if (attackChunk ~= -1) then
|
|
|
|
addDeathGenerator(attackChunk, -FIVE_DEATH_PHEROMONE_GENERATOR_AMOUNT)
|
|
|
|
if attackChunk.playerBaseGenerator or
|
|
|
|
(attackChunk[PLAYER_PHEROMONE] >= Universe.attackPlayerThreshold)
|
|
|
|
then
|
|
|
|
attack = true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if attack then
|
2023-03-26 23:26:15 +02:00
|
|
|
Squad.decompressSquad(squad)
|
2023-03-13 03:31:46 +02:00
|
|
|
cmd = Queries.attackCommand
|
2023-03-12 06:51:13 +02:00
|
|
|
|
|
|
|
if not squad.rabid then
|
|
|
|
squad.frenzy = true
|
|
|
|
squad.frenzyPosition.x = groupPosition.x
|
|
|
|
squad.frenzyPosition.y = groupPosition.y
|
|
|
|
end
|
|
|
|
else
|
2023-03-13 03:31:46 +02:00
|
|
|
cmd = Queries.moveCommand
|
2023-03-12 06:51:13 +02:00
|
|
|
if squad.rabid or squad.frenzy then
|
|
|
|
cmd.distraction = DEFINES_DISTRACTION_BY_ANYTHING
|
|
|
|
else
|
|
|
|
cmd.distraction = DEFINES_DISTRACTION_BY_ENEMY
|
|
|
|
end
|
|
|
|
end
|
2023-03-26 23:04:24 +02:00
|
|
|
|
|
|
|
TargetPosition.x = position.x
|
|
|
|
TargetPosition.y = position.y
|
2023-03-13 03:31:46 +02:00
|
|
|
setPositionInCommand(cmd, TargetPosition)
|
2023-03-12 06:51:13 +02:00
|
|
|
|
|
|
|
group.set_command(cmd)
|
|
|
|
end
|
|
|
|
|
|
|
|
local function buildMove(map, squad)
|
|
|
|
local group = squad.group
|
|
|
|
local groupPosition = group.position
|
2023-03-26 23:04:24 +02:00
|
|
|
local position = findMovementPosition(map.surface, groupPosition) or groupPosition
|
2023-03-12 06:51:13 +02:00
|
|
|
|
2023-03-26 23:04:24 +02:00
|
|
|
setPositionInCommand(Queries.settleCommand, position)
|
2023-03-12 06:51:13 +02:00
|
|
|
|
2023-03-26 23:26:15 +02:00
|
|
|
Squad.decompressSquad(squad)
|
|
|
|
|
2023-03-13 03:31:46 +02:00
|
|
|
group.set_command(Queries.compoundSettleCommand)
|
2023-03-12 06:51:13 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
function Squad.cleanSquads(tick)
|
|
|
|
local squads = Universe.groupNumberToSquad
|
|
|
|
local groupId = Universe.squadIterator
|
|
|
|
local squad
|
|
|
|
if not groupId then
|
|
|
|
groupId, squad = next(squads, groupId)
|
|
|
|
else
|
|
|
|
squad = squads[groupId]
|
|
|
|
end
|
|
|
|
if not groupId then
|
|
|
|
Universe.squadIterator = nil
|
|
|
|
if (tableSize(squads) == 0) then
|
|
|
|
-- this is needed as the next command remembers the max length a table has been
|
|
|
|
Universe.groupNumberToSquad = {}
|
|
|
|
end
|
|
|
|
else
|
|
|
|
Universe.squadIterator = next(squads, groupId)
|
|
|
|
local group = squad.group
|
|
|
|
if not group.valid then
|
|
|
|
if squad.chunk ~= -1 then
|
|
|
|
addDeathGenerator(squad.chunk, -FIVE_DEATH_PHEROMONE_GENERATOR_AMOUNT)
|
|
|
|
end
|
|
|
|
removeSquadFromChunk(squad)
|
|
|
|
if squad.settlers then
|
|
|
|
Universe.builderCount = Universe.builderCount - 1
|
|
|
|
if Universe.builderCount < 0 then
|
|
|
|
Universe.builderCount = 0
|
|
|
|
end
|
|
|
|
else
|
|
|
|
Universe.squadCount = Universe.squadCount - 1
|
|
|
|
if Universe.squadCount < 0 then
|
|
|
|
Universe.squadCount = 0
|
|
|
|
end
|
|
|
|
if squad.type == BASE_AI_STATE_AGGRESSIVE then
|
|
|
|
local base = squad.base
|
|
|
|
base.sentAggressiveGroups = base.sentAggressiveGroups - 1
|
|
|
|
if base.sentAggressiveGroups < 0 then
|
|
|
|
base.sentAggressiveGroups = 0
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
squads[groupId] = nil
|
|
|
|
elseif (group.state == 4) then
|
|
|
|
squad.wanders = 0
|
|
|
|
Squad.squadDispatch(squad.map, squad, tick)
|
|
|
|
elseif (squad.commandTick and (squad.commandTick < tick)) then
|
|
|
|
if squad.wanders > 5 then
|
|
|
|
squad.group.destroy()
|
|
|
|
else
|
|
|
|
squad.wanders = squad.wanders + 1
|
2023-03-13 03:31:46 +02:00
|
|
|
local cmd = Queries.wander2Command
|
2023-03-12 06:51:13 +02:00
|
|
|
squad.commandTick = tick + COMMAND_TIMEOUT
|
|
|
|
group.set_command(cmd)
|
|
|
|
group.start_moving()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function Squad.squadDispatch(map, squad, tick)
|
|
|
|
local group = squad.group
|
|
|
|
if group and group.valid then
|
|
|
|
local status = squad.status
|
|
|
|
if (status == SQUAD_RAIDING) then
|
|
|
|
squad.commandTick = tick + COMMAND_TIMEOUT
|
2023-03-13 03:31:46 +02:00
|
|
|
attackMove(squad)
|
2023-03-12 06:51:13 +02:00
|
|
|
elseif (status == SQUAD_SETTLING) then
|
|
|
|
squad.commandTick = tick + COMMAND_TIMEOUT
|
2023-03-13 03:31:46 +02:00
|
|
|
settleMove(squad)
|
2023-03-12 06:51:13 +02:00
|
|
|
elseif (status == SQUAD_RETREATING) then
|
|
|
|
squad.commandTick = tick + COMMAND_TIMEOUT
|
|
|
|
if squad.settlers then
|
|
|
|
squad.status = SQUAD_SETTLING
|
2023-03-13 03:31:46 +02:00
|
|
|
settleMove(squad)
|
2023-03-12 06:51:13 +02:00
|
|
|
else
|
|
|
|
squad.status = SQUAD_RAIDING
|
2023-03-13 03:31:46 +02:00
|
|
|
attackMove(squad)
|
2023-03-12 06:51:13 +02:00
|
|
|
end
|
|
|
|
elseif (status == SQUAD_BUILDING) then
|
|
|
|
squad.commandTick = tick + COMMAND_TIMEOUT
|
|
|
|
removeSquadFromChunk(squad)
|
|
|
|
buildMove(map, squad)
|
|
|
|
elseif (status == SQUAD_GUARDING) then
|
|
|
|
squad.commandTick = tick + COMMAND_TIMEOUT
|
|
|
|
if squad.settlers then
|
|
|
|
squad.status = SQUAD_SETTLING
|
2023-03-13 03:31:46 +02:00
|
|
|
settleMove(squad)
|
2023-03-12 06:51:13 +02:00
|
|
|
else
|
|
|
|
squad.status = SQUAD_RAIDING
|
2023-03-13 03:31:46 +02:00
|
|
|
attackMove(squad)
|
2023-03-12 06:51:13 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
--[[
|
|
|
|
Expects all neighbors adjacent to a chunk
|
|
|
|
--]]
|
|
|
|
local function scoreNeighborsForRetreat(chunk, neighborDirectionChunks, scoreFunction, map)
|
|
|
|
local highestChunk = -1
|
|
|
|
local highestScore = -MAGIC_MAXIMUM_NUMBER
|
|
|
|
local highestDirection
|
|
|
|
|
|
|
|
for x=1,8 do
|
|
|
|
local neighborChunk = neighborDirectionChunks[x]
|
|
|
|
if (neighborChunk ~= -1) then
|
2023-03-13 01:13:32 +02:00
|
|
|
if (chunk == -1) or canMoveChunkDirection(x, chunk, neighborChunk) then
|
|
|
|
local score = scoreFunction(neighborChunk)
|
2023-03-12 06:51:13 +02:00
|
|
|
if (score > highestScore) then
|
|
|
|
highestScore = score
|
|
|
|
highestChunk = neighborChunk
|
|
|
|
highestDirection = x
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local nextHighestChunk = -1
|
|
|
|
local nextHighestScore = highestScore
|
|
|
|
local nextHighestDirection
|
|
|
|
|
|
|
|
if (highestChunk ~= -1) then
|
|
|
|
neighborDirectionChunks = getNeighborChunks(map, highestChunk.x, highestChunk.y)
|
|
|
|
for x=1,8 do
|
|
|
|
local neighborChunk = neighborDirectionChunks[x]
|
|
|
|
|
|
|
|
if ((neighborChunk ~= -1) and ((chunk == -1) or (neighborChunk.id ~= chunk.id)) and
|
2023-03-13 01:13:32 +02:00
|
|
|
canMoveChunkDirection(x, highestChunk, neighborChunk)) then
|
|
|
|
local score = scoreFunction(neighborChunk)
|
2023-03-12 06:51:13 +02:00
|
|
|
if (score > nextHighestScore) then
|
|
|
|
nextHighestScore = score
|
|
|
|
nextHighestChunk = neighborChunk
|
|
|
|
nextHighestDirection = x
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if (nextHighestChunk == nil) then
|
|
|
|
nextHighestChunk = -1
|
|
|
|
end
|
|
|
|
|
|
|
|
return highestChunk, highestDirection, nextHighestChunk, nextHighestDirection
|
|
|
|
end
|
|
|
|
|
|
|
|
local function findNearbyRetreatingSquad(map, chunk)
|
|
|
|
if chunk.squads then
|
|
|
|
for _,squad in pairs(chunk.squads) do
|
|
|
|
local unitGroup = squad.group
|
|
|
|
if (squad.status == SQUAD_RETREATING) and unitGroup and unitGroup.valid then
|
|
|
|
return squad
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local neighbors = getNeighborChunks(map, chunk.x, chunk.y)
|
|
|
|
|
|
|
|
for i=1,#neighbors do
|
|
|
|
local neighbor = neighbors[i]
|
|
|
|
if (neighbor ~= -1) and neighbor.squads then
|
|
|
|
for _,squad in pairs(neighbor.squads) do
|
|
|
|
local unitGroup = squad.group
|
|
|
|
if (squad.status == SQUAD_RETREATING) and unitGroup and unitGroup.valid then
|
|
|
|
return squad
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return nil
|
|
|
|
end
|
|
|
|
|
|
|
|
function Squad.retreatUnits(chunk, cause, map, tick, radius)
|
|
|
|
if (tick - getRetreatTick(chunk) > COOLDOWN_RETREAT) and (getEnemyStructureCount(chunk) == 0) then
|
|
|
|
|
|
|
|
setRetreatTick(chunk, tick)
|
|
|
|
local exitPath,exitDirection,
|
|
|
|
nextExitPath,nextExitDirection = scoreNeighborsForRetreat(chunk,
|
|
|
|
getNeighborChunks(map,
|
|
|
|
chunk.x,
|
|
|
|
chunk.y),
|
|
|
|
scoreRetreatLocation,
|
|
|
|
map)
|
|
|
|
local position = {
|
|
|
|
x = chunk.x + 16,
|
|
|
|
y = chunk.y + 16
|
|
|
|
}
|
|
|
|
local retreatPosition
|
|
|
|
local surface = map.surface
|
|
|
|
if (exitPath == -1) then
|
|
|
|
return
|
|
|
|
elseif (nextExitPath ~= -1) then
|
|
|
|
retreatPosition = findMovementPosition(
|
|
|
|
surface,
|
2023-03-13 03:31:46 +02:00
|
|
|
positionFromScaledDirections(
|
|
|
|
position,
|
|
|
|
1,
|
|
|
|
exitDirection,
|
|
|
|
nextExitDirection
|
2023-03-12 06:51:13 +02:00
|
|
|
)
|
|
|
|
)
|
|
|
|
exitPath = nextExitPath
|
|
|
|
else
|
|
|
|
retreatPosition = findMovementPosition(
|
|
|
|
surface,
|
2023-03-13 03:31:46 +02:00
|
|
|
positionFromScaledDirections(
|
|
|
|
position,
|
|
|
|
1,
|
|
|
|
exitDirection
|
2023-03-12 06:51:13 +02:00
|
|
|
)
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
if retreatPosition then
|
|
|
|
position.x = retreatPosition.x
|
|
|
|
position.y = retreatPosition.y
|
|
|
|
else
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
local newSquad = findNearbyRetreatingSquad(map, exitPath)
|
|
|
|
local created = false
|
|
|
|
|
|
|
|
if not newSquad then
|
|
|
|
if (Universe.squadCount < Universe.AI_MAX_SQUAD_COUNT) then
|
|
|
|
created = true
|
|
|
|
local base = findNearbyBase(chunk)
|
|
|
|
if not base then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
newSquad = Squad.createSquad(position, map, nil, false, base)
|
|
|
|
else
|
|
|
|
return
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-03-13 03:31:46 +02:00
|
|
|
Queries.fleeCommand.from = cause
|
|
|
|
Queries.retreatCommand.group = newSquad.group
|
2023-03-12 06:51:13 +02:00
|
|
|
|
2023-03-13 03:31:46 +02:00
|
|
|
Queries.formRetreatCommand.unit_search_distance = radius
|
2023-03-12 06:51:13 +02:00
|
|
|
|
2023-03-13 03:31:46 +02:00
|
|
|
local foundUnits = surface.set_multi_command(Queries.formRetreatCommand)
|
2023-03-12 06:51:13 +02:00
|
|
|
|
|
|
|
if (foundUnits == 0) then
|
|
|
|
if created then
|
|
|
|
newSquad.group.destroy()
|
|
|
|
end
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
if created then
|
|
|
|
Universe.groupNumberToSquad[newSquad.groupNumber] = newSquad
|
|
|
|
Universe.squadCount = Universe.squadCount + 1
|
|
|
|
end
|
|
|
|
|
|
|
|
newSquad.status = SQUAD_RETREATING
|
|
|
|
|
|
|
|
addSquadToChunk(chunk, newSquad)
|
|
|
|
|
|
|
|
newSquad.frenzy = true
|
|
|
|
local squadPosition = newSquad.group.position
|
|
|
|
newSquad.frenzyPosition.x = squadPosition.x
|
|
|
|
newSquad.frenzyPosition.y = squadPosition.y
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function Squad.createSquad(position, map, group, settlers, base)
|
|
|
|
local unitGroup = group or map.surface.create_unit_group({position=position})
|
|
|
|
|
|
|
|
local squad = {
|
|
|
|
group = unitGroup,
|
2023-03-26 23:26:15 +02:00
|
|
|
canBeCompressed = true,
|
|
|
|
compressionSet = nil,
|
|
|
|
compressionText = nil,
|
2023-03-12 06:51:13 +02:00
|
|
|
status = SQUAD_GUARDING,
|
|
|
|
rabid = false,
|
|
|
|
penalties = {},
|
|
|
|
base = base,
|
|
|
|
type = base.stateAI,
|
|
|
|
frenzy = false,
|
|
|
|
map = map,
|
|
|
|
wanders = 0,
|
|
|
|
settlers = settlers or false,
|
|
|
|
kamikaze = false,
|
|
|
|
frenzyPosition = {x = 0,
|
|
|
|
y = 0},
|
|
|
|
maxDistance = 0,
|
|
|
|
groupNumber = unitGroup.group_number,
|
|
|
|
originPosition = {x = 0,
|
|
|
|
y = 0},
|
|
|
|
commandTick = nil,
|
|
|
|
chunk = -1
|
|
|
|
}
|
|
|
|
|
|
|
|
if settlers then
|
|
|
|
squad.maxDistance = calculateSettlerMaxDistance()
|
|
|
|
end
|
|
|
|
|
|
|
|
if position then
|
|
|
|
squad.originPosition.x = position.x
|
|
|
|
squad.originPosition.y = position.y
|
|
|
|
elseif group then
|
|
|
|
squad.originPosition.x = group.position.x
|
|
|
|
squad.originPosition.y = group.position.y
|
|
|
|
end
|
|
|
|
|
|
|
|
return squad
|
|
|
|
end
|
|
|
|
|
|
|
|
function Squad.calculateKamikazeSquadThreshold(memberCount)
|
|
|
|
local threshold = (memberCount / Universe.attackWaveMaxSize) * 0.2 + (Universe.evolutionLevel * 0.2)
|
|
|
|
return threshold
|
|
|
|
end
|
|
|
|
|
|
|
|
function Squad.calculateKamikazeSettlerThreshold(memberCount)
|
|
|
|
local threshold = (memberCount / Universe.expansionMaxSize) * 0.2 + (Universe.evolutionLevel * 0.2)
|
|
|
|
return threshold
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
local function settlerWaveScaling()
|
|
|
|
return mCeil(gaussianRandomRangeRG(Universe.settlerWaveSize,
|
|
|
|
Universe.settlerWaveDeviation,
|
|
|
|
Universe.expansionMinSize,
|
|
|
|
Universe.expansionMaxSize,
|
|
|
|
Universe.random))
|
|
|
|
end
|
|
|
|
|
|
|
|
local function attackWaveScaling()
|
|
|
|
return mCeil(gaussianRandomRangeRG(Universe.attackWaveSize,
|
|
|
|
Universe.attackWaveDeviation,
|
|
|
|
1,
|
|
|
|
Universe.attackWaveUpperBound,
|
|
|
|
Universe.random))
|
|
|
|
end
|
|
|
|
|
2023-03-12 22:02:31 +02:00
|
|
|
local function attackWaveValidCandidate(chunk)
|
2023-03-12 06:51:13 +02:00
|
|
|
if isActiveNest(chunk) then
|
|
|
|
return true
|
|
|
|
end
|
2023-03-12 22:02:31 +02:00
|
|
|
local base = chunk.base
|
2023-03-12 06:51:13 +02:00
|
|
|
if (base.stateAI == BASE_AI_STATE_RAIDING) or
|
|
|
|
(base.stateAI == BASE_AI_STATE_SIEGE) or
|
|
|
|
(base.stateAI == BASE_AI_STATE_ONSLAUGHT)
|
|
|
|
then
|
|
|
|
return isActiveRaidNest(chunk)
|
|
|
|
end
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
2023-03-13 01:13:32 +02:00
|
|
|
local function scoreSettlerLocation(neighborChunk)
|
2023-03-12 06:51:13 +02:00
|
|
|
return neighborChunk[RESOURCE_PHEROMONE] + -neighborChunk[PLAYER_PHEROMONE]
|
|
|
|
end
|
|
|
|
|
2023-03-13 01:13:32 +02:00
|
|
|
local function scoreSiegeSettlerLocation(neighborChunk)
|
2023-03-12 06:51:13 +02:00
|
|
|
return (neighborChunk[RESOURCE_PHEROMONE] + neighborChunk[BASE_PHEROMONE]) + -neighborChunk[PLAYER_PHEROMONE]
|
|
|
|
end
|
|
|
|
|
2023-03-13 01:13:32 +02:00
|
|
|
local function scoreUnitGroupLocation(neighborChunk)
|
2023-03-12 06:51:13 +02:00
|
|
|
return neighborChunk[PLAYER_PHEROMONE] + neighborChunk[BASE_PHEROMONE]
|
|
|
|
end
|
|
|
|
|
2023-03-13 01:13:32 +02:00
|
|
|
local function validUnitGroupLocation(neighborChunk)
|
2023-03-12 06:51:13 +02:00
|
|
|
return (getPassable(neighborChunk) == CHUNK_ALL_DIRECTIONS) and
|
|
|
|
(not neighborChunk.nestCount)
|
|
|
|
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
|
|
|
|
|
|
|
|
--[[
|
|
|
|
Expects all neighbors adjacent to a chunk
|
|
|
|
--]]
|
2023-03-13 01:13:32 +02:00
|
|
|
local function scoreNeighborsForFormation(chunk, validFunction, scoreFunction)
|
2023-03-12 06:51:13 +02:00
|
|
|
local highestChunk = -1
|
|
|
|
local highestScore = -MAGIC_MAXIMUM_NUMBER
|
|
|
|
local highestDirection
|
2023-03-13 01:13:32 +02:00
|
|
|
local neighborChunks = getNeighborChunks(chunk.map, chunk.x, chunk.y)
|
2023-03-12 06:51:13 +02:00
|
|
|
for x=1,8 do
|
|
|
|
local neighborChunk = neighborChunks[x]
|
2023-03-13 01:13:32 +02:00
|
|
|
if (neighborChunk ~= -1) and validFunction(neighborChunk) then
|
|
|
|
local score = scoreFunction(neighborChunk)
|
2023-03-12 06:51:13 +02:00
|
|
|
if (score > highestScore) then
|
|
|
|
highestScore = score
|
|
|
|
highestChunk = neighborChunk
|
|
|
|
highestDirection = x
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
return highestChunk, highestDirection
|
|
|
|
end
|
|
|
|
|
2023-03-12 23:28:49 +02:00
|
|
|
function Squad.rallyUnits(chunk, tick)
|
|
|
|
if (tick - getRallyTick(chunk)) < COOLDOWN_RALLY then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
setRallyTick(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 = Universe.vengenceQueue
|
|
|
|
local map = chunk.map
|
|
|
|
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 then
|
|
|
|
local base = rallyChunk.base
|
|
|
|
if rallyChunk.nestCount
|
|
|
|
and (base.unitPoints >= AI_VENGENCE_SQUAD_COST)
|
|
|
|
then
|
2023-03-12 06:51:13 +02:00
|
|
|
vengenceQueue[rallyChunk.id] = rallyChunk
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-03-13 01:13:32 +02:00
|
|
|
local function deploySquad(name, chunk, cost, vengence, attacker)
|
2023-03-12 22:02:31 +02:00
|
|
|
local base = chunk.base
|
2023-03-12 06:51:13 +02:00
|
|
|
|
2023-03-13 01:13:32 +02:00
|
|
|
local lackingPoints = ((base.unitPoints - cost) < 0)
|
|
|
|
if attacker then
|
|
|
|
if lackingPoints
|
|
|
|
or ((chunk[BASE_PHEROMONE] < 0.0001) and (chunk[PLAYER_PHEROMONE] < 0.0001))
|
|
|
|
or (Universe.squadCount > Universe.AI_MAX_SQUAD_COUNT)
|
|
|
|
or (not vengence and not attackWaveValidCandidate(chunk))
|
|
|
|
or (Universe.random() > Universe.formSquadThreshold)
|
|
|
|
then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
else
|
|
|
|
if lackingPoints
|
|
|
|
or (Universe.builderCount > Universe.AI_MAX_BUILDER_COUNT)
|
|
|
|
or (not vengence and (base.sentExpansionGroups > base.maxExpansionGroups))
|
|
|
|
or (Universe.random() > Universe.formSquadThreshold)
|
|
|
|
then
|
|
|
|
return
|
2023-03-12 06:51:13 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-03-13 01:13:32 +02:00
|
|
|
local scoringFunction
|
|
|
|
if attacker then
|
|
|
|
scoringFunction = scoreUnitGroupLocation
|
|
|
|
else
|
|
|
|
scoringFunction = scoreSettlerLocation
|
|
|
|
if base.stateAI == BASE_AI_STATE_SIEGE then
|
|
|
|
scoringFunction = scoreSiegeSettlerLocation
|
2023-03-12 06:51:13 +02:00
|
|
|
end
|
2023-03-13 01:13:32 +02:00
|
|
|
end
|
2023-03-12 06:51:13 +02:00
|
|
|
|
2023-03-13 01:13:32 +02:00
|
|
|
local squadPath, squadDirection = scoreNeighborsForFormation(
|
|
|
|
chunk,
|
|
|
|
validUnitGroupLocation,
|
|
|
|
scoringFunction
|
|
|
|
)
|
|
|
|
if (squadPath == -1) then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
local map = chunk.map
|
|
|
|
local surface = map.surface
|
2023-03-13 03:31:46 +02:00
|
|
|
TargetPosition.x = chunk.x + HALF_CHUNK_SIZE
|
|
|
|
TargetPosition.y = chunk.y + HALF_CHUNK_SIZE
|
2023-03-13 01:13:32 +02:00
|
|
|
|
|
|
|
local squadPosition = findDeploymentPosition(
|
|
|
|
surface,
|
2023-03-13 03:31:46 +02:00
|
|
|
positionFromScaledDirections(
|
|
|
|
TargetPosition,
|
|
|
|
1.25,
|
|
|
|
squadDirection
|
|
|
|
)
|
2023-03-13 01:13:32 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
if not squadPosition then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
local squad = Squad.createSquad(squadPosition, map, nil, not attacker, base)
|
|
|
|
|
|
|
|
local scaledWaveSize
|
|
|
|
if attacker then
|
|
|
|
scaledWaveSize = attackWaveScaling()
|
|
|
|
else
|
|
|
|
scaledWaveSize = settlerWaveScaling()
|
|
|
|
end
|
2023-03-13 03:31:46 +02:00
|
|
|
Queries.formGroupCommand.group = squad.group
|
|
|
|
Queries.formCommand.unit_count = scaledWaveSize
|
|
|
|
local foundUnits = surface.set_multi_command(Queries.formCommand)
|
2023-03-13 01:13:32 +02:00
|
|
|
if (foundUnits == 0) then
|
|
|
|
if squad.group.valid then
|
|
|
|
squad.group.destroy()
|
2023-03-12 06:51:13 +02:00
|
|
|
end
|
2023-03-13 01:13:32 +02:00
|
|
|
return
|
2023-03-12 06:51:13 +02:00
|
|
|
end
|
|
|
|
|
2023-03-13 01:13:32 +02:00
|
|
|
if attacker then
|
|
|
|
squad.kamikaze = Universe.random() < Squad.calculateKamikazeSquadThreshold(foundUnits)
|
|
|
|
Universe.squadCount = Universe.squadCount + 1
|
|
|
|
if not vengence and (base.stateAI == BASE_AI_STATE_AGGRESSIVE) then
|
|
|
|
base.sentAggressiveGroups = base.sentAggressiveGroups + 1
|
2023-03-12 06:51:13 +02:00
|
|
|
end
|
2023-03-13 01:13:32 +02:00
|
|
|
else
|
|
|
|
local kamikazeThreshold = Squad.calculateKamikazeSettlerThreshold(foundUnits)
|
|
|
|
if base.stateAI == BASE_AI_STATE_SIEGE then
|
|
|
|
kamikazeThreshold = kamikazeThreshold * 2.5
|
|
|
|
end
|
|
|
|
squad.kamikaze = Universe.random() < kamikazeThreshold
|
|
|
|
base.sentExpansionGroups = base.sentExpansionGroups + 1
|
|
|
|
Universe.builderCount = Universe.builderCount + 1
|
2023-03-12 06:51:13 +02:00
|
|
|
end
|
2023-03-13 01:13:32 +02:00
|
|
|
|
|
|
|
squad.rabid = Universe.random() < 0.03
|
|
|
|
|
2023-03-26 23:26:15 +02:00
|
|
|
Squad.compressSquad(squad)
|
|
|
|
|
2023-03-13 01:13:32 +02:00
|
|
|
Universe.groupNumberToSquad[squad.groupNumber] = squad
|
|
|
|
modifyBaseUnitPoints(base, -cost, name, squadPosition.x, squadPosition.y)
|
|
|
|
end
|
|
|
|
|
|
|
|
function Squad.formSettlers(chunk)
|
|
|
|
deploySquad("Settler", chunk, AI_SETTLER_COST, false, false)
|
|
|
|
end
|
|
|
|
|
|
|
|
function Squad.formVengenceSettler(chunk)
|
|
|
|
deploySquad("Vengence Settler", chunk, AI_VENGENCE_SETTLER_COST, true, false)
|
2023-03-12 06:51:13 +02:00
|
|
|
end
|
|
|
|
|
2023-03-12 22:02:31 +02:00
|
|
|
function Squad.formSquads(chunk)
|
2023-03-13 01:13:32 +02:00
|
|
|
deploySquad("Squad", chunk, AI_SQUAD_COST, false, true)
|
|
|
|
end
|
2023-03-12 06:51:13 +02:00
|
|
|
|
2023-03-13 01:13:32 +02:00
|
|
|
function Squad.formVengenceSquad(chunk)
|
|
|
|
deploySquad("Vengence Squad", chunk, AI_VENGENCE_SQUAD_COST, true, true)
|
2023-03-12 06:51:13 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
function Squad.init(universe)
|
|
|
|
Universe = universe
|
2023-03-26 23:05:49 +02:00
|
|
|
Queries = universe.squadQueries
|
|
|
|
if Queries then
|
|
|
|
TargetPosition = Queries.targetPosition
|
|
|
|
SearchPath = Queries.searchPath
|
|
|
|
end
|
2023-03-12 06:51:13 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
SquadG = Squad
|
|
|
|
return Squad
|