Download Lua/Tactical/UnitAmbientLife.lua from sirnii/myspace: direct link, hf CLI and curl.
- Browser
- Download file 42.8 kB
-
https://huggingface.co/spaces/sirnii/myspace/resolve/main/Lua/Tactical/UnitAmbientLife.lua
- Command line
-
hf download hf://spaces/sirnii/myspace/Lua/Tactical/UnitAmbientLife.lua
-
curl -L -o UnitAmbientLife.lua https://huggingface.co/spaces/sirnii/myspace/resolve/main/Lua/Tactical/UnitAmbientLife.lua
42.8 kB
| DefineClass.AmbientLifeZoneUnit = { | |
| zone = false, | |
| } | |
| local function MapGroups(map, groups) | |
| for _, group in ipairs(groups) do | |
| map[group] = true | |
| end | |
| end | |
| function AmbientLifeZoneUnit:GetGroupsMap() | |
| local groups_map = {} | |
| if self.zone then | |
| MapGroups(groups_map, self.zone.Groups) | |
| end | |
| MapGroups(groups_map, self.Groups) | |
| return groups_map | |
| end | |
| function AmbientLifeZoneUnit:GroupsMatch(other, groups_map) | |
| groups_map = groups_map or self:GetGroupsMap() | |
| if not next(groups_map) then return true end | |
| for _, group in ipairs(other.Groups) do | |
| if groups_map[group] then | |
| return true | |
| end | |
| end | |
| end | |
| local function AnimationsCombo(obj) | |
| obj = GetParentTableOfKind(obj, "AnimationSet") | |
| if not obj then return end | |
| if not obj.Entity then return false, function() return true end end | |
| local states = GetStates(obj.Entity) | |
| for i = #states, 1, -1 do | |
| local state = states[i] | |
| if string.starts_with(state, "_") or IsErrorState(obj.Entity, GetStateIdx(state)) then | |
| table.remove(states, i) | |
| end | |
| end | |
| table.sort(states) | |
| return states | |
| end | |
| local function FindAnimTriplet(obj, base_anim) | |
| base_anim = string.lower(base_anim) | |
| local anim_start = base_anim .. "_start" | |
| local anim_idle = base_anim .. "_idle" | |
| local anim_end = base_anim .. "_end" | |
| local anims = AnimationsCombo(obj) | |
| local anim_start_found, anim_idle_found, anim_end_found | |
| for _, anim in ipairs(anims) do | |
| anim_start_found = anim_start_found or ((anim_start == string.lower(anim)) and anim) | |
| anim_idle_found = anim_idle_found or ((anim_idle == string.lower(anim)) and anim) | |
| anim_end_found = anim_end_found or ((anim_end == string.lower(anim)) and anim) | |
| if anim_start_found and anim_idle_found and anim_end_found then | |
| return anim_start_found, anim_idle_found, anim_end_found | |
| end | |
| end | |
| end | |
| DefineClass.AnimationWeight = { | |
| __parents = {"PropertyObject"}, | |
| properties = { | |
| {id = "AnimStart", name = "Animation Start", editor = "dropdownlist", default = false, items = AnimationsCombo, | |
| help = "Play this BEFORE the random animation", | |
| }, | |
| {id = "Animation", "Animation", editor = "dropdownlist", default = false, items = AnimationsCombo, | |
| help = "Animation to play", | |
| }, | |
| {id = "AnimEnd", name = "Animation End", editor = "dropdownlist", default = false, items = AnimationsCombo, | |
| help = "Play this AFTER the random animation", | |
| }, | |
| {id = "Weight", name = "Weight", editor = "number", default = 100, min = 0, | |
| help = "Used for the weighted random.", | |
| }, | |
| {id = "GameStates", name = "Game States", editor = "set", default = false, three_state = true, | |
| items = function() return GetGameStateFilter() end, | |
| help = "The animation can be played only when these GameState requirements are met", | |
| }, | |
| }, | |
| } | |
| function AnimationWeight:GetEditorView() | |
| return Untranslated(string.format("%s (Weight: %d)", self.Animation or "No Animation", self.Weight)) | |
| end | |
| function AnimationWeight:OnEditorSetProperty(prop_id) | |
| local anim_start_found, anim_idle_found, anim_end_found | |
| if prop_id == "AnimStart" then | |
| local value = string.lower(self:GetProperty("AnimStart")) | |
| local base_anim = string.match(value, "(.*)_start$") | |
| if not base_anim then return end | |
| anim_start_found, anim_idle_found, anim_end_found = FindAnimTriplet(self, base_anim) | |
| elseif prop_id == "Animation" then | |
| local value = string.lower(self:GetProperty("Animation")) | |
| local base_anim = string.match(value, "(.*)_idle$") | |
| if not base_anim then return end | |
| anim_start_found, anim_idle_found, anim_end_found = FindAnimTriplet(self, base_anim) | |
| elseif prop_id == "AnimEnd" then | |
| local value = string.lower(self:GetProperty("AnimEnd")) | |
| local base_anim = string.match(value, "(.*)_end$") | |
| if not base_anim then return end | |
| anim_start_found, anim_idle_found, anim_end_found = FindAnimTriplet(self, base_anim) | |
| end | |
| if anim_start_found and anim_idle_found and anim_start_found then | |
| self:SetProperty("AnimStart", anim_start_found) | |
| self:SetProperty("Animation", anim_idle_found) | |
| self:SetProperty("AnimEnd", anim_end_found) | |
| end | |
| end | |
| DefineClass.AnimationSet = { | |
| __parents = {"Preset"}, | |
| properties = { | |
| {id = "Entity", name = "Entity", editor = "dropdownlist", default = "Male", | |
| items = function() return GetAllAnimatedEntities("CharacterEntity") end}, | |
| help = "Used only for filtering the available animations", | |
| {id = "AnimIdle", name = "Animation Idle", editor = "nested_list", default = false, base_class = "AnimationWeight", | |
| inclusive = true, help = "Animation to play choosen via weighted-random.", | |
| }, | |
| }, | |
| } | |
| function AnimationSet:GetAnimationsChances(unit) | |
| local total_chance = 0 | |
| local slots = {} | |
| for _, entry in ipairs(self.AnimIdle) do | |
| if MatchGameState(entry.GameStates) and (not unit:HasMember("CanPlay") or unit:CanPlay(entry)) then | |
| total_chance = total_chance + entry.Weight | |
| table.insert(slots, { | |
| total_chance = total_chance, | |
| AnimStart = entry.AnimStart, | |
| Animation = entry.Animation, | |
| AnimEnd = entry.AnimEnd, | |
| }) | |
| end | |
| end | |
| return slots | |
| end | |
| function AnimationSet:GetRandomAnimationSet(unit) | |
| local slots = self:GetAnimationsChances(unit) | |
| if #slots > 0 then | |
| return slots[GetRandomItemByWeight(slots, unit:Random(slots[#slots].total_chance), "total_chance")] | |
| end | |
| end | |
| function AnimationSet:Play(unit, flags, crossfade) | |
| local anim_entry = self:GetRandomAnimationSet(unit) | |
| if not anim_entry then return end | |
| unit:PrePlay(anim_entry) | |
| if anim_entry.AnimStart then | |
| unit:SetState(anim_entry.AnimStart, flags or 0, crossfade or -1) | |
| Sleep(unit:TimeToAnimEnd()) | |
| end | |
| if anim_entry.Animation then | |
| unit:SetState(anim_entry.Animation, flags or 0, crossfade or -1) | |
| local time = unit:TimeToAnimEnd() | |
| if unit.zone then | |
| local step = unit:GetStepVector() | |
| local step_len = step:Len() | |
| if step_len > 0 then | |
| local area_pos = unit.zone:GetAreaPositions() | |
| if #area_pos > 0 then | |
| local pos = point(point_unpack(area_pos[1 + unit:Random(#area_pos)])) | |
| local dir = pos - unit:GetPos() | |
| if dir:Len2() > 0 then | |
| dir = SetLen(dir, step_len) | |
| unit:Face(pos, 300) | |
| unit:SetPos(unit:GetPos() + dir, time) | |
| end | |
| end | |
| end | |
| end | |
| WaitMsg(unit, time) | |
| end | |
| if anim_entry.AnimEnd then | |
| unit:SetState(anim_entry.AnimEnd, flags or 0, crossfade or -1) | |
| Sleep(unit:TimeToAnimEnd()) | |
| end | |
| unit:PostPlay(anim_entry) | |
| return anim_entry | |
| end | |
| local offset_x = const.SlabSizeX / 2 - 70 * guic | |
| local offset_y = const.SlabSizeY / 2 - 70 * guic | |
| local pt_up = point(0, -offset_y, 0) | |
| local pt_left = point(-offset_x, 0, 0) | |
| local pt_right = point(offset_x, 0, 0) | |
| local pt_down = point(0, offset_y, 0) | |
| MapVar("g_CoversReserved", {}) | |
| UnitRoutines = { "Ambient", "StandStill", "Patrol", "AdvanceTo" } | |
| function OnMsg.CombatEnd() | |
| g_CoversReserved = {} | |
| end | |
| function Unit:GetRoutineAreaMarkers() | |
| local markers | |
| if self.routine_area == "self" then | |
| if IsValid(self.routine_spawner) and not IsKindOf(self.routine_spawner, "AL_Football") then | |
| markers = {self.routine_spawner} | |
| end | |
| else | |
| local group = Groups[self.routine_area] | |
| if group and #group > 0 then | |
| markers = {} | |
| for _, m in ipairs(group) do | |
| if IsValid(m) and m:IsKindOf("GridMarker") then | |
| markers[#markers+1] = m | |
| end | |
| end | |
| else | |
| StoreErrorSource(self.routine_spawner, "Unknown or empty group %s specified for Routine Area", self.routine_area) | |
| end | |
| end | |
| return markers | |
| end | |
| function Unit:GetRandomVisitable(low_covers, filter, ...) | |
| local markers = self:GetRoutineAreaMarkers() | |
| local visitables_table = {} | |
| for _, marker in ipairs(markers) do | |
| local visitable, total = GetRandomVisitableForMarker(self, marker, filter, ...) | |
| if visitable then | |
| visitables_table[#visitables_table + 1] = { visitable = visitable, weight = total } | |
| end | |
| end | |
| if low_covers then | |
| local pos = GetPassSlab(self) or self:GetPos() | |
| local width = 10 * const.SlabSizeX | |
| local height = 10 * const.SlabSizeY | |
| local area_left = pos:x() - width / 2 | |
| local area_top = pos:y() - height / 2 | |
| local restrict_area = box(area_left, area_top, area_left + width, area_top + height) | |
| local voxels = GetCombatPathDestinations(self, pos, nil, nil, nil, nil, restrict_area, "ignore occupied", "move_through_occupied") | |
| for _, marker in ipairs(markers) do | |
| local area = marker:GetAreaBox() | |
| ForEachCover(area, const.CoverLow, function(x, y, z, up, right, down, left) | |
| local packed_pos = point_pack(x, y, z) | |
| if g_CoversReserved[packed_pos] then | |
| return | |
| end | |
| if not table.find(voxels, packed_pos) then | |
| return | |
| end | |
| if not CanOccupy(self, x, y, z) then | |
| return | |
| end | |
| local angles = {} | |
| if up == const.CoverLow then | |
| table.insert(angles, GetCoverDirAngle("up") + 180 * 60) | |
| end | |
| if right == const.CoverLow then | |
| table.insert(angles, GetCoverDirAngle("right") + 180 * 60) | |
| end | |
| if down == const.CoverLow then | |
| table.insert(angles, GetCoverDirAngle("down") + 180 * 60) | |
| end | |
| if left == const.CoverLow then | |
| table.insert(angles, GetCoverDirAngle("left") + 180 * 60) | |
| end | |
| local visitable = { false, point(x, y, z), angles, cover = true } | |
| visitables_table[#visitables_table + 1] = { visitable = visitable, weight = 1 } | |
| end) | |
| end | |
| end | |
| if #visitables_table > 0 then | |
| local selected = table.weighted_rand(visitables_table, "weight", InteractionRand(1000000, "AmbientLife")) | |
| return selected.visitable | |
| end | |
| end | |
| function Unit:ReserveVisitable(visitable) | |
| if visitable.reserved == self.handle then return end | |
| local dest = visitable[2] | |
| if visitable.cover then | |
| assert(not g_CoversReserved[point_pack(dest)], "Cover spot reserved!") | |
| g_CoversReserved[point_pack(dest)] = self.handle | |
| else | |
| assert(not visitable.reserved, "Trying to reserve already used visitable!") | |
| visitable.reserved = self.handle | |
| end | |
| end | |
| function Unit:GetVisitable() | |
| return table.find_value(g_Visitables, "reserved", self.handle) | |
| end | |
| function Unit:FreeVisitable(visitable) | |
| self.visit_reached = false | |
| self.perpetual_marker = false | |
| visitable = visitable or self:GetVisitable() | |
| if visitable then | |
| if visitable.cover then | |
| g_CoversReserved[point_pack(visitable[2])] = nil | |
| else | |
| visitable.reserved = nil | |
| end | |
| end | |
| end | |
| function Unit:TeleportToCower() | |
| local visitable = self:GetRandomVisitable("low covers") | |
| if visitable then | |
| self:ReserveVisitable(visitable) | |
| local obj, dest, lookat = table.unpack(visitable) | |
| if dest then | |
| self:SetPos(dest) | |
| else | |
| self:SetPos(obj:GetPosXYZ()) | |
| end | |
| if visitable.cover then | |
| local angle = table.rand(lookat, self:Random()) | |
| self:SetOrientationAngle(angle) | |
| elseif lookat then | |
| self:Face(lookat) | |
| else | |
| self:SetOrientationAngle(obj:GetAngle()) | |
| end | |
| end | |
| self:SetCommand("Cower") | |
| self:SetCommandParamValue("Cower", "move_anim", "Run") | |
| self:UpdateMoveAnim() | |
| end | |
| local function al_filter_ignorechair(visitable) | |
| return not IsKindOf(visitable[1], "AL_SitChair") | |
| end | |
| function Unit:AmbientRoutine() | |
| local filter | |
| if self.carry_flare or self:Random(100) < const.AmbientLife.RoamChance then | |
| -- roam filter | |
| filter = function(self, visitable) | |
| local marker = visitable[1] | |
| if not IsKindOf(marker, "AL_Roam") then | |
| return false | |
| end | |
| if self.zone then | |
| if not self.zone:CheckZTolerance(marker) then | |
| return false | |
| end | |
| if self.last_roam and IsCloser(self, marker, self.zone.MinRoamDist) then | |
| return false | |
| end | |
| end | |
| return true | |
| end | |
| elseif self.last_visit and IsKindOf(self.last_visit, "AL_SitChair") then | |
| filter = function(self, visitable) | |
| return not IsKindOf(visitable[1], "AL_SitChair") | |
| end | |
| end | |
| local visitable = self:GetRandomVisitable(nil, filter, self) | |
| if visitable then | |
| -- regular Visit | |
| --local visitable = table.rand(visitables, self:Random()) | |
| self:ReserveVisitable(visitable) | |
| self:SetCommand("Visit", visitable) | |
| else | |
| -- fallback to Roam/StandStill | |
| local markers = self:GetRoutineAreaMarkers() | |
| local area_markers = table.ifilter(markers, function(_, marker) | |
| return marker.AreaWidth * marker.AreaHeight > 0 | |
| end) | |
| if #area_markers > 0 then | |
| local marker = #area_markers == 1 and area_markers[1] or table.rand(area_markers, self:Random()) | |
| if marker.AreaWidth * marker.AreaHeight > 1 then | |
| self:TakeSlabExploration() | |
| self:SetCommand("RoamSingle", marker) | |
| else | |
| if marker.Routine ~= "StandStill" then | |
| if marker.Type == "DefenderPriority" then | |
| -- switch them silently to StandStill | |
| self.routine = "StandStill" | |
| else | |
| StoreErrorSource(marker, "Marker with 1x1 area used for Ambient Roam behavior - set a larger area!") | |
| end | |
| end | |
| end | |
| else | |
| self.routine = "StandStill" -- silently switch to StandStill because of map patching(was a VME) | |
| end | |
| end | |
| self:IdleRoutine_StandStill() | |
| end | |
| function Unit:IdleRoutine_StandStill(timeout, dont_halt) | |
| local cur_style = GetAnimationStyle(self, self.cur_idle_style) | |
| local anim_style = | |
| cur_style and cur_style.VariationGroup == "StandStill" and cur_style | |
| or GetRandomAnimationStyle(self, "StandStill") | |
| or self:GetIdleStyle() | |
| self.cur_idle_style = anim_style and anim_style.Name or nil | |
| local pos, angle = self:GetVoxelSnapPos() | |
| self:SetTargetDummyFromPos(pos, angle) | |
| Sleep(self:TimeToAngleInterpolationEnd()) | |
| self:TakeSlabExploration() | |
| if anim_style then | |
| local anim = self:GetStateText() | |
| if anim_style:HasAnimation(anim) or anim == anim_style.Start then | |
| Sleep(self:TimeToAnimEnd()) | |
| elseif GameTimeAdvanced then | |
| if (anim_style.Start or "") ~= "" and IsValidAnim(self, anim_style.Start) then | |
| self:PlayTransitionAnims(anim_style.Start) -- play possible another style End animation | |
| self:SetState(anim_style.Start, const.eKeepComponentTargets) | |
| Sleep(self:TimeToAnimEnd()) | |
| else | |
| self:PlayTransitionAnims(anim_style:GetMainAnim()) -- play possible another style End animation | |
| end | |
| end | |
| local start_time = GameTime() | |
| while not timeout or GameTime() - start_time < timeout do | |
| self:SetState(anim_style:GetRandomAnim(self), const.eKeepComponentTargets) | |
| if not GameTimeAdvanced then | |
| self:RandomizeAnimPhase() | |
| end | |
| Sleep(self:TimeToAnimEnd()) | |
| end | |
| end | |
| local base_idle = self:GetIdleBaseAnim() | |
| if self:GetStateText() ~= base_idle then | |
| if IsAnimVariant(self:GetStateText(), base_idle) and self:GetAnimPhase(1) > 0 then | |
| Sleep(self:TimeToAnimEnd()) | |
| end | |
| self:SetState(base_idle, const.eKeepComponentTargets) | |
| if not GameTimeAdvanced then | |
| self:RandomizeAnimPhase() | |
| end | |
| end | |
| if self:GetVariationsCount(base_idle) <= 1 then | |
| if dont_halt then | |
| Sleep(self:TimeToAnimEnd()) | |
| return | |
| else | |
| Halt() | |
| end | |
| end | |
| local start_time = GameTime() | |
| while not timeout or GameTime() - start_time < timeout do | |
| local time = self:RandRange(const.Combat.IdleVariantMinTime, const.Combat.IdleVariantMaxTime) | |
| Sleep(time) | |
| Sleep(self:TimeToAnimEnd()) | |
| self:SetRandomAnim(base_idle, const.eKeepComponentTargets, nil, true) | |
| Sleep(self:TimeToAnimEnd()) | |
| self:SetState(base_idle, const.eKeepComponentTargets) | |
| end | |
| end | |
| function Unit:IdleRoutine() | |
| if self:HasStatusEffect("Suspicious") then | |
| self:SuspiciousRoutine() | |
| elseif self.routine == "Ambient" then | |
| self:AmbientRoutine() | |
| elseif self.routine == "StandStill" then | |
| self:IdleRoutine_StandStill() | |
| elseif self.routine == "Patrol" then | |
| if self.routine_area == "self" then | |
| StoreErrorSource(self.routine_spawner, "Patrol routine should have waypoint markers specified") | |
| else | |
| local route = GetRouteFromMarkerGroup(self.routine_area) | |
| local min_id | |
| if #route == 0 then | |
| StoreErrorSource(self.routine_spawner, string.format("Marker group %s referenced in Patrol routine - not found", self.routine_area)) | |
| else | |
| local min_id = 1 | |
| local closest_pos = route[1][1] | |
| for i = 2, #route do | |
| local p = route[i][1] | |
| if IsCloser2D(self, p, closest_pos) then | |
| min_id = i | |
| closest_pos = p | |
| end | |
| end | |
| if not GameState.sync_loading then | |
| Sleep(self:TimeToAngleInterpolationEnd()) | |
| self:TakeSlabExploration() | |
| end | |
| self:SetCommandParamValue("Patrol", "move_anim", "Walk") | |
| self:SetCommand("Patrol", self.routine_area, min_id, "loop","end_orient") | |
| end | |
| end | |
| self:IdleRoutine_StandStill() -- fallback | |
| elseif self.routine == "AdvanceTo" then | |
| if self.routine_area == "self" then | |
| StoreErrorSource(self.routine_spawner, "AdvanceTo routine should have waypoint marker specified") | |
| else | |
| local markers = MapGetMarkers(false, self.routine_area) | |
| if not markers or #markers == 0 then | |
| StoreErrorSource(self.routine_spawner, string.format("Marker group %s referenced in AdvanceTo routine - not found", self.routine_area)) | |
| else | |
| if #markers > 1 then | |
| StoreErrorSource(self.routine_spawner, string.format("Marker group %s referenced in AdvanceTo routine - %d markers found, only one will be used", self.routine_area, #markers)) | |
| end | |
| if #(markers[1]:GetAreaPositions() or empty_table) > 0 then | |
| self:SetCommandParamValue("AdvanceTo", "move_anim", "Walk") | |
| local params = self:GetCommandParamsTbl("Idle") | |
| if params.PropagateAnimParams then | |
| self:SetCommandParams("AdvanceTo", params) | |
| end | |
| self:SetCommand("AdvanceTo", markers[1]:GetHandle()) | |
| end | |
| end | |
| end | |
| self:IdleRoutine_StandStill() -- fallback | |
| else | |
| StoreErrorSource(self, "Unknown routine %s", self.routine) | |
| Sleep(1000) | |
| end | |
| end | |
| function Unit:GetRoamPos(marker) | |
| if IsPoint(marker) then | |
| return marker | |
| end | |
| assert(IsKindOf(marker, "GridMarker")) | |
| local positions | |
| if self.zone == marker then | |
| positions = marker:GetAreaPositions("ignore_occupied", "outside_repulse", "skip_tunnels", "z_tolerance") | |
| else | |
| positions = marker:GetAreaPositions("ignore_occupied", "outside_repulse", "skip_tunnels") | |
| if self.zone then | |
| positions = self.zone:FilterZTolerance(positions) | |
| end | |
| end | |
| local count = #positions | |
| if count == 0 then | |
| return | |
| end | |
| local cur_x, cur_y = self:GetPosXYZ() | |
| local cur_angle = self:GetOrientationAngle() | |
| local tries = Min(const.AmbientLife.RoamKeepDirTries, count) | |
| local min_dist = 5 * guim | |
| local best_pos, best_angle | |
| for i = 1, tries do | |
| local idx = 1 + self:Random(count) | |
| local packed_pos = positions[idx] | |
| local x, y = point_unpack(packed_pos) | |
| if i == 1 or not IsCloser2D(cur_x, cur_y, x, y, min_dist) then | |
| local angle = abs(CalcOrientation(cur_x, cur_y, 0, x, y, 0) - cur_angle) | |
| if i == 1 or angle < best_angle then | |
| best_pos, best_angle = packed_pos, angle | |
| end | |
| end | |
| end | |
| return point(point_unpack(best_pos)) | |
| end | |
| GameVar("gv_FlareCarriers", 0) | |
| function OnMsg.EnterSector(game_start, load_game) | |
| if load_game then return end | |
| local sector = gv_Sectors[gv_CurrentSectorId] | |
| gv_FlareCarriers = sector.MinFlareCarriers + InteractionRand(sector.MaxFlareCarriers - sector.MinFlareCarriers + 1, "AmbientLifeSpawn") | |
| end | |
| function Unit:RoamAttachFlare() | |
| self:ForEachAttach("GrenadeVisual", DoneObject) | |
| local flare = PlaceObject("GrenadeVisual", {fx_actor_class = "FlareStick"}) | |
| self:Attach(flare, self:GetSpotBeginIndex("Weaponr")) | |
| flare:SetSoundMute(self:IsSoundMuted()) | |
| self.carry_flare = true | |
| self:UpdateOutfit() | |
| end | |
| function ItemFallDown(obj) | |
| if not IsValid(obj) then | |
| return | |
| end | |
| local x, y, z = FindFallDownPos(obj) | |
| if not x then | |
| local step_obj | |
| step_obj, z = WalkableSlabByPoint(obj, "downward only") | |
| if not z then | |
| return | |
| end | |
| x, y = obj:GetPosXYZ() | |
| end | |
| local z3d = z or terrain.GetHeight(x, y) | |
| local height = Max(0, select(3, obj:GetVisualPosXYZ()) - z3d) | |
| obj:SetGravity() | |
| local fall_time = height > 0 and obj:GetGravityFallTime(height) or 0 | |
| obj:SetPos(x, y, z3d, fall_time) | |
| Sleep(fall_time) | |
| if IsValid(obj) then | |
| obj:SetGravity(0) | |
| if not z then | |
| obj:SetPos(x, y) | |
| end | |
| end | |
| end | |
| function Unit:RoamDropFlare() | |
| if not self.carry_flare then return end | |
| local attaches = self:GetAttaches("GrenadeVisual") | |
| for _, obj in ipairs(attaches) do | |
| if obj.fx_actor_class == "FlareStick" then | |
| local flare_pos = self:GetSpotLocPos(obj:GetAttachSpot()) | |
| obj:Detach() | |
| obj:SetSoundMute(false) | |
| obj:SetHierarchyEnumFlags(const.efVisible) | |
| local flare = PlaceObject("FlareOnGround") | |
| flare:SetPos(flare_pos) | |
| flare.item_class = "FlareStick" | |
| flare.campaign_time = Game.CampaignTime | |
| flare.remaining_time = 4 * 5000 | |
| flare.Despawn = true | |
| obj:SetAxis(axis_z) | |
| obj:SetAngle(0) | |
| flare.visual_obj = obj | |
| flare:UpdateVisualObj() | |
| -- flare fall down | |
| CreateGameTimeThread(ItemFallDown, flare) | |
| end | |
| end | |
| self.carry_flare = nil | |
| if not self:IsDead() then | |
| if self.cur_idle_style and string.match(self.cur_idle_style, "Flare") then | |
| self.cur_idle_style = false | |
| end | |
| if self:IsCommandThread() then | |
| self:SetRandomAnim(self:GetIdleBaseAnim()) | |
| else | |
| self:InterruptCommand("Idle") | |
| end | |
| end | |
| end | |
| function Unit:PlayRoamAnimation(marker) | |
| if gv_FlareCarriers > 0 and (GameState.Night or GameState.Underground) and self.team.player_enemy and | |
| self.species == "Human" and not self.infected and not self.carry_flare | |
| and self:Random(100) < 50 | |
| then | |
| gv_FlareCarriers = gv_FlareCarriers - 1 | |
| self:RoamAttachFlare() | |
| end | |
| local exec_time = GameTime() | |
| local pos = self:GetRoamPos(marker) | |
| if pos then | |
| NetUpdateHash("PlayRoamAnimation", pos) | |
| self:GotoSlab(pos) | |
| end | |
| if self.species == "Human" then | |
| if self.carry_flare then | |
| self:PlayIdleStyle("Idle_Flare", nil, 1) | |
| else | |
| local prop_meta = self:GetPropertyMetadata("RoamAnimationSet") | |
| local anim_set = Presets.AnimationSet[prop_meta.preset_group][self.RoamAnimationSet] | |
| if anim_set then | |
| anim_set:Play(self, const.eKeepComponentTargets) | |
| self:TakeSlabExploration() | |
| end | |
| end | |
| end | |
| if GameTime() - exec_time <= 0 then | |
| -- at least play an "idle" | |
| self:SetRandomAnim(self:GetIdleBaseAnim()) | |
| local timeToEnd = self:TimeToAnimEnd() | |
| timeToEnd = timeToEnd ~= 0 and timeToEnd or 500 -- Infinite loop prevention | |
| Sleep(timeToEnd) | |
| end | |
| end | |
| function Unit:Roam(marker, end_orient) | |
| if g_Combat or not marker then | |
| self:SetBehavior() | |
| self:SetCommand("Idle") | |
| return | |
| end | |
| self:SetBehavior("Roam", {marker, end_orient}) | |
| self:ChangeStance(nil, nil, "Standing") | |
| while true do | |
| self:PlayRoamAnimation(marker) | |
| end | |
| self:SetBehavior() | |
| if end_orient then | |
| self:SetOrientationAngle(marker:GetAngle(), 200) | |
| end | |
| end | |
| function Unit:RoamSingle(marker) | |
| if g_Combat or not marker then | |
| self:ClearBehaviors("RoamSingle") | |
| self:SetCommand("Idle") | |
| return | |
| end | |
| if self.species == "Hyena" then | |
| self:ClearBehaviors("RoamSingle") | |
| self:SetCommand("RoamHyenaLead") | |
| return | |
| end | |
| self:SetBehavior("RoamSingle", {marker}) | |
| self:ChangeStance(nil, nil, "Standing") | |
| self:PlayRoamAnimation(marker) | |
| self:SetBehavior() | |
| end | |
| function Unit:GetRandomMoveStyle(walk_type) | |
| local move_style | |
| if walk_type then | |
| move_style = GetRandomAnimationStyle(self, walk_type) | |
| else | |
| if self:IsProstitute() then | |
| move_style = GetRandomAnimationStyle(self, "SeduceWalk") | |
| end | |
| if not move_style then | |
| move_style = GetRandomAnimationStyle(self, "Walk") | |
| end | |
| end | |
| return move_style | |
| end | |
| function Unit:ResetMoveStyle() | |
| self.move_style = false | |
| end | |
| function Unit:Visit(visitable, already_in_perpetual) | |
| local start_pos = self.visit_test and self:GetPos() | |
| self:PushDestructor(function(self) | |
| self:FreeVisitable() -- visitable can change meanwhile | |
| self:SetBehavior() | |
| Msg("VisitFinished", self, visitable) | |
| end) | |
| assert(not visitable.reserved or self.handle == visitable.reserved, "Double usage of visitable!") | |
| self:SetBehavior("Visit", { visitable } ) | |
| local marker, dest, lookat = visitable[1], visitable[2], visitable[3] | |
| if marker then | |
| self.last_roam = IsKindOf(marker, "AL_Roam") and marker | |
| self.last_visit = marker | |
| -- NOTE: it is a valid case someone to delete a marker and the save to become broken | |
| marker:Visit(self, dest, lookat, already_in_perpetual) | |
| else | |
| self:IdleRoutine_StandStill() | |
| end | |
| self:PopAndCallDestructor() | |
| if self.visit_test then | |
| self:GotoSlab(start_pos) | |
| DoneObject(self) | |
| end | |
| end | |
| function Unit:IsVisiting() | |
| local visiting = self.behavior == "Visit" or self.behavior == "Roam" or self.behavior == "RoamSingle" | |
| return visiting and self.behavior == self.command | |
| end | |
| function Unit:CowerRun() | |
| local run_angle = self:Random(360 * 60) | |
| local delta = const.AmbientLife.CowerRunAngleSpanAvoid / 2 | |
| if self.cower_angle - delta < run_angle and run_angle < self.cower_angle + delta then | |
| run_angle = self.cower_angle + (run_angle < self.cower_angle and -delta or delta) | |
| end | |
| self.cower_from, self.cower_angle = false, false | |
| local dir = Rotate(pt_right, run_angle) | |
| local pos = self:GetPos() | |
| local dest = pos + SetLen(dir, const.AmbientLife.CowerRunDist) | |
| local slab_x, slab_y, slab_z = SnapToPassSlabXYZ(dest) | |
| while not slab_x and not IsCloser2D(dest, pos, guim) do | |
| dest = dest - dir | |
| slab_x, slab_y, slab_z = SnapToPassSlabXYZ(dest) | |
| end | |
| if slab_x then | |
| PlayFX("CowerRun", "start", self, self.gender) | |
| self:GotoSlab(point(slab_x, slab_y, slab_z)) | |
| PlayFX("CowerRun", "end", self, self.gender) | |
| end | |
| self.cower_cooldown = GameTime() + const.AmbientLife.CowerRunCooldownTime | |
| end | |
| function Unit:CanChangeCowerSpot() | |
| if g_Combat or self.cower_cooldown and GameTime() - self.cower_cooldown < 0 then | |
| return | |
| end | |
| local roll = self:Random(100) | |
| return roll < const.AmbientLife.CowerSpotChangeChance | |
| end | |
| function Unit:PlayIdleStyle(idle_style, duration, anim_cycles) | |
| local anim_style = GetAnimationStyle(self, idle_style) | |
| if not anim_style then return end | |
| local end_time | |
| if duration and duration > 0 then | |
| end_time = now() + duration | |
| if IsValidAnim(self, anim_style.End) then | |
| end_time = end_time - self:GetAnimDuration(anim_style.End) | |
| end | |
| end | |
| self.cur_idle_style = idle_style | |
| local cur_anim = self:GetStateText() | |
| if anim_style:HasAnimation(cur_anim) then | |
| Sleep(self:TimeToAnimEnd()) | |
| else | |
| local start_anim = anim_style.Start or "" | |
| if start_anim ~= "" and cur_anim ~= start_anim and IsValidAnim(self, start_anim) then | |
| self:SetState(start_anim, const.eKeepComponentTargets) | |
| Sleep(self:TimeToAnimEnd()) | |
| end | |
| end | |
| while true do | |
| local anim = anim_style:GetRandomAnim(self) | |
| self:SetState(anim, const.eKeepComponentTargets) | |
| Sleep(self:TimeToAnimEnd()) | |
| anim_cycles = anim_cycles and (anim_cycles - 1) | |
| if (end_time and now() - end_time >= 0) or (anim_cycles and anim_cycles <= 0) then | |
| if IsValidAnim(self, anim_style.End) then | |
| self:SetState(anim_style.End, const.eKeepComponentTargets) | |
| Sleep(self:TimeToAnimEnd()) | |
| end | |
| break | |
| end | |
| end | |
| end | |
| function Unit:PlayAnimStyleEndAnim(idle_style) | |
| local anim_style = GetAnimationStyle(self, idle_style) | |
| if anim_style and IsValidAnim(self, anim_style.End) then | |
| local cur_anim = self:GetStateText() | |
| if cur_anim == anim_style.Start or anim_style:HasAnimation(cur_anim) then | |
| if cur_anim == anim_style.Start then | |
| Sleep(self:TimeToAnimEnd()) | |
| if not IsValid(self) then return end | |
| end | |
| self:SetState(anim_style.End, const.eKeepComponentTargets) | |
| Sleep(self:TimeToAnimEnd()) | |
| end | |
| end | |
| end | |
| function Unit:CanCower() | |
| if self.cower_forbidden then return end | |
| return (g_Combat or not self.conflict_ignore) and self.species == "Human" and not self:IsDead() | |
| end | |
| function Unit:Cower(find_cower_spot, timeout, restore_behavior, restore_behavior_params) | |
| assert(self:IsValidPos()) | |
| CreateGameTimeThread(function() | |
| Sleep(self:Random(500)) | |
| PlayFX("Cower", "start", self, self.gender) | |
| end) | |
| if not g_Combat then | |
| local params = {find_cower_spot, timeout} | |
| if timeout and not restore_behavior and self.behavior ~= "Cower" then | |
| restore_behavior = self.behavior | |
| restore_behavior_params = self.behavior_params | |
| params[#params + 1] = restore_behavior | |
| params[#params + 1] = restore_behavior_params | |
| end | |
| self:SetBehavior("Cower", params) | |
| self:SetCombatBehavior("Cower", params) | |
| else | |
| local params = {find_cower_spot, timeout} | |
| if timeout and not restore_behavior and self.combat_behavior ~= "Cower" then | |
| restore_behavior = self.combat_behavior | |
| restore_behavior_params = self.combat_behavior_params | |
| params[#params + 1] = restore_behavior | |
| params[#params + 1] = restore_behavior_params | |
| end | |
| self:SetBehavior("Cower", params) | |
| self:SetCombatBehavior("Cower", params) | |
| end | |
| self:SetTargetDummy(false) | |
| self:UninterruptableGoto(self:GetVisualPos()) -- stop on the nearest free slab | |
| local cur_anim = self:GetStateText() | |
| local anim_style_group = GetHighestCover(self) and "CowerCover" or "Cower" | |
| local anim_style = GetAnimationStyle(self, self.cur_idle_style) | |
| if not anim_style or anim_style.VariationGroup ~= anim_style_group then | |
| anim_style = GetRandomAnimationStyle(self, anim_style_group) | |
| self.cur_idle_style = anim_style and anim_style.Name or nil | |
| end | |
| -- restore from load game | |
| if anim_style and (cur_anim == anim_style.Start or anim_style:HasAnimation(cur_anim)) and not self:IsAnimEnd() then | |
| Sleep(self:TimeToAnimEnd()) | |
| end | |
| self:PushDestructor(function(self) | |
| self:FreeVisitable() | |
| if not IsValid(self) then return end | |
| PlayFX("Cower", "end", self, self.gender) | |
| if self.behavior == "Cower" then | |
| if restore_behavior then | |
| self:SetBehavior(restore_behavior, restore_behavior_params) | |
| else | |
| self:SetBehavior() | |
| end | |
| end | |
| if self.combat_behavior == "Cower" then | |
| if restore_behavior then | |
| self:SetCombatBehavior(restore_behavior, restore_behavior_params) | |
| else | |
| self:SetCombatBehavior() | |
| end | |
| end | |
| self:PlayAnimStyleEndAnim(self.cur_idle_style) | |
| end) | |
| local start_time = now() | |
| while (self:CanCower() or timeout) and (not timeout or (now() - start_time) < timeout) do | |
| if find_cower_spot or self:CanChangeCowerSpot() then | |
| local visitable = self:GetRandomVisitable("low covers") | |
| if visitable then | |
| self:FreeVisitable() | |
| self:ReserveVisitable(visitable) | |
| local marker, pos, lookat = unpack_params(visitable) | |
| pos = pos or marker:GetPos() | |
| self:PlayAnimStyleEndAnim(self.cur_idle_style) | |
| if self:GotoSlab(pos, nil, nil, "Run") then | |
| self:UpdateMoveAnim(nil, "Run", pos) | |
| if self:Goto(pos, "sl") and lookat then | |
| local angle = visitable.cover and table.rand(lookat, self:Random()) or CalcOrientation(self, lookat) | |
| self:SetOrientationAngle(angle, 200) | |
| end | |
| end | |
| anim_style_group = GetHighestCover(self) and "CowerCover" or "Cower" | |
| anim_style = GetRandomAnimationStyle(self, anim_style_group) | |
| self.cur_idle_style = anim_style and anim_style.Name or nil | |
| end | |
| self.cower_cooldown = false | |
| end | |
| find_cower_spot = false | |
| if not g_Combat and self.cower_from then | |
| self:CowerRun() | |
| end | |
| local visitable = self:GetVisitable() | |
| local marker = visitable and visitable[1] | |
| if marker and IsValidAnim(self, marker.VisitIdle) then | |
| local anim, phase = self:GetNearbyUniqueRandomAnim(marker.VisitIdle) | |
| self:SetState(anim, const.eKeepComponentTargets) | |
| if phase > 0 then | |
| self:SetAnimPhase(1, phase) | |
| end | |
| elseif anim_style then | |
| local cur_anim = self:GetStateText() | |
| local is_start_anim = cur_anim == anim_style.Start | |
| local is_idle_anim = anim_style:HasAnimation(cur_anim) | |
| local is_external_anim = not is_start_anim and not is_idle_anim | |
| if is_external_anim and IsValidAnim(self, anim_style.Start) then | |
| self:SetState(anim_style.Start, const.eKeepComponentTargets) | |
| elseif is_external_anim or is_start_anim and self:IsAnimEnd() or is_idle_anim and self:GetAnimPhase(1) == 0 then | |
| self:SetState(anim_style:GetRandomAnim(self), const.eKeepComponentTargets) | |
| end | |
| else | |
| -- fallback | |
| self:SetState("civ_Standing_Fear", const.eKeepComponentTargets) | |
| end | |
| self:SetFootPlant(true) | |
| self:SetTargetDummy(nil, nil, nil, 0) | |
| WaitMsg(self, self:TimeToAnimEnd()) | |
| if not g_Combat and self.cower_from then | |
| self:CowerRun() | |
| end | |
| end | |
| self:PopAndCallDestructor() | |
| end | |
| local function CanCowerOnAttack(unit) | |
| return IsValid(unit) and IsKindOf(unit, "Unit") and unit.team.side == "neutral" and unit:CanCower() and unit.command ~= "Cower" | |
| end | |
| function OnMsg.Attack(action, results, attack_args) | |
| local target = attack_args.target | |
| if not IsValid(target) or not CanCowerOnAttack(target) then | |
| return | |
| end | |
| local target_pos = GetPackedPosAndStance(target) | |
| local target_sight = target:GetSightRadius() | |
| local cowards = MapGet(target, const.AmbientLife.CowerPropagateRadius, "Unit", function(unit) | |
| if not unit:IsDead() and CanCowerOnAttack(unit) then | |
| return stance_pos_dist(target_pos, GetPackedPosAndStance(unit)) <= target_sight | |
| end | |
| end) | |
| local timeout_min = const.AmbientLife.CowerTimeoutMin | |
| local timeout_range = const.AmbientLife.CowerTimeoutMax - timeout_min | |
| for _, unit in ipairs(cowards) do | |
| local timeout = timeout_min + unit:Random(timeout_range) | |
| unit:SetCommand("Cower", "find cower spot", timeout) | |
| end | |
| end | |
| function Unit:AdvanceTo(handle, delay) -- advance behavior | |
| if delay then | |
| Sleep(delay) | |
| if g_Combat then | |
| if self.combat_behavior == "AdvanceTo" then | |
| self:SetCombatBehavior() | |
| end | |
| return | |
| end | |
| end | |
| local marker = HandleToObject[handle] | |
| if not IsKindOf(marker, "GridMarker") then | |
| StoreErrorSource(marker or self, "Invalid marker handle in Unit:AdvanceTo") | |
| self:SetBehavior() | |
| return | |
| end | |
| local positions = marker:GetAreaPositions() | |
| if #(positions or empty_table) == 0 then | |
| self:SetBehavior() | |
| return | |
| end | |
| self:SetBehavior("AdvanceTo", {handle}) | |
| if self.team and self.team.player_enemy then | |
| self:AddStatusEffect("HighAlert") | |
| end | |
| local goto_pos = table.interaction_rand(positions, "Behavior") | |
| goto_pos = point(point_unpack(goto_pos)) | |
| self:GotoSlab(goto_pos, nil, nil, self:GetCommandParam("move_anim") or "Walk") | |
| local x, y = self:GetGridCoords() | |
| if marker:IsVoxelInsideArea(x, y) then | |
| self:SetBehavior("Roam", {marker}) | |
| local params = self:GetCommandParamsTbl("AdvanceTo") | |
| if params.PropagateAnimParams then | |
| self:SetCommandParams("Roam", params) | |
| end | |
| else | |
| Sleep(100) | |
| end | |
| end | |
| MapVar("g_MarkerGroupRoute", {}) | |
| MapVar("g_MarkerGroupAngle", {}) | |
| function ResetMarkerGroupRouteCache() | |
| g_MarkerGroupRoute = {} | |
| g_MarkerGroupAngle = {} | |
| end | |
| function GetRouteFromMarkerGroup(marker_group) | |
| if g_MarkerGroupRoute[marker_group] then return g_MarkerGroupRoute[marker_group], g_MarkerGroupAngle[marker_group] end | |
| local markers = MapGetMarkers("Waypoint", marker_group) | |
| local route = {} | |
| local angle | |
| if markers and #markers > 0 then | |
| for i = 1, #markers do | |
| for j = 1, #markers do | |
| if markers[j].ID == tostring(i) then | |
| if i == #markers then | |
| angle = markers[j]:GetAngle() | |
| end | |
| route[#route + 1] = {markers[j]:GetPos(), markers[j].FlavorAnim} | |
| break | |
| end | |
| end | |
| end | |
| else | |
| markers = MapGetMarkers(nil, marker_group) | |
| if markers and #markers > 0 then | |
| local marker = markers[1] | |
| for i = 2, #markers do | |
| if marker.AreaWidth + marker.AreaHeight < markers[i].AreaWidth + markers[i].AreaHeight then | |
| marker = markers[i] | |
| end | |
| end | |
| if marker then | |
| route = marker:GetMarkerCornerPositions() | |
| angle = marker:GetAngle() | |
| end | |
| end | |
| end | |
| g_MarkerGroupRoute[marker_group] = route | |
| g_MarkerGroupAngle[marker_group] = angle | |
| return route, angle | |
| end | |
| function Unit:Patrol(marker_group, next_id, loop, end_orient) | |
| self:SetBehavior("Patrol", {marker_group, next_id, loop, end_orient}) | |
| next_id = next_id or 1 | |
| local route, angle = GetRouteFromMarkerGroup(marker_group) | |
| if next_id > #route or next_id < 1 then | |
| if not loop or next_id == 1 then | |
| -- get the previous (last existing) marker to make the unit face in the way it points | |
| if route[next_id - 1] and end_orient then | |
| self:SetOrientationAngle(angle, not GameTimeAdvanced and 0 or 200) | |
| end | |
| self:SetBehavior() | |
| self:SetCommand("Idle") | |
| return | |
| end | |
| self:SetCommand("Patrol", marker_group, 1, loop, end_orient) -- restart from id 1 | |
| return | |
| end | |
| local route_node = route[next_id] | |
| local route_pos = GetPassSlab(route_node[1]) or route_node[1] | |
| if not self:GotoSlab(route_pos) then | |
| local has_path, closest_pos = pf.HasPosPath(self:GetPos(), route_pos, self:GetPfClass()) | |
| if not has_path or closest_pos ~= route_pos then | |
| self:Teleport(route_pos) | |
| end | |
| end | |
| local waypoint_anim = route_node[2] | |
| if waypoint_anim and waypoint_anim ~= "" then | |
| self:SetState(waypoint_anim, const.eKeepComponentTargets) | |
| self:SetFootPlant(true) | |
| Sleep(self:TimeToAnimEnd()) | |
| else | |
| waypoint_anim = self:TryGetActionAnim("IdlePassive", self.stance) | |
| if waypoint_anim then | |
| self:SetRandomAnim(waypoint_anim) | |
| Sleep(Min(5000, self:TimeToAnimEnd())) | |
| end | |
| end | |
| self:SetCommand("Patrol", marker_group, next_id + 1, loop, end_orient) | |
| end | |
| function Unit:ExitMap(marker, start_time) | |
| if not marker then | |
| self:SetBehavior() | |
| return | |
| end | |
| if not start_time then | |
| start_time = GameTime() + 3000 | |
| end | |
| self:SetBehavior("ExitMap", {marker, start_time}) | |
| if not self:IsAmbientUnit() and start_time - GameTime() > 0 then | |
| self:SetRandomAnim(self:GetIdleBaseAnim()) | |
| Sleep(start_time - GameTime()) | |
| end | |
| self:ChangeStance(nil, nil, "Standing") | |
| ObjModified(self) | |
| self:PushDestructor(function(self) | |
| if IsValid(self) then | |
| self:OverwritePFClass(false) | |
| end | |
| end) | |
| if self.species == "Human" then | |
| self:OverwritePFClass(CalcPFClass("player1")) | |
| end | |
| self:GotoSlab(marker:GetPos(), nil, nil, self:GetCommandParam("move_anim") or "Walk") | |
| self:PopAndCallDestructor() | |
| self:Despawn() | |
| end | |
| function Unit:EnterMap(zone, pos, wait_time) | |
| if GameState.Conflict or GameState.ConflictScripted then | |
| DoneObject(self) | |
| return | |
| end | |
| self.enter_map_wait_time = GameTime() + wait_time | |
| self.enter_map_pos = pos | |
| local marker = zone:GetEntranceMarker() | |
| self:PushDestructor(function(self) | |
| if not IsValid(self) then return end | |
| self:SetPos(marker and marker:GetPos() or pos) | |
| self.enter_map_wait_time = false | |
| self.enter_map_pos = false | |
| ObjModified(self) | |
| end) | |
| if wait_time then | |
| Sleep(wait_time) | |
| end | |
| self:PopAndCallDestructor() | |
| if not IsValid(self) then return end | |
| self:PushDestructor(function(self) | |
| if IsValid(self) then | |
| self:OverwritePFClass(false) | |
| end | |
| end) | |
| if self.species == "Human" then | |
| self:OverwritePFClass(CalcPFClass("player1")) | |
| end | |
| self:SetCommandParamValue(self.command, "move_anim", "Walk") | |
| self:GotoSlab(pos) | |
| self:PopAndCallDestructor() | |
| ObjModified(self) | |
| if GameState.Conflict or GameState.ConflictScripted then | |
| self:SetCommand("Cower", "find cower spot") | |
| self:SetCommandParamValue("Cower", "move_anim", "Run") | |
| self:UpdateMoveAnim() | |
| else | |
| self:AmbientRoutine() | |
| end | |
| end | |
| function Unit:GoBackAfterCombat(pos) | |
| self:GotoSlab(pos) | |
| self:SetBehavior() | |
| end | |
| function Unit:IsAmbientUnit() | |
| return IsKindOf(self.routine_spawner, "AmbientZoneMarker") | |
| end | |
| function Unit:IsProstitute() | |
| return self.unitdatadef_id == "WorkingGirl" or self.unitdatadef_id == "WorkingGuy" | |
| end | |
| function Unit:ResetAmbientLife(kick_perpetual_units, force_immediate_kick) | |
| if not self.command == "Visit" then return end | |
| local visitable = self.behavior_params[1] | |
| local marker = visitable[1] | |
| if not (marker and marker:CanVisit(self)) or (kick_perpetual_units and self.perpetual_marker) then | |
| self:SetBehavior() | |
| self:SetCommand("Idle") | |
| if force_immediate_kick then | |
| -- NOTE: Visit command does all the exit work in destructor which may be late(next ms after ResetAmbientLife) | |
| -- since the next effect UnitsStealForPerpetualMarkers may happen in the same ms as ResetAmbientLife | |
| -- and the following not cleared members may prevent perpetual markers stealing | |
| if self.perpetual_marker then | |
| self.perpetual_marker.perpetual_unit = false | |
| self.perpetual_marker = false | |
| end | |
| end | |
| end | |
| end | |
| DefineClass.AL_CorpseMarker = { | |
| __parents = {"GameDynamicSpawnObject"}, | |
| flags = {gofSyncObject = true}, | |
| } | |
| DefineClass.AL_Mourn_FromCorspe = { | |
| __parents = {"AL_Mourn", "AL_CorpseMarker"}, | |
| } | |
| DefineClass.AL_Maraud_FromCorspe = { | |
| __parents = {"AL_Maraud", "AL_CorpseMarker"}, | |
| } | |
| function Unit:__PlaceCorpseMarker(class, visit_min, visit_max) | |
| local randomPos = RotateRadius(50 * guic, self:Random(360 * 60), self) | |
| if GetPassSlabXYZ(randomPos) then | |
| local marker = PlaceObject(class, {corpse = self, Teleport = false}) | |
| marker:SetPos(randomPos) | |
| marker:Face(self) | |
| marker.VisitMinDuration = visit_min + self:Random(visit_max - visit_min) | |
| table.insert(g_Visitables, marker:GenerateVisitable()) | |
| return marker | |
| end | |
| end | |
| function Unit:PlaceALDeadMarkers() | |
| if self.dead_markers_tried then return end | |
| self.dead_markers_tried = true | |
| local tries = 10 | |
| assert(not self.mourn) | |
| for try = 1, tries do | |
| local mourn = self:__PlaceCorpseMarker("AL_Mourn_FromCorspe", const.AmbientLife.MournVisitMin, const.AmbientLife.MournVisitMax) | |
| if mourn then | |
| self.mourn = mourn | |
| break | |
| end | |
| end | |
| assert(not self.maraud) | |
| for try = 1, tries do | |
| local maraud = self:__PlaceCorpseMarker("AL_Maraud_FromCorspe", const.AmbientLife.MaraudVisitMin, const.AmbientLife.MaraudVisitMax) | |
| if maraud then | |
| self.maraud = maraud | |
| break | |
| end | |
| end | |
| end | |
| local function KickOutVisitorAndDeleteMarker(marker) | |
| local visitable, idx = table.find_value(g_Visitables, 1, marker) | |
| local visitor = visitable and visitable.reserved and HandleToObject[visitable.reserved] | |
| if IsValid(visitor) then | |
| visitor:SetBehavior() | |
| visitor:SetCommand("Idle") | |
| end | |
| if idx then | |
| table.remove(g_Visitables, idx) | |
| end | |
| end | |
| function Unit:RemoveALDeadMarkers() | |
| if self.mourn then | |
| KickOutVisitorAndDeleteMarker(self.mourn) | |
| DoneObject(self.mourn) | |
| self.mourn = false | |
| end | |
| if self.maraud then | |
| KickOutVisitorAndDeleteMarker(self.maraud) | |
| DoneObject(self.maraud) | |
| self.maraud = false | |
| end | |
| end | |
| function SavegameSectorDataFixups.AL_Corpses(sector_data, lua_revision, handle_data) | |
| local spawn_data = sector_data.spawn | |
| local length = #(spawn_data or "") | |
| local units, invalid_dead_markers, marker_class = {}, {}, {} | |
| for i = 1, length, 2 do | |
| local class, handle = spawn_data[i], spawn_data[i + 1] | |
| if class == "AL_Mourn_FromCorspe" or class == "AL_Maraud_FromCorspe" then | |
| local marker_data = handle_data[handle] | |
| if not marker_data.corpse then | |
| table.insert(invalid_dead_markers, marker_data) | |
| marker_class[marker_data] = class | |
| end | |
| elseif class == "Unit" then | |
| table.insert(units, handle) | |
| local unit_data = handle_data[handle] | |
| if unit_data.behavior == "Dead" and not unit_data.dead_markers_tried then | |
| unit_data.dead_markers_tried = not not (unit_data.mourn or unit_data.maraud) | |
| end | |
| end | |
| end | |
| if #invalid_dead_markers > 0 then | |
| local mourn_units, maraud_units = {}, {} | |
| for _, handle in ipairs(units) do | |
| local unit_data = handle_data[handle] | |
| if unit_data.behavior == "Dead" then | |
| table.insert(mourn_units, unit_data) | |
| table.insert(maraud_units, unit_data) | |
| end | |
| end | |
| for _, marker_data in ipairs(invalid_dead_markers) do | |
| local units = marker_class[marker_data] == "AL_Mourn_FromCorspe" and mourn_units or maraud_units | |
| local marker_pos = marker_data.pos | |
| local closest_unit = table.min(units, function(unit_data) | |
| return marker_pos:Dist(unit_data.pos) | |
| end) | |
| table.remove_value(units, closest_unit) | |
| marker_data.corpse = closest_unit and closest_unit.handle or false | |
| end | |
| end | |
| end | |