crate spawning update
This commit is contained in:
@@ -7,6 +7,10 @@
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Issue #11
|
||||
PR #17
|
||||
Supply crate spawning now checks for free space and will not spawn if the area is too crowded.
|
||||
Additionally different units will spawn in different areas depending on loading side.
|
||||
|
||||
## [0.12.1] 2026-07
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
---@class MaxLoadConfig
|
||||
---@field maxInternalLoad number
|
||||
|
||||
---@type table<string, MaxLoadConfig>
|
||||
local MaxLoadConfig = {
|
||||
["Mi-8MT"] = {
|
||||
maxInternalLoad = 4000,
|
||||
},
|
||||
["CH-47Fbl1"] = {
|
||||
maxInternalLoad = 10000
|
||||
},
|
||||
["Mi-24P"] = {
|
||||
maxInternalLoad = 2000
|
||||
},
|
||||
["UH-1H"] = {
|
||||
maxInternalLoad = 2000
|
||||
}
|
||||
}
|
||||
|
||||
return MaxLoadConfig
|
||||
@@ -0,0 +1,42 @@
|
||||
---@class DropZoneSlice
|
||||
---@field centerAngle number Angle in degrees (0=forward, 90=right, 180=rear, 270=left)
|
||||
---@field angleWidth number Total width of the slice in degrees (e.g. 60 = ±30°)
|
||||
---@field minRadius number Minimum search radius in meters (safe distance from helicopter)
|
||||
---@field maxRadius number Maximum search radius in meters
|
||||
---@field spacing number Distance increment when searching outward in meters
|
||||
|
||||
---@class SupplyLoadConfig
|
||||
---@field maxInternalLoad number
|
||||
---@field dropZones Array<DropZoneSlice>
|
||||
|
||||
---@type table<string, SupplyLoadConfig>
|
||||
local SupplyLoadConfig = {
|
||||
["Mi-8MT"] = {
|
||||
maxInternalLoad = 4000,
|
||||
dropZones = {
|
||||
{ centerAngle = 180, angleWidth = 60, minRadius = 20, maxRadius = 50, spacing = 5 },
|
||||
}
|
||||
},
|
||||
["CH-47Fbl1"] = {
|
||||
maxInternalLoad = 10000,
|
||||
dropZones = {
|
||||
{ centerAngle = 180, angleWidth = 60, minRadius = 20, maxRadius = 75, spacing = 5 },
|
||||
}
|
||||
},
|
||||
["Mi-24P"] = {
|
||||
maxInternalLoad = 2000,
|
||||
dropZones = {
|
||||
{ centerAngle = 270, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 },
|
||||
{ centerAngle = 90, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 },
|
||||
}
|
||||
},
|
||||
["UH-1H"] = {
|
||||
maxInternalLoad = 2000,
|
||||
dropZones = {
|
||||
{ centerAngle = 270, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 },
|
||||
{ centerAngle = 90, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return SupplyLoadConfig
|
||||
@@ -3,7 +3,7 @@ local Util = require("classes.util.Util")
|
||||
local DcsUtil = require("classes.util.DcsUtil")
|
||||
local SpearheadEvents = require("classes.spearhead_events")
|
||||
local SupplyConfigHelper = require("classes.stageClasses.helpers.SupplyConfigHelper")
|
||||
local MaxLoadConfig = require("classes.stageClasses.helpers.MaxLoadConfig")
|
||||
local SupplyLoadConfig = require("classes.stageClasses.helpers.SupplyLoadConfig")
|
||||
|
||||
---@class SupplyUnitEventListener
|
||||
---@field supplyUnitSpawned fun(self:SupplyUnitEventListener, unit:Unit) | nil
|
||||
@@ -284,9 +284,6 @@ function SupplyUnitsTracker:UnloadRequested(unitID, crateType, missionCommandsHe
|
||||
return
|
||||
end
|
||||
|
||||
self:RemoveCargoFromUnit(unitID, crateType)
|
||||
self:UpdateWeightForUnit(unit)
|
||||
|
||||
local cargoConfig = SupplyConfigHelper.getSupplyConfig(crateType)
|
||||
|
||||
if cargoConfig == nil then
|
||||
@@ -294,7 +291,16 @@ function SupplyUnitsTracker:UnloadRequested(unitID, crateType, missionCommandsHe
|
||||
return
|
||||
end
|
||||
|
||||
local cargoPos = self:GetCargoPlacePosition(unit)
|
||||
local cargoPos = self:GetCargoPlacePosition(unit, cargoConfig.staticType)
|
||||
|
||||
if cargoPos == nil then
|
||||
self._logger:warn("No valid position found to drop cargo for unit: " .. unit:getName())
|
||||
trigger.action.outTextForUnit(unit:getID(), "No valid position to drop cargo. Unloading area is too crowded.", 10)
|
||||
return
|
||||
end
|
||||
|
||||
self:RemoveCargoFromUnit(unitID, crateType)
|
||||
self:UpdateWeightForUnit(unit)
|
||||
|
||||
cargoCount = cargoCount + 1
|
||||
local cargoSpawnObject = {
|
||||
@@ -394,7 +400,7 @@ function SupplyUnitsTracker:TryLoadCrateInUnit(unit, crateType, commandHelper)
|
||||
end
|
||||
end
|
||||
|
||||
local unitConfig = MaxLoadConfig[unit:getTypeName()]
|
||||
local unitConfig = SupplyLoadConfig[unit:getTypeName()]
|
||||
if unitConfig == nil then
|
||||
trigger.action.outTextForUnit(unit:getID(), "Your unit type is not configured for logistics: " .. crateType, 5)
|
||||
self._logger:error("Invalid unit type: " .. unit:getTypeName())
|
||||
@@ -437,53 +443,173 @@ function SupplyUnitsTracker:UnitRequestCrateSpawn(groupID, crateType)
|
||||
end
|
||||
end
|
||||
|
||||
---@class SupplyUnitsBoundingBox
|
||||
---@field min Vec3 World space minimum
|
||||
---@field max Vec3 World space maximum
|
||||
|
||||
---@param foundObject Object
|
||||
---@return SupplyUnitsBoundingBox?
|
||||
function SupplyUnitsTracker:GetBoundingBoxes(foundObject)
|
||||
|
||||
local desc = foundObject:getDesc()
|
||||
if foundObject:getCategory() == Object.Category.SCENERY then
|
||||
desc = SceneryObject.getDescByName(foundObject:getTypeName())
|
||||
end
|
||||
|
||||
if desc == nil or desc.box == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
local objPos = foundObject:getPoint()
|
||||
local box = desc.box
|
||||
|
||||
-- Convert relative bbox to world space by adding object position
|
||||
---@type SupplyUnitsBoundingBox
|
||||
return {
|
||||
min = {
|
||||
x = objPos.x + box.min.x,
|
||||
y = objPos.y + box.min.y,
|
||||
z = objPos.z + box.min.z
|
||||
},
|
||||
max = {
|
||||
x = objPos.x + box.max.x,
|
||||
y = objPos.y + box.max.y,
|
||||
z = objPos.z + box.max.z
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
---Check if two axis-aligned bounding boxes collide with safety margin
|
||||
---@param crateBBox SupplyUnitsBoundingBox The crate's bbox in world space
|
||||
---@param objBBox SupplyUnitsBoundingBox The existing object's bbox in world space
|
||||
---@param safetyMargin number Safety margin around objects
|
||||
---@return boolean True if collision detected
|
||||
function SupplyUnitsTracker:CheckBBoxCollision(crateBBox, objBBox, safetyMargin)
|
||||
|
||||
-- Apply safety margin to object bbox
|
||||
local objMin = {
|
||||
x = objBBox.min.x - safetyMargin,
|
||||
y = objBBox.min.y - safetyMargin,
|
||||
z = objBBox.min.z - safetyMargin
|
||||
}
|
||||
local objMax = {
|
||||
x = objBBox.max.x + safetyMargin,
|
||||
y = objBBox.max.y + safetyMargin,
|
||||
z = objBBox.max.z + safetyMargin
|
||||
}
|
||||
|
||||
-- AABB collision detection
|
||||
return crateBBox.min.x <= objMax.x and crateBBox.max.x >= objMin.x and
|
||||
crateBBox.min.y <= objMax.y and crateBBox.max.y >= objMin.y and
|
||||
crateBBox.min.z <= objMax.z and crateBBox.max.z >= objMin.z
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param unit Unit
|
||||
---@return Vec3
|
||||
function SupplyUnitsTracker:GetCargoPlacePosition(unit)
|
||||
---@param crateTypeName string
|
||||
---@return Vec3?
|
||||
function SupplyUnitsTracker:GetCargoPlacePosition(unit, crateTypeName)
|
||||
|
||||
local pos = unit:getPosition()
|
||||
local preferredPos = {
|
||||
x = pos.p.x - 10 * pos.x.x,
|
||||
y = pos.p.y - 10 * pos.x.y,
|
||||
z = pos.p.z - 10 * pos.x.z
|
||||
local unitPos = unit:getPosition()
|
||||
|
||||
-- Get crate bbox - relative to placement position
|
||||
local crateDesc = StaticObject.getDescByName(crateTypeName) --[[@as table]]
|
||||
if crateDesc == nil or crateDesc.box == nil then
|
||||
self._logger:error("Could not get bbox for crate type: " .. crateTypeName)
|
||||
return nil
|
||||
end
|
||||
|
||||
local crateRelativeBBox = crateDesc.box
|
||||
|
||||
-- Get drop zone config for this unit
|
||||
local dropZones = {
|
||||
{ centerAngle = 180, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 }
|
||||
}
|
||||
|
||||
return preferredPos
|
||||
if SupplyLoadConfig[unit:getTypeName()] ~= nil then
|
||||
dropZones = SupplyLoadConfig[unit:getTypeName()].dropZones
|
||||
end
|
||||
|
||||
-- Get occupied objects with their bboxes
|
||||
local searchVolume = {
|
||||
id = world.VolumeType.SPHERE,
|
||||
params = {
|
||||
point = {
|
||||
x = unitPos.p.x,
|
||||
y = unitPos.p.y,
|
||||
z = unitPos.p.z
|
||||
},
|
||||
radius = 100 -- Search a large area
|
||||
}
|
||||
}
|
||||
|
||||
-- local volume = {
|
||||
-- id = world.VolumeType.SPHERE,
|
||||
-- params = {
|
||||
-- point = preferredPos,
|
||||
-- radius = 10
|
||||
-- }
|
||||
-- }
|
||||
local occupiedObjects = {}
|
||||
local found = function(foundItem, val)
|
||||
local bbox = self:GetBoundingBoxes(foundItem)
|
||||
if bbox then
|
||||
table.insert(occupiedObjects, {
|
||||
pos = foundItem:getPoint(),
|
||||
bbox = bbox
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- local occupiedPosX = {}
|
||||
-- local occupiedPosZ = {}
|
||||
world.searchObjects(searchVolume.id, searchVolume, found)
|
||||
|
||||
-- ---@param foundItem Object
|
||||
-- local found = function(foundItem, val)
|
||||
local safetyMargin = 3 -- Safety margin around objects
|
||||
|
||||
-- local foundPos = foundItem:getPoint()
|
||||
-- Search through each slice
|
||||
for _, zone in ipairs(dropZones) do
|
||||
-- Calculate angle range for this slice
|
||||
local minAngle = zone.centerAngle - (zone.angleWidth / 2)
|
||||
local maxAngle = zone.centerAngle + (zone.angleWidth / 2)
|
||||
|
||||
-- local z = math.floor(foundPos.z)
|
||||
-- for i = z - 3 , z + 3 do
|
||||
-- occupiedPosZ[i] = true
|
||||
-- end
|
||||
-- Search outward in rings starting from minRadius
|
||||
for distance = zone.minRadius, zone.maxRadius, zone.spacing do
|
||||
-- Check multiple positions within the angular slice
|
||||
local angleStep = math.min(15, zone.angleWidth / 3) -- Divide slice into sections
|
||||
|
||||
-- local x = math.floor(foundPos.x)
|
||||
-- for i = x - 3 , x + 3 do
|
||||
-- occupiedPosX[i] = true
|
||||
-- end
|
||||
-- end
|
||||
for angle = minAngle, maxAngle, angleStep do
|
||||
local radians = math.rad(angle)
|
||||
|
||||
-- world.searchObjects(volume.id, volume.params, found)
|
||||
-- Calculate position at this angle and distance
|
||||
-- Angle 0 = forward, 90 = right, 180 = rear, 270 = left
|
||||
local candidateX = unitPos.p.x + distance * math.sin(radians)
|
||||
local candidateY = unitPos.p.y + distance * math.cos(radians)
|
||||
local candidateZ = land.getHeight({ x = candidateX, y = candidateY })
|
||||
|
||||
-- Convert crate's relative bbox to world space at this position
|
||||
local crateBBoxWorldSpace = {
|
||||
min = {
|
||||
x = candidateX + crateRelativeBBox.min.x,
|
||||
y = candidateZ + crateRelativeBBox.min.y,
|
||||
z = candidateY + crateRelativeBBox.min.z
|
||||
},
|
||||
max = {
|
||||
x = candidateX + crateRelativeBBox.max.x,
|
||||
y = candidateZ + crateRelativeBBox.max.y,
|
||||
z = candidateY + crateRelativeBBox.max.z
|
||||
}
|
||||
}
|
||||
|
||||
-- Check if crate bbox collides with any existing objects
|
||||
local collides = false
|
||||
for _, obj in ipairs(occupiedObjects) do
|
||||
if self:CheckBBoxCollision(crateBBoxWorldSpace, obj.bbox, safetyMargin) then
|
||||
collides = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not collides then
|
||||
return { x = candidateX, y = candidateZ, z = candidateY }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- No free spot found
|
||||
return nil
|
||||
end
|
||||
|
||||
return SupplyUnitsTracker
|
||||
Reference in New Issue
Block a user