myspace / Lua /Tactical /Bombard.lua
sirnii's picture
Upload 1816 files
b6a38d7 verified
raw
history blame
16.5 kB
DefineClass.Ordnance = { __parents = { "SquadBagItem", "OrdnanceProperties", "InventoryStack", "BobbyRayShopAmmoProperties"} }
MapVar("g_Bombard", {})
PersistableGlobals.g_Bombard = false
MapVar("bombard_activate_thread", false)
PersistableGlobals.bombard_activate_thread = false
function ExplosionPrecalcDamageAndStatusEffects(self, attacker, target, attack_pos, damage, hit, effect, attack_args, record_breakdown, action, prediction)
local dmg_mod, effects
local is_unit = IsKindOf(target, "Unit")
if is_unit then
dmg_mod = hit.explosion_center and self.CenterUnitDamageMod or self.AreaUnitDamageMod
effects = hit.explosion_center and self.CenterAppliedEffects or self.AreaAppliedEffects
else
dmg_mod = hit.explosion_center and self.CenterObjDamageMod or self.AreaObjDamageMod
end
damage = MulDivRound(damage, dmg_mod, 100)
if HasPerk(attacker, "DangerClose") then
local targetRange = attacker:GetDist(attack_pos)
local dangerClose = CharacterEffectDefs.DangerClose
local rangeThreshold = dangerClose:ResolveValue("rangeThreshold") * const.SlabSizeX
if targetRange <= rangeThreshold then
local mod = dangerClose:ResolveValue("damageMod")
damage = damage + MulDivRound(damage, mod, 100)
end
end
BaseWeapon.PrecalcDamageAndStatusEffects(self, attacker, target, attack_pos, damage, hit, effect, attack_args, record_breakdown, action, prediction)
if IsKindOf(target, "Unit") then
for _, effect in ipairs(effects) do
table.insert_unique(hit.effects, effect)
end
end
end
Ordnance.PrecalcDamageAndStatusEffects = ExplosionPrecalcDamageAndStatusEffects
function Ordnance:GetAreaAttackParams(action_id, attacker, target_pos, step_pos)
target_pos = target_pos or self:GetPos()
local aoeType = self.aoeType
local max_range = self.AreaOfEffect
if aoeType == "fire" then
max_range = 2
end
local params = {
attacker = false,
weapon = self,
target_pos = target_pos,
step_pos = step_pos or target_pos,
stance = "Prone",
min_range = self.AreaOfEffect,
max_range = self.AreaOfEffect,
center_range = self.CenterAreaOfEffect,
damage_mod = 100,
attribute_bonus = 0,
aoe_type = aoeType,
can_be_damaged_by_attack = true,
explosion = true, -- damage dealt depends on target stance
}
return params
end
--no impact force for ordnance as per design
function Ordnance:GetImpactForce()
return 0
end
function Ordnance:GetDistanceImpactForce(distance)
return 0
end
local ExplorationBombardTickLen = 500
local function ExplorationBombardUpdate()
if g_Combat or IsSetpiecePlaying() then return end
local activate_zone
local deactivate_zones = {}
for idx, zone in ipairs(g_Bombard) do
if zone.attacker and zone.attacker.combat_behavior ~= "PreparedBombardIdle" and zone.attacker.combat_behavior ~= "PrepareBombard" then
deactivate_zones[#deactivate_zones + 1] = idx
elseif zone.remaining_time >= 0 then
zone.remaining_time = Max(0, zone.remaining_time - ExplorationBombardTickLen)
if zone.remaining_time == 0 then
if not activate_zone or activate_zone.attacker and not zone.attacker then
activate_zone = zone
end
elseif zone.timer_text then
zone.timer_text.ui.idText:SetText(Untranslated(zone.remaining_time / 1000))
end
end
end
for _, idx in ipairs(deactivate_zones) do
local zone = table.remove(g_Bombard, idx)
if IsValid(zone.attacker) then
zone.prepared_bombard_zone = nil
end
if IsValid(zone) then
DoneObject(zone)
end
end
if activate_zone and not IsValidThread(bombard_activate_thread) then -- only one bombardment at a time
bombard_activate_thread = CreateGameTimeThread(function()
if IsValid(activate_zone.attacker) then
activate_zone.attacker:StartBombard() -- consume ammo
end
activate_zone:Activate()
table.remove_value(g_Bombard, g_Bombard)
bombard_activate_thread = false
end)
end
end
MapGameTimeRepeat("ExplorationBombard", ExplorationBombardTickLen, ExplorationBombardUpdate)
DefineClass.BombardZone = {
__parents = { "GameDynamicSpawnObject" },
side = false, -- owner team side
radius = false,
ordnance = false, -- template
num_shots = 0,
visual = false,
bombard_offset = 0,
bombard_dir = 0, -- angle
ordnance_launch_delay = 800, -- delay (in ms) between dropping down two consecutive shells
attacker = false,
weapon_id = false,
weapon_condition = false,
remaining_time = -1, -- when used in exploration
timer_text = false, -- ui
}
function BombardZone:GameInit()
self:UpdateVisual()
end
function BombardZone:Done()
table.remove_value(g_Bombard, self)
if self.visual then
DoneObject(self.visual)
self.visual = nil
end
if self.timer_text then
self.timer_text:delete()
self.timer_text = false
end
end
function BombardZone:Setup(pos, radius, side, ordnance, num_shots, activation_time)
assert(radius and radius < 100) -- radius should be in voxels
self:SetPos(pos)
self.radius = radius
self.side = side
self.ordnance = (type(ordnance) == "string") and ordnance or ordnance.class
self.num_shots = num_shots
if activation_time then
self.remaining_time = activation_time
self.timer_text = CreateBadgeFromPreset("InteractableBadge", { target = self, spot = "Origin"})
self.timer_text.ui.idText:SetVisible(true)
end
if not self.attacker then ShowBombardTutorial() end
table.insert(g_Bombard, self)
self:UpdateVisual()
end
function BombardZone:IsValidZone()
local ordnance = g_Classes[self.ordnance]
return IsValid(self) and self:IsValidPos() and self.radius and self.side and ordnance and self.num_shots > 0
end
function BombardZone:UpdateVisual()
local ordnance = g_Classes[self.ordnance]
if not self:IsValidZone() then
if self.visual then
DoneObject(self.visual)
self.visual = nil
end
return
end
local pos = self:GetPos()
local radius = (self.radius + ordnance.AreaOfEffect) * const.SlabSizeX
if not self.visual then
local ally = self.side == "player1" or self.side == "player2" or self.side == "neutral"
self.visual = MortarAOEVisuals:new({mode = ally and "Ally" or "Enemy"}, nil, {
explosion_pos = pos,
range = radius,
})
end
self.visual:RecreateAoeTiles(self.visual.data)
end
function BombardZone:Activate()
if not self:IsValidZone() then
DoneObject(self)
return
end
local attacker = self.attacker
local pos = self:GetPos()
if attacker and attacker.command == "PreparedBombardIdle" then
-- camera over attacker
if g_Combat and attacker:GetEnumFlags(const.efVisible) ~= 0 then
SnapCameraToObj(attacker)
end
-- attacker animation
attacker:SetState("nw_Standing_MortarFire")
local duration = attacker:TimeToAnimEnd()
CreateGameTimeThread(function(attacker, duration)
Sleep(duration)
if attacker.command == "PreparedBombardIdle" then
attacker:SetState("nw_Standing_MortarIdle")
end
end, attacker, duration)
-- firing
local firing_time = duration
local weapon = attacker:GetActiveWeapons()
local visual_weapon = weapon and weapon:GetVisualObj()
if IsValid(visual_weapon) and attacker.command == "PreparedBombardIdle" then
PlayFX("MortarFiring", "start", visual_weapon)
end
for i = 1, self.num_shots do
Sleep(i * firing_time / self.num_shots - (i - 1) * firing_time / self.num_shots)
if IsValid(visual_weapon) and attacker.command == "PreparedBombardIdle" then
PlayFX("MortarFire", "start", visual_weapon)
end
end
PlayFX("MortarFiring", "end", visual_weapon)
end
--No need to reset them as it is assumed AIExecutionController:Done to run later on.
if g_Combat then
LockCameraMovement("bombard")
AdjustCombatCamera("set", nil, self)
end
Sleep(const.Combat.BombardSetupHoldTime)
if IsSetpiecePlaying() then return end
local ordnance = PlaceInventoryItem(self.ordnance)
assert(ordnance) -- IsValidZone checks the template already
local radius = self.radius * const.SlabSizeX
local fall_threads = {}
if self.visual then
Sleep(600) -- delay to match camera transition
DoneObject(self.visual)
self.visual = nil
end
if self.timer_text then
self.timer_text:delete()
self.timer_text = false
end
--[[if IsValid(self.attacker) then
local weapon = self.attacker:GetActiveWeapons()
local visual_obj = weapon:GetVisualObj(self)
PlayFX("WeaponFire", "start", visual_obj, nil, nil, axis_z)
end--]]
if self.side == "player1" or self.side == "player2" or self.side == "neutral" then
ShowTacticalNotification("allyMortarFire",true)
else
ShowTacticalNotification("enemyMortarFire",true)
end
for i = 1, self.num_shots do
-- pick a random position in the circle
local dist = InteractionRand(radius, "Bombard")
local angle = InteractionRand(360*60, "Bombard")
local fall_pos = RotateRadius(dist, angle, pos):SetTerrainZ(const.SlabSizeZ / 2)
local sky_pos = fall_pos + point(0, 0, 100*guim)
if self.bombard_offset > 0 then
sky_pos = RotateRadius(self.bombard_offset, self.bombard_dir, sky_pos)
end
-- find the explosion pos (collision from the sky downwards)
local col, pts = CollideSegmentsNearest(sky_pos, fall_pos)
if col then
fall_pos = pts[1]
end
-- animate the fall
fall_threads[i] = CreateGameTimeThread(function()
local visual = PlaceObject("OrdnanceVisual")
visual:ChangeEntity(ordnance.Entity or "MilitaryCamp_Grenade_01")
visual.fx_actor_class = self.ordnance
visual:SetPos(sky_pos)
local fall_time = MulDivRound(sky_pos:Dist(fall_pos), 1000, const.Combat.MortarFallVelocity)
visual:SetPos(fall_pos, fall_time)
Sleep(fall_time)
if not IsSetpiecePlaying() then
-- trigger explosion based on <ordnance>
ExplosionDamage(self.attacker, ordnance, fall_pos, visual)
end
DoneObject(visual)
Msg(CurrentThread())
end)
Sleep(self.ordnance_launch_delay)
end
for _, thread in ipairs(fall_threads) do
if IsValidThread(thread) then
WaitMsg(thread, 1000)
end
end
if self.side == "player1" or self.side == "player2" or self.side == "neutral" then
HideTacticalNotification("allyMortarFire")
else
HideTacticalNotification("enemyMortarFire")
end
DoneObject(ordnance)
DoneObject(self)
if IsValid(self.attacker) then
self.attacker:InterruptPreparedAttack()
end
end
function BombardZone:GetDynamicData(data)
data.side = self.side
data.radius = self.radius
data.ordnance = self.ordnance
data.num_shots = self.num_shots
if self.ordnance_launch_delay ~= BombardZone.ordnance_launch_delay then
data.ordnance_launch_delay = self.ordnance_launch_delay
end
data.attacker = IsValid(self.attacker) and self.attacker:GetHandle() or nil
data.remaining_time = self.remaining_time
end
function BombardZone:SetDynamicData(data)
self:Setup(self:GetPos(), data.radius, data.side, data.ordnance, data.num_shots)
self.ordnance_launch_delay = data.ordnance_launch_delay
if data.attacker then
self.attacker = HandleToObject[data.attacker]
end
self.remaining_time = data.remaining_time
end
function ActivateBombardZones(side)
while true do
local activate_zone
for i, zone in ipairs(g_Bombard) do
if zone.side == side then
if not activate_zone or activate_zone.attacker and not zone.attacker then
activate_zone = zone
end
end
end
if not activate_zone then
break
end
activate_zone:Activate()
end
end
function OnMsg.EnterSector()
-- check all (enemy) squads on the sector for .Bombard
local _, enemy_squads = GetSquadsInSector(gv_CurrentSectorId)
local bombard
for _, squad in ipairs(enemy_squads) do
local def = EnemySquadDefs[squad.enemy_squad_def or false]
bombard = bombard or (def and def.Bombard)
end
ChangeGameState("Bombard", bombard or false)
end
function OnMsg.CombatEnd()
for i = #g_Bombard, 1, -1 do
local zone = g_Bombard[i]
if IsValid(zone.attacker) and not zone.attacker:IsDead() then
zone.attacker:InterruptPreparedAttack()
zone.attacker:RemovePreparedAttackVisuals()
else
DoneObject(zone)
end
end
end
function OnMsg.CombatStart()
for i = #g_Bombard, 1, -1 do
local zone = g_Bombard[i]
if not zone:IsValidZone() then
DoneObject(zone)
end
end
end
DefineClass("OrdnanceVisual", "SpawnFXObject", "ComponentCustomData")
DefineClass.BombardMarker = {
__parents = { "GridMarker" },
properties = {
{ category = "Bombard", id = "Side", editor = "dropdownlist", items = function() return Sides end, default = "enemy1", },
{ category = "Bombard", id = "Ordnance", editor = "preset_id", default = false, preset_class = "InventoryItemCompositeDef", preset_filter = function (preset, obj) return preset.object_class == "Ordnance" end, },
{ category = "Bombard", id = "AreaRadius", name = "Area Radius", editor = "number", min = 1, max = 99, default = 3, },
{ category = "Bombard", id = "NumShots", name = "Num Shells", editor = "number", min = 1, default = 1, },
{ category = "Bombard", id = "LaunchOffset", name = "Launch Offset", help = "defines the direction of the fall together with Launch Angle; if left as 0 the shells will fall directly down",
editor = "number", default = 0, scale = "m", },
{ category = "Bombard", id = "LaunchAngle", name = "Launch Angle", help = "defines the direction of the fall together with Launch Offset",
editor = "number", default = 0, scale = "deg", },
{ category = "Marker", id = "AreaWidth", no_edit = true, },
{ category = "Marker", id = "AreaHeight", no_edit = true, },
{ category = "Marker", id = "Reachable", no_edit = true, default = false, },
{ category = "Marker", id = "GroundVisuals", no_edit = true, },
{ category = "Marker", id = "DeployRolloverText", no_edit = true, },
{ category = "Marker", id = "Color", no_edit = true, default = RGB(255, 255, 255), },
},
recalc_area_on_pass_rebuild = true,
}
function BombardMarker:ExecuteTriggerEffects()
if not g_Combat then
StoreErrorSource(self, "BombardMarker activated outside of combat, ignoring...")
return
end
local team_idx = g_Teams and table.find(g_Teams, "side", self.Side)
local team = team_idx and g_Teams[team_idx]
if not team then
StoreErrorSource(self, "BombardMarker failed to find team of side " .. self.Side)
return
end
local zone = PlaceObject("BombardZone")
zone:Setup(self:GetPos(), self.AreaRadius, self.Side, self.Ordnance, self.NumShots)
zone.bombard_offset = self.LaunchOffset
zone.bombard_dir = self.LaunchAngle
end
DefineClass.IsBombardQueued = {
__parents = { "Condition" },
properties = {
{ id = "BombardId", editor = "text", default = "", },
{ id = "Negate", editor = "bool" },
},
EditorNestedObjCategory = "Combat",
}
function IsBombardQueued:__eval()
if not g_Combat or not g_Combat.queued_bombards then
return false
end
return g_Combat.queued_bombards[self.BombardId]
end
function IsBombardQueued:GetEditorView()
if self.Negate then
return Untranslated("If bombardment " .. self.BombardId .. " is not queued")
end
return Untranslated("If bombardment " .. self.BombardId .. " is queued")
end
DefineClass.BombardEffect = {
__parents = { "Effect" },
properties = {
{ id = "BombardId", editor = "text", default = "", },
{ id = "Side", editor = "dropdownlist", items = function() return Sides end, default = "enemy1", },
{ id = "Ordnance", editor = "preset_id", default = false, preset_class = "InventoryItemCompositeDef", preset_filter = function (preset, obj) return preset.object_class == "Ordnance" end, },
{ id = "AreaRadius", name = "Area Radius", editor = "number", min = 1, max = 99, default = 3, },
{ id = "NumShots", name = "Num Shells", editor = "number", min = 1, default = 1, },
{ id = "LaunchOffset", name = "Launch Offset", help = "defines the direction of the fall together with Launch Angle; if left as 0 the shells will fall directly down",
editor = "number", default = 5*guim, scale = "m", },
{ id = "LaunchAngle", name = "Launch Angle", help = "defines the direction of the fall together with Launch Offset",
editor = "number", default = 20*60, scale = "deg", },
},
}
function BombardEffect:__exec()
local team = table.find(g_Teams or empty_table, "side", self.Side)
if not g_Combat or not team then
return
end
g_Combat:QueueBombard(self.BombardId, team, self.AreaRadius, self.Ordnance, self.NumShots, self.LaunchOffset, self.LaunchAngle)
end
function BombardEffect:GetEditorView()
return Untranslated("<Side> Bombard (<BombardId>)")
end
function BombardEffect:GetError()
if (self.BombardId or "") == "" then
return "Please specify BombardId"
end
if (self.Ordnance or "") == "" then
return "Please specify bombard Ordnance"
end
end