Files
DarkflameServer/dScripts/02_server/Map/AM/AmShieldGeneratorQuickbuild.cpp
Aaron Kimbrell 2d76c81bba feat: server side knockback for AI moved objects
The client only simulates knockbacks on the character it controls
(LWOControllablePhysComponent::msgKnockback); enemies and NPCs are drawn
where the server puts them, so a knockback on them did nothing and
scripts stunned them instead.

MovementAIComponent::Knockback now flies the object the way the client
flies its character: a vector longer than 5 lifts it 0.5, throws it with
that velocity under WorldConfig gravity (times its gravity scale) until
it lands on the navmesh, no sooner than 250ms later; a shorter one moves
it by the vector. Walls taller than a step stop sideways motion, the
landing is put back onto the navmesh, and pathing and the combat AI wait
until it lands (destinations set mid air are walked to afterwards).
Position and velocity go out in the normal serialization every tick.

KnockbackBehavior builds the vector like the client's Cast (strength
capped at 300, angle as elevation, relative, caster and ignore_self) and
knocks back server moved targets that aren't immune, both when the
server casts and when a client's skill hits. The blocked bit it writes
now also answers for the target's knockback immunity, so a player whose
client hasn't caught up with a Personal Fortress isn't knocked out of it.

The AM shield generators knock enemies back like live instead of
stunning them.

Fixes #257
Refs #185

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:54 -05:00

204 lines
5.7 KiB
C++

#include "AmShieldGeneratorQuickbuild.h"
#include "EntityManager.h"
#include "DestroyableComponent.h"
#include "GameMessages.h"
#include "EffectsMessages.h"
#include "MovementAIComponent.h"
#include "BaseCombatAIComponent.h"
#include "SkillComponent.h"
#include "EntityInfo.h"
#include "QuickBuildComponent.h"
#include "MissionComponent.h"
void AmShieldGeneratorQuickbuild::OnStartup(Entity* self) {
self->SetProximityRadius(20, "shield");
self->SetProximityRadius(21, "buffer");
}
void AmShieldGeneratorQuickbuild::OnProximityUpdate(Entity* self, Entity* entering, std::string name, std::string status) {
auto* destroyableComponent = entering->GetComponent<DestroyableComponent>();
if (name == "shield") {
if (!destroyableComponent->HasFaction(4) || entering->IsPlayer()) {
return;
}
auto enemiesInProximity = self->GetVar<std::vector<LWOOBJID>>(u"Enemies");
if (status == "ENTER") {
EnemyEnteredShield(self, entering);
const auto& iter = std::find(enemiesInProximity.begin(), enemiesInProximity.end(), entering->GetObjectID());
if (iter == enemiesInProximity.end()) {
enemiesInProximity.push_back(entering->GetObjectID());
}
} else if (status == "LEAVE") {
const auto& iter = std::find(enemiesInProximity.begin(), enemiesInProximity.end(), entering->GetObjectID());
if (iter != enemiesInProximity.end()) {
enemiesInProximity.erase(iter);
}
}
self->SetVar<std::vector<LWOOBJID>>(u"Enemies", enemiesInProximity);
}
if (name != "buffer" || !entering->IsPlayer()) {
return;
}
auto entitiesInProximity = self->GetVar<std::vector<LWOOBJID>>(u"Players");
if (status == "ENTER") {
const auto& iter = std::find(entitiesInProximity.begin(), entitiesInProximity.end(), entering->GetObjectID());
if (iter == entitiesInProximity.end()) {
entitiesInProximity.push_back(entering->GetObjectID());
}
} else if (status == "LEAVE") {
const auto& iter = std::find(entitiesInProximity.begin(), entitiesInProximity.end(), entering->GetObjectID());
if (iter != entitiesInProximity.end()) {
entitiesInProximity.erase(iter);
}
}
self->SetVar<std::vector<LWOOBJID>>(u"Players", entitiesInProximity);
}
void AmShieldGeneratorQuickbuild::OnDie(Entity* self, Entity* killer) {
self->CancelAllTimers();
auto* child = Game::entityManager->GetEntity(self->GetVar<LWOOBJID>(u"Child"));
if (child != nullptr) {
child->Kill();
}
}
void AmShieldGeneratorQuickbuild::OnTimerDone(Entity* self, std::string timerName) {
if (timerName == "BuffPlayers") {
BuffPlayers(self);
self->AddTimer("BuffPlayers", 3.0f);
} else if (timerName == "PlayFX") {
GameMessages::PlayFXEffect(self->GetObjectID(), 5351, u"generatorOn", "generatorOn").Send(UNASSIGNED_SYSTEM_ADDRESS);
self->AddTimer("PlayFX", 1.5f);
} else if (timerName == "RefreshEnemies") {
auto enemiesInProximity = self->GetVar<std::vector<LWOOBJID>>(u"Enemies");
for (const auto enemyID : enemiesInProximity) {
auto* enemy = Game::entityManager->GetEntity(enemyID);
if (enemy != nullptr) {
EnemyEnteredShield(self, enemy);
}
}
self->AddTimer("RefreshEnemies", 1.5f);
}
}
void AmShieldGeneratorQuickbuild::OnQuickBuildComplete(Entity* self, Entity* target) {
StartShield(self);
auto enemiesInProximity = self->GetVar<std::vector<LWOOBJID>>(u"Enemies");
for (const auto enemyID : enemiesInProximity) {
auto* enemy = Game::entityManager->GetEntity(enemyID);
if (enemy != nullptr) {
enemy->Smash();
}
}
auto entitiesInProximity = self->GetVar<std::vector<LWOOBJID>>(u"Players");
for (const auto playerID : entitiesInProximity) {
auto* player = Game::entityManager->GetEntity(playerID);
if (player == nullptr) {
continue;
}
auto* missionComponent = player->GetComponent<MissionComponent>();
if (missionComponent == nullptr) {
return;
}
missionComponent->ForceProgressTaskType(987, 1, 1, false);
}
}
void AmShieldGeneratorQuickbuild::StartShield(Entity* self) {
self->AddTimer("PlayFX", 1.5f);
self->AddTimer("BuffPlayers", 3.0f);
self->AddTimer("RefreshEnemies", 1.5f);
const auto myPos = self->GetPosition();
const auto myRot = self->GetRotation();
EntityInfo info{};
info.lot = 13111;
info.pos = myPos;
info.rot = myRot;
info.spawnerID = self->GetObjectID();
auto* child = Game::entityManager->CreateEntity(info);
self->SetVar(u"Child", child->GetObjectID());
BuffPlayers(self);
}
void AmShieldGeneratorQuickbuild::BuffPlayers(Entity* self) {
auto* skillComponent = self->GetComponent<SkillComponent>();
if (skillComponent == nullptr) {
return;
}
auto entitiesInProximity = self->GetVar<std::vector<LWOOBJID>>(u"Players");
for (const auto playerID : entitiesInProximity) {
auto* player = Game::entityManager->GetEntity(playerID);
if (player == nullptr) {
return;
}
skillComponent->CalculateBehavior(1200, 27024, playerID, true);
}
}
void AmShieldGeneratorQuickbuild::EnemyEnteredShield(Entity* self, Entity* intruder) {
auto* quickBuildComponent = self->GetComponent<QuickBuildComponent>();
if (quickBuildComponent == nullptr || quickBuildComponent->GetState() != eQuickBuildState::COMPLETED) {
return;
}
auto* baseCombatAIComponent = intruder->GetComponent<BaseCombatAIComponent>();
auto* movementAIComponent = intruder->GetComponent<MovementAIComponent>();
if (baseCombatAIComponent == nullptr || movementAIComponent == nullptr) {
return;
}
// Knock the enemy back the way it came
auto dir = QuatUtils::Forward(intruder->GetRotation()) * -1;
dir.y += 15;
dir.x *= 50;
dir.z *= 50;
if (NiPoint3::DistanceSquared(self->GetPosition(), intruder->GetPosition()) < 20 * 20) {
movementAIComponent->Knockback(dir);
}
// Forget the players so it doesn't just run into the shield again
baseCombatAIComponent->ClearThreat();
}