diff --git a/.clang-format b/.clang-format index 039810cca..db9ae9ed3 100644 --- a/.clang-format +++ b/.clang-format @@ -113,7 +113,10 @@ BreakBeforeBraces: Custom BreakBeforeTernaryOperators: false BreakConstructorInitializers: AfterColon BreakStringLiterals: false -ColumnLimit: 120 +# Kept at 0 to match the Cpp section: some tools (e.g. CLion's ClangFormat +# integration) misclassify C++ .h files as ObjC and would otherwise re-wrap +# lines at the column limit. +ColumnLimit: 0 CompactNamespaces: false ConstructorInitializerAllOnOneLineOrOnePerLine: true ConstructorInitializerIndentWidth: 4 diff --git a/trinity/CMakeLists.txt b/trinity/CMakeLists.txt index dc0e9c6e0..3aa2a7b29 100644 --- a/trinity/CMakeLists.txt +++ b/trinity/CMakeLists.txt @@ -569,6 +569,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 @@ -839,6 +842,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 54d5e1b52..70c7e738c 100644 --- a/trinity/Eve/SpaceObject/Children/EveChildMesh.h +++ b/trinity/Eve/SpaceObject/Children/EveChildMesh.h @@ -84,8 +84,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 ); @@ -193,7 +193,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..22dfa6c94 --- /dev/null +++ b/trinity/Eve/SpaceObject/Children/EveChildTurret.cpp @@ -0,0 +1,974 @@ +// 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" + +// names of system bones like they are in the cmf file +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 +}; + +// 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(); + + 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::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( s_systemBoneSkeletonNames[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]]; + ModifySystemBoneTransform( + static_cast( bone ), + &targetPosOS, + nullptr, + boneTransform.position, + boneTransform.rotation ); + } + } +} + +void EveChildTurret::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; + rotation = quat; + } + break; + case SYSBONE_COUNTER_ROTATION: { + // inverse(!!) rotation of turret 360 degress, 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; + 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 directionNormal = Normalize( *target ); + float height = TriClamp( directionNormal.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; + } +} + +void EveChildTurret::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; + rotation = quat; +} + +float EveChildTurret::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 EveChildTurret::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; + } +} +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..9a920b5f4 --- /dev/null +++ b/trinity/Eve/SpaceObject/Children/EveChildTurret.h @@ -0,0 +1,182 @@ +// Copyright © 2026 Fenris Creations ehf. + +#pragma once +#ifndef EveChildTurret_H +#define EveChildTurret_H + +#include "EveChildMesh.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 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: + // 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, + }; + + // setup the attached firing effect + void InitializeFiringEffect(); + + void InitializeAnimation() override; + + // 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; + + 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]; + // 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; + + // 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..ed17ee659 --- /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_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( "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 specfified 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/EveTurretSet.cpp b/trinity/Eve/Turret/EveTurretSet.cpp index 87efc0812..151c7f0ff 100644 --- a/trinity/Eve/Turret/EveTurretSet.cpp +++ b/trinity/Eve/Turret/EveTurretSet.cpp @@ -67,9 +67,9 @@ bool IsUsingCMF( TriGeometryRes* geometryResource ) // -------------------------------------------------------------------------------- // Description: -// Initialize data members, set everything to inlavid/empty and call -// ::PrepareResouce(), which will create a vertex decleration and a -// special istance buffer for the instance rendering. Also load the +// Initialize data members, set everything to invalid/empty and call +// ::PrepareResource(), which will create a vertex declaration and a +// special instance buffer for the instance rendering. Also load the // shader for shadow generation // -------------------------------------------------------------------------------- EveTurretSet::EveTurretSet( IRoot* lockobj ) : @@ -1419,7 +1419,7 @@ void EveTurretSet::UpdateSingleTurrets() // First thing to do is to keep the world-matrices of all turrets up to date, // cause most likely the ship has moved. Then sample the granny animation // for each single turret of this set, cause they are animated independently. -// Just before collapsing the skeleton matrices, sneek in a bone modification +// Just before collapsing the skeleton matrices, sneak in a bone modification // for the auto tracking. // Also smoothly do some fading between states and tracking positions. // SeeAlso: @@ -1564,7 +1564,7 @@ void EveTurretSet::UpdateTurretTransforms( const Matrix* turretTransformMatrix ) // -------------------------------------------------------------------------------- Matrix EveTurretSet::GetFiringBoneWorldTransform( unsigned int muzzle ) const { - // there MUST be an avtive turret aka a "firing turret"! + // there MUST be an active turret aka a "firing turret"! unsigned int closestTurret = m_activeTurret; // so if we don't have one, calc one temporarily if( closestTurret == INVALID_TURRET_INDEX ) @@ -1735,7 +1735,7 @@ void EveTurretSet::ModifySystemBoneTransform( SystemBones bone, const Vector3* t case SYSBONE_ROTATION: case SYSBONE_ROTATION01: case SYSBONE_ROTATION02: { - // rotation of turret 360 degress, alpha is between -pi and pi + // 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; @@ -2918,7 +2918,7 @@ void EveTurretSet::EnterStateIdle() break; case STATE_TARGETING: case STATE_FIRING: - // stop shooting, fadout tracking, then into active loop + // stop shooting, fadeout tracking, then into active loop m_delayToFadeOutTracking = 0.0001f; m_activeTurret = INVALID_TURRET_INDEX; m_target->StopFireAtLocator(); @@ -3178,7 +3178,7 @@ void EveTurretSet::EnterStateReloading() switch( m_state ) { case STATE_DEACTIVE: - // ingnore this state change: when the turret is inactive, no reload state can be shown! + // ignore this state change: when the turret is inactive, no reload state can be shown! break; case STATE_INVALID: case STATE_IDLE: @@ -3191,7 +3191,7 @@ void EveTurretSet::EnterStateReloading() break; case STATE_TARGETING: case STATE_FIRING: - // stop shooting, fadout tracking, then into active loop + // stop shooting, fadeout tracking, then into active loop m_delayToFadeOutTracking = 0.0001f; m_activeTurret = INVALID_TURRET_INDEX; m_target->StopFireAtLocator(); diff --git a/trinity/Eve/Turret/EveTurretTarget.cpp b/trinity/Eve/Turret/EveTurretTarget.cpp index a4126ae26..d3298ced1 100644 --- a/trinity/Eve/Turret/EveTurretTarget.cpp +++ b/trinity/Eve/Turret/EveTurretTarget.cpp @@ -332,6 +332,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..5a1769c0a 100644 --- a/trinity/Eve/Turret/EveTurretTarget.h +++ b/trinity/Eve/Turret/EveTurretTarget.h @@ -48,6 +48,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; diff --git a/trinity/Include/ITr2PoseModifier.h b/trinity/Include/ITr2PoseModifier.h new file mode 100644 index 000000000..1d23e3284 --- /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 0dbe67aa5..04adc45bb 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 ) @@ -1494,6 +1496,11 @@ void Tr2GrannyAnimation::EndAnimation() } +void Tr2GrannyAnimation::StopAnimations( float delay ) +{ + m_baseLayer.StopAnimations( delay ); +} + void Tr2GrannyAnimation::ClearAnimations() { m_baseLayer.ClearAnimations(); @@ -1691,6 +1698,12 @@ void Tr2GrannyAnimation::PrePhysicsAnimation( Be::Time time, const Matrix& model UpdateAimingBone( skeleton ); + if( m_poseModifier ) + { + m_poseModifier->ModifyPose( skeleton, m_pose ); + } + + if( m_boneOffset.NeedRebind( (uint32_t)skeleton.bones.size() ) && skeleton.bones.size() ) { std::vector bones( skeleton.bones.size() ); @@ -2133,6 +2146,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; @@ -2434,7 +2457,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..7b100211f 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(); @@ -219,6 +224,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",