2022-12-27 11:12:37 +01:00
|
|
|
#include "framework.h"
|
2023-01-10 16:45:16 +11:00
|
|
|
#include "Objects/TR3/Entity/PunaBoss.h"
|
2023-01-05 21:05:03 +11:00
|
|
|
|
2022-12-29 13:39:50 +01:00
|
|
|
#include "Game/control/box.h"
|
|
|
|
#include "Game/control/los.h"
|
2022-12-28 12:19:41 +01:00
|
|
|
#include "Game/effects/effects.h"
|
2023-01-05 21:05:03 +11:00
|
|
|
#include "Game/effects/item_fx.h"
|
|
|
|
#include "Game/effects/lightning.h"
|
2022-12-29 13:39:50 +01:00
|
|
|
#include "Game/Lara/lara_helpers.h"
|
2023-01-05 21:05:03 +11:00
|
|
|
#include "Game/misc.h"
|
|
|
|
#include "Math/Math.h"
|
2023-01-10 18:06:26 +11:00
|
|
|
#include "Objects/Effects/Boss.h"
|
2023-01-05 21:05:03 +11:00
|
|
|
#include "Specific/level.h"
|
|
|
|
#include "Specific/setup.h"
|
2022-12-28 12:19:41 +01:00
|
|
|
|
2023-01-05 21:05:03 +11:00
|
|
|
using namespace TEN::Effects::Boss;
|
2022-12-29 13:39:50 +01:00
|
|
|
using namespace TEN::Effects::Items;
|
2023-01-05 21:05:03 +11:00
|
|
|
using namespace TEN::Effects::Lightning;
|
2022-12-27 11:12:37 +01:00
|
|
|
|
|
|
|
namespace TEN::Entities::Creatures::TR3
|
|
|
|
{
|
2023-01-10 16:42:21 +11:00
|
|
|
constexpr auto PUNA_LIGHTNING_DAMAGE = 350;
|
2023-01-05 21:05:03 +11:00
|
|
|
|
|
|
|
constexpr auto PUNA_ATTACK_RANGE = BLOCK(20);
|
|
|
|
constexpr auto PUNA_ALERT_RANGE = BLOCK(2.5f);
|
|
|
|
|
2023-01-10 18:00:16 +11:00
|
|
|
constexpr auto PUNA_TURN_RATE_MAX = ANGLE(3.0f);
|
|
|
|
constexpr auto PUNA_CHAIR_TURN_RATE_MAX = ANGLE(7.0f);
|
|
|
|
constexpr auto PUNA_HEAD_X_ANGLE_MAX = ANGLE(20.0f);
|
|
|
|
constexpr auto PUNA_ADJUST_LIGHTNING_X_ANGLE = ANGLE(3.0f);
|
2023-01-05 21:05:03 +11:00
|
|
|
|
2023-01-10 18:00:16 +11:00
|
|
|
constexpr auto PUNA_EXPLOSION_NUM_MAX = 120;
|
|
|
|
constexpr auto PUNA_HEAD_ATTACK_NUM_MAX = 4;
|
2023-01-05 21:05:03 +11:00
|
|
|
constexpr auto PUNA_EFFECT_COLOR = Vector4(0.0f, 0.75f, 0.75f, 1.0f);
|
|
|
|
|
|
|
|
const auto PunaBossHeadBite = BiteInfo(Vector3::Zero, 8);
|
|
|
|
const auto PunaBossHandBite = BiteInfo(Vector3::Zero, 14);
|
|
|
|
|
|
|
|
enum PunaState
|
|
|
|
{
|
|
|
|
PUNA_STATE_IDLE = 0,
|
|
|
|
PUNA_STATE_HEAD_ATTACK = 1,
|
|
|
|
PUNA_STATE_HAND_ATTACK = 2,
|
|
|
|
PUNA_STATE_DEATH = 3
|
|
|
|
};
|
|
|
|
|
|
|
|
enum PunaAnim
|
|
|
|
{
|
|
|
|
PUNA_ANIM_IDLE = 0,
|
|
|
|
PUNA_ANIM_HEAD_ATTACK = 1,
|
|
|
|
PUNA_ANIM_HAND_ATTACK = 2,
|
|
|
|
PUNA_ANIM_DEATH = 3
|
|
|
|
};
|
|
|
|
|
|
|
|
enum class PunaAttackType
|
|
|
|
{
|
|
|
|
AwaitPlayer,
|
2023-01-10 23:03:35 +11:00
|
|
|
DeathLightning,
|
|
|
|
SummonLightning,
|
2023-01-10 16:42:21 +11:00
|
|
|
Wait // Used while an active lizard is nearby.
|
2023-01-05 21:05:03 +11:00
|
|
|
};
|
|
|
|
|
|
|
|
void InitialisePuna(short itemNumber)
|
|
|
|
{
|
|
|
|
auto& item = g_Level.Items[itemNumber];
|
|
|
|
|
|
|
|
InitialiseCreature(itemNumber);
|
|
|
|
SetAnimation(&item, PUNA_ANIM_IDLE);
|
|
|
|
CheckForRequiredObjects(item);
|
|
|
|
|
2023-01-10 16:42:21 +11:00
|
|
|
// Save Puna's default angle. It will be used while waiting (i.e. an active lizard is nearby).
|
|
|
|
// NOTE: Since Puna is oriented to face away from the player, add 180 degrees.
|
2023-01-10 17:28:01 +11:00
|
|
|
item.SetFlagField(BOSSFlag_Rotation, item.Pose.Orientation.y + ANGLE(180.0f));
|
|
|
|
item.SetFlagField(BOSSFlag_AttackType, (int)PunaAttackType::AwaitPlayer); // normal behaviour at start.
|
|
|
|
item.SetFlagField(BOSSFlag_ShieldIsEnabled, 1); // activated at start.
|
|
|
|
item.SetFlagField(BOSSFlag_AttackCount, 0);
|
|
|
|
item.SetFlagField(BOSSFlag_DeathCount, 0);
|
|
|
|
item.SetFlagField(BOSSFlag_ItemNumber, NO_ITEM);
|
|
|
|
item.SetFlagField(BOSSFlag_ExplodeCount, 0);
|
2023-01-05 21:05:03 +11:00
|
|
|
|
2023-01-10 16:42:21 +11:00
|
|
|
// If there is no lizard nearby, remove the lizard flag.
|
|
|
|
if (!IsLizardActiveNearby(item, true))
|
2023-01-10 17:28:01 +11:00
|
|
|
item.ClearFlags(BOSSFlag_Object, BOSS_Lizard);
|
2023-01-05 21:05:03 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
void PunaControl(short itemNumber)
|
|
|
|
{
|
|
|
|
if (!CreatureActive(itemNumber))
|
|
|
|
return;
|
|
|
|
|
|
|
|
auto& item = g_Level.Items[itemNumber];
|
|
|
|
auto& object = Objects[item.ObjectNumber];
|
|
|
|
auto& creature = *GetCreatureInfo(&item);
|
|
|
|
|
|
|
|
static auto targetPos = Vector3i::Zero;
|
|
|
|
|
|
|
|
auto headOrient = EulerAngles::Zero;
|
|
|
|
short headingAngle = 0;
|
|
|
|
short prevYOrient = 0;
|
|
|
|
|
|
|
|
bool hasTurned = false;
|
2023-01-10 17:29:33 +11:00
|
|
|
bool isLizardActiveNearby = IsLizardActiveNearby(item);
|
2023-01-05 21:05:03 +11:00
|
|
|
|
|
|
|
if (item.HitPoints <= 0)
|
|
|
|
{
|
|
|
|
if (item.Animation.ActiveState != PUNA_STATE_DEATH)
|
|
|
|
{
|
|
|
|
SetAnimation(&item, PUNA_ANIM_DEATH);
|
|
|
|
SoundEffect(SFX_TR3_PUNA_BOSS_DEATH, &item.Pose);
|
|
|
|
item.ItemFlags[BOSSFlag_DeathCount] = 1;
|
|
|
|
creature.MaxTurn = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
int frameEnd = g_Level.Anims[object.animIndex + PUNA_ANIM_DEATH].frameEnd;
|
|
|
|
if (item.Animation.FrameNumber >= frameEnd)
|
|
|
|
{
|
|
|
|
// Avoid having the object stop working.
|
|
|
|
item.Animation.FrameNumber = frameEnd;
|
|
|
|
item.MeshBits.ClearAll();
|
|
|
|
|
2023-01-10 18:00:16 +11:00
|
|
|
if (item.GetFlagField(BOSSFlag_ExplodeCount) < PUNA_EXPLOSION_NUM_MAX)
|
2023-01-05 21:05:03 +11:00
|
|
|
item.ItemFlags[BOSSFlag_ExplodeCount]++;
|
|
|
|
|
2023-01-10 18:00:16 +11:00
|
|
|
if (item.GetFlagField(BOSSFlag_ExplodeCount) < PUNA_EXPLOSION_NUM_MAX)
|
2023-01-05 21:05:03 +11:00
|
|
|
ExplodeBoss(itemNumber, item, 61, PUNA_EFFECT_COLOR); // Do explosion effect.
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2023-01-10 17:28:01 +11:00
|
|
|
auto deathCount = item.GetFlagField(BOSSFlag_DeathCount);
|
2023-01-05 21:05:03 +11:00
|
|
|
item.Pose.Orientation.z = (Random::GenerateInt() % deathCount) - (item.ItemFlags[BOSSFlag_DeathCount] >> 1);
|
|
|
|
|
|
|
|
if (deathCount < 2048)
|
|
|
|
item.ItemFlags[BOSSFlag_DeathCount] += 32;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
prevYOrient = item.Pose.Orientation.y;
|
|
|
|
|
|
|
|
AI_INFO AI;
|
|
|
|
CreatureAIInfo(&item, &AI);
|
|
|
|
if (AI.ahead)
|
|
|
|
{
|
|
|
|
headOrient.x = AI.xAngle;
|
|
|
|
headOrient.y = AI.angle;
|
|
|
|
}
|
|
|
|
|
2023-01-10 17:28:01 +11:00
|
|
|
if (item.TestFlagField(BOSSFlag_AttackType, (int)PunaAttackType::AwaitPlayer) && creature.Enemy != nullptr)
|
2023-01-05 21:05:03 +11:00
|
|
|
{
|
|
|
|
float distance = Vector3i::Distance(creature.Enemy->Pose.Position, item.Pose.Position);
|
|
|
|
|
|
|
|
if (distance <= BLOCK(2.5f))
|
2023-01-10 23:03:35 +11:00
|
|
|
item.SetFlagField(BOSSFlag_AttackType, (int)PunaAttackType::DeathLightning);
|
2023-01-05 21:05:03 +11:00
|
|
|
|
|
|
|
// Rotate the object on puna boss chair.
|
|
|
|
creature.JointRotation[0] += PUNA_CHAIR_TURN_RATE_MAX;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get target.
|
2023-01-10 23:03:35 +11:00
|
|
|
if (item.TestFlagField(BOSSFlag_AttackType, (int)PunaAttackType::DeathLightning))
|
2023-01-05 21:05:03 +11:00
|
|
|
{
|
|
|
|
creature.Target = creature.Enemy->Pose.Position;
|
|
|
|
}
|
2023-01-10 17:28:01 +11:00
|
|
|
else if (item.TestFlags(BOSSFlag_Object, BOSS_Lizard) &&
|
2023-01-10 23:03:35 +11:00
|
|
|
item.TestFlagField(BOSSFlag_AttackType, (int)PunaAttackType::SummonLightning) &&
|
2023-01-10 17:28:01 +11:00
|
|
|
item.TestFlagField(BOSSFlag_ItemNumber, NO_ITEM) &&
|
2023-01-10 17:29:33 +11:00
|
|
|
!item.TestFlagField(BOSSFlag_AttackType, (int)PunaAttackType::Wait) && isLizardActiveNearby)
|
2023-01-05 21:05:03 +11:00
|
|
|
{
|
2023-01-10 18:00:16 +11:00
|
|
|
// Get random lizard item number.
|
2023-01-10 17:28:01 +11:00
|
|
|
item.SetFlagField(BOSSFlag_ItemNumber, (short)GetLizardItemNumber(item));
|
2023-01-05 21:05:03 +11:00
|
|
|
creature.Target = GetLizardTargetPosition(item);
|
|
|
|
}
|
2023-01-10 17:28:01 +11:00
|
|
|
else if (item.TestFlags(BOSSFlag_Object, BOSS_Lizard) &&
|
|
|
|
item.TestFlagField(BOSSFlag_AttackType, (int)PunaAttackType::Wait) &&
|
|
|
|
!item.TestFlagField(BOSSFlag_ItemNumber, NO_ITEM))
|
2023-01-05 21:05:03 +11:00
|
|
|
{
|
|
|
|
// Rotate to idle position while player fights lizard.
|
2023-01-10 17:28:01 +11:00
|
|
|
auto targetOrient = EulerAngles(item.Pose.Orientation.x, item.GetFlagField(BOSSFlag_Rotation), item.Pose.Orientation.z);
|
2023-01-05 21:05:03 +11:00
|
|
|
item.Pose.Orientation.InterpolateConstant(targetOrient, ANGLE(3.0f));
|
|
|
|
|
|
|
|
// Check if target is dead.
|
2023-01-10 17:28:01 +11:00
|
|
|
auto& summonItem = g_Level.Items[item.GetFlagField(BOSSFlag_ItemNumber)];
|
2023-01-05 21:05:03 +11:00
|
|
|
|
|
|
|
if (summonItem.HitPoints <= 0)
|
|
|
|
{
|
|
|
|
// Reset the attack type, attack count, itemNumber, and restart the sequence.
|
2023-01-10 23:03:35 +11:00
|
|
|
item.SetFlagField(BOSSFlag_AttackType, (int)PunaAttackType::DeathLightning);
|
2023-01-10 17:28:01 +11:00
|
|
|
item.SetFlagField(BOSSFlag_AttackCount, 0);
|
|
|
|
item.SetFlagField(BOSSFlag_ItemNumber, NO_ITEM);
|
2023-01-05 21:05:03 +11:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (item.HitStatus)
|
|
|
|
SoundEffect(SFX_TR3_PUNA_BOSS_TAKE_HIT, &item.Pose);
|
|
|
|
|
|
|
|
short headYOrient = GetPunaHeadOrientToTarget(item, creature.Target.ToVector3());
|
|
|
|
headingAngle = CreatureTurn(&item, creature.MaxTurn);
|
|
|
|
|
|
|
|
switch (item.Animation.ActiveState)
|
|
|
|
{
|
|
|
|
case PUNA_STATE_IDLE:
|
|
|
|
creature.MaxTurn = PUNA_TURN_RATE_MAX;
|
2023-01-10 17:28:01 +11:00
|
|
|
item.SetFlagField(BOSSFlag_ShieldIsEnabled, 1);
|
2023-01-05 21:05:03 +11:00
|
|
|
|
|
|
|
if ((item.Animation.TargetState != PUNA_STATE_HAND_ATTACK && item.Animation.TargetState != PUNA_STATE_HEAD_ATTACK) &&
|
|
|
|
AI.angle > ANGLE(-1.0f) && AI.angle < ANGLE(1.0f) &&
|
|
|
|
creature.Enemy->HitPoints > 0 &&
|
2023-01-10 18:00:16 +11:00
|
|
|
item.GetFlagField(BOSSFlag_AttackCount) < PUNA_HEAD_ATTACK_NUM_MAX &&
|
2023-01-10 23:03:35 +11:00
|
|
|
!item.TestFlagField(BOSSFlag_AttackType, (int)PunaAttackType::SummonLightning) && !item.TestFlagField(BOSSFlag_AttackType, (int)PunaAttackType::Wait))
|
2023-01-05 21:05:03 +11:00
|
|
|
{
|
|
|
|
creature.MaxTurn = 0;
|
|
|
|
targetPos = creature.Target;
|
|
|
|
targetPos.y -= CLICK(2);
|
2023-01-10 23:03:35 +11:00
|
|
|
item.SetFlagField(BOSSFlag_AttackType, (int)PunaAttackType::DeathLightning);
|
2023-01-05 21:05:03 +11:00
|
|
|
|
|
|
|
if (Random::TestProbability(1 / 3.0f))
|
|
|
|
item.Animation.TargetState = PUNA_STATE_HEAD_ATTACK;
|
|
|
|
else
|
|
|
|
item.Animation.TargetState = PUNA_STATE_HAND_ATTACK;
|
|
|
|
|
2023-01-10 17:29:33 +11:00
|
|
|
if (item.TestFlags(BOSSFlag_Object, BOSS_Lizard) && isLizardActiveNearby)
|
2023-01-05 21:05:03 +11:00
|
|
|
item.ItemFlags[BOSSFlag_AttackCount]++;
|
|
|
|
}
|
2023-01-10 18:00:16 +11:00
|
|
|
else if (item.ItemFlags[BOSSFlag_AttackCount] >= PUNA_HEAD_ATTACK_NUM_MAX &&
|
2023-01-05 21:05:03 +11:00
|
|
|
creature.Enemy->HitPoints > 0 &&
|
|
|
|
item.ItemFlags[BOSSFlag_AttackType] != (int)PunaAttackType::Wait)
|
|
|
|
{
|
2023-01-10 23:03:35 +11:00
|
|
|
item.SetFlagField(BOSSFlag_AttackType, (int)PunaAttackType::SummonLightning);
|
2023-01-05 21:05:03 +11:00
|
|
|
|
2023-01-10 17:28:01 +11:00
|
|
|
if (!item.TestFlagField(BOSSFlag_ItemNumber, NO_ITEM))
|
2023-01-05 21:05:03 +11:00
|
|
|
{
|
|
|
|
if (headYOrient > ANGLE(-1.0f) && headYOrient < ANGLE(1.0f))
|
|
|
|
{
|
|
|
|
targetPos = creature.Target;
|
|
|
|
targetPos.y -= CLICK(2);
|
|
|
|
item.Animation.TargetState = PUNA_STATE_HAND_ATTACK;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case PUNA_STATE_HEAD_ATTACK:
|
2023-01-10 17:28:01 +11:00
|
|
|
item.SetFlagField(BOSSFlag_ShieldIsEnabled, 0);
|
2023-01-05 21:05:03 +11:00
|
|
|
creature.MaxTurn = 0;
|
|
|
|
|
|
|
|
if (item.Animation.FrameNumber == GetFrameNumber(&item, 14))
|
2023-01-10 18:00:16 +11:00
|
|
|
DoPunaLightning(item, targetPos.ToVector3(), PunaBossHeadBite, 10, false);
|
2023-01-05 21:05:03 +11:00
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case PUNA_STATE_HAND_ATTACK:
|
2023-01-10 17:28:01 +11:00
|
|
|
item.SetFlagField(BOSSFlag_ShieldIsEnabled, 0);
|
2023-01-05 21:05:03 +11:00
|
|
|
creature.MaxTurn = 0;
|
|
|
|
|
|
|
|
if (item.Animation.FrameNumber == GetFrameNumber(&item, 30))
|
|
|
|
{
|
2023-01-10 17:28:01 +11:00
|
|
|
if (item.TestFlags(BOSSFlag_Object, BOSS_Lizard) &&
|
2023-01-10 23:03:35 +11:00
|
|
|
item.TestFlagField(BOSSFlag_AttackType, (int)PunaAttackType::SummonLightning) &&
|
2023-01-10 17:29:33 +11:00
|
|
|
!item.TestFlagField(BOSSFlag_ItemNumber, NO_ITEM) && isLizardActiveNearby)
|
2023-01-05 21:05:03 +11:00
|
|
|
{
|
2023-01-10 18:00:16 +11:00
|
|
|
DoPunaLightning(item, targetPos.ToVector3(), PunaBossHandBite, 5, true);
|
2023-01-05 21:05:03 +11:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2023-01-10 18:00:16 +11:00
|
|
|
DoPunaLightning(item, targetPos.ToVector3(), PunaBossHandBite, 10, false);
|
2023-01-05 21:05:03 +11:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Rotate chair.
|
|
|
|
creature.JointRotation[0] += PUNA_CHAIR_TURN_RATE_MAX;
|
|
|
|
|
|
|
|
CreatureJoint(&item, 1, headOrient.y);
|
|
|
|
CreatureJoint(&item, 2, headOrient.x, PUNA_HEAD_X_ANGLE_MAX);
|
|
|
|
CreatureAnimation(itemNumber, headingAngle, 0);
|
|
|
|
|
|
|
|
// Emit sound while chair is rotating.
|
|
|
|
if (prevYOrient != item.Pose.Orientation.y && !hasTurned)
|
|
|
|
{
|
|
|
|
hasTurned = true;
|
|
|
|
SoundEffect(SFX_TR3_PUNA_BOSS_TURN_CHAIR, &item.Pose);
|
|
|
|
}
|
|
|
|
else if (prevYOrient == item.Pose.Orientation.y)
|
|
|
|
{
|
|
|
|
hasTurned = false;
|
|
|
|
StopSoundEffect(SFX_TR3_PUNA_BOSS_CHAIR_2);
|
|
|
|
StopSoundEffect(SFX_TR3_PUNA_BOSS_TURN_CHAIR);
|
|
|
|
}
|
|
|
|
}
|
2023-01-10 16:42:21 +11:00
|
|
|
|
2023-01-10 18:00:16 +11:00
|
|
|
void DoPunaLightning(ItemInfo& item, const Vector3& pos, const BiteInfo& bite, int intensity, bool isSummon)
|
2023-01-10 16:42:21 +11:00
|
|
|
{
|
|
|
|
const auto& creature = *GetCreatureInfo(&item);
|
|
|
|
|
|
|
|
auto origin = GameVector(GetJointPosition(&item, bite.meshNum, bite.Position), item.RoomNumber);
|
2023-01-11 01:07:29 +11:00
|
|
|
auto target = GameVector(Geometry::TranslatePoint(origin.ToVector3(), pos - origin.ToVector3(), PUNA_ATTACK_RANGE), creature.Enemy->RoomNumber);
|
2023-01-10 16:42:21 +11:00
|
|
|
|
|
|
|
if (isSummon)
|
|
|
|
{
|
|
|
|
TriggerLightning((Vector3i*)&origin, (Vector3i*)&target, intensity, 0, 255, 0, 30, LI_SPLINE | LI_THINOUT, 50, 10);
|
|
|
|
TriggerDynamicLight(origin.x, origin.y, origin.z, 20, 0, 255, 0);
|
|
|
|
SpawnLizard(item);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
auto hitPos = Vector3i::Zero;
|
|
|
|
MESH_INFO* mesh = nullptr;
|
|
|
|
TriggerLightning((Vector3i*)&origin, (Vector3i*)&target, intensity, 0, 255, 255, 30, LI_SPLINE | LI_THINOUT, 50, 10);
|
|
|
|
TriggerDynamicLight(origin.x, origin.y, origin.z, 20, 0, 255, 255);
|
|
|
|
|
|
|
|
if (ObjectOnLOS2(&origin, &target, &hitPos, &mesh, ID_LARA) == GetLaraInfo(creature.Enemy)->ItemNumber)
|
|
|
|
{
|
|
|
|
if (creature.Enemy->HitPoints <= PUNA_LIGHTNING_DAMAGE)
|
|
|
|
{
|
|
|
|
ItemElectricBurn(creature.Enemy);
|
|
|
|
DoDamage(creature.Enemy, PUNA_LIGHTNING_DAMAGE);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
DoDamage(creature.Enemy, PUNA_LIGHTNING_DAMAGE);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
short GetPunaHeadOrientToTarget(ItemInfo& item, const Vector3& target)
|
|
|
|
{
|
2023-01-10 17:28:01 +11:00
|
|
|
if (!item.TestFlags(BOSSFlag_Object, BOSS_Lizard))
|
2023-01-10 16:42:21 +11:00
|
|
|
return NO_ITEM;
|
|
|
|
|
|
|
|
auto pos = GetJointPosition(&item, PunaBossHeadBite.meshNum).ToVector3();
|
|
|
|
auto orient = Geometry::GetOrientToPoint(pos, target);
|
|
|
|
return (orient.y - item.Pose.Orientation.y);
|
|
|
|
}
|
|
|
|
|
|
|
|
std::vector<int> GetLizardEntityList(const ItemInfo& item)
|
|
|
|
{
|
|
|
|
auto entityList = std::vector<int>{};
|
|
|
|
|
|
|
|
for (auto& currentEntity : g_Level.Items)
|
|
|
|
{
|
|
|
|
if (currentEntity.ObjectNumber == ID_LIZARD &&
|
|
|
|
currentEntity.RoomNumber == item.RoomNumber &&
|
|
|
|
currentEntity.HitPoints > 0 &&
|
|
|
|
currentEntity.Status == ITEM_INVISIBLE &&
|
|
|
|
!(currentEntity.Flags & IFLAG_KILLED))
|
|
|
|
{
|
|
|
|
entityList.push_back(currentEntity.Index);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return entityList;
|
|
|
|
}
|
|
|
|
|
|
|
|
Vector3 GetLizardTargetPosition(ItemInfo& item)
|
|
|
|
{
|
2023-01-10 17:28:01 +11:00
|
|
|
if (!item.TestFlagField(BOSSFlag_ItemNumber, NO_ITEM))
|
2023-01-10 16:42:21 +11:00
|
|
|
{
|
2023-01-10 17:28:01 +11:00
|
|
|
const auto& targetEntity = g_Level.Items[item.GetFlagField(BOSSFlag_ItemNumber)];
|
2023-01-10 16:42:21 +11:00
|
|
|
return targetEntity.Pose.Position.ToVector3();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Failsafe.
|
|
|
|
const auto& creature = *GetCreatureInfo(&item);
|
|
|
|
return creature.Target.ToVector3();
|
|
|
|
}
|
|
|
|
|
|
|
|
int GetLizardItemNumber(const ItemInfo& item)
|
|
|
|
{
|
2023-01-10 17:28:01 +11:00
|
|
|
if (!item.TestFlags(BOSSFlag_Object, BOSS_Lizard))
|
2023-01-10 16:42:21 +11:00
|
|
|
return NO_ITEM;
|
|
|
|
|
|
|
|
auto lizardList = GetLizardEntityList(item);
|
|
|
|
if (lizardList.empty())
|
|
|
|
return NO_ITEM;
|
|
|
|
|
|
|
|
if (lizardList.size() == 1)
|
|
|
|
return lizardList[0];
|
|
|
|
else
|
|
|
|
return lizardList[Random::GenerateInt(0, lizardList.size() - 1)];
|
|
|
|
}
|
|
|
|
|
|
|
|
bool IsLizardActiveNearby(const ItemInfo& item, bool isInitializing)
|
|
|
|
{
|
|
|
|
for (auto& currentEntity : g_Level.Items)
|
|
|
|
{
|
|
|
|
// Check if the entity is a lizard.
|
|
|
|
if (currentEntity.ObjectNumber != ID_LIZARD)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
// Check if the entity is in the same room as Puna.
|
|
|
|
if (currentEntity.RoomNumber != item.RoomNumber)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
// If the enity is currently initializing, return early.
|
|
|
|
if (isInitializing)
|
|
|
|
return true;
|
|
|
|
|
|
|
|
// Check status of entity.
|
|
|
|
if (currentEntity.HitPoints > 0 &&
|
|
|
|
currentEntity.Status == ITEM_INVISIBLE &&
|
|
|
|
!(currentEntity.Flags & IFLAG_KILLED))
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
void SpawnLizard(ItemInfo& item)
|
|
|
|
{
|
2023-01-10 17:28:01 +11:00
|
|
|
if (!item.TestFlagField(BOSSFlag_ItemNumber, NO_ITEM))
|
2023-01-10 16:42:21 +11:00
|
|
|
{
|
2023-01-10 17:28:01 +11:00
|
|
|
auto itemNumber = item.GetFlagField(BOSSFlag_ItemNumber);
|
2023-01-10 15:24:04 +01:00
|
|
|
auto& currentItem = g_Level.Items[itemNumber];
|
2023-01-10 16:42:21 +11:00
|
|
|
|
|
|
|
for (int i = 0; i < 20; i++)
|
2023-01-10 15:24:04 +01:00
|
|
|
SpawnSummonSmoke(currentItem.Pose.Position.ToVector3());
|
2023-01-10 16:42:21 +11:00
|
|
|
|
|
|
|
AddActiveItem(itemNumber);
|
2023-01-10 15:24:04 +01:00
|
|
|
currentItem.ItemFlags[0] = 1; // Flag 1 = spawned lizard.
|
|
|
|
item.SetFlagField(BOSSFlag_AttackType, (int)PunaAttackType::Wait);
|
2023-01-10 16:42:21 +11:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void SpawnSummonSmoke(const Vector3& pos)
|
|
|
|
{
|
|
|
|
auto& smoke = *GetFreeParticle();
|
|
|
|
|
|
|
|
smoke.sR = 16;
|
|
|
|
smoke.sG = 64;
|
|
|
|
smoke.sB = 0;
|
|
|
|
smoke.dR = 8;
|
|
|
|
smoke.dG = 32;
|
|
|
|
smoke.dB = 0;
|
|
|
|
smoke.colFadeSpeed = 16 + (GetRandomControl() & 7);
|
|
|
|
smoke.fadeToBlack = 64;
|
|
|
|
smoke.sLife = smoke.life = (GetRandomControl() & 15) + 96;
|
|
|
|
|
|
|
|
smoke.blendMode = BLEND_MODES::BLENDMODE_ADDITIVE;
|
|
|
|
smoke.extras = 0;
|
|
|
|
smoke.dynamic = -1;
|
|
|
|
|
|
|
|
smoke.x = pos.x + ((GetRandomControl() & 127) - 64);
|
|
|
|
smoke.y = pos.y - (GetRandomControl() & 31);
|
|
|
|
smoke.z = pos.z + ((GetRandomControl() & 127) - 64);
|
|
|
|
smoke.xVel = ((GetRandomControl() & 255) - 128);
|
|
|
|
smoke.yVel = -(GetRandomControl() & 15) - 16;
|
|
|
|
smoke.zVel = ((GetRandomControl() & 255) - 128);
|
|
|
|
smoke.friction = 0;
|
|
|
|
|
|
|
|
if (Random::TestProbability(1 / 2.0f))
|
|
|
|
{
|
|
|
|
smoke.rotAng = GetRandomControl() & 4095;
|
|
|
|
smoke.flags = SP_SCALE | SP_DEF | SP_ROTATE | SP_EXPDEF | SP_WIND;
|
|
|
|
|
|
|
|
if (GetRandomControl() & 1)
|
|
|
|
smoke.rotAdd = -(GetRandomControl() & 7) - 4;
|
|
|
|
else
|
|
|
|
smoke.rotAdd = (GetRandomControl() & 7) + 4;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
smoke.flags = SP_SCALE | SP_DEF | SP_EXPDEF | SP_WIND;
|
|
|
|
}
|
|
|
|
|
|
|
|
smoke.spriteIndex = Objects[ID_DEFAULT_SPRITES].meshIndex;
|
|
|
|
smoke.scalar = 3;
|
|
|
|
smoke.gravity = -(GetRandomControl() & 7) - 8;
|
|
|
|
smoke.maxYvel = -(GetRandomControl() & 7) - 4;
|
|
|
|
int size = (GetRandomControl() & 128) + 256;
|
|
|
|
smoke.size = smoke.sSize = size >> 1;
|
|
|
|
smoke.dSize = size;
|
|
|
|
smoke.on = true;
|
|
|
|
}
|
2022-12-27 11:12:37 +01:00
|
|
|
}
|