|
DefineClass.ShipmentSquadPreset = { |
|
__parents = { "Preset" }, |
|
properties = { |
|
{ id = "SquadName", editor = "text", default = "", translate = true }, |
|
{ id = "IntelTitle", editor = "text", default = "", translate = true }, |
|
{ id = "IntelText", editor = "text", default = "", translate = true }, |
|
{ id = "icon", editor = "ui_image", default = "" }, |
|
{ id = "intel_icon", editor = "ui_image", default = "" }, |
|
{ id = "badge_icon", editor = "ui_image", default = "" }, |
|
{ id = "squad_icon", editor = "ui_image", default = "" }, |
|
{ id = "squad_icon_2", editor = "ui_image", default = "" }, |
|
{ id = "item", editor = "combo", items = ClassDescendantsCombo("InventoryItem"), default = "DiamondBriefcase" }, |
|
{ id = "enemy_squad_def", editor = "combo", items = function (self) return table.keys(EnemySquadDefs, true) end, default = "DiamondBriefcase" }, |
|
{ id = "EnableConditions", name = "Conditions", editor = "nested_list", default = false, base_class = "Condition", }, |
|
{ id = "weight", editor = "number", default = 100 }, |
|
|
|
{ category = "Timeline", id = "TimelineEventTitle", editor = "text", default = "", translate = true }, |
|
{ category = "Timeline", id = "TimelineEventText", editor = "text", default = "", translate = true }, |
|
{ category = "Timeline", id = "TimelineEventHint", editor = "text", default = "", translate = true }, |
|
}, |
|
GlobalMap = "ShipmentPresets", |
|
Documentation = "Defines a squad preset that can be spawned from the diamond briefcase logic in SpawnDynamicDBSquad(). The properties mainly define how the spawned shipment squad will look like in the satellite UI, the condition for spawning the squad and the items and units it consists of.", |
|
} |
|
|
|
DefineModItemPreset("ShipmentSquadPreset", { |
|
EditorName = "Shipment squad preset", |
|
EditorSubmenu = "Satellite", |
|
}) |
|
|
|
function DbgShipmentShowMeSourceDest() |
|
DbgClearSectorTexts() |
|
for _, s in pairs(gv_Sectors) do |
|
local text = false |
|
if s.DBSourceSector then |
|
text = "Source" |
|
end |
|
if s.DBDestinationSector then |
|
if text then |
|
text = text .. " " |
|
end |
|
text = (text or "") .. "Dest" |
|
end |
|
if text then |
|
DbgAddSectorText(s.Id, text) |
|
end |
|
end |
|
end |
|
|
|
|
|
local lMaxStaticSquads = 3 |
|
local lMinDynamicSquadRouteLength = 10 |
|
local lMaxDynamicSquads = 2 |
|
local lDynamicSquadDayCooldown = 3 |
|
local lDynamicSquadDayChanceToSpawn = 7 |
|
DynamicSquadSpawnChanceOnScout = 25 |
|
|
|
|
|
|
|
function InitDiamondBriefcaseSquads(guaranteed_spawn) |
|
local viableSectors = {} |
|
for id, sector in sorted_pairs(gv_Sectors) do |
|
if sector.Guardpost and (not guaranteed_spawn or not table.find(guaranteed_spawn, id)) then |
|
viableSectors[#viableSectors + 1] = id |
|
end |
|
end |
|
|
|
local spawned = 0 |
|
local spawnOn = {} |
|
for i = 0, lMaxStaticSquads do |
|
if #viableSectors == 0 then break end |
|
|
|
local random = BraidRandom(xxhash(Game.id, i), 1, #viableSectors) |
|
local randomId = table.remove(viableSectors, random) |
|
spawnOn[#spawnOn + 1] = randomId |
|
end |
|
|
|
for i, sector in ipairs(guaranteed_spawn) do |
|
spawnOn[#spawnOn + 1] = sector |
|
end |
|
|
|
local squadDef = EnemySquadDefs["DiamondBriefcase"] |
|
local squadDefCarrier = squadDef.DiamondBriefcaseCarrier |
|
assert(squadDefCarrier) |
|
for i, sectorId in ipairs(spawnOn) do |
|
|
|
local _, enemySquads = GetSquadsInSector(sectorId) |
|
if enemySquads and #enemySquads > 0 then |
|
for i, sq in ipairs(enemySquads) do |
|
if sq.diamond_briefcase then |
|
goto continue |
|
end |
|
end |
|
end |
|
|
|
local bestUnit = false |
|
for i, sq in ipairs(enemySquads) do |
|
local units = sq.units |
|
for i, u in ipairs(units) do |
|
local ud = gv_UnitData[u] |
|
local template = UnitDataDefs[ud.class] |
|
if not ud.villain and not template.ImportantNPC then |
|
bestUnit = ud |
|
break |
|
end |
|
end |
|
if bestUnit then break end |
|
end |
|
|
|
if not bestUnit then |
|
local unitIds, unitNames, unitSources, unitAppearance = GenerateRandEnemySquadUnits(squadDef.id) |
|
local units = GenerateUnitsFromTemplates(sectorId, unitIds, "StaticDB", unitNames, unitAppearance) |
|
local squad_id = CreateNewSatelliteSquad( |
|
{ |
|
Side = "enemy1", |
|
CurrentSector = sectorId, |
|
Name = squadDef.displayName and _InternalTranslate(squadDef.displayName) or SquadName:GetNewSquadName("enemy1", units), |
|
diamond_briefcase = true, |
|
shipment_preset_id = "DiamondShipment", |
|
enemy_squad_def = squadDef.id, |
|
}, |
|
units |
|
) |
|
for i, s in ipairs(unitSources) do |
|
if s == squadDefCarrier then |
|
bestUnit = units[i] |
|
break |
|
end |
|
end |
|
bestUnit = gv_UnitData[bestUnit] |
|
assert(bestUnit) |
|
end |
|
|
|
local dbItem = PlaceInventoryItem("DiamondBriefcase") |
|
dbItem.drop_chance = 100 |
|
dbItem.extra_tag = "dynamic-db" |
|
bestUnit:AddItem("Inventory", dbItem) |
|
|
|
::continue:: |
|
end |
|
end |
|
|
|
GameVar("DynamicDBSquadAccumChance", 0) |
|
GameVar("DynamicDBSquadLastSpawnTime", 0) |
|
|
|
function OnMsg.NewDay() |
|
if gv_SatelliteAttacksHalted then return end |
|
|
|
DynamicDBSquadAccumChance = DynamicDBSquadAccumChance or 0 |
|
DynamicDBSquadLastSpawnTime = DynamicDBSquadLastSpawnTime or 0 |
|
|
|
if DynamicDBSquadLastSpawnTime - Game.CampaignTime > const.Scale.day * lDynamicSquadDayCooldown then |
|
return |
|
end |
|
|
|
local currentDynamicSquadsOnMap = 0 |
|
for i, sq in ipairs(g_SquadsArray) do |
|
if sq.diamond_briefcase_dynamic then |
|
currentDynamicSquadsOnMap = currentDynamicSquadsOnMap + 1 |
|
end |
|
end |
|
if currentDynamicSquadsOnMap >= lMaxDynamicSquads then return end |
|
|
|
DynamicDBSquadAccumChance = DynamicDBSquadAccumChance + lDynamicSquadDayChanceToSpawn |
|
if DynamicDBSquadAccumChance > InteractionRand(100, "ShipmentRoll") then |
|
SpawnDynamicDBSquad() |
|
DynamicDBSquadLastSpawnTime = Game.CampaignTime |
|
DynamicDBSquadAccumChance = 0 |
|
end |
|
end |
|
|
|
function PickShipmentPreset() |
|
local shipmentPresets = Presets.ShipmentSquadPreset |
|
|
|
local weights = {} |
|
for i, group in ipairs(shipmentPresets) do |
|
for i, preset in ipairs(group) do |
|
if EvalConditionList(preset.Conditions) then |
|
weights[#weights + 1] = { preset.weight, preset.id } |
|
end |
|
end |
|
end |
|
|
|
return GetWeightedRandom(weights, xxhash(Game.id, Game.CampaignTime, gv_NextSquadUniqueId)) |
|
end |
|
|
|
function GetAllShipmentItems() |
|
local shipmentPresets = Presets.ShipmentSquadPreset |
|
|
|
local items = {} |
|
for i, group in ipairs(shipmentPresets) do |
|
for i, preset in ipairs(group) do |
|
items[#items + 1] = { preset.item, preset.id } |
|
end |
|
end |
|
return items |
|
end |
|
|
|
if FirstLoad then |
|
ShipmentItemsCache = false |
|
end |
|
|
|
function OnMsg.DataLoaded() |
|
ShipmentItemsCache = GetAllShipmentItems() |
|
end |
|
|
|
function HasAnyShipmentItem(unit) |
|
local hasBriefcase, shipmentPresetId = false, false |
|
for i, itemPair in ipairs(ShipmentItemsCache) do |
|
hasBriefcase = not not unit:HasItem(itemPair[1]) |
|
if hasBriefcase then |
|
shipmentPresetId = itemPair[2] |
|
break |
|
end |
|
end |
|
return hasBriefcase, shipmentPresetId |
|
end |
|
|
|
|
|
if FirstLoad then |
|
DBRoutesCacheDynamic = false |
|
DBRoutesMaxDistanceForLandOnlyCheck = 10 |
|
end |
|
|
|
if config.Mods then |
|
|
|
function OnMsg.NewGame() |
|
DBRoutesCacheDynamic = false |
|
end |
|
|
|
function OnMsg.StartSatelliteGameplay() |
|
if ModsLoaded and #ModsLoaded > 0 and not DBRoutesCacheDynamic then |
|
DelayedCall(0, GenerateDynamicDBPathCache) |
|
end |
|
end |
|
|
|
function OnMsg.GameMetadataLoaded() |
|
if ModsLoaded and #ModsLoaded > 0 then |
|
GenerateDynamicDBPathCache() |
|
end |
|
end |
|
|
|
function OnMsg.CampaignStarted() |
|
if ModsLoaded and #ModsLoaded > 0 then |
|
GenerateDynamicDBPathCache() |
|
end |
|
end |
|
|
|
end |
|
|
|
function SpawnDynamicDBSquad(overrideSourceDest, srcOrDstSectorFilter) |
|
local routes = DBRoutesCacheDynamic or DBRoutesCacheStatic |
|
if not routes then return end |
|
|
|
local weights = {} |
|
if overrideSourceDest then |
|
local src = overrideSourceDest[1] |
|
local dst = overrideSourceDest[2] |
|
if src and dst then |
|
local route = GenerateRouteDijkstra(src, dst, false, empty_table, nil, nil, "diamonds") |
|
if route then |
|
route.source = src |
|
route.dest = dst |
|
weights[#weights + 1] = { 100, route } |
|
routes = empty_table |
|
else |
|
return |
|
end |
|
end |
|
end |
|
|
|
|
|
for i, route in ipairs(routes) do |
|
if srcOrDstSectorFilter and route.source ~= srcOrDstSectorFilter and route.dest ~= srcOrDstSectorFilter then |
|
goto continue |
|
end |
|
|
|
local srcSector = gv_Sectors[route.source] |
|
local dstSector = gv_Sectors[route.dest] |
|
if not srcSector.reveal_allowed or srcSector.Side ~= "enemy1" then goto continue end |
|
if not dstSector.reveal_allowed or dstSector.Side ~= "enemy1" then goto continue end |
|
if srcSector.no_ddb or dstSector.no_ddb then goto continue end |
|
|
|
|
|
|
|
local weightPerSector = MulDivRound(1, 1000, #route) |
|
local weight = 0 |
|
local playerSectorsAround = {} |
|
for i, sId in ipairs(route) do |
|
local prevSector = route[i - 1] |
|
local nextSector = route[i + 1] |
|
local sector = gv_Sectors[sId] |
|
|
|
|
|
if sector.Side ~= "player1" then |
|
weight = weight + weightPerSector |
|
ForEachSectorAround(sId, 1, function(sectorAroundId) |
|
if sId ~= sectorAroundId and sectorAroundId ~= prevSector and sectorAroundId ~= nextSector then |
|
local sectorAround = gv_Sectors[sectorAroundId] |
|
if sectorAround.Side == "player1" and not playerSectorsAround[sectorAroundId] then |
|
playerSectorsAround[sectorAroundId] = true |
|
playerSectorsAround[#playerSectorsAround + 1] = sectorAroundId |
|
end |
|
end |
|
end) |
|
else |
|
weight = weight - 100 |
|
end |
|
end |
|
|
|
local playerSectors = #playerSectorsAround |
|
if playerSectors <= 2 then |
|
playerSectors = 0 |
|
end |
|
|
|
weight = weight + weightPerSector * playerSectors * 100 |
|
weight = weight + #route * 2 |
|
weights[#weights + 1] = { weight, route, playerSectors } |
|
|
|
::continue:: |
|
end |
|
if #weights == 0 then return end |
|
|
|
|
|
table.sort(weights, function(a, b) return a[1] > b[1] end) |
|
if #weights > 4 then |
|
local halfWeights = #weights / 2 |
|
table.iclear(weights, halfWeights) |
|
end |
|
|
|
local randomRoute = GetWeightedRandom(weights, xxhash(Game.id, Game.CampaignTime, gv_NextSquadUniqueId)) |
|
if not randomRoute then return end |
|
|
|
local presetId = PickShipmentPreset() or "DiamondShipment" |
|
local preset = ShipmentPresets[presetId] |
|
if not preset then return end |
|
|
|
local sectorId = randomRoute.source |
|
local squadDef = EnemySquadDefs[preset.enemy_squad_def or "DiamondBriefcase"] |
|
local squadDefCarrier = squadDef.DiamondBriefcaseCarrier |
|
local unitIds, unitNames, unitSources, unitAppearance = GenerateRandEnemySquadUnits(squadDef.id) |
|
local units = GenerateUnitsFromTemplates(sectorId, unitIds, "Shipment", unitNames, unitAppearance) |
|
local carrier = false |
|
for i, s in ipairs(unitSources) do |
|
if s == squadDefCarrier then |
|
carrier = units[i] |
|
end |
|
end |
|
carrier = gv_UnitData[carrier] |
|
assert(carrier) |
|
|
|
local dbItem = PlaceInventoryItem(preset.item or "DiamondBriefcase") |
|
dbItem.drop_chance = 100 |
|
dbItem.extra_tag = "dynamic-db" |
|
carrier:AddItem("Inventory", dbItem) |
|
|
|
local squad_id = CreateNewSatelliteSquad( |
|
{ |
|
Side = "enemy1", |
|
CurrentSector = sectorId, |
|
Name = preset.SquadName and _InternalTranslate(preset.SquadName) or SquadName:GetNewSquadName("enemy1", units), |
|
diamond_briefcase = true, |
|
diamond_briefcase_dynamic = true, |
|
shipment_preset_id = presetId, |
|
always_visible = true, |
|
enemy_squad_def = squadDef.id, |
|
image = preset.squad_icon |
|
}, |
|
units |
|
) |
|
|
|
local squad = gv_Squads[squad_id] |
|
randomRoute = table.copy(randomRoute) |
|
randomRoute = {randomRoute} |
|
randomRoute.despawn_at_last_sector = true |
|
randomRoute.diamond_briefcase = true |
|
SetSatelliteSquadRoute(squad, randomRoute) |
|
end |
|
|
|
function GenerateDynamicDBPathCache(save, ged) |
|
local activeMods = config.Mods and ModsLoaded and #ModsLoaded > 0 |
|
|
|
if save and activeMods then |
|
if ged then |
|
ged:ShowMessage("Warning", "Stop mods before saving the DB cache as they might cause incorrect generation of routes.") |
|
end |
|
return |
|
end |
|
|
|
if activeMods and DBRoutesCacheDynamic then |
|
return |
|
end |
|
|
|
PauseInfiniteLoopDetection("DBPathfinding") |
|
local st = GetPreciseTicks() |
|
local routeCache = {} |
|
local sources = {} |
|
local destinations = {} |
|
local campaign = GetCurrentCampaignPreset() |
|
local cols = campaign.sector_columns |
|
local rows = campaign.sector_rows |
|
|
|
|
|
local cache_sorted_sectors = {} |
|
local cache_sectors_shortcuts = {} |
|
local cache_neighbors = {} |
|
|
|
for id, sector in sorted_pairs(gv_Sectors) do |
|
cache_sorted_sectors[#cache_sorted_sectors + 1] = id |
|
cache_sectors_shortcuts[#cache_sectors_shortcuts + 1] = GetShortcutsAtSector(id, "force_twoway") |
|
cache_neighbors[id] = GetNeighborSectors(id) |
|
|
|
if IsSectorUnderground(id) then goto continue end |
|
|
|
if sector.DBSourceSector and not sources[id] then |
|
sources[#sources + 1] = id |
|
sources[id] = "src" |
|
end |
|
|
|
local row, col = sector_unpack(id) |
|
local isEdgeSector = row == rows or cols == col or row == 1 or col == 1 |
|
if (sector.DBDestinationSector or isEdgeSector) and not destinations[id] then |
|
destinations[#destinations + 1] = id |
|
destinations[id] = isEdgeSector and "edge" or "dest" |
|
end |
|
|
|
::continue:: |
|
end |
|
|
|
if #sources == 0 or #destinations == 0 then |
|
DBRoutesCacheDynamic = {} |
|
return |
|
end |
|
|
|
local dedupe = {} |
|
for i, source in ipairs(sources) do |
|
for i, dest in ipairs(destinations) do |
|
if source == dest then goto continue end |
|
|
|
local dist = GetSectorDistance(source, dest) |
|
local r |
|
if dist <= DBRoutesMaxDistanceForLandOnlyCheck then |
|
r = GenerateRouteDijkstraSimplified(source, dest, "land_only", "diamonds", cache_sorted_sectors, cache_sectors_shortcuts, cache_neighbors) |
|
else |
|
r = GenerateRouteDijkstraSimplified(source, dest, "land_water_boatless", "diamonds", cache_sorted_sectors, cache_sectors_shortcuts, cache_neighbors) |
|
end |
|
if not r then goto continue end |
|
|
|
|
|
if destinations[dest] == "edge" then |
|
local edgeSectorsToRemove = 0 |
|
for i = #r, 1, -1 do |
|
local sectorId = r[i] |
|
local row, col = sector_unpack(sectorId) |
|
local isEdgeSector = row == rows or cols == col or row == 1 or col == 1 |
|
if isEdgeSector then |
|
edgeSectorsToRemove = edgeSectorsToRemove + 1 |
|
else |
|
break |
|
end |
|
end |
|
if edgeSectorsToRemove > 1 then |
|
local routeLength = #r |
|
for i = 0, edgeSectorsToRemove - 2 do |
|
r[routeLength - i] = nil |
|
end |
|
dest = r[#r] |
|
end |
|
end |
|
|
|
if dedupe[source .. " " .. dest] then goto continue end |
|
|
|
local startSector = source |
|
local firstSectorInRoute = r[1] |
|
local sX, sY = sector_unpack(startSector) |
|
local fX, fY = sector_unpack(firstSectorInRoute) |
|
assert(abs(sX - fX) == 1 or abs(sY - fY) == 1) |
|
|
|
|
|
if #r >= lMinDynamicSquadRouteLength then |
|
r.source = source |
|
r.dest = dest |
|
dedupe[source .. " " .. dest] = true |
|
routeCache[#routeCache + 1] = r |
|
end |
|
|
|
::continue:: |
|
end |
|
end |
|
|
|
if save then |
|
local data = {} |
|
data = routeCache |
|
|
|
local code = TableToLuaCode(data) |
|
code = "if FirstLoad then \nDBRoutesCacheStatic = " .. code .. "\nend" |
|
SaveSVNFile("svnProject/Lua/DiamondPaths.generated.lua", code) |
|
else |
|
DBRoutesCacheDynamic = routeCache |
|
end |
|
|
|
ResumeInfiniteLoopDetection("DBPathfinding") |
|
end |
|
|
|
local function pq_parent(i) |
|
return DivRound(i - 1, 2) |
|
end |
|
|
|
local function pq_left_child(i) |
|
return 2 * i |
|
end |
|
|
|
local function pq_right_child(i) |
|
return 2 * i + 1 |
|
end |
|
|
|
local function swap(t, i, j, key) |
|
local tempid = t[i].id |
|
local tempIdxCache = t[i].idx_in_cache |
|
local tempWeight = t[i].weight |
|
|
|
t[i].id = t[j].id |
|
t[i].idx_in_cache = t[j].idx_in_cache |
|
t[i].weight = t[j].weight |
|
|
|
t[j].id = tempid |
|
t[j].idx_in_cache = tempIdxCache |
|
t[j].weight = tempWeight |
|
|
|
if key then |
|
t[t[i][key]] = i |
|
t[t[j][key]] = j |
|
end |
|
end |
|
|
|
local function pq_shift_up(t, i, field, key) |
|
local parent = t[pq_parent(i)] |
|
local parent_idx = pq_parent(i) |
|
while i > 1 and (parent[field] > t[i][field] or (parent[field] == t[i][field] and parent.idx_in_cache > t[i].idx_in_cache and parent_idx ~= 1)) do |
|
swap(t, parent_idx, i, key) |
|
i = parent_idx |
|
parent_idx = pq_parent(i) |
|
parent = t[parent_idx] |
|
end |
|
end |
|
|
|
local function pq_shift_down(t, i, field, key) |
|
local curr_idx = i |
|
local size = t.table_size |
|
local curr_node = t[curr_idx] |
|
|
|
local left_child_idx = pq_left_child(i) |
|
local left_node = t[left_child_idx] |
|
if left_child_idx <= size and (left_node[field] < curr_node[field] or (left_node[field] == curr_node[field] and left_node.idx_in_cache < curr_node.idx_in_cache)) then |
|
curr_idx = left_child_idx |
|
curr_node = t[curr_idx] |
|
end |
|
|
|
local right_child_idx = pq_right_child(i) |
|
local right_node = t[right_child_idx] |
|
if right_child_idx <= size and (right_node[field] < curr_node[field] or (right_node[field] == curr_node[field] and right_node.idx_in_cache < curr_node.idx_in_cache)) then |
|
curr_idx = right_child_idx |
|
end |
|
|
|
if i ~= curr_idx then |
|
swap(t, i, curr_idx, key) |
|
pq_shift_down(t, curr_idx, field, key) |
|
end |
|
end |
|
|
|
function pq_insert(t, node, field, key) |
|
t.table_size = t.table_size + 1 |
|
local size = t.table_size |
|
t[size] = node |
|
t[t[size][key]] = size |
|
pq_shift_up(t, size, field, key) |
|
end |
|
|
|
function pq_pop_max(t, field, key) |
|
local max_prio_node = t[1] |
|
local size = t.table_size |
|
swap(t, 1, size, key) |
|
t[t[size][key]] = nil |
|
t[size] = nil |
|
t.table_size = t.table_size - 1 |
|
pq_shift_down(t, 1, field, key) |
|
|
|
return max_prio_node |
|
end |
|
|
|
function pq_change_prio(t, i, field, value, key) |
|
local old_value = t[i][field] |
|
t[i][field] = value |
|
|
|
if value > old_value then |
|
pq_shift_down(t, i, field, key) |
|
else |
|
pq_shift_up(t, i, field, key) |
|
end |
|
end |
|
|
|
function pq_remove(t, i, field, key) |
|
t[i] = t[1] |
|
|
|
pq_shift_up(t, i, field, key) |
|
pq_pop_max(t, field, key) |
|
end |
|
|
|
local function GetMinUnvisitedPathSizeSector(unvisited, sector_path_size) |
|
local min = max_int |
|
local min_sector |
|
local min_sector_idx |
|
for idx, sector in ipairs(unvisited) do |
|
local sector_value = sector_path_size[sector] |
|
if sector and sector_value < min then |
|
min = sector_value |
|
min_sector = sector |
|
min_sector_idx = idx |
|
end |
|
end |
|
if min == max_int then |
|
return false |
|
end |
|
return min_sector, min_sector_idx |
|
end |
|
|
|
function GenerateRouteDijkstraSimplified(start_sector, end_sector, pass_mode, side, cache_sorted_sectors, cache_sectors_shortcuts, cache_neighbors) |
|
local startIsUnderground = gv_Sectors and gv_Sectors[start_sector] and gv_Sectors[start_sector].GroundSector |
|
local endIsUnderground = gv_Sectors and gv_Sectors[end_sector] and gv_Sectors[end_sector].GroundSector |
|
|
|
assert(not startIsUnderground and not endIsUnderground, "Diamond briefcase routes should start/end on ground sectors.") |
|
|
|
if start_sector == end_sector then |
|
return false |
|
end |
|
|
|
if GetSectorDistance(start_sector, end_sector) == 1 then |
|
local dir = GetSectorDirection(start_sector, end_sector) |
|
local time = GetSectorTravelTime(start_sector, end_sector, nil, nil, pass_mode, nil, side, dir, nil, cache_neighbors[start_sector]) |
|
if time then |
|
return { end_sector } |
|
end |
|
end |
|
|
|
local underground_sector_map = {} |
|
local priority_queue = {} |
|
priority_queue.table_size = 0 |
|
local prev = {} |
|
|
|
local currIdx |
|
for idx, sector_id in ipairs(cache_sorted_sectors) do |
|
if sector_id == start_sector then |
|
pq_insert(priority_queue, {id = sector_id, weight = 0, idx_in_cache = idx}, "weight", "id") |
|
currIdx = idx |
|
else |
|
pq_insert(priority_queue, {id = sector_id, weight = max_int, idx_in_cache = idx}, "weight", "id") |
|
end |
|
|
|
local sectorPreset = gv_Sectors[sector_id] |
|
if sectorPreset.GroundSector then |
|
underground_sector_map[sectorPreset.GroundSector] = sector_id |
|
end |
|
end |
|
|
|
local curr = start_sector |
|
while true do |
|
local currPreset = gv_Sectors[curr] |
|
local curr_node = priority_queue[1] |
|
local currentIsUnderground = not not currPreset.GroundSector |
|
|
|
for _, dir in ipairs(const.WorldDirections) do |
|
local neigh = cache_neighbors[curr] and cache_neighbors[curr][dir] or GetNeighborSector(curr, dir) |
|
if neigh and currentIsUnderground then |
|
neigh = neigh .. "_Underground" |
|
end |
|
|
|
if priority_queue[neigh] then |
|
local time = GetSectorTravelTime(curr, neigh, nil, nil, pass_mode, nil, side, dir, cache_sectors_shortcuts[currIdx], cache_neighbors[curr]) |
|
if time then |
|
local time_value = time + curr_node.weight |
|
if time_value < priority_queue[priority_queue[neigh]].weight then |
|
pq_change_prio(priority_queue, priority_queue[neigh], "weight", time_value, "id") |
|
prev[neigh] = curr |
|
end |
|
end |
|
end |
|
end |
|
|
|
local last_node = pq_pop_max(priority_queue, "weight", "id") |
|
local new_node = priority_queue[1] |
|
local weight = new_node and new_node.weight |
|
if weight and weight ~= max_int then |
|
curr = new_node.id |
|
currIdx = new_node.idx_in_cache |
|
else |
|
curr = false |
|
end |
|
|
|
if not curr then |
|
return false |
|
end |
|
if curr == end_sector then |
|
local s = curr |
|
local route_rev = {} |
|
local water_sectors = 0 |
|
while s ~= start_sector do |
|
table.insert(route_rev, s) |
|
s = prev[s] |
|
end |
|
|
|
local reversedRoute = table.reverse(route_rev) |
|
return reversedRoute |
|
end |
|
end |
|
end |
|
|
|
function GetStaticDiamondBriefcaseSquadOnSector(sectorId) |
|
local _, enemySquads = GetSquadsInSector(sectorId) |
|
if enemySquads and #enemySquads > 0 then |
|
for i, sq in ipairs(enemySquads) do |
|
if sq.diamond_briefcase and not sq.diamond_briefcase_dynamic then |
|
return sq |
|
end |
|
end |
|
end |
|
return false |
|
end |
|
|
|
local function lCheckDiamondBadge() |
|
local sector = gv_Sectors[gv_CurrentSectorId] |
|
if not sector then return end |
|
local hasIntel = sector.intel_discovered |
|
|
|
for i, u in ipairs(g_Units) do |
|
|
|
local hasBriefcase, shipmentPresetId = HasAnyShipmentItem(u) |
|
hasBriefcase = hasBriefcase and (u.team.side == "enemy1" or u.team.side == "enemy2" or u:IsDead()) |
|
local showBriefcase = hasIntel or u:IsDead() |
|
|
|
if hasBriefcase and hasIntel then |
|
local ud = GameState.entering_sector and gv_UnitData[u.session_id] |
|
local statusEffectObj = ud or u |
|
statusEffectObj:AddStatusEffect("DiamondCarrier") |
|
end |
|
|
|
local needsBadge = hasBriefcase and showBriefcase |
|
local diamondBadge = TargetHasBadgeOfPreset("DiamondBadge", u) |
|
local hasBadge = not not diamondBadge |
|
|
|
if needsBadge ~= hasBadge then |
|
if needsBadge then |
|
CreateBadgeFromPreset("DiamondBadge", { target = u, spot = u:GetInteractableBadgeSpot() or "Origin" }, u) |
|
else |
|
diamondBadge:Done() |
|
end |
|
end |
|
|
|
local badge = TargetHasBadgeOfPreset("DiamondBadge", u) |
|
if badge then |
|
local shipmentPreset = ShipmentPresets[shipmentPresetId or "DiamondShipment"] |
|
badge.ui.idImageIntel:SetImage(shipmentPreset.intel_icon) |
|
badge.ui.idImage:SetImage(shipmentPreset.badge_icon) |
|
badge.ui:SetRolloverTitle(shipmentPreset.IntelTitle) |
|
badge.ui:SetRolloverText(shipmentPreset.IntelText) |
|
end |
|
end |
|
end |
|
|
|
OnMsg.CloseSatelliteView = lCheckDiamondBadge |
|
OnMsg.EnterSector = lCheckDiamondBadge |
|
OnMsg.CombatEnd = lCheckDiamondBadge |
|
function OnMsg.InventoryChange(u) |
|
if not IsKindOf(u, "Unit") and not TargetHasBadgeOfPreset("DiamondBadge", u) then return end |
|
lCheckDiamondBadge() |
|
end |
|
function OnMsg.IntelDiscovered(sectorId) |
|
if sectorId ~= gv_CurrentSectorId then return end |
|
lCheckDiamondBadge() |
|
end |