GTG Safe Zone
Blocking actions
While the zone is quiet, a defender who pulls the trigger inside it should get a shot that does not go off and a line that says why. Doing that is harder than it sounds, because most of the obvious switches are read too late.
The condition
Actions are blocked when a character stands in a zone and the alarm is off. That is a different question from the damage guard: it asks only about place and alarm. While nothing is going on, nobody shoots in the zone, an intruder included; the moment an intruder is spotted, the lock drops for everyone.
// Client and server both ask this; it uses only the replicated outline and the replicated alarm flag.
static bool AreActionsBlockedFor(IEntity entity)
{
MyAD_Zone zone = FindProtectingZoneOf(entity);
return zone != null && !zone.m_bAlarmActive;
}
Three locks on different levels
None of them is enough on its own, and they work on different things.
| Lock | What it stops | Why that level |
|---|---|---|
SetWeaponNoFireTime() | The shot | A state with a duration, not a flag. It does not matter where in the frame the code runs. |
SetFireWeaponWanted(false) | The fire request of AI and mounts | Cleared every frame. For a player the request has been read by the time this runs. |
| Input context | Throw and melee | Both can be cancelled while they happen. A shot that has gone off cannot. |
Here is the hook on the character controller, cut down to the three:
modded class SCR_CharacterControllerComponent
{
protected bool m_bMyAD_Blocked;
protected static const float NO_FIRE_SECONDS = 0.5; // const inside a class has to be static
override void OnPrepareControls(IEntity owner, ActionManager am, float dt, bool player)
{
super.OnPrepareControls(owner, am, dt, player);
m_bMyAD_Blocked = MyAD_Zone.AreActionsBlockedFor(owner);
if (!m_bMyAD_Blocked)
return;
SetFireWeaponWanted(false);
// Refreshed every frame, a little longer than a frame, so one hitch does not open a gap.
if (!player || MyAD_IsWeaponInPlay())
SetWeaponNoFireTime(NO_FIRE_SECONDS);
CharacterInputContext input = GetInputContext();
if (!input)
return;
input.SetWeaponPullTrigger(false);
input.SetMeleeAttack(false);
if (input.GetThrow())
{
input.SetThrow(false);
input.CancelThrow();
}
}
override bool GetCanMeleeAttack()
{
if (m_bMyAD_Blocked)
return false;
return super.GetCanMeleeAttack();
}
}
Do not stop the player from drawing
The time lock has a price. Vanilla uses SetWeaponNoFireTime() in one place, after a gadget is put away, and in
that state the animation also holds the weapon down. Refreshed every frame for everyone in the zone, it meant that nobody
could equip a weapon at all – players on a keyboard hardly noticed, because the weapon tends to be up already when they
enter, but console players did.
The cure is to arm the lock for a player only once the weapon is really out. A weapon that is not in the hand cannot be in any of the states below, so drawing stays free and the lock engages afterwards. It is not enough to ask whether the weapon is raised: from low ready, which is a partly lowered weapon, that question answers no, and a pull of the trigger raises and fires in the same moment.
// True as soon as the weapon is out, in whatever stance.
protected bool MyAD_IsWeaponInPlay()
{
return IsWeaponRaised()
|| IsPartiallyLowered() // low ready
|| IsWeaponADS()
|| GetIsWeaponDeployed()
|| GetIsWeaponDeployedBipod();
}
The visible consequence is that inside a zone a weapon cannot be kept raised; it sinks back down. That is a side effect to accept, not a fault to fix.
This is feedback
All of the above runs on the client, where the input is. Treat it accordingly: it makes the zone feel like a zone, and it tells the player why nothing happens. The messages ("Safe Zone Active!") are local lines, limited to one every two seconds – without that, holding the trigger would write sixty lines a second. What you can rely on is the damage check on the server, described on Invulnerability.