|
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()) |
|
|
|
|
|
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, ...) |
|
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(...) |
|
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) |
|
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) |
|
return spot |
|
end |
|
|
|
function CalcStainParamsFromShot(target, attacker, hit) |
|
|
|
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 |
|
|
|
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 |
|
local StainChanceStanding = 10 |
|
local StainChanceCrouch = 80 |
|
local StainChanceProne = 100 |
|
local StainClearChance = 90 |
|
|
|
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 |
|
return true |
|
end |
|
end |
|
|
|
function Unit:WalkUpdateStains(foot) |
|
|
|
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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
if self.stance == "Prone" then |
|
local spot = (foot == "left") and "Shoulderl" or "Shoulderr" |
|
if check_stain_update_timer(stain_update_times, spot, time) and AsyncRand(100) < StainClearChance then |
|
self:WashStainsFromSpot(spot) |
|
end |
|
end |
|
return |
|
end |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
if self.stance == "Prone" then |
|
local spot = (foot == "left") and "Shoulderl" or "Shoulderr" |
|
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 |