#include "framework.h" #include "Objects/TR4/Entity/tr4_ahmet.h" #include "Game/collision/sphere.h" #include "Game/control/box.h" #include "Game/control/control.h" #include "Game/control/lot.h" #include "Game/effects/effects.h" #include "Game/effects/weather.h" #include "Game/itemdata/creature_info.h" #include "Game/items.h" #include "Game/Lara/lara.h" #include "Game/misc.h" #include "Game/people.h" #include "Math/Math.h" #include "Sound/sound.h" #include "Specific/level.h" #include "Specific/setup.h" using namespace TEN::Effects::Environment; using namespace TEN::Math; namespace TEN::Entities::TR4 { constexpr auto AHMET_SWIPE_ATTACK_DAMAGE = 80; constexpr auto AHMET_BITE_ATTACK_DAMAGE = 120; constexpr auto AHMET_ATTACK_RANGE = SQUARE(BLOCK(0.67f)); constexpr auto AHMET_AWARE_RANGE = SQUARE(BLOCK(1)); constexpr auto AHMET_IDLE_RANGE = SQUARE(BLOCK(1.25f)); constexpr auto AHMET_RUN_RANGE = SQUARE(BLOCK(2.5f)); constexpr auto AHMET_WALK_FORWARD_TURN_ANGLE = ANGLE(5.0f); constexpr auto AHMET_RUN_FORWARD_TURN_ANGLE = ANGLE(8.0f); constexpr auto AHMET_VIEW_ANGLE = ANGLE(45.0f); constexpr auto AHMET_ENEMY_ANGLE = ANGLE(90.0f); const auto AhmetBiteLeft = CreatureBiteInfo(Vector3i::Zero, 16); const auto AhmetBiteRight = CreatureBiteInfo(Vector3i::Zero, 22); const auto AhmetBiteJaw = CreatureBiteInfo(Vector3i::Zero, 11); const auto AhmetSwipeAttackLeftJoints = std::vector{ 14, 15, 16, 17 }; const auto AhmetSwipeAttackRightJoints = std::vector{ 20, 21, 22, 23 }; enum AhmetState { // No state 0. AHMET_STATE_IDLE = 1, AHMET_STATE_WALK_FORWARD = 2, AHMET_STATE_RUN_FORWARD = 3, AHMET_STATE_SWIPE_ATTACK = 4, AHMET_STATE_JUMP_BITE_ATTACK = 5, AHMET_STATE_JUMP_SWIPE_ATTACK = 6, AHMET_STATE_DEATH = 7 }; enum AhmetAnim { AHMET_ANIM_IDLE = 0, AHMET_ANIM_RUN_FORWARD = 1, AHMET_ANIM_SWIPE_ATTACK = 2, AHMET_ANIM_JUMP_BITE_ATTACK_START = 3, AHMET_ANIM_JUMP_BITE_ATTACK_CONTINUE = 4, AHMET_ANIM_JUMP_BITE_ATTACK_END = 5, AHMET_ANIM_WALK_FORWARD = 6, AHMET_ANIM_JUMP_SWIPE_ATTACK_START = 7, AHMET_ANIM_JUMP_SWIPE_ATTACK_CONTINUE = 8, AHMET_ANIM_JUMP_SWIPE_ATTACK_END = 9, AHMET_ANIM_DEATH = 10, AHMET_ANIM_IDLE_TO_WALK_FORWARD = 11, AHMET_ANIM_WALK_FORWARD_TO_IDLE = 12, AHMET_ANIM_IDLE_TO_RUN_FORWARD = 13, AHMET_ANIM_RUN_FORWARD_TO_IDLE = 14, }; // TODO enum AhmetFlags { }; static void TriggerAhmetDeathEffect(ItemInfo* item) { // HACK: Using CreatureSpheres here in release mode results in total mess-up // of LaraSpheres, which in-game appears as a ghostly Lara fire silhouette. // Later, both CreatureSpheres and LaraSpheres globals should be eradicated. static SPHERE spheres[MAX_SPHERES] = {}; if (!(Wibble & 7)) { int meshCount = GetSpheres(item, spheres, SPHERES_SPACE_WORLD, Matrix::Identity); auto sphere = &spheres[(Wibble / 8) & 1]; for (int i = meshCount; i > 0; i--, sphere += 2) TriggerFireFlame(sphere->x, sphere->y, sphere->z, FlameType::Medium); } TriggerDynamicLight( item->Pose.Position.x, item->Pose.Position.y - CLICK(1), item->Pose.Position.z, 13, (GetRandomControl() & 0x3F) - 64, (GetRandomControl() & 0x1F) + 96, 0 ); SoundEffect(SFX_TR4_LOOP_FOR_SMALL_FIRES, &item->Pose); } void InitializeAhmet(short itemNumber) { auto* item = &g_Level.Items[itemNumber]; InitializeCreature(itemNumber); SetAnimation(item, AHMET_ANIM_IDLE); item->ItemFlags[0] = item->Pose.Position.x / SECTOR(1); item->ItemFlags[1] = (item->Pose.Position.y * 4) / SECTOR(1); item->ItemFlags[2] = item->Pose.Position.z / SECTOR(1); } void AhmetControl(short itemNumber) { if (!CreatureActive(itemNumber)) return; auto* item = &g_Level.Items[itemNumber]; if (item->TriggerFlags == 1) { item->TriggerFlags = 0; return; } auto* creature = GetCreatureInfo(item); short angle = 0; auto extraHeadRot = EulerAngles::Zero; if (item->HitPoints <= 0) { if (item->Animation.ActiveState == AHMET_STATE_DEATH) { // Don't clear. if (item->Animation.FrameNumber == g_Level.Anims[item->Animation.AnimNumber].frameEnd) { item->Animation.FrameNumber = (g_Level.Anims[item->Animation.AnimNumber].frameEnd - 1); item->Collidable = false; } } else { SetAnimation(item, AHMET_ANIM_DEATH); Lara.Context.InteractedItem = itemNumber; // TODO: Check if it's really required! -- TokyoSU 3/8/2022 } TriggerAhmetDeathEffect(item); } else { if (item->AIBits != 0) // Does this entity have AI object? NOTE: Previous one checked "& ALL_AIOBJ" -- TokyoSU 3/8/2022 GetAITarget(creature); AI_INFO AI, laraAI; CreatureAIInfo(item, &AI); if (creature->Enemy->IsLara()) { laraAI.angle = AI.angle; laraAI.distance = AI.distance; } else { int dx = LaraItem->Pose.Position.x - item->Pose.Position.x; // TODO: Make ahmet to not use LaraItem global -- TokyoSU 3/8/2022 int dz = LaraItem->Pose.Position.z - item->Pose.Position.z; laraAI.angle = short(phd_atan(dx, dz)) - item->Pose.Orientation.y; laraAI.distance = SQUARE(dx) + SQUARE(dz); } GetCreatureMood(item, &AI, true); CreatureMood(item, &AI, true); angle = CreatureTurn(item, creature->MaxTurn); //creature->Enemy = LaraItem; // No need since CreatureAIInfo() set it. -- TokyoSU if (laraAI.distance < AHMET_AWARE_RANGE || item->HitStatus || TargetVisible(item, &laraAI)) { AlertAllGuards(itemNumber); } if (AI.ahead) extraHeadRot.y = AI.angle; switch (item->Animation.ActiveState) { case AHMET_STATE_IDLE: creature->MaxTurn = 0; creature->Flags = 0; if (item->AIBits & GUARD) { item->Animation.TargetState = AHMET_STATE_IDLE; extraHeadRot.y = AIGuard(creature); } else if (item->AIBits & PATROL1) { item->Animation.TargetState = AHMET_STATE_WALK_FORWARD; extraHeadRot.y = 0; } else if (creature->Mood == MoodType::Bored || creature->Mood == MoodType::Escape) { if (Lara.TargetEntity == item || !AI.ahead) // TODO: Make ahmet not use LaraInfo global. -- TokyoSU 3/8/2022 item->Animation.TargetState = AHMET_STATE_RUN_FORWARD; else item->Animation.TargetState = AHMET_STATE_IDLE; } else if (AI.bite && AI.distance < AHMET_ATTACK_RANGE) item->Animation.TargetState = AHMET_STATE_SWIPE_ATTACK; else if ((AI.angle >= AHMET_VIEW_ANGLE || AI.angle <= -AHMET_VIEW_ANGLE) || AI.distance >= AHMET_IDLE_RANGE) { if (item->Animation.RequiredState != NO_STATE) item->Animation.TargetState = item->Animation.RequiredState; else { if (!AI.ahead || AI.distance >= AHMET_RUN_RANGE) item->Animation.TargetState = AHMET_STATE_RUN_FORWARD; else item->Animation.TargetState = AHMET_STATE_WALK_FORWARD; } } else if (Random::TestProbability(1 / 2.0f)) item->Animation.TargetState = AHMET_STATE_JUMP_BITE_ATTACK; else item->Animation.TargetState = AHMET_STATE_JUMP_SWIPE_ATTACK; break; case AHMET_STATE_WALK_FORWARD: creature->MaxTurn = AHMET_WALK_FORWARD_TURN_ANGLE; if (item->AIBits & PATROL1) { item->Animation.TargetState = AHMET_STATE_WALK_FORWARD; extraHeadRot.y = 0; } else if (AI.bite && AI.distance < AHMET_IDLE_RANGE) item->Animation.TargetState = AHMET_STATE_IDLE; else if (creature->Mood == MoodType::Escape || AI.distance > AHMET_RUN_RANGE || !AI.ahead || (AI.enemyFacing > -AHMET_ENEMY_ANGLE || AI.enemyFacing < AHMET_ENEMY_ANGLE)) { item->Animation.TargetState = AHMET_STATE_RUN_FORWARD; } break; case AHMET_STATE_RUN_FORWARD: creature->MaxTurn = AHMET_RUN_FORWARD_TURN_ANGLE; creature->Flags = 0; if (item->AIBits & GUARD || ((creature->Mood == MoodType::Bored || creature->Mood == MoodType::Escape) && (Lara.TargetEntity == item && AI.ahead)) || (AI.bite && AI.distance < AHMET_IDLE_RANGE)) { item->Animation.TargetState = AHMET_STATE_IDLE; } else if (AI.ahead && AI.distance < AHMET_RUN_RANGE && (AI.enemyFacing < -AHMET_ENEMY_ANGLE || AI.enemyFacing > AHMET_ENEMY_ANGLE)) { item->Animation.TargetState = AHMET_STATE_WALK_FORWARD; } break; case AHMET_STATE_SWIPE_ATTACK: creature->MaxTurn = 0; if (abs(AI.angle) >= ANGLE(5.0f)) { if (AI.angle >= 0) item->Pose.Orientation.y += ANGLE(5.0f); else item->Pose.Orientation.y -= ANGLE(5.0f); } else item->Pose.Orientation.y += AI.angle; if (!(creature->Flags & 1) && item->Animation.FrameNumber > (g_Level.Anims[item->Animation.AnimNumber].frameBase + 7) && item->TouchBits.Test(AhmetSwipeAttackLeftJoints)) { DoDamage(creature->Enemy, AHMET_SWIPE_ATTACK_DAMAGE); CreatureEffect2(item, AhmetBiteLeft, 10, -1, DoBloodSplat); creature->Flags |= 1; } else if (!(creature->Flags & 2) && item->Animation.FrameNumber > (g_Level.Anims[item->Animation.AnimNumber].frameBase + 32) && item->TouchBits.Test(AhmetSwipeAttackRightJoints)) { DoDamage(creature->Enemy, AHMET_SWIPE_ATTACK_DAMAGE); CreatureEffect2(item, AhmetBiteRight, 10, -1, DoBloodSplat); creature->Flags |= 2; } break; case AHMET_STATE_JUMP_BITE_ATTACK: creature->MaxTurn = 0; if (item->Animation.AnimNumber == (Objects[item->ObjectNumber].animIndex + AHMET_ANIM_JUMP_SWIPE_ATTACK_START)) { if (abs(AI.angle) >= ANGLE(5.0f)) { if (AI.angle >= 0) item->Pose.Orientation.y += ANGLE(5.0f); else item->Pose.Orientation.y -= ANGLE(5.0f); } else item->Pose.Orientation.y += AI.angle; } else { if (!(creature->Flags & 1) && item->Animation.AnimNumber == (Objects[item->ObjectNumber].animIndex + AHMET_ANIM_JUMP_BITE_ATTACK_CONTINUE)) { if (item->Animation.FrameNumber > (g_Level.Anims[item->Animation.AnimNumber].frameBase + 11) && item->TouchBits.Test(AhmetSwipeAttackLeftJoints)) { DoDamage(creature->Enemy, AHMET_BITE_ATTACK_DAMAGE); CreatureEffect2(item, AhmetBiteJaw, 10, -1, DoBloodSplat); creature->Flags |= 1; } } } break; case AHMET_STATE_JUMP_SWIPE_ATTACK: creature->MaxTurn = 0; if (item->Animation.AnimNumber == (Objects[item->ObjectNumber].animIndex + AHMET_ANIM_JUMP_SWIPE_ATTACK_START)) { if (abs(AI.angle) >= ANGLE(5.0f)) { if (AI.angle >= 0) item->Pose.Orientation.y += ANGLE(5.0f); else item->Pose.Orientation.y -= ANGLE(5.0f); } else item->Pose.Orientation.y += AI.angle; } else { if (!(creature->Flags & 1) && item->Animation.FrameNumber > (g_Level.Anims[item->Animation.AnimNumber].frameBase + 14) && item->TouchBits.Test(AhmetSwipeAttackLeftJoints)) { DoDamage(creature->Enemy, AHMET_SWIPE_ATTACK_DAMAGE); CreatureEffect2(item, AhmetBiteLeft, 10, -1, DoBloodSplat); creature->Flags |= 1; } else if (!(creature->Flags & 2) && item->Animation.FrameNumber > (g_Level.Anims[item->Animation.AnimNumber].frameBase + 22) && item->TouchBits.Test(AhmetSwipeAttackRightJoints)) { DoDamage(creature->Enemy, AHMET_SWIPE_ATTACK_DAMAGE); CreatureEffect2(item, AhmetBiteRight, 10, -1, DoBloodSplat); creature->Flags |= 2; } } break; } } TestTriggers(item, true); CreatureTilt(item, 0); CreatureJoint(item, 0, extraHeadRot.y); CreatureAnimation(itemNumber, angle, 0); } bool RespawnAhmet(short itemNumber) { auto* item = &g_Level.Items[itemNumber]; if (item->Animation.ActiveState != AHMET_STATE_DEATH || item->Animation.FrameNumber != g_Level.Anims[item->Animation.AnimNumber].frameEnd) { return false; } Weather.Flash(255, 64, 0, 0.03f); item->Pose.Position.x = (item->ItemFlags[0] * SECTOR(1)) + CLICK(2); item->Pose.Position.y = (item->ItemFlags[1] * CLICK(1)); item->Pose.Position.z = (item->ItemFlags[2] * SECTOR(1)) + CLICK(2); auto outsideRoom = IsRoomOutside(item->Pose.Position.x, item->Pose.Position.y, item->Pose.Position.z); if (item->RoomNumber != outsideRoom) ItemNewRoom(itemNumber, outsideRoom); SetAnimation(item, AHMET_ANIM_IDLE); item->HitPoints = Objects[item->ObjectNumber].HitPoints; AddActiveItem(itemNumber); item->Status = ITEM_ACTIVE; item->Collidable = true; item->AfterDeath = 0; item->Flags &= ~IFLAG_INVISIBLE; EnableEntityAI(itemNumber, true); item->TriggerFlags = 1; return true; } }