refactor: re-write AOE, add FilterTargets, Update TacArc Reading (#1035)

* Re-write AOE behavior for new filter targets
Update Tacarc to use new filter targets
Added dev commands for skill and attack debugging

* Get all entities by detroyable
rather than controllable physics
Since destroyables are what can be hit

* Re-work filter targets to be 100% live accurate
reduce memory usage by only using one vector and removing invalid entries
get entities in the proximity rather than all entities with des comps in the instance, as was done in live

* remove debuging longs and remove oopsie

* address feedback

* make log more useful

* make filter more flat

* Add some more checks to filter targets
add pvp checks to isenemy

* fix typing

* Add filter target to TacArc and update filter target

* fix double declaration

* Some debugging logs

* Update TacArc reading

* make log clearer

* logs

* Update TacArcBehavior.cpp

* banana

* fix max targets

* remove extreanous parenthesesuuesdsds

* make behavior slot use a real type

---------

Co-authored-by: David Markowitz <EmosewaMC@gmail.com>
This commit is contained in:
Aaron Kimbrell 2023-10-09 15:18:51 -05:00 committed by GitHub
parent 471d65707c
commit d8ac148cee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 474 additions and 366 deletions

View File

@ -298,6 +298,16 @@ std::vector<Entity*> EntityManager::GetEntitiesByLOT(const LOT& lot) const {
return entities; return entities;
} }
std::vector<Entity*> EntityManager::GetEntitiesByProximity(NiPoint3 reference, float radius) const{
std::vector<Entity*> entities = {};
if (radius > 1000.0f) return entities;
for (const auto& entity : m_Entities) {
if (NiPoint3::Distance(reference, entity.second->GetPosition()) <= radius) entities.push_back(entity.second);
}
return entities;
}
Entity* EntityManager::GetZoneControlEntity() const { Entity* EntityManager::GetZoneControlEntity() const {
return m_ZoneControlEntity; return m_ZoneControlEntity;
} }

View File

@ -28,6 +28,7 @@ public:
std::vector<Entity*> GetEntitiesInGroup(const std::string& group); std::vector<Entity*> GetEntitiesInGroup(const std::string& group);
std::vector<Entity*> GetEntitiesByComponent(eReplicaComponentType componentType) const; std::vector<Entity*> GetEntitiesByComponent(eReplicaComponentType componentType) const;
std::vector<Entity*> GetEntitiesByLOT(const LOT& lot) const; std::vector<Entity*> GetEntitiesByLOT(const LOT& lot) const;
std::vector<Entity*> GetEntitiesByProximity(NiPoint3 reference, float radius) const;
Entity* GetZoneControlEntity() const; Entity* GetZoneControlEntity() const;
// Get spawn point entity by spawn name // Get spawn point entity by spawn name

View File

@ -20,134 +20,114 @@ void AreaOfEffectBehavior::Handle(BehaviorContext* context, RakNet::BitStream* b
return; return;
} }
if (targetCount > this->m_maxTargets) { if (this->m_useTargetPosition && branch.target == LWOOBJID_EMPTY) return;
if (targetCount == 0){
PlayFx(u"miss", context->originator);
return; return;
} }
std::vector<LWOOBJID> targets; if (targetCount > this->m_maxTargets) {
Game::logger->Log("AreaOfEffectBehavior", "Serialized size is greater than max targets! Size: %i, Max: %i", targetCount, this->m_maxTargets);
return;
}
auto caster = context->caster;
if (this->m_useTargetAsCaster) context->caster = branch.target;
std::vector<LWOOBJID> targets;
targets.reserve(targetCount); targets.reserve(targetCount);
for (auto i = 0u; i < targetCount; ++i) { for (auto i = 0u; i < targetCount; ++i) {
LWOOBJID target{}; LWOOBJID target{};
if (!bitStream->Read(target)) { if (!bitStream->Read(target)) {
Game::logger->Log("AreaOfEffectBehavior", "failed to read in target %i from bitStream, aborting target Handle!", i); Game::logger->Log("AreaOfEffectBehavior", "failed to read in target %i from bitStream, aborting target Handle!", i);
return;
}; };
targets.push_back(target); targets.push_back(target);
} }
for (auto target : targets) { for (auto target : targets) {
branch.target = target; branch.target = target;
this->m_action->Handle(context, bitStream, branch); this->m_action->Handle(context, bitStream, branch);
} }
context->caster = caster;
PlayFx(u"cast", context->originator);
} }
void AreaOfEffectBehavior::Calculate(BehaviorContext* context, RakNet::BitStream* bitStream, BehaviorBranchContext branch) { void AreaOfEffectBehavior::Calculate(BehaviorContext* context, RakNet::BitStream* bitStream, BehaviorBranchContext branch) {
auto* self = Game::entityManager->GetEntity(context->caster); auto* caster = Game::entityManager->GetEntity(context->caster);
if (self == nullptr) { if (!caster) return;
Game::logger->Log("AreaOfEffectBehavior", "Invalid self for (%llu)!", context->originator);
return; // determine the position we are casting the AOE from
auto reference = branch.isProjectile ? branch.referencePosition : caster->GetPosition();
if (this->m_useTargetPosition) {
if (branch.target == LWOOBJID_EMPTY) return;
auto branchTarget = Game::entityManager->GetEntity(branch.target);
if (branchTarget) reference = branchTarget->GetPosition();
} }
auto reference = branch.isProjectile ? branch.referencePosition : self->GetPosition(); reference += this->m_offset;
std::vector<Entity*> targets; std::vector<Entity*> targets {};
targets = Game::entityManager->GetEntitiesByProximity(reference, this->m_radius);
auto* presetTarget = Game::entityManager->GetEntity(branch.target); context->FilterTargets(targets, this->m_ignoreFactionList, this->m_includeFactionList, this->m_targetSelf, this->m_targetEnemy, this->m_targetFriend, this->m_targetTeam);
if (presetTarget != nullptr) {
if (this->m_radius * this->m_radius >= Vector3::DistanceSquared(reference, presetTarget->GetPosition())) {
targets.push_back(presetTarget);
}
}
int32_t includeFaction = m_includeFaction;
if (self->GetLOT() == 14466) // TODO: Fix edge case
{
includeFaction = 1;
}
// Gets all of the valid targets, passing in if should target enemies and friends
for (auto validTarget : context->GetValidTargets(m_ignoreFaction, includeFaction, m_TargetSelf == 1, m_targetEnemy == 1, m_targetFriend == 1)) {
auto* entity = Game::entityManager->GetEntity(validTarget);
if (entity == nullptr) {
Game::logger->Log("AreaOfEffectBehavior", "Invalid target (%llu) for (%llu)!", validTarget, context->originator);
continue;
}
if (std::find(targets.begin(), targets.end(), entity) != targets.end()) {
continue;
}
auto* destroyableComponent = entity->GetComponent<DestroyableComponent>();
if (destroyableComponent == nullptr) {
continue;
}
if (destroyableComponent->HasFaction(m_ignoreFaction)) {
continue;
}
const auto distance = Vector3::DistanceSquared(reference, entity->GetPosition());
if (this->m_radius * this->m_radius >= distance && (this->m_maxTargets == 0 || targets.size() < this->m_maxTargets)) {
targets.push_back(entity);
}
}
// sort by distance
std::sort(targets.begin(), targets.end(), [reference](Entity* a, Entity* b) { std::sort(targets.begin(), targets.end(), [reference](Entity* a, Entity* b) {
const auto aDistance = Vector3::DistanceSquared(a->GetPosition(), reference); const auto aDistance = NiPoint3::Distance(a->GetPosition(), reference);
const auto bDistance = Vector3::DistanceSquared(b->GetPosition(), reference); const auto bDistance = NiPoint3::Distance(b->GetPosition(), reference);
return aDistance < bDistance;
}
);
return aDistance > bDistance; // resize if we have more than max targets allows
}); if (targets.size() > this->m_maxTargets) targets.resize(this->m_maxTargets);
const uint32_t size = targets.size(); bitStream->Write<uint32_t>(targets.size());
bitStream->Write(size); if (targets.size() == 0) {
PlayFx(u"miss", context->originator);
if (size == 0) {
return; return;
} } else {
context->foundTarget = true;
// write all the targets to the bitstream
for (auto* target : targets) {
bitStream->Write(target->GetObjectID());
}
context->foundTarget = true; // then cast all the actions
for (auto* target : targets) {
for (auto* target : targets) { branch.target = target->GetObjectID();
bitStream->Write(target->GetObjectID()); this->m_action->Calculate(context, bitStream, branch);
}
PlayFx(u"cast", context->originator, target->GetObjectID()); PlayFx(u"cast", context->originator);
}
for (auto* target : targets) {
branch.target = target->GetObjectID();
this->m_action->Calculate(context, bitStream, branch);
} }
} }
void AreaOfEffectBehavior::Load() { void AreaOfEffectBehavior::Load() {
this->m_action = GetAction("action"); this->m_action = GetAction("action"); // required
this->m_radius = GetFloat("radius", 0.0f); // required
this->m_maxTargets = GetInt("max targets", 100);
if (this->m_maxTargets == 0) this->m_maxTargets = 100;
this->m_useTargetPosition = GetBoolean("use_target_position", false);
this->m_useTargetAsCaster = GetBoolean("use_target_as_caster", false);
this->m_offset = NiPoint3(
GetFloat("offset_x", 0.0f),
GetFloat("offset_y", 0.0f),
GetFloat("offset_z", 0.0f)
);
this->m_radius = GetFloat("radius"); // params after this are needed for filter targets
const auto parameters = GetParameterNames();
this->m_maxTargets = GetInt("max targets"); for (const auto& parameter : parameters) {
if (parameter.first.rfind("include_faction", 0) == 0) {
this->m_ignoreFaction = GetInt("ignore_faction"); this->m_includeFactionList.push_front(parameter.second);
} else if (parameter.first.rfind("ignore_faction", 0) == 0) {
this->m_includeFaction = GetInt("include_faction"); this->m_ignoreFactionList.push_front(parameter.second);
}
this->m_TargetSelf = GetInt("target_self"); }
this->m_targetSelf = GetBoolean("target_self", false);
this->m_targetEnemy = GetInt("target_enemy"); this->m_targetEnemy = GetBoolean("target_enemy", false);
this->m_targetFriend = GetBoolean("target_friend", false);
this->m_targetFriend = GetInt("target_friend"); this->m_targetTeam = GetBoolean("target_team", false);
} }

View File

@ -1,34 +1,26 @@
#pragma once #pragma once
#include "Behavior.h" #include "Behavior.h"
#include <forward_list>
class AreaOfEffectBehavior final : public Behavior class AreaOfEffectBehavior final : public Behavior
{ {
public: public:
Behavior* m_action; explicit AreaOfEffectBehavior(const uint32_t behaviorId) : Behavior(behaviorId) {}
uint32_t m_maxTargets;
float m_radius;
int32_t m_ignoreFaction;
int32_t m_includeFaction;
int32_t m_TargetSelf;
int32_t m_targetEnemy;
int32_t m_targetFriend;
/*
* Inherited
*/
explicit AreaOfEffectBehavior(const uint32_t behaviorId) : Behavior(behaviorId) {
}
void Handle(BehaviorContext* context, RakNet::BitStream* bitStream, BehaviorBranchContext branch) override; void Handle(BehaviorContext* context, RakNet::BitStream* bitStream, BehaviorBranchContext branch) override;
void Calculate(BehaviorContext* context, RakNet::BitStream* bitStream, BehaviorBranchContext branch) override; void Calculate(BehaviorContext* context, RakNet::BitStream* bitStream, BehaviorBranchContext branch) override;
void Load() override; void Load() override;
private:
Behavior* m_action;
uint32_t m_maxTargets;
float m_radius;
bool m_useTargetPosition;
bool m_useTargetAsCaster;
NiPoint3 m_offset;
std::forward_list<int32_t> m_ignoreFactionList {};
std::forward_list<int32_t> m_includeFactionList {};
bool m_targetSelf;
bool m_targetEnemy;
bool m_targetFriend;
bool m_targetTeam;
}; };

View File

@ -15,6 +15,7 @@
#include "PhantomPhysicsComponent.h" #include "PhantomPhysicsComponent.h"
#include "RebuildComponent.h" #include "RebuildComponent.h"
#include "eReplicaComponentType.h" #include "eReplicaComponentType.h"
#include "TeamManager.h"
#include "eConnectionType.h" #include "eConnectionType.h"
BehaviorSyncEntry::BehaviorSyncEntry() { BehaviorSyncEntry::BehaviorSyncEntry() {
@ -307,46 +308,123 @@ void BehaviorContext::Reset() {
this->scheduledUpdates.clear(); this->scheduledUpdates.clear();
} }
std::vector<LWOOBJID> BehaviorContext::GetValidTargets(int32_t ignoreFaction, int32_t includeFaction, bool targetSelf, bool targetEnemy, bool targetFriend) const { void BehaviorContext::FilterTargets(std::vector<Entity*>& targets, std::forward_list<int32_t>& ignoreFactionList, std::forward_list<int32_t>& includeFactionList, bool targetSelf, bool targetEnemy, bool targetFriend, bool targetTeam) const {
auto* entity = Game::entityManager->GetEntity(this->caster);
std::vector<LWOOBJID> targets; // if we aren't targeting anything, then clear the targets vector
if (!targetSelf && !targetEnemy && !targetFriend && !targetTeam && ignoreFactionList.empty() && includeFactionList.empty()) {
if (entity == nullptr) { targets.clear();
Game::logger->Log("BehaviorContext", "Invalid entity for (%llu)!", this->originator); return;
return targets;
} }
if (!ignoreFaction && !includeFaction) { // if the caster is not there, return empty targets list
for (auto entry : entity->GetTargetsInPhantom()) { auto* caster = Game::entityManager->GetEntity(this->caster);
auto* instance = Game::entityManager->GetEntity(entry); if (!caster) {
Game::logger->LogDebug("BehaviorContext", "Invalid caster for (%llu)!", this->originator);
if (instance == nullptr) { targets.clear();
continue; return;
}
targets.push_back(entry);
}
} }
if (ignoreFaction || includeFaction || (!entity->HasComponent(eReplicaComponentType::PHANTOM_PHYSICS) && targets.empty())) { auto index = targets.begin();
DestroyableComponent* destroyableComponent; while (index != targets.end()) {
if (!entity->TryGetComponent(eReplicaComponentType::DESTROYABLE, destroyableComponent)) { auto candidate = *index;
return targets;
// make sure we don't have a nullptr
if (!candidate) {
index = targets.erase(index);
continue;
} }
auto entities = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::CONTROLLABLE_PHYSICS); // handle targeting the caster
for (auto* candidate : entities) { if (candidate == caster){
const auto id = candidate->GetObjectID(); // if we aren't targeting self, erase, otherise increment and continue
if (!targetSelf) index = targets.erase(index);
else index++;
continue;
}
if ((id != entity->GetObjectID() || targetSelf) && destroyableComponent->CheckValidity(id, ignoreFaction || includeFaction, targetEnemy, targetFriend)) { // make sure that the entity is targetable
targets.push_back(id); if (!CheckTargetingRequirements(candidate)) {
index = targets.erase(index);
continue;
}
// get factions to check against
// CheckTargetingRequirements checks for a destroyable component
// but we check again because bounds check are necessary
auto candidateDestroyableComponent = candidate->GetComponent<DestroyableComponent>();
if (!candidateDestroyableComponent) {
index = targets.erase(index);
continue;
}
// if they are dead, then earse and continue
if (candidateDestroyableComponent->GetIsDead()){
index = targets.erase(index);
continue;
}
// if their faction is explicitly included, increment and continue
auto candidateFactions = candidateDestroyableComponent->GetFactionIDs();
if (CheckFactionList(includeFactionList, candidateFactions)){
index++;
continue;
}
// check if they are a team member
if (targetTeam){
auto* team = TeamManager::Instance()->GetTeam(this->caster);
if (team){
// if we find a team member keep it and continue to skip enemy checks
if(std::find(team->members.begin(), team->members.end(), candidate->GetObjectID()) != team->members.end()){
index++;
continue;
}
} }
} }
}
return targets; // if the caster doesn't have a destroyable component, return an empty targets list
auto* casterDestroyableComponent = caster->GetComponent<DestroyableComponent>();
if (!casterDestroyableComponent) {
targets.clear();
return;
}
// if we arent targeting a friend, and they are a friend OR
// if we are not targeting enemies and they are an enemy OR.
// if we are ignoring their faction is explicitly ignored
// erase and continue
auto isEnemy = casterDestroyableComponent->IsEnemy(candidate);
if ((!targetFriend && !isEnemy) ||
(!targetEnemy && isEnemy) ||
CheckFactionList(ignoreFactionList, candidateFactions)) {
index = targets.erase(index);
continue;
}
index++;
}
return;
}
// some basic checks as well as the check that matters for this: if the quickbuild is complete
bool BehaviorContext::CheckTargetingRequirements(const Entity* target) const {
// if the target is a nullptr, then it's not valid
if (!target) return false;
// ignore quickbuilds that aren't completed
auto* targetQuickbuildComponent = target->GetComponent<RebuildComponent>();
if (targetQuickbuildComponent && targetQuickbuildComponent->GetState() != eRebuildState::COMPLETED) return false;
return true;
}
// returns true if any of the object factions are in the faction list
bool BehaviorContext::CheckFactionList(std::forward_list<int32_t>& factionList, std::vector<int32_t>& objectsFactions) const {
if (factionList.empty() || objectsFactions.empty()) return false;
for (auto faction : factionList){
if(std::find(objectsFactions.begin(), objectsFactions.end(), faction) != objectsFactions.end()) return true;
}
return false;
} }

View File

@ -6,6 +6,7 @@
#include "GameMessages.h" #include "GameMessages.h"
#include <vector> #include <vector>
#include <forward_list>
class Behavior; class Behavior;
@ -106,7 +107,11 @@ struct BehaviorContext
void Reset(); void Reset();
std::vector<LWOOBJID> GetValidTargets(int32_t ignoreFaction = 0, int32_t includeFaction = 0, const bool targetSelf = false, const bool targetEnemy = true, const bool targetFriend = false) const; void FilterTargets(std::vector<Entity*>& targetsReference, std::forward_list<int32_t>& ignoreFaction, std::forward_list<int32_t>& includeFaction, const bool targetSelf = false, const bool targetEnemy = true, const bool targetFriend = false, const bool targetTeam = false) const;
bool CheckTargetingRequirements(const Entity* target) const;
bool CheckFactionList(std::forward_list<int32_t>& factionList, std::vector<int32_t>& objectsFactions) const;
explicit BehaviorContext(LWOOBJID originator, bool calculation = false); explicit BehaviorContext(LWOOBJID originator, bool calculation = false);

View File

@ -2,9 +2,9 @@
#ifndef BEHAVIORSLOT_H #ifndef BEHAVIORSLOT_H
#define BEHAVIORSLOT_H #define BEHAVIORSLOT_H
#include <cstdint>
enum class BehaviorSlot enum class BehaviorSlot : int32_t {
{
Invalid = -1, Invalid = -1,
Primary, Primary,
Offhand, Offhand,

View File

@ -12,16 +12,24 @@
#include <vector> #include <vector>
void TacArcBehavior::Handle(BehaviorContext* context, RakNet::BitStream* bitStream, BehaviorBranchContext branch) { void TacArcBehavior::Handle(BehaviorContext* context, RakNet::BitStream* bitStream, BehaviorBranchContext branch) {
if (this->m_targetEnemy && this->m_usePickedTarget && branch.target > 0) { std::vector<Entity*> targets = {};
this->m_action->Handle(context, bitStream, branch);
return; if (this->m_usePickedTarget && branch.target != LWOOBJID_EMPTY) {
auto target = Game::entityManager->GetEntity(branch.target);
if (!target) Game::logger->Log("TacArcBehavior", "target %llu is null", branch.target);
else {
targets.push_back(target);
context->FilterTargets(targets, this->m_ignoreFactionList, this->m_includeFactionList, this->m_targetSelf, this->m_targetEnemy, this->m_targetFriend, this->m_targetTeam);
if (!targets.empty()) {
this->m_action->Handle(context, bitStream, branch);
return;
}
}
} }
bool hit = false; bool hasTargets = false;
if (!bitStream->Read(hasTargets)) {
if (!bitStream->Read(hit)) { Game::logger->Log("TacArcBehavior", "Unable to read hasTargets from bitStream, aborting Handle! %i", bitStream->GetNumberOfUnreadBits());
Game::logger->Log("TacArcBehavior", "Unable to read hit from bitStream, aborting Handle! %i", bitStream->GetNumberOfUnreadBits());
return; return;
}; };
@ -35,26 +43,23 @@ void TacArcBehavior::Handle(BehaviorContext* context, RakNet::BitStream* bitStre
if (blocked) { if (blocked) {
this->m_blockedAction->Handle(context, bitStream, branch); this->m_blockedAction->Handle(context, bitStream, branch);
return; return;
} }
} }
if (hit) { if (hasTargets) {
uint32_t count = 0; uint32_t count = 0;
if (!bitStream->Read(count)) { if (!bitStream->Read(count)) {
Game::logger->Log("TacArcBehavior", "Unable to read count from bitStream, aborting Handle! %i", bitStream->GetNumberOfUnreadBits()); Game::logger->Log("TacArcBehavior", "Unable to read count from bitStream, aborting Handle! %i", bitStream->GetNumberOfUnreadBits());
return; return;
}; };
if (count > m_maxTargets && m_maxTargets > 0) { if (count > m_maxTargets) {
count = m_maxTargets; Game::logger->Log("TacArcBehavior", "Bitstream has too many targets Max:%i Recv:%i", this->m_maxTargets, count);
return;
} }
std::vector<LWOOBJID> targets; for (auto i = 0u; i < count; i++) {
for (auto i = 0u; i < count; ++i) {
LWOOBJID id{}; LWOOBJID id{};
if (!bitStream->Read(id)) { if (!bitStream->Read(id)) {
@ -62,17 +67,19 @@ void TacArcBehavior::Handle(BehaviorContext* context, RakNet::BitStream* bitStre
return; return;
}; };
targets.push_back(id); if (id != LWOOBJID_EMPTY) {
auto* canidate = Game::entityManager->GetEntity(id);
if (canidate) targets.push_back(canidate);
} else {
Game::logger->Log("TacArcBehavior", "Bitstream has LWOOBJID_EMPTY as a target!");
}
} }
for (auto target : targets) { for (auto target : targets) {
branch.target = target; branch.target = target->GetObjectID();
this->m_action->Handle(context, bitStream, branch); this->m_action->Handle(context, bitStream, branch);
} }
} else { } else this->m_missAction->Handle(context, bitStream, branch);
this->m_missAction->Handle(context, bitStream, branch);
}
} }
void TacArcBehavior::Calculate(BehaviorContext* context, RakNet::BitStream* bitStream, BehaviorBranchContext branch) { void TacArcBehavior::Calculate(BehaviorContext* context, RakNet::BitStream* bitStream, BehaviorBranchContext branch) {
@ -82,23 +89,15 @@ void TacArcBehavior::Calculate(BehaviorContext* context, RakNet::BitStream* bitS
return; return;
} }
const auto* destroyableComponent = self->GetComponent<DestroyableComponent>(); std::vector<Entity*> targets = {};
if (this->m_usePickedTarget && branch.target != LWOOBJID_EMPTY) {
if ((this->m_usePickedTarget || context->clientInitalized) && branch.target > 0) { auto target = Game::entityManager->GetEntity(branch.target);
const auto* target = Game::entityManager->GetEntity(branch.target); targets.push_back(target);
context->FilterTargets(targets, this->m_ignoreFactionList, this->m_includeFactionList, this->m_targetSelf, this->m_targetEnemy, this->m_targetFriend, this->m_targetTeam);
if (target == nullptr) { if (!targets.empty()) {
this->m_action->Handle(context, bitStream, branch);
return; return;
} }
// If the game is specific about who to target, check that
if (destroyableComponent == nullptr || ((!m_targetFriend && !m_targetEnemy
|| m_targetFriend && destroyableComponent->IsFriend(target)
|| m_targetEnemy && destroyableComponent->IsEnemy(target)))) {
this->m_action->Calculate(context, bitStream, branch);
}
return;
} }
auto* combatAi = self->GetComponent<BaseCombatAIComponent>(); auto* combatAi = self->GetComponent<BaseCombatAIComponent>();
@ -107,50 +106,25 @@ void TacArcBehavior::Calculate(BehaviorContext* context, RakNet::BitStream* bitS
auto reference = self->GetPosition(); //+ m_offset; auto reference = self->GetPosition(); //+ m_offset;
std::vector<Entity*> targets; targets.clear();
std::vector<LWOOBJID> validTargets; std::vector<Entity*> validTargets = Game::entityManager->GetEntitiesByProximity(reference, this->m_maxRange);
if (combatAi != nullptr) { // filter all valid targets, based on whether we target enemies or friends
if (combatAi->GetTarget() != LWOOBJID_EMPTY) { context->FilterTargets(validTargets, this->m_ignoreFactionList, this->m_includeFactionList, this->m_targetSelf, this->m_targetEnemy, this->m_targetFriend, this->m_targetTeam);
validTargets.push_back(combatAi->GetTarget());
}
}
// Find all valid targets, based on whether we target enemies or friends
for (const auto& contextTarget : context->GetValidTargets()) {
if (destroyableComponent != nullptr) {
const auto* targetEntity = Game::entityManager->GetEntity(contextTarget);
if (m_targetEnemy && destroyableComponent->IsEnemy(targetEntity)
|| m_targetFriend && destroyableComponent->IsFriend(targetEntity)) {
validTargets.push_back(contextTarget);
}
} else {
validTargets.push_back(contextTarget);
}
}
for (auto validTarget : validTargets) { for (auto validTarget : validTargets) {
if (targets.size() >= this->m_maxTargets) { if (targets.size() >= this->m_maxTargets) {
break; break;
} }
auto* entity = Game::entityManager->GetEntity(validTarget); if (std::find(targets.begin(), targets.end(), validTarget) != targets.end()) {
if (entity == nullptr) {
Game::logger->Log("TacArcBehavior", "Invalid target (%llu) for (%llu)!", validTarget, context->originator);
continue; continue;
} }
if (std::find(targets.begin(), targets.end(), entity) != targets.end()) { if (validTarget->GetIsDead()) continue;
continue;
}
if (entity->GetIsDead()) continue; const auto otherPosition = validTarget->GetPosition();
const auto otherPosition = entity->GetPosition();
const auto heightDifference = std::abs(otherPosition.y - casterPosition.y); const auto heightDifference = std::abs(otherPosition.y - casterPosition.y);
@ -180,8 +154,8 @@ void TacArcBehavior::Calculate(BehaviorContext* context, RakNet::BitStream* bitS
const float degreeAngle = std::abs(Vector3::Angle(forward, normalized) * (180 / 3.14) - 180); const float degreeAngle = std::abs(Vector3::Angle(forward, normalized) * (180 / 3.14) - 180);
if (distance >= this->m_minDistance && this->m_maxDistance >= distance && degreeAngle <= 2 * this->m_angle) { if (distance >= this->m_minRange && this->m_maxRange >= distance && degreeAngle <= 2 * this->m_angle) {
targets.push_back(entity); targets.push_back(validTarget);
} }
} }
@ -228,43 +202,48 @@ void TacArcBehavior::Calculate(BehaviorContext* context, RakNet::BitStream* bitS
} }
void TacArcBehavior::Load() { void TacArcBehavior::Load() {
this->m_usePickedTarget = GetBoolean("use_picked_target"); this->m_maxRange = GetFloat("max range");
this->m_height = GetFloat("height", 2.2f);
this->m_distanceWeight = GetFloat("distance_weight", 0.0f);
this->m_angleWeight = GetFloat("angle_weight", 0.0f);
this->m_angle = GetFloat("angle", 45.0f);
this->m_minRange = GetFloat("min range", 0.0f);
this->m_offset = NiPoint3(
GetFloat("offset_x", 0.0f),
GetFloat("offset_y", 0.0f),
GetFloat("offset_z", 0.0f)
);
this->m_method = GetInt("method", 1);
this->m_upperBound = GetFloat("upper_bound", 4.4f);
this->m_lowerBound = GetFloat("lower_bound", 0.4f);
this->m_usePickedTarget = GetBoolean("use_picked_target", false);
this->m_useTargetPostion = GetBoolean("use_target_position", false);
this->m_checkEnv = GetBoolean("check_env", false);
this->m_useAttackPriority = GetBoolean("use_attack_priority", false);
this->m_action = GetAction("action"); this->m_action = GetAction("action");
this->m_missAction = GetAction("miss action"); this->m_missAction = GetAction("miss action");
this->m_checkEnv = GetBoolean("check_env");
this->m_blockedAction = GetAction("blocked action"); this->m_blockedAction = GetAction("blocked action");
this->m_minDistance = GetFloat("min range"); this->m_maxTargets = GetInt("max targets", 100);
if (this->m_maxTargets == 0) this->m_maxTargets = 100;
this->m_maxDistance = GetFloat("max range"); this->m_farHeight = GetFloat("far_height", 5.0f);
this->m_farWidth = GetFloat("far_width", 5.0f);
this->m_nearHeight = GetFloat("near_height", 5.0f);
this->m_nearWidth = GetFloat("near_width", 5.0f);
this->m_maxTargets = GetInt("max targets"); // params after this are needed for filter targets
const auto parameters = GetParameterNames();
this->m_targetEnemy = GetBoolean("target_enemy"); for (const auto& parameter : parameters) {
if (parameter.first.rfind("include_faction", 0) == 0) {
this->m_targetFriend = GetBoolean("target_friend"); this->m_includeFactionList.push_front(parameter.second);
} else if (parameter.first.rfind("ignore_faction", 0) == 0) {
this->m_targetTeam = GetBoolean("target_team"); this->m_ignoreFactionList.push_front(parameter.second);
}
this->m_angle = GetFloat("angle"); }
this->m_targetSelf = GetBoolean("target_caster", false);
this->m_upperBound = GetFloat("upper_bound"); this->m_targetEnemy = GetBoolean("target_enemy", false);
this->m_targetFriend = GetBoolean("target_friend", false);
this->m_lowerBound = GetFloat("lower_bound"); this->m_targetTeam = GetBoolean("target_team", false);
this->m_farHeight = GetFloat("far_height");
this->m_farWidth = GetFloat("far_width");
this->m_method = GetInt("method");
this->m_offset = {
GetFloat("offset_x"),
GetFloat("offset_y"),
GetFloat("offset_z")
};
} }

View File

@ -2,56 +2,42 @@
#include "Behavior.h" #include "Behavior.h"
#include "dCommonVars.h" #include "dCommonVars.h"
#include "NiPoint3.h" #include "NiPoint3.h"
#include <forward_list>
class TacArcBehavior final : public Behavior class TacArcBehavior final : public Behavior {
{
public: public:
bool m_usePickedTarget; explicit TacArcBehavior(const uint32_t behavior_id) : Behavior(behavior_id) {}
Behavior* m_action;
bool m_checkEnv;
Behavior* m_missAction;
Behavior* m_blockedAction;
float m_minDistance;
float m_maxDistance;
uint32_t m_maxTargets;
bool m_targetEnemy;
bool m_targetFriend;
bool m_targetTeam;
float m_angle;
float m_upperBound;
float m_lowerBound;
float m_farHeight;
float m_farWidth;
uint32_t m_method;
NiPoint3 m_offset;
/*
* Inherited
*/
explicit TacArcBehavior(const uint32_t behavior_id) : Behavior(behavior_id) {
}
void Handle(BehaviorContext* context, RakNet::BitStream* bitStream, BehaviorBranchContext branch) override; void Handle(BehaviorContext* context, RakNet::BitStream* bitStream, BehaviorBranchContext branch) override;
void Calculate(BehaviorContext* context, RakNet::BitStream* bitStream, BehaviorBranchContext branch) override; void Calculate(BehaviorContext* context, RakNet::BitStream* bitStream, BehaviorBranchContext branch) override;
void Load() override; void Load() override;
private:
float m_maxRange;
float m_height;
float m_distanceWeight;
float m_angleWeight;
float m_angle;
float m_minRange;
NiPoint3 m_offset;
uint32_t m_method;
float m_upperBound;
float m_lowerBound;
bool m_usePickedTarget;
bool m_useTargetPostion;
bool m_checkEnv;
bool m_useAttackPriority;
Behavior* m_action;
Behavior* m_missAction;
Behavior* m_blockedAction;
uint32_t m_maxTargets;
float m_farHeight;
float m_farWidth;
float m_nearHeight;
float m_nearWidth;
std::forward_list<int32_t> m_ignoreFactionList {};
std::forward_list<int32_t> m_includeFactionList {};
bool m_targetSelf;
bool m_targetEnemy;
bool m_targetFriend;
bool m_targetTeam;
}; };

View File

@ -363,9 +363,10 @@ void DestroyableComponent::SetIsShielded(bool value) {
void DestroyableComponent::AddFaction(const int32_t factionID, const bool ignoreChecks) { void DestroyableComponent::AddFaction(const int32_t factionID, const bool ignoreChecks) {
// Ignore factionID -1 // Ignore factionID -1
if (factionID == -1 && !ignoreChecks) { if (factionID == -1 && !ignoreChecks) return;
return;
} // if we already have that faction, don't add it again
if (std::find(m_FactionIDs.begin(), m_FactionIDs.end(), factionID) != m_FactionIDs.end()) return;
m_FactionIDs.push_back(factionID); m_FactionIDs.push_back(factionID);
m_DirtyHealth = true; m_DirtyHealth = true;
@ -407,6 +408,14 @@ void DestroyableComponent::AddFaction(const int32_t factionID, const bool ignore
} }
bool DestroyableComponent::IsEnemy(const Entity* other) const { bool DestroyableComponent::IsEnemy(const Entity* other) const {
if (m_Parent->IsPlayer() && other->IsPlayer()){
auto* thisCharacterComponent = m_Parent->GetComponent<CharacterComponent>();
if (!thisCharacterComponent) return false;
auto* otherCharacterComponent = other->GetComponent<CharacterComponent>();
if (!otherCharacterComponent) return false;
if (thisCharacterComponent->GetPvpEnabled() && otherCharacterComponent->GetPvpEnabled()) return true;
return false;
}
const auto* otherDestroyableComponent = other->GetComponent<DestroyableComponent>(); const auto* otherDestroyableComponent = other->GetComponent<DestroyableComponent>();
if (otherDestroyableComponent != nullptr) { if (otherDestroyableComponent != nullptr) {
for (const auto enemyFaction : m_EnemyFactionIDs) { for (const auto enemyFaction : m_EnemyFactionIDs) {
@ -485,43 +494,6 @@ Entity* DestroyableComponent::GetKiller() const {
return Game::entityManager->GetEntity(m_KillerID); return Game::entityManager->GetEntity(m_KillerID);
} }
bool DestroyableComponent::CheckValidity(const LWOOBJID target, const bool ignoreFactions, const bool targetEnemy, const bool targetFriend) const {
auto* targetEntity = Game::entityManager->GetEntity(target);
if (targetEntity == nullptr) {
Game::logger->Log("DestroyableComponent", "Invalid entity for checking validity (%llu)!", target);
return false;
}
auto* targetDestroyable = targetEntity->GetComponent<DestroyableComponent>();
if (targetDestroyable == nullptr) {
return false;
}
auto* targetQuickbuild = targetEntity->GetComponent<RebuildComponent>();
if (targetQuickbuild != nullptr) {
const auto state = targetQuickbuild->GetState();
if (state != eRebuildState::COMPLETED) {
return false;
}
}
if (ignoreFactions) {
return true;
}
// Get if the target entity is an enemy and friend
bool isEnemy = IsEnemy(targetEntity);
bool isFriend = IsFriend(targetEntity);
// Return true if the target type matches what we are targeting
return (isEnemy && targetEnemy) || (isFriend && targetFriend);
}
void DestroyableComponent::Heal(const uint32_t health) { void DestroyableComponent::Heal(const uint32_t health) {
auto current = static_cast<uint32_t>(GetHealth()); auto current = static_cast<uint32_t>(GetHealth());
const auto max = static_cast<uint32_t>(GetMaxHealth()); const auto max = static_cast<uint32_t>(GetMaxHealth());

View File

@ -371,14 +371,6 @@ public:
*/ */
Entity* GetKiller() const; Entity* GetKiller() const;
/**
* Checks if the target ID is a valid enemy of this entity
* @param target the target ID to check for
* @param ignoreFactions whether or not check for the factions, e.g. just return true if the entity cannot be smashed
* @return if the target ID is a valid enemy
*/
bool CheckValidity(LWOOBJID target, bool ignoreFactions = false, bool targetEnemy = true, bool targetFriend = false) const;
/** /**
* Attempt to damage this entity, handles everything from health and armor to absorption, immunity and callbacks. * Attempt to damage this entity, handles everything from health and armor to absorption, immunity and callbacks.
* @param damage the damage to attempt to apply * @param damage the damage to attempt to apply

View File

@ -1158,19 +1158,7 @@ void InventoryComponent::AddItemSkills(const LOT lot) {
const auto skill = FindSkill(lot); const auto skill = FindSkill(lot);
if (skill == 0) { SetSkill(slot, skill);
return;
}
if (index != m_Skills.end()) {
const auto old = index->second;
GameMessages::SendRemoveSkill(m_Parent, old);
}
GameMessages::SendAddSkill(m_Parent, skill, static_cast<int>(slot));
m_Skills.insert_or_assign(slot, skill);
} }
void InventoryComponent::RemoveItemSkills(const LOT lot) { void InventoryComponent::RemoveItemSkills(const LOT lot) {
@ -1197,7 +1185,7 @@ void InventoryComponent::RemoveItemSkills(const LOT lot) {
if (slot == BehaviorSlot::Primary) { if (slot == BehaviorSlot::Primary) {
m_Skills.insert_or_assign(BehaviorSlot::Primary, 1); m_Skills.insert_or_assign(BehaviorSlot::Primary, 1);
GameMessages::SendAddSkill(m_Parent, 1, static_cast<int>(BehaviorSlot::Primary)); GameMessages::SendAddSkill(m_Parent, 1, BehaviorSlot::Primary);
} }
} }
@ -1627,3 +1615,29 @@ void InventoryComponent::UpdatePetXml(tinyxml2::XMLDocument* document) {
petInventoryElement->LinkEndChild(petElement); petInventoryElement->LinkEndChild(petElement);
} }
} }
bool InventoryComponent::SetSkill(int32_t slot, uint32_t skillId){
BehaviorSlot behaviorSlot = BehaviorSlot::Invalid;
if (slot == 1 ) behaviorSlot = BehaviorSlot::Primary;
else if (slot == 2 ) behaviorSlot = BehaviorSlot::Offhand;
else if (slot == 3 ) behaviorSlot = BehaviorSlot::Neck;
else if (slot == 4 ) behaviorSlot = BehaviorSlot::Head;
else if (slot == 5 ) behaviorSlot = BehaviorSlot::Consumable;
else return false;
return SetSkill(behaviorSlot, skillId);
}
bool InventoryComponent::SetSkill(BehaviorSlot slot, uint32_t skillId){
if (skillId == 0) return false;
const auto index = m_Skills.find(slot);
if (index != m_Skills.end()) {
const auto old = index->second;
GameMessages::SendRemoveSkill(m_Parent, old);
}
GameMessages::SendAddSkill(m_Parent, skillId, slot);
m_Skills.insert_or_assign(slot, skillId);
return true;
}

View File

@ -367,6 +367,11 @@ public:
*/ */
void UnequipScripts(Item* unequippedItem); void UnequipScripts(Item* unequippedItem);
std::map<BehaviorSlot, uint32_t> GetSkills(){ return m_Skills; };
bool SetSkill(int slot, uint32_t skillId);
bool SetSkill(BehaviorSlot slot, uint32_t skillId);
~InventoryComponent() override; ~InventoryComponent() override;
private: private:

View File

@ -1159,7 +1159,7 @@ void GameMessages::SendPlayerReachedRespawnCheckpoint(Entity* entity, const NiPo
SEND_PACKET; SEND_PACKET;
} }
void GameMessages::SendAddSkill(Entity* entity, TSkillID skillID, int slotID) { void GameMessages::SendAddSkill(Entity* entity, TSkillID skillID, BehaviorSlot slotID) {
int AICombatWeight = 0; int AICombatWeight = 0;
bool bFromSkillSet = false; bool bFromSkillSet = false;
int castType = 0; int castType = 0;
@ -1189,8 +1189,8 @@ void GameMessages::SendAddSkill(Entity* entity, TSkillID skillID, int slotID) {
bitStream.Write(skillID); bitStream.Write(skillID);
bitStream.Write(slotID != -1); bitStream.Write(slotID != BehaviorSlot::Invalid);
if (slotID != -1) bitStream.Write(slotID); if (slotID != BehaviorSlot::Invalid) bitStream.Write(slotID);
bitStream.Write(temporary); bitStream.Write(temporary);

View File

@ -36,6 +36,7 @@ enum class ePetTamingNotifyType : uint32_t;
enum class eUseItemResponse : uint32_t; enum class eUseItemResponse : uint32_t;
enum class eQuickBuildFailReason : uint32_t; enum class eQuickBuildFailReason : uint32_t;
enum class eRebuildState : uint32_t; enum class eRebuildState : uint32_t;
enum class BehaviorSlot : int32_t;
namespace GameMessages { namespace GameMessages {
class PropertyDataMessage; class PropertyDataMessage;
@ -119,7 +120,7 @@ namespace GameMessages {
void SendSetPlayerControlScheme(Entity* entity, eControlScheme controlScheme); void SendSetPlayerControlScheme(Entity* entity, eControlScheme controlScheme);
void SendPlayerReachedRespawnCheckpoint(Entity* entity, const NiPoint3& position, const NiQuaternion& rotation); void SendPlayerReachedRespawnCheckpoint(Entity* entity, const NiPoint3& position, const NiQuaternion& rotation);
void SendAddSkill(Entity* entity, TSkillID skillID, int slotID); void SendAddSkill(Entity* entity, TSkillID skillID, BehaviorSlot slotID);
void SendRemoveSkill(Entity* entity, TSkillID skillID); void SendRemoveSkill(Entity* entity, TSkillID skillID);
void SendFinishArrangingWithItem(Entity* entity, const LWOOBJID& buildAreaID); void SendFinishArrangingWithItem(Entity* entity, const LWOOBJID& buildAreaID);

View File

@ -1841,6 +1841,86 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit
ChatPackets::SendSystemMessage(sysAddr, u"Deleted inventory " + GeneralUtils::UTF8ToUTF16(args[0])); ChatPackets::SendSystemMessage(sysAddr, u"Deleted inventory " + GeneralUtils::UTF8ToUTF16(args[0]));
} }
if (chatCommand == "castskill" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER && args.size() >= 1) {
auto* skillComponent = entity->GetComponent<SkillComponent>();
if (skillComponent){
uint32_t skillId;
if (!GeneralUtils::TryParse(args[0], skillId)) {
ChatPackets::SendSystemMessage(sysAddr, u"Error getting skill ID.");
return;
} else {
skillComponent->CastSkill(skillId, entity->GetObjectID(), entity->GetObjectID());
ChatPackets::SendSystemMessage(sysAddr, u"Cast skill");
}
}
}
if (chatCommand == "setskillslot" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER && args.size() >= 2) {
uint32_t skillId;
int slot;
auto* inventoryComponent = entity->GetComponent<InventoryComponent>();
if (inventoryComponent){
if (!GeneralUtils::TryParse(args[0], slot)) {
ChatPackets::SendSystemMessage(sysAddr, u"Error getting slot.");
return;
} else {
if (!GeneralUtils::TryParse(args[1], skillId)) {
ChatPackets::SendSystemMessage(sysAddr, u"Error getting skill.");
return;
} else {
if(inventoryComponent->SetSkill(slot, skillId)) ChatPackets::SendSystemMessage(sysAddr, u"Set skill to slot successfully");
else ChatPackets::SendSystemMessage(sysAddr, u"Set skill to slot failed");
}
}
}
}
if (chatCommand == "setfaction" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER && args.size() >= 1) {
auto* destroyableComponent = entity->GetComponent<DestroyableComponent>();
if (destroyableComponent){
int32_t faction;
if (!GeneralUtils::TryParse(args[0], faction)) {
ChatPackets::SendSystemMessage(sysAddr, u"Error getting faction.");
return;
} else {
destroyableComponent->SetFaction(faction);
ChatPackets::SendSystemMessage(sysAddr, u"Set faction and updated enemies list");
}
}
}
if (chatCommand == "addfaction" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER && args.size() >= 1) {
auto* destroyableComponent = entity->GetComponent<DestroyableComponent>();
if (destroyableComponent){
int32_t faction;
if (!GeneralUtils::TryParse(args[0], faction)) {
ChatPackets::SendSystemMessage(sysAddr, u"Error getting faction.");
return;
} else {
destroyableComponent->AddFaction(faction);
ChatPackets::SendSystemMessage(sysAddr, u"Added faction and updated enemies list");
}
}
}
if (chatCommand == "getfactions" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER) {
auto* destroyableComponent = entity->GetComponent<DestroyableComponent>();
if (destroyableComponent){
ChatPackets::SendSystemMessage(sysAddr, u"Friendly factions:");
for (const auto entry : destroyableComponent->GetFactionIDs()) {
ChatPackets::SendSystemMessage(sysAddr, (GeneralUtils::to_u16string(entry)));
}
ChatPackets::SendSystemMessage(sysAddr, u"Enemy factions:");
for (const auto entry : destroyableComponent->GetEnemyFactionsIDs()) {
ChatPackets::SendSystemMessage(sysAddr, (GeneralUtils::to_u16string(entry)));
}
}
}
if (chatCommand == "inspect" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER && args.size() >= 1) { if (chatCommand == "inspect" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER && args.size() >= 1) {
Entity* closest = nullptr; Entity* closest = nullptr;
@ -1980,6 +2060,14 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit
destuctable->SetFaction(-1); destuctable->SetFaction(-1);
destuctable->AddFaction(faction, true); destuctable->AddFaction(faction, true);
} }
} else if (args[1] == "-cf") {
auto* destuctable = entity->GetComponent<DestroyableComponent>();
if (!destuctable) {
ChatPackets::SendSystemMessage(sysAddr, u"No destroyable component on this entity!");
return;
}
if (destuctable->IsEnemy(closest)) ChatPackets::SendSystemMessage(sysAddr, u"They are our enemy");
else ChatPackets::SendSystemMessage(sysAddr, u"They are NOT our enemy");
} else if (args[1] == "-t") { } else if (args[1] == "-t") {
auto* phantomPhysicsComponent = closest->GetComponent<PhantomPhysicsComponent>(); auto* phantomPhysicsComponent = closest->GetComponent<PhantomPhysicsComponent>();

View File

@ -106,7 +106,11 @@ These commands are primarily for development and testing. The usage of many of t
|Set Level|`/setlevel <requested_level> (username)`|Sets the using entities level to the requested level. Takes an optional parameter of an in-game players username to set the level of.|8| |Set Level|`/setlevel <requested_level> (username)`|Sets the using entities level to the requested level. Takes an optional parameter of an in-game players username to set the level of.|8|
|crash|`/crash`|Crashes the server.|9| |crash|`/crash`|Crashes the server.|9|
|rollloot|`/rollloot <loot matrix index> <item id> <amount>`|Given a `loot matrix index`, look for `item id` in that matrix `amount` times and print to the chat box statistics of rolling that loot matrix.|9| |rollloot|`/rollloot <loot matrix index> <item id> <amount>`|Given a `loot matrix index`, look for `item id` in that matrix `amount` times and print to the chat box statistics of rolling that loot matrix.|9|
|castskill|`/castskill <skill id>`|Casts the skill as the player|9|
|setskillslot|`/setskillslot <slot> <skill id>`||8|
|setfaction|`/setfaction <faction id>`|Clears the users current factions and sets it|8|
|addfaction|`/addfaction <faction id>`|Add the faction to the users list of factions|8|
|getfactions|`/getfactions`|Shows the player's factions|8|
## Detailed `/inspect` Usage ## Detailed `/inspect` Usage
`/inspect <component> (-m <waypoint> | -a <animation> | -s | -p | -f (faction) | -t)` `/inspect <component> (-m <waypoint> | -a <animation> | -s | -p | -f (faction) | -t)`
@ -120,6 +124,7 @@ Finds the closest entity with the given component or LDF variable (ignoring play
* `-s`: Prints the entity's settings and spawner ID. * `-s`: Prints the entity's settings and spawner ID.
* `-p`: Prints the entity's position * `-p`: Prints the entity's position
* `-f`: If the entity has a destroyable component, prints whether the entity is smashable and its friendly and enemy faction IDs; if `faction` is specified, adds that faction to the entity. * `-f`: If the entity has a destroyable component, prints whether the entity is smashable and its friendly and enemy faction IDs; if `faction` is specified, adds that faction to the entity.
* `-cf`: check if the entity is enemy or friend
* `-t`: If the entity has a phantom physics component, prints the effect type, direction, directional multiplier, and whether the effect is active; in any case, if the entity has a trigger, prints the trigger ID. * `-t`: If the entity has a phantom physics component, prints the effect type, direction, directional multiplier, and whether the effect is active; in any case, if the entity has a trigger, prints the trigger ID.
## Game Master Levels ## Game Master Levels