You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Assets/Engine/Scripts/Entities/Triggers/ProximityTrigger.lua

617 lines
16 KiB
Lua

----------------------------------------------------------------------------------------------------
--
-- Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution.
--
-- SPDX-License-Identifier: Apache-2.0 OR MIT
--
--
--
--
--
-- Description: Network-ready Proximity Trigger
--
----------------------------------------------------------------------------------------------------
ProximityTrigger =
{
Properties =
{
DimX = 5,
DimY = 5,
DimZ = 5,
bEnabled = 1,
EnterDelay = 0,
ExitDelay = 0,
bOnlyPlayer = 1,
bOnlyMyPlayer = 0,
bOnlyAI = 0,
bOnlySpecialAI = 0,
esFactionFilter = "",
OnlySelectedEntity = "None",
bRemoveOnTrigger = 0,
bTriggerOnce = 0,
ScriptCommand = "",
PlaySequence = "",
bInVehicleOnly = 0,
bOnlyOneEntity = 0,
UsableMessage = "",
bActivateWithUseButton = 0,
MultiplayerOptions =
{
bNetworked = 0,
bPerPlayer = 0,
},
},
Client={},
Server={},
Editor =
{
Model="Editor/Objects/T.cgf",
Icon="Trigger.bmp",
ShowBounds = 1,
IsScalable = false;
IsRotatable = false;
},
}
Net.Expose
{
Class = ProximityTrigger,
ClientMethods =
{
ClEnter = { RELIABLE_ORDERED, PRE_ATTACH, ENTITYID, INT8},
ClLeave = { RELIABLE_ORDERED, PRE_ATTACH, ENTITYID, INT8},
},
ServerMethods =
{
SvRequestUse = { RELIABLE_ORDERED, PRE_ATTACH, ENTITYID },
},
ServerProperties = {}
}
function ProximityTrigger:OnPropertyChange()
self:OnReset();
end
function ProximityTrigger:OnReset()
-- Precalcs some CVars used to test triggering entities based in their names. ("selectedEntity" is a misleading name for all this)
self.bUsesExactSelectedEntity = false;
self.bUsesWildcardSelectedEntity = false;
if (self.Properties.OnlySelectedEntity~="None" and self.Properties.OnlySelectedEntity~="") then
local indexChar = string.find( self.Properties.OnlySelectedEntity, "*" );
if (indexChar and indexChar>1) then
if (indexChar<5) then
LogWarning( "proximity trigger: '%s' is using a too much generic name for 'selectedEntity' field", self:GetName() );
end
self.bUsesWildcardSelectedEntity = true;
self.stringRootSelectedEntity = string.sub( self.Properties.OnlySelectedEntity, 1, indexChar - 1 );
else
self.bUsesExactSelectedEntity = true;
end
end
if (self.timers) then
for i,v in pairs(self.timers) do
self:KillTimer(i);
end
end
self.timerId = 0;
self.enabled = nil;
self.usable = tonumber(self.Properties.bActivateWithUseButton)~=0;
self.triggerOnce = tonumber(self.Properties.bTriggerOnce)~=0;
self.localOnly = self.Properties.MultiplayerOptions.bNetworked==0;
self.perPlayer = tonumber(self.Properties.MultiplayerOptions.bPerPlayer)~=0;
self.isServer=CryAction.IsServer();
self.isClient=CryAction.IsClient();
self.inside={};
self.timers={};
if (not self.localOnly) then
self.triggeredPP={};
self.triggeredOncePP={};
else
self:SetFlags(ENTITY_FLAG_CLIENT_ONLY,0);
end
self.triggeredOnce=nil;
self.triggered=nil;
self.insideCount=0;
local min = { x=-self.Properties.DimX/2, y=-self.Properties.DimY/2, z=-self.Properties.DimZ/2 };
local max = { x=self.Properties.DimX/2, y=self.Properties.DimY/2, z=self.Properties.DimZ/2 };
self:SetUpdatePolicy( ENTITY_UPDATE_PHYSICS );
self:SetTriggerBBox( min, max );
self:Enable(tonumber(self.Properties.bEnabled)~=0);
self:InvalidateTrigger();
self:ActivateOutput("NrOfEntitiesInside", 0);
end
function ProximityTrigger:Enable(enable)
self.enabled=enable;
self:RegisterForAreaEvents(enable and 1 or 0);
end
function ProximityTrigger:OnSpawn()
self:OnReset();
end
function ProximityTrigger:OnDestroy()
end
function ProximityTrigger:OnSave(tbl)
tbl.enabled = self.enabled;
tbl.triggered = self.triggered;
tbl.triggeredOnce = self.triggeredOnce;
end
function ProximityTrigger:OnLoad(tbl)
self:OnReset();
self.enabled = tbl.enabled;
self.triggered = tbl.triggered;
self.triggeredOnce = tbl.triggeredOnce;
end
function ProximityTrigger:Event_Enter(entityId)
if (not self.enabled) then return; end; -- TODO: might need a self.active here
if (self.triggerOnce) then
if (self.localOnly) then
if (self.triggeredOnce) then
return;
end
elseif (self.perPlayer and self.triggeredOncePP[entityId]) then -- TODO: will need to skip this for non-player entities
return;
elseif (not self.perPlayer and self.triggeredOnce) then
return;
end
end
self.triggered=true;
self.triggeredOnce=true;
if (not self.localOnly and entityId) then
self.triggeredPP[entityId]=true;
self.triggeredOncePP[entityId]=true;
end
-- Log(" ProximityTrigger: %s:Event_Enter(%s) inside: %d", self:GetName(), EntityName(entityId), self.insideCount);
self:Trigger(entityId, self.insideCount);
--BroadcastEvent(self, "Enter");
self:ActivateOutput("Enter", entityId or NULL_ENTITY);
if (not self.localOnly and self.isServer) then
self.otherClients:ClEnter(g_localChannelId, entityId or NULL_ENTITY, self.insideCount);
end
end
function ProximityTrigger.Client:ClEnter(entityId, insideCount)
-- Log("ProximityTrigger: %s.Client:ClEnter(%s) count: %d", self:GetName(), EntityName(entityId), insideCount);
self:Trigger(entityId, insideCount);
--BroadcastEvent(self, "Enter");
self:ActivateOutput("Enter", entityId);
end
function ProximityTrigger:Event_Leave(entityId)
if (not self.enabled) then return; end;
if (self.localOnly and not self.triggered) then return; end;
if (self.perPlayer) then
if (not self.localOnly and entityId) then
if (not self.triggeredPP[entityId]) then
return;
end;
end
else
if (not self.triggered) then
return;
end;
end
--only disable triggered when all players are gone
if(self.insideCount == 0) then
self.triggered=nil;
end
if (not self.localOnly and entityId and self.insideCount == 0) then
self.triggeredPP[entityId]=nil;
end
--Log("%s:Event_Leave(%s)", self:GetName(), EntityName(entityId));
self:ActivateOutput("Sender", entityId or NULL_ENTITY);
self:ActivateOutput("NrOfEntitiesInside", self.insideCount);
--BroadcastEvent(self, "Leave");
self:ActivateOutput("Leave", entityId or NULL_ENTITY);
if (not self.localOnly and self.isServer) then
self.otherClients:ClLeave(g_localChannelId, entityId or NULL_ENTITY, self.insideCount);
end
end
function ProximityTrigger.Client:ClLeave(entityId, inside)
-- Log("%s.Client:ClLeave(%s, %s)", self:GetName(), EntityName(entityId), tostring(inside));
self:ActivateOutput("Sender", entityId);
self:ActivateOutput("NrOfEntitiesInside", inside);
self:ActivateOutput("Leave", entityId);
--BroadcastEvent(self, "Leave");
end
function ProximityTrigger:Event_Enable()
if (self.enabled) then return; end;
-- Act as if everyone entered the trigger
self.enabled = true;
for k,v in pairs(self.inside) do
self:Event_Enter( k );
end;
self:ActivateOutput("NrOfEntitiesInside", self.insideCount);
BroadcastEvent(self, "Enable");
end
function ProximityTrigger:Event_Disable()
if (not self.enabled) then return; end;
-- Act as if everyone left the trigger
self.enabled = false;
for k,v in pairs(self.inside) do
self:Event_Leave( k );
end;
self:ActivateOutput("NrOfEntitiesInside", 0);
BroadcastEvent(self, "Disable");
end
function ProximityTrigger:CreateTimer(entityId, time, leave)
local timerId=self.timerId;
if (timerId>1023) then
timerId=0;
end
timerId=timerId+1;
self.timerId=timerId;
if (leave) then
timerId=timerId+1024;
end
self.timers[timerId]=entityId;
self:SetTimer(timerId, time);
end
function ProximityTrigger.Client:OnTimer(timerId, msec)
if (self.localOnly and not self.isServer) then
self:OnTimer(timerId, msec);
end
end
function ProximityTrigger.Server:OnTimer(timerId, msec)
self:OnTimer(timerId, msec);
end
function ProximityTrigger:OnTimer(timerId, msec)
if (timerId==2048) then
self:CheckAIDeaths()
return;
end
local entityId=self.timers[timerId];
if (not entityId) then return; end;
if (timerId>1023) then
self:Event_Leave(entityId);
else
self:Event_Enter(entityId);
end
end
function ProximityTrigger:CheckAIDeaths()
local amountInside = 0;
for k,v in pairs(self.inside) do
local entity = System.GetEntity( k );
if (entity~=nil and entity.ai~=nil and entity.lastHealth<=0) then
self.inside[k] = nil;
else
amountInside = amountInside + 1;
end
end
if (amountInside~=self.insideCount) then
self.insideCount = amountInside;
if (self.enabled) then
self:ActivateOutput("NrOfEntitiesInside", self.insideCount);
end
end
if (amountInside~=0) then
self:CreateAIDeathsCheckTrigger();
else
self.timers[2048]=false; --to know that we are not using the AI trigger now
end
end
-- this timer should be created only when it is a trigger that reacts to AI (which should be only a few), and there are AIs inside
function ProximityTrigger:CreateAIDeathsCheckTrigger()
self.timers[2048]=true; -- to know that we are using the AI timer. 2048 is the AI timer id
self:SetTimer( 2048, 3000);
end
function ProximityTrigger.Server:SvRequestUse(userId)
local entity=System.GetEntity(userId);
if (entity) then
self:OnUsed(entity);
end
end
function ProximityTrigger:OnUsed(user)
if (not self:CanTrigger(user)) then return; end;
Log("%s:OnUsed(%s)", self:GetName(), EntityName(user));
self:LockUsability();
if (self.localOnly or self.isServer) then
self:CreateTimer(user.id, self.Properties.EnterDelay*1000);
else
self.server:SvRequestUse(user.id);
end
end
function ProximityTrigger:Trigger(entityId, inside)
if (self.enabled) then
self:ActivateOutput("NrOfEntitiesInside", inside);
end
if(self.Properties.ScriptCommand and self.Properties.ScriptCommand~="")then
local f = loadstring(self.Properties.ScriptCommand);
if (f~=nil) then
f();
end
end
if(self.Properties.PlaySequence~="")then
Movie.PlaySequence(self.Properties.PlaySequence);
end
self:ActivateOutput("Sender", entityId or NULL_ENTITY);
if(AI ~= nil) then
self:ActivateOutput("Faction", AI.GetFactionOf(entityId or NULL_ENTITY) or "");
end
local isAPlayer = ActorSystem.IsPlayer(entityId);
if self.Properties.bRemoveOnTrigger ~= 0 and not isAPlayer then
System.RemoveEntity(entityId);
end
end
function ProximityTrigger:EnteredArea(entity, areaId)
if (not self:CanTrigger(entity, areaId)) then
return;
end
if (tonumber(self.Properties.bOnlyOneEntity)~=0 and self.insideCount>0) then
return;
end
self.inside[entity.id]=true;
self.insideCount=self.insideCount+1;
if (not entity.ai) then
if (self.Properties.bActivateWithUseButton~=0) then
return;
end
end
if (not self.enabled) then return; end;
self:CreateTimer(entity.id, self.Properties.EnterDelay*1000);
if (entity.ai and self.timers[2048]~=true) then -- 2048 is the special timer id used to check the deaths of AIs
self:CreateAIDeathsCheckTrigger();
end
end
function ProximityTrigger:LeftArea(entity, areaId)
if (not self:CanTrigger(entity, areaId)) then
return;
end
self.inside[entity.id]=nil;
self.insideCount=self.insideCount-1;
if(self.Properties.ExitDelay==0) then
self.Properties.ExitDelay=0.01;
end
if (not self.enabled) then return; end;
self:CreateTimer(entity.id, self.Properties.ExitDelay*1000, true);
end
function ProximityTrigger.Server:OnEnterArea(entity, areaId)
if (self:CanTrigger(entity)) then
self:EnteredArea(entity, areaId);
end
end
function ProximityTrigger.Server:OnLeaveArea(entity, areaId)
self:LeftArea(entity, areaId);
end
function ProximityTrigger.Client:OnEnterArea(entity, areaId)
if (not self:CanTrigger(entity)) then return; end;
if (entity.actor) then
if (self.usable and self.enabled) then
self:LockUsability(true);
end
end
if (not self.localOnly or self.isServer) then return; end;
self:EnteredArea(entity, areaId);
end
function ProximityTrigger.Client:OnLeaveArea(entity, areaId)
if (entity.actor) then
if (self.usable and self.enabled) then
self:LockUsability(true);
end
end
if (not self.localOnly or self.isServer) then return; end;
self:LeftArea(entity, areaId);
end
function ProximityTrigger:CanTrigger(entity)
local Properties = self.Properties;
if (entity.ai and entity.lastHealth and (entity.lastHealth <= 0)) then
return false;
end
local isAPlayer = ActorSystem.IsPlayer(entity.id);
if (Properties.bOnlyPlayer ~= 0 and not(isAPlayer)) then
return false;
end
if (Properties.bOnlySpecialAI ~= 0 and entity.ai and entity.Properties.special==0) then
return false;
end
if (Properties.bOnlyAI ~=0 and not entity.ai) then
return false;
end
if (Properties.bOnlyMyPlayer ~= 0 and entity ~= g_localActor) then
return false;
end
if (Properties.bInVehicleOnly ~= 0 and not entity.vehicleId) then
return false;
end
-- looks for exact name match, if defined
if (self.bUsesExactSelectedEntity and entity:GetName()~=Properties.OnlySelectedEntity) then
return false;
end
-- looks for generic (wildcard) name match, if defined
if (self.bUsesWildcardSelectedEntity) then
local indexStringFound = string.find( entity:GetName(), self.stringRootSelectedEntity );
if (indexStringFound~=1) then
return false;
end
end
if (Properties.esFactionFilter ~= "") then
local faction = AI.GetFactionOf(entity.id) or "";
if (faction ~= Properties.esFactionFilter) then
return false
end
end
return true;
end
function ProximityTrigger:IsUsable(user)
return self.usable and self.enabled;
end
function ProximityTrigger:LockUsability(lock)
local player=g_localActor;
if (player) then
if (lock) then
player.actor:SetExtensionParams("Interactor", {locker = self.id, lockId = self.id, lockIdx = 1});
else
player.actor:SetExtensionParams("Interactor", {locker = self.id, lockId = NULL_ENTITY, lockIdx = 0});
end
end
end
function ProximityTrigger:GetUsableMessage()
return self.Properties.UsableMessage or "";
end
ProximityTrigger.FlowEvents =
{
Inputs =
{
Disable = { ProximityTrigger.Event_Disable, "bool" },
Enable = { ProximityTrigger.Event_Enable, "bool" },
Enter = { ProximityTrigger.Event_Enter, "bool" },
Leave = { ProximityTrigger.Event_Leave, "bool" },
},
Outputs =
{
NrOfEntitiesInside = "int",
Disable = "bool",
Enable = "bool",
Enter = "entity",
Leave = "entity",
Sender = "entity",
Faction = "string",
},
}