Download Lua/Tactical/UnitStain.lua from sirnii/myspace: direct link, hf CLI and curl.
- Browser
- Download file 11 kB
-
https://huggingface.co/spaces/sirnii/myspace/resolve/main/Lua/Tactical/UnitStain.lua
- Command line
-
hf download hf://spaces/sirnii/myspace/Lua/Tactical/UnitStain.lua
-
curl -L -o UnitStain.lua https://huggingface.co/spaces/sirnii/myspace/resolve/main/Lua/Tactical/UnitStain.lua
11 kB
| 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 |