myspace / Lua /Tactical /UnitInteractions.lua
sirnii's picture
Upload 1816 files
b6a38d7 verified
raw
history blame
29.8 kB
-- general interaction
local pfFinished = const.pfFinished
local pfFailed = const.pfFailed
local pfDestLocked = const.pfDestLocked
local pfTunnel = const.pfTunnel
local objEnumSize = const.SlabSizeX * 1
function OnMsg.ClassesBuilt()
for name, class in pairs(g_Classes) do
if IsKindOf(class, "CustomInteractable") then
objEnumSize = Max(class.range_in_tiles * const.SlabSizeX, objEnumSize)
end
end
end
function Unit:GetReachableObjects(class, filter)
if not IsValid(self) then return {} end
local unitPos = self:GetOccupiedPos() or self:GetPos()
local hasAmbient = false
class = class or "Interactable"
return MapGet(unitPos, objEnumSize, class, function(obj)
if obj == self then return false end
local isAmbient = IsKindOf(obj, "Unit") and obj:IsAmbientUnit()
if isAmbient and hasAmbient then
return false
end
-- Check if close enough to any of the interaction positions.
local positions = obj:GetInteractionPos(self)
local closeEnough = self:CloseEnoughToInteract(positions, obj, unitPos)
if not closeEnough then return false end
local valid = not filter or filter()
if valid then
hasAmbient = isAmbient
end
return valid
end) or {}
end
local skip_conflict_banter_filter = { skipConflictCheck = true }
function Unit:GetInteractionInfo()
if self.command == "ExitMap" or self.command == "Die" then return end
if self.interacting_unit then
return
end
if not self.visible or self:GetOpacity() == 0 then return end
if self:IsDead() and self.command == "Dead" then
if not self.immortal and self:GetItemInSlot("InventoryDead") then
return Presets.CombatAction.Interactions.Interact_LootUnit
end
return
end
if not self:IsNPC() then return end
local conversationId = FindEnabledConversation(self)
if conversationId and not g_Combat then
local conversation = Conversations[conversationId]
if GetSectorConflict() and conversation.disabledInConflict then
return Presets.CombatAction.Interactions.Interact_NotNow
else
return Presets.CombatAction.Interactions.Interact_Talk
end
elseif self.enabled and self.spawner and
#(self.spawner.InteractionEffects or empty_table) ~= 0 and
EvalConditionList(self.spawner.InteractionConditions or empty_table) then
return Presets.CombatAction.Interactions.Interact_UnitCustomInteraction, self.spawner.InteractionVisuals
end
if not self:GetAllBanters("findFirst", skip_conflict_banter_filter) then return end
if GetSectorConflict() then
if not self:GetAllBanters("findFirst") then
return Presets.CombatAction.Interactions.Interact_NotNow
end
end
return Presets.CombatAction.Interactions.Interact_Banter
end
function Unit:GetInteractionCombatAction(unit)
if self.behavior == "Hang" then return false end
if unit and self:IsOnEnemySide(unit) and not unit:IsDead() and not self:IsDead() then return false end
return self:GetInteractionInfo()
end
function Unit:PopulateVisualCache()
-- nop
end
local face_duration = 200
function Unit:BeginInteraction(other_unit)
--note: self is the interactable here
if self:IsDead() then return end
self.angle_before_interaction = self.angle_before_interaction or self:GetOrientationAngle()
self:Face(other_unit, face_duration)
Sleep(face_duration)
end
function Unit:EndInteraction(other_unit)
--note: self is the interactable here
if self.angle_before_interaction and self.command ~= "PlayInteractionBanter" and not self:IsPersistentDead() then
self:SetOrientationAngle(self.angle_before_interaction, face_duration)
self.angle_before_interaction = false
end
Interactable.EndInteraction(self, other_unit)
end
function Unit:GetInteractionPos(unit)
if not IsValid(self) then return false end
local x, y, z = SnapToPassSlabXYZ(self)
if x and (not unit or unit:IsEqualPos(x, y, z)) then
return point(x, y, z)
end
if g_Combat and self:IsDead() then
return self:GetPos()
end
local closestMeleePos = unit and unit:GetClosestMeleeRangePos(self, nil, false, "interaction")
if closestMeleePos then return closestMeleePos end
-- Sitting units don't return valid melee pos
if self.command == "Visit" then
return GetClosestMeleeRangePos(unit, self)
end
end
function Unit:GetInteractionPosWith(target, ignore_occupied)
local positions = target:GetInteractionPos(self)
if not positions then
return
end
if type(positions) == "table" then
if #positions == 0 then
return
end
if ignore_occupied == nil then
ignore_occupied = positions.ignore_occupied
end
end
local pfflags = self:GetPathFlags()
if ignore_occupied then
pfflags = pfflags & ~const.pfmDestlock
end
local has_path, closest_pos = pf.HasPosPath(self, positions, nil, 0, 0, self, 0, nil, pfflags)
-- Could path to
if has_path and closest_pos then
if closest_pos == positions or type(positions) == "table" and table.find(positions, closest_pos) then
return closest_pos
-- Couldnt path, but unit is close enough
elseif self:CloseEnoughToInteract(positions, target) then
return closest_pos
end
end
end
function Unit:CloseEnoughToInteract(positions, interactable, selfPosOverride)
if not positions then
return false
end
local my_pos = selfPosOverride or self:GetPos()
if type(positions) == "table" then
if table.find(positions, my_pos) then
return true
end
elseif IsPoint(positions) then
local pos = positions
local minDist = interactable.range_in_tiles * const.SlabSizeX
if IsCloser2D(my_pos, pos, minDist) then
local start_z = my_pos:z() or terrain.GetHeight(my_pos)
local z = pos:z() or terrain.GetHeight(pos)
if abs(start_z - z) < const.Combat.InteractionMaxHeightDifference then
return true
end
end
end
return false
end
local function lSyncCheck(id, sync, ...)
if sync then
NetUpdateHash("CanInteractWith" .. tostring(id), ...)
end
end
local can_interact_unit_list = {}
function Unit:CanInteractWith(target, action_id, skip_cost, from_ui, sync)
lSyncCheck(0, sync, target, action_id, skip_cost, from_ui)
if not IsKindOf(target, "Interactable") then
lSyncCheck(1, sync, target, action_id, skip_cost, from_ui)
return false
end
local prevInteractUnit = g_Units[target.being_interacted_with]
if IsValid(prevInteractUnit) and prevInteractUnit:IsLocalPlayerControlled() ~= self:IsLocalPlayerControlled() then
--uninteractable while other player is interacting with it.
lSyncCheck(11, sync, target, action_id, skip_cost, from_ui)
return false
end
if not SpawnedByEnabledMarker(target) or not target.enabled then
lSyncCheck(2, sync, target, action_id, skip_cost, from_ui, target.enabled, gv_CurrentSectorId, (gv_CurrentSectorId and gv_Sectors[gv_CurrentSectorId].intel_discovered))
return false
end
if IsKindOf(target, "SlabWallWindow") then
if not target:ShouldShowUnitInteraction() then
lSyncCheck(3, sync, target, action_id, skip_cost, from_ui)
return false
end
end
local visual = ResolveInteractableVisualObjects(target, nil, nil, "findFirst")
if not visual then
lSyncCheck(4, sync, target, action_id, skip_cost, from_ui)
return false
end
if action_id == "Interact_CustomInteractable" and IsKindOf(target, "CustomInteractable") then
can_interact_unit_list[1] = self
if from_ui or target:GetUIState(can_interact_unit_list) == "enabled" then
lSyncCheck(5, sync, target, action_id, skip_cost, from_ui)
return true
end
end
-- These door interactions are additional actions that do not override Open/Close
local combat_action
if action_id and IsKindOf(target, "Lockpickable") and table.find(LockpickableActionIds, action_id) then
combat_action = CombatActions[action_id]
else
combat_action = target:GetInteractionCombatAction(self)
end
if not combat_action or action_id and action_id ~= combat_action.id then
lSyncCheck(6, sync, target, action_id, skip_cost, from_ui)
return false
end
-- special-case Interact_Attack for units using emplacements that prevent their movement
if combat_action.id == "Interact_Attack" and self:HasStatusEffect("ManningEmplacement") then
can_interact_unit_list[1] = self
if not CombatActionTargetFilters.MGBurstFire(target, can_interact_unit_list) then
return false
end
elseif g_Combat and not skip_cost then
if not target or self:HasStatusEffect("StationedMachineGun") or self:HasStatusEffect("ManningEmplacement") then
return false
end
local has_path
local combatPath = GetCombatPath(self)
local paths_ap = combatPath.paths_ap
local positions = target:GetInteractionPos(self)
if IsPoint(positions) then
if paths_ap[point_pack(positions)] then
has_path = true
end
else
for i, pos in ipairs(positions) do
if paths_ap[point_pack(pos)] then
has_path = true
break
end
end
end
if not has_path then
return false
end
end
if not from_ui then
can_interact_unit_list[1] = self
local state, reason = combat_action:GetUIState(can_interact_unit_list, {target = target, skip_cost = skip_cost, goto_ap = 0})
if state ~= "enabled" then
lSyncCheck(7, sync, target, action_id, skip_cost, from_ui)
return false, reason
end
end
lSyncCheck(8, sync, target, action_id, skip_cost, from_ui)
return true, combat_action
end
function Unit:RegisterInteractingUnit(unit)
assert(self.interacting_unit == false)
self.interacting_unit = unit
end
function Unit:UnregisterInteractingUnit(unit)
assert(not unit or self.interacting_unit == unit)
self.interacting_unit = nil
end
local inventoryCloseIdToMsg = {
"Player1ClosedInventory",
"Player2ClosedInventory",
}
function NetSyncEvents.OpenInventory(remoteId)
NetGossip("Open", "Inventory", GetCurrentPlaytime())
end
function NetSyncEvents.ClosedInventory(remoteId)
NetGossip("Close", "Inventory", GetCurrentPlaytime())
Msg(inventoryCloseIdToMsg[remoteId])
end
function OnMsg.OpenInventorySubDialog()
NetSyncEvent("OpenInventory", netUniqueId)
end
function OnMsg.CloseInventorySubDialog()
NetSyncEvent("ClosedInventory", netUniqueId)
end
function Unit:BeingInteracted()
local params = self:GetCommandParamsTbl("Idle")
self:SetCommandParams(self.command, params)
local base_anim = self:GetIdleBaseAnim()
if not IsAnimVariant(self:GetStateText(), base_anim) then
self:SetRandomAnim(base_anim)
end
local target = g_Units[self.being_interacted_with]
if target then
self:SetOrientationAngle(CalcOrientation(self, target), 200)
end
while self.being_interacted_with or IsUnitPartOfAnyActiveBanter(self) do
Sleep(500)
end
Sleep(2000 + self:Random(1000))
end
local function lInteractionLoadingBar(self, action_id, target)
local action = CombatActions[action_id]
if (action and action.InteractionLoadingBar) or IsKindOf(target, "RangeGrantMarker") or (action_id == "Interact_CustomInteractable" and target.InteractionLoadingBar) then
local time = const.InteractionActionProgressBarTime
local barUI = SpawnProgressBar(time)
local spot = self:GetSpotBeginIndex("Headstatic")
barUI:AddDynamicPosModifier({id = "attached_ui", target = self, spot_idx = spot})
self:PushDestructor(function(self)
if barUI.window_state ~= "destroying" then
barUI:Close()
end
end)
PlayFX("InteractionLoadingBar", "start")
self:SetRandomAnim(self:GetIdleBaseAnim())
self:Face(target, 200)
Sleep(time)
PlayFX("InteractionLoadingBar", "end")
self:PopDestructor()
end
end
function Unit:InteractWith(action_id, cost_ap, pos, goto_ap, target, from_ui)
local can_interact = not (g_Combat and self:HasPreparedAttack()) and self:CanInteractWith(target, action_id, true, from_ui, "sync check")
NetUpdateHash("Unit_InteractWith", self, not not g_Combat, (g_Combat and self:HasPreparedAttack()), can_interact)
if can_interact and target.being_interacted_with then
local prevInteractUnit = g_Units[target.being_interacted_with]
if not g_Combat and
prevInteractUnit and
self:IsLocalPlayerControlled() == prevInteractUnit:IsLocalPlayerControlled() and
prevInteractUnit.goto_target then
prevInteractUnit:InterruptCommand("Idle")
else
can_interact = false
--print("interactable already being interacted with")
end
end
if not can_interact then
if g_Combat then
self:GainAP(cost_ap)
CombatActionInterruped(self)
end
PlayFX("IactDisabled", "start")
return
end
self:InterruptPreparedAttack()
local target_restore_behavior
local cmd_to_restore
if action_id == "Interact_Banter" or action_id == "Interact_Talk" or action_id == "Interact_UnitCustomInteraction" then
target_restore_behavior = IsKindOf(target, "Unit") and (target.behavior == "Visit" or target.behavior == "Cower" or
(target:GetCommandParamsTbl("Idle") and target:GetCommandParamsTbl("Idle").idle_stance ~= "Standing"))
self:SetRandomAnim(self:GetIdleBaseAnim())
local behavior_finished
if target.behavior == "Patrol" or target.behavior == "GoBackAfterCombat" then
behavior_finished = true
cmd_to_restore = {cmd = "Patrol", cmd_params = target.behavior_params}
elseif target.behavior == "Roam" or target.behavior == "RoamSingle" then
if IsKindOf(target.traverse_tunnel, "SlabTunnelLadder") then
target:SetActionInterruptCallback(function()
target:SetCommand(false)
Msg("LadderTraversed")
end)
while target.traverse_tunnel do
WaitMsg("LadderTraversed", 300)
end
end
behavior_finished = true
elseif target.goto_target and target.interruptable then
behavior_finished = true
elseif target:IsIdleCommand() and target.interruptable then
behavior_finished = true
end
if behavior_finished then
target.being_interacted_with = self.session_id
target:InterruptCommand("BeingInteracted")
end
else
target.being_interacted_with = self.session_id
end
self:PushDestructor(function()
if target.being_interacted_with == self.session_id then
target.being_interacted_with = false
end
end)
-- approach target
local action_cost = g_Combat and cost_ap and cost_ap - (goto_ap or 0) or 0
local can_interact
-- Play VR for selected loot to be opened
if self:IsMerc() and (action_id == "Interact_LootUnit" or action_id == "Interact_LootContainer") and not IsBlockingLockpickState(target and target.lockpickState or "") then
PlayVoiceResponse(self, "LootOpened")
end
if g_Combat then
self:PushDestructor(function(self)
self:GainAP(action_cost)
end)
PlayFX("InteractGoto", "start", self, target)
can_interact = self:CombatGoto(action_id, goto_ap or 0, pos)
self:PopDestructor()
else
self:PushDestructor(function(self)
if cmd_to_restore then
target:SetCommand(cmd_to_restore.cmd, table.unpack(cmd_to_restore.cmd_params))
end
end)
local positions, angle, preferred = target:GetInteractionPos(self)
can_interact = self:CloseEnoughToInteract(positions, target)
if not can_interact and preferred then
PlayFX("InteractGoto", "start", self, target)
NetUpdateHash("InteractGotoPreferred", self, target, preferred)
can_interact = self:GotoSlab(preferred)
end
if not can_interact and positions and (IsPoint(positions) or #positions > 0) then
PlayFX("InteractGoto", "start", self, target)
NetUpdateHash("InteractGoto", self, target, positions)
can_interact = self:GotoSlab(positions)
end
self:PopDestructor()
end
can_interact = can_interact and self:CanInteractWith(target, action_id, true, from_ui, "sync check")
NetUpdateHash("Unit_InteractWith2", can_interact)
if not can_interact then
if g_Combat then
self:GainAP(action_cost)
CombatActionInterruped(self)
end
self:PopAndCallDestructor()
return
end
local base_idle = self:GetIdleBaseAnim()
if not IsAnimVariant(self:GetStateText(), base_idle) then
local anim = self:GetNearbyUniqueRandomAnim(base_idle)
self:SetState(anim)
end
--execute
lInteractionLoadingBar(self, action_id, target)
local looting = action_id == "Interact_LootUnit" or action_id == "Interact_LootContainer"
NetUpdateHash("Unit_InteractWith3", looting)
local containersInArea
if looting then
containersInArea = InventoryGetLootContainers(target)
MultipleRegisterInteractingUnit(containersInArea, self)
else
target:RegisterInteractingUnit(self)
end
local interactionLogSpecifier = false
self:PushDestructor(function(self)
NetUpdateHash("Unit_InteractWith_Destro")
if not IsValid(target) then return end
if looting then
MultipleUnregisterInteractingUnit(containersInArea, self)
else
target:UnregisterInteractingUnit(self)
end
if not target_restore_behavior then
target:EndInteraction(self)
if cmd_to_restore and (not target.behavior or target.behavior == "") then
target:SetCommand(cmd_to_restore.cmd, table.unpack(cmd_to_restore.cmd_params))
end
end
InteractableVisibilityUpdate({self})
target:LogInteraction(self, action_id, "end", interactionLogSpecifier)
end)
local canOpen
if looting then
local fxName = action_id
local fxTarget = target
if target and target.los_check_obj then
local containerObj = target.los_check_obj
if IsKindOf(containerObj, "DummyUnit") then
fxName = "Interact_LootUnit"
else
fxTarget = GetObjMaterial(containerObj:GetPos(), containerObj) or target
end
end
PlayFX(fxName, "start", self, fxTarget)
canOpen = true
if action_id == "Interact_LootContainer" then
NetUpdateHash("Unit_InteractWith_PreOpen")
if not target:Open(self) then
canOpen = false
end
NetUpdateHash("Unit_InteractWith_PostOpen", canOpen)
end
NetUpdateHash("Unit_InteractWith4", canOpen)
if canOpen then
self:SetRandomAnim(self:GetIdleBaseAnim())
self:Face(target, 200)
Sleep(200)
end
end
if not target_restore_behavior then
target:BeginInteraction(self)
end
target:LogInteraction(self, action_id, "start")
PlayFX("Interact", "start", IsKindOf(target, "CustomInteractable") and target.Visuals or target, target)
if action_id == "Interact_UnitCustomInteraction" then
SetCombatActionState(self, "PostAction")
self:SetRandomAnim(self:GetIdleBaseAnim())
self:Face(target, 200)
if not target_restore_behavior then
target:Face(self, 2000)
end
Sleep(200)
self:PushDestructor(function(self)
if target.spawner.ExecuteInteractionEffectsSequentially then
ExecuteSequentialEffects(target.spawner.InteractionEffects, "CustomInteractable", table.imap({self}, "handle"), target.handle)
else
ExecuteEffectList(target.spawner.InteractionEffects, self, { target_units = { self, target }, interactable = target })
end
end)
self:PopAndCallDestructor()
elseif action_id == "Interact_CustomInteractable" then
--PlayFX is triggered inside CustomInteractable:Execute()
self:PushDestructor(function(self)
-- the call below may result in execution various Effects, which are executed in a pcall
-- when this happens and the actor (self) dies as a result, SetCommand("Die") will attempt to
-- delete the current thread and fire an assert as a result
-- to avoid this, we make the call in a destructor to make sure the pcall finishes before the thread is deleted
target:Execute({self}, {target = target})
end)
self:PopAndCallDestructor()
else
if not looting then
-- looting already PlayFX-ed above
PlayFX(action_id, "start", self, target)
end
if action_id == "Interact_DoorOpen" then
local err = target:InteractDoor(self, "open")
-- Locked doors (failed to open) will refund ap cost to align
-- behavior with an edge case in Unit:CombatGoto (ok:Boyan)
if err == "failed" then
self:GainAP(action_cost)
end
elseif action_id == "Interact_DoorClose" then
target:InteractDoor(self, "closed")
elseif action_id == "Interact_WindowBreak" then
target:InteractWindow(self, "broken")
elseif action_id == "Interact_Banter" then
SetCombatActionState(self, "PostAction")
self:SetRandomAnim(self:GetIdleBaseAnim())
self:Face(target, 200)
Sleep(200)
if target_restore_behavior then
target:PlayInteractionBanter("no rotation")
else
target.dont_clear_queue = true
target:SetCommand("PlayInteractionBanter")
end
elseif action_id == "Interact_Talk" then
self:SetRandomAnim(self:GetIdleBaseAnim())
self:Face(target, 200)
Sleep(200)
local conversation = FindEnabledConversation(target)
if conversation then
OpenConversationDialog(self, conversation, false, "interaction", target)
WaitMsg("CloseConversationDialog")
end
if IsValidTarget(target) then -- target may have been despawned or killed by the conversation (e.g. switching sides)
if not target_restore_behavior then
target:SetCommand("Idle")
end
end
elseif action_id == "Interact_Disarm" then
self:Face(target, 200)
interactionLogSpecifier = target:AttemptDisarm(self)
if interactionLogSpecifier and interactionLogSpecifier == "success" then
PlayVoiceResponse(self, "MineDisarmed")
end
elseif looting then
if canOpen then
PlayResponseOpenContainer(self, target)
SetCombatActionState(self, "PostAction")
local inventoryHash = GetInventoryHash(target.Inventory)
local local_control = self:IsLocalPlayerControlled()
if local_control then
OpenInventory(self, target)
end
SetCombatActionState(self, nil) --end this combat action in order for next actions to go through, such as moving itmems in inv.
if netInGame then
WaitMsg(inventoryCloseIdToMsg[self.ControlledBy])
else
WaitMsg("CloseInventorySubDialog")
end
local inventoryHashAfterLoot = GetInventoryHash(target.Inventory)
if inventoryHash ~= inventoryHashAfterLoot then
interactionLogSpecifier = "looted"
end
end
elseif action_id == "LockImpossible" or action_id == "NoToolsLocked" or action_id == "NoToolsBlocked" then
target:PlayCannotOpenFX(self)
elseif action_id == "Interact_Exit" then
target:UnitLeaveSector(self)
end
end
self:PopAndCallDestructor()
self:PopAndCallDestructor()
local dlg = GetInGameInterfaceModeDlg()
if IsKindOf(dlg, "IModeCommonUnitControl") then dlg:UpdateInteractablesHighlight(false, "force") end
end
function Unit:PlayInteractionBanter(no_rotation)
--unit points to the one doing the interaction
local unitList = { self }
local filterContext = self.last_played_banter_id and { filter_if_other = self.last_played_banter_id } or false
local randomBanterGroup = self:GetRandomBanterGroup(filterContext)
if not next(randomBanterGroup) then
CombatLog("debug", T{Untranslated("No available banters for <unit>. Please check the actor name."), unit = self:GetDisplayName()})
return
end
local angle_before_interaction = self.angle_before_interaction
self.angle_before_interaction = false
EndAllBanter()
if self.sequential_banter then
for i, bant in ipairs(randomBanterGroup) do
PlayAndWaitBanter(bant, unitList, self)
end
else
local idx = self:Random(#randomBanterGroup) + 1
local banter = randomBanterGroup[idx]
self.last_played_banter_id = banter
PlayAndWaitBanter(banter, unitList, self)
end
if not no_rotation and angle_before_interaction then
self:SetOrientationAngle(angle_before_interaction, face_duration)
Sleep(face_duration)
end
end
local get_banters_unit_list = {}
function Unit:GetAllBanters(findFirst, filterContext)
local unitList = get_banters_unit_list
unitList[1] = self
local allBanterGroups, list_unplayed
local marker = self.zone or self.spawner
if marker then
--GET ALL BANTER GROUPS FROM THE UNIT'S MARKER
for i, group in ipairs(marker.BanterGroups) do
local availableBanters, _, unplayed = FilterAvailableBanters(Presets.BanterDef[group], filterContext, unitList, false, findFirst)
if availableBanters then
if findFirst then return true end
allBanterGroups = allBanterGroups or {}
if unplayed and not list_unplayed then
list_unplayed = true
table.iclear(allBanterGroups)
end
if unplayed or not list_unplayed then
table.insert(allBanterGroups, {group = group, banters = availableBanters})
end
end
end
--GET ALL SPECIFIC BANTERS FROM THE UNIT'S MARKER INTO ONE GROUP
if next(marker.SpecificBanters) then
local availableBanters, _, unplayed = FilterAvailableBanters(marker.SpecificBanters, filterContext, unitList, false, findFirst)
if availableBanters then
if findFirst then return true end
allBanterGroups = allBanterGroups or {}
if unplayed and not list_unplayed then
list_unplayed = true
table.iclear(allBanterGroups)
end
if unplayed or not list_unplayed then
table.insert(allBanterGroups, {group = "MarkerSpecificBanters", banters = availableBanters})
end
end
end
end
--GET ALL BANTERS FROM unit.banters (might come from squads entering sector or other places)
if next(self.banters) then
local availableBanters, _, unplayed = FilterAvailableBanters(self.banters, filterContext, unitList, false, findFirst)
if availableBanters then
if findFirst then return true end
allBanterGroups = allBanterGroups or {}
if unplayed and not list_unplayed then
list_unplayed = true
table.iclear(allBanterGroups)
end
if unplayed or not list_unplayed then
table.insert(allBanterGroups, {group = "UnitAdditionalBanters", banters = availableBanters})
end
end
end
return allBanterGroups
end
function Unit:GetRandomBanterGroup(ctx)
local allBanterGroups = self:GetAllBanters(false, ctx)
if allBanterGroups then
local idx = self:Random(#allBanterGroups) + 1
local randomBanterGroup = allBanterGroups[idx]
if randomBanterGroup then
return randomBanterGroup.banters
end
end
return empty_table
end
function Unit:GetInteractableBadgeSpot()
if self:IsDead() then
return "Torso"
end
return "Headstatic"
end
local lockpickableFxActions = {
Lockpick = "Lockpick",
Break = "BreakLock",
Cut = "Cut",
}
function Unit:Lockpick(action_id, cost_ap, pos, goto_ap, target)
local can_interact = self:CanInteractWith(target, action_id, true)
if not can_interact then
if g_Combat then
self:GainAP(cost_ap)
CombatActionInterruped(self)
end
return
end
local action = CombatActions[action_id]
-- approach target
local action_cost = g_Combat and cost_ap - goto_ap or 0
local can_interact
if g_Combat then
self:PushDestructor(function(self)
self:GainAP(action_cost)
end)
PlayFX("InteractGoto", "start", self, target)
can_interact = self:CombatGoto(action_id, goto_ap, pos)
self:PopDestructor()
else
local positions, angle, preferred = target:GetInteractionPos(self)
can_interact = self:CloseEnoughToInteract(positions, target)
if not can_interact and preferred then
PlayFX("InteractGoto", "start", self, target)
NetUpdateHash("LockpickGotoPreferred", target, preferred)
can_interact = self:GotoSlab(preferred)
end
if not can_interact and positions then
PlayFX("InteractGoto", "start", self, target)
NetUpdateHash("LockpickGoto", target, positions)
can_interact = self:GotoSlab(positions)
end
end
can_interact = can_interact and self:CanInteractWith(target, action_id, true)
if not can_interact then
if g_Combat then
self:GainAP(action_cost)
CombatActionInterruped(self)
end
return
end
lInteractionLoadingBar(self, action_id, target)
--execute
local fx_action = lockpickableFxActions[action_id]
target:RegisterInteractingUnit(self)
self:PushDestructor(function(self)
if fx_action then
target:PlayLockpickableFX(fx_action, "end")
end
if not IsValid(target) then return end
target:UnregisterInteractingUnit(self)
target:EndInteraction(self)
end)
if fx_action then
target:PlayLockpickableFX(fx_action, "start")
end
target:BeginInteraction(self)
target:LogInteraction(self, action_id, "start")
PlayFX("Interact", "start", self, target)
self:Face(target, 200)
Sleep(200)
local result
if action_id == "Lockpick" then
result = target:InteractLockpick(self)
elseif action_id == "Break" then
result = target:InteractBreak(self)
elseif action_id == "Cut" then
target:InteractCut(self)
end
target:LogInteraction(self, action_id, "end", result)
if result == "success" and IsKindOf(target, "Door") then
Msg("DoorUnlocked", target, self)
end
-- The container is now openable
if target.lockpickState == "closed" then
local combat_action = target:GetInteractionCombatAction()
if combat_action then
combat_action:Execute({ self }, { target = target })
end
end
self:PopAndCallDestructor()
local dlg = GetInGameInterfaceModeDlg()
if IsKindOf(dlg, "IModeCommonUnitControl") then dlg:UpdateInteractablesHighlight() end
end
-- overheard conversations
function Unit:OverheardConversationHeadTo(point, face_point, marker)
self:PushDestructor(function(self)
if self.command ~= "OverheardConversation" and self.command ~= "OverheardConversationHeadTo" then
if g_debugOverheardMarker then print("Unit got interrupted en route to marker.", self.command) end
marker:SetCommand("StopRunning")
end
end)
self:ChangeStance(nil, nil, "Standing")
if point ~= self:GetPos() and self:GotoSlab(point) ~= pfFinished then
self:PopAndCallDestructor()
end
if self.carry_flare then
self:RoamDropFlare()
end
Msg("OverheardConversationPointReached", self)
self:SetRandomAnim(self:GetIdleBaseAnim())
Sleep(1000)
self:Face(face_point, 300)
-- Wait for the other units to get here.
self:OverheardConversationWaiting()
end
function Unit:OverheardConversationWaiting()
while true do
Sleep(1000)
if g_Combat then
self:SetCommand("Idle")
break
end
end
end
function Unit:OverheardConversation(face_point, marker)
self:PushDestructor(function(self)
if marker.playing_banter then
if g_debugOverheardMarker then print("Unit got interrupted during overheard banter.", self.command) end
marker:SetCommand("StopRunning")
end
end)
self:SetRandomAnim(self:GetIdleBaseAnim())
if face_point then
self:Face(face_point, 300)
end
-- Wait for banter to finish.
self:OverheardConversationWaiting()
end