#include "World/CWallCrawlerSwarm.hpp" #include "World/CActorParameters.hpp" #include "Collision/CMaterialList.hpp" #include "Graphics/CSkinnedModel.hpp" #include "Graphics/CBooRenderer.hpp" #include "World/CScriptDoor.hpp" #include "World/CWorld.hpp" #include "World/CPlayer.hpp" #include "World/CScriptWaypoint.hpp" #include "Collision/CMetroidAreaCollider.hpp" #include "Collision/CGameCollision.hpp" #include "Camera/CFirstPersonCamera.hpp" #include "Character/CSteeringBehaviors.hpp" #include "Weapon/CGameProjectile.hpp" #include "Graphics/CVertexMorphEffect.hpp" #include "GameGlobalObjects.hpp" #include "CStateManager.hpp" #include "CSimplePool.hpp" #include "World/CPhysicsActor.hpp" #include "TCastTo.hpp" namespace urde { static CMaterialList MakeMaterialList() { return CMaterialList(EMaterialTypes::Scannable, EMaterialTypes::Trigger, EMaterialTypes::NonSolidDamageable, EMaterialTypes::RadarObject); } CWallCrawlerSwarm::CWallCrawlerSwarm(TUniqueId uid, bool active, std::string_view name, const CEntityInfo& info, const zeus::CVector3f& boundingBoxExtent, const zeus::CTransform& xf, EFlavor flavor, const CAnimRes& animRes, s32 launchAnim, s32 attractAnim, CAssetId part1, CAssetId part2, CAssetId part3, CAssetId part4, const CDamageInfo& crabDamage, const CDamageInfo& scarabExplodeDamage, float crabDamageCooldown, float boidRadius, float touchRadius, float playerTouchRadius, u32 numBoids, u32 maxCreatedBoids, float animPlaybackSpeed, float separationRadius, float cohesionMagnitude, float alignmentWeight, float separationMagnitude, float moveToWaypointWeight, float attractionMagnitude, float attractionRadius, float boidGenRate, u32 maxLaunches, float scarabBoxMargin, float scarabScatterXYVelocity, float scarabTimeToExplode, const CHealthInfo& hInfo, const CDamageVulnerability& dVuln, s32 launchSfx, s32 scatterSfx, const CActorParameters& aParams) : CActor(uid, active, name, info, xf, CModelData::CModelDataNull(), MakeMaterialList(), aParams, kInvalidUniqueId) , x118_boundingBoxExtent(boundingBoxExtent) , x13c_separationRadius(separationRadius) , x140_cohesionMagnitude(cohesionMagnitude) , x144_alignmentWeight(alignmentWeight) , x148_separationMagnitude(separationMagnitude) , x14c_moveToWaypointWeight(moveToWaypointWeight) , x150_attractionMagnitude(attractionMagnitude) , x154_attractionRadius(attractionRadius) , x158_scarabScatterXYVelocity(scarabScatterXYVelocity) , x15c_scarabTimeToExplode(scarabTimeToExplode) , x160_animPlaybackSpeed(animPlaybackSpeed) , x364_boidGenRate(boidGenRate) , x370_crabDamageCooldown(crabDamageCooldown) , x374_boidRadius(boidRadius) , x378_touchRadius(touchRadius) , x37c_scarabBoxMargin(scarabBoxMargin) , x380_playerTouchRadius(playerTouchRadius) , x384_crabDamage(crabDamage) , x3a0_scarabExplodeDamage(scarabExplodeDamage) , x3bc_hInfo(hInfo) , x3c4_dVuln(dVuln) , x548_numBoids(numBoids) , x54c_maxCreatedBoids(maxCreatedBoids) , x554_maxLaunches(maxLaunches) , x558_flavor(flavor) { x168_partitionedBoidLists.resize(125); x55c_launchSfx = CSfxManager::TranslateSFXID(launchSfx != -1 ? u16(launchSfx) : u16(0xffff)); x55e_scatterSfx = CSfxManager::TranslateSFXID(scatterSfx != -1 ? u16(scatterSfx) : u16(0xffff)); x560_24_enableLighting = true; x560_25_useSoftwareLight = true; x560_26_modelAssetDirty = false; CAnimRes attractAnimRes(animRes); attractAnimRes.SetCanLoop(true); attractAnimRes.SetDefaultAnim(attractAnim != -1 ? attractAnim : 0); CAnimRes launchAnimRes(animRes); launchAnimRes.SetCanLoop(true); launchAnimRes.SetDefaultAnim(launchAnim != -1 ? launchAnim : 0); x4b0_modelDatas.emplace_back(new CModelData(animRes)); x4b0_modelDatas.emplace_back(new CModelData(animRes)); x4b0_modelDatas.emplace_back(new CModelData(animRes)); x4b0_modelDatas.emplace_back(new CModelData(animRes)); x4b0_modelDatas.emplace_back(new CModelData(attractAnimRes)); x4b0_modelDatas.emplace_back(new CModelData(attractAnimRes)); x4b0_modelDatas.emplace_back(new CModelData(attractAnimRes)); x4b0_modelDatas.emplace_back(new CModelData(attractAnimRes)); x4b0_modelDatas.emplace_back(new CModelData(launchAnimRes)); x4b0_modelDatas.emplace_back(new CModelData(animRes)); if (aParams.GetXRayAssets().first.IsValid()) { for (int i = 0; i < 9; ++i) x4b0_modelDatas[i]->SetXRayModel(aParams.GetXRayAssets()); x560_26_modelAssetDirty = true; } if (aParams.GetThermalAssets().first.IsValid()) { for (int i = 0; i < 9; ++i) x4b0_modelDatas[i]->SetXRayModel(aParams.GetThermalAssets()); x560_26_modelAssetDirty = true; } if (part1.IsValid()) x4f0_particleDescs.push_back(g_SimplePool->GetObj({FOURCC('PART'), part1})); if (part2.IsValid()) x4f0_particleDescs.push_back(g_SimplePool->GetObj({FOURCC('PART'), part2})); if (part3.IsValid()) x4f0_particleDescs.push_back(g_SimplePool->GetObj({FOURCC('PART'), part3})); if (part4.IsValid()) x4f0_particleDescs.push_back(g_SimplePool->GetObj({FOURCC('PART'), part4})); for (const auto& t : x4f0_particleDescs) { x524_particleGens.emplace_back(new CElementGen(t)); x524_particleGens.back()->SetParticleEmission(false); } } void CWallCrawlerSwarm::Accept(IVisitor& visitor) { visitor.Visit(this); } void CWallCrawlerSwarm::AllocateSkinnedModels(CStateManager& mgr, CModelData::EWhichModel which) { //x430_.clear(); for (int i = 0; i < 9; ++i) { //x430_.push_back(x4b0_[i]->PickAnimatedModel(which).Clone()); x4b0_modelDatas[i]->EnableLooping(true); x4b0_modelDatas[i]->AdvanceAnimation( x4b0_modelDatas[i]->GetAnimationData()->GetAnimTimeRemaining("Whole Body"sv) * (i * 0.0625f), mgr, x4_areaId, true); } //x430_.push_back(x4b0_.back()->PickAnimatedModel(which).Clone()); x4dc_whichModel = which; } void CWallCrawlerSwarm::AddDoorRepulsors(CStateManager& mgr) { size_t doorCount = 0; for (CEntity* ent : mgr.GetPhysicsActorObjectList()) { if (TCastToPtr door = ent) if (door->GetAreaIdAlways() == x4_areaId) ++doorCount; } x4e0_doorRepulsors.reserve(doorCount); for (CEntity* ent : mgr.GetPhysicsActorObjectList()) { if (TCastToPtr door = ent) { if (door->GetAreaIdAlways() == x4_areaId) { if (auto tb = door->GetTouchBounds()) x4e0_doorRepulsors.emplace_back(tb->center(), (tb->min - tb->max).magnitude() * 0.75f); } } } } void CWallCrawlerSwarm::AcceptScriptMsg(EScriptObjectMessage msg, TUniqueId sender, CStateManager& mgr) { CActor::AcceptScriptMsg(msg, sender, mgr); switch (msg) { case EScriptObjectMessage::Registered: x108_boids.reserve(size_t(x548_numBoids)); for (int i = 0; i < x548_numBoids; ++i) x108_boids.emplace_back(zeus::CTransform(), i); AllocateSkinnedModels(mgr, CModelData::EWhichModel::Normal); AddDoorRepulsors(mgr); CreateShadow(false); break; default: break; } } void CWallCrawlerSwarm::UpdateParticles(float dt) { for (auto& p : x524_particleGens) p->Update(dt); } int CWallCrawlerSwarm::SelectLockOnIdx(CStateManager& mgr) const { zeus::CTransform fpCamXf = mgr.GetCameraManager()->GetFirstPersonCamera()->GetTransform(); if (x42c_lockOnIdx != -1) { const CBoid& b = x108_boids[x42c_lockOnIdx]; if (b.GetActive()) { zeus::CVector3f dir = b.GetTranslation() - fpCamXf.origin; float mag = dir.magnitude(); dir = dir / mag; if (fpCamXf.basis[1].dot(dir) > 0.92388f) { if (mgr.RayStaticIntersection(fpCamXf.origin, dir, mag, CMaterialFilter::MakeInclude(EMaterialTypes::Solid)).IsInvalid()) return x42c_lockOnIdx; } } return -1; } int ret = -1; float omtd = mgr.GetPlayer().GetOrbitMaxTargetDistance(mgr); float omtdSq = omtd * omtd; float maxDot = 0.5f; for (int i = 0; i < x108_boids.size(); ++i) { const CBoid& b = x108_boids[i]; if (b.GetActive()) { zeus::CVector3f delta = b.GetTranslation() - fpCamXf.origin; if (delta.magSquared() > omtdSq) continue; if (delta.canBeNormalized()) { float thisDot = fpCamXf.basis[1].dot(delta.normalized()); if (thisDot > maxDot) { ret = i; maxDot = thisDot; } } } } return ret; } zeus::CAABox CWallCrawlerSwarm::GetBoundingBox() const { zeus::CVector3f he = x118_boundingBoxExtent * 0.75f; return zeus::CAABox(-he, he).getTransformedAABox(x34_transform); } TUniqueId CWallCrawlerSwarm::GetWaypointForState(EScriptObjectState state, CStateManager& mgr) const { for (const auto& c : GetConnectionList()) { if (c.x0_state == state && c.x4_msg == EScriptObjectMessage::Follow) return mgr.GetIdForScript(c.x8_objId); } return kInvalidUniqueId; } bool CWallCrawlerSwarm::PointOnSurface(const CCollisionSurface& surf, const zeus::CVector3f& pos, const zeus::CPlane& plane) const { zeus::CVector3f projPt = ProjectPointToPlane(pos, surf.GetVert(0), plane.normal()); for (int i = 0; i < 3; ++i) { if (plane.normal().dot((projPt - surf.GetVert(i)).cross(surf.GetVert((i + 2) % 3) - surf.GetVert(i))) < 0.f) return false; } return true; } bool CWallCrawlerSwarm::FindBestSurface(const CAreaCollisionCache& ccache, const zeus::CVector3f& pos, float radius, CCollisionSurface& out) const { bool ret = false; float radiusSq = radius * radius; zeus::CSphere sphere(pos, radius); for (const auto& c : ccache) { for (const auto& n : c) { if (CCollidableSphere::Sphere_AABox_Bool(sphere, n.GetBoundingBox())) { auto triList = n.GetTriangleArray(); for (int i = 0; i < triList.GetSize(); ++i) { CCollisionSurface surf = n.GetOwner().GetMasterListTriangle(triList.GetAt(i)); zeus::CPlane plane = surf.GetPlane(); float distSq = std::fabs(plane.pointToPlaneDist(pos)); if (distSq < radiusSq && PointOnSurface(surf, pos, plane)) { float dist = 0.f; if (distSq != 0.f) dist = std::sqrt(distSq); sphere.radius = dist; out = surf; ret = true; } } } } } return ret; } CCollisionSurface CWallCrawlerSwarm::FindBestCollisionInBox(CStateManager& mgr, const zeus::CVector3f& wpPos) const { CCollisionSurface ret(zeus::skRight, zeus::skForward, zeus::skUp, 0xffffffff); zeus::CVector3f aabbExtents = GetBoundingBox().extents(); float f25 = 0.1f; while (f25 < 1.f) { zeus::CVector3f scaledExtents = aabbExtents * f25; CAreaCollisionCache ccache(zeus::CAABox(wpPos - scaledExtents, wpPos + scaledExtents)); CGameCollision::BuildAreaCollisionCache(mgr, ccache); if (FindBestSurface(ccache, wpPos, 2.f * scaledExtents.magnitude(), ret)) return ret; f25 += 0.1f; } return ret; } static zeus::CTransform LookAt(const zeus::CUnitVector3f& a, const zeus::CUnitVector3f& b, const zeus::CRelAngle& ang) { float dot = a.dot(b); if (zeus::close_enough(dot, 1.f)) return zeus::CTransform(); if (dot > -0.99981f) return zeus::CQuaternion::clampedRotateTo(a, b, ang).toTransform(); if (a != zeus::skRight && b != zeus::skRight) return zeus::CQuaternion::fromAxisAngle(a.cross(zeus::skRight), ang).toTransform(); return zeus::CQuaternion::fromAxisAngle(a.cross(zeus::skUp), ang).toTransform(); } void CWallCrawlerSwarm::CreateBoid(CStateManager& mgr, int idx) { //zeus::CAABox aabb = GetBoundingBox(); TUniqueId wpId = GetWaypointForState(EScriptObjectState::Patrol, mgr); if (TCastToConstPtr wp = mgr.GetObjectById(wpId)) { CCollisionSurface surf = FindBestCollisionInBox(mgr, wp->GetTranslation()); x108_boids[idx].Transform() = zeus::CTransform::Translate( ProjectPointToPlane(wp->GetTranslation(), surf.GetVert(0), surf.GetNormal()) + surf.GetNormal() * x374_boidRadius); if (zeus::close_enough(zeus::skUp.dot(surf.GetNormal()), -1.f)) { x108_boids[idx].Transform().setRotation(zeus::CTransform( zeus::skRight, zeus::skBack, zeus::skDown, zeus::skZero3f)); } else { x108_boids[idx].Transform().setRotation(LookAt(zeus::skUp, surf.GetNormal(), M_PIF)); } x108_boids[idx].x80_24_active = true; x108_boids[idx].x30_velocity = zeus::skZero3f; x108_boids[idx].x3c_targetWaypoint = wpId; x108_boids[idx].x7c_framesNotOnSurface = 0; x108_boids[idx].x48_timeToDie = 0.f; x108_boids[idx].x80_27_scarabExplodeTimerEnabled = false; x108_boids[idx].x78_health = x3bc_hInfo.GetHP(); } } void CWallCrawlerSwarm::ExplodeBoid(CBoid& boid, CStateManager& mgr) { KillBoid(boid, mgr, 0.f, 1.f); mgr.ApplyDamageToWorld(GetUniqueId(), *this, boid.GetTranslation(), x3a0_scarabExplodeDamage, CMaterialFilter::MakeInclude({EMaterialTypes::Player})); } void CWallCrawlerSwarm::SetExplodeTimers(const zeus::CVector3f& pos, float radius, float minTime, float maxTime) { float radiusSq = radius * radius; float range = maxTime - minTime; for (auto& b : x108_boids) { if (b.GetActive() && b.x48_timeToDie <= 0.f) { float dist = (b.GetTranslation() - pos).magSquared(); if (dist < radiusSq) { float fac = dist / radiusSq * range + minTime; if (b.x4c_timeToExplode > fac || b.x4c_timeToExplode == 0.f) b.x4c_timeToExplode = fac; } } } } CWallCrawlerSwarm::CBoid* CWallCrawlerSwarm::GetListAt(const zeus::CVector3f& pos) { zeus::CAABox aabb = GetBoundingBox(); zeus::CVector3f ints = (pos - aabb.min) / ((aabb.max - aabb.min) / 5.f); int idx = int(ints.x()) + int(ints.y()) * 5 + int(ints.z()) * 25; if (idx < 0 || idx >= 125) return x360_outlierBoidList; return x168_partitionedBoidLists[idx]; } void CWallCrawlerSwarm::BuildBoidNearList(const CBoid& boid, float radius, rstl::reserved_vector& nearList) { CBoid* b = GetListAt(boid.GetTranslation()); while (b && nearList.size() < 50) { float distSq = (b->GetTranslation() - boid.GetTranslation()).magSquared(); if (distSq != 0.f && distSq < radius) nearList.push_back(b); b = b->x44_next; } } void CWallCrawlerSwarm::ApplySeparation(const CBoid& boid, const rstl::reserved_vector& nearList, zeus::CVector3f& aheadVec) const { if (nearList.empty()) return; float minDist = FLT_MAX; zeus::CVector3f pos; for (CBoid* b : nearList) { float dist = (boid.GetTranslation() - b->GetTranslation()).magSquared(); if (dist != 0.f && dist < minDist) { minDist = dist; pos = b->GetTranslation(); } } ApplySeparation(boid, pos, x13c_separationRadius, x148_separationMagnitude, aheadVec); } void CWallCrawlerSwarm::ApplySeparation(const CBoid& boid, const zeus::CVector3f& separateFrom, float separationRadius, float separationMagnitude, zeus::CVector3f& aheadVec) const { zeus::CVector3f delta = boid.GetTranslation() - separateFrom; if (delta.canBeNormalized()) { float deltaDistSq = delta.magSquared(); float capDeltaDistSq = separationRadius * separationRadius; if (deltaDistSq < capDeltaDistSq) aheadVec += (1.f - deltaDistSq / capDeltaDistSq) * delta.normalized() * separationMagnitude; } } void CWallCrawlerSwarm::ScatterScarabBoid(CBoid& boid, CStateManager& mgr) const { zeus::CVector3f oldDir = boid.Transform().basis[1]; boid.Transform().setRotation(zeus::CTransform()); boid.Transform() = LookAt(boid.Transform().basis[1], oldDir, M_PIF).multiplyIgnoreTranslation(boid.Transform()); boid.x30_velocity = zeus::skZero3f; float angle = mgr.GetActiveRandom()->Float() * (2.f * M_PIF); float mag = mgr.GetActiveRandom()->Float() * x158_scarabScatterXYVelocity; boid.x30_velocity.x() = mag * std::cos(angle); boid.x30_velocity.y() = mag * std::sin(angle); boid.x80_26_launched = true; boid.x7c_remainingLaunchNotOnSurfaceFrames = 5; CSfxManager::AddEmitter(x55c_launchSfx, boid.GetTranslation(), zeus::skZero3f, true, false, 0x7f, x4_areaId); } void CWallCrawlerSwarm::MoveToWayPoint(CBoid& boid, CStateManager& mgr, zeus::CVector3f& aheadVec) const { if (TCastToPtr wp = mgr.ObjectById(boid.x3c_targetWaypoint)) { CScriptWaypoint* useWp = wp.GetPtr(); if ((useWp->GetTranslation() - boid.GetTranslation()).magSquared() < x164_waypointGoalRadius * x164_waypointGoalRadius) { boid.x3c_targetWaypoint = useWp->NextWaypoint(mgr); if (boid.x3c_targetWaypoint == kInvalidUniqueId) { if (x558_flavor == EFlavor::Scarab) { ScatterScarabBoid(boid, mgr); } else { boid.x80_24_active = false; return; } } else { useWp = TCastToPtr(mgr.ObjectById(boid.x3c_targetWaypoint)).GetPtr(); } } aheadVec += (useWp->GetTranslation() - boid.GetTranslation()).normalized() * x14c_moveToWaypointWeight; } } void CWallCrawlerSwarm::ApplyCohesion(const CBoid& boid, const rstl::reserved_vector& nearList, zeus::CVector3f& aheadVec) const { if (nearList.empty()) return; zeus::CVector3f avg; for (CBoid* b : nearList) avg += b->GetTranslation(); avg = avg / float(nearList.size()); ApplyCohesion(boid, avg, x13c_separationRadius, x140_cohesionMagnitude, aheadVec); } void CWallCrawlerSwarm::ApplyCohesion(const CBoid& boid, const zeus::CVector3f& cohesionFrom, float cohesionRadius, float cohesionMagnitude, zeus::CVector3f& aheadVec) const { zeus::CVector3f delta = cohesionFrom - boid.GetTranslation(); if (delta.canBeNormalized()) { float distSq = delta.magSquared(); float capDistSq = cohesionRadius * cohesionRadius; aheadVec += ((distSq > capDistSq) ? 1.f : distSq / capDistSq) * delta.normalized() * cohesionMagnitude; } } void CWallCrawlerSwarm::ApplyAlignment(const CBoid& boid, const rstl::reserved_vector& nearList, zeus::CVector3f& aheadVec) const { if (nearList.empty()) return; zeus::CVector3f avg; for (CBoid* b : nearList) avg += b->Transform().basis[1]; avg = avg / float(nearList.size()); aheadVec += zeus::CVector3f::getAngleDiff(boid.GetTransform().basis[1], avg) / M_PIF * (avg * x144_alignmentWeight); } void CWallCrawlerSwarm::ApplyAttraction(const CBoid& boid, const zeus::CVector3f& attractTo, float attractionRadius, float attractionMagnitude, zeus::CVector3f& aheadVec) const { zeus::CVector3f delta = attractTo - boid.GetTranslation(); if (delta.canBeNormalized()) { float distSq = delta.magSquared(); float capDistSq = attractionRadius * attractionRadius; aheadVec += ((distSq > capDistSq) ? 0.f : (1.f - distSq / capDistSq)) * delta.normalized() * attractionMagnitude; } } void CWallCrawlerSwarm::UpdateBoid(const CAreaCollisionCache& ccache, CStateManager& mgr, float dt, CBoid& boid) { if (boid.x80_27_scarabExplodeTimerEnabled) { if (x558_flavor == EFlavor::Scarab && boid.x4c_timeToExplode > 0.f) { boid.x4c_timeToExplode -= 2.f * dt; if (boid.x4c_timeToExplode <= 0.f) ExplodeBoid(boid, mgr); } } else if (boid.x80_26_launched) { float radius = 2.f * x374_boidRadius; float boidMag = boid.x30_velocity.magnitude(); float f20 = boidMag * dt; zeus::CVector3f f25 = (-boid.x30_velocity / boidMag) * x374_boidRadius; zeus::CVector3f f28 = boid.GetTranslation(); bool found = false; while (f20 >= 0.f && !found) { CCollisionSurface surf(zeus::skRight, zeus::skForward, zeus::skUp, 0xffffffff); if (FindBestSurface(ccache, boid.x30_velocity * dt * 1.5f + f28, radius, surf) && boid.x7c_remainingLaunchNotOnSurfaceFrames == 0) { if (x558_flavor != EFlavor::Scarab) { boid.Transform() = LookAt(boid.Transform().basis[2], surf.GetNormal(), M_PIF).multiplyIgnoreTranslation(boid.Transform()); } auto plane = surf.GetPlane(); boid.Translation() += -(plane.pointToPlaneDist(boid.GetTranslation()) - x374_boidRadius - 0.01f) * plane.normal(); boid.x7c_framesNotOnSurface = 0; boid.x80_26_launched = false; if (x558_flavor == EFlavor::Scarab) { boid.x80_27_scarabExplodeTimerEnabled = true; boid.x4c_timeToExplode = x15c_scarabTimeToExplode; CSfxManager::AddEmitter(x55e_scatterSfx, boid.GetTranslation(), zeus::skZero3f, true, false, 0x7f, x4_areaId); } found = true; } f20 -= x374_boidRadius; f28 += f25; } if (!found) { boid.x30_velocity += zeus::CVector3f(0.f, 0.f, -(x558_flavor == EFlavor::Scarab ? 3.f * CPhysicsActor::GravityConstant() : CPhysicsActor::GravityConstant())) * dt; if (boid.x7c_remainingLaunchNotOnSurfaceFrames) boid.x7c_remainingLaunchNotOnSurfaceFrames -= 1; } } else if (boid.x7c_framesNotOnSurface >= 30) { boid.x80_24_active = false; } else { float radius = 2.f * x374_boidRadius; bool found = false; CCollisionSurface surf(zeus::skRight, zeus::skForward, zeus::skUp, 0xffffffff); if (FindBestSurface(ccache, boid.GetTranslation() + boid.x30_velocity * dt * 1.5f, radius, surf)) { boid.x50_surface = surf; boid.Transform() = LookAt(boid.Transform().basis[2], surf.GetNormal(), zeus::degToRad(180.f * dt)). multiplyIgnoreTranslation(boid.Transform()); auto plane = surf.GetPlane(); float dist = plane.pointToPlaneDist(boid.GetTranslation()); if (dist <= 1.5f * x374_boidRadius) { boid.Translation() += -(dist - x374_boidRadius - 0.01f) * plane.normal(); boid.x7c_framesNotOnSurface = 0; found = true; } } if (!found) { boid.Transform() = LookAt(boid.Transform().basis[2], boid.Transform().basis[1], boid.x30_velocity.magnitude() / x374_boidRadius * dt). multiplyIgnoreTranslation(boid.Transform()); boid.x7c_framesNotOnSurface += 1; } rstl::reserved_vector nearList; BuildBoidNearList(boid, x13c_separationRadius, nearList); zeus::CVector3f aheadVec = boid.Transform().basis[1] * 0.3f; for (int r26 = 0; r26 < 8; ++r26) { switch (r26) { case 0: for (auto& rep : x4e0_doorRepulsors) { if ((rep.x0_center - boid.GetTranslation()).magSquared() < rep.xc_mag * rep.xc_mag) ApplySeparation(boid, rep.x0_center, rep.xc_mag, 4.5f, aheadVec); } break; case 4: ApplySeparation(boid, nearList, aheadVec); break; case 5: MoveToWayPoint(boid, mgr, aheadVec); break; case 6: ApplyCohesion(boid, nearList, aheadVec); break; case 7: ApplyAlignment(boid, nearList, aheadVec); break; case 3: ApplyAttraction(boid, mgr.GetPlayer().GetTranslation(), x154_attractionRadius, x150_attractionMagnitude, aheadVec); break; default: break; } if (aheadVec.magSquared() >= 9.f) break; } boid.Transform() = LookAt(boid.Transform().basis[1], ProjectVectorToPlane(aheadVec, boid.Transform().basis[2]).normalized(), M_PIF * dt). multiplyIgnoreTranslation(boid.Transform()); } } void CWallCrawlerSwarm::LaunchBoid(CBoid& boid, const zeus::CVector3f& dir) { zeus::CVector3f pos = boid.GetTranslation(); static float skAttackTime = std::sqrt(2.5f / CPhysicsActor::GravityConstant()) * 2.f; static float skAttackVelocity = 15.f / skAttackTime; zeus::CVector3f deltaFlat = dir - pos; float deltaZ = deltaFlat.z(); deltaFlat.z() = 0.f; float deltaMag = deltaFlat.magnitude(); boid.Transform().setRotation(zeus::CTransform()); boid.Transform() = LookAt(boid.Transform().basis[1], deltaFlat.normalized(), M_PIF).multiplyIgnoreTranslation(boid.Transform()); zeus::CVector3f vec(skAttackVelocity * boid.Transform().basis[1].toVec2f(), 0.5f * skAttackVelocity); if (deltaMag > FLT_EPSILON) { deltaFlat = deltaFlat / deltaMag; float dot = deltaFlat.dot(vec); if (dot > FLT_EPSILON) { bool r29 = deltaZ < 0.f; float _12c, _130; float f25 = 0.f; if (CSteeringBehaviors::SolveQuadratic(-CPhysicsActor::GravityConstant(), vec.z(), -deltaZ, _12c, _130)) f25 = r29 ? _130 : _12c; if (!r29) f25 += deltaMag / dot; if (f25 < 10.f) { vec.x() = deltaMag / f25 * deltaFlat.x() * 0.6f; vec.y() = deltaMag / f25 * deltaFlat.y() * 0.6f; vec.z() = deltaZ / f25 - 0.5f * -CPhysicsActor::GravityConstant() * f25; } } } boid.x30_velocity = vec; boid.x80_26_launched = true; boid.x7c_remainingLaunchNotOnSurfaceFrames = 1; CSfxManager::AddEmitter(x55c_launchSfx, pos, zeus::skZero3f, true, false, 0x7f, x4_areaId); } static const int kParticleCounts[] = {8, 2, 0, 0}; void CWallCrawlerSwarm::AddParticle(const zeus::CTransform& xf) { int i = 0; for (auto& p : x524_particleGens) { p->SetParticleEmission(true); p->SetTranslation(xf.origin); p->ForceParticleCreation(kParticleCounts[i]); p->SetParticleEmission(false); ++i; } } void CWallCrawlerSwarm::KillBoid(CBoid& boid, CStateManager& mgr, float deathRattleChance, float deadChance) { x130_lastKilledOffset = boid.GetTranslation(); AddParticle(boid.Transform()); boid.x80_24_active = false; float sendDeadRoll = mgr.GetActiveRandom()->Float(); float sendDeathRattleRoll = mgr.GetActiveRandom()->Float(); if (sendDeathRattleRoll < deathRattleChance) SendScriptMsgs(EScriptObjectState::DeathRattle, mgr, EScriptObjectMessage::None); if (sendDeadRoll < deadChance) SendScriptMsgs(EScriptObjectState::Dead, mgr, EScriptObjectMessage::None); } void CWallCrawlerSwarm::UpdatePartition() { x168_partitionedBoidLists.clear(); x168_partitionedBoidLists.resize(125); x360_outlierBoidList = nullptr; zeus::CAABox aabb = GetBoundingBox(); zeus::CVector3f vec = (aabb.max - aabb.min) / 5.f; for (auto& b : x108_boids) { if (b.GetActive()) { zeus::CVector3f divVec = (b.Translation() - aabb.min) / vec; int xIdx = int(divVec.x()); int yIdx = int(divVec.y()); int zIdx = int(divVec.z()); int idx = xIdx + yIdx * 5 + zIdx * 25; if (idx < 0 || idx >= 125 || xIdx < 0 || xIdx >= 5 || yIdx < 0 || yIdx >= 5 || zIdx < 0 || zIdx >= 5) { b.x44_next = x360_outlierBoidList; x360_outlierBoidList = &b; } else { b.x44_next = x168_partitionedBoidLists[idx]; x168_partitionedBoidLists[idx] = &b; } } } } zeus::CVector3f CWallCrawlerSwarm::FindClosestCell(const zeus::CVector3f& pos) const { float minDist = FLT_MAX; zeus::CVector3f ret; for (int r28 = 0; r28 < 5; ++r28) { for (int r29 = 0; r29 < 5; ++r29) { for (int r25 = 0; r25 < 5; ++r25) { zeus::CAABox aabb = BoxForPosition(r28, r29, r25, 0.1f); float dist = (aabb.center() - pos).magSquared(); if (dist < minDist) { minDist = dist; ret = aabb.center(); } } } } return ret; } void CWallCrawlerSwarm::UpdateEffects(CStateManager& mgr, CAnimData& aData, int vol) { if (aData.GetPassedSoundPOICount() > 0 && !CAnimData::g_SoundPOINodes.empty()) { for (int i = 0; i < aData.GetPassedSoundPOICount(); ++i) { const CSoundPOINode& n = CAnimData::g_SoundPOINodes[i]; if (n.GetPoiType() == EPOIType::Sound && (n.GetCharacterIndex() == -1 || n.GetCharacterIndex() == aData.GetCharacterIndex())) { u16 sfx = CSfxManager::TranslateSFXID(u16(n.GetSfxId() & 0xffff)); bool loop = bool(n.GetSfxId() >> 31); if (!loop) { CAudioSys::C3DEmitterParmData parmData; parmData.x0_pos = FindClosestCell(mgr.GetPlayer().GetTranslation()); static float maxDist = n.GetMaxDist(); static float falloff = n.GetFalloff(); parmData.x18_maxDist = maxDist; parmData.x1c_distComp = falloff; parmData.x20_flags = 0x1; parmData.x24_sfxId = sfx; parmData.x26_maxVol = zeus::clamp(0, vol, 127) / 127.f; parmData.x27_minVol = 20.f / 127.f; parmData.x28_important = false; parmData.x29_prio = 0x7f; CSfxManager::AddEmitter(parmData, true, 0x7f, false, x4_areaId); } } } } } zeus::CAABox CWallCrawlerSwarm::BoxForPosition(int x, int y, int z, float f) const { zeus::CAABox aabb = GetBoundingBox(); zeus::CVector3f vec = (aabb.max - aabb.min) / 5.f; return zeus::CAABox(zeus::CVector3f(x, y, z) * vec + aabb.min - f, zeus::CVector3f(x + 1, y + 1, z + 1) * vec + aabb.min + f); } void CWallCrawlerSwarm::Think(float dt, CStateManager& mgr) { if (!GetActive()) return; if (x560_26_modelAssetDirty && CModelData::GetRenderingModel(mgr) != x4dc_whichModel) { auto which = CModelData::GetRenderingModel(mgr); if (which != x4dc_whichModel) AllocateSkinnedModels(mgr, which); } xe4_27_notInSortedLists = true; x368_boidGenCooldownTimer -= dt; x36c_crabDamageCooldownTimer -= dt; ++x100_thinkCounter; const CGameArea* area = mgr.GetWorld()->GetAreaAlways(x4_areaId); auto occState = area->IsPostConstructed() ? area->GetOcclusionState() : CGameArea::EOcclusionState::Occluded; if (occState != CGameArea::EOcclusionState::Visible) { if (x104_occludedTimer > 0.f) x104_occludedTimer -= dt; if (x104_occludedTimer <= 0.f) return; if (x100_thinkCounter & 0x2) return; } else { x104_occludedTimer = 7.f; } UpdateParticles(dt); x42c_lockOnIdx = SelectLockOnIdx(mgr); xe7_31_targetable = x42c_lockOnIdx != -1; if (x42c_lockOnIdx == -1) RemoveMaterial(EMaterialTypes::Target, EMaterialTypes::Orbit, mgr); else AddMaterial(EMaterialTypes::Target, EMaterialTypes::Orbit, mgr); while ((x54c_maxCreatedBoids == 0 || x550_createdBoids < x54c_maxCreatedBoids) && x368_boidGenCooldownTimer <= 0.f) { int idx = 0; bool madeBoid = false; for (auto& b : x108_boids) { if (!b.GetActive()) { CreateBoid(mgr, idx); x550_createdBoids += 1; x368_boidGenCooldownTimer += 1.f / x364_boidGenRate; madeBoid = true; break; } ++idx; } if (!madeBoid) { x368_boidGenCooldownTimer += 1.f / x364_boidGenRate; break; } } UpdatePartition(); xe8_aabox = GetBoundingBox(); int r21 = 0; for (int r26 = 0; r26 < 5; ++r26) { for (int r27 = 0; r27 < 5; ++r27) { for (int r20 = 0; r20 < 5; ++r20) { int idx = r20 * 25 + r27 * 5 + r26; if (CBoid* boid = x168_partitionedBoidLists[idx]) { zeus::CAABox aabb = BoxForPosition(r26, r27, r20, x374_boidRadius + 0.5f); CAreaCollisionCache ccache(aabb); CGameCollision::BuildAreaCollisionCache(mgr, ccache); while (boid) { r21 += 1; if (boid->GetActive()) { if (x558_flavor == EFlavor::Scarab) { xe8_aabox.accumulateBounds(boid->Translation() + x37c_scarabBoxMargin); xe8_aabox.accumulateBounds(boid->Translation() - x37c_scarabBoxMargin); } else { xe8_aabox.accumulateBounds(boid->Translation()); } } if (((x100_thinkCounter & 0x1) == (r21 & 0x1) && boid->GetActive() && boid->x48_timeToDie < 0.1f) || boid->x80_26_launched) UpdateBoid(ccache, mgr, dt, *boid); boid = boid->x44_next; } } } } } for (CBoid* boid = x360_outlierBoidList; boid; boid = boid->x44_next) { r21 += 1; if (boid->GetActive()) xe8_aabox.accumulateBounds(boid->Translation()); if (((x100_thinkCounter & 0x1) == (r21 & 0x1) && boid->GetActive() && boid->x48_timeToDie < 0.1f) || boid->x80_26_launched) { float margin = 1.5f + x374_boidRadius + 0.5f; zeus::CAABox aabb(boid->Translation() - margin, boid->Translation() + margin); CAreaCollisionCache ccache(aabb); CGameCollision::BuildAreaCollisionCache(mgr, ccache); UpdateBoid(ccache, mgr, dt, *boid); } } x4b0_modelDatas[8]->GetAnimationData()->SetPlaybackRate(x160_animPlaybackSpeed); x4b0_modelDatas[8]->AdvanceAnimation(dt, mgr, x4_areaId, true); SAdvancementDeltas deltas1, deltas2; int r9 = 0; int r3 = 0; int r8 = 0; bool _38F8[4] = {}; bool _38F4[4] = {}; for (const auto& b : x108_boids) { if (b.GetActive() && !b.x80_26_launched) { if (b.x80_27_scarabExplodeTimerEnabled || b.x80_28_nearPlayer) { _38F8[r9 & 0x3] = true; ++r3; } else { _38F4[r9 & 0x3] = true; ++r8; } } ++r9; } for (int i = 0; i < 4; ++i) { x4b0_modelDatas[i]->GetAnimationData()->SetPlaybackRate(x160_animPlaybackSpeed); deltas1 = x4b0_modelDatas[i]->AdvanceAnimation(dt, mgr, x4_areaId, true); x4b0_modelDatas[i+4]->GetAnimationData()->SetPlaybackRate(x160_animPlaybackSpeed); deltas2 = x4b0_modelDatas[i+4]->AdvanceAnimation(dt, mgr, x4_areaId, true); if (x4b0_modelDatas[i]->HasAnimData() && _38F4[i]) UpdateEffects(mgr, *x4b0_modelDatas[i]->GetAnimationData(), r8 * 44 / x548_numBoids + 0x53); if (x4b0_modelDatas[i+4]->HasAnimData() && _38F8[i]) UpdateEffects(mgr, *x4b0_modelDatas[i+4]->GetAnimationData(), r3 * 44 / x548_numBoids + 0x53); for (int r20 = i; r20 < x108_boids.size(); r20 += 4) { CBoid& b = x108_boids[r20]; if (b.GetActive()) { if (b.x80_26_launched) { b.Translation() += b.x30_velocity * dt; } else if (b.x48_timeToDie > 0.f) { b.x48_timeToDie -= dt; if (b.x48_timeToDie < 0.7f * mgr.GetActiveRandom()->Float()) KillBoid(b, mgr, 1.f, 0.05f); } else if (b.x80_27_scarabExplodeTimerEnabled || b.x80_28_nearPlayer) { b.x30_velocity = b.Transform().rotate(deltas2.x0_posDelta) * 1.5f / dt; b.Translation() += b.x30_velocity * dt; } else { b.x30_velocity = b.Transform().rotate(deltas1.x0_posDelta) * 1.5f / dt; b.Translation() += b.x30_velocity * dt; } } } } if (x558_flavor == EFlavor::Crab) { zeus::CVector3f playerPos = mgr.GetPlayer().GetTranslation(); for (auto& b : x108_boids) { if (b.GetActive() && zeus::close_enough(b.x48_timeToDie, 0.f) && !b.x80_26_launched) b.x80_28_nearPlayer = (playerPos - b.Translation()).magnitude() < x154_attractionRadius; } } if (x558_flavor == EFlavor::Parasite && x554_maxLaunches > 0) { zeus::CVector3f _383c = mgr.GetPlayer().GetTranslation() + zeus::skUp; static const CMaterialFilter filter = CMaterialFilter::MakeInclude(EMaterialTypes::Solid); int numLaunched = 0; for (auto& b : x108_boids) { if (b.GetActive() && b.x80_26_launched) ++numLaunched; } for (auto it = x108_boids.begin(); it != x108_boids.end() && numLaunched < x554_maxLaunches; ++it) { CBoid& b = *it; if (b.GetActive() && zeus::close_enough(b.x48_timeToDie, 0.f) && !b.x80_26_launched && (b.Translation() - _383c).magSquared() < 18.f * 18.f && mgr.GetActiveRandom()->Float() <= 0.02f) { zeus::CVector3f dir = _383c - b.Translation(); float mag = dir.magnitude(); dir = dir / mag; if (mgr.RayStaticIntersection(b.Translation(), dir, mag, filter).IsInvalid()) { LaunchBoid(b, _383c); ++numLaunched; } } } } } void CWallCrawlerSwarm::PreRender(CStateManager& mgr, const zeus::CFrustum& frustum) { for (int i = 0; i < 5; ++i) x4b0_modelDatas[i]->GetAnimationData()->PreRender(); bool activeBoid = false; for (auto& b : x108_boids) { if (b.GetActive()) { b.x80_25_inFrustum = frustum.sphereFrustumTest(zeus::CSphere(b.GetTranslation(), 2.f * x374_boidRadius)); activeBoid = true; } else { b.x80_25_inFrustum = false; } } xe4_30_outOfFrustum = !activeBoid; } void CWallCrawlerSwarm::RenderParticles() const { for (const auto& p : x524_particleGens) g_Renderer->AddParticleGen(*p); } void CWallCrawlerSwarm::AddToRenderer(const zeus::CFrustum&, const CStateManager& mgr) const { if (GetActive()) { RenderParticles(); if (!xe4_30_outOfFrustum) { if (CanRenderUnsorted(mgr)) Render(mgr); else EnsureRendered(mgr); } } } zeus::CColor CWallCrawlerSwarm::SoftwareLight(const CStateManager& mgr, const zeus::CAABox& aabb) const { CActorLights lights(8, zeus::skZero3f, 4, 4, false, false, false, 0.1f); lights.SetDirty(); lights.SetCastShadows(false); lights.SetFindShadowLight(false); lights.BuildAreaLightList(mgr, *mgr.GetWorld()->GetAreaAlways(x4_areaId), aabb); lights.BuildDynamicLightList(mgr, aabb); zeus::CColor ret = lights.GetAmbientColor(); ret.a() = 1.f; zeus::CVector3f center = aabb.center(); u32 lightCount = lights.GetActiveLightCount(); for (u32 i = 0; i < lightCount; ++i) { const CLight& light = lights.GetLight(i); float dist = (light.GetPosition() - center).magnitude(); float att = 1.f / (dist * dist * light.GetAttenuationQuadratic() + dist * light.GetAttenuationLinear() + light.GetAttenuationConstant()); ret += zeus::CColor::lerp(zeus::skBlack, light.GetColor(), 0.8f * std::min(att, 1.f)); } return ret; } void CWallCrawlerSwarm::HardwareLight(const CStateManager& mgr, const zeus::CAABox& aabb) const { CActorLights lights(8, zeus::skZero3f, 4, 4, false, false, false, 0.1f); lights.SetDirty(); lights.SetCastShadows(false); lights.SetFindShadowLight(false); lights.BuildAreaLightList(mgr, *mgr.GetWorld()->GetAreaAlways(x4_areaId), aabb); lights.BuildDynamicLightList(mgr, aabb); for (auto& m : x4b0_modelDatas) { lights.ActivateLights(*m->PickAnimatedModel(x4dc_whichModel).GetModelInst()); if (auto iceModel = m->GetAnimationData()->GetIceModel()) lights.ActivateLights(*iceModel->GetModelInst()); } } void CWallCrawlerSwarm::RenderBoid(const CBoid* boid, u32& drawMask, bool thermalHot, const CModelFlags& flags) const { u32 modelIndex = 0x0; if (boid->x80_26_launched) modelIndex = 0x8; else if (boid->x48_timeToDie > 0.f) modelIndex = 0x9; else if (boid->x80_27_scarabExplodeTimerEnabled || boid->x80_28_nearPlayer) modelIndex += 0x4; CModelData& mData = *x4b0_modelDatas[modelIndex]; CSkinnedModel& model = mData.PickAnimatedModel(x4dc_whichModel); if (!model.GetModelInst()->TryLockTextures()) return; u32 thisDrawMask = 1u << modelIndex; if (drawMask & thisDrawMask) { drawMask &= ~thisDrawMask; mData.GetAnimationData()->BuildPose(); } model.GetModelInst()->SetAmbientColor(boid->x40_ambientLighting); CGraphics::SetModelMatrix(boid->GetTransform()); if (boid->x48_timeToDie > 0.f && !thermalHot) { CModelFlags useFlags(0, 0, 3, zeus::skWhite); mData.GetAnimationData()->Render(model, useFlags, {}, nullptr); if (auto iceModel = mData.GetAnimationData()->GetIceModel()) { if (!iceModel->GetModelInst()->TryLockTextures()) return; iceModel->GetModelInst()->SetAmbientColor(zeus::skWhite); float alpha = 1.f - boid->x48_timeToDie; zeus::CColor color(1.f, alpha > 0.f ? boid->x48_timeToDie : 1.f); CModelFlags iceFlags(5, 0, 3, color); mData.GetAnimationData()->Render(*iceModel, iceFlags, {}, nullptr); } } else if (thermalHot) { CModelFlags thermFlags(5, 0, 3, zeus::skWhite); mData.RenderThermal(zeus::skWhite, zeus::CColor(0.f, 0.25f), thermFlags); } else { mData.GetAnimationData()->Render(model, flags, {}, nullptr); } } void CWallCrawlerSwarm::Render(const CStateManager& mgr) const { SCOPED_GRAPHICS_DEBUG_GROUP(fmt::format(fmt("CWallCrawlerSwarm::Render {} {} {}"), x8_uid, xc_editorId, x10_name).c_str(), zeus::skOrange); u32 drawMask = 0xffffffff; bool r24 = x560_24_enableLighting; bool r23 = x560_25_useSoftwareLight; if (!r24) { // Ambient 50% grey } bool thermalHot = mgr.GetThermalDrawFlag() == EThermalDrawFlag::Hot; CModelFlags flags(0, 0, 3, zeus::skWhite); if (mgr.GetPlayerState()->GetActiveVisor(mgr) == CPlayerState::EPlayerVisor::XRay) flags = CModelFlags(5, 0, 3, zeus::CColor(1.f, 0.3f)); for (int r27 = 0; r27 < 5; ++r27) { for (int r28 = 0; r28 < 5; ++r28) { for (int r21 = 0; r21 < 5; ++r21) { int idx = r21 * 25 + r28 * 5 + r27; if (CBoid* b = x168_partitionedBoidLists[idx]) { if (r24) { zeus::CAABox aabb = BoxForPosition(r27, r28, r21, 0.f); if (r23) { if ((idx & 0x3) == (x100_thinkCounter & 0x3)) { zeus::CColor color = SoftwareLight(mgr, aabb); for (CBoid* b2 = b; b2; b2 = b2->x44_next) { if (b2->GetActive()) b2->x40_ambientLighting = zeus::CColor::lerp(b2->x40_ambientLighting, color, 0.3f); } } } else { HardwareLight(mgr, aabb); } } for (CBoid* b2 = b; b2; b2 = b2->x44_next) { if (b2->x80_25_inFrustum && b2->GetActive()) RenderBoid(b2, drawMask, thermalHot, flags); } } } } } CBoid* b = x360_outlierBoidList; for (int i = 1; b; ++i, b = b->x44_next) { if (b->x80_25_inFrustum && b->GetActive()) { if (r24) { zeus::CAABox aabb(b->GetTranslation() - x374_boidRadius, b->GetTranslation() + x374_boidRadius); if (r23) { if ((i & 0x3) == (x100_thinkCounter & 0x3)) { zeus::CColor color = SoftwareLight(mgr, aabb); if (b->GetActive()) b->x40_ambientLighting = zeus::CColor::lerp(b->x40_ambientLighting, color, 0.3f); } } else { HardwareLight(mgr, aabb); } } RenderBoid(b, drawMask, thermalHot, flags); } } DrawTouchBounds(); } bool CWallCrawlerSwarm::CanRenderUnsorted(const CStateManager&) const { return true; } void CWallCrawlerSwarm::CalculateRenderBounds() { x9c_renderBounds = GetBoundingBox(); } std::optional CWallCrawlerSwarm::GetTouchBounds() const { return {xe8_aabox}; } void CWallCrawlerSwarm::Touch(CActor& other, CStateManager& mgr) { CActor::Touch(other, mgr); if (TCastToPtr proj = other) { if (x3c4_dVuln.WeaponHurts(proj->GetDamageInfo().GetWeaponMode(), false)) { if (auto projTb = proj->GetTouchBounds()) { float f0 = 0.1f + x378_touchRadius; float f30 = f0 * f0; for (auto& b : x108_boids) { if (b.GetActive()) { zeus::CAABox aabb(b.GetTranslation() - f30, b.GetTranslation() + f30); if (aabb.intersects(*projTb)) { b.x78_health -= proj->GetDamageInfo().GetDamage(x3c4_dVuln); if (b.x78_health <= 0.f) KillBoid(b, mgr, 1.f, 0.1f); } } } } } } if (TCastToPtr player = other) { float radius = zeus::close_enough(x380_playerTouchRadius, 0.f) ? x378_touchRadius : x380_playerTouchRadius; if (auto playerTb = player->GetTouchBounds()) { for (auto& b : x108_boids) { if (b.GetActive() && b.x48_timeToDie <= 0.f) { if (x558_flavor == EFlavor::Scarab && b.x80_27_scarabExplodeTimerEnabled) { zeus::CAABox aabb(b.GetTranslation() - x37c_scarabBoxMargin, b.GetTranslation() + x37c_scarabBoxMargin); if (playerTb->intersects(aabb)) { ExplodeBoid(b, mgr); SetExplodeTimers(b.GetTranslation(), 0.5f, 0.5f, 2.5f); } } zeus::CAABox aabb(b.GetTranslation() - radius, b.GetTranslation() + radius); if (playerTb->intersects(aabb)) { if (b.GetActive() && x558_flavor == EFlavor::Parasite) { CDamageInfo dInfo(CWeaponMode(EWeaponType::AI), 2.0e-05f, 0.f, 0.f); mgr.ApplyDamage(GetUniqueId(), player->GetUniqueId(), GetUniqueId(), dInfo, CMaterialFilter::MakeIncludeExclude({EMaterialTypes::Solid}, {}), zeus::skZero3f); KillBoid(b, mgr, 0.f, 1.f); } else if (x558_flavor == EFlavor::Scarab) { ExplodeBoid(b, mgr); } else if (x36c_crabDamageCooldownTimer <= 0.f) { mgr.ApplyDamage(GetUniqueId(), player->GetUniqueId(), GetUniqueId(), x384_crabDamage, CMaterialFilter::MakeIncludeExclude({EMaterialTypes::Solid}, {}), zeus::skZero3f); x36c_crabDamageCooldownTimer = x370_crabDamageCooldown; break; } } } } } } } zeus::CVector3f CWallCrawlerSwarm::GetOrbitPosition(const CStateManager&) const { if (x42c_lockOnIdx == -1) return x124_lastOrbitPosition; x124_lastOrbitPosition = x108_boids[x42c_lockOnIdx].GetTranslation(); return x124_lastOrbitPosition; } zeus::CVector3f CWallCrawlerSwarm::GetAimPosition(const CStateManager&, float dt) const { if (x42c_lockOnIdx == -1) return x124_lastOrbitPosition; return x108_boids[x42c_lockOnIdx].x30_velocity * dt + x124_lastOrbitPosition; } } // namespace urde