1570 lines
46 KiB
Lua

if baseUtilsG then
return baseUtilsG
end
local baseUtils = {}
-- imports
local mathUtils = require("MathUtils")
local constants = require("Constants")
local chunkPropertyUtils = require("ChunkPropertyUtils")
local mapUtils = require("MapUtils")
-- constants
local FACTION_CHANGING_MAPPING = constants.FACTION_CHANGING_MAPPING
local FACTION_EVOLVE_MAPPING = constants.FACTION_EVOLVE_MAPPING
local MAGIC_MAXIMUM_NUMBER = constants.MAGIC_MAXIMUM_NUMBER
local BASE_AI_STATE_ACTIVE = constants.BASE_AI_STATE_ACTIVE
local BASE_AI_STATE_MUTATE = constants.BASE_AI_STATE_MUTATE
local BASE_EVOLVE_THRESHOLD = constants.BASE_EVOLVE_THRESHOLD
local GLOBAL_EVOLVE_COOLDOWN = constants.GLOBAL_EVOLVE_COOLDOWN
local FACTION_SET = constants.FACTION_SET
local BASE_DEADZONE_TTL = constants.BASE_DEADZONE_TTL
local BASE_AI_MIN_STATE_DURATION = constants.BASE_AI_MIN_STATE_DURATION
local BASE_AI_MAX_STATE_DURATION = constants.BASE_AI_MAX_STATE_DURATION
local HIVE_BUILDINGS_COST = constants.HIVE_BUILDINGS_COST
local BASE_DISTANCE_THRESHOLD = constants.BASE_DISTANCE_THRESHOLD
local BASE_DISTANCE_LEVEL_BONUS = constants.BASE_DISTANCE_LEVEL_BONUS
local BASE_DISTANCE_TO_EVO_INDEX = constants.BASE_DISTANCE_TO_EVO_INDEX
local BASE_COLLECTION_THRESHOLD = constants.BASE_COLLECTION_THRESHOLD
local CHUNK_SIZE = constants.CHUNK_SIZE
-- imported functions
local randomTickEvent = mathUtils.randomTickEvent
local euclideanDistancePoints = mathUtils.euclideanDistancePoints
local getChunkByPosition = mapUtils.getChunkByPosition
local gaussianRandomRange = mathUtils.gaussianRandomRange
local linearInterpolation = mathUtils.linearInterpolation
local mFloor = math.floor
local mMin = math.min
local mMax = math.max
local distort = mathUtils.distort
local getChunkBase = chunkPropertyUtils.getChunkBase
local setChunkBase = chunkPropertyUtils.setChunkBase
local getResourceGenerator = chunkPropertyUtils.getResourceGenerator
local getNestCount = chunkPropertyUtils.getNestCount
local getHiveCount = chunkPropertyUtils.getHiveCount
local getTurretCount = chunkPropertyUtils.getTurretCount
local getEnemyStructureCount = chunkPropertyUtils.getEnemyStructureCount
local getNestActiveness = chunkPropertyUtils.getNestActiveness
local next = next
local mRandom = math.random
-- module code
local function evoToTier(universe, evolutionFactor)
local v
for i=10,1,-1 do
if universe.evoToTierMapping[i] <= evolutionFactor then
v = i
if mRandom() <= 0.65 then
break
end
end
end
return v
end
-- + !КДА 2021.11
local function evoToTierNorandom(universe, evolutionFactor)
local v
for i=10,1,-1 do
if universe.evoToTierMapping[i] <= evolutionFactor then
v = i
break
end
end
return v
end
-- - !КДА 2021.11
function baseUtils.findNearbyBase(map, chunk, maxDistance, baseChangingChance)
local x = chunk.x
local y = chunk.y
local closest = MAGIC_MAXIMUM_NUMBER
local foundBase = getChunkBase(map, chunk)
if not foundBase then
if baseChangingChance and (mRandom() < baseChangingChance) then
return nil
else
local bases = map.universe.bases
local basesScanned = 0
for i, base in pairs(bases) do
if base.mapIndex == map.surface.index then
basesScanned = basesScanned + 1
local distance = euclideanDistancePoints(base.x, base.y, x, y)
if (distance <= base.distanceThreshold) and (distance < closest) then
closest = distance
foundBase = bases[i] --base
end
end
end
if maxDistance and (closest > maxDistance) then
foundBase = nil
end
end
end
return foundBase
end
-- + !КДА 2021.11
-- return faction name.
local function getFactionFromAligment(baseAlignment)
if not baseAlignment then
return "neutral"
end
local factions = {}
local factionTotal = 0
local ratesTotal = 0
for faction, rate in pairs(baseAlignment) do
factionTotal = factionTotal + 1
factions[factionTotal] = faction
ratesTotal = ratesTotal + mMax(rate,0)
end
if factionTotal == 0 then
return "neutral"
end
if factionTotal == 1 then
return factions[1]
end
local roll = mRandom()*ratesTotal
for faction, rate in pairs(baseAlignment) do
roll = roll - mMax(rate,0)
if (roll <= 0) then
return faction
end
end
return factions[1]
end
-- - !КДА 2021.11
-- return faction name. "neutral", "troll, etc..
local function findBaseMutation(map, targetEvolution, targetTier)
local universe = map.universe
local tier
if targetTier then
tier = targetTier
else
tier = evoToTierNorandom(universe, targetEvolution or map.evolutionLevel)
end
local alignments = universe.evolutionTableAlignment[tier]
local roll = mRandom()
for i=1,#alignments do
local alignment = alignments[i]
roll = roll - alignment[1]
if (roll <= 0) then
return alignment[2]
end
end
return (alignments[#alignments] or "neutral")
end
-- example: exceptFactions = {"neutral", "acid"}
-- alignments[x] = {0.2, "neutral"}...
-- filteredAligments{neutral = 0.2, acid = 0.1, ...}
-- mutationRates = {n1, n2,...} ==> return {faction1 = n1, faction2 = n2,...}
-- return {troll = 1, electic = 1,...}
local function findBaseMutations(map, targetEvolution, targetTier, mutationCount, mutationRates,exceptFactions)
local universe = map.universe
local tier
if targetTier then
tier = targetTier
else
tier = evoToTierNorandom(universe, targetEvolution or map.evolutionLevel)
end
local alignments = universe.evolutionTableAlignment[tier]
local filteredAligments = {}
local aligmentsCount = 0
local ratesSum = 0
for i=1,#alignments do
local alignment = alignments[i]
local FactionEnabled = true
if exceptFactions then
for u=1,#exceptFactions do
if alignment[2] == exceptFactions[u] then
FactionEnabled = false
break
end
end
end
if FactionEnabled then
aligmentsCount = aligmentsCount + 1
ratesSum = ratesSum + alignment[1]
filteredAligments[alignment[2]] = alignment[1]
end
end
local maxMutations = 1
if mutationCount then
maxMutations = mutationCount
end
local result = {}
for u = 1, maxMutations do
local roll = mRandom()*ratesSum
for faction, rate in pairs(filteredAligments) do
local alignment = alignments[i]
roll = roll - filteredAligments[faction]
if (roll <= 0) then
if mutationRates and mutationRates[u] then
result[faction] = mutationRates[u]
else
result[faction] = 1
end
aligmentsCount = aligmentsCount - 1
ratesSum = ratesSum - filteredAligments[faction]
filteredAligments[faction] = nil
break
end
end
if aligmentsCount == 0 then
break
end
end
return result
end
local function initialEntityUpgrade(faction, tier, maxTier, map, useHiveType)
local evolutionTable = map.universe.buildingEvolveLookup
local entity
local useTier
local tierRoll = mRandom()
if (tierRoll < 0.4) then
useTier = maxTier
elseif (tierRoll < 0.7) then
useTier = mMax(maxTier - 1, tier)
elseif (tierRoll < 0.9) then
useTier = mMax(maxTier - 2, tier)
else
useTier = mMax(maxTier - 3, tier)
end
local upgradesTier = evolutionTable[faction]
if not upgradesTier then
upgradesTier = evolutionTable["neutral"]
end
local upgrades = upgradesTier[useTier]
if upgrades then
if useHiveType then
for ui=1,#upgrades do
local upgrade = upgrades[ui]
if upgrade[3] == useHiveType then
entity = upgrade[2][mRandom(#upgrade[2])]
break
end
end
end
if not entity then
local roll = mRandom()
for ui=1,#upgrades do
local upgrade = upgrades[ui]
roll = roll - upgrade[1]
if (roll <= 0) then
entity = upgrade[2][mRandom(#upgrade[2])]
break
end
end
end
end
return entity
end
local function entityEvolve(originalEntity, map)
local universe = map.universe
local hiveType = universe.buildingHiveTypeLookup[originalEntity.name]
if not hiveType then
return originalEntity.name
end
local mapTypes = FACTION_EVOLVE_MAPPING[hiveType]
if not mapTypes then
return originalEntity.name
end
local faction = universe.enemyAlignmentLookup[originalEntity.name]
if not faction then
return originalEntity.name
end
local tier = universe.buildingTierLookup[originalEntity.name]
if not tier then
return originalEntity.name
end
local evolutionTable = map.universe.buildingEvolveLookup
local upgradesTier = evolutionTable[faction]
if not upgradesTier then
upgradesTier = evolutionTable["neutral"]
end
local upgrades = upgradesTier[tier]
if not upgrades then
return originalEntity.name
end
-- example: evolutionTable["troll"][3][1]={0.1, buildingSet, "biter-spawner"} (tier 3)
local factionUpgrades = {}
local ratesSum = 0
for i=1, #upgrades do
local upgrade = upgrades[i]
if mapTypes[upgrade[3]] then
ratesSum = ratesSum + upgrade[1]
factionUpgrades[#factionUpgrades+1] = {upgrade[1], upgrade[2], upgrade[3]}
end
end
local initRoll = mRandom()*ratesSum
local roll = initRoll + 0
for i = 1,#factionUpgrades do
local upgrade = factionUpgrades[i]
roll = roll - upgrade[1]
if roll<=0 then
local buildingSet = upgrade[2]
-- if (upgrade[3]~= hiveType) and (oldFaction==faction) then
-- game.print("entityUpgrade---------------start")
-- for u = 1,#factionUpgrades do
-- game.print(""..u..":"..factionUpgrades[u][1].."," .. factionUpgrades[u][3]..( (u == 1 and "<--here") or ""))
-- end
-- game.print("entityUpgrade---------------end")
-- end
return buildingSet[mRandom(#buildingSet)]
end
end
return originalEntity.name
end
local function entityUpgrade(faction, tier, maxTier, originalEntity, map)
local universe = map.universe
local hiveType = universe.buildingHiveTypeLookup[originalEntity.name]
if not hiveType then
return originalEntity.name
end
local oldFaction = universe.enemyAlignmentLookup[originalEntity.name]
local mapTypes = {}
if oldFaction==faction then
mapTypes[hiveType] = 0
else
mapTypes = FACTION_CHANGING_MAPPING[hiveType]
if not mapTypes then
return originalEntity.name
end
end
local useTier
if tier == maxTier then
useTier = maxTier
else
local tierRoll = mRandom()
if (tierRoll < 0.4) then
useTier = maxTier
elseif (tierRoll < 0.7) then
useTier = mMax(maxTier - 1, tier)
elseif (tierRoll < 0.9) then
useTier = mMax(maxTier - 2, tier)
else
useTier = mMax(maxTier - 3, tier)
end
end
local evolutionTable = map.universe.buildingEvolveLookup
if not evolutionTable[faction] then
return originalEntity.name
end
local upgrades = evolutionTable[faction][useTier]
if not upgrades then
return originalEntity.name
end
-- example: evolutionTable["troll"][3][1]={0.1, buildingSet, "biter-spawner"} (tier 3)
local factionUpgrades = {}
local ratesSum = 0
for i=1, #upgrades do
local upgrade = upgrades[i]
if mapTypes[upgrade[3]] then
ratesSum = ratesSum + upgrade[1]
factionUpgrades[#factionUpgrades+1] = {upgrade[1], upgrade[2], upgrade[3]}
end
end
local initRoll = mRandom()*ratesSum
local roll = initRoll + 0
for i = 1,#factionUpgrades do
local upgrade = factionUpgrades[i]
roll = roll - upgrade[1]
if roll<=0 then
local buildingSet = upgrade[2]
-- if (upgrade[3]~= hiveType) and (oldFaction==faction) then
-- game.print("entityUpgrade---------------start")
-- for u = 1,#factionUpgrades do
-- game.print(""..u..":"..factionUpgrades[u][1].."," .. factionUpgrades[u][3]..( (u == 1 and "<--here") or ""))
-- end
-- game.print("entityUpgrade---------------end")
-- end
return buildingSet[mRandom(#buildingSet)]
end
end
return originalEntity.name
end
local function findEntityUpgrade(faction, currentEvo, evoIndex, originalEntity, map, evolve)
local universe = map.universe
local adjCurrentEvo = mMax(
((faction ~= universe.enemyAlignmentLookup[originalEntity.name]) and 0) or currentEvo,
0
)
local tier = evoToTier(universe, adjCurrentEvo)
local maxTier = evoToTierNorandom(universe, evoIndex)
if (tier > maxTier) then
tier = maxTier
-- return nil
end
if evolve then
local chunk = getChunkByPosition(map, originalEntity.position)
local makeHive = false
if not chunk == -1 then
if getResourceGenerator(map, chunk) > 0 then
if mRandom() < 0.2 then
makeHive = true
end
else
if mRandom() < 0.02 then
makeHive = true
end
end
end
return initialEntityUpgrade(faction, tier, maxTier, map, (makeHive and "hive"))
else
return entityUpgrade(faction, tier, maxTier, originalEntity, map)
end
end
local function findBaseInitialAlignment(map, evoIndex)
local dev = evoIndex * 0.3
local evoTop = evoIndex
local result
if mRandom() < 0.05 then
result = findBaseMutations(map, evoTop, nil, 2, {0.5, 0.5}) --{troll = 0.5, acid = 0.5} -- + !КДА 2021.11
else
result = findBaseMutations(map, evoTop, nil, 2, {0.8, 0.2})
end
return result
end
function baseUtils.recycleBases(universe)
if not universe.bases then
return
end
local bases = universe.bases
local id = universe.recycleBaseIterator
if not id then
id, base = next(bases, nil)
end
if not id then
universe.recycleBaseIterator = nil
else
local base
base = bases[id]
if base then
local map = universe.maps[base.mapIndex]
if not map then
universe.growingBases[id] = nil
if universe.growingBasesIterator == id then
universe.growingBasesIterator = nil
end
bases[id] = nil
return
end
for chunk, _ in pairs(base.chunks) do
chunkBase = getChunkBase(map, chunk)
if (not chunkBase) then
setChunkBase(map, chunk, base)
--game.print("restore lost chunk [gps=" .. chunk.x .. "," .. chunk.y .."] to base [gps=" .. base.x .. "," .. base.y .."]") -- debug
elseif chunkBase.id ~= base.id then
base.chunks[chunk] = nil
chunkBase.chunks[chunk] = true
--game.print("reassign chunk [gps=" .. chunk.x .. "," .. chunk.y .."]".." from [gps=" .. base.x .. "," .. base.y .."] to [gps=" .. chunkBase.x .. "," .. chunkBase.y .."]") -- debug
end
end
local chunk = next(base.chunks, nil)
if not chunk then
--game.print("recycled base [gps=" .. base.x .. "," .. base.y .."]".. serpent.dump(base.chunks)) -- debug
universe.growingBases[id] = nil
if universe.growingBasesIterator == id then
universe.growingBasesIterator = nil
end
map.basesToGrow[id] = nil
bases[id] = nil
end
end
universe.recycleBaseIterator = next(bases, id)
end
end
local function deleteAssociatedUnits(entity)
if entity.valid and (entity.type == "unit-spawner") then
if entity.units then
for i, unit in pairs(entity.units) do
if unit and unit.valid and (unit.type == "unit") and (not unit.unit_group) then
unit.destroy() --unit.die(unit.force)
end
end
end
end
end
-- baseAlignment{fire = 0.8, acid = 0.2,...}
function baseUtils.upgradeEntity(entity, baseAlignment, map, disPos, evolve, addEntity)
local surface = map.surface
local position = entity.position
local currentEvo = (addEntity and 0) or entity.prototype.build_base_evolution_requirement or 0
local universe = map.universe
local faction
if type(baseAlignment)=="string" then
faction = baseAlignment
else
faction = getFactionFromAligment(baseAlignment)
end
if not faction then
if not addEntity then
entity.destroy()
end
return nil
end
local distance = mMin(1, euclideanDistancePoints(position.x, position.y, 0, 0) * BASE_DISTANCE_TO_EVO_INDEX)
local evoIndex = mMax(distance, map.evolutionLevel)
local spawnerName = findEntityUpgrade(faction,
currentEvo,
evoIndex,
entity,
map,
evolve)
if spawnerName and (addEntity or (spawnerName~=entity.name)) then
--game.print(entity.name.."->"..spawnerName.." [gps=" .. entity.position.x .. "," .. entity.position.y .."]") -- debug
if not addEntity then
deleteAssociatedUnits(entity)
entity.destroy()
end
local name = universe.buildingSpaceLookup[spawnerName] or spawnerName
local query = universe.upgradeEntityQuery
query.name = name
query.position = disPos or position
if addEntity or (not surface.can_place_entity(query)) then
local newPosition = surface.find_non_colliding_position(
(addEntity and "chunk-scanner-squad-rampant") or name,
disPos or position,
CHUNK_SIZE,
1,
true
)
-- if addEntity then
-- game.print(entity.name.."baseUtils.upgradeEntity: add source = "..entity.name .." [gps=" .. entity.position.x .. "," .. entity.position.y .."]") -- debug
-- end
if addEntity and not newPosition then
return nil
end
query.position = newPosition or disPos or position
end
query.name = spawnerName
if remote.interfaces["kr-creep"] then
remote.call("kr-creep", "spawn_creep_at_position", surface, query.position)
end
local newEntity = surface.create_entity(query)
return newEntity
end
return nil
end
function baseUtils.evolveEntity(entity, map)
if not entity.valid then
return nil
end
local oldName = entity.name
local spawnerName = entityEvolve(entity, map)
if spawnerName and (spawnerName~=oldName)then
-- game.print("evolve "..entity.name.."-->"..spawnerName) -- debug
local surface = map.surface
local position = entity.position
local universe = map.universe
deleteAssociatedUnits(entity)
entity.destroy()
local name = universe.buildingSpaceLookup[spawnerName] or spawnerName
local query = universe.upgradeEntityQuery
query.name = name
query.position = position
if not surface.can_place_entity(query) then
local newPosition = surface.find_non_colliding_position(
name,
position,
CHUNK_SIZE,
1,
true
)
query.position = newPosition or position
end
query.name = spawnerName
if remote.interfaces["kr-creep"] then
remote.call("kr-creep", "spawn_creep_at_position", surface, query.position)
end
return surface.create_entity(query)
else
--game.print("cant evolve "..entity.name)
end
return nil
end
function baseUtils.changeEntityTier(entity, newTier, map)
local surface = map.surface
local position = entity.position
local universe = map.universe
local oldFaction = universe.enemyAlignmentLookup[entity.name]
if not oldFaction then
return nil
end
local oldName = entity.name
--local tier = universe.buildingTierLookup[entity.name]
local spawnerName = entityUpgrade(oldFaction, newTier, newTier, entity, map)
if spawnerName and (spawnerName~=oldName)then
local name = spawnerName
local query = {}
query.name = name
query.position = position --position
query.move_stuck_players = true
local NewEntity = surface.create_entity(query)
if NewEntity then
deleteAssociatedUnits(entity)
entity.destroy()
return NewEntity
end
end
return nil
-- if not (spawnerName==oldName)then
-- entity.destroy()
-- local name = universe.buildingSpaceLookup[spawnerName] or spawnerName
-- local query = universe.upgradeEntityQuery
-- query.name = name
-- query.position = position
-- if not surface.can_place_entity(query) then
-- local newPosition = surface.find_non_colliding_position(
-- name,
-- position,
-- CHUNK_SIZE,
-- 1,
-- true
-- )
-- query.position = newPosition or position
-- end
-- query.name = spawnerName
-- if remote.interfaces["kr-creep"] then
-- remote.call("kr-creep", "spawn_creep_at_position", surface, query.position)
-- end
-- local newEntity = surface.create_entity(query)
-- if newEntity.valid then
-- return newEntity
-- else
-- return nil
-- end
-- end
-- return nil
end
function baseUtils.changeEntityAligment(entity, newAlignments, map)
if not entity.valid then
return nil
end
local surface = map.surface
local position = entity.position
local universe = map.universe
local newFaction
if type(newAlignments)=="string" then
newFaction = newAlignments
else
newFaction = getFactionFromAligment(newAlignments)
end
if not newFaction then
return nil
end
local oldFaction = universe.enemyAlignmentLookup[entity.name]
if oldFaction == newFaction then
return nil
end
local oldName = entity.name
local tier = universe.buildingTierLookup[entity.name]
local spawnerName = entityUpgrade(newFaction, tier, tier, entity, map)
if spawnerName and (spawnerName~=oldName) then
local name = spawnerName
local query = {}
query.name = name
query.position = position --position
query.move_stuck_players = true
local NewEntity = surface.create_entity(query)
if NewEntity then
deleteAssociatedUnits(entity)
entity.destroy()
return NewEntity
end
end
return nil
end
-- - !КДА 2021.11
local function replaceDeletedFactions(baseAlignment, map)
local evolutionTable = map.universe.buildingEvolveLookup
local factionsArray = {}
local alignmentCount = 0
for faction, rate in pairs(baseAlignment) do
alignmentCount = alignmentCount + 1
if not evolutionTable[faction] then
factionsArray[#factionsArray+1] = faction
end
end
for i = 1,#factionsArray do
alignmentCount = alignmentCount - 1
baseAlignment[factionsArray[i]] = nil
-- game.print("Removeing deleted faction:"..factionsArray[i])
end
if alignmentCount == 0 then
baseAlignment["neutral"] = 1
end
end
local function baseAlignmentAsString(baseAlignment)
local baseAlignmentAsString = ""
for faction, rate in pairs(baseAlignment) do
baseAlignmentAsString = baseAlignmentAsString..faction.."="..rate.."%, "
end
return baseAlignmentAsString
end
-- base.baseAlignment{troll = 0.7, acid = .3}
-- upgradeType = 0 - change rates: instant changes (changing base.alignment)
-- 1 - change rates, allow change lowest rated faction: instant changes
-- 2 - change lowest rated faction: lasting changes (changing base.newAlignmentAndSteps)
-- 3 - new aligments (new tier, for example): lasting changes
-- 4 - new rates for same factions: lasting changes
local function upgradeBase(map, base, upgradeType)
local RateStep = 0.05
local result = false
local oldAlignment
if base.newAlignmentAndSteps then
oldAlignment = base.newAlignmentAndSteps[2]
else
oldAlignment = base.alignment
end
local baseAlignment = base.alignment
local baseTier = base.tier
-- debug
-- local alignmentStr = ""
-- for faction, rate in pairs(oldAlignment) do
-- alignmentStr = alignmentStr .. faction .. ": "..rate..","
-- end
--
if (upgradeType == 0) or (upgradeType == 1) then -- not used since v1.3. So, mistakes possible
local factions = {}
local factionTotal = 0
for faction, rate in pairs(oldAlignment) do
factionTotal = factionTotal + 1
factions[factionTotal] = faction
end
if factionTotal>1 then
local roll = mRandom(factionTotal)
local roll2 = mRandom(factionTotal-1)
if roll2 >= roll then roll2 = roll2+1 end
if (oldAlignment[factions[roll]] - RateStep) < 0.005 then -- if rate too low, then do nothing or change faction
if upgradeType == 1 then
newFaction = findBaseMutation(map, nil, baseTier)
if not oldAlignment[newFaction] then
oldAlignment[newFaction] = oldAlignment[factions[roll]]
oldAlignment[factions[roll]] = nil
factions[roll] = newFaction -- optional, but let it be
result = true
end
end
else
oldAlignment[factions[roll]] = oldAlignment[factions[roll]] - RateStep
oldAlignment[factions[roll2]] = oldAlignment[factions[roll2]] + RateStep
result = true
end
end
elseif upgradeType == 2 then
local newFaction = findBaseMutation(map, nil, baseTier)
if not oldAlignment[newFaction] then
local oldFaction
local newAlignmentAndSteps = {10, {}}
local newAlignment = newAlignmentAndSteps[2]
for faction, rate in pairs(oldAlignment) do
newAlignment[faction] = rate
if not oldFaction or rate<oldAlignment[oldFaction] then -- looking for lowest rate
oldFaction = faction
end
end
if oldFaction then
if not newAlignment[newFaction] then
newAlignment[newFaction] = newAlignment[oldFaction]
newAlignment[oldFaction] = nil
base.newAlignmentAndSteps = newAlignmentAndSteps
result = true
end
end
end
elseif upgradeType == 3 then
local factionTotal = 0
local factionRates = {}
for faction, rate in pairs(oldAlignment) do
factionTotal = factionTotal + 1
factionRates[factionTotal] = rate
end
base.newAlignmentAndSteps = {30, findBaseMutations(map, nil, baseTier, factionTotal, factionRates)}
result = true
elseif upgradeType == 4 then
local newAlignmentAndSteps = {30, {}}
local newAlignment = newAlignmentAndSteps[2]
local ratesTotal = 0
for faction, rate in pairs(oldAlignment) do
newAlignment[faction] = mRandom(1, 10)
ratesTotal = ratesTotal + newAlignment[faction]
end
local finalRatesTotal = 0
for faction, rate in pairs(newAlignment) do
newAlignment[faction] = math.floor(100* newAlignment[faction] / ratesTotal) * 0.01
finalRatesTotal = finalRatesTotal + newAlignment[faction]
end
local faction = next(newAlignment, nil)
if faction then
newAlignment[faction] = newAlignment[faction] + (1 - finalRatesTotal)
base.newAlignmentAndSteps = newAlignmentAndSteps
result = true
end
end
-- debug
-- local newAlignmentDebug
-- if base.newAlignmentAndSteps then
-- newAlignmentDebug = base.newAlignmentAndSteps[2]
-- else
-- newAlignmentDebug = base.baseAlignment
-- end
-- if newAlignmentDebug then
-- alignmentStr = alignmentStr.."->"
-- for faction, rate in pairs(newAlignmentDebug) do
-- alignmentStr = alignmentStr .. faction .. ": "..rate..","
-- end
-- game.print("upgradeBase #"..base.id..", type = " ..upgradeType.." "..alignmentStr.." [gps=" .. base.x .. "," .. base.y .."]")
-- alignmentStr = ""
-- for chunk, _ in pairs(base.chunks) do
-- alignmentStr = alignmentStr .. "[gps=" .. chunk.x .. "," .. chunk.y .."]"
-- end
-- game.print(alignmentStr)
-- end
return result
end
local function updateBaseFactionsTotal(base)
local factionsTotal = {}
local totalCount = 0
for chunk, counts in pairs(base.chunkFactions) do
for faction,count in pairs(counts) do
if count>0 then
factionsTotal[faction] = (factionsTotal[faction] or 0) + count
totalCount = totalCount + count
end
end
end
base.factionsTotal = {totalCount, factionsTotal}
end
local function calculateDynamicRates(dynamicRates, factionsTotal)
if not factionsTotal then
return
end
local totalDynamicRate = 0
local changesCnt = 0 -- recommended changes
local obsoleteCnt = 0
local dynamicRatesTable = dynamicRates.dynamicRatesTable
dynamicRatesTable.dynamicRate = {}
local fieldRate = dynamicRatesTable.rate
local fieldDynamicRate = dynamicRatesTable.dynamicRate
local fieldReqCount = dynamicRatesTable.reqCount
for faction,rates in pairs(fieldRate) do
fieldDynamicRate[faction] = fieldReqCount[faction] - (factionsTotal[faction] or 0)
local dynamicRate = fieldDynamicRate[faction]
if dynamicRate>0 then
totalDynamicRate = totalDynamicRate + dynamicRate
end
changesCnt = changesCnt + mMax(mFloor(dynamicRate), 0)
if (fieldRate[faction] or 0) <=0 and (factionsTotal[faction] or 0)>0 then
obsoleteCnt = obsoleteCnt + 1
end
end
if totalDynamicRate == 0 then totalDynamicRate = 1 end
dynamicRates.totalDynamicRate = totalDynamicRate
dynamicRates.changesCnt = changesCnt
dynamicRates.obsoleteCnt = obsoleteCnt
end
-- return {totalRate=N, totalDynamicRate=N, changesCnt=N, obsoleteCnt=N, dynamicRatesTable={rate={},reqCount = {},dynamicRate = {}})
-- example : base.factionsTotal{{"acid",7},{"fire", 3}}, base.alignment{{"acid",0.8},{"troll",0.2}}
-- => totalCount=10, totalRate = 1
-- => dynamicRatesTable["reqCount"] = {acid = 10*(0.8/1)=8, troll = 10*(0.2/1)=2, fire = 10*0=0 }
-- => dynamicRatesTable["dynamicRate"] = {acid = 8-7=1, troll = 2 - 0 = 2, fire = 0-3 =>0 } , totalDynamicRate = 1+2=3
-- => 33% acid, 67% troll, 0% fire
function baseUtils.getDynamicRates(base)
local msgSting = "baseUtils.getDynamicRates: ("..tostring(base.x)..","..tostring(base.y)..")" -- + !КДА 2021.11
local aligments
aligments = base.alignment
local totalRate = 0
local dynamicRates = {totalRate = 0, totalDynamicRate = 0, changesCnt = 0, obsoleteCnt = 0, dynamicRatesTable={}}
local dynamicRatesTable = {rate={}}
local fieldRate = dynamicRatesTable.rate
for faction,rate in pairs(aligments) do
totalRate = totalRate + rate
fieldRate[faction] = rate
end
if totalRate == 0 then totalRate = 1 end
local totalCount
if not(base.factionsTotal) or base.factionsTotal[1]==0 then
totalCount = 0
else
totalCount = base.factionsTotal[1]
end
local factionsTotal = base.factionsTotal[2]
for faction,rate in pairs(factionsTotal) do
if not fieldRate[faction] then
fieldRate[faction] = 0
end
end
dynamicRatesTable.reqCount = {}
local fieldReqCount = dynamicRatesTable.reqCount
for faction,rates in pairs(fieldRate) do
fieldReqCount[faction] = totalCount*(fieldRate[faction]/totalRate)
end
dynamicRates.dynamicRatesTable = dynamicRatesTable
calculateDynamicRates(dynamicRates, factionsTotal)
dynamicRates.totalRate = totalRate
return dynamicRates
end
function baseUtils.changeEntityAndUpdateDynamicRates(entity, dynamicRates, map, base)
local universe = map.universe
local oldCnt = 0
local newFaction
if not universe.buildingHiveTypeLookup[entity.name] then
return nil
end
local oldFaction = universe.enemyAlignmentLookup[entity.name]
if not oldFaction then
return nil
end
local dynamicRatesTable = dynamicRates.dynamicRatesTable
if (dynamicRates.dynamicRatesTable.dynamicRate[oldFaction] or 0)>0 then
return nil
end
-- preventing the "flashing" of the hive.
if (universe.buildingHiveTypeLookup[entity.name] == "hive") and (dynamicRates.dynamicRatesTable.dynamicRate[oldFaction] or 0)>-0.3 then
return nil
end
if entity.type =="unit-spawner" then
oldCnt = 1
newFaction = getFactionFromAligment(dynamicRates.dynamicRatesTable.dynamicRate)
else
newFaction = getFactionFromAligment(dynamicRates.dynamicRatesTable.rate)
end
if oldFaction == newFaction then
return nil
end
local newEntity = baseUtils.changeEntityAligment(entity, newFaction, map)
if newEntity then
if base and base.factionsTotal then
local factionsTotal = base.factionsTotal[2]
local newCnt = 0
if newEntity.type =="unit-spawner" then
newCnt = 1
end
if oldCnt > 0 then
factionsTotal[oldFaction] = (factionsTotal[oldFaction] or 0) - oldCnt
end
if newCnt > 0 then
factionsTotal[newFaction] = (factionsTotal[newFaction] or 0) - newCnt
end
base.factionsTotal[1] = base.factionsTotal[1] + newCnt - oldCnt
if oldCnt>0 or newCnt>0 then
calculateDynamicRates(dynamicRates, factionsTotal)
end
end
return newEntity
end
return nil
end
local function updateBaseStats(base)
local aligments
aligments = base.alignment
if not aligments then
return false
end
-- base.factionsTotal = {totalCount, factionsTotal}
updateBaseFactionsTotal(base)
dynamicRates = baseUtils.getDynamicRates(base) -- => {totalRate=N, totalDynamicRate=N, changesCnt=N, obsoleteCnt=N, dynamicRatesTable=Table)
base.changingEntities = false
-- if dynamicRates.obsoleteCnt > 0 then
-- base.changingEntities = true
-- else
if base.factionsTotal then
local totalCount = base.factionsTotal[1]
local thresholdCount = 0
if totalCount <= 3 then
thresholdCount = 1
elseif totalCount<10 then
thresholdCount = 2
else
thresholdCount = mMin(totalCount*0.2, 8)
end
if dynamicRates.changesCnt >= thresholdCount then
base.changingEntities = true
end
end
end
-- alignmentAndStep{N, alignment}
-- be careful: changing alignmentAndStep[1]
local function convertAlignment(alignment, alignmentAndStep, writeToalignment)
if not alignmentAndStep then
return alignment
end
local steps = alignmentAndStep[1]
if steps<=1 then
if not writeToalignment then
alignmentAndStep[1] = 0
return alignmentAndStep[2]
else
alignmentAndStep[1] = 1
end
end
local newAlignment = {}
for faction,rate in pairs(alignmentAndStep[2]) do
newAlignment[faction] = rate
end
for faction,rate in pairs(alignment) do
newAlignment[faction] = (newAlignment[faction] or 0) - rate
end
local TableToChange
if writeToalignment then
TableToChange = alignment
else
TableToChange = newAlignment
end
for faction,rate in pairs(newAlignment) do
TableToChange[faction] = (alignment[faction] or 0) + rate/steps
if TableToChange[faction]<=0 then TableToChange[faction]=nil end
end
alignmentAndStep[1] = alignmentAndStep[1] - 1
return TableToChange
end
function baseUtils.chunkCanGrow(map, chunk)
if chunk.growFails and (chunk.growFails > 5) then
return false
end
local nestCount = getNestCount(map, chunk) + getHiveCount(map, chunk)
local turretCount = getTurretCount(map, chunk)
if (nestCount < 3) or ((nestCount + turretCount) < 10) then
return true
end
--game.print("baseUtils.chunkCanGrow: [gps=" .. chunk.x .. "," .. chunk.y .."] = false" ) -- debug
return false
end
function baseUtils.processBase(chunk, map, tick, base)
if not base.alignment then
return
end
if not tick then
return
end
if not chunk then
return
end
if not base.thisIsRampantEnemy then
base.tick = tick+36000
return
end
local surface = map.surface
local universe = map.universe
local point = universe.position
point.x = chunk.x + (CHUNK_SIZE * mRandom())
point.y = chunk.y + (CHUNK_SIZE * mRandom())
local newTier = evoToTierNorandom(map.universe, map.evolutionLevel) - base.tierHandicap
if base.tier < newTier then
base.tier = newTier
base.nextMutationTick = tick + 36000
upgradeBase(map, base, 3)
base.state = BASE_AI_STATE_ACTIVE
elseif (base.state == BASE_AI_STATE_MUTATE) and ((base.nextMutationTick or 0) < tick) and (not base.changingEntities) and (not base.newAlignmentAndSteps) then
local mutateRoll = mRandom()
--base.nextMutationTick = tick + 36000
if (mutateRoll < 0.05) then
upgradeBase(map, base, 3)
elseif (mutateRoll< 0.3) then
upgradeBase(map, base, 2)
else
upgradeBase(map, base, 4)
end
base.state = BASE_AI_STATE_ACTIVE
end
replaceDeletedFactions(base.alignment, map)
if base.newAlignmentAndSteps then
replaceDeletedFactions(base.newAlignmentAndSteps[2], map)
end
updateBaseStats(base)
------- growing
local baseCanGrow = false
local chunksCount = 0
local activeChunks = 0
for chunk, _ in pairs(base.chunks) do
if (getNestActiveness(map, chunk) > 0) then
activeChunks = activeChunks + 1
-- baseCanGrow = baseCanGrow or baseUtils.chunkCanGrow(map, chunk) -- only active chunk
end
chunksCount = chunksCount + 1
end
if (activeChunks > 0) and (chunksCount <= 5) then
for chunk, _ in pairs(base.chunks) do
baseCanGrow = baseCanGrow or baseUtils.chunkCanGrow(map, chunk) -- only active chunk
end
end
if baseCanGrow then
if not map.basesToGrow[base.id] then
map.basesToGrow[base.id] = base
-- game.print("add base to growing list: #"..base.id) -- debug
end
else
if map.basesToGrow[base.id] then
universe.growingBases[base.id] = nil
map.basesToGrow[base.id] = nil
if universe.growingBasesIterator == base.id then
universe.growingBasesIterator = nil
end
-- game.print("base stop growing: #"..base.id.." activeChunks = "..activeChunks..", baseCanGrow =".. tostring(baseCanGrow)..", chunksCount = "..chunksCount) -- debug
end
end
-------
if base.newAlignmentAndSteps then
convertAlignment(base.alignment, base.newAlignmentAndSteps, true)
if base.newAlignmentAndSteps[1] == 0 then base.newAlignmentAndSteps = nil end
end
if (universe.evolveTick <=tick) and (not base.changingEntities) and (not base.newAlignmentAndSteps) and ((base.evolveTick or 0) <= tick) then
local entities = surface.find_entities_filtered(universe.filteredEntitiesPointQueryLimited)
if #entities ~= 0 then
local entity = entities[1]
local cost = universe.costLookup[entity.name]
if cost then
local newEntity = baseUtils.evolveEntity(entity, map)
if newEntity then
base.evolveTick = tick + cost
universe.evolveTick = tick + GLOBAL_EVOLVE_COOLDOWN
end
end
end
end
base.tick = tick
end
function baseUtils.createBase(map, chunk, tick, thisIsRampantEnemy)
if not tick then
return nil
end
if not chunk then
return nil
end
local universe = map.universe
local x = chunk.x
local y = chunk.y
local distance = euclideanDistancePoints(x, y, 0, 0)
local meanLevel = mFloor(distance * 0.005)
local distanceIndex = mMin(1, distance * BASE_DISTANCE_TO_EVO_INDEX)
local evoIndex = mMax(distanceIndex, map.evolutionLevel)
local tier = evoToTierNorandom(map.universe, evoIndex)
local tierHandicap
if tier == 1 then
if mRandom()<0.7 then
tierHandicap = 0
else
tierHandicap = 1
end
else
tierHandicap = mRandom(mMin(3, tier))-1
end
local baseTick = (tick or 0)
local alignment
alignment = findBaseInitialAlignment(map, evoIndex) or {neutral = 1}
local baseLevel = gaussianRandomRange(meanLevel, meanLevel * 0.3, meanLevel * 0.50, meanLevel * 1.50)
local baseDistanceThreshold = gaussianRandomRange(BASE_DISTANCE_THRESHOLD,
BASE_DISTANCE_THRESHOLD * 0.2,
BASE_DISTANCE_THRESHOLD * 0.75,
BASE_DISTANCE_THRESHOLD * 1.50)
local distanceThreshold = (baseLevel * BASE_DISTANCE_LEVEL_BONUS) + baseDistanceThreshold
local base = {
x = x,
y = y,
distanceThreshold = distanceThreshold, -- not used, but maybe later...
tick = baseTick,
alignment = alignment,
state = BASE_AI_STATE_ACTIVE,
damagedBy = {},
stateTick = 0,
createdTick = (tick or 0),
evolveTick = (tick or 0) + BASE_EVOLVE_THRESHOLD,
nextMutationTick = tick + 36000,
points = 0,
thisIsRampantEnemy = thisIsRampantEnemy,
mapIndex = map.surface.index,
chunks = {},
basesToGrow = {},
id = universe.baseId
, chunkFactions = {}, factionsTotal = nil, changingEntities = false, tier = tier, tierHandicap = tierHandicap, newAlignmentAndSteps = nil
}
universe.baseId = universe.baseId + 1
setChunkBase(map, chunk, base)
universe.bases[base.id] = base
return base
end
-- + !КДА 2021.11
function baseUtils.findNewBaseAligment(map, chunk)
if not chunk then
return {neutral = 1}
end
local x = chunk.x
local y = chunk.y
local distance = euclideanDistancePoints(x, y, 0, 0)
local distanceIndex = mMin(1, distance * BASE_DISTANCE_TO_EVO_INDEX)
local evoIndex = mMax(distanceIndex, map.evolutionLevel)
local alignment = findBaseInitialAlignment(map, evoIndex) or {neutral = 1}
return alignment
end
-- - !КДА 2021.11
function baseUtils.rebuildNativeTables(universe)
local alignmentSet = {}
universe.evolutionTableAlignment = alignmentSet
local buildingSpaceLookup = {}
universe.buildingSpaceLookup = buildingSpaceLookup
local enemyAlignmentLookup = {}
universe.enemyAlignmentLookup = enemyAlignmentLookup
local evoToTierMapping = {}
universe.evoToTierMapping = evoToTierMapping
local upgradeLookup = {}
universe.upgradeLookup = upgradeLookup
local buildingEvolveLookup = {}
universe.buildingEvolveLookup = buildingEvolveLookup
local costLookup = {}
universe.costLookup = costLookup
local buildingHiveTypeLookup = {}
universe.buildingHiveTypeLookup = buildingHiveTypeLookup
local buildingTierLookup = {} -- + !КДА 2021.11
universe.buildingTierLookup = buildingTierLookup
for i=1,10 do
evoToTierMapping[#evoToTierMapping+1] = (((i - 1) * 0.1) ^ 0.8) - 0.01
end
for i=1,#FACTION_SET do
local faction = FACTION_SET[i]
local factionUpgradeLookup = {}
upgradeLookup[faction.type] = factionUpgradeLookup
local factionBuildingPicker = {}
buildingEvolveLookup[faction.type] = factionBuildingPicker
for t=1,10 do
local alignments = alignmentSet[t]
if not alignments then
alignments = {}
alignmentSet[t] = alignments
end
--[[
alignments table is a table that is used for selecting what factions are available
to pick given an evolution level.
evolutionTable is a table that given a faction allows the selection of a building
type based on the propabilities given. Once the the building type is selected given
a faction, then the evolution decides what level of building to select
--]]
local factionAcceptRate = faction.acceptRate
local low = factionAcceptRate[1]
local high = factionAcceptRate[2]
if (low <= t) and (t <= high) then
alignments[#alignments+1] = {linearInterpolation((t - low) / (high - low), factionAcceptRate[3], factionAcceptRate[4]), faction.type}
end
local tieredUpgradeBuildingSet = factionUpgradeLookup[t]
if not tieredUpgradeBuildingSet then
tieredUpgradeBuildingSet = {}
factionUpgradeLookup[t] = tieredUpgradeBuildingSet
end
local tieredBuildingPickerSet = factionBuildingPicker[t]
if not tieredBuildingPickerSet then
tieredBuildingPickerSet = {}
factionBuildingPicker[t] = tieredBuildingPickerSet
end
for b=1,#faction.buildings do
local building = faction.buildings[b]
local buildingSet = tieredUpgradeBuildingSet[building.type]
if not buildingSet then
buildingSet = {}
tieredUpgradeBuildingSet[building.type] = buildingSet
end
local variationSet = {}
for v=1,universe.ENEMY_VARIATIONS do
local entry = faction.type .. "-" .. building.name .. "-v" .. v .. "-t" .. t .. "-rampant"
enemyAlignmentLookup[entry] = faction.type
costLookup[entry] = HIVE_BUILDINGS_COST[building.type]
buildingHiveTypeLookup[entry] = building.type
buildingTierLookup[entry] = t
if v==1 then
variationSet[#variationSet+1] = entry
end
end
local buildingAcceptRate = building.acceptRate
local buildingLow = buildingAcceptRate[1]
local buildingHigh = buildingAcceptRate[2]
if (buildingLow <= t) and (t <= buildingHigh) then
for vi=1,#variationSet do
local variation = variationSet[vi]
buildingSet[#buildingSet+1] = variation
end
tieredBuildingPickerSet[#tieredBuildingPickerSet+1] = {
linearInterpolation((t - buildingLow) / (buildingHigh - buildingLow),
buildingAcceptRate[3],
buildingAcceptRate[4]),
variationSet,
building.type
}
end
end
end
end
for t=1,10 do
local alignments = alignmentSet[t]
local totalAlignment = 0
for i=1,#alignments do
totalAlignment = totalAlignment + alignments[i][1]
end
for i=1,#alignments do
alignments[i][1] = alignments[i][1] / totalAlignment
end
for fi=1,#FACTION_SET do
local faction = FACTION_SET[fi]
local factionBuildingSet = buildingEvolveLookup[faction.type][t]
local totalBuildingSet = 0
for i=1,#factionBuildingSet do
totalBuildingSet = totalBuildingSet + factionBuildingSet[i][1]
end
for i=1,#factionBuildingSet do
factionBuildingSet[i][1] = factionBuildingSet[i][1] / totalBuildingSet
end
end
end
local cnt = 0 ---------------------------
if universe.maps then
for _,base in pairs(universe.bases) do
local map = universe.maps[base.mapIndex]
if map then
local evoIndex = evoToTier(universe, map.evolutionLevel)
cnt = cnt + 1
for x=1,#base.alignment do
local alignment = base.alignment[x]
if not universe.buildingEvolveLookup[alignment] then
base.alignment = findBaseInitialAlignment(map, evoIndex) or {neutral = 1}
break
end
end
end
end
end
end
function baseUtils.setRandomBaseToMutate(universe)
if not universe.NEW_ENEMIES then
return
end
if game.tick < 2000 then
return
end
local basesId = {}
for _,base in pairs(universe.bases) do
if base and base.thisIsRampantEnemy and base.alignment and (base.state ~= BASE_AI_STATE_MUTATE) and (base.tier > 1) then
basesId[#basesId + 1] = base.id
end
end
if #basesId == 0 then
return
end
local base = universe.bases[basesId[mRandom(1, #basesId)]]
base.state = BASE_AI_STATE_MUTATE
end
-- + !КДА 2021.10 debug
function baseUtils.ShowNewBaseAligments(universe, targetEvolution)
if universe == nil then
return ""
end
local tier = evoToTierNorandom(universe, targetEvolution)
local alignments = universe.evolutionTableAlignment[tier]
game.print("Evo = "..tostring(targetEvolution*100).."%, tier:"..tostring(tier))
for i=1,#alignments do
local alignment = alignments[i]
local aligmentStats = tostring(i)..":"
for x=1,#alignment do
aligmentStats = aligmentStats .. ", " .. alignment[x]
end
game.print(aligmentStats)
end
return ""
end
function baseUtils.ShowEvoToTierMapping(universe)
for i=10,1,-1 do
game.print(tostring(i)..":"..tostring(universe.evoToTierMapping[i]))
end
return ""
end
-- - !КДА
baseUtilsG = baseUtils
return baseUtils