|
local Behaviors |
|
local BehaviorsList |
|
|
|
MapVar("BehaviorLabels", {}) |
|
MapVar("BehaviorLabelsUpdate", {}) |
|
MapVar("BehaviorAreaUpdate", sync_set()) |
|
|
|
|
|
|
|
local function GatherFXSourceTags() |
|
local tags = {} |
|
Msg("GatherFXSourceTags", tags) |
|
ForEachPreset("FXSourcePreset", function(preset, group, tags) |
|
for tag in pairs(preset.Tags) do |
|
tags[tag] = true |
|
end |
|
end, tags) |
|
return table.keys(tags, true) |
|
end |
|
|
|
|
|
|
|
function FXSourceUpdate(self, game_state_changed, forced_match, forced_update) |
|
assert(not DisableSoundFX) |
|
if not IsValid(self) or not forced_update and self.update_disabled then |
|
return |
|
end |
|
local preset = self:GetPreset() or empty_table |
|
local fx_event = preset.Event |
|
if fx_event then |
|
local match = forced_match |
|
if match == nil then |
|
match = MatchGameState(self.game_states) |
|
end |
|
if not match then |
|
fx_event = false |
|
end |
|
end |
|
if fx_event and Behaviors then |
|
for name, set in pairs(preset.Behaviors) do |
|
local behavior = Behaviors[name] |
|
if behavior then |
|
local enabled = behavior:IsFXEnabled(self, preset) |
|
if set and not enabled or not set and enabled then |
|
fx_event = false |
|
break |
|
end |
|
end |
|
end |
|
end |
|
local current_fx = self.current_fx |
|
if fx_event == current_fx and not forced_update then |
|
return |
|
end |
|
if current_fx then |
|
PlayFX(current_fx, "end", self) |
|
end |
|
if fx_event then |
|
PlayFX(fx_event, "start", self) |
|
end |
|
self.current_fx = fx_event or nil |
|
if game_state_changed then |
|
if current_fx and not fx_event and preset.PlayOnce then |
|
self.update_disabled = true |
|
end |
|
self:OnGameStateChanged() |
|
end |
|
end |
|
|
|
|
|
|
|
DefineClass.FXSourceBehavior = { |
|
__parents = { "PropertyObject" }, |
|
id = false, |
|
CreateLabel = false, |
|
LabelUpdateMsg = false, |
|
LabelUpdateDelay = 0, |
|
LabelUpdateDelayStep = 50, |
|
IsFXEnabled = return_true, |
|
} |
|
|
|
function FXSourceBehavior:GetEditorView() |
|
return Untranslated(self.id or self.class) |
|
end |
|
|
|
function FXSourceUpdateBehaviorLabels() |
|
local now = GameTime() |
|
local labels_to_update = BehaviorLabelsUpdate |
|
local sources_to_update = BehaviorAreaUpdate |
|
if not next(sources_to_update) then |
|
sources_to_update = false |
|
end |
|
local labels = BehaviorLabels |
|
local next_time = max_int64 |
|
local pass_edits |
|
local FXSourceUpdate = FXSourceUpdate |
|
for _, name in ipairs(labels_to_update) do |
|
local def = Behaviors[name] |
|
local label = def and labels[name] |
|
if label then |
|
local delay = def.LabelUpdateDelay |
|
local time = labels_to_update[name] |
|
if now < time then |
|
if next_time < time then |
|
next_time = time |
|
end |
|
elseif now <= time + delay then |
|
if not pass_edits then |
|
pass_edits = true |
|
SuspendPassEdits("FXSource") |
|
end |
|
if delay == 0 then |
|
for _, source in ipairs(label) do |
|
FXSourceUpdate(source) |
|
if sources_to_update then |
|
sources_to_update:remove(source) |
|
end |
|
end |
|
else |
|
local step = def.LabelUpdateDelayStep |
|
local steps = 1 + delay / step |
|
local BraidRandom = BraidRandom |
|
local seed = xxhash(name, MapLoadRandom) |
|
for i, source in ipairs(label) do |
|
local delta |
|
delta, seed = BraidRandom(seed, steps) |
|
local time_i = time + delta * step |
|
if now == time_i then |
|
FXSourceUpdate(source) |
|
if sources_to_update then |
|
sources_to_update:remove(source) |
|
end |
|
elseif now < time_i and next_time > time_i then |
|
next_time = time_i |
|
end |
|
end |
|
end |
|
end |
|
end |
|
end |
|
if pass_edits then |
|
ResumePassEdits("FXSource") |
|
end |
|
return (next_time > now and next_time < max_int) and (next_time - now) or nil |
|
end |
|
|
|
|
|
|
|
MapGameTimeRepeat("FXSourceUpdateBehaviorLabels", nil, function() |
|
local sleep = FXSourceUpdateBehaviorLabels() |
|
WaitWakeup(sleep) |
|
end) |
|
|
|
function FXSourceUpdateBehaviorLabel(id) |
|
if not BehaviorLabels[id] then |
|
return |
|
end |
|
local list = BehaviorLabelsUpdate |
|
if not list[id] then |
|
list[#list + 1] = id |
|
end |
|
list[id] = GameTime() |
|
WakeupPeriodicRepeatThread("FXSourceUpdateBehaviorLabels") |
|
end |
|
|
|
function FXSourceUpdateBehaviorArea() |
|
local sources_to_update = BehaviorAreaUpdate |
|
if not next(sources_to_update) then return end |
|
SuspendPassEdits("FXSource") |
|
local FXSourceUpdate = FXSourceUpdate |
|
for _, source in ipairs(sources_to_update) do |
|
FXSourceUpdate(source) |
|
end |
|
table.clear(sources_to_update, true) |
|
ResumePassEdits("FXSource") |
|
end |
|
|
|
function FXSourceUpdateBehaviorAround(id, pos, radius) |
|
local def = Behaviors[id] |
|
local label = def and BehaviorLabels[id] |
|
if not label then |
|
return |
|
end |
|
local list = BehaviorAreaUpdate |
|
MapForEach(pos, radius, "FXSource", function(source, label, list) |
|
if label[source] then |
|
list:insert(source) |
|
end |
|
end, label, list) |
|
if not next(list) then |
|
return |
|
end |
|
WakeupPeriodicRepeatThread("FXSourceUpdateBehaviorArea") |
|
end |
|
|
|
MapGameTimeRepeat("FXSourceUpdateBehaviorArea", nil, function() |
|
FXSourceUpdateBehaviorArea() |
|
WaitWakeup() |
|
end) |
|
|
|
function OnMsg.ClassesBuilt() |
|
ClassDescendants("FXSourceBehavior", function(class, def) |
|
local id = def.id |
|
if id then |
|
Behaviors = table.create_set(Behaviors, id, def) |
|
assert(not def.LabelUpdateMsg or def.CreateLabel) |
|
if def.CreateLabel and def.LabelUpdateMsg then |
|
OnMsg[def.LabelUpdateMsg] = function() |
|
FXSourceUpdateBehaviorLabel(id) |
|
end |
|
end |
|
end |
|
end) |
|
BehaviorsList = Behaviors and table.keys(Behaviors, true) |
|
end |
|
|
|
local function RegisterBehaviors(source, labels, preset) |
|
if not Behaviors then |
|
return |
|
end |
|
preset = preset or source:GetPreset() |
|
if not preset or not preset.Event then |
|
return |
|
end |
|
for name, set in pairs(preset.Behaviors) do |
|
local behavior = Behaviors[name] |
|
if behavior and behavior.CreateLabel then |
|
labels = labels or BehaviorLabels |
|
local label = labels[name] |
|
if not label then |
|
labels[name] = {source, [source] = true} |
|
elseif not label[source] then |
|
label[#label + 1] = source |
|
label[source] = true |
|
end |
|
end |
|
end |
|
end |
|
|
|
function FXSourceRebuildLabels() |
|
BehaviorLabels = {} |
|
BehaviorLabelsUpdate = BehaviorLabelsUpdate or {} |
|
MapForEach("map", "FXSource", const.efMarker, RegisterBehaviors, BehaviorLabels) |
|
end |
|
|
|
local function UnregisterBehaviors(source, labels) |
|
for name, label in pairs(labels or BehaviorLabels) do |
|
if label[source] then |
|
table.remove_value(label, source) |
|
label[source] = nil |
|
end |
|
end |
|
end |
|
|
|
|
|
|
|
DefineClass.FXBehaviorChance = { |
|
__parents = { "FXSourceBehavior" }, |
|
id = "Chance", |
|
CreateLabel = true, |
|
properties = { |
|
{ category = "FX: Chance", id = "EnableChance", name = "Chance", editor = "number", default = 100, min = 0, max = 100, scale = "%", slider = true }, |
|
{ category = "FX: Chance", id = "ChangeInterval", name = "Change Interval", editor = "number", default = 0, min = 0, scale = function(self) return self.IntervalScale end, help = "Time needed to change the chance result." }, |
|
{ category = "FX: Chance", id = "IntervalScale", name = "Interval Scale", editor = "choice", default = false, items = function() return table.keys(const.Scale, true) end }, |
|
{ category = "FX: Chance", id = "IsGameTime", name = "Game Time", editor = "bool", default = false, help = "Change interval time type. Game Time is needed for events messing with the game logic." }, |
|
}, |
|
} |
|
|
|
function FXBehaviorChance:IsFXEnabled(source, preset) |
|
local chance = preset and preset.EnableChance or 100 |
|
if chance >= 100 then return true end |
|
local time = (preset.IsGameTime and GameTime() or RealTime()) / Max(1, preset.ChangeInterval or 0) |
|
local seed = xxhash(source.handle, time, MapLoadRandom) |
|
return (seed % 100) < chance |
|
end |
|
|
|
|
|
|
|
DefineClass.FXSourcePreset = { |
|
__parents = { "Preset" }, |
|
properties = { |
|
{ category = "FX", id = "Event", name = "FX Event", editor = "combo", default = false, items = function(fx) return ActionFXClassCombo(fx) end }, |
|
{ category = "FX", id = "GameStates", name = "Game State", editor = "set", default = set(), three_state = true, items = function() return GetGameStateFilter() end }, |
|
{ category = "FX", id = "PlayOnce", name = "Play Once", editor = "bool", default = false, help = "Kill the object if the FX is no more matched after changing game state", }, |
|
{ category = "FX", id = "EditorPlay", name = "Editor Play", editor = "choice", default = "force play", items = {"no change", "force play", "force stop"}, developer = true }, |
|
{ category = "FX", id = "Entity", name = "Editor Entity", editor = "combo", default = false, items = function() return GetAllEntitiesCombo() end }, |
|
{ category = "FX", id = "Tags", name = "Tags", editor = "set", default = set(), items = GatherFXSourceTags, help = "Help the game logic find this source if needed", }, |
|
{ category = "FX", id = "Behaviors", name = "Behaviors", editor = "set", default = set(), items = function() return BehaviorsList end, three_state = true, }, |
|
{ category = "FX", id = "ConditionText", name = "Condition", editor = "text", default = "", read_only = true, no_edit = function(self) return not next(self.Behaviors) end }, |
|
{ category = "FX", id = "Actor", name = "FX Actor", editor = "combo", default = false, items = function(fx) return ActorFXClassCombo(fx) end}, |
|
{ category = "FX", id = "Scale", name = "Scale", editor = "number", default = false }, |
|
{ category = "FX", id = "Color", name = "Color", editor = "color", default = false }, |
|
{ category = "FX", id = "FXButtons", editor = "buttons", default = false, buttons = {{ name = "Map Select", func = "ActionSelect" }} }, |
|
}, |
|
GlobalMap = "FXSourcePresets", |
|
EditorMenubarName = "FX Sources", |
|
EditorMenubar = "Editors.Art", |
|
EditorIcon = "CommonAssets/UI/Icons/atoms electron physic.png", |
|
} |
|
|
|
function FXSourcePreset:GetConditionText() |
|
local texts = {} |
|
for name, set in pairs(self.Behaviors) do |
|
if set then |
|
texts[#texts + 1] = name |
|
else |
|
texts[#texts + 1] = "not " .. name |
|
end |
|
end |
|
return table.concat(texts, " and ") |
|
end |
|
|
|
function FXSourcePreset:ActionSelect() |
|
if GetMap() == "" then |
|
return |
|
end |
|
editor.ClearSel() |
|
editor.AddToSel(MapGet("map", "FXSource", const.efMarker, function(obj, id) |
|
return obj.FxPreset == id |
|
end, self.id)) |
|
end |
|
|
|
function FXSourcePreset:OnEditorSetProperty(prop_id) |
|
if GetMap() == "" then |
|
return |
|
end |
|
local prop = self:GetPropertyMetadata(prop_id) |
|
if not prop or prop.category ~= "FX" then |
|
return |
|
end |
|
MapForEach("map", "FXSource", const.efMarker, function(obj, self) |
|
if obj.FxPreset == self.id then |
|
obj:SetPreset(self) |
|
end |
|
end, self) |
|
end |
|
|
|
function FXSourcePreset:GetProperties() |
|
local orig_props = Preset.GetProperties(self) |
|
local props = orig_props |
|
if Behaviors then |
|
for name, set in pairs(self.Behaviors) do |
|
local classdef = Behaviors[name] |
|
local propsi = classdef and classdef.properties |
|
if propsi and #propsi > 0 then |
|
if props == orig_props then |
|
props = table.icopy(props) |
|
end |
|
props = table.iappend(props, propsi) |
|
end |
|
end |
|
end |
|
return props |
|
end |
|
|
|
DefineClass("FXSourceAutoResolve") |
|
|
|
DefineClass.FXSource = { |
|
__parents = { "Object", "FXObject", "EditorEntityObject", "EditorCallbackObject", "EditorTextObject", "FXSourceAutoResolve" }, |
|
flags = { efMarker = true, efWalkable = false, efCollision = false, efApplyToGrids = false }, |
|
editor_text_offset = point(0, 0, -guim), |
|
editor_text_style = "FXSourceText", |
|
editor_entity = "ParticlePlaceholder", |
|
entity = "InvisibleObject", |
|
|
|
properties = { |
|
{ category = "FX Source", id = "FxPreset", name = "FX Preset", editor = "preset_id", default = false, preset_class = "FXSourcePreset", buttons = {{ name = "Start", func = "ActionStart" }, { name = "End", func = "ActionEnd" }} }, |
|
{ category = "FX Source", id = "Playing", name = "Playing", editor = "bool", default = false, dont_save = true, read_only = true }, |
|
}, |
|
|
|
current_fx = false, |
|
update_disabled = false, |
|
game_states = false, |
|
|
|
prefab_no_fade_clamp = true, |
|
} |
|
|
|
function FXSource:GetPlaying() |
|
return not not self.current_fx |
|
end |
|
|
|
function FXSource:EditorGetText() |
|
return (self.FxPreset or "") ~= "" and self.FxPreset or self.class |
|
end |
|
|
|
function FXSource:GetEditorLabel() |
|
local label = self.class |
|
if (self.FxPreset or "") ~= "" then |
|
label = label .. " (" .. self.FxPreset .. ")" |
|
end |
|
return label |
|
end |
|
|
|
function FXSource:GetError() |
|
if not self.FxPreset then |
|
return "FX source has no FX preset assigned." |
|
end |
|
end |
|
|
|
function FXSource:GameInit() |
|
if ChangingMap then |
|
return |
|
end |
|
FXSourceUpdate(self) |
|
end |
|
|
|
FXSourceAutoResolve.EditorExit = FXSourceUpdate |
|
|
|
function FXSourceAutoResolve:EditorEnter() |
|
CreateRealTimeThread(function() |
|
WaitChangeMapDone() |
|
if GetMap() == "" then |
|
return |
|
end |
|
local match |
|
if IsEditorActive() then |
|
local preset = self:GetPreset() or empty_table |
|
local editor_play = preset.EditorPlay |
|
if editor_play == "force play" then |
|
match = true |
|
elseif editor_play == "force stop" then |
|
match = false |
|
end |
|
end |
|
FXSourceUpdate(self, nil, match) |
|
end) |
|
end |
|
|
|
function FXSourceAutoResolve:OnEditorSetProperty(prop_id) |
|
if prop_id == "FxPreset" then |
|
FXSourceUpdate(self, nil, self:GetPlaying(), true) |
|
self:EditorTextUpdate() |
|
end |
|
end |
|
|
|
MapVar("FXSourceStates", false) |
|
MapVar("FXSourceUpdateThread", false) |
|
|
|
function FXSource:SetGameStates(states) |
|
states = states or false |
|
local prev_states = self.game_states |
|
if prev_states == states then |
|
return |
|
end |
|
local counters = FXSourceStates or {} |
|
FXSourceStates = counters |
|
for state in pairs(states) do |
|
counters[state] = (counters[state] or 0) + 1 |
|
end |
|
for state in pairs(prev_states) do |
|
local count = counters[state] or 0 |
|
assert(count > 0) |
|
if count > 1 then |
|
counters[state] = count - 1 |
|
else |
|
counters[state] = nil |
|
end |
|
end |
|
self.game_states = states or nil |
|
end |
|
|
|
function FXSource:OnGameStateChanged() |
|
end |
|
|
|
function FXSource:SetFxPreset(id) |
|
if (id or "") == "" then |
|
self.FxPreset = nil |
|
self:SetPreset() |
|
return |
|
end |
|
self.FxPreset = id |
|
self:SetPreset(FXSourcePresets[id]) |
|
end |
|
|
|
function FXSource:SetPreset(preset) |
|
UnregisterBehaviors(self) |
|
|
|
if not preset then |
|
self:SetGameStates(false) |
|
self:ChangeEntity(FXSource.entity) |
|
self.fx_actor_class = nil |
|
self:SetState("idle") |
|
self:SetScale(100) |
|
self:SetColorModifier(const.clrNoModifier) |
|
FXSourceUpdate(self, nil, false) |
|
return |
|
end |
|
|
|
RegisterBehaviors(self, nil, preset) |
|
self:SetGameStates(preset.GameStates) |
|
if preset.Entity then |
|
self.editor_entity = preset.Entity |
|
if IsEditorActive() then |
|
self:ChangeEntity(preset.Entity) |
|
end |
|
end |
|
if preset.Actor then |
|
self.fx_actor_class = preset.Actor |
|
end |
|
if preset.State then |
|
self:SetState(preset.State) |
|
end |
|
if preset.Scale then |
|
self:SetScale(preset.Scale) |
|
end |
|
if preset.Color then |
|
self:SetColorModifier(preset.Color) |
|
end |
|
if self.current_fx then |
|
FXSourceUpdate(self, nil, true) |
|
end |
|
end |
|
|
|
function FXSource:GetPreset() |
|
return FXSourcePresets[self.FxPreset] |
|
end |
|
|
|
function FXSource:Done() |
|
self:SetGameStates(false) |
|
FXSourceUpdate(self, nil, false) |
|
UnregisterBehaviors(self) |
|
end |
|
|
|
|
|
|
|
function FXSource:ActionStart() |
|
FXSourceUpdate(self, nil, true, true) |
|
ObjModified(self) |
|
end |
|
|
|
function FXSource:ActionEnd() |
|
FXSourceUpdate(self, nil, false, true) |
|
ObjModified(self) |
|
end |
|
|
|
local function FXSourceUpdateAll(area, ...) |
|
SuspendPassEdits("FXSource") |
|
MapForEach(area, "FXSource", const.efMarker, FXSourceUpdate, ...) |
|
ResumePassEdits("FXSource") |
|
end |
|
|
|
function FXSourceUpdateOnGameStateChange(delay) |
|
if GetMap() == "" then |
|
return |
|
end |
|
delay = delay or GameTime() == 0 and 0 or config.MapSoundUpdateDelay or 1000 |
|
DeleteThread(FXSourceUpdateThread) |
|
FXSourceUpdateThread = CreateGameTimeThread(function(delay) |
|
if delay <= 0 then |
|
FXSourceUpdateAll("map", "game_state_changed") |
|
else |
|
local boxes = GetMapBoxesCover(config.MapSoundBoxesCoverParts or 8, "MapSoundBoxesCover") |
|
local count = #boxes |
|
for i, box in ipairs(boxes) do |
|
FXSourceUpdateAll(box, "game_state_changed") |
|
Sleep((i + 1) * delay / count - i * delay / count) |
|
end |
|
end |
|
FXSourceUpdateThread = false |
|
end, delay) |
|
end |
|
|
|
function OnMsg.ChangeMapDone() |
|
FXSourceUpdateOnGameStateChange() |
|
end |
|
|
|
function OnMsg.GameStateChanged(changed) |
|
if ChangingMap or GetMap() == "" then return end |
|
local GameStateDefs, FXSourceStates = GameStateDefs, FXSourceStates |
|
if not FXSourceStates then return end |
|
for id in sorted_pairs(changed) do |
|
if GameStateDefs[id] and (FXSourceStates[id] or 0) > 0 then |
|
FXSourceUpdateOnGameStateChange() |
|
break |
|
end |
|
end |
|
end |
|
|
|
if Platform.developer then |
|
|
|
local function ReplaceWithSources(objs, fx_src_preset) |
|
if #(objs or "") == 0 then |
|
return 0 |
|
end |
|
XEditorUndo:BeginOp{ objects = objs, name = "ReplaceWithFXSource" } |
|
editor.ClearSel() |
|
local sources = {} |
|
for _, obj in ipairs(objs) do |
|
local pos, axis, angle, scale, coll = obj:GetPos(), obj:GetAxis(), obj:GetAngle(), obj:GetScale(), obj:GetCollectionIndex() |
|
DoneObject(obj) |
|
local src = PlaceObject("FXSource") |
|
src:SetGameFlags(const.gofPermanent) |
|
src:SetAxisAngle(axis, angle) |
|
src:SetScale(scale) |
|
src:SetPos(pos) |
|
src:SetCollectionIndex(coll) |
|
src:SetFxPreset(fx_src_preset) |
|
sources[#sources + 1] = src |
|
end |
|
Msg("EditorCallback", "EditorCallbackPlace", sources) |
|
editor.AddToSel(sources) |
|
XEditorUndo:EndOp(sources) |
|
return #sources |
|
end |
|
|
|
function ReplaceMapSounds(snd_name, fx_src_preset) |
|
local objs = MapGet("map", "SoundSource", function(obj) |
|
for _, entry in ipairs(obj.Sounds) do |
|
if entry.Sound == snd_name then |
|
return true |
|
end |
|
end |
|
end) |
|
local count = ReplaceWithSources(objs, fx_src_preset) |
|
print(count, "sounds replaced and selected") |
|
end |
|
|
|
function ReplaceMapParticles(prtcl_name, fx_src_preset) |
|
local objs = MapGet("map", "ParSystem", function(obj) |
|
return obj:GetParticlesName() == prtcl_name |
|
end) |
|
local count = ReplaceWithSources(objs, fx_src_preset) |
|
print(count, "particles replaced and selected") |
|
end |
|
|
|
end |
|
|