Skip to main content

Pulse.Team

Pulse.Team gives you a resolver pattern for friend/enemy classification. Instead of writing the same team-check conditions in every component, you define the rules once and share them.


The problem

Without a resolver, every feature that cares about who's an enemy has its own version of:

if player.Team ~= LocalPlayer.Team
and player ~= LocalPlayer
and player.Character ~= nil
and not isAllySpecialCase(player)
then
-- it's an enemy
end

Duplicated 6 times = 6 places to update when your logic changes. A resolver centralises this.


Creating a resolver

local resolver = Pulse.Team.resolver({
isSelf = function(p) return p == _LocalPlayer end,
isValid = function(p) return p.Character ~= nil end,
isHostile = function(p)
-- true = this entity should be targeted
return func.IsOnWarriorsTeam() ~= func.IsWarrior(p)
end,
exclude = {
-- array of functions; if any returns true, skip this entity
func.IsAllySpecialCase,
},
})

All fields are optional. The resolver applies them in order:

  1. Skip if isSelf returns true
  2. Skip if isValid returns false
  3. Skip if any exclude function returns true
  4. If isHostile returns true → enemy

Using a resolver

resolver:isEnemy(player) -- → bool: is this specific entity an enemy?
resolver:filter(playerList) -- → array of enemies only
resolver:partition(playerList) -- → { enemies = {}, friendlies = {} }

-- src/misc/helpers/globals.lua

local _playerResolver = Pulse.Team.resolver({
isSelf = function(p) return p == _LocalPlayer end,
isValid = function(p) return p.Character ~= nil end,
isHostile = function(p) return func.IsOnWarriorsTeam() ~= func.IsWarrior(p) end,
exclude = { func.IsParamountShifterFriendly },
})

func.IsEnemy = function(p) return _playerResolver:isEnemy(p) end
func.PlayerResolver = _playerResolver

Then in any component:

for _, p in ipairs(func.PlayerResolver:filter(func.GetCachedPlayers())) do
-- only enemies
end

The exclude array

exclude is checked after isHostile. If ANY exclude function returns true, the entity is treated as friendly — even if isHostile is also true. This is how you handle "always ally" special cases:

exclude = {
func.IsOurOwnShifter, -- never target yourself when shifted
func.ShouldIgnoreAttackTitan, -- special ally that uses the enemy team
}

Limitations

  • Resolvers are pure functions — they don't track state. The result can change between calls if the underlying team state changes (e.g., a player switches teams mid-game).
  • partition iterates the full list. For very large lists (hundreds of entities), consider filter + your own logic if you only need one partition.