diff --git a/trinity/CMakeLists.txt b/trinity/CMakeLists.txt index cbcebdd1b..57d8785ed 100644 --- a/trinity/CMakeLists.txt +++ b/trinity/CMakeLists.txt @@ -572,6 +572,9 @@ set(_SOURCES Eve/SpaceObject/Children/EveChildSpherePin_Blue.cpp Eve/SpaceObject/Children/EveChildTransform.cpp Eve/SpaceObject/Children/EveChildTransform.cpp + Eve/SpaceObject/Children/EveChildTurret.cpp + Eve/SpaceObject/Children/EveChildTurret.h + Eve/SpaceObject/Children/EveChildTurret_Blue.cpp Eve/SpaceObject/Children/EveCloudEditableVolume.cpp Eve/SpaceObject/Children/EveCloudEditableVolume.h Eve/SpaceObject/Children/EveCloudEditableVolume_Blue.cpp @@ -691,6 +694,8 @@ set(_SOURCES Eve/SpaceObjectFactory/EveSOFDNA_Blue.cpp Eve/SpaceObjectFactory/EveSOFUtils.cpp Eve/SpaceObjectFactory/EveSOFUtils.h + Eve/Turret/EveTurretAiming.cpp + Eve/Turret/EveTurretAiming.h Eve/Turret/EveTurretFiringFX.cpp Eve/Turret/EveTurretFiringFX.h Eve/Turret/EveTurretFiringFX_Blue.cpp @@ -845,6 +850,7 @@ set(_SOURCES Include/ITr2InstanceData.h Include/ITr2Interior.h Include/ITr2MultiPassScene.h + Include/ITr2PoseModifier.h Include/ITr2Scene.h Include/ITr2Updateable.h Include/ITr2ValueBinding.h diff --git a/trinity/Eve/SpaceObject/Children/EveChildMesh.h b/trinity/Eve/SpaceObject/Children/EveChildMesh.h index ca9411a7c..96c7efc33 100644 --- a/trinity/Eve/SpaceObject/Children/EveChildMesh.h +++ b/trinity/Eve/SpaceObject/Children/EveChildMesh.h @@ -83,8 +83,8 @@ BLUE_CLASS( EveChildMesh ) : void UpdateVisibility( const EveUpdateContext& updateContext, const Matrix& parentTransform, Tr2Lod parentLod ); void GetRenderables( std::vector & renderables ); bool GetBoundingSphere( Vector4 & sphere, BoundingSphereQuery query = EVE_BOUNDS_NORMAL ) const; - void UpdateSyncronous( const EveUpdateContext& updateContext, const EveChildUpdateParams& params ); - void UpdateAsyncronous( const EveUpdateContext& updateContext, const EveChildUpdateParams& params ); + void UpdateSyncronous( const EveUpdateContext& updateContext, const EveChildUpdateParams& params ) override; + void UpdateAsyncronous( const EveUpdateContext& updateContext, const EveChildUpdateParams& params ) override; void GetLocalToWorldTransform( Matrix & transform ) const; void ChangeLOD( Tr2Lod lod ) override; virtual void Setup( const Vector3* scale, const Quaternion* rotation, const Vector3* translation, Tr2Lod lowestLodVisible ); @@ -210,7 +210,7 @@ BLUE_CLASS( EveChildMesh ) : virtual void ReleaseResources( TriStorage s ); virtual bool OnPrepareResources(); - void InitializeAnimation(); + virtual void InitializeAnimation(); bool ShouldReflect() const; bool DisplayDecals() const; diff --git a/trinity/Eve/SpaceObject/Children/EveChildTurret.cpp b/trinity/Eve/SpaceObject/Children/EveChildTurret.cpp new file mode 100644 index 000000000..f99ba66db --- /dev/null +++ b/trinity/Eve/SpaceObject/Children/EveChildTurret.cpp @@ -0,0 +1,859 @@ +// Copyright © 2026 Fenris Creations ehf. + +#include "StdAfx.h" +#include "EveChildTurret.h" +#include "Eve/Turret/EveTurretFiringFX.h" +#include "Tr2GrannyAnimation.h" +#include "Tr2MeshBase.h" +#include "TriMath.h" +#include "TriObserverLocal.h" + +// invalids +constexpr unsigned int INVALID_BONE_INDEX = 0xffffffff; +constexpr unsigned int INVALID_TURRET_INDEX = 0xffffffff; + +// some very static timings, no need to confuse artists by exposing them +constexpr float TRACKING_FADE_TIME = 1.f; + +EveChildTurret::EveChildTurret( IRoot* lockobj ) : + EveChildMesh( lockobj ) +{ + for( unsigned int i = 0; i < SYSBONE_MAX; ++i ) + { + m_systemBoneID[i] = INVALID_BONE_INDEX; + } + + m_target.CreateInstance(); + m_target->SetFadeOnLocatorChange( true ); + + PrepareResources(); +} + +EveChildTurret::~EveChildTurret() +{ + if( m_hookedUpdater && m_hookedUpdater->GetPoseModifier() == this ) + { + m_hookedUpdater->SetPoseModifier( nullptr ); + } + + if( m_firingEffect ) + { + m_firingEffect->CleanUp(); + } + + ReleaseCachedGeometryData(); + + ReleaseResources( TRISTORAGE_ALL ); +} +bool EveChildTurret::Initialize() +{ + // pass down some user-defined data into submodules we don't save out. + m_target->SetImpactBehaviour( m_impactSize, m_impactBehaviour ); + + // an inline-authored firingEffect wins over the res path at load time + if( !m_firingEffect && !m_firingEffectResPath.empty() ) + { + SetFiringEffect( BeResMan->LoadObject( m_firingEffectResPath.c_str() ).p ); + } + + return EveChildMesh::Initialize(); +} +bool EveChildTurret::OnModified( Be::Var* value ) +{ + if( IsMatch( value, m_impactSize ) || IsMatch( value, m_impactBehaviour ) ) + { + m_target->SetImpactBehaviour( m_impactSize, m_impactBehaviour ); + } + if( IsMatch( value, m_firingEffectResPath ) && !m_firingEffectResPath.empty() ) + { + SetFiringEffect( BeResMan->LoadObject( m_firingEffectResPath.c_str() ).p ); + } + return EveChildMesh::OnModified( value ); +} +void EveChildTurret::RegisterComponents() +{ + EveChildMesh::RegisterComponents(); + const auto registry = GetComponentRegistry(); + if( registry && m_display ) + { + if( EveEntityPtr entity = BlueCastPtr( m_firingEffect ) ) + { + entity->Register( registry ); + } + } +} +void EveChildTurret::UnRegisterComponents() +{ + EveChildMesh::UnRegisterComponents(); + const auto registry = this->GetComponentRegistry(); + if( registry ) + { + if( EveEntityPtr entity = BlueCastPtr( m_firingEffect ) ) + { + entity->UnRegister( registry ); + } + } +} +void EveChildTurret::GetDebugOptions( Tr2DebugRendererOptions& options ) +{ + EveChildMesh::GetDebugOptions( options ); + + if( m_firingEffect ) + { + m_firingEffect->GetDebugOptions( options ); + } + + if( m_turretMovementObserver ) + { + m_turretMovementObserver->GetDebugOptions( options ); + } +} +void EveChildTurret::RenderDebugInfo( ITr2DebugRenderer2& renderer ) +{ + EveChildMesh::RenderDebugInfo( renderer ); + + if( m_firingEffect ) + { + m_firingEffect->RenderDebugInfo( renderer ); + } + + if( m_turretMovementObserver && m_playMovementSound ) + { + m_turretMovementObserver->RenderDebugInfo( renderer ); + } +} + +void EveChildTurret::UpdateSyncronous( const EveUpdateContext& updateContext, const EveChildUpdateParams& params ) +{ + float deltaT = updateContext.GetDeltaT(); + + if( m_firingEffect ) + { + m_firingEffect->SetDisplaySourceObject( IsVisible( updateContext ) ); + } + + UpdateCachedGeometryData(); + + // setup and update attached firing effect + if( m_firingEffect ) + { + // if the attached firing effect is looping, then we must recheck if active turret is still the best, + if( m_firingEffect->IsLooping() ) + { + if( m_state == STATE_FIRING ) + { + // don't do it every frame, cause this will result in popping + m_recheckTimeLeft -= deltaT; + if( m_recheckTimeLeft < 0.f ) + { + Vector3 source = m_worldTransform.GetTranslation(); + Vector3 position = source; + if( int closestLocator = m_target->FindClosestLocator( &source, &position ) ) + { + if( closestLocator != m_target->GetLocator() ) + { + // Set up the firing states correctly + SetupFiringState(); + } + } + // recheck every 2 seconds + m_recheckTimeLeft = 2.f; + } + } + } + m_firingEffect->UpdateSynchronous( updateContext ); + } + + // update the target locator position + Vector3 position = m_worldTransform.GetTranslation(); + if( m_firingEffect ) + { + m_firingEffect->GetStartPosition( position ); + } + + m_target->Update( deltaT, &position ); + + if( m_mesh && m_turretMovementObserver != nullptr ) + { + m_turretMovementObserver->Update( m_worldTransform ); + } + EveChildMesh::UpdateSyncronous( updateContext, params ); +} + +void EveChildTurret::UpdateAsyncronous( const EveUpdateContext& updateContext, const EveChildUpdateParams& params ) +{ + float deltaT = updateContext.GetDeltaT(); + // handle fading of turret tracking + if( m_trackingInfluenceDelta != 0.f ) + { + m_trackingInfluence += m_trackingInfluenceDelta * deltaT; + if( m_trackingInfluence > m_maxTrackingTime ) + { + m_trackingInfluence = m_maxTrackingTime; + m_trackingInfluenceDelta = 0.f; + } + else if( m_trackingInfluence < 0.f ) + { + m_trackingInfluence = 0.f; + m_trackingInfluenceDelta = 0.f; + } + } + + if( m_delayToFadeOutTracking > 0.f ) + { + m_delayToFadeOutTracking -= deltaT; + if( m_delayToFadeOutTracking <= 0.f ) + { + m_delayToFadeOutTracking = 0.f; + m_trackingInfluenceDelta = -1.f / TRACKING_FADE_TIME; + } + } + + if( m_delayToFadeInTracking > 0.f ) + { + m_delayToFadeInTracking -= deltaT; + if( m_delayToFadeInTracking <= 0.f ) + { + m_delayToFadeInTracking = 0.f; + m_trackingInfluenceDelta = 1.f / TRACKING_FADE_TIME; + } + } + + // Should handle all the mesh data and transforms + EveChildMesh::UpdateAsyncronous( updateContext, params ); + + // setup and update attached firing effect + if( m_firingEffect ) + { + if( m_mesh ) + { + // update all muzzle points in the firing effect + for( unsigned int i = 0; i < m_firingEffect->GetPerMuzzleEffectCount(); ++i ) + { + // get world transform of this muzzle bone + Matrix matrix = GetFiringBoneWorldTransform( i ); + // and set it to the muzzle + m_firingEffect->SetMuzzleTransform( i, &matrix ); + } + m_firingEffectMuzzlePosSet = true; + } + + m_firingEffect->SetEndPosition( m_target->GetTargetPosition() ); + + // time update (return value tells us if effect is ready to fire!) + if( m_firingEffect->UpdateAsynchronous( updateContext ) ) + { + // if we haven't initialised muzzle positions, do it now + // this can happen, and if we don't do this all effects originate from + // the turret root until turret geometry is loaded and muzzle positions + // properly set + if( !m_firingEffectMuzzlePosSet ) + { + for( unsigned int i = 0; i < m_firingEffect->GetPerMuzzleEffectCount(); ++i ) + { + m_firingEffect->SetMuzzleTransform( i, &m_worldTransform ); + } + + m_firingEffectMuzzlePosSet = true; + } + m_firingEffect->SetDisplayDestObject( m_target->ShowDestObject() ); + } + } +} + +void EveChildTurret::UpdateVisibility( const EveUpdateContext& updateContext, const Matrix& parentTransform, Tr2Lod parentLod ) +{ + EveChildMesh::UpdateVisibility( updateContext, parentTransform, parentLod ); + + if( m_display && m_firingEffect ) + { + m_firingEffect->UpdateVisibility( updateContext ); + } +} + +void EveChildTurret::GetRenderables( std::vector& renderables ) +{ + EveChildMesh::GetRenderables( renderables ); + + if( m_display && m_firingEffect ) + { + m_firingEffect->GetRenderables( renderables ); + } +} + +void EveChildTurret::RegisterWithQuadRenderer( Tr2QuadRenderer& quadRenderer ) +{ + EveChildMesh::RegisterWithQuadRenderer( quadRenderer ); + + if( m_firingEffect ) + { + m_firingEffect->RegisterWithQuadRenderer( quadRenderer ); + } +} + +void EveChildTurret::AddQuadsToQuadRenderer( const TriFrustum& frustum, Tr2QuadRenderer& quadRenderer ) const +{ + EveChildMesh::AddQuadsToQuadRenderer( frustum, quadRenderer ); + + if( m_display && m_firingEffect ) + { + m_firingEffect->AddQuadsToQuadRenderer( frustum, quadRenderer ); + } +} + +void EveChildTurret::UpdateCachedGeometryData() +{ + auto* geometryRes = GetGeometryRes(); + if( geometryRes == m_cachedGeometryRes ) + { + return; + } + ReleaseCachedGeometryData(); + if( geometryRes && geometryRes->IsGood() ) + { + BuildCachedGeometryData( *geometryRes ); + m_cachedGeometryRes = geometryRes; + } +} +void EveChildTurret::BuildCachedGeometryData( TriGeometryRes& geometryRes ) +{ + if( geometryRes.GetSkeletonCount() ) + { + if( TriGeometryResSkeletonData* skeletonData = geometryRes.GetSkeletonData( 0 ) ) + { + for( int i = 0; i < SYSBONE_MAX; ++i ) + { + // in case we don't find system bone, ::FindJoint() returns 0xffffffff + m_systemBoneID[i] = skeletonData->FindJoint( EveTurretAiming::GetSystemBoneName( i ) ); + } + + InitializeFiringEffect(); + } + } + + InitializeAnimation(); + + ForceIdleAnimation(); +} + +void EveChildTurret::ReleaseCachedGeometryData() +{ + m_cachedGeometryRes = nullptr; + m_firingEffectMuzzlePosSet = false; +} + +void EveChildTurret::EnterStateDeactive() +{ + switch( m_state ) + { + case STATE_DEACTIVE: + // do nothing if we are already in this state + break; + case STATE_IDLE: + case STATE_RELOADING: + // no fadeout of tracking, just play deactive anim and then the deactive loop + m_trackingInfluence = 0.f; + PlayAnimation( "Pack", "Inactive" ); + m_delayToFadeOutTracking = 0.f; + break; + case STATE_FIRING: + // stop shooting + if( m_firingEffect ) + { + m_firingEffect->StopFiring(); + } + // DON'T break, just continue with stopping things: + case STATE_TARGETING: + // fadeout the tracking, play deactive anim and then the deactive loop + m_delayToFadeOutTracking = 0.0001f; + m_target->StopFireAtLocator(); + + PlayAnimation( "Pack", "Inactive", TRACKING_FADE_TIME ); + break; + + default: + break; + } + m_state = STATE_DEACTIVE; +} + +void EveChildTurret::EnterStateIdle() +{ + if( !m_isOnline ) + { + return; + } + + switch( m_state ) + { + case STATE_INVALID: + case STATE_RELOADING: + // just play active loop + PlayAnimation( "", "Active" ); + break; + case STATE_DEACTIVE: + // start unpack animation, disable tracking and then into active loop + PlayAnimation( "Deploy", "Active" ); + m_trackingInfluence = 0.f; + break; + case STATE_IDLE: + // do nothing here + break; + case STATE_TARGETING: + case STATE_FIRING: + // stop shooting, fadeout tracking, then into active loop + m_delayToFadeOutTracking = 0.0001f; + m_target->StopFireAtLocator(); + if( m_firingEffect ) + { + m_firingEffect->StopFiring(); + } + PlayAnimation( "", "Active", TRACKING_FADE_TIME ); + + if( m_playMovementSound && !m_targetingToIdleMovementAudioEvent.empty() ) + { + SendEventToAudEmitter( m_turretMovementObserver, m_targetingToIdleMovementAudioEvent ); + } + break; + } + m_state = STATE_IDLE; +} + +void EveChildTurret::EnterStateTargeting() +{ + float animLength = 0.f; + if( !m_isOnline ) + { + return; + } + + // what state are we in? + switch( m_state ) + { + case STATE_DEACTIVE: + // play deploy anim, then active loop and fade in tracking + animLength = PlayAnimation( "Deploy", "Active", TRACKING_FADE_TIME ); + // fade in tracking + m_delayToFadeInTracking = animLength + 0.0001f; + break; + case STATE_IDLE: + case STATE_RELOADING: + // fadein tracking, play active loop + m_delayToFadeInTracking = 0.0001f; + PlayAnimation( "", "Active", TRACKING_FADE_TIME ); + break; + case STATE_TARGETING: + break; + case STATE_FIRING: + // stop shooting, then into active loop + m_target->StopFireAtLocator(); + if( m_firingEffect ) + { + m_firingEffect->StopFiring(); + } + PlayAnimation( "", "Active", 0.f ); + break; + + default: + break; + } + m_state = STATE_TARGETING; +} + +void EveChildTurret::EnterStateFiring() +{ + if( !SetupFiringState() ) + { + return; + } + + // only if we are in firing mode, call ::StopFiring() on the effect right before + // we call ::PrepareFiring(), it'll clean things up in the effect + if( m_firingEffect && m_state == STATE_FIRING ) + { + if( m_firingEffect->IsLooping() ) + { + // We don't want to start and stop the curves when the turret is looping and firing + m_firingEffect->PrepareFiringEffectMoveObjects(); + return; + } + m_firingEffect->StopFiring(); + } + + if( m_firingEffect ) + { + if( m_maxCyclingFirePos > 1 ) + { + m_firingEffect->PrepareFiring( 0.f, m_currentCyclingFiresPos, m_cyclingFireGroupCount ); + } + else + { + m_firingEffect->PrepareFiring( 0.f ); + } + + if( m_target != nullptr ) + { + m_firingEffect->SetImpactConfiguration( m_target->GetImpactConfiguration() ); + } + } + + // finally, we can set state + m_state = STATE_FIRING; +} + +bool EveChildTurret::SetupFiringState() +{ + if( m_state == STATE_DEACTIVE ) + { + // this state change is forbidden! + CCP_LOGERR( "EveChildTurret %s wants to fire but is in deactive state.", m_name.c_str() ); + return false; + } + int closestLocator = -1; + { + Vector3 source = m_worldTransform.GetTranslation(); + Vector3 position = source; + closestLocator = m_target->FindClosestLocator( &source, &position ); + } + + // if this turret is set to cycle through the muzzles for firing, do it here + if( m_maxCyclingFirePos > 1 ) + { + m_currentCyclingFiresPos += m_cyclingFireGroupCount; + if( m_currentCyclingFiresPos >= m_maxCyclingFirePos * m_cyclingFireGroupCount ) + { + m_currentCyclingFiresPos = 0; + } + } + + // timing: is the length of the firing effect known? + float effectTotalTime = m_firingEffect ? m_firingEffect->GetFiringDuration() : 0.f; + float effectPeakTime = m_firingEffect ? m_firingEffect->GetFiringPeakTime() : 0.f; + + Vector3 source = m_parentData.transform.GetTranslation(); + + // what state are we in? + switch( m_state ) + { + case STATE_IDLE: + case STATE_RELOADING: + // fadein tracking, play fire anim (only one the firing turret!) and then the active anim + m_delayToFadeInTracking = 0.0001f; + + PlayAnimation( GetFireAnimationName(), "Active", m_maxTrackingTime ); + // assign locator and turret + m_target->StartFireAtLocator( closestLocator, m_maxTrackingTime + effectPeakTime, effectTotalTime - effectPeakTime, &source ); + break; + case STATE_FIRING: + case STATE_TARGETING: + PlayAnimation( GetFireAnimationName(), "Active", m_maxTrackingTime ); + m_target->StartFireAtLocator( closestLocator, m_maxTrackingTime + effectPeakTime, effectTotalTime - effectPeakTime, &source ); + break; + + default: + break; + } + + return true; +} + +void EveChildTurret::EnterStateReloading() +{ + // what state are we in? + switch( m_state ) + { + case STATE_DEACTIVE: + // ignore this state change: when the turret is inactive, no reload state can be shown! + break; + case STATE_INVALID: + case STATE_IDLE: + case STATE_RELOADING: + // just play reloading anim and then loop + PlayAnimation( "Reload", "Active", 0.f ); + break; + case STATE_TARGETING: + case STATE_FIRING: + // stop shooting, fadeout tracking, then into active loop + m_delayToFadeOutTracking = 0.0001f; + m_target->StopFireAtLocator(); + if( m_firingEffect ) + { + m_firingEffect->StopFiring(); + } + + PlayAnimation( "Reload", "Active", TRACKING_FADE_TIME ); + break; + + default: + break; + } + m_state = STATE_RELOADING; +} + +void EveChildTurret::ForceStateDeactive() +{ + // turn it all off + m_trackingInfluence = 0.f; + m_delayToFadeOutTracking = 0.f; + m_target->StopFireAtLocator(); + if( m_firingEffect ) + { + m_firingEffect->StopFiring(); + } + // finally, we can set state + m_state = STATE_DEACTIVE; + + // now force-play the deactive anim for this state + ForceIdleAnimation(); +} + +void EveChildTurret::ForceIdleAnimation() +{ + std::string idleAnimName = ""; + // what state? + switch( m_state ) + { + case STATE_DEACTIVE: + idleAnimName = "Inactive"; + break; + case STATE_IDLE: + case STATE_RELOADING: + case STATE_TARGETING: + case STATE_FIRING: + idleAnimName = "Active"; + break; + + default: + break; + } + + if( idleAnimName.length() > 0 ) + { + PlayAnimation( "", idleAnimName, 0.f ); + } +} + +void EveChildTurret::ForceStateTargeting() +{ + m_trackingInfluence = m_maxTrackingTime; + m_trackingInfluenceDelta = 0.f; + + m_state = STATE_TARGETING; + + // now force-play the deactive anim for this state + PlayAnimation( "", "Active", 0.f ); +} + +Matrix EveChildTurret::GetFiringBoneWorldTransform( unsigned int muzzle ) const +{ + if( !m_mesh ) + { + return m_worldTransform; + } + + Matrix matrix = m_worldTransform; + + // get the boneID for that muzzle from firing effect + if( !m_firingEffect ) + { + return matrix; + } + unsigned int boneID = m_firingEffect->GetPerMuzzleBoneID( muzzle ); + return GetTurretBoneTransform( boneID ); +} + +void EveChildTurret::InitializeFiringEffect() +{ + m_firingEffectMuzzlePosSet = false; + if( !m_firingEffect ) + { + return; + } + m_firingEffect->RegisterWithQuadRenderer( *Tr2QuadRenderer::Instance() ); + + auto geometryResource = GetGeometryRes(); + if( geometryResource && geometryResource->GetSkeletonCount() ) + { + if( TriGeometryResSkeletonData* skeletonData = geometryResource->GetSkeletonData( 0 ) ) + { + const auto muzzleCount = m_firingEffect->GetPerMuzzleEffectCount(); + if( muzzleCount > EveTurretFiringFX::MUZZLECOUNT_MAX ) + { + CCP_LOGERR( "Upper limit of firing bones is %d, this turret has %d", EveTurretFiringFX::MUZZLECOUNT_MAX, muzzleCount ); + } + + const unsigned int boneCount = std::min( muzzleCount, static_cast( EveTurretFiringFX::MUZZLECOUNT_MAX ) ); + // firing bones should always be on the format Pos_FireXX where XX can range form 01 to 99 + for( unsigned int i = 0; i < boneCount; ++i ) + { + char boneName[32]; + int boneNameIndex = i + 1; + snprintf( boneName, sizeof boneName, "%s%02u", m_firingEffect->GetFiringBoneName(), boneNameIndex ); + + // in case we don't find positional bone, ::FindJoint() returns 0xffffffff + m_firingEffect->SetMuzzleBoneID( i, skeletonData->FindJoint( boneName ) ); + } + } + } +} + +void EveChildTurret::InitializeAnimation() +{ + if( !m_animationUpdater ) + { + m_animationUpdater.CreateInstance(); + } + EveChildMesh::InitializeAnimation(); + + if( m_hookedUpdater != m_animationUpdater ) + { + if( m_hookedUpdater && m_hookedUpdater->GetPoseModifier() == this ) + { + m_hookedUpdater->SetPoseModifier( nullptr ); + } + m_animationUpdater->SetPoseModifier( this ); + m_hookedUpdater = m_animationUpdater; + } +} + +void EveChildTurret::ModifyPose( const cmf::Skeleton& skeleton, cmf::SkeletonPose& pose ) +{ + if( m_trackingInfluence == 0.f ) + { + return; + } + + Vector3 targetPosOS = TransformCoord( *m_target->GetTrackingPosition(), Inverse( m_worldTransform ) ); + + for( unsigned int bone = 0; bone < SYSBONE_MAX; ++bone ) + { + // covers Invalid since INVALID_BONE_INDEX is max + if( m_systemBoneID[bone] < pose.boneTransforms.size() ) + { + cmf::Transform& boneTransform = pose.boneTransforms[m_systemBoneID[bone]]; + m_aiming.ModifySystemBoneTransform( + static_cast( bone ), + &targetPosOS, + nullptr, + m_trackingInfluence, + boneTransform.position, + boneTransform.rotation ); + } + } +} + +Matrix EveChildTurret::GetTurretBoneTransform( uint32_t boneID ) const +{ + Matrix matrix = m_worldTransform; + if( m_animationUpdater ) + { + const auto& worldTransforms = m_animationUpdater->GetWorldTransforms(); + // covers Invalid since INVALID_BONE_INDEX is max_float + if( boneID < worldTransforms.size() ) + { + return worldTransforms[boneID] * matrix; + } + } + return matrix; +} + +TriGeometryRes* EveChildTurret::GetGeometryRes() const +{ + return m_mesh ? m_mesh->GetGeometryResource() : nullptr; +} + +float EveChildTurret::PlayAnimation( const std::string& animName, const std::string& animNameIdle, float delay ) +{ + if( !m_animationUpdater ) + { + return 0.f; + } + + m_animationUpdater->StopAnimations( delay ); + + float animLength = 0.f; + if( !animName.empty() ) + { + if( m_animationUpdater->PlayAnimation( animName.c_str(), false, 1, 0.f, 1.f, false ) ) + { + animLength = m_animationUpdater->FindAnimationDurationByName( animName.c_str() ); + } + } + if( !animNameIdle.empty() ) + { + m_animationUpdater->PlayAnimation( animNameIdle.c_str(), false, 0, 0.f, 1.f, false ); + } + + return animLength; +} + +std::string EveChildTurret::GetFireAnimationName() const +{ + // if m_currentCyclingFiresPos is 0, it's just "Fire" + std::string res = "Fire"; + if( m_currentCyclingFiresPos > 0 ) + { + res.push_back( '0' ); + res.push_back( '0' + m_currentCyclingFiresPos / m_cyclingFireGroupCount ); + } + + return res; +} + +EveTurretFiringFX* EveChildTurret::GetFiringEffect() +{ + return m_firingEffect; +} + +void EveChildTurret::SetFiringEffect( EveTurretFiringFX* firingEffect ) +{ + auto registry = GetComponentRegistry(); + if( EveEntityPtr entity = BlueCastPtr( m_firingEffect ) ) + { + entity->UnRegister( registry ); + } + m_firingEffect = firingEffect; + if( EveEntityPtr entity = BlueCastPtr( m_firingEffect ) ) + { + entity->Register( registry ); + } + InitializeFiringEffect(); +} + +void EveChildTurret::SetTargetObject( IRoot* target ) +{ + if( !target ) + { + return; + } + ITriTargetablePtr oldTargetPtr = m_target->GetTargetable(); + + // attach to target + m_target->SetTargetable( target ); + + if( m_playMovementSound && !m_idleToTargetingMovementAudioEvent.empty() ) + { + // Always trigger movement sounds if coming from IDLE state, otherwise trigger it only if you're targeting a new object. + if( m_state == STATE_IDLE || !oldTargetPtr.IsEqualObject( m_target->GetTargetable() ) ) + { + SendEventToAudEmitter( m_turretMovementObserver, m_idleToTargetingMovementAudioEvent ); + } + } + + // update the firing effect we have one + SetTargetScale(); +} + +ITriTargetablePtr EveChildTurret::GetTargetObject() +{ + return m_target->GetTargetable(); +} + +void EveChildTurret::SetTargetScale() +{ + if( m_firingEffect ) + { + float radius = m_target->GetRadius(); + m_firingEffect->SetScaleByRadius( radius ); + } +} diff --git a/trinity/Eve/SpaceObject/Children/EveChildTurret.h b/trinity/Eve/SpaceObject/Children/EveChildTurret.h new file mode 100644 index 000000000..0dbfcbc99 --- /dev/null +++ b/trinity/Eve/SpaceObject/Children/EveChildTurret.h @@ -0,0 +1,145 @@ +// Copyright © 2026 Fenris Creations ehf. + +#pragma once +#ifndef EveChildTurret_H +#define EveChildTurret_H + +#include "EveChildMesh.h" +#include "Eve/Turret/EveTurretAiming.h" +#include "Eve/Turret/EveTurretTarget.h" +#include "Include/ITr2PoseModifier.h" + +BLUE_DECLARE( EveTurretFiringFX ); +BLUE_DECLARE( EveTurretTarget ); + +BLUE_CLASS( EveChildTurret ) : + public EveChildMesh, + public ITr2PoseModifier +{ +public: + EXPOSE_TO_BLUE(); + + EveChildTurret( IRoot* lockobj = nullptr ); + ~EveChildTurret(); + + bool Initialize() override; + bool OnModified( Be::Var * value ) override; + void RegisterComponents() override; + void UnRegisterComponents() override; + + void GetDebugOptions( Tr2DebugRendererOptions & options ) override; + void RenderDebugInfo( ITr2DebugRenderer2 & renderer ) override; + + // EveSpaceObjectChild + void UpdateSyncronous( const EveUpdateContext& updateContext, const EveChildUpdateParams& params ) override; + void UpdateAsyncronous( const EveUpdateContext& updateContext, const EveChildUpdateParams& params ) override; + void UpdateVisibility( const EveUpdateContext& updateContext, const Matrix& parentTransform, Tr2Lod parentLod ) override; + void GetRenderables( std::vector & renderables ) override; + void RegisterWithQuadRenderer( Tr2QuadRenderer & quadRenderer ) override; + void AddQuadsToQuadRenderer( const TriFrustum& frustum, Tr2QuadRenderer& quadRenderer ) const override; + + void UpdateCachedGeometryData(); + void BuildCachedGeometryData( TriGeometryRes & geometryRes ); + void ReleaseCachedGeometryData(); + + // action + void EnterStateDeactive(); + void EnterStateIdle(); + void EnterStateTargeting(); + void EnterStateFiring(); + bool SetupFiringState(); + void EnterStateReloading(); + + void ForceStateDeactive(); + void ForceIdleAnimation(); + void ForceStateTargeting(); + + Matrix GetFiringBoneWorldTransform( unsigned int muzzle ) const; + + // turret set states + enum State + { + STATE_INVALID = 0, + STATE_DEACTIVE, + STATE_IDLE, + STATE_TARGETING, + STATE_FIRING, + STATE_RELOADING, + }; + + void ModifyPose( const cmf::Skeleton& skeleton, cmf::SkeletonPose& pose ) override; + +protected: + // setup the attached firing effect + void InitializeFiringEffect(); + + void InitializeAnimation() override; + + Matrix GetTurretBoneTransform( uint32_t boneID ) const; + + TriGeometryRes* GetGeometryRes() const; + + // animation + float PlayAnimation( const std::string& animName, const std::string& animNameIdle, float delay = 0.f ); + std::string GetFireAnimationName() const; + + EveTurretFiringFX* GetFiringEffect(); + void SetFiringEffect( EveTurretFiringFX * firingEffect ); + + // Assign the target object + void SetTargetObject( IRoot * target ); + ITriTargetablePtr GetTargetObject(); + void SetTargetScale(); + + // target (object we are tracking) + EveTurretTargetPtr m_target; + + bool m_isOnline = true; + + // impacts + float m_impactSize = 0.f; + ImpactBehaviour::Type m_impactBehaviour = ImpactBehaviour::DAMAGE_LOCATOR; + + // tracking + float m_trackingInfluence = 0.f; + float m_trackingInfluenceDelta = 0.f; + float m_delayToFadeOutTracking = 0.f; + float m_delayToFadeInTracking = 0.f; + float m_maxTrackingTime = 1.f; + + // animation: updater we last hooked our pose modifier into (to unhook on swap) + Tr2GrannyAnimationPtr m_hookedUpdater; + + uint32_t m_maxCyclingFirePos = 1; + uint32_t m_cyclingFireGroupCount = 1; + uint32_t m_currentCyclingFiresPos = 0; + + // system bones + unsigned int m_systemBoneID[SYSBONE_MAX]; + // sysbone aiming math + tuning (shared with EveTurretSet) + EveTurretAiming m_aiming; + + // state of turret set + State m_state = STATE_IDLE; + + float m_recheckTimeLeft = -1.f; + + // firing effect redfile path + std::string m_firingEffectResPath; + + // firing effect + EveTurretFiringFXPtr m_firingEffect; + bool m_firingEffectMuzzlePosSet = false; + + // Audio specific attributes + bool m_playMovementSound = true; + TriObserverLocalPtr m_turretMovementObserver; + std::wstring m_idleToTargetingMovementAudioEvent; + std::wstring m_targetingToIdleMovementAudioEvent; + + TriGeometryResPtr m_cachedGeometryRes; +}; + +TYPEDEF_BLUECLASS( EveChildTurret ); + +#endif diff --git a/trinity/Eve/SpaceObject/Children/EveChildTurret_Blue.cpp b/trinity/Eve/SpaceObject/Children/EveChildTurret_Blue.cpp new file mode 100644 index 000000000..5631efb4a --- /dev/null +++ b/trinity/Eve/SpaceObject/Children/EveChildTurret_Blue.cpp @@ -0,0 +1,97 @@ +// Copyright © 2026 Fenris Creations ehf. + +#include "StdAfx.h" +#include "EveChildTurret.h" +#include "Eve/Turret/EveTurretFiringFX.h" + +BLUE_DEFINE( EveChildTurret ); + +extern Be::VarChooser ImpactBehaviourChooser[]; + +const Be::ClassInfo* EveChildTurret::ExposeToBlue() +{ + EXPOSURE_BEGIN( EveChildTurret, "" ) + MAP_INTERFACE( EveChildTurret ) + + + MAP_ATTRIBUTE( "isOnline", m_isOnline, "Indicate if turret is active", Be::READWRITE ) + MAP_ATTRIBUTE( "trackingInfluence", m_trackingInfluence, "How much tracking is allowed?", Be::READ ) + MAP_ATTRIBUTE( "maxTrackingTime", m_maxTrackingTime, "How long does tracking take?", Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE( "state", m_state, "State of the turret", Be::READ | Be::PERSIST ) + + MAP_PROPERTY( "targetObject", GetTargetObject, SetTargetObject, "object this turret will track" ) + MAP_ATTRIBUTE( "target", m_target, "Info on the target", Be::READ ) + + MAP_ATTRIBUTE( "sysBoneHeight", m_aiming.m_sysBoneHeight, "System bone HEIGHT extension factor", Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE( "sysBonePitchFactor", m_aiming.m_sysBonePitchFactor, "main pitch factor", Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE( "sysBonePitchOffset", m_aiming.m_sysBonePitchOffset, "main pitch offset (in degrees!)", Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE( "sysBonePitchMin", m_aiming.m_sysBonePitchMin, "main pitch minimum clamp value, prevents the turret from targeting down too much (in degrees!)", Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE( "sysBonePitchMax", m_aiming.m_sysBonePitchMax, "main pitch maximum clamp value, prevents the turret from targeting down too much (in degrees!)", Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE( "sysBonePitch01Factor", m_aiming.m_sysBonePitch01Factor, "pitch 01 factor", Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE( "sysBonePitch01Offset", m_aiming.m_sysBonePitch01Offset, "pitch 01 offset (in degrees!)", Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE( "sysBonePitch02Factor", m_aiming.m_sysBonePitch02Factor, "pitch 02 factor", Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE( "sysBonePitch02Offset", m_aiming.m_sysBonePitch02Offset, "pitch 02 offset (in degrees!)", Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE( "sysBonePitch03Factor", m_aiming.m_sysBonePitch03Factor, "pitch 03 factor", Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE( "sysBonePitch03Offset", m_aiming.m_sysBonePitch03Offset, "pitch 03 offset (in degrees!)", Be::READWRITE | Be::PERSIST ) + + MAP_ATTRIBUTE( "maxCyclingFirePos", m_maxCyclingFirePos, "If greater than one we cycle through the given number of muzzles.", Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE( "cyclingFireGroupCount", m_cyclingFireGroupCount, "The number of muzzles in one cycle group, usually only one.", Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE( "currentCyclingFiresPos", m_currentCyclingFiresPos, "Current muzzle id due to cycling muzzles", Be::READ ) + + + MAP_ATTRIBUTE( "firingEffect", m_firingEffect, "", Be::HIDDEN ) // Needed to make Graphite able to detect bindings inside of the firing effect. + MAP_PROPERTY( "firingEffect", GetFiringEffect, SetFiringEffect, "The module for the firing effect of this turret" ) + MAP_ATTRIBUTE( "firingEffectResPath", m_firingEffectResPath, "A res path to the redfile containing the primary firing effect", Be::READWRITE | Be::PERSIST | Be::NOTIFY ) + + MAP_ATTRIBUTE( "impactSize", m_impactSize, "Size of impacts. No impact if size is 0 or less", Be::READWRITE | Be::NOTIFY | Be::PERSIST ) + MAP_ATTRIBUTE_WITH_CHOOSER( "impactBehaviour", m_impactBehaviour, "What do we want to hit? ", Be::READWRITE | Be::NOTIFY | Be::PERSIST | Be::ENUM, ImpactBehaviourChooser ) + + MAP_ATTRIBUTE( "turretMovementObserver", m_turretMovementObserver, "The observer for turret movement sounds. Note: the positioning of this observer is automatic.", Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE( "playMovementSound", m_playMovementSound, "If true this turret set will play its mechanical movement sounds if movement audio events are defined.", Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE( "idleToTargetingMovementAudioEvent", m_idleToTargetingMovementAudioEvent, "The event to send to the audio engine for mechanical noise when a turret moves from idle to targeting.", Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE( "targetingToIdleMovementAudioEvent", m_targetingToIdleMovementAudioEvent, "The event to send to the audio engine for mechanical noise when a turret moves from targeting to idle.", Be::READWRITE | Be::PERSIST ) + + MAP_METHOD_AND_WRAP( + "EnterStateDeactive", + EnterStateDeactive, + "Go into state deactive: play deactive anim and stay inside ship. \n:jessica-placement: TOOLBAR\n:jessica-icon: fa-bed\n" ) + + MAP_METHOD_AND_WRAP( + "EnterStateIdle", + EnterStateIdle, + "Go into state idle: play idle anim and face cannons forward. \n:jessica-placement: TOOLBAR\n:jessica-icon: fa-male\n" ) + + MAP_METHOD_AND_WRAP( + "EnterStateTargeting", + EnterStateTargeting, + "Go into state targeting: face cannons towards enemy. \n:jessica-placement: TOOLBAR\n:jessica-icon: fa-crosshairs\n" ) + + MAP_METHOD_AND_WRAP( + "EnterStateFiring", + EnterStateFiring, + "Go into state fire: play fire anim and face cannons towards enemy.\n:jessica-placement: TOOLBAR\n:jessica-icon: fa-fire-alt\n" ) + + MAP_METHOD_AND_WRAP( + "EnterStateReloading", + EnterStateReloading, + "Go into state reloading: play reload anim and then idle. \n:jessica-placement: TOOLBAR\n:jessica-icon: fa-sync\n" ) + + MAP_METHOD_AND_WRAP( + "ForceStateDeactive", + ForceStateDeactive, + "Force into state deactive: no anim, no transition, just flip." ) + + MAP_METHOD_AND_WRAP( + "ForceStateTargeting", + ForceStateTargeting, + "Force into state targeting: no anim, no transition, just flip." ) + + MAP_METHOD_AND_WRAP( + "GetFiringBoneWorldTransform", + GetFiringBoneWorldTransform, + "Returns the world transform matrix of the specified firing bone in the currently firing turret." + "\n:param idx: index of the firing bone in the current model." + "\n:returns: The world transform matrix." ) + + EXPOSURE_CHAINTO( EveChildMesh ) +} diff --git a/trinity/Eve/Turret/EveTurretAiming.cpp b/trinity/Eve/Turret/EveTurretAiming.cpp new file mode 100644 index 000000000..a8e3abb03 --- /dev/null +++ b/trinity/Eve/Turret/EveTurretAiming.cpp @@ -0,0 +1,182 @@ +// Copyright © 2026 Fenris Creations ehf. + +#include "StdAfx.h" +#include "EveTurretAiming.h" +#include "TriMath.h" + +// names of system bones like they are in the model skeleton +static constexpr const char* s_systemBoneSkeletonNames[] = { + "invalid", // SYSBONE_INVALID + "Sys_Rotation_Arm", // SYSBONE_ROTATION + "Sys_Rotation_Arm01", // SYSBONE_ROTATION1 + "Sys_Rotation_Arm02", // SYSBONE_ROTATION2 + "Sys_CounterRotation", // SYSBONE_COUNTER_ROTATION + "Sys_Pitch_Barrel", // SYSBONE_PITCH + "Sys_Pitch_Barrel1", // SYSBONE_PITCH1 + "Sys_Pitch_Barrel2", // SYSBONE_PITCH2 + "Sys_Height", // SYSBONE_SCALED_HEIGHT + "Sys_Pitch_Arm01", // SYSBONE_SCALED_PITCH01 + "Sys_Pitch_Arm02", // SYSBONE_SCALED_PITCH02 + "Sys_Pitch_Arm03", // SYSBONE_SCALED_PITCH03 + "Sys_Pitch_Arm04", // SYSBONE_SCALED_PITCH04 + "Sys_Pitch_Arm05", // SYSBONE_SCALED_PITCH05 + "Sys_Pitch_Arm06", // SYSBONE_SCALED_PITCH06 +}; + +const char* EveTurretAiming::GetSystemBoneName( unsigned int bone ) +{ + return bone < SYSBONE_MAX ? s_systemBoneSkeletonNames[bone] : s_systemBoneSkeletonNames[SYSBONE_INVALID]; +} + +// -------------------------------------------------------------------------------- +// Description: +// Depending on the type of the system bone, we calculate a new transform for +// it and apply it. All of this is highly "hard-coded", but there are some +// variables in there, so we can customize the tracking to individual turrets. +// The modification should pay attention the amount of modification needed, +// passed in trackingInfluence +// Arguments: +// bone - type of system bone +// target - position of target in "turret"-space +// position - the bone position that needs to get modified +// rotation - the bone rotation that needs to get modified +// -------------------------------------------------------------------------------- +void EveTurretAiming::ModifySystemBoneTransform( SystemBones bone, const Vector3* target, const Matrix* localTransform, float trackingInfluence, Vector3& position, Quaternion& rotation ) const +{ + switch( bone ) + { + case SYSBONE_INVALID: + break; + case SYSBONE_ROTATION: + case SYSBONE_ROTATION01: + case SYSBONE_ROTATION02: { + // rotation of turret 360 degrees, alpha is between -pi and pi + float alpha = atan2( target->x, target->z ); + // never forget do apply influence! + alpha *= trackingInfluence; + // 1st: make quaternion + Quaternion quat = RotationQuaternion( alpha, 0.f, 0.f ); + // 2nd: apply this quat after the original one + quat = rotation * quat; + rotation = quat; + } + break; + case SYSBONE_COUNTER_ROTATION: { + // inverse(!!) rotation of turret 360 degrees, alpha is between -pi and pi + float alpha = -atan2( target->x, target->z ); + // never forget do apply influence! + alpha *= trackingInfluence; + // 1st: make quaternion + Quaternion quat = RotationQuaternion( alpha, 0.f, 0.f ); + // 2nd: apply this quat after the original one + quat = rotation * quat; + rotation = quat; + } + break; + case SYSBONE_PITCH: + case SYSBONE_PITCH1: + case SYSBONE_PITCH2: { + CalcTransformForPitchBone( target, XMConvertToRadians( m_sysBonePitchMin ), XMConvertToRadians( m_sysBonePitchMax ), bone, localTransform, trackingInfluence, rotation ); + } + break; + case SYSBONE_SCALED_HEIGHT: { + // pitch of barrel 90 degrees + Vector3 directionNormal = Normalize( *target ); + float height = TriClamp( directionNormal.y, 0.f, 1.f ); + // never forget do apply influence! + height *= trackingInfluence; + // it's a pos extension with a scale + Vector3 pos = Vector3( 0.f, height * m_sysBoneHeight, 0.f ) + position; + position = pos; + } + break; + case SYSBONE_SCALED_PITCH01: + case SYSBONE_SCALED_PITCH02: + case SYSBONE_SCALED_PITCH03: + case SYSBONE_SCALED_PITCH04: + case SYSBONE_SCALED_PITCH05: + case SYSBONE_SCALED_PITCH06: { + CalcTransformForPitchBone( target, 0.f, XMConvertToRadians( m_sysBonePitchMax ), bone, nullptr, trackingInfluence, rotation ); + } + break; + default: + break; + } +} + +void EveTurretAiming::CalcTransformForPitchBone( const Vector3* target, float minPitch, float maxPitch, unsigned int boneIndex, const Matrix* localTransform, float trackingInfluence, Quaternion& rotation ) const +{ + float pitchOffset = GetBonePitchOffset( boneIndex ); + float pitchFactor = GetBonePitchFactor( boneIndex ); + // pitch of barrel 90 degrees + Vector3 bone_position( 0.f, 0.f, 0.f ); + + if( localTransform ) + { + bone_position = localTransform->GetTranslation(); + } + + Vector3 relTarget = *target - bone_position; + Vector3 dirNrm = Normalize( relTarget ); + float radians = asinf( dirNrm.y ); + + if( localTransform ) + { + Vector3 bone_direction = Normalize( bone_position ); + float d = Dot( bone_direction, *target ); + if( d < Length( bone_position ) ) + { + // Assuming up is enough for now to avoid cross products + radians = TriFloatSign( relTarget.y ) * XM_PI - radians; + } + } + + float alpha = TriClamp( radians, minPitch, maxPitch ); + // modify! + alpha = pitchFactor * alpha + XMConvertToRadians( pitchOffset ); + // never forget do apply influence! + alpha *= trackingInfluence; + // 1st: make quaternion + Quaternion quat = RotationQuaternion( 0.f, -alpha, 0.f ); + // 2nd: apply this quat after the original one + quat = rotation * quat; + rotation = quat; +} + +float EveTurretAiming::GetBonePitchFactor( unsigned int boneIndex ) const +{ + switch( boneIndex ) + { + case SYSBONE_PITCH: + case SYSBONE_PITCH1: + case SYSBONE_PITCH2: + return m_sysBonePitchFactor; + case SYSBONE_SCALED_PITCH01: + return m_sysBonePitch01Factor; + case SYSBONE_SCALED_PITCH02: + return m_sysBonePitch02Factor; + case SYSBONE_SCALED_PITCH03: + return m_sysBonePitch03Factor; + default: + return 1.0f; + } +} + +float EveTurretAiming::GetBonePitchOffset( unsigned int boneIndex ) const +{ + switch( boneIndex ) + { + case SYSBONE_PITCH: + case SYSBONE_PITCH1: + case SYSBONE_PITCH2: + return m_sysBonePitchOffset; + case SYSBONE_SCALED_PITCH01: + return m_sysBonePitch01Offset; + case SYSBONE_SCALED_PITCH02: + return m_sysBonePitch02Offset; + case SYSBONE_SCALED_PITCH03: + return m_sysBonePitch03Offset; + default: + return 0.0f; + } +} diff --git a/trinity/Eve/Turret/EveTurretAiming.h b/trinity/Eve/Turret/EveTurretAiming.h new file mode 100644 index 000000000..f32717dd3 --- /dev/null +++ b/trinity/Eve/Turret/EveTurretAiming.h @@ -0,0 +1,57 @@ +// Copyright © 2026 Fenris Creations ehf. + +#pragma once +#ifndef EveTurretAiming_H +#define EveTurretAiming_H + +// system-controlled bones +enum SystemBones +{ + SYSBONE_INVALID = 0, + SYSBONE_ROTATION, + SYSBONE_ROTATION01, + SYSBONE_ROTATION02, + SYSBONE_COUNTER_ROTATION, + SYSBONE_PITCH, + SYSBONE_PITCH1, + SYSBONE_PITCH2, + SYSBONE_SCALED_HEIGHT, + SYSBONE_SCALED_PITCH01, + SYSBONE_SCALED_PITCH02, + SYSBONE_SCALED_PITCH03, + SYSBONE_SCALED_PITCH04, + SYSBONE_SCALED_PITCH05, + SYSBONE_SCALED_PITCH06, + SYSBONE_MAX, +}; + +// Sysbone aiming math and tuning values shared by EveTurretSet and EveChildTurret. +// Hosts embed this by value and expose the members through their own Blue schema. +class EveTurretAiming +{ +public: + // name of a system bone as authored in the model skeleton + static const char* GetSystemBoneName( unsigned int bone ); + + void ModifySystemBoneTransform( SystemBones bone, const Vector3* target, const Matrix* localTransform, float trackingInfluence, Vector3& position, Quaternion& rotation ) const; + + void CalcTransformForPitchBone( const Vector3* target, float minPitch, float maxPitch, unsigned int boneIndex, const Matrix* localTransform, float trackingInfluence, Quaternion& rotation ) const; + + float GetBonePitchFactor( unsigned int boneIndex ) const; + float GetBonePitchOffset( unsigned int boneIndex ) const; + + // specific system bone values + float m_sysBoneHeight = 1.f; + float m_sysBonePitchOffset = 0.f; + float m_sysBonePitchFactor = 1.f; + float m_sysBonePitchMin = 0.f; + float m_sysBonePitchMax = 90.f; + float m_sysBonePitch01Offset = 0.f; + float m_sysBonePitch01Factor = 1.f; + float m_sysBonePitch02Offset = 0.f; + float m_sysBonePitch02Factor = 1.f; + float m_sysBonePitch03Offset = 0.f; + float m_sysBonePitch03Factor = 1.f; +}; + +#endif diff --git a/trinity/Eve/Turret/EveTurretSet.cpp b/trinity/Eve/Turret/EveTurretSet.cpp index e9809966b..4cf85beb3 100644 --- a/trinity/Eve/Turret/EveTurretSet.cpp +++ b/trinity/Eve/Turret/EveTurretSet.cpp @@ -26,25 +26,6 @@ using namespace Tr2RenderContextEnum; extern bool g_brokenMacOSNvidiaDrivers; -// names of system bones like they are in the granny file -static std::string s_systemBoneSkeletonNames[] = { - "invalid", // SYSBONE_INVALID - "Sys_Rotation_Arm", // SYSBONE_ROTATION - "Sys_Rotation_Arm01", // SYSBONE_ROTATION1 - "Sys_Rotation_Arm02", // SYSBONE_ROTATION2 - "Sys_CounterRotation", // SYSBONE_COUNTER_ROTATION - "Sys_Pitch_Barrel", // SYSBONE_PITCH - "Sys_Pitch_Barrel1", // SYSBONE_PITCH1 - "Sys_Pitch_Barrel2", // SYSBONE_PITCH2 - "Sys_Height", // SYSBONE_SCALED_HEIGHT - "Sys_Pitch_Arm01", // SYSBONE_SCALED_PITCH01 - "Sys_Pitch_Arm02", // SYSBONE_SCALED_PITCH02 - "Sys_Pitch_Arm03", // SYSBONE_SCALED_PITCH03 - "Sys_Pitch_Arm04", // SYSBONE_SCALED_PITCH04 - "Sys_Pitch_Arm05", // SYSBONE_SCALED_PITCH05 - "Sys_Pitch_Arm06", // SYSBONE_SCALED_PITCH06 -}; - // invalids const unsigned int INVALID_BONE_INDEX = 0xffffffff; const unsigned int INVALID_TURRET_INDEX = 0xffffffff; @@ -100,17 +81,6 @@ EveTurretSet::EveTurretSet( IRoot* lockobj ) : m_maxCyclingFirePos( 1 ), m_cyclingFireGroupCount( 1 ), m_currentCyclingFiresPos( 0 ), - m_sysBoneHeight( 1.f ), - m_sysBonePitchOffset( 0.f ), - m_sysBonePitchFactor( 1.f ), - m_sysBonePitchMin( 0.f ), - m_sysBonePitchMax( 90.f ), - m_sysBonePitch01Offset( 0.f ), - m_sysBonePitch01Factor( 1.f ), - m_sysBonePitch02Offset( 0.f ), - m_sysBonePitch02Factor( 1.f ), - m_sysBonePitch03Offset( 0.f ), - m_sysBonePitch03Factor( 1.f ), m_state( STATE_IDLE ), m_activeTurret( INVALID_TURRET_INDEX ), m_recheckTimeLeft( -1.f ), @@ -1073,7 +1043,7 @@ void EveTurretSet::RebuildCachedData( BlueAsyncRes* p ) for( int i = 0; i < SYSBONE_MAX; ++i ) { // in case we don't find system bone, ::FindJoint() returns 0xffffffff - m_systemBoneID[i] = skeletonData->FindJoint( s_systemBoneSkeletonNames[i].c_str() ); + m_systemBoneID[i] = skeletonData->FindJoint( EveTurretAiming::GetSystemBoneName( i ) ); } // try to link an existing firing effect to skeleton's bones @@ -1346,7 +1316,7 @@ void EveTurretSet::UpdateSingleTurrets() } // modify this bone's transform data cmf::Transform& boneTransform = turret.pose.boneTransforms[m_systemBoneID[bone]]; - ModifySystemBoneTransform( (SystemBones)bone, &targetPosOS, localTransformPtr, boneTransform.position, boneTransform.rotation ); + m_aiming.ModifySystemBoneTransform( (SystemBones)bone, &targetPosOS, localTransformPtr, m_trackingInfluence, boneTransform.position, boneTransform.rotation ); } } } @@ -1398,7 +1368,7 @@ void EveTurretSet::UpdateSingleTurrets() { Vector3 position = Vector3( boneTransform->Position[0], boneTransform->Position[1], boneTransform->Position[2] ); Quaternion rotation = *reinterpret_cast( boneTransform->Orientation ); - ModifySystemBoneTransform( (SystemBones)bone, &targetPosOS, localTransformPtr, position, rotation ); + m_aiming.ModifySystemBoneTransform( (SystemBones)bone, &targetPosOS, localTransformPtr, m_trackingInfluence, position, rotation ); GrannySetTransform( boneTransform, &position.x, (float*)&rotation, (float*)boneTransform->ScaleShear ); } } @@ -1626,7 +1596,7 @@ Matrix EveTurretSet::GetTurretBoneTransform( uint32_t closestTurret, uint32_t bo else { // if we don't have a valid pose, position is fine but orientation? depends on launcher settings... - if( m_sysBonePitchMin < 45.f ) + if( m_aiming.m_sysBonePitchMin < 45.f ) { // aiming directly at target, because target cone is large Vector3 nrmToTarget = *m_target->GetTrackingPosition() - m.GetTranslation(); @@ -1682,7 +1652,7 @@ Matrix EveTurretSet::GetTurretBoneTransform( uint32_t closestTurret, uint32_t bo else { // if we don't have a valid granny pose, position is fine but orientation? depends on launcher settings... - if( m_sysBonePitchMin < 45.f ) + if( m_aiming.m_sysBonePitchMin < 45.f ) { // aiming directly at target, because target cone is large Vector3 nrmToTarget = *m_target->GetTrackingPosition() - m.GetTranslation(); @@ -1713,84 +1683,6 @@ Matrix EveTurretSet::GetTurretBoneTransform( uint32_t closestTurret, uint32_t bo #endif } -// -------------------------------------------------------------------------------- -// Description: -// Depending on the type of the system bone, we calculate a new transform for -// it and apply it. All of this is highly "hard-coded", but there are some -// variables in there, so we can customize the tracking to individual turrets. -// The modification should pay attention the amount of modification needed, -// stored in m_trackingInfluence -// Arguments: -// bone - type of system bone -// target - position of target in "turret"-space -// position - the bone position that needs to get modified -// rotation - the bone rotation that needs to get modified -// -------------------------------------------------------------------------------- -void EveTurretSet::ModifySystemBoneTransform( SystemBones bone, const Vector3* target, const Matrix* localTransform, Vector3& position, Quaternion& rotation ) const -{ - switch( bone ) - { - case SYSBONE_INVALID: - break; - case SYSBONE_ROTATION: - case SYSBONE_ROTATION01: - case SYSBONE_ROTATION02: { - // rotation of turret 360 degrees, alpha is between -pi and pi - float alpha = atan2( target->x, target->z ); - // never forget do apply influence! - alpha *= m_trackingInfluence; - // 1st: make quaternion - Quaternion quat = RotationQuaternion( alpha, 0.f, 0.f ); - // 2nd: apply this quat after the original one - quat = rotation * quat; - // 3rd: make granny_transform from quat - rotation = quat; - } - break; - case SYSBONE_COUNTER_ROTATION: { - // inverse(!!) rotation of turret 360 degress, alpha is between -pi and pi - float alpha = -1.f * atan2( target->x, target->z ); - // never forget do apply influence! - alpha *= m_trackingInfluence; - // 1st: make quaternion - Quaternion quat = RotationQuaternion( alpha, 0.f, 0.f ); - // 2nd: apply this quat after the original one - quat = rotation * quat; - // 3rd: make granny_transform from quat - rotation = quat; - } - break; - case SYSBONE_PITCH: - case SYSBONE_PITCH1: - case SYSBONE_PITCH2: { - CalcTransformForPitchBone( target, XMConvertToRadians( m_sysBonePitchMin ), XMConvertToRadians( m_sysBonePitchMax ), bone, localTransform, rotation ); - } - break; - case SYSBONE_SCALED_HEIGHT: { - // pitch of barrel 90 degrees - Vector3 dirNrm = Normalize( *target ); - float height = TriClamp( dirNrm.y, 0.f, 1.f ); - // never forget do apply influence! - height *= m_trackingInfluence; - // it's a pos extension with a scale - Vector3 pos = Vector3( 0.f, height * m_sysBoneHeight, 0.f ) + position; - position = pos; - } - break; - case SYSBONE_SCALED_PITCH01: - case SYSBONE_SCALED_PITCH02: - case SYSBONE_SCALED_PITCH03: - case SYSBONE_SCALED_PITCH04: - case SYSBONE_SCALED_PITCH05: - case SYSBONE_SCALED_PITCH06: { - CalcTransformForPitchBone( target, 0.f, XMConvertToRadians( m_sysBonePitchMax ), bone, nullptr, rotation ); - } - break; - default: - break; - } -} - // -------------------------------------------------------------------------------- // Description: // This sets up a turret on a ship to a give position. The total number of @@ -3766,98 +3658,6 @@ ITriTargetablePtr EveTurretSet::GetTargetObject() return m_target->GetTargetable(); } -// -------------------------------------------------------------------------------- -// Description: -// Calculates the transform for a pitch bone -// -------------------------------------------------------------------------------- -void EveTurretSet::CalcTransformForPitchBone( const Vector3* target, float minPitch, float maxPitch, unsigned int boneIndex, const Matrix* localTransform, Quaternion& rotation ) const -{ - float pitchOffset = GetBonePitchOffset( boneIndex ); - float pitchFactor = GetBonePitchFactor( boneIndex ); - // pitch of barrel 90 degrees - Vector3 bone_position( 0.f, 0.f, 0.f ); - - if( localTransform ) - { - bone_position = localTransform->GetTranslation(); - } - - Vector3 relTarget = *target - bone_position; - Vector3 dirNrm = Normalize( relTarget ); - float radians = asinf( dirNrm.y ); - - if( localTransform ) - { - Vector3 bone_direction = Normalize( bone_position ); - float d = Dot( bone_direction, *target ); - if( d < Length( bone_position ) ) - { - // Assuming up is enough for now to avoid cross products - radians = TriFloatSign( relTarget.y ) * XM_PI - radians; - } - } - - float alpha = TriClamp( radians, minPitch, maxPitch ); - // modify! - alpha = pitchFactor * alpha + XMConvertToRadians( pitchOffset ); - // never forget do apply influence! - alpha *= m_trackingInfluence; - // 1st: make quaternion - Quaternion quat = RotationQuaternion( 0.f, -alpha, 0.f ); - // 2nd: apply this quat after the original one - quat = rotation * quat; - // 2nd: make granny_transform from quat - rotation = quat; -} - -// -------------------------------------------------------------------------------- -// Description: -// Returns the correct bone pitch factor based on the bone index -// If the bone index does not have a specific bone pitch factor, a default 1.0 is returned -// -------------------------------------------------------------------------------- -float EveTurretSet::GetBonePitchFactor( unsigned int boneIndex ) const -{ - switch( boneIndex ) - { - case SYSBONE_PITCH: - case SYSBONE_PITCH1: - case SYSBONE_PITCH2: - return m_sysBonePitchFactor; - case SYSBONE_SCALED_PITCH01: - return m_sysBonePitch01Factor; - case SYSBONE_SCALED_PITCH02: - return m_sysBonePitch02Factor; - case SYSBONE_SCALED_PITCH03: - return m_sysBonePitch03Factor; - default: - return 1.0f; - } -} - -// -------------------------------------------------------------------------------- -// Description: -// Returns the correct bone pitch offset based on the bone index -// If the bone index does not have a specific bone pitch offset, a default 0.0 is returned -// -------------------------------------------------------------------------------- -float EveTurretSet::GetBonePitchOffset( unsigned int boneIndex ) const -{ - switch( boneIndex ) - { - case SYSBONE_PITCH: - case SYSBONE_PITCH1: - case SYSBONE_PITCH2: - return m_sysBonePitchOffset; - case SYSBONE_SCALED_PITCH01: - return m_sysBonePitch01Offset; - case SYSBONE_SCALED_PITCH02: - return m_sysBonePitch02Offset; - case SYSBONE_SCALED_PITCH03: - return m_sysBonePitch03Offset; - default: - return 0.0f; - } -} - // -------------------------------------------------------------------------------- void EveTurretSet::RegisterWithQuadRenderer( Tr2QuadRenderer& quadRenderer ) { diff --git a/trinity/Eve/Turret/EveTurretSet.h b/trinity/Eve/Turret/EveTurretSet.h index 169157903..44bb7e287 100644 --- a/trinity/Eve/Turret/EveTurretSet.h +++ b/trinity/Eve/Turret/EveTurretSet.h @@ -8,6 +8,7 @@ #include "include/ITriTargetable.h" #include "Tr2ShLightingManager.h" #include "Tr2GrannyAnimation.h" +#include "EveTurretAiming.h" #include "EveTurretTarget.h" #include "Tr2DebugRenderer.h" @@ -254,27 +255,6 @@ BLUE_CLASS( EveTurretSet ) : }; private: - // system-controlled bones - enum SystemBones - { - SYSBONE_INVALID = 0, - SYSBONE_ROTATION, - SYSBONE_ROTATION01, - SYSBONE_ROTATION02, - SYSBONE_COUNTER_ROTATION, - SYSBONE_PITCH, - SYSBONE_PITCH1, - SYSBONE_PITCH2, - SYSBONE_SCALED_HEIGHT, - SYSBONE_SCALED_PITCH01, - SYSBONE_SCALED_PITCH02, - SYSBONE_SCALED_PITCH03, - SYSBONE_SCALED_PITCH04, - SYSBONE_SCALED_PITCH05, - SYSBONE_SCALED_PITCH06, - SYSBONE_MAX, - }; - // geom res load void InitializeGeometryResource(); // instance vertex buffer @@ -296,17 +276,6 @@ BLUE_CLASS( EveTurretSet ) : void UpdateSingleTurrets(); - // set transform for tracking - void ModifySystemBoneTransform( SystemBones bone, const Vector3* target, const Matrix* localTransform, Vector3& position, Quaternion& rotation ) const; - - // Calculates the pitch for a bone based on the parameters - void CalcTransformForPitchBone( const Vector3* target, float minPitch, float maxPitch, unsigned int boneIndex, const Matrix* localTransform, Quaternion& rotation ) const; - - // Returns the correct pitch factor for a specific bone index - float GetBonePitchFactor( unsigned int boneIndex ) const; - // Returns the correct pitch offset for a specific bone index - float GetBonePitchOffset( unsigned int boneIndex ) const; - // animation float PlayAnimation( unsigned int turretIndex, const std::string& animName, const std::string& animNameIdle, float delay = 0.f ); void StopAnimation( unsigned int turretIndex, float delay = 0.f ); @@ -452,18 +421,8 @@ BLUE_CLASS( EveTurretSet ) : // system bones unsigned int m_systemBoneID[SYSBONE_MAX]; - // specific system bone values - float m_sysBoneHeight; - float m_sysBonePitchOffset; - float m_sysBonePitchFactor; - float m_sysBonePitchMin; - float m_sysBonePitchMax; - float m_sysBonePitch01Offset; - float m_sysBonePitch01Factor; - float m_sysBonePitch02Offset; - float m_sysBonePitch02Factor; - float m_sysBonePitch03Offset; - float m_sysBonePitch03Factor; + // sysbone aiming math + tuning (shared with EveChildTurret) + EveTurretAiming m_aiming; Matrix GetTurretBoneTransform( uint32_t closestTurret, uint32_t boneID ) const; diff --git a/trinity/Eve/Turret/EveTurretSet_Blue.cpp b/trinity/Eve/Turret/EveTurretSet_Blue.cpp index fbe2562be..430e3d223 100644 --- a/trinity/Eve/Turret/EveTurretSet_Blue.cpp +++ b/trinity/Eve/Turret/EveTurretSet_Blue.cpp @@ -88,17 +88,17 @@ const Be::ClassInfo* EveTurretSet::ExposeToBlue() MAP_ATTRIBUTE( "cyclingFireGroupCount", m_cyclingFireGroupCount, "The number of muzzles in one cycle group, usually only one.", Be::READWRITE | Be::PERSIST ) MAP_ATTRIBUTE( "currentCyclingFiresPos", m_currentCyclingFiresPos, "Current muzzle id due to cycling muzzles", Be::READ ) - MAP_ATTRIBUTE( "sysBoneHeight", m_sysBoneHeight, "System bone HEIGHT extension factor", Be::READWRITE | Be::PERSIST ) - MAP_ATTRIBUTE( "sysBonePitchFactor", m_sysBonePitchFactor, "main pitch factor", Be::READWRITE | Be::PERSIST ) - MAP_ATTRIBUTE( "sysBonePitchOffset", m_sysBonePitchOffset, "main pitch offset (in degrees!)", Be::READWRITE | Be::PERSIST ) - MAP_ATTRIBUTE( "sysBonePitchMin", m_sysBonePitchMin, "main pitch minimum clamp value, prevents the turret from targeting down too much (in degrees!)", Be::READWRITE | Be::PERSIST ) - MAP_ATTRIBUTE( "sysBonePitchMax", m_sysBonePitchMax, "main pitch maximum clamp value, prevents the turret from targeting down too much (in degrees!)", Be::READWRITE | Be::PERSIST ) - MAP_ATTRIBUTE( "sysBonePitch01Factor", m_sysBonePitch01Factor, "pitch 01 factor", Be::READWRITE | Be::PERSIST ) - MAP_ATTRIBUTE( "sysBonePitch01Offset", m_sysBonePitch01Offset, "pitch 01 offset (in degrees!)", Be::READWRITE | Be::PERSIST ) - MAP_ATTRIBUTE( "sysBonePitch02Factor", m_sysBonePitch02Factor, "pitch 02 factor", Be::READWRITE | Be::PERSIST ) - MAP_ATTRIBUTE( "sysBonePitch02Offset", m_sysBonePitch02Offset, "pitch 02 offset (in degrees!)", Be::READWRITE | Be::PERSIST ) - MAP_ATTRIBUTE( "sysBonePitch03Factor", m_sysBonePitch03Factor, "pitch 03 factor", Be::READWRITE | Be::PERSIST ) - MAP_ATTRIBUTE( "sysBonePitch03Offset", m_sysBonePitch03Offset, "pitch 03 offset (in degrees!)", Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE( "sysBoneHeight", m_aiming.m_sysBoneHeight, "System bone HEIGHT extension factor", Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE( "sysBonePitchFactor", m_aiming.m_sysBonePitchFactor, "main pitch factor", Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE( "sysBonePitchOffset", m_aiming.m_sysBonePitchOffset, "main pitch offset (in degrees!)", Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE( "sysBonePitchMin", m_aiming.m_sysBonePitchMin, "main pitch minimum clamp value, prevents the turret from targeting down too much (in degrees!)", Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE( "sysBonePitchMax", m_aiming.m_sysBonePitchMax, "main pitch maximum clamp value, prevents the turret from targeting down too much (in degrees!)", Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE( "sysBonePitch01Factor", m_aiming.m_sysBonePitch01Factor, "pitch 01 factor", Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE( "sysBonePitch01Offset", m_aiming.m_sysBonePitch01Offset, "pitch 01 offset (in degrees!)", Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE( "sysBonePitch02Factor", m_aiming.m_sysBonePitch02Factor, "pitch 02 factor", Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE( "sysBonePitch02Offset", m_aiming.m_sysBonePitch02Offset, "pitch 02 offset (in degrees!)", Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE( "sysBonePitch03Factor", m_aiming.m_sysBonePitch03Factor, "pitch 03 factor", Be::READWRITE | Be::PERSIST ) + MAP_ATTRIBUTE( "sysBonePitch03Offset", m_aiming.m_sysBonePitch03Offset, "pitch 03 offset (in degrees!)", Be::READWRITE | Be::PERSIST ) MAP_ATTRIBUTE( "firingEffect", m_firingEffect, "", Be::HIDDEN ) // Needed to make Graphite able to detect bindings inside of the firing effect. MAP_PROPERTY( "firingEffect", GetFiringEffect, SetFiringEffect, "The module for the firing effect of this turret" ) diff --git a/trinity/Eve/Turret/EveTurretTarget.cpp b/trinity/Eve/Turret/EveTurretTarget.cpp index a4126ae26..23ad70d8f 100644 --- a/trinity/Eve/Turret/EveTurretTarget.cpp +++ b/trinity/Eve/Turret/EveTurretTarget.cpp @@ -21,6 +21,7 @@ EveTurretTarget::EveTurretTarget( IRoot* lockobj ) : m_trackingPosition( 0.f, 0.f, 0.f ), m_positionOld( 0.f, 0.f, 0.f ), m_positionOldInfluence( -1.f ), + m_fadeOnLocatorChange( false ), m_positionMiss( 0.f, 0.f, 0.f ), m_missQueue( "EveTurretTarget::m_missQueue" ), m_lastShotIsMiss( false ), @@ -94,6 +95,15 @@ int EveTurretTarget::GetLocator() const return m_locator; } +// -------------------------------------------------------------------------------- +// Description: +// Smooth the aim over locator changes instead of snapping +// -------------------------------------------------------------------------------- +void EveTurretTarget::SetFadeOnLocatorChange( bool fade ) +{ + m_fadeOnLocatorChange = fade; +} + // -------------------------------------------------------------------------------- // Description: // Start the firing procedure at a given locator @@ -103,6 +113,12 @@ void EveTurretTarget::StartFireAtLocator( int l, float delay, float length, cons // remember this locator m_locator = l; + if( m_fadeOnLocatorChange ) + { + m_positionOld = m_trackingPosition; + m_positionOldInfluence = 1.f; + } + // randomize miss positionm m_randomMissDistanceOffset = TriFloatRandom01(); float u = TriFloatRandom01(), v = TriFloatRandom01(); @@ -147,8 +163,15 @@ void EveTurretTarget::StopFireAtLocator() { // clear out the locator m_locator = -1; - // stopp anyy influence - m_positionOldInfluence = -1.f; + if( m_fadeOnLocatorChange ) + { + m_positionOld = m_trackingPosition; + m_positionOldInfluence = 1.f; + } + else + { + m_positionOldInfluence = -1.f; + } // reset the miss-system m_lastShotIsMiss = false; m_missQueue.clear(); @@ -332,6 +355,15 @@ void EveTurretTarget::SetBehaviour( bool laserMiss, bool projectileMiss, float i { m_laserMissBehaviour = laserMiss; m_projectileMissBehaviour = projectileMiss; + SetImpactBehaviour( impactSize, impactBehaviour ); +} + +// -------------------------------------------------------------------------------- +// Description: +// Set the impact configuration, leaving miss behaviour disabled/untouched +// -------------------------------------------------------------------------------- +void EveTurretTarget::SetImpactBehaviour( float impactSize, ImpactBehaviour::Type impactBehaviour ) +{ m_impactSize = impactSize; m_impactBehaviour = impactBehaviour; } diff --git a/trinity/Eve/Turret/EveTurretTarget.h b/trinity/Eve/Turret/EveTurretTarget.h index d4de78f8c..94dbad2c5 100644 --- a/trinity/Eve/Turret/EveTurretTarget.h +++ b/trinity/Eve/Turret/EveTurretTarget.h @@ -36,6 +36,7 @@ BLUE_CLASS( EveTurretTarget ) : // access locator int GetLocator() const; + void SetFadeOnLocatorChange( bool fade ); void StartFireAtLocator( int l, float delay, float length, const Vector3* source ); void StopFireAtLocator(); const Vector3* GetTrackingPosition() const; @@ -48,6 +49,7 @@ BLUE_CLASS( EveTurretTarget ) : // hit/miss void SetBehaviour( bool laserMiss, bool projectileMiss, float impactSize, ImpactBehaviour::Type impactBehaviour ); + void SetImpactBehaviour( float impactSize, ImpactBehaviour::Type impactBehaviour ); bool GetShotMissed() const; void SetShotMissed( bool missed ); double GetLastShotTime() const; @@ -79,6 +81,7 @@ BLUE_CLASS( EveTurretTarget ) : Vector3 m_targetPosition; Vector3 m_positionOld; float m_positionOldInfluence; + bool m_fadeOnLocatorChange; // hit/miss related data Vector3 m_positionMiss; diff --git a/trinity/Include/ITr2PoseModifier.h b/trinity/Include/ITr2PoseModifier.h new file mode 100644 index 000000000..500e04a8a --- /dev/null +++ b/trinity/Include/ITr2PoseModifier.h @@ -0,0 +1,19 @@ +// Copyright © 2026 Fenris Creations ehf. + +#pragma once + +#ifndef ITR2POSEMODIFIER_H +#define ITR2POSEMODIFIER_H + +#include + +class ITr2PoseModifier +{ +public: + virtual void ModifyPose( const cmf::Skeleton& skeleton, cmf::SkeletonPose& pose ) = 0; + +protected: + ~ITr2PoseModifier() = default; +}; + +#endif //ITR2POSEMODIFIER_H diff --git a/trinity/Tr2GrannyAnimation.cpp b/trinity/Tr2GrannyAnimation.cpp index fc16f5c55..0147819eb 100644 --- a/trinity/Tr2GrannyAnimation.cpp +++ b/trinity/Tr2GrannyAnimation.cpp @@ -6,6 +6,7 @@ #include "Resources/TriGeometryRes.h" #include "Tr2Renderer.h" #include "include/ITr2DebugRenderer.h" +#include "Include/ITr2PoseModifier.h" #include "Utilities/BoundingBox.h" #include "Utilities/BoundingSphere.h" #include "Tr2VertexDefinitionUtilities.h" @@ -88,6 +89,7 @@ Tr2GrannyAnimation::Tr2GrannyAnimation( IRoot* lockobj ) : m_additiveMode( false ), m_aimingBone( false ), m_aimBone( "" ), + m_poseModifier( nullptr ), m_paused( false ), m_pauseTime( 0.f ), m_totalPauseOffset( 0.f ) @@ -1506,6 +1508,11 @@ void Tr2GrannyAnimation::EndAnimation() } +void Tr2GrannyAnimation::StopAnimations( float delay ) +{ + m_baseLayer.StopAnimations( delay ); +} + void Tr2GrannyAnimation::ClearAnimations() { m_baseLayer.ClearAnimations(); @@ -1695,6 +1702,13 @@ void Tr2GrannyAnimation::PrePhysicsAnimation( Be::Time time, const Matrix& model m_morphAnimations.clear(); + // sampling only writes bones referenced by active animations, so restore the + // sampled pose first or the pose modifier compounds onto its own output + if( m_poseModifier && m_sampledPose.skeleton == m_pose.skeleton && m_sampledPose.boneTransforms.size() == m_pose.boneTransforms.size() ) + { + m_pose = m_sampledPose; + } + m_baseLayer.SampleAnimation( animationTime, &m_pose, m_eventListener, m_morphAnimations ); for( auto& [_, layer] : m_animationLayers ) { @@ -1703,6 +1717,13 @@ void Tr2GrannyAnimation::PrePhysicsAnimation( Be::Time time, const Matrix& model UpdateAimingBone( skeleton ); + if( m_poseModifier ) + { + m_sampledPose = m_pose; + m_poseModifier->ModifyPose( skeleton, m_pose ); + } + + if( m_boneOffset.NeedRebind( (uint32_t)skeleton.bones.size() ) && skeleton.bones.size() ) { std::vector bones( skeleton.bones.size() ); @@ -2147,6 +2168,16 @@ void Tr2GrannyAnimation::DisableAimBone() m_aimingBone = false; } +ITr2PoseModifier* Tr2GrannyAnimation::GetPoseModifier() const +{ + return m_poseModifier; +} + +void Tr2GrannyAnimation::SetPoseModifier( ITr2PoseModifier* poseModifier ) +{ + m_poseModifier = poseModifier; +} + void Tr2GrannyAnimation::SetAdditiveBlendMode( bool additive ) { m_additiveMode = additive; @@ -2448,7 +2479,7 @@ std::pair Tr2AnimationMeshBinding::GetBoneTransforms() { GrannyColumnMatrixMultiply4x3Transpose( (granny_real32*)( (granny_matrix_3x4*)m_boneTransforms.get() )[i], - (granny_real32*)m_meshSkeleton->Bones[meshToBone[i]].InverseWorld4x4, + (granny_real32*)( m_meshSkeleton->Bones[meshToBone[i]].InverseWorld4x4 ), (granny_real32*)GrannyGetWorldPose4x4( m_animation->m_worldPose, animBones[i] ) ); } } diff --git a/trinity/Tr2GrannyAnimation.h b/trinity/Tr2GrannyAnimation.h index 90ade9265..1cd15b2aa 100644 --- a/trinity/Tr2GrannyAnimation.h +++ b/trinity/Tr2GrannyAnimation.h @@ -13,6 +13,7 @@ BLUE_DECLARE( TriGeometryRes ); BLUE_DECLARE( Tr2GrannyAnimation ); class Tr2AnimationMeshBinding; +class ITr2PoseModifier; namespace Tr2GrannyAnimationUtils { @@ -75,6 +76,7 @@ BLUE_CLASS( Tr2GrannyAnimation ) : bool PlayAnimation( const char* animName, bool replace, int loopCount, float delay, float speed, bool clearWhenDone = true ); bool PlayLayerAnimationByName( const char* layer, const char* animName, bool replace, int loopCount, float delay, float speed, bool clearWhenDone ); void EndAnimation(); + void StopAnimations( float delay ); void ClearAnimations(); float GetAnimationChainCompleteTime(); @@ -94,6 +96,9 @@ BLUE_CLASS( Tr2GrannyAnimation ) : void AimBone( const char* boneName, float target_x, float target_y, float target_z, float axis_x, float axis_y, float axis_z ); void DisableAimBone(); + ITr2PoseModifier* GetPoseModifier() const; + void SetPoseModifier( ITr2PoseModifier * poseModifier ); + void SetAdditiveBlendMode( bool additive ); bool GetAdditiveBlendMode(); @@ -192,6 +197,7 @@ BLUE_CLASS( Tr2GrannyAnimation ) : #endif cmf::SkeletonPose m_pose; cmf::SkeletonPose m_tmpPose; + cmf::SkeletonPose m_sampledPose; std::vector m_skeletonBoneIndices; std::vector m_worldTransforms; @@ -219,6 +225,8 @@ BLUE_CLASS( Tr2GrannyAnimation ) : Vector3 m_aimBoneOrientation; Vector3 m_aimAxis; + ITr2PoseModifier* m_poseModifier; + bool m_useMeshBinding; bool m_animationEnabled; diff --git a/trinity/Tr2GrannyAnimationLayer.cpp b/trinity/Tr2GrannyAnimationLayer.cpp index 3458c5d6a..5833eb006 100644 --- a/trinity/Tr2GrannyAnimationLayer.cpp +++ b/trinity/Tr2GrannyAnimationLayer.cpp @@ -457,6 +457,41 @@ void Tr2GrannyAnimationLayer::ClearMorphTracks( granny_control* control ) } #endif +void Tr2GrannyAnimationLayer::StopAnimations( float delay ) +{ + m_animationQueue.clear(); + + if( m_sequencer ) + { + const float animationTime = GetLayerAnimationTime(); + m_sequencer->EnumerateAnimations( [&]( const std::shared_ptr& player ) { + player->SetStopTime( animationTime + delay ); + if( delay <= 0.f ) + { + ClearMorphTracks( player.get() ); + } + } ); + m_sequencer->RemoveFinishedAnimations( animationTime ); + } + +#if WITH_GRANNY + if( m_modelInstance ) + { + for( granny_model_control_binding* binding = GrannyModelControlsBegin( m_modelInstance ); binding != GrannyModelControlsEnd( m_modelInstance ); ) + { + granny_control* control = GrannyGetControlFromBinding( binding ); + binding = GrannyModelControlsNext( binding ); + GrannyCompleteControlAt( control, GetLayerAnimationTime() + delay ); + if( GrannyFreeControlIfComplete( control ) ) + { + ClearTextTracks( control ); + ClearMorphTracks( control ); + } + } + } +#endif +} + void Tr2GrannyAnimationLayer::ClearAnimations() { m_animationQueue.clear(); diff --git a/trinity/Tr2GrannyAnimationLayer.h b/trinity/Tr2GrannyAnimationLayer.h index a4e902862..b6155f35e 100644 --- a/trinity/Tr2GrannyAnimationLayer.h +++ b/trinity/Tr2GrannyAnimationLayer.h @@ -70,6 +70,7 @@ class Tr2GrannyAnimationLayer bool PlayAnimation( const Tr2GrannyAnimation* grannyAnimation, const char* animName, bool replace, int loopCount, float delay, float speed, bool clearWhenDone ); void QueueAnimation( const char* animName, bool replace, int loopCount, float delay, float speed, bool clearWhenDone ); void EndAnimation(); + void StopAnimations( float delay ); void ClearAnimations(); float GetAnimationChainCompleteTime(); float GetAnimationRemainingTime(); diff --git a/trinity/Tr2GrannyAnimation_Blue.cpp b/trinity/Tr2GrannyAnimation_Blue.cpp index 35192d6a8..2ae7ab057 100644 --- a/trinity/Tr2GrannyAnimation_Blue.cpp +++ b/trinity/Tr2GrannyAnimation_Blue.cpp @@ -85,6 +85,12 @@ const Be::ClassInfo* Tr2GrannyAnimation::ExposeToBlue() ClearAnimations, "ClearAnimations()\n\n" "Abruptly ends all animations." ) + MAP_METHOD_AND_WRAP( + "StopAnimations", + StopAnimations, + "StopAnimations( delay )\n\n" + "Stops all animations, current and queued.\n" + ":param delay: time (in seconds) from now until playing animations stop" ) MAP_METHOD_AND_WRAP( "PlayLayerAnimation",