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%s%s%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 = "", 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 '' & Moment ''"), } 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",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