myspace / CommonLua /Classes /ActionFX.lua
sirnii's picture
Upload 1816 files
b6a38d7 verified
raw
history blame
150 kB
DefineClass.FXObject = {
fx_action = false,
fx_action_base = false,
fx_actor_class = false,
fx_actor_base_class = false,
play_size_fx = true,
}
function FXObject:GetFXObjectActor()
return self
end
if FirstLoad then
s_EntitySizeCache = {}
s_EntityFXTargetCache = {}
s_EntityFXTargetSecondaryCache = {}
end
local function no_obj_no_edit(self)
return self.Source ~= "Actor" and self.Source ~= "Target"
end
function OnMsg.EntitiesLoaded()
local ae = GetAllEntities()
for entity in pairs(ae) do
local bbox = GetEntityBBox(entity)
local x, y, z = bbox:sizexyz()
local volume = x * y * z
if volume <= const.EntityVolumeSmall then
s_EntitySizeCache[entity] = "Small"
elseif volume <= const.EntityVolumeMedium then
s_EntitySizeCache[entity] = "Medium"
else
s_EntitySizeCache[entity] = "Large"
end
end
end
function FXObject:PlayDestructionFX()
local fx_target, fx_target_secondary = GetObjMaterialFXTarget(self)
local fx_type, fx_pos, _, fx_type_secondary = GetObjMaterial(false, self, fx_target, fx_target_secondary)
PlayFX("Death", "start", self, fx_type, fx_pos)
if fx_type_secondary then
PlayFX("Death", "start", self, fx_type_secondary)
end
if self.play_size_fx then
local entity = self:GetEntity()
local fx_target_size = s_EntityFXTargetCache[entity]
if not fx_target_size then
fx_target_size = string.format("%s:%s", fx_target or "", s_EntitySizeCache[entity] or "")
s_EntityFXTargetCache[entity] = fx_target_size
end
local bbox_center = self:GetPos() + self:GetEntityBBox():Center()
PlayFX("Death", "start", self, fx_target_size, bbox_center)
if fx_target_secondary then
local fx_target_secondary_size = s_EntityFXTargetSecondaryCache[entity]
if not fx_target_secondary_size then
fx_target_secondary_size = string.format("%s:%s", fx_target_secondary or "", s_EntitySizeCache[entity] or "")
s_EntityFXTargetSecondaryCache[entity] = fx_target_secondary_size
end
PlayFX("Death", "start", self, fx_target_secondary_size, bbox_center)
end
end
end
if FirstLoad then
FXEnabled = true
DisableSoundFX = false
DebugFX = false
DebugFXAction = false
DebugFXMoment = false
DebugFXActor = false
DebugFXTarget = false
DebugFXSound = false
DebugFXParticles = false
DebugFXParticlesName = false
end
local function DebugMatch(str, to_match)
return type(to_match) ~= "string" or type(str) == "string" and string.match(string.lower(str), string.lower(to_match))
end
local function DebugFXPrint(actionFXClass, actionFXMoment, actorFXClass, targetFXClass)
local actor_text = actorFXClass or ""
if type(actor_text) ~= "string" then
actor_text = FXInheritRules_Actors[actor_text] and table.concat(FXInheritRules_Actors[actor_text], "/") or ""
end
if DebugMatch(actor_text, DebugFX) or DebugFX == "UI" then
local target_text = targetFXClass or ""
if type(target_text) ~= "string" then
target_text = FXInheritRules_Actors[target_text] and table.concat(FXInheritRules_Actors[target_text], "/") or ""
end
local str = "PlayFX %s<tab 450>%s<tab 600>%s<tab 900>%s"
printf(str, actionFXClass, actionFXMoment or "", actor_text, target_text)
end
end
local function DebugMatchUIActor(actor)
if DebugFX ~= "UI" then return true end
return IsKindOf(actor, "XWindow")
end
--[[@@@
Triggers a global event that activates various game effects. These effects are specified by FX presets. All FX presets that match the combo **action - moment - actor - target** will be activated.
Normally the FX-s are one-time events, but they can also be continuous effects. To stop continuous FX, another PlayFX call is made, with different *moment*. The ending moment is specified in the FX preset, with "end" as default.
@function void PlayFX(string action, string moment, object actor, object target, point pos, point dir)
@param string action - The name of the FX action.
@param string moment - The action's moment. Normally an FX has a *start* and an *end*, but may have various moments in-between.
@param object actor - Used to give context to the FX. Can be a string or an object. If object is provided, then it's member *fx_actor_class* is used, or its class if no such member is available. The object can be used for many purposes by the FX (e.g. attaching effects to it)
@param object target - Similar to the **actor** argument. Used to give additional context to the FX.
@param point pos - Optional FX position. Normally the position of the FX is determined by rules in the FX preset, based on the actor or the target.
@param point dir - Optional FX direction. Normally the direction of the FX is determined by rules in the FX preset, based on the actor or the target.
--]]
function PlayFX(actionFXClass, actionFXMoment, actor, target, action_pos, action_dir)
if not FXEnabled then return end
actionFXMoment = actionFXMoment or false
local actor_obj = actor and IsKindOf(actor, "FXObject") and actor
local target_obj = target and IsKindOf(target, "FXObject") and target
local actorFXClass = actor_obj and (actor_obj.fx_actor_class or actor_obj.class) or actor or false
local targetFXClass = target_obj and (target_obj.fx_actor_class or target_obj.class) or target or false
dbg(DebugFX
and DebugMatch(actionFXClass, DebugFXAction)
and DebugMatch(actionFXMoment, DebugFXMoment)
and DebugMatch(actorFXClass, DebugFXActor)
and DebugMatch(targetFXClass, DebugFXTarget)
and DebugMatchUIActor(actor_obj)
and DebugFXPrint(actionFXClass, actionFXMoment, actorFXClass, targetFXClass))
local fxlist
local t
local t1 = FXCache
if t1 then
t = t1[actionFXClass]
if t then
t1 = t[actionFXMoment]
if t1 then
t = t1[actorFXClass]
if t then
fxlist = t[targetFXClass]
else
t = {}
t1[actorFXClass] = t
end
else
t1, t = t, {}
t1[actionFXMoment] = { [actorFXClass] = t }
end
else
t = {}
t1[actionFXClass] = { [actionFXMoment] = { [actorFXClass] = t } }
end
else
t = {}
FXCache = { [actionFXClass] = { [actionFXMoment] = { [actorFXClass] = t } } }
end
if fxlist == nil then
fxlist = GetPlayFXList(actionFXClass, actionFXMoment, actorFXClass, targetFXClass)
t[targetFXClass] = fxlist or false
end
local playedAnything = false
if fxlist then
actor_obj = actor_obj and actor_obj:GetFXObjectActor() or actor_obj
target_obj = target_obj and target_obj:GetFXObjectActor() or target_obj
for i = 1, #fxlist do
local fx = fxlist[i]
local chance = fx.Chance
if chance >= 100 or AsyncRand(100) < chance then
dbg(fx.DbgPrint and DebugFXPrint(actionFXClass, actionFXMoment, actorFXClass, targetFXClass))
dbg(fx.DbgBreak and bp())
fx:PlayFX(actor_obj, target_obj, action_pos, action_dir)
playedAnything = true
end
end
end
return playedAnything
end
if FirstLoad or ReloadForDlc then
FXLists = {}
FXRules = {}
FXInheritRules_Actions = false
FXInheritRules_Moments = false
FXInheritRules_Actors = false
FXInheritRules_Maps = false
FXInheritRules_DynamicActors = setmetatable({}, weak_keys_meta)
FXCache = false
end
function AddInRules(fx)
local action = fx.Action
local moment = fx.Moment
local actor = fx.Actor
local target = fx.Target
if target == "ignore" then target = "any" end
local rules = FXRules
rules[action] = rules[action] or {}
rules = rules[action]
rules[moment] = rules[moment] or {}
rules = rules[moment]
rules[actor] = rules[actor] or {}
rules = rules[actor]
rules[target] = rules[target] or {}
rules = rules[target]
table.insert(rules, fx)
FXCache = false
end
function RemoveFromRules(fx)
local rules = FXRules
rules = rules[fx.Action]
rules = rules and rules[fx.Moment]
rules = rules and rules[fx.Actor]
rules = rules and rules[fx.Target == "ignore" and "any" or fx.Target]
if rules then
table.remove_value(rules, fx)
end
FXCache = false
end
function RebuildFXRules()
FXRules = {}
FXCache = false
RebuildFXInheritActionRules()
RebuildFXInheritMomentRules()
RebuildFXInheritActorRules()
for classname, fxlist in sorted_pairs(FXLists) do
if g_Classes[classname]:IsKindOf("ActionFX") then
for i = 1, #fxlist do
fxlist[i]:RemoveFromRules()
fxlist[i]:AddInRules()
end
end
end
end
local function AddFXInheritRule(key, inherit, rules, added)
if not key or key == "" or key == "any" or key == inherit then
return
end
local list = rules[key]
if not list then
rules[key] = { inherit }
added[key] = { [inherit] = true }
else
local t = added[key]
if not t[inherit] then
list[#list+1] = inherit
t[inherit] = true
end
end
end
local function LinkFXInheritRules(rules, added)
for key, list in pairs(rules) do
local added = added[key]
local i, count = 1, #list
while i <= count do
local inherit_list = rules[list[i]]
if inherit_list then
for i = 1, #inherit_list do
local inherit = inherit_list[i]
if not added[inherit] then
count = count + 1
list[count] = inherit
added[inherit] = true
end
end
end
i = i + 1
end
end
end
function RebuildFXInheritActionRules()
PauseInfiniteLoopDetection("RebuildFXInheritActionRules")
local rules, added = {}, {}
FXInheritRules_Actions = rules
ClassDescendants("FXObject", function(classname, class)
local key = class.fx_action_base
if key then
local name = class.fx_action or classname
if name ~= key then
AddFXInheritRule(name, key, rules, added)
end
local parents = key ~= "" and key ~= "any" and class.__parents
if parents then
for i = 1, #parents do
local parent_class = g_Classes[parents[i]]
local inherit = IsKindOf(parent_class, "FXObject") and parent_class.fx_action_base
if inherit and key ~= inherit then
AddFXInheritRule(key, inherit, rules, added)
end
end
end
end
end)
local anim_metadatas = Presets.AnimMetadata
for _, group in ipairs(anim_metadatas) do
for _, anim_metadata in ipairs(group) do
local key = anim_metadata.id
local fx_inherits = anim_metadata.FXInherits
for _, fx_inherit in ipairs(fx_inherits) do
AddFXInheritRule(key, fx_inherit, rules, added)
end
end
end
local fxlist = FXLists.ActionFXInherit_Action
if fxlist then
for i = 1, #fxlist do
local fx = fxlist[i]
AddFXInheritRule(fx.Action, fx.Inherit, rules, added)
end
end
LinkFXInheritRules(rules, added)
ResumeInfiniteLoopDetection("RebuildFXInheritActionRules")
return rules
end
function RebuildFXInheritMomentRules()
local rules, added = {}, {}
FXInheritRules_Moments = rules
local fxlist = FXLists.ActionFXInherit_Moment
if fxlist then
for i = 1, #fxlist do
local fx = fxlist[i]
AddFXInheritRule(fx.Moment, fx.Inherit, rules, added)
end
end
LinkFXInheritRules(rules, added)
return rules
end
function RebuildFXInheritActorRules()
PauseInfiniteLoopDetection("RebuildFXInheritActorRules")
local rules, added = setmetatable({}, weak_keys_meta), {}
FXInheritRules_Actors = rules
-- class inherited
ClassDescendants("FXObject", function(classname, class)
local key = class.fx_actor_base_class
if key then
local name = class.fx_actor_class or classname
if name and name ~= key then
AddFXInheritRule(name, key, rules, added)
end
local parents = key and key ~= "" and key ~= "any" and class.__parents
if parents then
for i = 1, #parents do
local parent_class = g_Classes[parents[i]]
local inherit = IsKindOf(parent_class, "FXObject") and parent_class.fx_actor_base_class
if inherit and key ~= inherit then
AddFXInheritRule(key, inherit, rules, added)
end
end
end
end
end)
local custom_inherit = {}
Msg("GetCustomFXInheritActorRules", custom_inherit)
for i = 1, #custom_inherit, 2 do
local key = custom_inherit[i]
local inherit = custom_inherit[i+1]
if key and inherit and key ~= inherit then
AddFXInheritRule(key, inherit, rules, added)
end
end
local fxlist = FXLists.ActionFXInherit_Actor
if fxlist then
for i = 1, #fxlist do
local fx = fxlist[i]
AddFXInheritRule(fx.Actor, fx.Inherit, rules, added)
end
end
LinkFXInheritRules(rules, added)
for obj, list in pairs(FXInheritRules_DynamicActors) do
FXInheritRules_Actors[obj] = list
end
ResumeInfiniteLoopDetection("RebuildFXInheritActorRules")
return rules
end
function AddFXDynamicActor(obj, actor_class)
if not actor_class or actor_class == "" then return end
local list = FXInheritRules_DynamicActors[obj]
if not list then
local def_actor_class = obj.fx_actor_class or obj.class
local def_inherit = (FXInheritRules_Actors or RebuildFXInheritActorRules() )[def_actor_class]
list = { def_actor_class }
table.iappend(list, def_inherit)
if not table.find(list, actor_class) then
table.insert(list, actor_class)
local actor_class_inherit = FXInheritRules_Actors[actor_class]
if actor_class_inherit then
for i = 1, #actor_class_inherit do
local actor = actor_class_inherit[i]
if not table.find(list, actor) then
table.insert(list, actor)
end
end
end
end
FXInheritRules_DynamicActors[obj] = list
if FXInheritRules_Actors then
FXInheritRules_Actors[obj] = list
end
obj.fx_actor_class = obj
elseif not table.find(list, actor_class) then
table.insert(list, actor_class)
end
end
function ClearFXDynamicActor(obj)
FXInheritRules_DynamicActors[obj] = nil
if FXInheritRules_Actors then
FXInheritRules_Actors[obj] = nil
end
obj.fx_actor_class = nil
end
function OnMsg.PostDoneMap()
FXInheritRules_DynamicActors = setmetatable({}, weak_keys_meta)
FXCache = false
end
function OnMsg.DataLoaded()
RebuildFXRules()
end
if not FirstLoad and not ReloadForDlc then
function OnMsg.ClassesBuilt()
RebuildFXInheritActionRules()
RebuildFXInheritActorRules()
end
end
local HookActionFXCombo
local HookMomentFXCombo
local ActionFXBehaviorCombo
local ActionFXSpotCombo
local ActionFXAnimatedComboDecal = { "Normal", "PingPong" }
--============================= FX Orient =======================
local OrientationAxisCombo = {
{ text = "X", value = 1 },
{ text = "Y", value = 2 },
{ text = "Z", value = 3 },
{ text = "-X", value = -1 },
{ text = "-Y", value = -2 },
{ text = "-Z", value = -3 },
}
local OrientationAxes = { axis_x, axis_y, axis_z, [-1] = -axis_x, [-2] = -axis_y, [-3] = -axis_z }
local FXOrientationFunctions = {}
function FXOrientationFunctions.SourceAxisX(orientation_axis, source_obj)
if IsValid(source_obj) then
return OrientAxisToObjAxisXYZ(orientation_axis, source_obj, 1)
end
end
function FXOrientationFunctions.SourceAxisX2D(orientation_axis, source_obj)
if IsValid(source_obj) then
return OrientAxisToObjAxis2DXYZ(orientation_axis, source_obj, 1)
end
end
function FXOrientationFunctions.SourceAxisY(orientation_axis, source_obj)
if IsValid(source_obj) then
return OrientAxisToObjAxisXYZ(orientation_axis, source_obj, 2)
end
end
function FXOrientationFunctions.SourceAxisZ(orientation_axis, source_obj)
if IsValid(source_obj) then
return OrientAxisToObjAxisXYZ(orientation_axis, source_obj, 3)
end
end
function FXOrientationFunctions.ActionDir(orientation_axis, source_obj, posx, posy, posz, preset_angle, actor, target, action_pos, action_dir)
if action_dir and action_dir ~= point30 then
return OrientAxisToVectorXYZ(orientation_axis, action_dir)
elseif IsValid(actor) then
return OrientAxisToObjAxisXYZ(orientation_axis, actor:GetParent() or actor, 1)
end
end
function FXOrientationFunctions.ActionDir2D(orientation_axis, source_obj, posx, posy, posz, preset_angle, actor, target, action_pos, action_dir)
if action_dir and not action_dir:Equal2D(point20) then
local x, y = action_dir:xy()
return OrientAxisToVectorXYZ(orientation_axis, x, y, 0)
elseif IsValid(actor) then
return OrientAxisToObjAxis2DXYZ(orientation_axis, actor:GetParent() or actor, 1)
end
end
function FXOrientationFunctions.FaceTarget(orientation_axis, source_obj, posx, posy, posz, preset_angle, actor, target, action_pos, action_dir)
if posx and IsValid(target) and target:IsValidPos() then
local tx, ty, tz = target:GetSpotLocPosXYZ(-1)
if posx ~= tx or posy ~= ty or posz ~= tz then
return OrientAxisToVectorXYZ(orientation_axis, tx - posx, ty - posy, tz - posz)
end
end
if action_dir and action_dir ~= point30 then
return OrientAxisToVectorXYZ(orientation_axis, action_dir)
elseif IsValid(actor) then
return OrientAxisToObjAxisXYZ(orientation_axis, actor:GetParent() or actor, 1)
end
end
function FXOrientationFunctions.FaceTarget2D(orientation_axis, source_obj, posx, posy, posz, preset_angle, actor, target, action_pos, action_dir)
if posx and IsValid(target) and target:IsValidPos() then
local tx, ty = target:GetSpotLocPosXYZ(-1)
if posx ~= tx or posy ~= ty then
return OrientAxisToVectorXYZ(orientation_axis, tx - posx, ty - posy, 0)
end
end
if action_dir and not action_dir:Equal2D(point20) then
local x, y = action_dir:xy()
return OrientAxisToVectorXYZ(orientation_axis, x, y, 0)
elseif IsValid(actor) then
return OrientAxisToObjAxis2DXYZ(orientation_axis, actor:GetParent() or actor, 1)
end
end
function FXOrientationFunctions.FaceActor(orientation_axis, source_obj, posx, posy, posz, preset_angle, actor, target, action_pos, action_dir)
if posx and IsValid(actor) and actor:IsValidPos() then
local tx, ty, tz = actor:GetSpotLocPosXYZ(-1)
if posx ~= tx or posy ~= ty or posz ~= tz then
return OrientAxisToVectorXYZ(orientation_axis, tx - posx, ty - posy, tz - posz)
end
end
if action_dir and action_dir ~= point30 then
return OrientAxisToVectorXYZ(orientation_axis, action_dir)
elseif IsValid(actor) then
return OrientAxisToObjAxisXYZ(orientation_axis, actor:GetParent() or actor, 1)
end
end
function FXOrientationFunctions.FaceActor2D(orientation_axis, source_obj, posx, posy, posz, preset_angle, actor, target, action_pos, action_dir)
if posx and IsValid(actor) and actor:IsValidPos() then
local tx, ty = actor:GetSpotLocPosXYZ(-1)
if posx ~= tx or posy ~= ty then
return OrientAxisToVectorXYZ(orientation_axis, tx - posx, ty - posy, 0)
end
end
if action_dir and not action_dir:Equal2D(point20) then
local x, y = action_dir:xy()
return OrientAxisToVectorXYZ(orientation_axis, posx, posy, 0)
elseif IsValid(actor) then
return OrientAxisToObjAxis2DXYZ(orientation_axis, actor:GetParent() or actor, 1)
end
end
function FXOrientationFunctions.FaceActionPos(orientation_axis, source_obj, posx, posy, posz, preset_angle, actor, target, action_pos, action_dir)
if posx and action_pos and action_pos:IsValid() then
local tx, ty, tz = action_pos:xyz()
if tx ~= posx or ty ~= posy or (tz or posz) ~= posz then
return OrientAxisToVectorXYZ(orientation_axis, tx - posx, ty - posy, (tz or posz) - posz)
end
end
if action_dir and action_dir ~= point30 then
return OrientAxisToVectorXYZ(orientation_axis, action_dir)
elseif IsValid(actor) then
return OrientAxisToObjAxisXYZ(orientation_axis, actor:GetParent() or actor, 1)
end
end
function FXOrientationFunctions.FaceActionPos2D(orientation_axis, source_obj, posx, posy, posz, preset_angle, actor, target, action_pos, action_dir)
if posx and action_pos and action_pos:IsValid() then
local tx, ty = action_pos:xy()
if tx ~= posx or ty ~= posy then
return OrientAxisToVectorXYZ(orientation_axis, tx - posx, ty - posy, 0)
end
end
if action_dir and not action_dir:Equal2D(point20) then
local tx, ty = action_dir:xy()
return OrientAxisToVectorXYZ(orientation_axis, tx, ty, 0)
elseif IsValid(actor) then
return OrientAxisToObjAxis2DXYZ(orientation_axis, actor:GetParent() or actor, 1)
end
end
function FXOrientationFunctions.Random2D(orientation_axis)
return OrientAxisToVectorXYZ(orientation_axis, Rotate(axis_x, AsyncRand(360*60)))
end
function FXOrientationFunctions.SpotX(orientation_axis)
if orientation_axis == 1 then
return 0, 0, 4096, 0
end
return OrientAxisToVectorXYZ(orientation_axis, axis_x)
end
function FXOrientationFunctions.SpotY(orientation_axis)
if orientation_axis == 2 then
return 0, 0, 4096, 0
end
return OrientAxisToVectorXYZ(orientation_axis, axis_y)
end
function FXOrientationFunctions.SpotZ(orientation_axis)
if orientation_axis == 3 then
return 0, 0, 4096, 0
end
return OrientAxisToVectorXYZ(orientation_axis, axis_z)
end
function FXOrientationFunctions.RotateByPresetAngle(orientation_axis, source_obj, posx, posy, posz, preset_angle, actor, target, action_pos, action_dir)
local axis = OrientationAxes[orientation_axis]
local axis_x, axis_y, axis_z = axis:xyz()
return axis_x, axis_y, axis_z, preset_angle * 60
end
local function OrientByTerrainAndAngle(fixedAngle, source_obj, posx, posy, posz)
if source_obj and not source_obj:IsValidZ() or posz - terrain.GetHeight(posx, posy) < 250 then
local norm = terrain.GetTerrainNormal(posx, posy)
if not norm:Equal2D(point20) then
local axis, angle = AxisAngleFromOrientation(norm, fixedAngle)
local axisx, axisy, axisz = axis:xyz()
return axisx, axisy, axisz, angle
end
end
return 0, 0, 4096, fixedAngle
end
function FXOrientationFunctions.OrientByTerrainWithRandomAngle(orientation_axis, source_obj, posx, posy, posz)
local randomAngle = AsyncRand(-90 * 180, 90 * 180)
return OrientByTerrainAndAngle(randomAngle, source_obj, posx, posy, posz)
end
function FXOrientationFunctions.OrientByTerrainToActionPos(orientation_axis, source_obj, posx, posy, posz, preset_angle, actor, target, action_pos, action_dir)
local tX, tY, tZ, tA = OrientByTerrainAndAngle(0, source_obj, posx, posy, posz)
local fX, fY, fZ, fA = FXOrientationFunctions.FaceActionPos2D(orientation_axis, source_obj, posx, posy, posz, preset_angle, actor, target, action_pos, action_dir)
if not fX then
return tX, tY, tZ, tA
end
local axis, angle = ComposeRotation(point(fX, fY, fZ), fA, point(tX, tY, tZ), tA)
return axis:x(), axis:y(), axis:z(), angle
end
function FXOrientationFunctions.OrientByTerrainToActionDir(orientation_axis, source_obj, posx, posy, posz, preset_angle, actor, target, action_pos, action_dir)
local tX, tY, tZ, tA = OrientByTerrainAndAngle(0, source_obj, posx, posy, posz)
local fX, fY, fZ, fA = FXOrientationFunctions.ActionDir(orientation_axis, source_obj, posx, posy, posz, preset_angle, actor, target, action_pos, action_dir)
if not fX then
return tX, tY, tZ, tA
end
local axis, angle = ComposeRotation(point(fX, fY, fZ), fA, point(tX, tY, tZ), tA)
return axis:x(), axis:y(), axis:z(), angle + (preset_angle * 60)
end
local ActionFXOrientationCombo = table.keys2(FXOrientationFunctions, true, "")
local ActionFXOrientationComboDecal = table.copy(ActionFXOrientationCombo, false)
local function FXCalcOrientation(orientation, ...)
local fn = orientation and FXOrientationFunctions[orientation]
if fn then
return fn(...)
end
end
local function FXOrient(fx_obj, posx, posy, posz, parent, spot, attach, axisx, axisy, axisz, angle, attach_offset)
if attach and parent and IsValid(parent) and not IsBeingDestructed(parent) then
if spot then
parent:Attach(fx_obj, spot)
else
parent:Attach(fx_obj)
end
if attach_offset then
fx_obj:SetAttachOffset(attach_offset)
end
if angle and angle ~= 0 then
fx_obj:SetAttachAxis(axisx, axisy, axisz)
fx_obj:SetAttachAngle(angle)
end
else
fx_obj:Detach()
if (not posx or not angle) and parent and IsValid(parent) and parent:IsValidPos() then
if not posx and not angle then
posx, posy, posz, angle, axisx, axisy, axisz = parent:GetSpotLocXYZ(spot or -1)
elseif not posx then
posx, posy, posz = parent:GetSpotLocPosXYZ(spot or -1)
else
local _x, _y, _z
_x, _y, _z, angle, axisx, axisy, axisz = parent:GetSpotLocXYZ(spot or -1)
end
end
if angle then
fx_obj:SetAxis(axisx, axisy, axisz)
fx_obj:SetAngle(angle)
end
if posx then
if posz and fx_obj:GetGameFlags(const.gofAttachedOnGround) == 0 then
fx_obj:SetPos(posx, posy, posz)
else
fx_obj:SetPos(posx, posy, const.InvalidZ)
end
end
end
end
local ActionFXDetailLevel = {
-- please keep this sorted from most important to least important particles
-- and synced with OptionsData.Options.Effects.hr.FXDetailThreshold
{ text = "<unspecified>", value = 101 },
{ text = "Essential", value = 100 }, -- DON'T move from position 2 (see below)
{ text = "Optional", value = 60 },
{ text = "EyeCandy", value = 40 },
}
function ActionFXDetailLevelCombo()
return ActionFXDetailLevel
end
local ParticleDetailLevelMax = ActionFXDetailLevel[2].value
local function PreciseDetachObj(obj)
-- parent scale is lost after Detach
-- attach offset position and orientation are not restored by Detach
local px, py, pz = obj:GetVisualPosXYZ()
local axis = obj:GetVisualAxis()
local angle = obj:GetVisualAngle()
local scale = obj:GetWorldScale()
obj:Detach()
obj:SetPos(px, py, pz)
obj:SetAxis(axis)
obj:SetAngle(angle)
obj:SetScale(scale)
end
function DumpFXCacheInfo()
local FX = {}
local used_fx = 0
local cache_tables = 0
local cached_lists = 0
local cached_empty_fx = 0
local total_fx = 0
for action_id, actions in pairs(FXRules) do
for moment_id, moments in pairs(actions) do
for actor_id, actors in pairs(moments) do
for target_id, targets in pairs(actors) do
total_fx = total_fx + #targets
end
end
end
end
for action_id, actions in pairs(FXCache) do
cache_tables = cache_tables + 1
for moment_id, moments in pairs(actions) do
cache_tables = cache_tables + 1
for actor_id, actors in pairs(moments) do
cache_tables = cache_tables + 1
for target_id, targets in pairs(actors) do
cache_tables = cache_tables + 1
if targets then
cache_tables = cache_tables + 1
cached_lists = cached_lists + 1
used_fx = used_fx + #targets
for _, fx in ipairs(targets) do
local count = (FX[fx] or 0) + 1
FX[fx] = count
if count == 1 then
FX[#FX + 1] = fx
end
end
else
cached_empty_fx = cached_empty_fx + 1
end
end
end
end
end
table.sort(FX, function(a,b) return FX[a] > FX[b] end)
printf("Used tables in the cache = %d", cache_tables)
printf("Empty play fx = %d%%", cached_empty_fx * 100 / (cached_lists + cached_empty_fx))
printf("Used FX = %d (%d%%)", used_fx, used_fx * 100 / total_fx)
print("Most used FX:")
for i = 1, Min(10, #FX) do
local fx = FX[i]
printf("FX[%s] = %d", fx.class, FX[fx])
end
end
--============================= Action FX =======================
DefineClass.ActionFXEndRule = {
__parents = {"PropertyObject"},
properties = {
{ id = "EndAction", category = "Lifetime", default = "", editor = "combo", items = function(fx) return HookActionFXCombo(fx) end },
{ id = "EndMoment", category = "Lifetime", default = "", editor = "combo", items = function(fx) return HookMomentFXCombo(fx) end },
},
EditorView = Untranslated("Action '<EndAction>' & Moment '<EndMoment>'"),
}
function ActionFXEndRule:OnEditorSetProperty(prop_id, old_value, ged)
local preset = ged:GetParentOfKind("SelectedObject", "ActionFX")
if preset and preset:IsKindOf("ActionFX") then
local current_value = self[prop_id]
self[prop_id] = old_value
preset:RemoveFromRules()
self[prop_id] = current_value
preset:AddInRules()
end
end
DefineClass.ActionFX = {
__parents = { "FXPreset" },
properties = {
{ id = "Action", category = "Match", default = "any", editor = "combo", items = function(fx) return ActionFXClassCombo(fx) end },
{ id = "Moment", category = "Match", default = "any", editor = "combo", items = function(fx) return ActionMomentFXCombo(fx) end,
buttons = {{
name = "View Animation",
func = function(self) OpenAnimationMomentsEditor(self.Actor, FXActionToAnim(self.Action)) end,
is_hidden = function(self) return self:IsKindOf("GedMultiSelectAdapter") or not AppearanceLocateByAnimation(FXActionToAnim(self.Action), self.Actor) end,
}},
},
{ id = "Actor", category = "Match", default = "any", editor = "combo", items = function(fx) return ActorFXClassCombo(fx) end },
{ id = "Target", category = "Match", default = "any", editor = "combo", items = function(fx) return TargetFXClassCombo(fx) end },
{ id = "GameStatesFilter", name = "Game State", category = "Match", editor = "set", default = set(), three_state = true,
items = function() return GetGameStateFilter() end
},
{ id = "FxId", category = "Match", default = "", editor = "text", help = "Empty by default.\nFX Remove requires it to define which FX should be removed." },
{ id = "DetailLevel", category = "Match", default = ActionFXDetailLevel[1].value, editor = "combo", items = ActionFXDetailLevel, name = "Detail level category", help = "Determines the options detail levels at which the FX triggers. Essential will trigger always, Optional at high/medium setting, and EyeCandy at high setting only.", },
{ id = "Chance", category = "Match", editor = "number", default = 100, min = 0, max = 100, slider = true, help = "Chance the FX will be placed." },
{ id = "Disabled", category = "Match", default = false, editor = "bool", help = "Disabled FX are not played.", color = function(o) return o.Disabled and RGB(255,0,0) or nil end },
{ id = "Delay", name = "Delay (ms)", category = "Lifetime", default = 0, editor = "number", help = "In game time, in milliseconds.\nFX is not played when the actor is interrupted while in the delay." },
{ id = "Time", name = "Time (ms)", category = "Lifetime", default = 0, editor = "number", help = "Duration, in milliseconds."},
{ id = "GameTime", category = "Lifetime", editor = "bool", default = false },
{ id = "EndRules", category = "Lifetime", default = false, editor = "nested_list", base_class = "ActionFXEndRule", inclusive = true, },
{ id = "Behavior", category = "Lifetime", default = "", editor = "dropdownlist", items = function(fx) return ActionFXBehaviorCombo(fx) end },
{ id = "BehaviorMoment", category = "Lifetime", default = "", editor = "combo", items = function(fx) return HookMomentFXCombo(fx) end },
{ category = "Test", id = "Solo", default = false, editor = "bool", developer = true, dont_save = true, help = "Debug feature, if any fx's are set as solo, only they will be played."},
{ category = "Test", id = "DbgPrint", name = "DebugFX", default = false, editor = "bool", developer = true, dont_save = true, help = "Debug feature, print when this FX is about to play."},
{ category = "Test", id = "DbgBreak", name = "Break", default = false, editor = "bool", developer = true, dont_save = true, help = "Debug feature, break execution in the Lua debugger when this FX is about to play."},
{ category = "Test", id = "AnimEntity", name = "Anim Entity", default = "", editor = "text", help = "Specifies that this FX is linked to a specific animation. Auto fills the anims and moments available. An error will be issued if the action and the moment aren't found in that entity." },
{ id = "AnimRevisionEntity", default = false, editor = "text", no_edit = true },
{ id = "AnimRevision", default = false, editor = "number", no_edit = true },
{ id = "_reconfirm", category = "Preset", editor = "buttons", buttons = {{ name = "Confirm Changes", func = "ConfirmChanges" }},
no_edit = function(self) return not self:GetAnimationChangedWarning() end,
},
},
fx_type = "",
behaviors = false,
-- loc props
Source = "Actor",
SourceProp = "",
Spot = "",
SpotsPercent = -1,
Offset = false,
OffsetDir = "SourceAxisX",
Orientation = "",
PresetOrientationAngle = 0,
OrientationAxis = 1,
Attach = false,
Cooldown = 0,
}
ActionFX.Documentation = [[Defines rules for playing effects in the game on certain events.
An FX event is raised from the code using the PlayFX function. It has four main arguments: action, moment, actor, and target. All ActionFX presets matching these arguments will be activated.]]
function ActionFX:GenerateCode(code)
-- drop non property elements
local behaviors = self.behaviors
self.behaviors = nil
FXPreset.GenerateCode(self, code)
self.behaviors = behaviors
end
if FirstLoad or ReloadForDlc then
if Platform.developer then
g_SoloFX_count = 0
g_SoloFX_list = {} --used to turn off all solo fx at once
function ClearAllSoloFX()
local t = table.copy(g_SoloFX_list) --so we can iterate safely.
for i, v in ipairs(t) do
v:SetSolo(false)
end
end
else
function ClearAllSoloFX()
end
end
end
if Platform.developer then
function ActionFX:SetSolo(val)
if self.Solo == val then return end
if val then
g_SoloFX_count = g_SoloFX_count + 1
g_SoloFX_list[#g_SoloFX_list + 1] = self
else
g_SoloFX_count = g_SoloFX_count - 1
table.remove(g_SoloFX_list, table.find(g_SoloFX_list, self))
end
self.Solo = val
FXCache = false
end
end
function ActionFX:Done()
self:RemoveFromRules()
end
function ActionFX:PlayFX(actor, target, action_pos, action_dir)
end
function ActionFX:DestroyFX(actor, target)
local fx = self:AssignFX(actor, target, nil)
if not fx then
return
elseif IsValidThread(fx) then
DeleteThread(fx)
end
end
function ActionFX:AddInRules()
AddInRules(self)
self:HookBehaviors()
end
function ActionFX:RemoveFromRules()
RemoveFromRules(self)
self:UnhookBehaviors()
end
function ActionFX:HookBehaviors()
if not self.Disabled then
-- hook behavior
if self.Behavior ~= "" and self.BehaviorMoment ~= "" and self.BehaviorMoment ~= self.Moment then
self:HookBehaviorFX(self.Behavior, self.Action, self.BehaviorMoment, self.Actor, self.Target)
end
end
-- hook end action (even if disabled; this will allow currently playing FXs restored from savegames to stop)
if self.EndRules then
for idx, fxend in ipairs(self.EndRules) do
local end_action = fxend.EndAction ~= "" and fxend.EndAction or self.Action
local end_moment = fxend.EndMoment
if end_action ~= self.Action or end_moment ~= "" and end_moment ~= self.Moment then
self:HookBehaviorFX("DestroyFX", end_action, end_moment, self.Actor, self.Target)
end
end
end
end
function ActionFX:UnhookBehaviors()
local behaviors = self.behaviors
if not behaviors then return end
for i = #behaviors, 1, -1 do
local fx = behaviors[i]
RemoveFromRules(fx)
fx:delete()
end
self.behaviors = nil
end
function ActionFX:HookBehaviorFX(behavior, action, moment, actor, target)
for _, fx in ipairs(self.behaviors) do
if fx.Action == action and fx.Moment == moment
and fx.Actor == actor and fx.Target == target
and fx.fx == self and fx.BehaviorFXMethod == behavior then
StoreErrorSource(self, string.format("%s behaviors with the same action (%s), actor (%s), moment (%s), and target (%s) in this ActionFX", behavior, action, actor, moment, target))
break
end
end
self.behaviors = self.behaviors or {}
local fx = ActionFXBehavior:new{ Action = action, Moment = moment, Actor = actor, Target = target, fx = self, BehaviorFXMethod = behavior }
table.insert(self.behaviors, fx)
AddInRules(fx)
end
local rules_props = {
Action = true,
Moment = true,
Actor = true,
Target = true,
Disabled = true,
Behavior = true,
BehaviorMoment = true,
EndRules = true,
Cooldown = true,
}
function ActionFX:OnEditorSetProperty(prop_id, old_value)
-- remember the animation revision when the FX rule is linked to an animation moment
if (prop_id == "Action" or prop_id == "Moment") and self.Action ~= "any" and self.Moment ~= "any" then
local animation = FXActionToAnim(self.Action)
local appearance = AppearanceLocateByAnimation(animation, "__missing_appearance")
local entity = appearance and AppearancePresets[appearance].Body or self.AnimEntity ~= "" and self.AnimEntity
self.AnimRevisionEntity = entity or nil
self.AnimRevision = entity and EntitySpec:GetAnimRevision(entity, animation) or nil
end
if not rules_props[prop_id] then return end
local value = self[prop_id]
self[prop_id] = old_value
self:RemoveFromRules()
self[prop_id] = value
self:AddInRules()
end
function ActionFX:TrackFX()
return self.behaviors and true or false
end
function ActionFX:GameStatesMatched(game_states)
if not self.GameStatesFilter then return true end
for state, active in pairs(game_states) do
if self.GameStatesFilter[state] ~= active then
return
end
end
return true
end
function ActionFX:GetVariation(props_list)
local variations = 0
for i, prop in ipairs(props_list) do
if self[prop] ~= "" then
variations = variations + 1
end
end
if variations == 0 then
return
end
local id = AsyncRand(variations) + 1
for i, prop in ipairs(props_list) do
if self[prop] ~= "" then
id = id - 1
if id == 0 then
return self[prop]
end
end
end
end
function ActionFX:CreateThread(...)
if self.GameTime then
assert(self.Source ~= "UI")
return CreateGameTimeThread(...)
end
if self.Source == "UI" then
return CreateRealTimeThread(...)
end
local thread = CreateMapRealTimeThread(...)
MakeThreadPersistable(thread)
return thread
end
if FirstLoad then
FX_Assigned = {}
end
local function FilterFXValues(data, f)
if not data then return false end
local result = {}
for fx_preset, actor_map in pairs(data) do
local result_actor_map = setmetatable({}, weak_keys_meta)
for actor, target_map in pairs(actor_map) do
if f(actor, nil) then
local result_target_map = setmetatable({}, weak_keys_meta)
for target, fx in pairs(target_map) do
if f(actor, target) then
result_target_map[target] = fx
end
end
if next(result_target_map) ~= nil then
result_actor_map[actor] = result_target_map
end
end
end
if next(result_actor_map) ~= nil then
result[fx_preset] = result_actor_map
end
end
return result
end
local IsKindOf = IsKindOf
function OnMsg.PersistSave(data)
data["FX_Assigned"] = FilterFXValues(FX_Assigned, function(actor, target)
if IsKindOf(actor, "XWindow") then
return false
end
if IsKindOf(target, "XWindow") then
return false
end
return true
end)
end
function OnMsg.PersistLoad(data)
FX_Assigned = data.FX_Assigned or {}
end
function OnMsg.ChangeMapDone()
FX_Assigned = FilterFXValues(FX_Assigned, function(actor, target)
if IsKindOf(actor, "XWindow") then
if not target or IsKindOf(target, "XWindow") then
return true
end
end
return false
end)
end
function ActionFX:AssignFX(actor, target, fx)
local t = FX_Assigned[self]
if not t then
if fx == nil then return end
t = setmetatable({}, weak_keys_meta)
FX_Assigned[self] = t
end
local t2 = t[actor or false]
if not t2 then
if fx == nil then return end
t2 = setmetatable({}, weak_keys_meta)
t[actor or false] = t2
end
local id = self.Target == "ignore" and "ignore" or target or false
local prev_fx = t2[id]
t2[id] = fx
return prev_fx
end
function ActionFX:GetAssignedFX(actor, target)
local o = FX_Assigned[self]
o = o and o[actor or false]
o = o and o[self.Target == "ignore" and "ignore" or target or false]
return o
end
function ActionFX:GetLocObj(actor, target)
local obj
local source = self.Source
if source == "Actor" then
obj = IsValid(actor) and actor
elseif source == "ActorParent" then
obj = IsValid(actor) and GetTopmostParent(actor)
elseif source == "ActorOwner" then
obj = actor and IsValid(actor.NetOwner) and actor.NetOwner
elseif source == "Target" then
obj = IsValid(target) and target
elseif source == "Camera" then
obj = IsValid(g_CameraObj) and g_CameraObj
end
if obj then
if self.SourceProp ~= "" then
local prop = obj:GetProperty(self.SourceProp)
obj = prop and IsValid(prop) and prop
elseif self.Spot ~= "" then
local o = obj:GetObjectBySpot(self.Spot)
if o ~= nil then
obj = o
end
end
end
return obj
end
function ActionFX:GetLoc(actor, target, action_pos, action_dir)
if self.Source == "ActionPos" then
if action_pos and action_pos:IsValid() then
local posx, posy, posz = action_pos:xyz()
return 1, nil, nil, self:FXOrientLoc(nil, posx, posy, posz, nil, nil, nil, nil, actor, target, action_pos, action_dir)
elseif IsValid(actor) and actor:IsValidPos() then
-- use actor position for default
local posx, posy, posz = GetTopmostParent(actor):GetSpotLocPosXYZ(-1)
return 1, nil, nil, self:FXOrientLoc(nil, posx, posy, posz, nil, nil, nil, nil, actor, target, action_pos, action_dir)
end
return 0
end
-- find loc obj
local obj = self:GetLocObj(actor, target)
if not obj then
return 0
end
local spots_count, first_spot, spots_list = self:GetLocObjSpots(obj)
if (spots_count or 0) <= 0 then
return 0
elseif spots_count == 1 then
local posx, posy, posz, angle, axisx, axisy, axisz
if obj:IsValidPos() then
posx, posy, posz, angle, axisx, axisy, axisz = obj:GetSpotLocXYZ(first_spot or -1)
end
return 1, obj, first_spot, self:FXOrientLoc(obj, posx, posy, posz, angle, axisx, axisy, axisz, actor, target, action_pos, action_dir)
end
local params = {}
for i = 0, spots_count-1 do
local spot = spots_list and spots_list[i+1] or first_spot + i
local posx, posy, posz, angle, axisx, axisy, axisz
if obj:IsValidPos() then
posx, posy, posz, angle, axisx, axisy, axisz = obj:GetSpotLocXYZ(spot)
end
posx, posy, posz, angle, axisx, axisy, axisz = self:FXOrientLoc(obj, posx, posy, posz, angle, axisx, axisy, axisz, actor, target, action_pos, action_dir)
params[8*i+1] = spot
params[8*i+2] = posx
params[8*i+3] = posy
params[8*i+4] = posz
params[8*i+5] = angle
params[8*i+6] = axisx
params[8*i+7] = axisy
params[8*i+8] = axisz
end
return spots_count, obj, params
end
function ActionFX:FXOrientLoc(obj, posx, posy, posz, angle, axisx, axisy, axisz, actor, target, action_pos, action_dir)
local orientation = self.Orientation
if orientation == "" and self.Attach then
orientation = "SpotX"
end
if posx then
local offset = self.Offset
if offset and offset ~= point30 then
local o_axisx, o_axisy, o_axisz, o_angle = FXCalcOrientation(self.OffsetDir, 1, obj, posx, posy, posz, 0, actor, target, action_pos, action_dir)
local x, y, z
if (o_angle or 0) == 0 or o_axisx == 0 and o_axisy == 0 and offset:Equal2D(point20) then
x, y, z = offset:xyz()
else
x, y, z = RotateAxisXYZ(offset, point(o_axisx, o_axisy, o_axisz), o_angle)
end
posx = posx + x
posy = posy + y
if posz and z then
posz = posz + z
end
end
end
local o_axisx, o_axisy, o_axisz, o_angle = FXCalcOrientation(orientation, self.OrientationAxis, obj, posx, posy, posz, self.PresetOrientationAngle, actor, target, action_pos, action_dir)
if o_angle then
angle, axisx, axisy, axisz = o_angle, o_axisx, o_axisy, o_axisz
end
return posx, posy, posz, angle, axisx, axisy, axisz
end
function ActionFX:GetLocObjSpots(obj)
local percent = self.SpotsPercent
if percent == 0 then
return 0
end
local spot_name = self.Spot
if spot_name == "" or spot_name == "Origin" or not obj:HasSpot(spot_name) then
return 1
elseif percent < 0 then
return 1, obj:GetRandomSpot(spot_name)
else
local first_spot, last_spot = obj:GetSpotRange(spot_name)
local spots_count = last_spot - first_spot + 1
local count = spots_count
if percent < 100 then
local remainder = count * percent % 100
local roll = remainder > 0 and AsyncRand(100) or 0
count = count * percent / 100 + (roll < remainder and 1 or 0)
end
if count <= 0 then
return
elseif count == 1 then
return 1, first_spot + (spots_count > 1 and AsyncRand(spots_count) or 0)
elseif count >= spots_count then
return spots_count, first_spot
end
local spots = {}
for i = 1, count do
local k = i + AsyncRand(spots_count-i+1)
spots[i], spots[k] = spots[k] or first_spot + k - 1, spots[i] or first_spot + i - 1
end
return count, nil, spots
end
end
function FXAnimToAction(anim)
return anim
end
function FXActionToAnim(action)
return action
end
local GetEntityAnimMoments = GetEntityAnimMoments
function OnMsg.GatherFXMoments(list, fx)
local entity = fx and rawget(fx, "AnimEntity") or ""
if entity == "" or not IsValidEntity(entity) then
return
end
local anim = fx and fx.Action
if not anim or anim == "any" or anim == "" or not EntityStates[anim] then
return
end
for _, moment in ipairs(GetEntityAnimMoments(entity, anim)) do
list[#list + 1] = moment.Type
end
end
function ActionFX:GetError()
local entity = self.AnimEntity or ""
if entity ~= "" then
if not IsValidEntity(entity) then
return "No such entity: " .. entity
end
local anim = self.Action or ""
if anim ~= "" and anim ~= "any" then
if not EntityStates[anim] then
return "Invalid state: " .. anim
end
if not HasState(entity, anim) then
return "No such anim: " .. entity .. "." .. anim
end
local moment = self.Moment or ""
if moment ~= "" and moment ~= "any" then
local moments = GetEntityAnimMoments(entity, anim)
if not table.find(moments, "Type", moment) then
return "No such moment: " .. entity .. "." .. anim .. "." .. moment
end
end
end
end
end
function ActionFX:GetAnimationChangedWarning()
local entity, anim = self.AnimRevisionEntity, FXActionToAnim(self.Action)
if entity and not IsValidEntity(entity) then
return string.format("Entity %s with which this FX was created no longer exists.\nPlease test/readjust it and click Confirm Changes below.", entity)
end
if entity and not HasState(entity, anim) then
return string.format("Entity %s with which this FX was created no longer has animation %s.\nPlease test/readjust it and click Confirm Changes below.", entity, anim)
end
-- return entity and EntitySpec:GetAnimRevision(entity, anim) > self.AnimRevision and
-- string.format("Animation %s was updated after this FX.\nPlease test/readjust it and click Confirm Changes below.", anim)
end
function ActionFX:GetWarning()
return self:GetAnimationChangedWarning()
end
function ActionFX:ConfirmChanges()
self.AnimRevision = EntitySpec:GetAnimRevision(self.AnimRevisionEntity, FXActionToAnim(self.Action))
ObjModified(self)
end
--============================= Test FX =======================
if FirstLoad then
LastTestActionFXObject = false
end
function OnMsg.DoneMap()
LastTestActionFXObject = false
end
local function TestActionFXObjectEnd(obj)
DoneObject(obj)
if LastTestActionFXObject == obj then
LastTestActionFXObject = false
return
end
if obj or not LastTestActionFXObject then
return
end
DoneObject(LastTestActionFXObject)
LastTestActionFXObject = false
end
--============================= Inherit FX =======================
DefineClass.ActionFXInherit = {
__parents = { "FXPreset" },
}
DefineClass.ActionFXInherit_Action = {
__parents = { "ActionFXInherit" },
properties = {
{ id = "Action", category = "Inherit", default = "", editor = "combo", items = function(fx) return ActionFXClassCombo(fx) end },
{ id = "Inherit", category = "Inherit", default = "", editor = "combo", items = function(fx) return ActionFXClassCombo(fx) end },
{ id = "All", category = "Inherit", default = "", editor = "text", lines = 5, read_only = true, dont_save = true }
},
fx_type = "Inherit Action",
}
function ActionFXInherit_Action:Done()
FXInheritRules_Actions = false
FXCache = false
end
function ActionFXInherit_Action:GetAll()
local list = (FXInheritRules_Actions or RebuildFXInheritActionRules())[self.Action]
return list and table.concat(list, "\n") or ""
end
function ActionFXInherit_Action:OnEditorSetProperty(prop_id, old_value, ged)
FXInheritRules_Actions = false
FXCache = false
end
DefineClass.ActionFXInherit_Moment = {
__parents = { "ActionFXInherit" },
properties = {
{ id = "Moment", category = "Inherit", default = "", editor = "combo", items = function(fx) return ActionMomentFXCombo(fx) end },
{ id = "Inherit", category = "Inherit", default = "", editor = "combo", items = function(fx) return ActionMomentFXCombo(fx) end },
{ id = "All", category = "Inherit", default = "", editor = "text", lines = 5, read_only = true, dont_save = true }
},
fx_type = "Inherit Moment",
}
function ActionFXInherit_Moment:Done()
FXInheritRules_Moments = false
FXCache = false
end
function ActionFXInherit_Moment:GetAll()
local list = (FXInheritRules_Moments or RebuildFXInheritMomentRules())[self.Moment]
return list and table.concat(list, "\n") or ""
end
function ActionFXInherit_Moment:OnEditorSetProperty(prop_id, old_value, ged)
FXInheritRules_Moments = false
FXCache = false
end
DefineClass.ActionFXInherit_Actor = {
__parents = { "ActionFXInherit" },
properties = {
{ id = "Actor", category = "Inherit", default = "", editor = "combo", items = function(fx) return ActorFXClassCombo(fx) end },
{ id = "Inherit", category = "Inherit", default = "", editor = "combo", items = function(fx) return ActorFXClassCombo(fx) end },
{ id = "All", category = "Inherit", default = "", editor = "text", lines = 5, read_only = true, dont_save = true }
},
fx_type = "Inherit Actor",
}
function ActionFXInherit_Actor:Done()
FXInheritRules_Actors = false
FXCache = false
end
function ActionFXInherit_Actor:GetAll()
local list = (FXInheritRules_Actors or RebuildFXInheritActorRules())[self.Actor]
return list and table.concat(list, "\n") or ""
end
function ActionFXInherit_Actor:OnEditorSetProperty(prop_id, old_value, ged)
FXInheritRules_Actors = false
FXCache = false
end
--============================= Behavior FX =======================
DefineClass.ActionFXBehavior = {
__parents = { "InitDone" },
properties = {
{ id = "Action", default = "any" },
{ id = "Moment", default = "any" },
{ id = "Actor", default = "any" },
{ id = "Target", default = "any" },
},
fx = false,
BehaviorFXMethod = "",
fx_type = "Behavior",
Disabled = false,
Delay = 0,
Map = "any",
Id = "",
DetailLevel = 100,
Chance = 100,
}
function ActionFXBehavior:PlayFX(actor, target, ...)
self.fx[self.BehaviorFXMethod](self.fx, actor, target, ...)
end
--============================= Remove FX =======================
DefineClass.ActionFXRemove = {
__parents = { "ActionFX" },
properties = {
{ id = "Time", editor = false },
{ id = "EndRules", editor = false },
{ id = "Behavior", editor = false },
{ id = "BehaviorMoment", editor = false },
{ id = "Delay", editor = false },
{ id = "GameTime", editor = false },
},
fx_type = "FX Remove",
Documentation = ActionFX.Documentation .. "\n\nRemoves an action fx."
}
function ActionFXRemove:HookBehaviors()
end
function ActionFXRemove:UnhookBehaviors()
end
--============================= Sound FX =======================
local MarkObjSound = empty_func
function OnMsg.ChangeMap()
if not config.AllowSoundFXOnMapChange then
DisableSoundFX = true
end
end
function OnMsg.ChangeMapDone()
DisableSoundFX = false
end
DefineClass.ActionFXSound = {
__parents = { "ActionFX" },
properties = {
{ category = "Match", id = "Cooldown", name = "Cooldown (ms)", default = 0, editor = "number", help = "Cooldown, in real time milliseconds." },
{ category = "Sound", id = "Sound", default = "", editor = "preset_id", preset_class = "SoundPreset", buttons = {{name = "Test", func = "TestActionFXSound"}, {name = "Stop", func = "StopActionFXSound"}}},
{ category = "Sound", id = "DistantRadius", default = 0, editor = "number", scale = "m", help = "Defines the radius for playing DistantSound." },
{ category = "Sound", id = "DistantSound", default = "", editor = "preset_id", preset_class = "SoundPreset",
help = "This sound will be played if the distance from the camera is greater than DistantRadius."
},
{ category = "Sound", id = "FadeIn", default = 0, editor = "number", },
{ category = "Sound", id = "FadeOut", default = 0, editor = "number", },
{ category = "Sound", id = "Source", default = "Actor", editor = "dropdownlist", items = { "UI", "Actor", "Target", "ActionPos", "Camera" }, help = "Sound listener object or position." },
{ category = "Sound", id = "Spot", default = "", editor = "combo", items = function(fx) return ActionFXSpotCombo(fx) end, no_edit = no_obj_no_edit },
{ category = "Sound", id = "SpotsPercent", default = -1, editor = "number", no_edit = no_obj_no_edit, help = "Percent of random spots that should be used. One random spot is used when the value is negative." },
{ category = "Sound", id = "Offset", default = point30, editor = "point", scale = "m", help = "Offset against source object" },
{ category = "Sound", id = "OffsetDir", default = "SourceAxisX", no_edit = function(self) return self.AttachToObj end, editor = "dropdownlist", items = function(fx) return ActionFXOrientationCombo end },
{ category = "Sound", id = "AttachToObj", name = "Attach To Source", editor = "bool", default = false, help = "Attach to the actor or target (the Source) and move with it." },
{ category = "Sound", id = "SkipSame", name = "Skip Same Sound", editor = "bool", default = false, no_edit = PropChecker("AttachToObj", false), help = "Don't start a new sound if the object has the same sound already playing" },
{ category = "Sound", id = "AttachToObjHelp", editor = "help", default = false,
help = "Sounds attached to an object are played whenever the camera gets close, even if it was away when the object was created.\n\nOnly one sound can be attached to an object, and attaching a new sound removes the previous one. Use this for single sounds that are emitted permanently." },
},
fx_type = "Sound",
Documentation = ActionFX.Documentation .. "\n\nThis mod item creates and plays sound effects when an FX action is triggered. Inherits ActionFX. Read ActionFX first for the common properties.",
DocumentationLink = "Docs/ModItemActionFXSound.md.html"
}
MapVar("FXCameraSounds", {}, weak_keys_meta)
function OnMsg.DoneMap()
for fx in pairs(FXCameraSounds) do
FXCameraSounds[fx] = nil
if fx.sound_handle then
SetSoundVolume(fx.sound_handle, -1, not config.AllowSoundFXOnMapChange and fx.fade_out or 0) -- -1 means destroy
fx.sound_handle = nil
end
DeleteThread(fx.thread)
end
end
function OnMsg.PostLoadGame()
for fx in pairs(FXCameraSounds) do
local sound = fx.Sound or ""
local handle = sound ~= "" and PlaySound(sound, nil, 300)
if not handle then
FXCameraSounds[fx] = nil
DeleteThread(fx.thread)
else
fx.sound_handle = handle
end
end
end
function ActionFXSound:TrackFX()
if self.behaviors or self.FadeOut > 0 or self.Time > 0 or self.Source == "Camera" or (self.AttachToObj and self.Spot ~= "") or self.Cooldown > 0 then
return true
end
return false
end
function ActionFXSound:PlayFX(actor, target, action_pos, action_dir)
if self.Sound == "" and self.DistandSound == "" or DisableSoundFX then
return
end
if self.Cooldown > 0 then
local fx = self:GetAssignedFX(actor, target)
if fx and fx.time and RealTime() - fx.time < self.Cooldown then
return
end
end
local count, obj, posx, posy, posz, spot
local source = self.Source
if source ~= "UI" and source ~= "Camera" then
count, obj, spot, posx, posy, posz = self:GetLoc(actor, target, action_pos, action_dir)
if count == 0 then
--print("FX Sound with bad position:", self.Sound, "\n\t- Actor:", actor and actor.class, "\n\t- Target:", target and target.class)
return
end
end
if self.Delay <= 0 then
self:PlaceFXSound(actor, target, count, obj, spot, posx, posy, posz)
return
end
local thread = self:CreateThread(function(self, ...)
Sleep(self.Delay)
self:PlaceFXSound(...)
end, self, actor, target, count, obj, spot, posx, posy, posz)
if self:TrackFX() then
local fx = self:DestroyFX(actor, target)
if not fx then
fx = {}
self:AssignFX(actor, target, fx)
end
fx.thread = thread
end
end
local function WaitDestroyFX(self, fx, actor, target)
Sleep(self.Time)
if fx.thread == CurrentThread() then
self:DestroyFX(actor, target)
end
end
function ActionFXSound:PlaceFXSound(actor, target, count, obj, spot, posx, posy, posz)
local handle, err
local source = self.Source
if source == "UI" or source == "Camera" then
handle, err = PlaySound(self.Sound, nil, self.FadeIn)
else
if Platform.developer then
-- Make the check for positional sounds only if we have .Sound (removed on non-dev and xbox)
local sounds = SoundPresets
if sounds and next(sounds) then
local sound = self.Sound
if sound == "" then
sound = self.DistantSound
end
local snd = sounds[sound]
if not snd then
printf('once', 'FX sound not found "%s"', sound)
return
end
local snd_type = SoundTypePresets[snd.type]
if not snd_type then
printf('once', 'FX sound type not found "%s"', snd.type)
return
end
local positional = snd_type.positional
if not positional then
printf('once', 'FX non-positional sound "%s" (type "%s") played on Source position: %s', sound, snd.type, source)
return
end
end
end
if (count or 1) == 1 then
handle, err = self:PlaceSingleFXSound(actor, target, 1, obj, spot, posx, posy, posz)
else
for i = 0, count - 1 do
local h, e = self:PlaceSingleFXSound(actor, target, i + 1, obj, unpack_params(spot, 8*i+1, 8*i+4))
if h then
handle = handle or {}
table.insert(handle, h)
else
err = e
end
end
end
end
if DebugFXSound and (type(DebugFXSound) ~= "string" or IsKindOf(obj or actor, DebugFXSound)) then
printf('FX sound %s "%s",<tab 450>matching: %s - %s - %s - %s', handle and "play" or "fail", self.Sound, self.Action, self.Moment, self.Actor, self.Target)
if not handle and err then
print(" FX sound error:", err)
end
end
if not handle then
return
end
if self.Cooldown <= 0 and not self:TrackFX() then
return
end
local fx = self:GetAssignedFX(actor, target)
if not fx then
fx = {}
self:AssignFX(actor, target, fx)
end
if self.Cooldown > 0 then
fx.time = RealTime()
end
if self:TrackFX() then
fx.sound_handle = handle
fx.fade_out = self.FadeOut
if source == "Camera" and FXCameraSounds then
FXCameraSounds[fx] = true
-- "persist" camera sounds (restart them upon loading game) if they have a rule to stop them, e.g. rain sounds
if self.EndRules and next(self.EndRules) then
fx.Sound = self.Sound
end
end
if self.Time <= 0 then
return
end
fx.thread = self:CreateThread(WaitDestroyFX, self, fx, actor, target)
end
end
function ActionFXSound:GetError()
if (self.Sound or "") == "" and (self.DistantSound or "") == "" then
return "No sound specified"
end
end
function ActionFXSound:GetProjectReplace(sound, actor)
return sound
end
function ActionFXSound:PlaceSingleFXSound(actor, target, idx, obj, spot, posx, posy, posz)
if obj and (not IsValid(obj) or not obj:IsValidPos()) then
return
end
local sound = self.Sound or ""
local distant_sound = self.DistantSound or ""
local distant_radius = self.DistantRadius
if distant_sound ~= "" and distant_radius > 0 then
local x, y = posx, posy
if obj then
x, y = obj:GetVisualPosXYZ()
end
if not IsCloser2D(camera.GetPos(), x, y, distant_radius) then
sound = distant_sound
end
end
if sound == "" then return end
sound = self:GetProjectReplace(sound, actor)-- give it a chance ot be replaced by project specific logic
local handle, err
if not obj then
return PlaySound(sound, nil, self.FadeIn, false, point(posx, posy, posz or const.InvalidZ))
elseif not self.AttachToObj then
if self.Spot == "" and self.Offset == point30 then
return PlaySound(sound, nil, self.FadeIn, false, obj)
else
return PlaySound(sound, nil, self.FadeIn, false, point(posx, posy, posz or const.InvalidZ))
end
elseif self.Spot == "" and self.Offset == point30 then
local sname, sbank, stype, shandle, sduration, stime = obj:GetSound()
if not self.SkipSame or sound ~= sbank then
if sound ~= sbank or stime ~= GameTime() or self:TrackFX() then
dbg(MarkObjSound(self, obj, sound))
obj:SetSound(sound, 1000, self.FadeIn)
end
end
else
local sound_dummy
if idx == 1 then
self:DestroyFX(actor, target)
end
local fx = self:GetAssignedFX(actor, target)
if fx then
local list = fx.sound_dummies
for i = list and #list or 0, 1, -1 do
local o = list[i]
if o:GetAttachSpot() == spot then
sound_dummy = o
break
end
end
else
fx = {}
if self:TrackFX() then
self:AssignFX(actor, target, fx)
end
end
if not sound_dummy or not IsValid(sound_dummy) then
sound_dummy = PlaceObject("SoundDummy")
fx.sound_dummies = fx.sound_dummies or {}
table.insert(fx.sound_dummies, sound_dummy)
end
if spot then
obj:Attach(sound_dummy, spot)
else
obj:Attach(sound_dummy)
end
dbg(MarkObjSound(self, sound_dummy, sound))
sound_dummy:SetAttachOffset(self.Offset)
sound_dummy:SetSound(sound, 1000, self.FadeIn)
end
end
function ActionFXSound:DestroyFX(actor, target)
local fx = self:GetAssignedFX(actor, target)
if self.AttachToObj then
if self.Spot == "" then
local obj = self:GetLocObj(actor, target)
if IsValid(obj) then
obj:StopSound(self.FadeOut)
end
else
if not fx then return end
local list = fx.sound_dummies
for i = list and #list or 0, 1, -1 do
local o = list[i]
if not IsValid(o) then
table.remove(list, i)
else
o:StopSound(self.FadeOut)
end
end
end
else
if not fx then return end
if FXCameraSounds then
FXCameraSounds[fx] = nil
end
local handle = fx.sound_handle
if handle then
if type(handle) == "table" then
for i = 1, #handle do
SetSoundVolume(handle[i], -1, self.FadeOut) -- -1 means destroy
end
else
SetSoundVolume(handle, -1, self.FadeOut) -- -1 means destroy
end
fx.sound_handle = nil
end
if fx.thread and fx.thread ~= CurrentThread() then
DeleteThread(fx.thread)
fx.thread = nil
end
end
return fx
end
if FirstLoad then
l_snd_test_handle = false
end
function TestActionFXSound(editor_obj, fx, prop_id)
StopActionFXSound()
l_snd_test_handle = PlaySound(fx.Sound)
end
function StopActionFXSound()
if l_snd_test_handle then
StopSound(l_snd_test_handle)
l_snd_test_handle = false
end
end
----
--============================= Wind Mod FX =======================
local function custom_mod_no_edit(self)
return #(self.Presets or "") > 0
end
local function no_obj_or_attach_no_edit(self)
return self.AttachToObj or self.Source ~= "Actor" and self.Source ~= "Target"
end
local function attach_no_edit(self)
return self.AttachToObj
end
DefineClass.ActionFXWindMod = {
__parents = { "ActionFX" },
properties = {
{ category = "Wind Mod", id = "Source", default = "Actor", editor = "dropdownlist", items = { "UI", "Actor", "Target", "ActionPos", "Camera" }, help = "Sound mod object or position." },
{ category = "Wind Mod", id = "AttachToObj", name = "Attach To Source", editor = "bool", default = false, no_edit = no_obj_no_edit, help = "Attach to the actor or target (the Source) and move with it." },
{ category = "Wind Mod", id = "Spot", default = "", editor = "combo", items = function(fx) return ActionFXSpotCombo(fx) end, no_edit = no_obj_or_attach_no_edit },
{ category = "Wind Mod", id = "Offset", default = point30, editor = "point", scale = "m", no_edit = attach_no_edit, help = "Offset against source" },
{ category = "Wind Mod", id = "OffsetDir", default = "SourceAxisX", no_edit = attach_no_edit, editor = "dropdownlist", items = function(fx) return ActionFXOrientationCombo end, },
{ category = "Wind Mod", id = "ModBySpeed", default = false, no_edit = no_obj_no_edit, editor = "bool", help = "Modify the wind strength by the speed of the object" },
{ category = "Wind Mod", id = "ModBySize", default = false, no_edit = no_obj_no_edit, editor = "bool", help = "Modify the wind radius by the size of the object" },
{ category = "Wind Mod", id = "OnTerrainOnly", default = true, editor = "bool", help = "Allow the wind mod only on terrain" },
{ category = "Wind Mod", id = "Presets", default = false, editor = "string_list", items = function() return table.keys(WindModifierParams, true) end, buttons = {{name = "Test", func = "TestActionFXWindMod"}, {name = "Stop", func = "StopActionFXWindMod"}, {name = "Draw Debug", func = "DbgWindMod"}}},
{ category = "Wind Mod", id = "AttachOffset", name = "Offset", default = point30, editor = "point", no_edit = custom_mod_no_edit },
{ category = "Wind Mod", id = "HalfHeight", name = "Capsule half height", default = guim, scale = "m", editor = "number", no_edit = custom_mod_no_edit },
{ category = "Wind Mod", id = "Range", name = "Capsule inner radius", default = guim, scale = "m", editor = "number", no_edit = custom_mod_no_edit, help = "Min range of action (vertex deformation) 100%" },
{ category = "Wind Mod", id = "OuterRange", name = "Capsule outer radius", default = guim, scale = "m", editor = "number", no_edit = custom_mod_no_edit, help = "Max range of action (vertex deformation)" },
{ category = "Wind Mod", id = "Strength", name = "Strength", default = 10000, scale = 1000, editor = "number", no_edit = custom_mod_no_edit, help = "Strength vertex deformation" },
{ category = "Wind Mod", id = "ObjHalfHeight", name = "Obj Capsule half height", default = guim, scale = "m", editor = "number", no_edit = custom_mod_no_edit, help = "Patch deform" },
{ category = "Wind Mod", id = "ObjRange", name = "Obj Capsule inner radius", default = guim, scale = "m", editor = "number", no_edit = custom_mod_no_edit, help = "Patch deform" },
{ category = "Wind Mod", id = "ObjOuterRange", name = "Obj Capsule outer radius", default = guim, scale = "m", editor = "number", no_edit = custom_mod_no_edit, help = "Patch deform" },
{ category = "Wind Mod", id = "ObjStrength", name = "Obj Strength", default = 10000, scale = 1000, editor = "number", no_edit = custom_mod_no_edit, help = "Patch deform" },
{ category = "Wind Mod", id = "SizeAttenuation", name = "Size Attenuation", default = 5000, scale = 1000, editor = "number", no_edit = custom_mod_no_edit },
{ category = "Wind Mod", id = "HarmonicConst", name = "Frequency", default = 10000, scale = 1000, editor = "number", no_edit = custom_mod_no_edit},
{ category = "Wind Mod", id = "HarmonicDamping", name = "Damping ratio", default = 800, scale = 1000, editor = "number", no_edit = custom_mod_no_edit },
{ category = "Wind Mod", id = "WindModifierMask", name = "Modifier Mask", default = -1, editor = "flags", size = function() return #(const.WindModifierMaskFlags or "") end, items = function() return const.WindModifierMaskFlags end, no_edit = custom_mod_no_edit },
},
fx_type = "Wind Mod",
SpotsPercent = -1,
GameTime = true,
}
function ActionFXWindMod:TrackFX()
if self.behaviors or self.Time > 0 then
return true
end
return false
end
function ActionFXWindMod:DbgWindMod(fx)
hr.WindModifierDebug = 1 - hr.WindModifierDebug
end
function ActionFXWindMod:PlayFX(actor, target, action_pos, action_dir)
local count, obj, spot, posx, posy, posz = self:GetLoc(actor, target, action_pos, action_dir)
if count == 0 then
return
end
if self.OnTerrainOnly and not posz then
return
end
if self.Delay <= 0 then
self:PlaceFXWindMod(actor, target, count, obj, spot, posx, posy, posz)
return
end
local thread = self:CreateThread(function(self, ...)
Sleep(self.Delay)
self:PlaceFXWindMod(...)
end, self, actor, target, count, obj, spot, posx, posy, posz)
if self:TrackFX() then
local fx = self:DestroyFX(actor, target)
if not fx then
fx = {}
self:AssignFX(actor, target, fx)
end
fx.thread = thread
end
end
local function PlaceSingleFXWindMod(params, attach_to, pos, range_mod, strength_mod, speed_mod)
return terrain.SetWindModifier(
(pos or point30):Add(params.AttachOffset or point30),
params.HalfHeight,
range_mod and params.Range * range_mod / guim or params.Range,
range_mod and params.OuterRange * range_mod / guim or params.OuterRange,
strength_mod and params.Strength * strength_mod / guim or params.Strength,
params.ObjHalfHeight,
range_mod and params.ObjRange * range_mod / guim or params.ObjRange,
range_mod and params.ObjOuterRange * range_mod / guim or params.ObjOuterRange,
strength_mod and params.ObjStrength * strength_mod / guim or params.ObjStrength,
params.SizeAttenuation,
speed_mod and params.HarmonicConst * speed_mod / 1000 or params.HarmonicConst,
speed_mod and params.HarmonicDamping * speed_mod / 1000 or params.HarmonicDamping,
0,
0,
params.WindModifierMask or -1,
attach_to)
end
function ActionFXWindMod:PlaceFXWindMod(actor, target, count, obj, spot, posx, posy, posz, range_mod, strength_mod, speed_mod)
range_mod = range_mod or self.ModBySize and obj and obj:GetRadius()
strength_mod = strength_mod or self.ModBySpeed and obj and obj:GetSpeed()
speed_mod = speed_mod or self.GameTime and GetTimeFactor()
if speed_mod <= 0 then
speed_mod = false
end
local attach_to = self.AttachToObj and obj
local pos = point30
if not attach_to then
pos = point(posx, posy, posz)
end
local ids
if #(self.Presets or "") == 0 then
ids = PlaceSingleFXWindMod(self, attach_to, pos, range_mod, strength_mod, speed_mod)
else
for _, preset in ipairs(self.Presets) do
local params = WindModifierParams[preset]
if params then
local id = PlaceSingleFXWindMod(params, attach_to, pos, range_mod, strength_mod, speed_mod)
if not ids then
ids = id
elseif type(ids) == "table" then
ids[#ids + 1] = id
else
ids = { ids, id }
end
end
end
end
if not ids or not self:TrackFX() then
return
end
local fx = self:GetAssignedFX(actor, target)
if not fx then
fx = {}
self:AssignFX(actor, target, fx)
end
fx.wind_mod_ids = ids
if self.Time <= 0 then
return
end
fx.thread = self:CreateThread(WaitDestroyFX, self, fx, actor, target)
end
function ActionFXWindMod:DestroyFX(actor, target)
local fx = self:GetAssignedFX(actor, target)
if not fx then return end
local wind_mod_ids = self.wind_mod_ids
if wind_mod_ids then
if type(wind_mod_ids) == "number" then
terrain.RemoveWindModifier(wind_mod_ids)
else
for _, id in ipairs(wind_mod_ids) do
terrain.RemoveWindModifier(id)
end
end
fx.wind_mod_ids = nil
end
if fx.thread and fx.thread ~= CurrentThread() then
DeleteThread(fx.thread)
fx.thread = nil
end
return fx
end
if FirstLoad then
l_windmod_test_id = false
end
function TestActionFXWindMod(editor_obj, fx, prop_id)
StopActionFXWindMod()
local obj = selo() or SelectedObj
if not IsValid(obj) then
print("No object selected!")
return
end
local actor, target, count, spot
local x, y, z = obj:GetVisualPosXYZ()
l_windmod_test_id = fx:PlaceFXWindMod(actor, target, count, obj, spot, x, y, z, nil, nil, 1000) or false
end
function StopActionFXWindMod()
if l_windmod_test_id then
terrain.RemoveWindModifier(l_windmod_test_id)
l_windmod_test_id = false
end
end
----
DefineClass.ActionFXUIParticles = {
__parents = {"ActionFX"},
properties = {
{ id = "Particles", category = "Particles", default = "", editor = "combo", items = UIParticlesComboItems },
{ id = "Foreground", category = "Particles", default = false, editor = "bool" },
{ id = "HAlign", category = "Particles", default = "middle", editor = "choice", items = function() return GetUIParticleAlignmentItems(true) end },
{ id = "VAlign", category = "Particles", default = "middle", editor = "choice", items = function() return GetUIParticleAlignmentItems(false) end },
{ id = "TransferToParent", category = "Lifetime", default = false, editor = "bool", help = "Should particles continue to live after the host control dies?" },
{ id = "StopEmittersOnTransfer", category = "Lifetime", default = true, editor = "bool", no_edit = function(self) return not self.TransferToParent end },
{ id = "GameTime", editor = false },
},
Time = -1,
fx_type = "UI Particles",
}
function ActionFXUIParticles:TrackFX()
return true
end
function ActionFXUIParticles:PlayFX(actor, target, action_pos, action_dir)
assert(IsKindOf(actor, "XControl"))
local stop_fx = self:GetAssignedFX(actor, target)
if stop_fx then
stop_fx()
end
local create_particles = function(self, actor, target)
local id = UIL.PlaceUIParticles(self.Particles)
self:AssignFX(actor, target, function()
actor:StopParticle(id)
end)
actor:AddParSystem(id, self.Particles, UIParticleInstance:new({
foreground = self.Foreground,
lifetime = self.Time,
transfer_to_parent = self.TransferToParent,
stop_on_transfer = self.StopEmittersOnTransfer,
halign = self.HAlign,
valign = self.VAlign,
}))
end
if self.Delay > 0 then
local delay_thread = CreateRealTimeThread(function(self, actor, target)
Sleep(self.Delay)
if actor.window_state == "open" then
create_particles(self, actor, target)
end
end, self, actor, target)
self:AssignFX(actor, target, function() DeleteThread(delay_thread) end)
else
create_particles(self, actor, target)
end
end
function ActionFXUIParticles:DestroyFX(actor, target)
local stop_fx = self:GetAssignedFX(actor, target)
if stop_fx then
stop_fx()
end
return false
end
DefineClass.ActionFXUIShaderEffect = {
__parents = {"ActionFX"},
properties = {
{ id = "EffectId", category = "FX", default = "", editor = "preset_id", preset_class = "UIFxModifierPreset" },
{ id = "GameTime", editor = false },
},
Time = -1,
fx_type = "UI Effect",
}
function ActionFXUIShaderEffect:TrackFX()
return true
end
function ActionFXUIShaderEffect:PlayFX(actor, target, action_pos, action_dir)
assert(IsKindOf(actor, "XFxModifier"))
local stop_fx = self:GetAssignedFX(actor, target)
if stop_fx then
stop_fx()
end
local old_fx_id = actor.EffectId
local play_fx_impl = function(self, actor, target)
actor:SetUIEffectModifierId(self.EffectId)
if self.Time > 0 then
CreateRealTimeThread(function(self, actor, target)
Sleep(self.Time)
self:DestroyFX(actor, target)
end, self, actor, target)
end
end
local delay_thread = false
if self.Delay > 0 then
delay_thread = CreateRealTimeThread(function(self, actor, target)
Sleep(self.Delay)
if actor.window_state == "open" then
play_fx_impl(self, actor, target)
end
end, self, actor, target)
else
play_fx_impl(self, actor, target)
end
self:AssignFX(actor, target, function()
if delay_thread then
DeleteThread(delay_thread)
end
if actor.UIEffectModifierId == self.EffectId then
actor:SetUIEffectModifierId(old_fx_id)
end
end)
end
function ActionFXUIShaderEffect:DestroyFX(actor, target)
local stop_fx = self:GetAssignedFX(actor, target)
if stop_fx then
stop_fx()
end
return false
end
--======================= Particles FX =======================
DefineClass.ActionFXParticles = {
__parents = { "ActionFX" },
properties = {
{ id = "Particles", category = "Particles", default = "", editor = "combo", items = ParticlesComboItems, buttons = {{name = "Test", func = "TestActionFXParticles"}, {name = "Edit", func = "ActionEditParticles"}}},
{ id = "Particles2", category = "Particles", default = "", editor = "combo", items = ParticlesComboItems, buttons = {{name = "Test", func = "TestActionFXParticles"}, {name = "Edit", func = "ActionEditParticles"}}},
{ id = "Particles3", category = "Particles", default = "", editor = "combo", items = ParticlesComboItems, buttons = {{name = "Test", func = "TestActionFXParticles"}, {name = "Edit", func = "ActionEditParticles"}}},
{ id = "Particles4", category = "Particles", default = "", editor = "combo", items = ParticlesComboItems, buttons = {{name = "Test", func = "TestActionFXParticles"}, {name = "Edit", func = "ActionEditParticles"}}},
{ id = "Flags", category = "Particles", default = "", editor = "dropdownlist", items = { "", "OnGround", "LockedOrientation", "Mirrored", "OnGroundTiltByGround" } },
{ id = "AlwaysVisible", category = "Particles", default = false, editor = "bool", },
{ id = "Scale", category = "Particles", default = 100, editor = "number" },
{ id = "ScaleMember", category = "Particles", default = "", editor = "text" },
{ id = "Source", category = "Placement", default = "Actor", editor = "dropdownlist", items = { "Actor", "ActorParent", "ActorOwner", "Target", "ActionPos", "Camera" }, help = "Particles source object or position" },
{ id = "SourceProp", category = "Placement", default = "", editor = "combo", items = function(fx) return ActionFXSourcePropCombo() end, help = "Source object property object" },
{ id = "Spot", category = "Placement", default = "Origin", editor = "combo", items = function(fx) return ActionFXSpotCombo(fx) end, help = "Particles source object spot" },
{ id = "SpotsPercent", category = "Placement", default = -1, editor = "number", help = "Percent of random spots that should be used. One random spot is used when the value is negative." },
{ id = "Attach", category = "Placement", default = false, editor = "bool", help = "Set true if the particles should move with the source" },
{ id = "SingleAttach", category = "Placement", default = false, editor = "bool", help = "When enabled the FX will not place a new particle on the same spot if there is already one attached there. Only valid with Attach enabled." },
{ id = "Offset", category = "Placement", default = point30, editor = "point", scale = "m", help = "Offset against source object" },
{ id = "OffsetDir", category = "Placement", default = "SourceAxisX", editor = "dropdownlist", items = function(fx) return ActionFXOrientationCombo end },
{ id = "Orientation", category = "Placement", default = "", editor = "dropdownlist", items = function(fx) return ActionFXOrientationCombo end },
{ id = "PresetOrientationAngle", category = "Placement", default = 0, editor = "number", },
{ id = "OrientationAxis", category = "Placement", default = 1, editor = "dropdownlist", items = function(fx) return OrientationAxisCombo end },
{ id = "FollowTick", category = "Particles", default = 100, editor = "number" },
{ id = "UseActorColorModifier", category = "Particles", default = false, editor = "bool", help = "If true, parsys:SetColorModifer(actor). If false, sets dynamic param 'color_modifier' to the actor's color" },
},
fx_type = "Particles",
Documentation = ActionFX.Documentation .. "\n\nThis mod item creates and places particle systems when an FX action is triggered. Inherits ActionFX. Read ActionFX first for the common properties.",
DocumentationLink = "Docs/ModItemActionFXParticles.md.html"
}
local function no_dynamic(prop, param_type)
return function(self)
local name = self[prop]
if name == "" then return true end
local params = ParGetDynamicParams(self.Particles)
local par_type = params[name]
return not par_type or par_type.type ~= param_type
end
end
local fx_particles_dynamic_params = 4
local fx_particles_dynamic_names = {}
local fx_particles_dynamic_values = {}
local fx_particles_dynamic_colors = {}
local fx_particles_dynamic_points = {}
for i = 1, fx_particles_dynamic_params do
local prop = "DynamicName"..i
fx_particles_dynamic_names[i] = prop
fx_particles_dynamic_values[i] = "DynamicValue"..i
fx_particles_dynamic_colors[i] = "DynamicColor"..i
fx_particles_dynamic_points[i] = "DynamicPoint"..i
table.insert(ActionFXParticles.properties, { id = prop, category = "Particles", name = "Name", editor = "text", default = "", read_only = true, no_edit = function(self) return self[prop] == "" end })
table.insert(ActionFXParticles.properties, { id = fx_particles_dynamic_values[i], category = "Particles", name = "Value", editor = "number", default = 1, no_edit = no_dynamic(prop, "number")})
table.insert(ActionFXParticles.properties, { id = fx_particles_dynamic_colors[i], category = "Particles", name = "Color", editor = "color", default = 0, no_edit = no_dynamic(prop, "color")})
table.insert(ActionFXParticles.properties, { id = fx_particles_dynamic_points[i], category = "Particles", name = "Point", editor = "point", default = point(0,0), no_edit = no_dynamic(prop, "point")})
end
function ActionFXParticles:OnEditorSetProperty(prop_id, old_value, ged)
ActionFX.OnEditorSetProperty(self, prop_id, old_value, ged)
if prop_id == "Particles" then
self:UpdateDynamicParams()
end
end
function ActionFXParticles:OnEditorSelect(selected, ged)
if selected then
self:UpdateDynamicParams()
end
end
function ActionFXParticles:UpdateDynamicParams()
g_DynamicParamsDefs = {}
local params = ParGetDynamicParams(self.Particles)
local n = 1
for name, desc in sorted_pairs(params) do
self[ fx_particles_dynamic_names[n] ] = name
n = n + 1
if n > fx_particles_dynamic_params then
break
end
end
for i = n, fx_particles_dynamic_params do
self[ fx_particles_dynamic_names[i] ] = nil
end
end
function ActionFXParticles:IsEternal(par)
if IsValid(par) then
return IsParticleSystemEternal(par)
elseif IsValid(par[1]) then
return IsParticleSystemEternal(par[1])
end
end
function ActionFXParticles:GetDuration(par)
if IsValid(par) then
return GetParticleSystemDuration(par)
elseif par and IsValid(par[1]) then
return GetParticleSystemDuration(par[1])
end
return 0
end
function ActionFXParticles:PlayFX(actor, target, action_pos, action_dir)
local count, obj, spot, posx, posy, posz, angle, axisx, axisy, axisz = self:GetLoc(actor, target, action_pos, action_dir)
if count == 0 then
if self.SourceProp ~= "" then
printf("FX Particles %s (id %s) has invalid source %s with property: %s", self.Particles, self.id, self.Source, self.SourceProp)
else
printf("FX Particles %s (id %s) has invalid source: %s", self.Particles, self.id, self.Source)
end
return
end
local par
if self.Delay <= 0 then
par = self:PlaceFXParticles(count, obj, spot, posx, posy, posz, angle, axisx, axisy, axisz)
if not par then
return
end
self:TrackParticle(par, actor, target, action_pos, action_dir)
if self.Time <= 0 and self:IsEternal(par) then
return
end
end
local thread = self:CreateThread(function(self, actor, target, action_pos, action_dir, count, obj, spot, posx, posy, posz, angle, axisx, axisy, axisz, par)
if self.Delay > 0 then
Sleep(self.Delay)
if self.Attach then
-- NOTE: spot here should be recalculated as the anim can change in the meanwhile
count, obj, spot, posx, posy, posz, angle, axisx, axisy, axisz = self:GetLoc(actor, target, action_pos, action_dir)
end
par = self:PlaceFXParticles(count, obj, spot, posx, posy, posz, angle, axisx, axisy, axisz)
if not par then
return
end
self:TrackParticle(par, actor, target, action_pos, action_dir)
end
if par and (self.Time > 0 or not self:IsEternal(par)) then
if self.Time > 0 then
Sleep(self.Time)
else
Sleep(self:GetDuration(par))
end
if par == self:GetAssignedFX(actor, target) then
self:AssignFX(actor, target, nil)
end
if IsValid(par) then
StopParticles(par, true)
else
for _, p in ipairs(par) do
if IsValid(p) then
StopParticles(p, true)
end
end
end
end
end, self, actor, target, action_pos, action_dir, count, obj, spot, posx, posy, posz, angle, axisx, axisy, axisz, par)
if not par and self:TrackFX() then
self:DestroyFX(actor, target)
self:AssignFX(actor, target, thread)
end
end
function ActionFXParticles:HasDynamicParams()
local params = ParGetDynamicParams(self.Particles)
if next(params) then
for i = 1, fx_particles_dynamic_params do
local name = self[ fx_particles_dynamic_names[i] ]
if name == "" then break end
if params[name] then
return true
end
end
end
end
local function IsAttachedAtSpot(att, parent, spot)
local att_spot = att:GetAttachSpot()
if att_spot == (spot or -1) then
return true
end
local att_spot_name = parent:GetSpotName(att_spot)
local spot_name = spot and parent:GetSpotName(spot) or ""
if spot_name == att_spot_name or (spot_name == "Origin" or spot_name == "") and (att_spot_name == "Origin" or att_spot_name == "") then
return true
end
return false
end
function ActionFXParticles:PlaceFXParticles(count, obj, spot, posx, posy, posz, angle, axisx, axisy, axisz)
if self.Attach and (not obj or not IsValid(obj)) then
return
end
if count == 1 then
return self:PlaceSingleFXParticles(obj, spot, posx, posy, posz, angle, axisx, axisy, axisz)
end
local par
for i = 0, count - 1 do
local p = self:PlaceSingleFXParticles(obj, unpack_params(spot, 8*i+1, 8*i+8))
if p then
par = par or {}
table.insert(par, p)
end
end
return par
end
function ActionFXParticles:PlaceSingleFXParticles(obj, spot, posx, posy, posz, angle, axisx, axisy, axisz)
local particles, particles2, particles3, particles4 = self.Particles, self.Particles2, self.Particles3, self.Particles4
local parVariations = {}
if (particles or "")~="" then table.insert(parVariations,particles) end
if (particles2 or "")~="" then table.insert(parVariations,particles2) end
if (particles3 or "")~="" then table.insert(parVariations,particles3) end
if (particles4 or "")~="" then table.insert(parVariations,particles4) end
particles = select(1,(table.rand(parVariations))) or ""
if self.Attach and self.SingleAttach then
local count = obj:CountAttaches(particles, IsAttachedAtSpot, obj, spot)
if count > 0 then
return
end
end
if DebugFXParticles and (type(DebugFXParticles) ~= "string" or IsKindOf(obj, DebugFXParticles)) then
printf('FX particles %s', particles)
end
if DebugFXParticlesName and DebugMatch(particles, DebugFXParticlesName) then
printf('FX particles %s', particles)
end
local par = PlaceParticles(particles)
if not par then return end
if self.DetailLevel >= ParticleDetailLevelMax then
par:SetImportant(true)
end
NetTempObject(par)
local scale
local scale_member = self.ScaleMember
if scale_member ~= "" and obj and IsValid(obj) and obj:HasMember(scale_member) then
scale = obj[scale_member]
if scale and type(scale) == "function" then
scale = scale(obj)
end
end
scale = scale or self.Scale
if scale ~= 100 then
par:SetScale(scale)
end
local flags = self.Flags
if flags == "Mirrored" then
par:SetMirrored(true)
elseif flags == "LockedOrientation" then
par:SetGameFlags(const.gofLockedOrientation)
elseif flags == "OnGround" or flags == "OnGroundTiltByGround" then
par:SetGameFlags(const.gofAttachedOnGround)
end
-- fill in dynamic parameters
local dynamic_params = ParGetDynamicParams(particles)
if next(dynamic_params) then
for i = 1, fx_particles_dynamic_params do
local name = self[ fx_particles_dynamic_names[i] ]
if name == "" then break end
local def = dynamic_params[name]
if def then
if def.type == "color" then
par:SetParamDef(def, self:GetProperty(fx_particles_dynamic_colors[i]))
elseif def.type == "point" then
par:SetParamDef(def, self:GetProperty(fx_particles_dynamic_points[i]))
else
par:SetParamDef(def, self:GetProperty(fx_particles_dynamic_values[i]))
end
end
end
end
if self.AlwaysVisible then
local obj_iter = obj or par
while true do
local parent = obj_iter:GetParent()
if not parent then
obj_iter:SetGameFlags(const.gofAlwaysRenderable)
break
end
obj_iter = parent
end
end
if obj then
if self.UseActorColorModifier then
par:SetColorModifier(obj:GetColorModifier())
else
local def = dynamic_params["color_modifier"]
if def then
par:SetParamDef(def, obj:GetColorModifier())
end
end
end
FXOrient(par, posx, posy, posz, obj, spot, self.Attach, axisx, axisy, axisz, angle, self.Offset)
return par
end
function ActionFXParticles:TrackParticle(par, actor, target, action_pos, action_dir)
if self:TrackFX() then
self:AssignFX(actor, target, par)
end
if self.Behavior ~= "" and self.BehaviorMoment == "" then
self[self.Behavior](self, actor, target, action_pos, action_dir)
end
end
function ActionFXParticles:DestroyFX(actor, target)
local fx = self:AssignFX(actor, target, nil)
if not fx then
return
elseif IsValidThread(fx) then
DeleteThread(fx)
elseif IsValid(fx) then
StopParticles(fx)
elseif type(fx) == "table" and not getmetatable(fx) then
for i = 1, #fx do
local p = fx[i]
if IsValid(p) then
StopParticles(p)
end
end
end
end
function ActionFXParticles:BehaviorDetach(actor, target)
local fx = self:GetAssignedFX(actor, target)
if not fx then
return
elseif IsValidThread(fx) then
printf("FX Particles %s Detach Behavior can not be run before particle placing", self.Particles, self.Delay)
elseif IsValid(fx) then
PreciseDetachObj(fx)
elseif type(fx) == "table" and not getmetatable(fx) then
for i = 1, #fx do
local p = fx[i]
if IsValid(p) then
PreciseDetachObj(p)
end
end
end
end
function ActionFXParticles:BehaviorDetachAndDestroy(actor, target)
local fx = self:AssignFX(actor, target, nil)
if not fx then
return
elseif IsValidThread(fx) then
DeleteThread(fx)
elseif IsValid(fx) then
PreciseDetachObj(fx)
StopParticles(fx)
elseif type(fx) == "table" and not getmetatable(fx) then
for i = 1, #fx do
local p = fx[i]
if IsValid(p) then
PreciseDetachObj(p)
StopParticles(p)
end
end
end
end
function ActionFXParticles:BehaviorFollow(actor, target, action_pos, action_dir)
local fx = self:GetAssignedFX(actor, target)
if not fx then return end
local obj = self:GetLocObj(actor, target)
if not obj then
printf("FX Particles %s uses unsupported behavior/source combination: %s/%s", self.Particles, self.Behavior, self.Source)
return
end
self:CreateThread(function(self, fx, actor, target, obj, tick)
while IsValid(obj) and IsValid(fx) and self:GetAssignedFX(actor, target) == fx do
local x, y, z = obj:GetSpotLocPosXYZ(-1)
fx:SetPos(x, y, z, tick)
Sleep(tick)
end
end, self, fx, actor, target, obj, self.FollowTick)
end
function ActionEditParticles(editor_obj, fx, prop_id)
EditParticleSystem(fx.Particles)
end
function TestActionFXParticles(editor_obj, fx, prop_id)
TestActionFXObjectEnd()
local obj = PlaceParticles(fx.Particles)
if not obj then
return
end
LastTestActionFXObject = obj
obj:SetScale(fx.Scale)
if fx.Flags == "Mirrored" then
obj:SetMirrored(true)
elseif fx.Flags == "OnGround" then
obj:SetGameFlags(const.gofAttachedOnGround)
end
-- fill in dynamic parameters
local params = ParGetDynamicParams(fx.Particles)
if next(params) then
for i = 1, fx_particles_dynamic_params do
local name = fx[ fx_particles_dynamic_names[i] ]
if name == "" then break end
if params[name] then
local prop = (params[name].type == "color") and "DynamicColor" or "DynamicValue"
local value = fx:GetProperty(prop .. i)
obj:SetParam(name, value)
end
end
end
local eye_pos, look_at
if camera3p.IsActive() then
eye_pos, look_at = camera.GetEye(), camera3p.GetLookAt()
elseif cameraMax.IsActive() then
eye_pos, look_at = cameraMax.GetPosLookAt()
else
look_at = GetTerrainGamepadCursor()
end
local posx, posy, posz = look_at:xyz()
FXOrient(obj, posx, posy, posz)
editor_obj:CreateThread(function(obj)
Sleep(5000)
StopParticles(obj, true)
TestActionFXObjectEnd(obj)
end, obj)
end
--============================= Camera Shake FX =======================
DefineClass.ActionFXCameraShake = {
__parents = { "ActionFX" },
properties = {
{ id = "Preset", category = "Camera Shake", default = "Custom", editor = "dropdownlist",
items = function(self) return table.keys2(self.presets) end, buttons = {{ name = "Test", func = "TestActionFXCameraShake" }},
},
{ id = "Duration", category = "Camera Shake", default = 700, editor = "number", min = 100, max = 2000, slider = true, },
{ id = "Frequency", category = "Camera Shake", default = 25, editor = "number", min = 1, max = 100, slider = true, },
{ id = "ShakeOffset", category = "Camera Shake", default = 30*guic, editor = "number", min = 1*guic, max = 100*guic, slider = true, scale = "cm", },
{ id = "RollAngle", category = "Camera Shake", default = 0, editor = "number", min = 0, max = 30, slider = true, },
{ id = "Source", category = "Camera Shake", default = "Actor", editor = "dropdownlist", items = { "Actor", "Target", "ActionPos" }, help = "Shake position or object position" },
{ id = "Spot", category = "Camera Shake", default = "Origin", editor = "combo", items = function(fx) return ActionFXSpotCombo(fx) end, help = "Shake position object spot" },
{ id = "Offset", category = "Camera Shake", default = point30, editor = "point", scale = "m", help = "Shake position offset" },
{ id = "ShakeRadiusInSight", category = "Camera Shake", default = const.ShakeRadiusInSight, editor = "number", scale = "m",
name = "Fade radius (in sight)", help = "The distance from the source at which the camera shake fades out completely, if the source is in the camera view",
},
{ id = "ShakeRadiusOutOfSight", category = "Camera Shake", default = const.ShakeRadiusOutOfSight, editor = "number", scale = "m",
name = "Fade radius (out of sight)", help = "The distance from the source at which the camera shake fades out completely, if the source is out of the camera view",
},
{ id = "Time", editor = false },
{ id = "Behavior", editor = false },
{ id = "BehaviorMoment", editor = false },
},
presets = {
Custom = {},
Light = { Duration = 380, Frequency = 25, ShakeOffset = 6*guic, RollAngle = 3 },
Medium = { Duration = 460, Frequency = 25, ShakeOffset = 12*guic, RollAngle = 6 },
Strong = { Duration = 950, Frequency = 25, ShakeOffset = 15*guic, RollAngle = 9 },
},
fx_type = "Camera Shake",
}
function ActionFXCameraShake:PlayFX(actor, target, action_pos, action_dir)
if IsEditorActive() or EngineOptions.CameraShake == "Off" then return end
local count, obj, spot, posx, posy, posz = self:GetLoc(actor, target, action_pos, action_dir)
if count == 0 then
printf("FX Camera Shake has invalid source: %s", self.Source)
return
end
local power
if obj then
if NetIsRemote(obj) then
return -- camera shake FX is not applied for remote objects
end
if camera3p.IsActive() and camera3p.IsAttachedToObject(obj:GetParent() or obj) then
power = 100
end
end
power = power or posx and CameraShake_GetEffectPower(point(posx, posy, posz or const.InvalidZ), self.ShakeRadiusInSight, self.ShakeRadiusOutOfSight) or 0
if power == 0 then
return
end
if self.Delay <= 0 then
self:Shake(actor, target, power)
return
end
local thread = self:CreateThread(function(self, actor, target, power)
Sleep(self.Delay)
self:Shake(actor, target, power)
end, self, actor, target, power)
if self:TrackFX() then
self:DestroyFX(actor, target)
self:AssignFX(actor, target, thread)
end
end
function ActionFXCameraShake:DestroyFX(actor, target)
local fx = self:AssignFX(actor, target, nil)
if not fx then
return
elseif IsValidThread(fx) then
DeleteThread(fx)
local preset = self.presets[self.Preset]
local frequency = preset and preset.Frequency or self.Frequency
local shake_duration = frequency > 0 and Min(frequency, 200) or 0
camera.ShakeStop(shake_duration)
end
end
function ActionFXCameraShake:Shake(actor, target, power)
local preset = self.presets[self.Preset]
local duration = self.Duration >= 0 and (preset and preset.Duration or self.Duration) * power / 100 or -1
local frequency = preset and preset.Frequency or self.Frequency
if frequency <= 0 then return end
local shake_offset = (preset and preset.ShakeOffset or self.ShakeOffset) * power / 100
local shake_roll = (preset and preset.RollAngle or self.RollAngle) * power / 100
camera.Shake(duration, frequency, shake_offset, shake_roll)
if self:TrackFX() then
self:AssignFX(actor, target, camera3p_shake_thread )
end
end
function ActionFXCameraShake:SetPreset(value)
self.Preset = value
local preset = self.presets[self.Preset]
self.Duration = preset and preset.Duration or self.Duration
self.Frequency = preset and preset.Frequency or self.Frequency
self.ShakeOffset = preset and preset.ShakeOffset or self.ShakeOffset
self.RollAngle = preset and preset.RollAngle or self.RollAngle
end
function ActionFXCameraShake:OnEditorSetProperty(prop_id, old_value, ged)
ActionFX.OnEditorSetProperty(self, prop_id, old_value, ged)
if self.Preset ~= "Custom" and (prop_id == "Duration" or prop_id == "Frequency" or prop_id == "ShakeOffset" or prop_id == "RollAngle") then
local preset = self.presets[self.Preset]
if preset and preset[prop_id] and preset[prop_id] ~= self[prop_id] then
self.Preset = "Custom"
end
end
end
function TestActionFXCameraShake(editor_obj, fx, prop_id)
local preset = fx.presets[fx.Preset]
local duration = preset and preset.Duration or fx.Duration
local frequency = preset and preset.Frequency or fx.Frequency
local shake_offset = preset and preset.ShakeOffset or fx.ShakeOffset
local shake_roll = preset and preset.RollAngle or fx.RollAngle
camera.Shake(duration, frequency, shake_offset, shake_roll)
end
--============================= Radial Blur =======================
DefineClass.ActionFXRadialBlur = {
__parents = { "ActionFX" },
properties = {
{ id = "Strength", category = "Radial Blur", default = 300, editor = "number", buttons = {{name = "Test", func = "TestActionFXRadialBlur"}}},
{ id = "Duration", category = "Radial Blur", default = 800, editor = "number" },
{ id = "FadeIn", category = "Radial Blur", default = 30, editor = "number" },
{ id = "FadeOut", category = "Radial Blur", default = 350, editor = "number" },
{ id = "Source", category = "Placement", default = "Actor", editor = "dropdownlist", items = { "Actor", "ActorParent", "ActorOwner", "Target", "ActionPos" }, help = "Radial Blur position" },
},
fx_type = "Radial Blur",
}
if FirstLoad then
RadialBlurThread = false
g_RadiualBlurIsPaused = false
g_RadiualBlurPauseReasons = {}
end
--blatant copy paste from Pause(reason)
function PauseRadialBlur(reason)
reason = reason or false
if next(g_RadiualBlurPauseReasons) == nil then
g_RadiualBlurIsPaused = true
SetPostProcPredicate( "radial_blur", false )
g_RadiualBlurPauseReasons[reason] = true
else
g_RadiualBlurPauseReasons[reason] = true
end
end
function ResumeRadialBlur(reason)
reason = reason or false
if g_RadiualBlurPauseReasons[reason] ~= nil then
g_RadiualBlurPauseReasons[reason] = nil
if next(g_RadiualBlurPauseReasons) == nil then
g_RadiualBlurIsPaused = false
end
end
end
function OnMsg.DoneMap()
DeleteThread(RadialBlurThread)
RadialBlurThread = false
hr.RadialBlurStrength = 0
SetPostProcPredicate( "radial_blur", false )
end
function RadialBlur( duration, fadein, fadeout, strength )
DeleteThread(RadialBlurThread)
RadialBlurThread = self:CreateThread( function(duration, fadein, fadeout, strength)
SetPostProcPredicate( "radial_blur", not g_RadiualBlurIsPaused )
local time_step = 5
local t = 0
while t < fadein do
hr.RadialBlurStrength = strength * t / fadein
Sleep(time_step)
t = t + time_step
end
if t < duration - fadeout then
hr.RadialBlurStrength = strength
Sleep(duration - fadeout - t)
t = duration - fadeout
end
while t < duration do
hr.RadialBlurStrength = strength * (duration - t) / fadeout
Sleep(time_step)
t = t + time_step
end
hr.RadialBlurStrength = 0
SetPostProcPredicate( "radial_blur", false )
RadialBlurThread = false
end, duration, fadein, fadeout, strength)
end
function ActionFXRadialBlur:TrackFX()
return (self.behaviors or self.Time > 0) and true or false
end
function ActionFXRadialBlur:PlayFX(actor, target, action_pos, action_dir)
local count, obj, spot, posx, posy, posz, angle, axisx, axisy, axisz = self:GetLoc(actor, target, action_pos, action_dir)
if count == 0 then
printf("FX Radial Blur has invalid source: %s", self.Source)
return
end
if NetIsRemote(obj) then
return -- radial blur FX is not applied for remote objects
end
if self.Delay <= 0 then
RadialBlur(self.Duration, self.FadeIn, self.FadeOut, self.Strength)
if self:TrackFX() then
self:AssignFX(actor, target, RadialBlurThread)
end
else
self:CreateThread(function(self, actor, target)
Sleep(self.Delay)
RadialBlur(self.Duration, self.FadeIn, self.FadeOut, self.Strength)
if self:TrackFX() then
self:AssignFX(actor, target, RadialBlurThread)
end
end, self, actor, target)
end
end
function ActionFXRadialBlur:DestroyFX(actor, target)
local fx = self:AssignFX(actor, target, nil)
if not fx or fx ~= RadialBlurThread then
return
end
DeleteThread(RadialBlurThread)
RadialBlurThread = false
hr.RadialBlurStrength = 0
SetPostProcPredicate( "radial_blur", false )
end
function TestActionFXRadialBlur(editor_obj, fx, prop_id)
RadialBlur(fx.Duration, fx.FadeIn, fx.FadeOut, fx.Strength)
end
local function ActionFXObjectCombo(o)
local list = ClassDescendantsList("CObject", function (name, class)
return IsValidEntity(class:GetEntity()) or class.fx_spawn_enable
end)
table.sort(list, CmpLower)
return list
end
local function ActionFXObjectAnimationCombo(o)
local cls = g_Classes[o.Object]
local entity = cls and cls:GetEntity()
local list
if IsValidEntity(entity) then
list = GetStates(entity)
else
list = {"idle"} -- GetClassDescendantsStates("CObject")
end
table.sort(list, CmpLower)
return list
end
local function ActionFXObjectAnimationHelp(o)
local cls = g_Classes[o.Object]
local entity = cls and cls:GetEntity()
if IsValidEntity(entity) then
local help = {}
help[#help+1] = entity
local anim = o.Animation
if anim ~= "" and HasState(entity, anim) and not IsErrorState(entity, anim) then
help[#help+1] = "Duration: " .. GetAnimDuration(entity, anim)
local moments = GetStateMoments(entity, anim)
if #moments > 0 then
help[#help+1] = "Moments:"
for i = 1, #moments do
help[#help+1] = string.format(" %s = %d", moments[i].type, moments[i].time)
end
else
help[#help+1] = "No Moments"
end
end
return table.concat(help, "\n")
end
return ""
end
--============================= Object FX =======================
DefineClass.ActionFXObject = {
__parents = { "ActionFX", "ColorizableObject" },
properties = {
{ id = "AnimationLoops", category = "Lifetime", default = 0, editor = "number", help = "Additional time" },
{ id = "Object", name = "Object1", category = "Object", default = "", editor = "combo", items = function(fx) return ActionFXObjectCombo(fx) end, buttons = {{name = "Test", func = "TestActionFXObject"}}},
{ id = "Object2", category = "Object", default = "", editor = "combo", items = function(fx) return ActionFXObjectCombo(fx) end, buttons = {{name = "Test", func = "TestActionFXObject"}}},
{ id = "Object3", category = "Object", default = "", editor = "combo", items = function(fx) return ActionFXObjectCombo(fx) end, buttons = {{name = "Test", func = "TestActionFXObject"}}},
{ id = "Object4", category = "Object", default = "", editor = "combo", items = function(fx) return ActionFXObjectCombo(fx) end, buttons = {{name = "Test", func = "TestActionFXObject"}}},
{ id = "Animation", category = "Object", default = "idle", editor = "combo", items = function(fx) return ActionFXObjectAnimationCombo(fx) end, help = ActionFXObjectAnimationHelp },
{ id = "AnimationPhase", category = "Object", default = 0, editor = "number" },
{ id = "FadeIn", category = "Object", default = 0, editor = "number", help = "Included in the overall time" },
{ id = "FadeOut", category = "Object", default = 0, editor = "number", help = "Included in the overall time" },
{ id = "Flags", category = "Object", default = "", editor = "dropdownlist", items = { "", "OnGround", "LockedOrientation", "Mirrored", "OnGroundTiltByGround", "SyncWithParent" } },
{ id = "Scale", category = "Object", default = 100, editor = "number" },
{ id = "ScaleMember", category = "Object", default = "", editor = "text" },
{ id = "Opacity", category = "Object", default = 100, editor = "number", min = 0, max = 100, slider = true },
{ id = "ColorModifier", category = "Object", editor = "color", default = RGBA(100, 100, 100, 0), buttons = {{name = "Reset", func = "ResetColorModifier"}}},
{ id = "UseActorColorization", category = "Object", default = false, editor = "bool" },
{ id = "Source", category = "Placement", default = "Actor", editor = "dropdownlist", items = { "Actor", "ActorParent", "ActorOwner", "Target", "ActionPos" } },
{ id = "Spot", category = "Placement", default = "Origin", editor = "combo", items = function(fx) return ActionFXSpotCombo(fx) end },
{ id = "Attach", category = "Placement", default = false, editor = "bool", help = "Set true if the object should move with the source" },
{ id = "Offset", category = "Placement", default = point30, editor = "point", scale = "m" },
{ id = "OffsetDir", category = "Placement", default = "SourceAxisX", editor = "dropdownlist", items = function(fx) return ActionFXOrientationCombo end },
{ id = "Orientation", category = "Placement", default = "", editor = "dropdownlist", items = function(fx) return ActionFXOrientationCombo end },
{ id = "PresetOrientationAngle", category = "Placement", default = 0, editor = "number", },
{ id = "OrientationAxis", category = "Placement", default = 1, editor = "dropdownlist", items = function() return OrientationAxisCombo end },
{ id = "AlwaysVisible", category = "Object", default = false, editor = "bool", },
{ id = "anim_type", name = "Pick frame by", editor = "choice", items = function() return AnimatedTextureObjectTypes end, default = 0, help = "UV Scroll Animation playback type" },
{ id = "anim_speed", name = "Speed Multiplier", editor = "number", max = 4095, min = 0, default = 1000, help = "UV Scroll Animation playback speed" },
{ id = "sequence_time_remap", name = "Sequence time", editor = "curve4", max = 63, scale = 63, max_x = 15, scale_x = 15, default = MakeLine(0, 63, 15), help = "UV Scroll Animation playback time curve" },
{ id = "SortPriority", category = "Object", default = 0, editor = "number", min = -4, max = 3, no_edit = function(o) return not IsKindOf(rawget(_G, o.Object), "Decal") end },
},
fx_type = "Object",
variations_props = { "Object", "Object2", "Object3", "Object4" },
DocumentationLink = "Docs/ModItemActionFXObject.md.html",
Documentation = ActionFX.Documentation .. "\n\nThis mod item creates and places an object when an FX action is triggered. Inherits ActionFX. Read ActionFX first for the common properties."
}
function ActionFXObject:SetObject(value)
self.Object = value
local cls = g_Classes[self.Object]
local entity = cls and cls:GetEntity()
local anim = self.Animation
if (entity or "") == "" or not IsValidEntity(entity) or not HasState(entity, anim) or IsErrorState(entity, anim) then
anim = "idle"
end
self.Animation = anim
end
function ActionFXObject:PlayFX(actor, target, action_pos, action_dir)
local count, obj, spot, posx, posy, posz, angle, axisx, axisy, axisz = self:GetLoc(actor, target, action_pos, action_dir)
if count == 0 then
printf("FX Object %s has invalid source: %s", self.Object, self.Source)
return
end
local fx, wait_anim, wait_time, duration
if obj and self.Flags == "SyncWithParent" and self.AnimationPhase > obj:GetAnimPhase() then
wait_anim = obj:GetAnim(1)
wait_time = obj:TimeToPhase(1, self.AnimationPhase)
end
if self.Delay <= 0 and (wait_time or 0) <= 0 then
fx = self:PlaceFXObject(count, obj, spot, posx, posy, posz, angle, axisx, axisy, axisz, action_pos, action_dir)
if not fx then return end
self:TrackObject(fx, actor, target, action_pos, action_dir)
duration = self.Time + self.AnimationLoops * fx:GetAnimDuration()
if duration <= 0 then
return
end
end
local thread = self:CreateThread(function(self, fx, wait_anim, wait_time, duration, actor, target, count, obj, spot, posx, posy, posz, angle, axisx, axisy, axisz, action_pos, action_dir)
if self.Delay > 0 then
Sleep(self.Delay)
end
if wait_time and IsValid(obj) and obj:GetAnim(1) == wait_anim then
if not obj:IsKindOf("StateObject") then
Sleep(wait_time)
elseif not obj:WaitPhase(self.AnimationPhase) then
return
end
if not IsValid(obj) or obj:GetAnim(1) ~= wait_anim then
return
end
end
if not fx then
fx = self:PlaceFXObject(count, obj, spot, posx, posy, posz, angle, axisx, axisy, axisz, action_pos, action_dir)
if not fx then return end
self:TrackObject(fx, actor, target, action_pos, action_dir)
duration = self.Time + self.AnimationLoops * fx:GetAnimDuration()
if duration <= 0 then
return
end
end
local fadeout = self.FadeOut > 0 and Min(duration, self.FadeOut) or 0
Sleep(duration-fadeout)
if not IsValid(fx) then return end
if fx == self:GetAssignedFX(actor, target) then
self:AssignFX(actor, target, nil)
end
if fadeout > 0 then
if fx:GetOpacity() > 0 then
fx:SetOpacity(0, fadeout)
end
Sleep(fadeout)
end
DoneObject(fx)
end, self, fx, wait_anim, wait_time, duration, actor, target, count, obj, spot, posx, posy, posz, angle, axisx, axisy, axisz, action_pos, action_dir)
if not fx and self:TrackFX() then
self:DestroyFX(actor, target)
self:AssignFX(actor, target, thread)
end
end
function ActionFXObject:GetMaxColorizationMaterials()
return const.MaxColorizationMaterials
end
function ActionFXObject:PlaceFXObject(count, obj, spot, posx, posy, posz, angle, axisx, axisy, axisz)
if self.Attach and (not obj or not IsValid(obj)) then
return
end
if count == 1 then
return self:PlaceSingleFXObject(obj, spot, posx, posy, posz, angle, axisx, axisy, axisz)
end
local list
for i = 0, count - 1 do
local o = self:PlaceSingleFXObject(obj, unpack_params(spot, 8*i+1, 8*i+8))
if o then
list = list or {}
table.insert(list, o)
end
end
return list
end
function ActionFXObject:CreateSingleFXObject(components)
local name = self:GetVariation(self.variations_props)
return PlaceObject(name, nil, components)
end
function ActionFXObject:PlaceSingleFXObject(obj, spot, posx, posy, posz, angle, axisx, axisy, axisz)
local components = const.cofComponentAnim | const.cofComponentColorizationMaterial
if obj and self.Attach then
components = components | const.cofComponentAttach
end
if self.FadeIn > 0 or self.FadeOut > 0 then
components = components | const.cofComponentInterpolation
end
local fx = self:CreateSingleFXObject(components)
if not fx then
return
end
NetTempObject(fx)
fx:SetColorModifier(self.ColorModifier)
local fx_scm = fx.SetColorizationMaterial
local color_src = self.UseActorColorization and obj and obj:GetMaxColorizationMaterials() > 0 and obj or self
fx_scm(fx, 1, color_src:GetEditableColor1(), color_src:GetEditableRoughness1(), color_src:GetEditableMetallic1())
fx_scm(fx, 2, color_src:GetEditableColor2(), color_src:GetEditableRoughness2(), color_src:GetEditableMetallic2())
fx_scm(fx, 3, color_src:GetEditableColor3(), color_src:GetEditableRoughness3(), color_src:GetEditableMetallic3())
local scale
local scale_member = self.ScaleMember
if scale_member ~= "" and IsValid(obj) and obj:HasMember(scale_member) then
scale = obj[scale_member]
if type(scale) == "function" then
scale = scale(obj)
if type(scale) ~= "number" then
assert(false, "invalid return value from ScaleMember function, scale will be set to 100")
scale = 100
end
end
end
scale = scale or self.Scale
fx:SetScale(scale)
fx:SetState(self.Animation, 0, 0)
if self.Flags == "OnGroundTiltByGround" then
fx:SetAnim(1, self.Animation, const.eOnGround + const.eTiltByGround, 0)
end
fx:SetAnimPhase(1, self.AnimationPhase)
if self.Flags == "Mirrored" then
fx:SetMirrored(true)
elseif self.Flags == "LockedOrientation" then
fx:SetGameFlags(const.gofLockedOrientation)
elseif self.Flags == "OnGround" or self.Flags == "OnGroundTiltByGround" then
fx:SetGameFlags(const.gofAttachedOnGround)
elseif self.Flags == "SyncWithParent" then
fx:SetGameFlags(const.gofSyncState)
end
if self.AlwaysVisible then
fx:SetGameFlags(const.gofAlwaysRenderable)
end
if not self.GameTime or self.Attach and obj:GetGameFlags(const.gofRealTimeAnim) ~= 0 then
fx:SetGameFlags(const.gofRealTimeAnim)
end
if self.FadeIn > 0 then
fx:SetOpacity(0)
fx:SetOpacity(self.Opacity, self.FadeIn)
else
fx:SetOpacity(self.Opacity)
end
if self.SortPriority ~= 0 and fx:IsKindOf("Decal") then
fx:Setsort_priority(self.SortPriority)
end
FXOrient(fx, posx, posy, posz, obj, spot, self.Attach, axisx, axisy, axisz, angle, self.Offset)
if IsKindOf(fx, "AnimatedTextureObject") then
fx:Setanim_speed(self.anim_speed)
fx:Setanim_type(self.anim_type)
fx:Setsequence_time_remap(self.sequence_time_remap)
end
return fx
end
function ActionFXObject:TrackObject(fx, actor, target, action_pos, action_dir)
if self:TrackFX() then
self:AssignFX(actor, target, fx)
end
if self.Behavior ~= "" and self.BehaviorMoment == "" then
self[self.Behavior](self, actor, target, action_pos, action_dir)
end
end
function ActionFXObject:DestroyFX(actor, target)
local fx = self:AssignFX(actor, target, nil)
if not fx then
return
elseif IsValidThread(fx) then
DeleteThread(fx)
elseif IsValid(fx) then
local fadeout = self.FadeOut
if fadeout <= 0 then
DoneObject(fx)
else
fx:SetOpacity(0, fadeout)
self:CreateThread(function(self, fx)
Sleep(self.FadeOut)
DoneObject(fx)
end, self, fx)
end
elseif type(fx) == "table" and not getmetatable(fx) then
local fadeout = self.FadeOut
if fadeout <= 0 then
DoneObjects(fx)
else
for _, o in ipairs(fx) do
if IsValid(o) then
o:SetOpacity(0, fadeout)
end
end
self:CreateThread(function(self, fx)
Sleep(self.FadeOut)
DoneObjects(fx)
end, self, fx)
end
end
end
function ActionFXObject:BehaviorDetach(actor, target)
local fx = self:GetAssignedFX(actor, target)
if not fx then
return
elseif IsValid(fx) then
PreciseDetachObj(fx)
elseif IsValidThread(fx) then
printf("FX Object %s Detach Behavior can not be run before the object is placed (Delay %d is very large)", self.Object, self.Delay)
end
end
DefineClass.ActionFXPassTypeObject = {
__parents = { "ActionFXObject" },
properties = {
{ id = "Object", editor = false, default = "PassTypeMarker", },
{ id = "Object2", editor = false, },
{ id = "Object3", editor = false, },
{ id = "Object4", editor = false, },
{ id = "Chance", editor = false, },
{ category = "Pass Type", id = "pass_type_radius", name = "Pass Radius", editor = "number", default = 0, scale = "m" },
{ category = "Pass Type", id = "pass_type_name", name = "Pass Type", editor = "choice", default = false, items = function() return PassTypesCombo end, },
},
fx_type = "Pass Type Object",
Chance = 100,
}
function ActionFXPassTypeObject:CreateSingleFXObject(components)
return PlaceObject(self.Object, {
PassTypeRadius = self.pass_type_radius,
PassTypeName = self.pass_type_name,
}, components)
end
function ActionFXPassTypeObject:PlaceSingleFXObject(obj, spot, posx, posy, posz, angle, axisx, axisy, axisz)
assert(not IsAsyncCode() or IsEditorActive())
local pass_type_fx = ActionFXObject.PlaceSingleFXObject(self, obj, spot, posx, posy, posz, angle, axisx, axisy, axisz)
if not pass_type_fx then return end
if not pass_type_fx:IsValidPos() then
DoneObject(pass_type_fx)
return
end
local x, y, z = pass_type_fx:GetPosXYZ()
local max_below = guim
local max_above = guim
z = terrain.FindPassableZ(pass_type_fx, 0, max_below, max_above)
pass_type_fx:MakeSync()
pass_type_fx:SetPos(x, y, z)
pass_type_fx:SetCostRadius()
return pass_type_fx
end
function TestActionFXObject(editor_obj, fx, prop_id)
TestActionFXObjectEnd()
local obj = PlaceObject(fx.Object)
if not obj then
return
end
LastTestActionFXObject = obj
obj:SetScale(fx.Scale)
obj:SetState(fx.Animation, 0, 0)
if fx.Orientation == "OnGroundTiltByGround" then
obj:SetAnim(1, fx.Animation, const.eOnGround + const.eTiltByGround, 0)
end
obj:SetAnimPhase(1, fx.AnimationPhase)
if fx.Flags == "Mirrored" then
obj:SetMirrored(true)
elseif fx.Flags == "OnGround" or fx.Flags == "OnGroundTiltByGround" then
obj:SetGameFlags(const.gofAttachedOnGround)
end
if fx.FadeIn > 0 then
obj:SetOpacity(0)
obj:SetOpacity(100, fx.FadeIn)
end
local time = fx.Time > 0 and fx.Time or 0
if fx.AnimationLoops > 0 then
time = time + fx.AnimationLoops * obj:GetAnimDuration()
end
local eye_pos, look_at
if camera3p.IsActive() then
eye_pos, look_at = camera.GetEye(), camera3p.GetLookAt()
elseif cameraMax.IsActive() then
eye_pos, look_at = cameraMax.GetPosLookAt()
else
look_at = GetTerrainGamepadCursor()
end
local posx, posy, posz = look_at:xyz()
FXOrient(obj, posx, posy, posz)
if time <= 0 then time = fx.FadeIn + fx.FadeOut + 2000 end
fx:CreateThread(function(fx, obj, time)
if fx.FadeOut > 0 then
local t = Min(time, fx.FadeOut)
Sleep(time-t)
if IsValid(obj) and t > 0 then
obj:SetOpacity(0, t)
Sleep(t)
end
else
Sleep(time)
end
TestActionFXObjectEnd(obj)
end, fx, obj, time)
end
-------- Backwards compat ----------
DefineClass.ActionFXDecal = {
__parents = { "ActionFXObject" },
fx_type = "Decal",
DocumentationLink = "Docs/ModItemActionFXDecal.md.html",
Documentation = ActionFX.Documentation .. "\n\nPlaces a decal when an FX action is triggered. Inherits ActionFX. Read ActionFX first for the common properties."
}
--============================= FX Controller Rumble =======================
local function AddValuesInComboTexts(values)
local list = {}
for k, v in pairs(values) do
list[#list+1] = k
end
table.sort(list, function(a,b) return values[a] < values[b] end)
local res = {}
for i = 1, #list do
list[i] = { text = string.format("%s : %d", list[i], values[list[i]]), value = list[i] }
end
return list
end
DefineClass.ActionFXControllerRumble = {
__parents = { "ActionFX" },
properties = {
{ id = "Power", category = "Vibration", default = "Medium", editor = "combo", items = function(fx) return AddValuesInComboTexts(fx.powers) end, help = "Controller left and right motors speed", buttons = {{name = "Test", func = "TestActionFXControllerRumble"}}},
{ id = "Duration", category = "Vibration", default = "Medium", editor = "combo", items = function(fx) return AddValuesInComboTexts(fx.durations) end, help = "Vibration duration in game time" },
{ id = "Controller", category = "Vibration", default = "Actor", editor = "dropdownlist", items = { "Actor", "Target" }, help = "Whose controller should vibrate" },
{ id = "GameTime", editor = false },
},
powers = {
Slight = 6000,
Light = 16000,
Medium = 24000,
FullSpeed = 65535,
},
durations = {
Short = 125,
Medium = 230,
},
fx_type = "Controller Rumble",
}
if FirstLoad then
ControllerRumbleThreads = {}
end
local function StopControllersRumble()
for i = #ControllerRumbleThreads, 0, -1 do
if ControllerRumbleThreads[i] then
DeleteThread(ControllerRumbleThreads[i])
ControllerRumbleThreads[i] = nil
XInput.SetRumble(i, 0, 0)
end
end
end
OnMsg.MsgPreControllersAssign = StopControllersRumble
OnMsg.DoneMap = StopControllersRumble
OnMsg.Pause = StopControllersRumble
function ControllerRumble(controller_id, duration, power_left, power_right)
if not GetAccountStorageOptionValue("ControllerRumble") or not duration or duration <= 0 then
power_left = 0
power_right = 0
end
XInput.SetRumble(controller_id, power_left, power_right)
DeleteThread(ControllerRumbleThreads[controller_id])
ControllerRumbleThreads[controller_id] = nil
if power_left > 0 or power_right > 0 then
ControllerRumbleThreads[controller_id] = CreateRealTimeThread(function(controller_id, duration)
Sleep(duration or 230)
XInput.SetRumble(controller_id, 0, 0)
ControllerRumbleThreads[controller_id] = nil
end, controller_id, duration)
end
end
function ActionFXControllerRumble:PlayFX(actor, target, action_pos, action_dir)
local obj
if self.Controller == "Actor" then
obj = IsValid(actor) and GetTopmostParent(actor)
elseif self.Controller == "Target" then
obj = IsValid(target) and target
end
if not obj then
printf("FX Rumble controller invalid source %s", self.Controller)
return
end
local controller_id
for loc_player = 1, LocalPlayersCount do
if obj == GetLocalHero(loc_player) or obj == PlayerControlObjects[loc_player] then
controller_id = GetActiveXboxControllerId(loc_player)
break
end
end
if controller_id then
self:VibrateController(controller_id, actor, target, action_pos, action_dir)
end
end
function ActionFXControllerRumble:VibrateController(controller_id, ...)
if self.Behavior ~= "" and self.BehaviorMoment == "" then
self[self.Behavior](self, ...)
else
local power = self.powers[self.Power] or tonumber(self.Power)
local duration = self.durations[self.Duration] or tonumber(self.Duration)
ControllerRumble(controller_id, duration, power, power)
end
end
function TestActionFXControllerRumble(editor_obj, fx, prop_id)
fx:VibrateController(0, "Test")
end
--============================= FX Light =======================
DefineClass.ActionFXLight = {
__parents = { "ActionFX" },
properties = {
{ category = "Light", id = "Type", editor = "combo", default = "PointLight", items = { "PointLight", "PointLightFlicker", "SpotLight", "SpotLightFlicker" } },
{ category = "Light", id = "CastShadows", editor = "bool", default = false, },
{ category = "Light", id = "DetailedShadows", editor = "bool", default = false, },
{ category = "Light", id = "Color", editor = "color", default = RGB(255,255,255), buttons = {{name = "Test", func = "TestActionFXLight"}}, no_edit = function(self) return self.Type ~= "PointLight" and self.Type ~= "SpotLight" end },
{ category = "Light", id = "Intensity", editor = "number", default = 100, min = 0, max = 255, slider = true, no_edit = function(self) return self.Type ~= "PointLight" and self.Type ~= "SpotLight" end },
{ category = "Light", id = "Color0", editor = "color", default = RGB(255,255,255), buttons = {{name = "Test", func = "TestActionFXLight"}}, no_edit = function(self) return self.Type == "PointLight" or self.Type == "StopLight" end },
{ category = "Light", id = "Intensity0", editor = "number", default = 100, min = 0, max = 255, slider = true, no_edit = function(self) return self.Type == "PointLight" or self.Type == "StopLight" end },
{ category = "Light", id = "Color1", editor = "color", default = RGB(255,255,255), buttons = {{name = "Test", func = "TestActionFXLight"}}, no_edit = function(self) return self.Type == "PointLight" or self.Type == "StopLight" end },
{ category = "Light", id = "Intensity1", editor = "number", default = 100, min = 0, max = 255, slider = true, no_edit = function(self) return self.Type == "PointLight" or self.Type == "StopLight" end },
{ category = "Light", id = "Period", editor = "number", default = 40000, min = 0, max = 100000, scale = 1000, slider = true, no_edit = function(self) return self.Type == "PointLight" or self.Type == "StopLight" end },
{ category = "Light", id = "Radius", editor = "number", default = 20, min = 0, max = 500*guim, color = RGB(255,50,50), color2 = RGB(50,50,255), slider = true, scale = "m" },
{ category = "Light", id = "FadeIn", editor = "number", default = 0, no_edit = function(self) return self.Type ~= "PointLight" and self.Type ~= "SpotLight" end },
{ category = "Light", id = "StartIntensity", editor = "number", default = 0, min = 0, max = 255, slider = true, no_edit = function(self) return self.Type ~= "PointLight" and self.Type ~= "SpotLight" end },
{ category = "Light", id = "StartColor", editor = "color", default = RGB(0,0,0), no_edit = function(self) return self.Type ~= "PointLight" and self.Type ~= "SpotLight" end },
{ category = "Light", id = "FadeOut", editor = "number", default = 0, no_edit = function(self) return self.Type ~= "PointLight" and self.Type ~= "SpotLight" end },
{ category = "Light", id = "FadeOutIntensity", editor = "number", default = 0, min = 0, max = 255, slider = true, no_edit = function(self) return self.Type ~= "PointLight" and self.Type ~= "SpotLight" end },
{ category = "Light", id = "FadeOutColor", editor = "color", default = RGB(0,0,0), no_edit = function(self) return self.Type ~= "PointLight" and self.Type ~= "SpotLight" end },
{ category = "Light", id = "ConeInnerAngle", editor = "number", default = 45, min = 5, max = (180 - 5), slider = true, no_edit = function(self) return self.Type ~= "SpotLight" and self.Type ~= "SpotLightFlicker" end },
{ category = "Light", id = "ConeOuterAngle", editor = "number", default = 45, min = 5, max = (180 - 5), slider = true, no_edit = function(self) return self.Type ~= "SpotLight" and self.Type ~= "SpotLightFlicker" end },
{ category = "Light", id = "LookAngle", editor = "number", default = 0, min = 0, max = 360*60 - 1, slider = true, scale = "deg", no_edit = function(self) return self.Type ~= "SpotLight" and self.Type ~= "SpotLightFlicker" end, },
{ category = "Light", id = "LookAxis", editor = "point", default = axis_z, scale = 4096, no_edit = function(self) return self.Type ~= "SpotLight" and self.Type ~= "SpotLightFlicker" end, },
{ category = "Light", id = "Interior", editor = "bool", default = true, },
{ category = "Light", id = "Exterior", editor = "bool", default = true, },
{ category = "Light", id = "InteriorAndExteriorWhenHasShadowmap", editor = "bool", default = true, },
{ category = "Light", id = "Always Renderable",editor = "bool", default = false },
{ category = "Light", id = "SourceRadius", name = "Source Radius (cm)", editor = "number", min = guic, max=20*guim, default = 10*guic, scale = guic, slider = true, color = RGB(200, 200, 0), autoattach_prop = true, help = "Radius of the light source in cm." },
{ category = "Placement", id = "Source", editor = "dropdownlist", default = "Actor", items = { "Actor", "ActorParent", "ActorOwner", "Target", "ActionPos" } },
{ category = "Placement", id = "Spot", editor = "combo", default = "Origin", items = function(fx) return ActionFXSpotCombo(fx) end },
{ category = "Placement", id = "Attach", editor = "bool", default = false, help = "Set true if the decal should move with the source" },
{ category = "Placement", id = "Offset", editor = "point", default = point30, scale = "m" },
{ category = "Placement", id = "OffsetDir", editor = "dropdownlist", default = "SourceAxisX", items = function(fx) return ActionFXOrientationCombo end },
{ category = "Placement", id = "Helper", editor = "bool", default = false, dont_save = true },
},
fx_type = "Light",
DocumentationLink = "Docs/ModItemActionFXLight.md.html",
Documentation = ActionFX.Documentation .. "\n\nThis mod item places light sources when an FX action is triggered. Inherits ActionFX. Read ActionFX first for the common properties."
}
function ActionFXLight:PlayFX(actor, target, action_pos, action_dir)
local count, obj, spot, posx, posy, posz, angle, axisx, axisy, axisz = self:GetLoc(actor, target, action_pos, action_dir)
if count == 0 then
printf("FX Light has invalid source: %s", self.Source)
return
end
local fx
if self.Delay <= 0 then
fx = self:PlaceFXLight(actor, target, obj, spot, posx, posy, posz, angle, axisx, axisy, axisz, action_pos, action_dir)
if not fx or self.Time <= 0 then
return
end
end
local thread = self:CreateThread(function(self, fx, actor, target, obj, spot, posx, posy, posz, angle, axisx, axisy, axisz, action_pos, action_dir)
if self.Delay > 0 then
Sleep(self.Delay)
fx = self:PlaceFXLight(actor, target, obj, spot, posx, posy, posz, angle, axisx, axisy, axisz, action_pos, action_dir)
if not fx or self.Time <= 0 then
return
end
end
local fadeout = (self.Type ~= "PointLightFlicker" and self.Type ~= "PointLightFlicker") and self.FadeOut > 0 and Min(self.Time, self.FadeOut) or 0
Sleep(self.Time-fadeout)
if not IsValid(fx) then return end
if fx == self:GetAssignedFX(actor, target) then
self:AssignFX(actor, target, nil)
end
if fadeout > 0 then
fx:Fade(self.FadeOutColor, self.FadeOutIntensity, fadeout)
Sleep(fadeout)
end
DoneObject(fx)
end, self, fx, actor, target, obj, spot, posx, posy, posz, angle, axisx, axisy, axisz, action_pos, action_dir)
if not fx and self:TrackFX() then
self:DestroyFX(actor, target)
self:AssignFX(actor, target, thread)
end
end
function ActionFXLight:PlaceFXLight(actor, target, obj, spot, posx, posy, posz, angle, axisx, axisy, axisz, action_pos, action_dir)
if self.Attach and not IsValid(obj) then
return
end
local fx = PlaceObject(self.Type)
NetTempObject(fx)
fx:SetCastShadows(self.CastShadows)
fx:SetDetailedShadows(self.DetailedShadows)
fx:SetAttenuationRadius(self.Radius)
fx:SetInterior(self.Interior)
fx:SetExterior(self.Exterior)
fx:SetInteriorAndExteriorWhenHasShadowmap(self.InteriorAndExteriorWhenHasShadowmap)
if self.AlwaysRenderable then
fx:SetGameFlags(const.gofAlwaysRenderable)
end
local detail_level = table.find_value(ActionFXDetailLevel, "value", self.DetailLevel)
if not detail_level then
local max_lower, min
for _, detail in ipairs(ActionFXDetailLevel) do
min = (not min or detail.value < min.value) and detail or min
if detail.value <= self.DetailLevel and (not max_lower or max_lower.value < detail.value) then
max_lower = detail
end
end
detail_level = max_lower or min
end
fx:SetDetailClass(detail_level.text)
if self.Helper then
fx:Attach(PointLight:new(), fx:GetSpotBeginIndex(self.Spot))
end
FXOrient(fx, posx, posy, posz, obj, spot, self.Attach, axisx, axisy, axisz, angle, self.Offset)
if self.GameTime then
fx:ClearGameFlags(const.gofRealTimeAnim)
end
if self.Type == "PointLight" or self.Type == "PointLightFlicker" then
fx:SetSourceRadius(self.SourceRadius)
end
if self.Type == "SpotLight" or self.Type == "SpotLightFlicker" then
fx:SetConeOuterAngle(self.ConeOuterAngle)
fx:SetConeInnerAngle(self.ConeInnerAngle)
fx:SetAxis(self.LookAxis)
fx:SetAngle(self.LookAngle)
end
if self.Type == "PointLightFlicker" or self.Type == "SpotLightFlicker" then
fx:SetColor0(self.Color0)
fx:SetIntensity0(self.Intensity0)
fx:SetColor1(self.Color1)
fx:SetIntensity1(self.Intensity1)
fx:SetPeriod(self.Period)
elseif self.FadeIn > 0 then
fx:SetColor(self.StartColor)
fx:SetIntensity(self.StartIntensity)
fx:Fade(self.Color, self.Intensity, self.FadeIn)
else
fx:SetColor(self.Color)
fx:SetIntensity(self.Intensity)
end
if self:TrackFX() then
self:AssignFX(actor, target, fx)
end
if self.Behavior ~= "" and self.BehaviorMoment == "" then
self[self.Behavior](self, actor, target, action_pos, action_dir)
end
self:OnLightPlaced(fx, actor, target, obj, spot, posx, posy, posz, angle, axisx, axisy, axisz, action_pos, action_dir)
return fx
end
function ActionFXLight:OnLightPlaced(fx, actor, target, obj, spot, posx, posy, posz, angle, axisx, axisy, axisz, action_pos, action_dir)
--project specific cb
end
function ActionFXLight:OnLightDone(fx)
--project specific cb
end
function ActionFXLight:DestroyFX(actor, target)
local fx = self:AssignFX(actor, target, nil)
if not fx then
return
elseif IsValid(fx) then
if (self.Type ~= "PointLightFlicker" and self.Type ~= "SpotLightFlicker") and self.FadeOut > 0 then
fx:Fade(self.FadeOutColor, self.FadeOutIntensity, self.FadeOut)
self:CreateThread(function(self, fx)
Sleep(self.FadeOut)
DoneObject(fx)
self:OnLightDone(fx)
end, self, fx)
else
DoneObject(fx)
self:OnLightDone(fx)
end
elseif IsValidThread(fx) then
DeleteThread(fx)
end
end
function ActionFXLight:BehaviorDetach(actor, target)
local fx = self:GetAssignedFX(actor, target)
if not fx then return end
if IsValidThread(fx) then
printf("FX Light Detach Behavior can not be run before the light is placed (Delay %d is very large)", self.Delay)
elseif IsValid(fx) then
PreciseDetachObj(fx)
end
end
function TestActionFXLight(editor_obj, fx, prop_id)
TestActionFXObjectEnd()
if (fx[prop_id] or "") == "" then
return
end
local obj = PlaceObject("PointLight")
if not obj then
return
end
LastTestActionFXObject = obj
local eye_pos, look_at
if camera3p.IsActive() then
eye_pos, look_at = camera.GetEye(), camera3p.GetLookAt()
elseif cameraMax.IsActive() then
eye_pos, look_at = cameraMax.GetPosLookAt()
else
look_at = GetTerrainGamepadCursor()
end
look_at = look_at:SetZ(terrain.GetHeight(look_at)+2*guim)
local posx, posy = look_at:xy()
local posz = terrain.GetHeight(look_at) + 2*guim
FXOrient(obj, posx, posy, posz)
obj:SetCastShadows(fx.CastShadows)
obj:SetDetailedShadows(fx.DetailedShadows)
obj:SetAttenuationRadius(fx.Radius)
obj:SetInterior(fx.Interior)
obj:SetExterior(fx.Exterior)
obj:SetInteriorAndExteriorWhenHasShadowmap(fx.InteriorAndExteriorWhenHasShadowmap)
if self.AlwaysRenderable then
obj:SetGameFlags(const.gofAlwaysRenderable)
end
if fx.Type == "PointLightFlicker" or fx.Type == "SpotLightFlicker" then
obj:SetColor0(fx.Color0)
obj:SetIntensity0(fx.Intensity0)
obj:SetColor1(fx.Color1)
obj:SetIntensity1(fx.Intensity1)
obj:SetPeriod(fx.Period)
elseif fx.FadeIn > 0 then
obj:SetColor(fx.StartColor)
obj:SetIntensity(fx.StartIntensity)
obj:Fade(fx.Color, fx.Intensity, fx.FadeIn)
else
obj:SetColor(fx.Color)
obj:SetIntensity(fx.Intensity)
end
if fx.Time >= 0 then
self:CreateThread(function(fx, obj)
local time = fx.Time
if fx.FadeOut > 0 then
local t = Min(time, fx.FadeOut)
Sleep(time-t)
if IsValid(obj) then
obj:Fade(fx.FadeOutColor, fx.FadeOutIntensity, t)
Sleep(t)
end
else
Sleep(time)
end
TestActionFXObjectEnd(obj)
end, fx, obj)
end
end
--============================= FX Colorization =======================
DefineClass.ActionFXColorization = {
__parents = { "ActionFX" },
properties = {
{ id = "Color1", category = "Colorization", editor = "color", default = RGB(255,255,255) },
{ id = "Color2_Enable", category = "Colorization", editor = "bool", default = false },
{ id = "Color2", category = "Colorization", editor = "color", default = RGB(255,255,255), read_only = function(self) return not self.Color2_Enable end },
{ id = "Color3_Enable", category = "Colorization", editor = "bool", default = false },
{ id = "Color3", category = "Colorization", editor = "color", default = RGB(255,255,255), read_only = function(self) return not self.Color3_Enable end },
{ id = "Color4_Enable", category = "Colorization", editor = "bool", default = false },
{ id = "Color4", category = "Colorization", editor = "color", default = RGB(255,255,255), read_only = function(self) return not self.Color4_Enable end },
{ id = "Source", category = "Colorization", default = "Actor", editor = "dropdownlist", items = { "Actor", "ActorParent", "ActorOwner", "Target" } },
},
fx_type = "Colorization",
}
_ColorizationFunc = function(self, color_modifier, actor, target, obj)
if self.Delay > 0 then
Sleep(self.Delay)
end
local fx = PlaceFX_Colorization(obj, color_modifier)
if self:TrackFX() then
self:AssignFX(actor, target, fx)
end
if fx and self.Time > 0 then
Sleep(self.Time)
RemoveFX_Colorization(obj, fx)
end
end
function ActionFXColorization:PlayFX(actor, target)
local obj = self:GetLocObj(actor, target)
if not IsValid(obj) then
printf("FX Colorization has invalid object: %s", self.Source)
return
end
local color_modifier = self:ChooseColor()
if self.Delay <= 0 and self.Time <= 0 then
local fx = PlaceFX_Colorization(obj, color_modifier)
if fx and self:TrackFX() then
self:AssignFX(actor, target, fx)
end
return
end
local thread = self:CreateThread(_ColorizationFunc, self, color_modifier, actor, target, obj)
if self:TrackFX() then
self:DestroyFX(actor, target)
self:AssignFX(actor, target, thread)
end
end
function ActionFXColorization:DestroyFX(actor, target)
local fx = self:AssignFX(actor, target, nil)
if not fx then
return
elseif IsValidThread(fx) then
DeleteThread(fx)
else
local obj = self:GetLocObj(actor, target)
RemoveFX_Colorization(obj, fx)
end
end
function ActionFXColorization:ChooseColor()
local color_variations = 1
if self.Color2_Enable then color_variations = color_variations + 1 end
if self.Color3_Enable then color_variations = color_variations + 1 end
if self.Color4_Enable then color_variations = color_variations + 1 end
if color_variations == 1 then
return self.Color1
end
local idx = AsyncRand(color_variations)
if idx == 0 then
return self.Color1
end
if self.Color2_Enable then
idx = idx - 1
if idx == 0 then
return self.Color2
end
end
if self.Color3_Enable then
idx = idx - 1
if idx == 0 then
return self.Color3
end
end
return self.Color4
end
DefineClass.ActionFXInitialColorization = {
__parents = { "ActionFXColorization" },
fx_type = "ColorizationInitial",
properties = {
{ id = "Target", },
{ id = "Delay", },
{ id = "Id", },
{ id = "Disabled", },
{ id = "Time", },
{ id = "EndRules", },
{ id = "Behavior", },
{ id = "BehaviorMoment", },
},
}
local default_color_modifier = RGBA(100, 100, 100, 0)
function ActionFXInitialColorization:PlayFX(actor, target, action_pos, action_dir)
local obj = self:GetLocObj(actor, target)
if not IsValid(obj) then
printf("FX Colorization has invalid object: %s", self.Source)
return
end
if obj:GetColorModifier() == default_color_modifier then
local color = self:ChooseColor()
obj:SetColorModifier(color)
end
end
MapVar("fx_colorization", {}, weak_keys_meta)
function PlaceFX_Colorization(obj, color_modifier)
if not IsValid(obj) then
return
end
local fx = { color_modifier }
local list = fx_colorization[obj]
if not list then
list = { obj:GetColorModifier() }
fx_colorization[obj] = list
end
table.insert(list, fx)
obj:SetColorModifier(color_modifier)
return fx
end
function RemoveFX_Colorization(obj, fx)
local list = fx_colorization[obj]
if not list then return end
if not IsValid(obj) then
fx_colorization[obj] = nil
return
end
local len = #list
if list[len] ~= fx then
table.remove_value(list, fx)
elseif len == 2 then
fx_colorization[obj] = nil
obj:SetColorModifier(list[1])
else
list[len] = nil
obj:SetColorModifier(list[len-1][1])
end
end
--=============================
DefineClass.SpawnFXObject = {
__parents = { "Object", "ComponentAttach" },
__hierarchy_cache = true,
fx_actor_base_class = "",
}
function SpawnFXObject:GameInit()
PlayFX("Spawn", "start", self)
end
function SpawnFXObject:Done()
if IsValid(self) and self:IsValidPos() then
PlayFX("Spawn", "end", self)
end
end
function OnMsg.GatherFXActions(list)
table.insert(list, "Spawn")
end
--=============================
function OnMsg.OptionsApply()
FXCache = false
end
function GetPlayFXList(actionFXClass, actionFXMoment, actorFXClass, targetFXClass, list)
local remove_ids
local inherit_actions = actionFXClass and (FXInheritRules_Actions or RebuildFXInheritActionRules())[actionFXClass]
local inherit_moments = actionFXMoment and (FXInheritRules_Moments or RebuildFXInheritMomentRules())[actionFXMoment]
local inherit_actors = actorFXClass and (FXInheritRules_Actors or RebuildFXInheritActorRules() )[actorFXClass]
local inherit_targets = targetFXClass and (FXInheritRules_Actors or RebuildFXInheritActorRules() )[targetFXClass]
local i, action = 0, actionFXClass
while true do
local rules = action and FXRules[action]
if rules then
local i, moment = 0, actionFXMoment
while true do
local rules = moment and rules[moment]
if rules then
local i, actor = 0, actorFXClass
while true do
local rules = actor and rules[actor]
if rules then
local i, target = 0, targetFXClass
while true do
local rules = target and rules[target]
if rules then
for i = 1, #rules do
local fx = rules[i]
if not fx.Disabled and fx.Chance > 0 and
fx.DetailLevel >= hr.FXDetailThreshold and
(not IsKindOf(fx, "ActionFX") or MatchGameState(fx.GameStatesFilter))
then
if fx.fx_type == "FX Remove" then
if fx.FxId ~= "" then
remove_ids = remove_ids or {}
remove_ids[fx.FxId] = "remove"
end
elseif fx.Action == "any" and fx.Moment == "any" then
-- invalid, probably just created FX
else
list = list or {}
list[#list+1] = fx
end
end
end
end
if target == "any" then break end
i = i + 1
target = inherit_targets and inherit_targets[i] or "any"
end
end
if actor == "any" then break end
i = i + 1
actor = inherit_actors and inherit_actors[i] or "any"
end
end
if moment == "any" then break end
i = i + 1
moment = inherit_moments and inherit_moments[i] or "any"
end
end
if action == "any" then break end
i = i + 1
action = inherit_actions and inherit_actions[i] or "any"
end
if list and remove_ids then
for i = #list, 1, -1 do
if remove_ids[list[i].FxId] == "remove" then
table.remove(list, i)
if i == 1 and #list == 0 then
list = nil
end
end
end
end
return list
end
if Platform.developer then
local old_GetPlayFXList = GetPlayFXList
function GetPlayFXList(...)
local list = old_GetPlayFXList(...)
if g_SoloFX_count > 0 and list then
for i = #list, 1, -1 do
local fx = list[i]
local solo
if fx.class == "ActionFXBehavior" then
solo = fx.fx.Solo
else
solo = fx.Solo
end
if solo then
table.remove(list, i)
end
end
end
return list
end
end
local function ListCopyMembersOnce(list, added, source, member)
if not source then return end
for i = 1, #source do
local v = source[i][member]
if not added[v] then
added[v] = true
list[#list+1] = v
end
end
end
local function ListCopyOnce(list, added, source)
if not source then return end
for i = 1, #source do
local v = source[i]
if not added[v] then
added[v] = true
list[#list+1] = v
end
end
end
StaticFXActionsCache = false
function GetStaticFXActionsCached()
if StaticFXActionsCache then
return StaticFXActionsCache
end
local list = {}
Msg("GatherFXActions", list)
local added = { any = true, [""] = true }
for i = #list, 1, -1 do
if not added[list[i]] then
added[list[i]] = true
else
list[i], list[#list] = list[#list], nil
end
end
ListCopyMembersOnce(list, added, FXLists.ActionFXInherit_Action, "Action")
ClassDescendants("FXObject", function(classname, class)
if class.fx_action_base then
local name = class.fx_action or classname
if not added[name] then
list[#list+1] = name
added[name] = true
end
end
end)
table.sort(list, CmpLower)
table.insert(list, 1, "any")
StaticFXActionsCache = list
return StaticFXActionsCache
end
function ActionFXClassCombo(fx)
local list = {}
local entity = fx and rawget(fx, "AnimEntity") or ""
if IsValidEntity(entity) then
list[#list + 1] = ""
for _, anim in ipairs(GetStates(entity)) do
list[#list + 1] = FXAnimToAction(anim)
end
list[#list + 1] = "----------"
end
table.iappend(list, GetStaticFXActionsCached())
return list
end
function ActionMomentFXCombo(fx)
local default_list = {
"any",
"",
"start",
"end",
"hit",
"interrupted",
"recharge",
"new_target",
"target_lost",
"channeling-start",
"channeling-end",
}
local list = {}
local added = { any = true }
for i = 1, #default_list do
added[default_list[i]] = true
end
for classname, fxlist in pairs(FXLists) do
if g_Classes[classname]:HasMember("Moment") then
ListCopyMembersOnce(list, added, fxlist, "Moment")
end
end
local list2 = {}
Msg("GatherFXMoments", list2, fx)
ListCopyOnce(list, added, list2)
for i = 1, #default_list do
table.insert(list, i, default_list[i])
end
added = {}
for i = #list, 1, -1 do
if not added[list[i]] then
added[list[i]] = true
else
list[i], list[#list] = list[#list], nil
end
end
table.sort(list, CmpLower)
return list
end
local function GatherFXActors(list)
Msg("GatherFXActors", list)
local added = { any = true }
for i = #list, 1, -1 do
if not added[list[i]] then
added[list[i]] = true
else
list[i], list[#list] = list[#list], nil
end
end
ListCopyMembersOnce(list, added, FXLists.ActionFXInherit_Actor, "Actor")
ClassDescendants("FXObject", function(classname, class)
if class.fx_actor_base_class then
local name = class.fx_actor_class or classname
if name and not added[name] then
list[#list+1] = name
added[name] = true
end
end
end)
table.sort(list, CmpLower)
table.insert(list, 1, "any")
end
StaticFXActorsCache = false
function ActorFXClassCombo()
if not StaticFXActorsCache then
local list = {}
GatherFXActors(list)
StaticFXActorsCache = list
end
return StaticFXActorsCache
end
StaticFXTargetsCache = false
function TargetFXClassCombo()
if not StaticFXTargetsCache then
local list = {}
Msg("GatherFXTargets", list)
GatherFXActors(list)
table.insert(list, 2, "ignore")
StaticFXTargetsCache = list
end
return StaticFXTargetsCache
end
function HookActionFXCombo(fx)
local actions = ActionFXClassCombo(fx)
table.remove_value(actions, "any")
table.insert(actions, 1, "")
return actions
end
function HookMomentFXCombo(fx)
local actions = ActionMomentFXCombo(fx)
table.remove_value(actions, "any")
table.insert(actions, 1, "")
return actions
end
function ActionMomentNamesCombo(fx)
local actions = ActionMomentFXCombo(fx)
table.remove_value(actions, "any")
table.remove_value(actions, "")
return actions
end
local class_to_behavior_items
function ActionFXBehaviorCombo(fx)
local class = fx.class
class_to_behavior_items = class_to_behavior_items or {}
local list = class_to_behavior_items[class]
if not list then
list = { { text = "Destroy", value = "DestroyFX" } }
for name, func in fx:__enum() do
if type(func) == "function" and type(name) == "string" then
local text
if string.starts_with(name, "Behavior") then
text = string.sub(name, 9)
end
if text then
list[#list + 1] = { text = text, value = name }
end
end
end
table.sort(list, function(a, b) return CmpLower(a.text, b.text) end)
table.insert(list, 1, { text = "", value = "" })
class_to_behavior_items[class] = list
end
return list
end
function ActionFXSpotCombo()
local list, added = {}, { Origin = true, [""] = true }
Msg("GatherFXSpots", list)
for i = #list, 1, -1 do
if not added[list[i]] then
added[list[i]] = true
else
list[i], list[#list] = list[#list], nil
end
end
for _, t1 in pairs(FXRules) do
for _, t2 in pairs(t1) do
for _, t3 in pairs(t2) do
for i = 1, #t3 do
local spot = rawget(t3[i], "Spot")
if spot and not added[spot] then
list[#list+1] = spot
added[spot] = true
end
end
end
end
end
table.sort(list, CmpLower)
table.insert(list, 1, "Origin")
return list
end
function ActionFXSourcePropCombo()
local list, added = {}, { [""] = true }
for _, t1 in pairs(FXRules) do
for _, t2 in pairs(t1) do
for _, t3 in pairs(t2) do
for i = 1, #t3 do
local spot = rawget(t3[i], "SourceProp")
if spot and not added[spot] then
list[#list+1] = spot
added[spot] = true
end
end
end
end
end
table.sort(list, CmpLower)
return list
end
DefineClass.CameraObj = {
__parents = { "SpawnFXObject", "CObject", "ComponentInterpolation" },
entity = "InvisibleObject",
flags = { gofAlwaysRenderable = true, efSelectable = false, cofComponentCollider = false },
}
MapVar("g_CameraObj", function()
local cam = CameraObj:new()
cam:SetSpecialOrientation(const.soUseCameraTransform)
return cam
end)
local IsValid = IsValid
local SnapToCamera
function OnMsg.OnRender()
local obj = g_CameraObj
SnapToCamera = SnapToCamera or IsEditorActive() and empty_func or CObject.SnapToCamera
if IsValid(obj) then
SnapToCamera(obj)
end
end
function OnMsg.GameEnterEditor()
if IsValid(g_CameraObj) then
g_CameraObj:ClearEnumFlags(const.efVisible)
end
SnapToCamera = empty_func
end
function OnMsg.GameExitEditor()
if IsValid(g_CameraObj) then
g_CameraObj:SetEnumFlags(const.efVisible)
end
SnapToCamera = CObject.SnapToCamera
end
if Platform.asserts then
if FirstLoad then
ObjToSoundInfo = false
end
local ObjSoundErrorHash = false
function OnMsg.ChangeMap()
ObjToSoundInfo = false
ObjSoundErrorHash = false
end
local function GetFXInfo(fx)
return string.format("%s-%s-%s-%s", tostring(fx.Action), tostring(fx.Moment), tostring(fx.Actor), tostring(fx.Target))
end
local function GetSoundInfo(fx, sound)
return string.format("'%s' from [%s]", sound, GetFXInfo(fx))
end
MarkObjSound = function(fx, obj, sound)
local time = RealTime() + GameTime()
ObjToSoundInfo = ObjToSoundInfo or setmetatable({}, weak_keys_meta)
local info = ObjToSoundInfo[obj]
if not info then
ObjToSoundInfo[obj] = { sound, fx, time }
return
end
--print(gt, rt, GetSoundInfo(fx, sound))
local prev_sound, prev_fx, prev_time = info[1], info[2], info[3]
if time == prev_time then
local sname, sbank, stype, shandle, sduration, stime = obj:GetSound()
if sbank == prev_sound then
local str = GetSoundInfo(fx, sound)
local str_prev = GetSoundInfo(prev_fx, prev_sound)
local err_hash = xxhash(str, str_prev)
ObjSoundErrorHash = ObjSoundErrorHash or {}
if not ObjSoundErrorHash[err_hash] then
ObjSoundErrorHash[err_hash] = err_hash
StoreErrorSource(obj, "Sound", str, "replaced", str_prev)
end
end
end
info[1], info[2], info[3] = sound, fx, time
end
end -- Platform.asserts