File size: 12,043 Bytes
b6a38d7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
DefineClass.AnimChangeHook =
{
	__parents = { "Object", "Movable" },
}

function AnimChangeHook:AnimationChanged(channel, old_anim, flags, crossfade)
end

function AnimChangeHook:SetState(anim, flags, crossfade, ...)
	local old_anim = self:GetStateText()
	if IsValid(self) and self:IsAnimEnd() then
		self:OnAnimMoment("end")
	end
	Object.SetState(self, anim, flags, crossfade, ...)
	self:AnimationChanged(1, old_anim, flags, crossfade)
end

local pfStep = pf.Step
local pfSleep = Sleep
function AnimChangeHook:Step(...)
	local old_state = self:GetState()
	local status, new_path = pfStep(self, ...)
	if old_state ~= self:GetState() then
		self:AnimationChanged(1, GetStateName(old_state), 0, nil)
	end
	return status, new_path
end

function AnimChangeHook:SetAnim(channel, anim, flags, crossfade, ...)
	local old_anim = self:GetStateText()
	Object.SetAnim(self, channel, anim, flags, crossfade, ...)
	self:AnimationChanged(channel, old_anim, flags, crossfade)
end

-- AnimMomentHook
DefineClass.AnimMomentHook =
{
	__parents = { "AnimChangeHook" },
	anim_moments_hook = false,				-- list with moments which have registered callback in the class
	anim_moments_single_thread = false,	-- if false every moment will have its own thread launched
	anim_moments_hook_threads = false,
	anim_moment_fx_target = false,
}

function AnimMomentHook:Init()
	self:StartAnimMomentHook()
end

function AnimMomentHook:Done()
	self:StopAnimMomentHook()
end

function AnimMomentHook:IsStartedAnimMomentHook()
	return self.anim_moments_hook_threads and true or false
end

function AnimMomentHook:WaitAnimMoment(moment)
	repeat
		local t = self:TimeToMoment(1, moment)
		local index = 1
		while t == 0 do
			index = index + 1
			t = self:TimeToMoment(1, moment, index)
		end
	until not WaitWakeup(t) -- if someone wakes us up we need to measure again
end

moment_hooks = {}

function AnimMomentHook:OnAnimMoment(moment, anim)
	anim = anim or GetStateName(self)
	PlayFX(FXAnimToAction(anim), moment, self, self.anim_moment_fx_target or nil)
	local anim_moments_hook = self.anim_moments_hook
	if type(anim_moments_hook) == "table" and anim_moments_hook[moment] then
		local method = moment_hooks[moment]
		return self[method](self, anim)
	end
end

function WaitTrackMoments(obj, callback, ...)
	callback = callback or obj.OnAnimMoment
	local last_state, last_phase, state_name, time, moment
	while true do
		local state, phase = obj:GetState(), obj:GetAnimPhase()
		if state ~= last_state then
			state_name = GetStateName(state)
			if phase == 0 then
				callback(obj, "start", state_name, ...)
			end
			time = nil
		end
		last_state, last_phase = state, phase
		if not time then
			moment, time = obj:TimeToNextMoment(1, 1)
		end
		if time then
			local time_to_end = obj:TimeToAnimEnd()
			if time_to_end <= time then
				if not WaitWakeup(time_to_end) then
					assert(IsValid(obj))
					callback(obj, "end", state_name, ...)
					if obj:IsAnimLooping(1) then
						callback(obj, "start", state_name, ...)
					end
					time = time - time_to_end
				else
					time = false
				end
			end
			-- if someone wakes us we need to query for a new moment
			if time then
				if time > 0 and WaitWakeup(time) then
					time = nil
				else
					assert(IsValid(obj))
					local index = 1
					repeat
						callback(obj, moment, state_name, ...)
						index = index + 1
						moment, time = obj:TimeToNextMoment(1, index)
					until time ~= 0
					if not time then
						WaitWakeup()
					end
				end
			end
		else
			WaitWakeup()
		end
	end
end

local gofRealTimeAnim = const.gofRealTimeAnim

function AnimMomentHook:StartAnimMomentHook()
	local moments = self.anim_moments_hook
	if not moments or self.anim_moments_hook_threads then
		return
	end
	if not IsValidEntity(self:GetEntity()) then 
		return 
	end
	local create_thread = self:GetGameFlags(gofRealTimeAnim) ~= 0 and CreateMapRealTimeThread or CreateGameTimeThread
	local threads
	if self.anim_moments_single_thread then
		threads = { create_thread(WaitTrackMoments, self) }
		ThreadsSetThreadSource(threads[1], "AnimMoment")
	else
		threads = { table.unpack(moments) }
		for _, moment in ipairs(moments) do
			threads[i] = create_thread(function(self, moment)
				local method = moment_hooks[moment]
				while true do
					self:WaitAnimMoment(moment)
					assert(IsValid(self))
					self[method](self)
				end
			end, self, moment)
			ThreadsSetThreadSource(threads[i], "AnimMoment")
		end
	end
	self.anim_moments_hook_threads = threads
end

function AnimMomentHook:StopAnimMomentHook()
	local thread_list = self.anim_moments_hook_threads or ""
	for i = 1, #thread_list do
		DeleteThread(thread_list[i])
	end
	self.anim_moments_hook_threads = nil
end

function AnimMomentHook:AnimMomentHookUpdate()
	for i, thread in ipairs(self.anim_moments_hook_threads) do
		Wakeup(thread)
	end
end

AnimMomentHook.AnimationChanged = AnimMomentHook.AnimMomentHookUpdate

function OnMsg.ClassesPostprocess()
	local str_to_moment_list = {} -- optimized to have one copy of each unique moment list

	ClassDescendants("AnimMomentHook", function(class_name, class, remove_prefix, str_to_moment_list)
		local moment_list
		for name, func in pairs(class) do
			local moment = remove_prefix(name, "OnMoment")
			if type(func) == "function" and moment and moment ~= "" then
				moment_list = moment_list or {}
				moment_list[#moment_list + 1] = moment
			end
		end
		for name, func in pairs(getmetatable(class)) do
			local moment = remove_prefix(name, "OnMoment")
			if type(func) == "function" and moment and moment ~= "" then
				moment_list = moment_list or {}
				moment_list[#moment_list + 1] = moment
			end
		end
		if moment_list then
			table.sort(moment_list)
			for _, moment in ipairs(moment_list) do
				moment_list[moment] = true
				moment_hooks[moment] = moment_hooks[moment] or ("OnMoment" .. moment)
			end
			local str = table.concat(moment_list, " ")
			moment_list = str_to_moment_list[str] or moment_list
			str_to_moment_list[str] = moment_list
			rawset(class, "anim_moments_hook", moment_list)
		end
	end, remove_prefix, str_to_moment_list)
end

---
DefineClass.StepObjectBase =
{
	__parents = { "AnimMomentHook" },
}

function StepObjectBase:StopAnimMomentHook()
	AnimMomentHook.StopAnimMomentHook(self)
end

if not Platform.ged then
	function OnMsg.ClassesGenerate()
		AppendClass.EntitySpecProperties = {
			properties = {
				{ id = "FXTargetOverride", name = "FX target override", category = "Misc", default = false,
					editor = "combo", items = function(fx) return ActionFXClassCombo(fx) end, entitydata = true,
				},
				{ id = "FXTargetSecondary", name = "FX target secondary", category = "Misc", default = false,
					editor = "combo", items = function(fx) return ActionFXClassCombo(fx) end, entitydata = true,
				},
			},
		}
	end
end

function GetObjMaterialFXTarget(obj)
	local entity_data = obj and EntityData[obj:GetEntity()]
	entity_data = entity_data and entity_data.entity
	if entity_data and entity_data.FXTargetOverride then
		return entity_data.FXTargetOverride, entity_data.FXTargetSecondary
	end

	local mat_type = obj and obj:GetMaterialType()
	local material_preset = mat_type and (Presets.ObjMaterial.Default or empty_table)[mat_type]
	local fx_target = (material_preset and material_preset.FXTarget ~= "") and material_preset.FXTarget or mat_type
	
	return fx_target, entity_data and entity_data.FXTargetSecondary
end

local surface_fx_types = {}
local enum_decal_water_radius = const.AnimMomentHookEnumDecalWaterRadius

function GetObjMaterial(pos, obj, surfaceType, fx_target_secondary)
	local surfacePos = pos
	if not surfaceType and obj then
		surfaceType, fx_target_secondary = GetObjMaterialFXTarget(obj)
	end

	local propagate_above
	if pos and not surfaceType then
		propagate_above = true
		if terrain.IsWater(pos) then
			local water_z = terrain.GetWaterHeight(pos)
			local dz = (pos:z() or terrain.GetHeight(pos)) - water_z
			if dz >= const.FXWaterMinOffsetZ and dz <= const.FXWaterMaxOffsetZ then
				if const.FXShallowWaterOffsetZ > 0 and dz > -const.FXShallowWaterOffsetZ then
					surfaceType = "ShallowWater"
				else
					surfaceType = "Water"
				end
				surfacePos = pos:SetZ(water_z)
			end
		end
		if not surfaceType and enum_decal_water_radius then
			local decal = MapFindNearest(pos, pos, enum_decal_water_radius, "TerrainDecal", function (obj, pos)
				if pos:InBox2D(obj) then
					local dz = (pos:z() or terrain.GetHeight(pos)) - select(3, obj:GetVisualPosXYZ())
					if dz <= const.FXDecalMaxOffsetZ and dz >= const.FXDecalMinOffsetZ then
						return true
					end
				end
			end, pos)
			if decal then
				surfaceType = decal:GetMaterialType()
				if surfaceType then
					surfacePos = pos:SetZ(select(3, decal:GetVisualPosXYZ()))
				end
			end
		end
		if not surfaceType then
			-- get the surface type
			local walkable_slab = const.SlabSizeX and WalkableSlabByPoint(pos) or GetWalkableObject(pos)
			if walkable_slab then
				surfaceType = walkable_slab:GetMaterialType()
				if surfaceType then
					surfacePos = pos:SetZ(select(3, walkable_slab:GetVisualPosXYZ()))
				end
			else
				local terrain_preset = TerrainTextures[terrain.GetTerrainType(pos)]
				surfaceType = terrain_preset and terrain_preset.type
				if surfaceType then
					surfacePos = pos:SetTerrainZ()
				end
			end
		end
	end
	
	local fx_type
	if surfaceType then
		fx_type = surface_fx_types[surfaceType]
		if not fx_type then			-- cache it for later use
			fx_type = "Surface:" .. surfaceType
			surface_fx_types[surfaceType] = fx_type
		end
	end
	local fx_type_secondary
	if fx_target_secondary then
		fx_type_secondary = surface_fx_types[fx_target_secondary]
		if not fx_type_secondary then			-- cache it for later use
			fx_type_secondary = "Surface:" .. fx_target_secondary
			surface_fx_types[fx_target_secondary] = fx_type_secondary
		end
	end

	return fx_type, surfacePos, propagate_above, fx_type_secondary
end


local enum_bush_radius = const.AnimMomentHookTraverseVegetationRadius

function StepObjectBase:PlayStepSurfaceFX(foot, spot_name)
	local spot = self:GetRandomSpot(spot_name)
	local pos = self:GetSpotLocPos(spot)
	local surface_fx_type, surface_pos, propagate_above = GetObjMaterial(pos)

	if surface_fx_type then
		local angle, axis = self:GetSpotVisualRotation(spot)
		local dir = RotateAxis(axis_x, axis, angle)
		local actionFX = self:GetStepActionFX()
		PlayFX(actionFX, foot, self, surface_fx_type, surface_pos, dir)
	end

	if propagate_above and enum_bush_radius then
		local bushes = MapGet(pos, enum_bush_radius, "TraverseVegetation", function(obj, pos) return pos:InBox(obj) end, pos)
		if bushes and bushes[1] then
			local veg_event = PlaceObject("VegetationTraverseEvent")
			veg_event:SetPos(pos)
			veg_event:SetActors(self, bushes)
		end
	end
end

function StepObjectBase:GetStepActionFX()
	return "Step"
end

DefineClass.StepObject = {
	__parents = { "StepObjectBase" },
}

function StepObject:OnMomentFootLeft()
	self:PlayStepSurfaceFX("FootLeft", "Leftfoot")
end

function StepObject:OnMomentFootRight()
	self:PlayStepSurfaceFX("FootRight", "Rightfoot")
end

function OnMsg.GatherFXActions(list)
	list[#list+1] = "Step"
end

function OnMsg.GatherFXTargets(list)
	local added = {}
	ForEachPreset("TerrainObj", function(terrain_preset)
		local type = terrain_preset.type
		if type ~= "" and not added[type] then
			list[#list+1] = "Surface:" .. type
			added[type] = true
		end
	end)
	local material_types = PresetsCombo("ObjMaterial")()
	for i = 2, #material_types do
		local type = material_types[i]
		if not added[type] then
			list[#list+1] = "Surface:" .. type
			added[type] = true
		end
	end
end

DefineClass.AutoAttachAnimMomentHookObject = {
	__parents = {"AutoAttachObject", "AnimMomentHook"},
	
	anim_moments_single_thread = true,
	anim_moments_hook = true,
}

function AutoAttachAnimMomentHookObject:SetState(...)
	AutoAttachObject.SetState(self, ...)
	AnimMomentHook.SetState(self, ...)
end

function AutoAttachAnimMomentHookObject:OnAnimMoment(moment, anim)
	return AnimMomentHook.OnAnimMoment(self, moment, anim)
end