File size: 10,978 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
DefineClass.UnitStain = {
	__parents = { "InitDone", "SkinDecalData" },
	properties = {
		{ id = "SpotIdx", editor = "number", default = -1 },
		{ id = "Rotation", editor = "number", default = -1 },
		{ id = "initialized", editor = "bool", default = false },
	},
	decal = false,
}

function UnitStain:Done()
	if self.decal then
		DoneObject(self.decal)
		self.decal = nil
	end
end

function UnitStainPresetName(base_entity, stain_type, spot)
	return string.format("%s-%s-%s", spot, stain_type, base_entity)
end

function CopyUnitStainProperties(from, to)
	local props = from:GetProperties()
	for _, prop in ipairs(props) do
		if to:GetPropertyMetadata(prop.id) then
			to:SetProperty(prop.id, from:GetProperty(prop.id))
		end
	end	
end

function UnitStain:InitParams(unit, params)
	if self.Spot == "" or self.DecType == "" then
		return
	end
	
	local base_entity = GetAnimEntity(unit:GetEntity(), unit:GetState())
	
	-- check for existing preset
	local id = UnitStainPresetName(base_entity, self.DecType, self.Spot)
	local preset = Presets.SkinDecalMetadata.Default and Presets.SkinDecalMetadata.Default[id]
	local base = Presets.SkinDecalType.Default[self.DecType]
	if preset then
		CopyUnitStainProperties(preset, self)
	elseif base then
		self.DecEntity = base.DefaultEntity
		self.DecScale = base.DefaultScale
	else
		print("unknown stain type: ", self.DecType)
	end
	
	local min, max = unit:GetSpotRange(self.Spot)	
	if min < 0 or max < 0 then
		return
	end
	self.SpotIdx = min + AsyncRand(max - min)
	self.Rotation = self.DecAttachAngleRange.from*60 + AsyncRand(self.DecAttachAngleRange.to*60 - self.DecAttachAngleRange.from*60)
	
	for param, value in pairs(params) do
		if self:GetPropertyMetadata(param) then
			self:SetProperty(param, value)
		end
	end
	
	self.initialized = true
	return true
end

function UnitStain:Apply(unit, params)
	if self.Spot == "" or self.DecType == "" then
		return
	end
	if self.decal then
		DoneObject(self.decal)
		self.decal = nil
	end
	
	if self.initialized then
		local min, max = unit:GetSpotRange(self.Spot)
		if self.SpotIdx < min or self.SpotIdx > max then
			self.initialized = false
		end
	end

	if not self.initialized and not self:InitParams(unit, params) then
		return
	end

	local dec = PlaceObject("SkinDecal")
	dec:ChangeEntity(self.DecEntity)
	unit:Attach(dec, self.SpotIdx, true)

	local axis, angle = ComposeRotation(axis_y, self.InvertFacing and -90*60 or 90*60, SkinDecalAttachAxis[self.DecAttachAxis], self.Rotation)

	dec:SetAttachAxis(axis)
	dec:SetAttachAngle(angle)
	dec:SetScale(self.DecScale)
	dec:SetAttachOffset(point(self.DecOffsetX * (self.InvertFacing and -1 or 1), self.DecOffsetY, self.DecOffsetZ))
	dec:SetColorModifier(self.ClrMod)
	self.decal = dec
	return dec
end

function Unit:AddStain(stain_type, spot, params)
	local stain = UnitStain:new()
	stain.DecType = stain_type
	stain.Spot = spot
	
	self.stains = self.stains or {}
	table.insert(self.stains, stain)
		
	stain:Apply(self, params)	
	
	return stain
end

function Unit:ClearStains(stain_type, ...) -- spots to clear
	if not self.stains then return end
	local nspots = select("#", ...)
	for i = #self.stains, 1, -1 do
		local stain = self.stains[i]
		if stain.DecType == stain_type then
			local match
			for j = 1, nspots do
				local spot = select(j, ...)
				if spot == stain.Spot then
					match = true
					break
				end
			end
			if nspots == 0 or match then
				DoneObject(stain)
				table.remove(self.stains, i)
			end
		end
	end
end

function Unit:ClearStainsFromSpots(...) -- spots to clear
	if not self.stains then return end	
	local nspots = select("#", ...)
	for i = #self.stains, 1, -1 do
		local stain = self.stains[i]
		local match
		for j = 1, nspots do
			local spot = select(j, ...)
			if spot == stain.Spot then
				match = true
				break
			end
		end
		if nspots == 0 or match then
			DoneObject(stain)
			table.remove(self.stains, i)
		end		
	end
end

function Unit:WashStainsFromSpot(spot) -- cleared by water, checks ClearedByWater flag from stain type
	if not self.stains then return end	
	for i = #self.stains, 1, -1 do
		local stain = self.stains[i]
		if (not spot or stain.Spot == spot) and SkinDecalTypes[stain.DecType] and SkinDecalTypes[stain.DecType].ClearedByWater then
			DoneObject(stain)
			table.remove(self.stains, i)
		end
	end
end

function Unit:CanStain(stain_type, spot)
	local target_prio = SkinDecalTypes[stain_type] and SkinDecalTypes[stain_type].SortKey or 0
	for _, stain in ipairs(self.stains) do
		if stain.Spot == spot then
			local curr_prio = SkinDecalTypes[stain.DecType] and SkinDecalTypes[stain.DecType].SortKey or 0
			if curr_prio >= target_prio then
				return false
			end
		end
	end
	return true
end

function Unit:HasStainType(stain_type)
	for _, stain in ipairs(self.stains) do
		if stain.DecType == stain_type then
			return true
		end
	end
end

local StainSpotGroups = {
	Head = { "Head", "Neck" },
	Torso = { "Ribslowerl", "Ribslowerr", "Ribsupperl", "Ribsupperr", "Torso", "Shoulderl", "Shoulderr" },
	Groin = { "Groin", "Pelvisl", "Pelvisr" },
	Arms = { "Shoulderl", "Shoulderr", "Elbowl", "Elbowr", "Wristl", "Wristr" },
	Legs = { "Kneel", "Kneer", "Anklel", "Ankler" },
	[false] = { "Ribslowerl", "Ribslowerr", "Ribsupperl", "Ribsupperr", "Torso", "Shoulderl", "Shoulderr", "Groin", "Pelvisl", "Pelvisr" },
}

function CheckStainSpotGroups()
	if not SelectedObj then return end
	for group, list in pairs(StainSpotGroups) do
		for _, spot in ipairs(list) do
			if not SelectedObj:HasSpot(spot) then
				printf("Invalid spot %s in group %s", spot, group)
			end
		end
	end
end

function GetRandomStainSpot(spot_group)
	local spot_group = StainSpotGroups[spot_group or false] or StainSpotGroups[false]
	local spot = table.rand(spot_group) -- cut off the returned index
	return spot
end

function CalcStainParamsFromShot(target, attacker, hit)
	-- find the nearest spot to the hit position, calculate the offset/orientation/facing to match it from that spot
	local spots_data = GetEntitySpots(target:GetEntity())
	local nearest_spot, nearest_dist, nearest_idx
	local hit_pos = hit.pos or target:GetSpotLocPos(target:GetSpotBeginIndex("Torso"))
	local attack_dir = SetLen(hit.shot_dir or (hit_pos - attacker:GetPos()), guim)
	for spot, spot_indices in pairs(spots_data) do
		for _, spot_idx in ipairs(spot_indices) do
			local pos, angle, axis, scale = target:GetSpotLoc(spot_idx)
			local dist = pos:Dist(hit_pos)
			if not nearest_dist or dist < nearest_dist then
				nearest_spot, nearest_dist, nearest_idx = spot, dist, spot_idx
			end
		end
	end
	--printf("nearest spot: %s (%d)", tostring(nearest_spot), nearest_idx or -1)
	if nearest_idx then
		local pos, angle, axis, scale = target:GetSpotLoc(nearest_idx)
		local spot_x = RotateAxis(point(guim, 0, 0), axis, angle)
		local spot_y = RotateAxis(point(0, guim, 0), axis, angle)
		local spot_z = RotateAxis(point(0, 0, guim), axis, angle)
		local invert_facing = false
		if Dot2D(spot_x, attack_dir) > 0 then
			invert_facing = true
		end

		local v = hit_pos - pos
		local ox = Dot(spot_x, v) / guim
		local oy = Dot(spot_y, v) / guim
		local oz = Dot(spot_z, v) / guim
		
		return nearest_spot, {
			InvertFacing = invert_facing,
			DecOffsetX = ox,
			DecOffsetY = oy,
			DecOffsetZ = oz,
			DecScale = (hit.impact_force or 0) > 0 and 100 or 60,
		}
	end
end

local StainApplyInterval = 3000 -- minimum time before rechecking if we should apply a stain on the same spot
local StainChanceStanding = 10
local StainChanceCrouch = 80
local StainChanceProne = 100
local StainClearChance = 90 -- when moving in water/shallowwater

local function check_stain_update_timer(stain_update_times, spot, time)
	local update_time = stain_update_times[spot] or time
	if time >= update_time then
		stain_update_times[spot] = time + StainApplyInterval -- timestamp the update
		return true
	end
end

function Unit:WalkUpdateStains(foot)
	-- ATTN: FX code, keep it async
	local surf_fx_type = GetObjMaterial(self:GetVisualPos())
	local time = GameTime()
	local stain_update_times = self.stain_update_times

	if surf_fx_type == "Surface:Water" or surf_fx_type == "Surface:ShallowWater" then -- clearing stains instead of adding
		-- feet spots (any stance)
		local spot = (foot == "left") and "Leftfoot" or "Rightfoot"
		if check_stain_update_timer(stain_update_times, spot, time) and AsyncRand(100) < StainClearChance then
			self:WashStainsFromSpot(spot)
		end		
		-- knee spots (Prone/Crouch)
		if self.stance ~= "Standing" then
			local spot = (foot == "left") and "Kneel" or "Kneer"
			if check_stain_update_timer(stain_update_times, spot, time) and AsyncRand(100) < StainClearChance then
				self:WashStainsFromSpot(spot)
			end
		end
		-- shoulder spots (Prone only)
		if self.stance == "Prone" then
			local spot = (foot == "left") and "Shoulderl" or "Shoulderr" -- maybe reverse the shoulders?
			if check_stain_update_timer(stain_update_times, spot, time) and AsyncRand(100) < StainClearChance then
				self:WashStainsFromSpot(spot)
			end
		end
		return
	end
		
	-- adding mud/dirt
	local stain_type
	if surf_fx_type == "Surface:Mud" then
		stain_type = "Mud"
	elseif surf_fx_type == "Surface:Dirt" or surf_fx_type == "Surface:Sand" then
		stain_type = "Dirt"
	end	
	if not stain_type then return end
	
	local stain_chance = StainChanceStanding
	if self.stance == "Crouch" then
		stain_chance = StainChanceCrouch
	elseif self.stance == "Prone" then
		stain_chance = StainChanceProne
	end
	
	-- feet spots (any stance)
	local spot = (foot == "left") and "Leftfoot" or "Rightfoot"
	if check_stain_update_timer(stain_update_times, spot, time) and AsyncRand(100) < stain_chance and self:CanStain(stain_type, spot) then
		self:ClearStainsFromSpots(spot)
		self:AddStain(stain_type, spot)
	end
	
	-- knee spots (Prone/Crouch)
	if self.stance ~= "Standing" then
		local spot = (foot == "left") and "Kneel" or "Kneer"
		if check_stain_update_timer(stain_update_times, spot, time) and AsyncRand(100) < stain_chance and self:CanStain(stain_type, spot) then
			self:ClearStainsFromSpots(spot)
			self:AddStain(stain_type, spot)
		end
	end
	
	-- shoulder spots (Prone only)
	if self.stance == "Prone" then
		local spot = (foot == "left") and "Shoulderl" or "Shoulderr" -- maybe reverse the shoulders?
		if check_stain_update_timer(stain_update_times, spot, time) and AsyncRand(100) < stain_chance and self:CanStain(stain_type, spot) then
			self:ClearStainsFromSpots(spot)
			self:AddStain(stain_type, spot)
		end
	end
end

function Unit:OnMomentFootLeft()
	if self.team and self.team.side ~= "neutral" then 
		self:WalkUpdateStains("left")	
	end
	return StepObject.OnMomentFootLeft(self)
end

function Unit:OnMomentFootRight()
	if self.team and self.team.side ~= "neutral" then 
		self:WalkUpdateStains("right")
	end
	return StepObject.OnMomentFootRight(self)
end

function OnMsg.EnterSector()
	for _, unit in ipairs(g_Units) do
		if not unit:IsDead() and not unit:HasStatusEffect("Wounded") then
			unit:ClearStains("Blood")
		end
	end
end