feat(Core/Scripts): Add gameobject_summon_groups with quaternion rotation support (#24708)

Co-authored-by: blinkysc <blinkysc@users.noreply.github.com>
This commit is contained in:
blinkysc
2026-02-15 15:05:00 -06:00
committed by GitHub
parent dcafad1a41
commit ce74c0b19c
12 changed files with 342 additions and 1 deletions

View File

@@ -0,0 +1,49 @@
-- DB update 2026_02_13_00 >> 2026_02_13_01
-- Add gameobject_summon_groups table
DROP TABLE IF EXISTS `gameobject_summon_groups`;
CREATE TABLE IF NOT EXISTS `gameobject_summon_groups` (
`summonerId` int unsigned NOT NULL DEFAULT '0',
`summonerType` tinyint unsigned NOT NULL DEFAULT '0',
`groupId` tinyint unsigned NOT NULL DEFAULT '0',
`entry` int unsigned NOT NULL DEFAULT '0',
`position_x` float NOT NULL DEFAULT '0',
`position_y` float NOT NULL DEFAULT '0',
`position_z` float NOT NULL DEFAULT '0',
`orientation` float NOT NULL DEFAULT '0',
`rotation0` float NOT NULL DEFAULT '0',
`rotation1` float NOT NULL DEFAULT '0',
`rotation2` float NOT NULL DEFAULT '0',
`rotation3` float NOT NULL DEFAULT '0',
`respawnTime` int unsigned NOT NULL DEFAULT '120',
`Comment` varchar(255) CHARACTER SET `utf8mb4` COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT ''
) ENGINE=InnoDB DEFAULT CHARSET=`utf8mb4` COLLATE=`utf8mb4_unicode_ci`;
-- Quest 619 "Enticing Negolash" - Gameobject spawns on quest turn-in at Ruined Lifeboat (GO 2289)
-- Sniff data: Barbequed Buzzard Wings (2332), Stranglevine Wine (2333), Baked Bread (2562)
DELETE FROM `gameobject_summon_groups` WHERE `summonerId` = 2289 AND `summonerType` = 1;
INSERT INTO `gameobject_summon_groups` (`summonerId`, `summonerType`, `groupId`, `entry`, `position_x`, `position_y`, `position_z`, `orientation`, `rotation0`, `rotation1`, `rotation2`, `rotation3`, `respawnTime`, `Comment`) VALUES
(2289, 1, 0, 2332, -14652.3798828125, 146.5117950439453125, 3.501179933547973632, 0.349065244197845458, 0, 0, 0.173647880554199218, 0.984807789325714111, 120, 'Enticing Negolash - Barbequed Buzzard Wings'),
(2289, 1, 0, 2332, -14652.9423828125, 146.867401123046875, 2.506906986236572265, 2.949595451354980468, 0, 0, 0.995395660400390625, 0.095851235091686248, 120, 'Enticing Negolash - Barbequed Buzzard Wings'),
(2289, 1, 0, 2332, -14653.017578125, 146.524169921875, 2.355585098266601562, 6.003933906555175781, 0, 0, -0.13917255401611328, 0.990268170833587646, 120, 'Enticing Negolash - Barbequed Buzzard Wings'),
(2289, 1, 0, 2332, -14653.044921875, 145.137908935546875, 2.653269052505493164, 5.84685373306274414, 0, 0, -0.21643924713134765, 0.976296067237854003, 120, 'Enticing Negolash - Barbequed Buzzard Wings'),
(2289, 1, 0, 2332, -14654.0361328125, 147.3063201904296875, 2.467313051223754882, 0.942476630210876464, 0, 0, 0.453989982604980468, 0.891006767749786376, 120, 'Enticing Negolash - Barbequed Buzzard Wings'),
(2289, 1, 0, 2332, -14654.45703125, 147.324005126953125, 2.456363916397094726, 1.815141916275024414, 0, 0, 0.788010597229003906, 0.615661680698394775, 120, 'Enticing Negolash - Barbequed Buzzard Wings'),
(2289, 1, 0, 2332, -14654.703125, 146.1419219970703125, 2.089060068130493164, 2.373644113540649414, 0, 0, 0.927183151245117187, 0.37460830807685852, 120, 'Enticing Negolash - Barbequed Buzzard Wings'),
(2289, 1, 0, 2332, -14654.87890625, 147.2779693603515625, 2.425240993499755859, 5.881760597229003906, 0, 0, -0.19936752319335937, 0.979924798011779785, 120, 'Enticing Negolash - Barbequed Buzzard Wings'),
(2289, 1, 0, 2332, -14655.1357421875, 146.6710968017578125, 2.230948925018310546, 2.652894020080566406, 0, 0, 0.970294952392578125, 0.241925001144409179, 120, 'Enticing Negolash - Barbequed Buzzard Wings'),
(2289, 1, 0, 2332, -14655.2763671875, 147.80206298828125, 2.639719009399414062, 6.161012649536132812, 0, 0, -0.06104850769042968, 0.998134791851043701, 120, 'Enticing Negolash - Barbequed Buzzard Wings'),
(2289, 1, 0, 2332, -14656.1884765625, 147.0958404541015625, 2.387770891189575195, 0.174532130360603332, 0, 0, 0.087155342102050781, 0.996194720268249511, 120, 'Enticing Negolash - Barbequed Buzzard Wings'),
(2289, 1, 0, 2332, -14656.8330078125, 148.8932647705078125, 3.288661956787109375, 5.096362113952636718, 0, 0, -0.55919265747070312, 0.829037725925445556, 120, 'Enticing Negolash - Barbequed Buzzard Wings'),
(2289, 1, 0, 2333, -14653.044921875, 145.3892364501953125, 2.85199904441833496, 4.852017402648925781, 0, 0, -0.65605831146240234, 0.754710197448730468, 120, 'Enticing Negolash - Stranglevine Wine'),
(2289, 1, 0, 2333, -14655.728515625, 148.9776458740234375, 4.056398868560791015, 3.473210096359252929, 0, 0, -0.98628520965576171, 0.165049895644187927, 120, 'Enticing Negolash - Stranglevine Wine'),
(2289, 1, 0, 2333, -14656.4482421875, 147.5946807861328125, 3.129076004028320312, 1.588248729705810546, 0, 0, 0.713250160217285156, 0.700909554958343505, 120, 'Enticing Negolash - Stranglevine Wine'),
(2289, 1, 0, 2333, -14656.8408203125, 147.4337310791015625, 3.102070093154907226, 3.019413232803344726, 0, 0, 0.998134613037109375, 0.061051756143569946, 120, 'Enticing Negolash - Stranglevine Wine'),
(2289, 1, 0, 2333, -14657.1513671875, 148.227569580078125, 2.886320114135742187, 1.535889506340026855, 0, 0, 0.694658279418945312, 0.719339847564697265, 120, 'Enticing Negolash - Stranglevine Wine'),
(2289, 1, 0, 2562, -14652.4326171875, 145.7533721923828125, 3.254641056060791015, 2.932138919830322265, 0, 0, 0.994521141052246093, 0.104535527527332305, 120, 'Enticing Negolash - Baked Bread'),
(2289, 1, 0, 2562, -14653.8486328125, 146.2042694091796875, 2.14631199836730957, 4.886923789978027343, 0, 0, -0.64278697967529296, 0.766044974327087402, 120, 'Enticing Negolash - Baked Bread'),
(2289, 1, 0, 2562, -14656.138671875, 148.3673248291015625, 3.515635967254638671, 5.637413978576660156, 0, 0, -0.31730461120605468, 0.948323667049407958, 120, 'Enticing Negolash - Baked Bread');
-- SmartAI: Ruined Lifeboat (GO 2289) - On Quest 619 Reward - Summon Gameobject Group 0
DELETE FROM `smart_scripts` WHERE `entryorguid` = 2289 AND `source_type` = 1 AND `id` = 1;
INSERT INTO `smart_scripts` (`entryorguid`, `source_type`, `id`, `link`, `event_type`, `event_phase_mask`, `event_chance`, `event_flags`, `event_param1`, `event_param2`, `event_param3`, `event_param4`, `event_param5`, `event_param6`, `action_type`, `action_param1`, `action_param2`, `action_param3`, `action_param4`, `action_param5`, `action_param6`, `target_type`, `target_param1`, `target_param2`, `target_param3`, `target_param4`, `target_x`, `target_y`, `target_z`, `target_o`, `comment`) VALUES
(2289, 1, 1, 0, 20, 0, 100, 0, 619, 0, 0, 0, 0, 0, 241, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Ruined Lifeboat - On Quest ''Enticing Negolash'' Rewarded - Summon Gameobject Group 0');

View File

@@ -3332,6 +3332,14 @@ void SmartScript::ProcessAction(SmartScriptHolder& e, Unit* unit, uint32 var0, u
}
break;
}
case SMART_ACTION_SUMMON_GAMEOBJECT_GROUP:
{
if (!GetBaseObject())
break;
GetBaseObject()->SummonGameObjectGroup(e.action.gameobjectGroup.group);
break;
}
default:
LOG_ERROR("sql.sql", "SmartScript::ProcessAction: Entry {} SourceType {}, Event {}, Unhandled Action type {}", e.entryOrGuid, e.GetScriptType(), e.event_id, e.GetActionType());
break;

View File

@@ -886,6 +886,7 @@ bool SmartAIMgr::CheckUnusedActionParams(SmartScriptHolder const& e)
case SMART_ACTION_DISABLE_REWARD: return sizeof(SmartAction::reward);
case SMART_ACTION_SET_ANIM_TIER: return sizeof(SmartAction::animTier);
case SMART_ACTION_SET_GOSSIP_MENU: return sizeof(SmartAction::setGossipMenu);
case SMART_ACTION_SUMMON_GAMEOBJECT_GROUP: return sizeof(SmartAction::gameobjectGroup);
case SMART_ACTION_DISMOUNT: return NO_PARAMS;
default:
LOG_WARN("sql.sql", "SmartAIMgr: entryorguid {} source_type {} id {} action_type {} is using an action with no unused params specified in SmartAIMgr::CheckUnusedActionParams(), please report this.",
@@ -2042,6 +2043,7 @@ bool SmartAIMgr::IsEventValid(SmartScriptHolder& e)
case SMART_ACTION_MOVEMENT_RESUME:
case SMART_ACTION_WORLD_SCRIPT:
case SMART_ACTION_SET_GOSSIP_MENU:
case SMART_ACTION_SUMMON_GAMEOBJECT_GROUP:
break;
default:
LOG_ERROR("sql.sql", "SmartAIMgr: Not handled action_type({}), event_type({}), Entry {} SourceType {} Event {}, skipped.", e.GetActionType(), e.GetEventType(), e.entryOrGuid, e.GetScriptType(), e.event_id);

View File

@@ -725,8 +725,9 @@ enum SMART_ACTION
SMART_ACTION_DISABLE_REWARD = 238, // reputation 0/1, loot 0/1
SMART_ACTION_SET_ANIM_TIER = 239, // animtier
SMART_ACTION_SET_GOSSIP_MENU = 240, // gossipMenuId
SMART_ACTION_SUMMON_GAMEOBJECT_GROUP = 241, // group
SMART_ACTION_AC_END = 241, // placeholder
SMART_ACTION_AC_END = 242, // placeholder
};
enum class SmartActionSummonCreatureFlags
@@ -1517,6 +1518,11 @@ struct SmartAction
{
uint32 gossipMenuId;
} setGossipMenu;
struct
{
uint32 group;
} gameobjectGroup;
};
};

View File

@@ -19,6 +19,7 @@
#define AZEROTHCORE_TEMPSUMMON_H
#include "Creature.h"
#include "G3D/Quat.h"
enum SummonerType
{
@@ -36,6 +37,15 @@ struct TempSummonData
uint32 time; ///< Despawn time, usable only with certain temp summon types
};
/// Stores data for temp gameobject summons
struct GameObjectSummonData
{
uint32 entry;
Position pos;
G3D::Quat rot;
uint32 respawnTime; ///< Duration in seconds; passed to SummonGameObject's respawnTime parameter
};
class TempSummon : public Creature
{
public:

View File

@@ -2284,6 +2284,18 @@ void Map::SummonCreatureGroup(uint8 group, std::list<TempSummon*>* list /*= null
list->push_back(summon);
}
void Map::SummonGameObjectGroup(uint8 group, std::list<GameObject*>* list /*= nullptr*/)
{
std::vector<GameObjectSummonData> const* data = sObjectMgr->GetGameObjectSummonGroup(GetId(), SUMMONER_TYPE_MAP, group);
if (!data)
return;
for (std::vector<GameObjectSummonData>::const_iterator itr = data->begin(); itr != data->end(); ++itr)
if (GameObject* go = SummonGameObject(itr->entry, itr->pos.GetPositionX(), itr->pos.GetPositionY(), itr->pos.GetPositionZ(), itr->pos.GetOrientation(), itr->rot.x, itr->rot.y, itr->rot.z, itr->rot.w, itr->respawnTime))
if (list)
list->push_back(go);
}
TempSummon* WorldObject::SummonCreature(uint32 id, float x, float y, float z, float ang, TempSummonType spwtype, uint32 despwtime, SummonPropertiesEntry const* properties, bool visibleBySummonerOnly)
{
if (!x && !y && !z)
@@ -2440,6 +2452,20 @@ void WorldObject::SummonCreatureGroup(uint8 group, std::list<TempSummon*>* list
list->push_back(summon);
}
void WorldObject::SummonGameObjectGroup(uint8 group, std::list<GameObject*>* list /*= nullptr*/)
{
ASSERT((IsGameObject() || IsCreature()) && "Only GOs and creatures can summon gameobject groups!");
std::vector<GameObjectSummonData> const* data = sObjectMgr->GetGameObjectSummonGroup(GetEntry(), IsGameObject() ? SUMMONER_TYPE_GAMEOBJECT : SUMMONER_TYPE_CREATURE, group);
if (!data)
return;
for (std::vector<GameObjectSummonData>::const_iterator itr = data->begin(); itr != data->end(); ++itr)
if (GameObject* go = SummonGameObject(itr->entry, itr->pos.GetPositionX(), itr->pos.GetPositionY(), itr->pos.GetPositionZ(), itr->pos.GetOrientation(), itr->rot.x, itr->rot.y, itr->rot.z, itr->rot.w, itr->respawnTime))
if (list)
list->push_back(go);
}
Creature* WorldObject::FindNearestCreature(uint32 entry, float range, bool alive) const
{
Creature* creature = nullptr;

View File

@@ -630,6 +630,7 @@ public:
GameObject* SummonGameObject(uint32 entry, float x, float y, float z, float ang, float rotation0, float rotation1, float rotation2, float rotation3, uint32 respawnTime, bool checkTransport = true, GOSummonType summonType = GO_SUMMON_TIMED_OR_CORPSE_DESPAWN);
Creature* SummonTrigger(float x, float y, float z, float ang, uint32 dur, bool setLevel = false, CreatureAI * (*GetAI)(Creature*) = nullptr);
void SummonCreatureGroup(uint8 group, std::list<TempSummon*>* list = nullptr);
void SummonGameObjectGroup(uint8 group, std::list<GameObject*>* list = nullptr);
[[nodiscard]] Creature* FindNearestCreature(uint32 entry, float range, bool alive = true) const;
[[nodiscard]] GameObject* FindNearestGameObject(uint32 entry, float range, bool onlySpawned = false) const;

View File

@@ -2224,6 +2224,87 @@ void ObjectMgr::LoadTempSummons()
LOG_INFO("server.loading", " ");
}
void ObjectMgr::LoadGameObjectSummons()
{
uint32 oldMSTime = getMSTime();
_goSummonDataStore.clear();
// 0 1 2 3 4 5 6 7 8 9 10 11 12
QueryResult result = WorldDatabase.Query("SELECT summonerId, summonerType, groupId, entry, position_x, position_y, position_z, orientation, rotation0, rotation1, rotation2, rotation3, respawnTime FROM gameobject_summon_groups");
if (!result)
{
LOG_WARN("server.loading", ">> Loaded 0 gameobject summons. DB table `gameobject_summon_groups` is empty.");
return;
}
uint32 count = 0;
do
{
Field* fields = result->Fetch();
uint32 summonerId = fields[0].Get<uint32>();
SummonerType summonerType = SummonerType(fields[1].Get<uint8>());
uint8 group = fields[2].Get<uint8>();
switch (summonerType)
{
case SUMMONER_TYPE_CREATURE:
if (!GetCreatureTemplate(summonerId))
{
LOG_ERROR("sql.sql", "Table `gameobject_summon_groups` has summoner with non existing entry {} for creature summoner type, skipped.", summonerId);
continue;
}
break;
case SUMMONER_TYPE_GAMEOBJECT:
if (!GetGameObjectTemplate(summonerId))
{
LOG_ERROR("sql.sql", "Table `gameobject_summon_groups` has summoner with non existing entry {} for gameobject summoner type, skipped.", summonerId);
continue;
}
break;
case SUMMONER_TYPE_MAP:
if (!sMapStore.LookupEntry(summonerId))
{
LOG_ERROR("sql.sql", "Table `gameobject_summon_groups` has summoner with non existing entry {} for map summoner type, skipped.", summonerId);
continue;
}
break;
default:
LOG_ERROR("sql.sql", "Table `gameobject_summon_groups` has unhandled summoner type {} for summoner {}, skipped.", summonerType, summonerId);
continue;
}
GameObjectSummonData data;
data.entry = fields[3].Get<uint32>();
if (!GetGameObjectTemplate(data.entry))
{
LOG_ERROR("sql.sql", "Table `gameobject_summon_groups` has gameobject in group [Summoner ID: {}, Summoner Type: {}, Group ID: {}] with non existing gameobject entry {}, skipped.", summonerId, summonerType, group, data.entry);
continue;
}
float posX = fields[4].Get<float>();
float posY = fields[5].Get<float>();
float posZ = fields[6].Get<float>();
float orientation = fields[7].Get<float>();
data.pos.Relocate(posX, posY, posZ, orientation);
data.rot = G3D::Quat(fields[8].Get<float>(), fields[9].Get<float>(), fields[10].Get<float>(), fields[11].Get<float>());
data.respawnTime = fields[12].Get<uint32>();
TempSummonGroupKey key(summonerId, summonerType, group);
_goSummonDataStore[key].push_back(data);
++count;
} while (result->NextRow());
LOG_INFO("server.loading", ">> Loaded {} Gameobject Summons in {} ms", count, GetMSTimeDiffToNow(oldMSTime));
LOG_INFO("server.loading", " ");
}
void ObjectMgr::LoadCreatures()
{
uint32 oldMSTime = getMSTime();

View File

@@ -507,6 +507,7 @@ typedef std::map<ObjectGuid, ObjectGuid> LinkedRespawnContainer;
typedef std::unordered_map<ObjectGuid::LowType, CreatureData> CreatureDataContainer;
typedef std::unordered_map<ObjectGuid::LowType, GameObjectData> GameObjectDataContainer;
typedef std::map<TempSummonGroupKey, std::vector<TempSummonData> > TempSummonDataContainer;
typedef std::map<TempSummonGroupKey, std::vector<GameObjectSummonData> > GameObjectSummonDataContainer;
typedef std::unordered_map<uint32, CreatureLocale> CreatureLocaleContainer;
typedef std::unordered_map<uint32, GameObjectLocale> GameObjectLocaleContainer;
typedef std::unordered_map<uint32, ItemLocale> ItemLocaleContainer;
@@ -1036,6 +1037,7 @@ public:
void LoadGameObjectQuestItems();
void LoadCreatureQuestItems();
void LoadTempSummons();
void LoadGameObjectSummons();
void LoadCreatures();
void LoadCreatureSparring();
void LoadLinkedRespawn();
@@ -1208,6 +1210,15 @@ public:
return nullptr;
}
[[nodiscard]] std::vector<GameObjectSummonData> const* GetGameObjectSummonGroup(uint32 summonerId, SummonerType summonerType, uint8 group) const
{
GameObjectSummonDataContainer::const_iterator itr = _goSummonDataStore.find(TempSummonGroupKey(summonerId, summonerType, group));
if (itr != _goSummonDataStore.end())
return &itr->second;
return nullptr;
}
[[nodiscard]] BroadcastText const* GetBroadcastText(uint32 id) const
{
BroadcastTextContainer::const_iterator itr = _broadcastTextStore.find(id);
@@ -1635,6 +1646,8 @@ private:
GameObjectTemplateAddonContainer _gameObjectTemplateAddonStore;
/// Stores temp summon data grouped by summoner's entry, summoner's type and group id
TempSummonDataContainer _tempSummonDataStore;
/// Stores gameobject summon data grouped by summoner's entry, summoner's type and group id
GameObjectSummonDataContainer _goSummonDataStore;
BroadcastTextContainer _broadcastTextStore;
ItemTemplateContainer _itemTemplateStore;

View File

@@ -337,6 +337,7 @@ public:
GameObject* SummonGameObject(uint32 entry, float x, float y, float z, float ang, float rotation0, float rotation1, float rotation2, float rotation3, uint32 respawnTime, bool checkTransport = true);
GameObject* SummonGameObject(uint32 entry, Position const& pos, float rotation0 = 0.0f, float rotation1 = 0.0f, float rotation2 = 0.0f, float rotation3 = 0.0f, uint32 respawnTime = 100, bool checkTransport = true);
void SummonCreatureGroup(uint8 group, std::list<TempSummon*>* list = nullptr);
void SummonGameObjectGroup(uint8 group, std::list<GameObject*>* list = nullptr);
Corpse* GetCorpse(ObjectGuid const& guid);
Creature* GetCreature(ObjectGuid const& guid);

View File

@@ -570,6 +570,9 @@ void World::SetInitialWorldSettings()
LOG_INFO("server.loading", "Loading Temporary Summon Data...");
sObjectMgr->LoadTempSummons(); // must be after LoadCreatureTemplates() and LoadGameObjectTemplates()
LOG_INFO("server.loading", "Loading Gameobject Summon Data...");
sObjectMgr->LoadGameObjectSummons(); // must be after LoadCreatureTemplates() and LoadGameObjectTemplates()
LOG_INFO("server.loading", "Loading Pet Levelup Spells...");
sSpellMgr->LoadPetLevelupSpellMap();

View File

@@ -0,0 +1,141 @@
/*
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ObjectMgr.h"
#include "SmartScriptMgr.h"
#include "TemporarySummon.h"
#include "WorldMock.h"
#include "gtest/gtest.h"
class GameObjectSummonGroupTest : public ::testing::Test
{
protected:
void SetUp() override
{
_previousWorld = std::move(sWorld);
auto* worldMock =
new ::testing::NiceMock<WorldMock>();
ON_CALL(*worldMock, getIntConfig(::testing::_))
.WillByDefault(::testing::Return(0));
sWorld.reset(worldMock);
}
void TearDown() override
{
sWorld = std::move(_previousWorld);
}
std::unique_ptr<IWorld> _previousWorld;
};
TEST_F(GameObjectSummonGroupTest, DataStructStoresFields)
{
GameObjectSummonData data;
data.entry = 2332;
data.pos.Relocate(-14652.38f, 146.51f, 3.50f, 0.35f);
data.rot = G3D::Quat(0.0f, 0.0f, 0.17f, 0.98f);
data.respawnTime = 120;
EXPECT_EQ(data.entry, 2332u);
EXPECT_FLOAT_EQ(data.pos.GetPositionX(), -14652.38f);
EXPECT_FLOAT_EQ(data.pos.GetPositionY(), 146.51f);
EXPECT_FLOAT_EQ(data.pos.GetPositionZ(), 3.50f);
EXPECT_FLOAT_EQ(data.pos.GetOrientation(), 0.35f);
EXPECT_FLOAT_EQ(data.rot.x, 0.0f);
EXPECT_FLOAT_EQ(data.rot.y, 0.0f);
EXPECT_FLOAT_EQ(data.rot.z, 0.17f);
EXPECT_FLOAT_EQ(data.rot.w, 0.98f);
EXPECT_EQ(data.respawnTime, 120u);
}
TEST_F(GameObjectSummonGroupTest, QuaternionIdentity)
{
GameObjectSummonData data;
data.rot = G3D::Quat(0.0f, 0.0f, 0.0f, 1.0f);
EXPECT_FLOAT_EQ(data.rot.x, 0.0f);
EXPECT_FLOAT_EQ(data.rot.y, 0.0f);
EXPECT_FLOAT_EQ(data.rot.z, 0.0f);
EXPECT_FLOAT_EQ(data.rot.w, 1.0f);
}
TEST_F(GameObjectSummonGroupTest, AccessorReturnsNullForMissing)
{
auto const* result = sObjectMgr->GetGameObjectSummonGroup(
99999, SUMMONER_TYPE_CREATURE, 0);
EXPECT_EQ(result, nullptr);
}
TEST_F(GameObjectSummonGroupTest, AccessorReturnsNullForAllTypes)
{
auto const* r1 = sObjectMgr->GetGameObjectSummonGroup(
99999, SUMMONER_TYPE_CREATURE, 0);
auto const* r2 = sObjectMgr->GetGameObjectSummonGroup(
99999, SUMMONER_TYPE_GAMEOBJECT, 0);
auto const* r3 = sObjectMgr->GetGameObjectSummonGroup(
99999, SUMMONER_TYPE_MAP, 0);
EXPECT_EQ(r1, nullptr);
EXPECT_EQ(r2, nullptr);
EXPECT_EQ(r3, nullptr);
}
TEST_F(GameObjectSummonGroupTest, DifferentGroupsAreIndependent)
{
auto const* g0 = sObjectMgr->GetGameObjectSummonGroup(
2289, SUMMONER_TYPE_GAMEOBJECT, 0);
auto const* g1 = sObjectMgr->GetGameObjectSummonGroup(
2289, SUMMONER_TYPE_GAMEOBJECT, 1);
// Both should be null since DB isn't loaded in tests,
// but they should be independent lookups
EXPECT_EQ(g0, nullptr);
EXPECT_EQ(g1, nullptr);
}
TEST_F(GameObjectSummonGroupTest, SmartActionEnumValue)
{
EXPECT_EQ(SMART_ACTION_SUMMON_GAMEOBJECT_GROUP, 241);
EXPECT_EQ(SMART_ACTION_AC_END, 242);
}
TEST_F(GameObjectSummonGroupTest, SmartActionUnionSize)
{
SmartAction action{};
action.gameobjectGroup.group = 5;
EXPECT_EQ(action.gameobjectGroup.group, 5u);
}
TEST_F(GameObjectSummonGroupTest, TempSummonGroupKeyOrdering)
{
TempSummonGroupKey k1(100, SUMMONER_TYPE_CREATURE, 0);
TempSummonGroupKey k2(100, SUMMONER_TYPE_GAMEOBJECT, 0);
TempSummonGroupKey k3(100, SUMMONER_TYPE_CREATURE, 1);
TempSummonGroupKey k4(200, SUMMONER_TYPE_CREATURE, 0);
// std::tuple ordering: summoner ID first, then type, then group
EXPECT_LT(k1, k2); // same id, creature < gameobject
EXPECT_LT(k1, k3); // same id+type, group 0 < 1
EXPECT_LT(k1, k4); // id 100 < 200
}
TEST_F(GameObjectSummonGroupTest, SummonerTypeValues)
{
EXPECT_EQ(SUMMONER_TYPE_CREATURE, 0);
EXPECT_EQ(SUMMONER_TYPE_GAMEOBJECT, 1);
EXPECT_EQ(SUMMONER_TYPE_MAP, 2);
}