fix(shooting gallery): a target at the end of its path respawns and despawns, without a score

SG_TARGET.lua, the targets' server script: at the last waypoint of its
path (onArrived with isLastPoint, which the client's PushToLua
0x00b1a910 sets for a moving platform's Arrived as for a waypoint
follower's) a target notifies the cannon, FinishedPath, and sg_cannon.lua
spawns the next one as after a hit (checkSpawn). A target set to despawn
there then dies silently after 1 s, a moving platform after 2 s.

Here ships and subs sat at the end of their path until the game ended,
and nothing came after them. The script cannot load (the targets have a
script row of their own), so the cannon does it:
- the respawn is split out of RegisterHit (CheckSpawn), and the die
  callback only registers a hit when the death is not the silent one;
- a moving platform target tells through PlatformAtLastWaypoint, a
  waypoint follower through a new MovementAIComponent waypoint callback.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-11-10 16:50:32 -06:00
parent 22c3168587
commit 8a238ab1d0
4 changed files with 69 additions and 8 deletions

View File

@@ -541,6 +541,9 @@ void MovementAIComponent::SetMaxSpeed(const float value) {
void MovementAIComponent::RunWaypointCommands(uint32_t waypointNum) {
m_Parent->GetScript()->OnWaypointReached(m_Parent, waypointNum);
// A callback may add another
const auto callbacks = m_WaypointReachedCallbacks;
for (const auto& callback : callbacks) callback(waypointNum);
if (!m_Path || waypointNum >= m_Path->pathWaypoints.size()) return;
const auto& commands = m_Path->pathWaypoints[waypointNum].commands;

View File

@@ -16,6 +16,7 @@
#include "eReplicaComponentType.h"
#include "Zone.h"
#include "dpKnockback.h"
#include <functional>
#include <optional>
#include <vector>
@@ -188,6 +189,12 @@ public:
*/
bool AtFinalWaypoint() const { return m_AtFinalWaypoint; }
/**
* Calls back at each waypoint of the path the entity follows, with the waypoint's number (what the Arrived game
* message tells an object's script)
*/
void AddWaypointReachedCallback(const std::function<void(uint32_t)>& callback) { m_WaypointReachedCallbacks.push_back(callback); }
/**
* Renders the entity stationary
*/
@@ -380,6 +387,8 @@ private:
// The number of waypoints that were on the path in the call to SetPath
uint32_t m_CurrentPathWaypointCount{ 0 };
std::vector<std::function<void(uint32_t)>> m_WaypointReachedCallbacks;
LWOOBJID m_FollowedTarget{ LWOOBJID_EMPTY };
/**

View File

@@ -287,7 +287,7 @@ void SGCannon::DoSpawnTimerFunc(Entity* self, const std::string& name) {
info.pos = path->pathWaypoints[0].position;
info.settings.Insert<SGEnemy>(u"SpawnData", toSpawn);
info.settings.Insert<std::string>(u"custom_script_server", "scripts/ai/ACT/SG_TARGET.lua"); // this script is never loaded;
info.settings.Insert<std::string>(u"custom_script_server", "scripts/ai/ACT/SG_TARGET.lua"); // never loaded (the targets have a script row): TargetFinishedPath does its part
info.settings.Insert<std::string>(u"custom_script_client", "scripts/client/ai/SG_TARGET_CLIENT.lua");
info.settings.Insert<std::string>(u"attached_path", path->pathName);
info.settings.Insert<uint32_t>(u"attached_path_start", 0);
@@ -322,10 +322,27 @@ void SGCannon::DoSpawnTimerFunc(Entity* self, const std::string& name) {
movementAI->SetPath(path->pathWaypoints);
}
enemy->AddDieCallback([this, self, enemy, name]() {
RegisterHit(self, enemy, name);
// A hit target scores (live's cannon ball tells the cannon of its kills). One that got to the end of its path
// tells the cannon and dies silently a moment later (SG_TARGET.lua), which is no hit.
const auto silentDeath = std::make_shared<bool>(false);
enemy->AddDieCallback([this, self, enemy, name, silentDeath]() {
if (!*silentDeath) RegisterHit(self, enemy, name);
});
// SG_TARGET.lua (onArrived): the Arrived message says whether the waypoint is the path's last one, for a moving
// platform as for an object following waypoints (the script reads isLastPoint, which the client's PushToLua
// 0x00b1a910 sets from the path and the waypoint)
const auto lastWaypoint = static_cast<uint32_t>(path->pathWaypoints.size() - 1);
if (movingPlatform) {
movingPlatform->AddPlatformAtLastWaypointCallback([this, self, enemy, name, silentDeath]() {
TargetFinishedPath(self, enemy, name, silentDeath);
});
} else if (movementAI) {
movementAI->AddWaypointReachedCallback([this, self, enemy, name, silentDeath, lastWaypoint](const uint32_t waypoint) {
if (waypoint == lastWaypoint) TargetFinishedPath(self, enemy, name, silentDeath);
});
}
// Save the enemy
if (enemy != nullptr) {
const_cast<std::vector<LWOOBJID>&>(self->GetVar<std::vector<LWOOBJID>>(SpawnedObjects)).push_back(enemy->GetObjectID());
@@ -684,6 +701,37 @@ void SGCannon::OnActivityNotify(Entity* self, GameMessages::ActivityNotify& noti
self->SetVar<std::vector<LWOOBJID>>(u"CannonBallKills", {});
}
void SGCannon::CheckSpawn(Entity* self, Entity* target, const std::string& timerName) {
if (!self->GetVar<bool>(GameStartedVariable)) return;
const auto& spawnInfo = target->GetVar<SGEnemy>(u"SpawnData");
if (spawnInfo.respawns && target->GetVar<uint32_t>(u"wave") == self->GetVar<uint32_t>(ThisWaveVariable)) {
const auto respawnTime = GeneralUtils::GenerateRandomNumber<float_t>(spawnInfo.minRespawnTime, spawnInfo.maxRespawnTime);
ActivityTimerStart(self, timerName, respawnTime, respawnTime);
}
}
// SG_TARGET.lua: at the last waypoint of its path a target notifies the cannon (FinishedPath: sg_cannon.lua spawns the
// next one as after a hit, without a score) and, when it is to despawn there, dies silently 1 s later, a moving
// platform after 2 s (so the clients' platforms get to the end of the path first).
void SGCannon::TargetFinishedPath(Entity* self, Entity* target, const std::string& timerName, const std::shared_ptr<bool>& silentDeath) {
// Once: a platform sent on would tell again
if (target->GetVar<bool>(u"FinishedPath")) return;
target->SetVar<bool>(u"FinishedPath", true);
CheckSpawn(self, target, timerName);
const auto& spawnInfo = target->GetVar<SGEnemy>(u"SpawnData");
if (!spawnInfo.despawnOnLastWaypoint) return;
target->AddCallbackTimer(spawnInfo.isMovingPlatform ? 2.0f : 1.0f, [target, silentDeath]() {
// Hit in the meantime
if (target->GetIsDead()) return;
*silentDeath = true;
target->Smash(target->GetObjectID(), eKillType::SILENT);
});
}
void SGCannon::RegisterHit(Entity* self, Entity* target, const std::string& timerName) {
if (!self->GetVar<bool>(GameStartedVariable)) return;
@@ -692,11 +740,7 @@ void SGCannon::RegisterHit(Entity* self, Entity* target, const std::string& time
self->SetVar<std::vector<LWOOBJID>>(u"CannonBallKills", cannonBallKills);
const auto& spawnInfo = target->GetVar<SGEnemy>(u"SpawnData");
if (spawnInfo.respawns && target->GetVar<uint32_t>(u"wave") == self->GetVar<uint32_t>(ThisWaveVariable)) {
const auto respawnTime = GeneralUtils::GenerateRandomNumber<float_t>(spawnInfo.minRespawnTime, spawnInfo.maxRespawnTime);
ActivityTimerStart(self, timerName, respawnTime, respawnTime);
}
CheckSpawn(self, target, timerName);
int32_t score = spawnInfo.score;

View File

@@ -1,4 +1,5 @@
#pragma once
#include <memory>
#include <regex>
#include "ActivityManager.h"
@@ -89,6 +90,10 @@ private:
void PauseChargeCannon(Entity* self);
void StopGame(Entity* self, bool cancel = false);
void RegisterHit(Entity* self, Entity* target, const std::string& timerName);
// sg_cannon.lua (checkSpawn): a target that respawns gets its next spawn, while its wave runs
void CheckSpawn(Entity* self, Entity* target, const std::string& timerName);
// SG_TARGET.lua (onArrived at the last waypoint): the cannon spawns the next one, and the target is gone a moment later
void TargetFinishedPath(Entity* self, Entity* target, const std::string& timerName, const std::shared_ptr<bool>& silentDeath);
void UpdateStreak(Entity* self);
float_t GetCurrentBonus(Entity* self);