// Entity state-machine processing code. #include "precompiled.h" #include "Entity.h" #include "EntityTemplate.h" #include "graphics/Model.h" #include "graphics/ObjectEntry.h" #include "graphics/SkeletonAnim.h" #include "graphics/SkeletonAnimDef.h" // Animation duration #include "graphics/Unit.h" #include "ProductionQueue.h" #include "maths/MathUtil.h" #include "Collision.h" #include "PathfindEngine.h" #include "graphics/Terrain.h" #include "ps/Game.h" #include "ps/World.h" enum EGotoSituation { NORMAL = 0, ALREADY_AT_DESTINATION, REACHED_DESTINATION, COLLISION_WITH_DESTINATION, COLLISION_NEAR_DESTINATION, COLLISION_OVERLAPPING_OBJECTS, COLLISION_OTHER, WOULD_LEAVE_MAP }; float CEntity::processChooseMovement( float distance ) { // Should we run or walk if (entf_get(ENTF_SHOULD_RUN) && m_staminaCurr > 0 && distance < m_runMaxRange && ( distance > m_runMinRange || entf_get(ENTF_IS_RUNNING) ) ) { if ( m_actor ) { if ( !m_actor->IsPlayingAnimation( "run" ) ) { m_actor->SetEntitySelection( L"run" ); m_actor->SetRandomAnimation( "run", false, m_runSpeed ); // Animation desync m_actor->GetModel()->Update( rand( 0, 1000 ) / 1000.0f ); entf_set(ENTF_IS_RUNNING); } } return m_runSpeed; } else { if ( m_actor ) { // Should we update animation? if ( !m_actor->IsPlayingAnimation( "walk" ) ) { m_actor->SetEntitySelection( L"walk" ); m_actor->SetRandomAnimation( "walk", false, m_speed ); // Animation desync m_actor->GetModel()->Update( rand( 0, 1000 ) / 1000.0f ); entf_clear(ENTF_IS_RUNNING); } } return m_speed; } } // Does all the shared processing for line-of-sight gotos uint CEntity::processGotoHelper( CEntityOrder* current, size_t timestep_millis, HEntity& collide ) { float timestep=timestep_millis/1000.0f; CVector2D delta; delta.x = (float)current->m_data[0].location.x - m_position.X; delta.y = (float)current->m_data[0].location.y - m_position.Z; float len = delta.length(); if( len < 0.1f ) return( ALREADY_AT_DESTINATION ); // Curve smoothing. // Here there be trig. float scale = processChooseMovement( len ) * timestep; // Note: Easy optimization: flag somewhere that this unit // is already pointing the way, and don't do this // trig every time.right m_targetorientation = atan2( delta.x, delta.y ); float deltatheta = m_targetorientation - (float)m_orientation.Y; while( deltatheta > PI ) deltatheta -= 2 * PI; while( deltatheta < -PI ) deltatheta += 2 * PI; if( fabs( deltatheta ) > 0.01f ) { if ( m_turningRadius != 0 ) { float maxTurningSpeed = ( m_speed / m_turningRadius ) * timestep; deltatheta = clamp( deltatheta, -maxTurningSpeed, maxTurningSpeed ); } m_orientation.Y = m_orientation.Y + deltatheta; m_ahead.x = sin( m_orientation.Y ); m_ahead.y = cos( m_orientation.Y ); // We can only really attempt to smooth paths the pathfinder // has flagged for us. If the turning-radius calculations are // applied to other types of waypoint, weirdness happens. // Things like an entity trying to walk to a point inside // his turning radius (which he can't do directly, so he'll // orbit the point indefinitely), or just massive deviations // making the paths we calculate useless. // It's also painful trying to watch two entities resolve their // collision when they're both bound by turning constraints. // So, as a compromise for the look of the thing, we'll just turn in // place until we're looking the right way. At least, that's what // seems logical. But in most cases that looks worse. So actually, // let's not. if( current->m_type != CEntityOrder::ORDER_GOTO_SMOOTHED ) m_orientation.Y = m_targetorientation; } else { m_ahead = delta / len; m_orientation.Y = m_targetorientation; } CEntity* _this = this; CVector2D targetXZ = g_Game->GetWorld()->GetTerrain()->getSlopeAngleFace( m_position.X, m_position.Z, _this ); while( targetXZ.x > PI ) targetXZ.x -= 2 * PI; while( targetXZ.x < -PI ) targetXZ.x += 2 * PI; while( targetXZ.y > PI ) targetXZ.y -= 2 * PI; while( targetXZ.y < -PI ) targetXZ.y += 2 * PI; float cX = m_base->m_anchorConformX, cZ = m_base->m_anchorConformZ; m_orientation.X = clamp( targetXZ.x, -cX, cX ); m_orientation.Z = clamp( targetXZ.y, -cZ, cZ ); m_orientation_unclamped.x = targetXZ.x; m_orientation_unclamped.y = targetXZ.y; CMovementEvent evt( m_orientation_unclamped.x ); DispatchEvent(&evt); if( m_bounds && m_bounds->m_type == CBoundingObject::BOUND_OABB ) ((CBoundingBox*)m_bounds)->setOrientation( m_ahead ); EGotoSituation rc = NORMAL; if( scale > len ) { scale = len; rc = REACHED_DESTINATION; } delta = m_ahead * scale; // What would happen if we moved forward a little? m_position.X += delta.x; m_position.Z += delta.y; if( m_bounds ) { m_bounds->setPosition( m_position.X, m_position.Z ); collide = getCollisionObject( this ); if( collide ) { // We'd hit something. Let's not. m_position.X -= delta.x; m_position.Z -= delta.y; m_bounds->m_pos -= delta; // Is it too late to avoid the collision? if( collide->m_bounds->intersects( m_bounds ) ) { // Yes. Oh dear. That can't be good. // This really shouldn't happen in the current build. debug_assert( false && "Overlapping objects" ); // Erm... do nothing? return( COLLISION_OVERLAPPING_OBJECTS ); } // No. Is our destination within the obstacle? if( collide->m_bounds->contains( current->m_data[0].location ) ) return( COLLISION_WITH_DESTINATION ); // No. Are we nearing our destination, do we wish to stop there, and is it obstructed? if( ( m_orderQueue.size() == 1 ) && ( len <= 10.0f ) ) { CBoundingCircle destinationObs( current->m_data[0].location.x, current->m_data[0].location.y, m_bounds->m_radius, 0.0f ); if( getCollisionObject( &destinationObs ) ) { // Yes. (Chances are a bunch of units were tasked to the same destination) return( COLLISION_NEAR_DESTINATION ); } } // No? return( COLLISION_OTHER ); } } // Will we step off the map? if( !g_Game->GetWorld()->GetTerrain()->isOnMap( m_position.X, m_position.Z ) ) { // Yes. That's not a particularly good idea, either. m_position.X -= delta.x; m_position.Z -= delta.y; if( m_bounds ) m_bounds->setPosition( m_position.X, m_position.Z ); // All things being equal, we should only get here while on a collision path // (No destination should be off the map) return( WOULD_LEAVE_MAP ); } // No. I suppose it's OK to go there, then. *disappointed* return( rc ); } bool CEntity::processGotoNoPathing( CEntityOrder* current, size_t timestep_millis ) { HEntity collide; switch( processGotoHelper( current, timestep_millis, collide ) ) { case ALREADY_AT_DESTINATION: // If on a collision path; decide where to go next. Otherwise, proceed to the next waypoint. if( current->m_type == CEntityOrder::ORDER_GOTO_COLLISION ) { repath(); } else { m_orderQueue.pop_front(); //entf_clear(ENTF_IS_RUNNING); //entf_clear(ENTF_SHOULD_RUN); } return( false ); case COLLISION_OVERLAPPING_OBJECTS: return( false ); case COLLISION_WITH_DESTINATION: // We're here... m_orderQueue.pop_front(); //entf_clear(ENTF_IS_RUNNING); //entf_clear(ENTF_SHOULD_RUN); return( false ); case COLLISION_NEAR_DESTINATION: { // Here's a weird idea: (I hope it works) // Spiral round the destination until a free point is found. CBoundingCircle destinationObs( current->m_data[0].location.x, current->m_data[0].location.y, m_bounds->m_radius, 0.0f ); float interval = destinationObs.m_radius; float r = interval, theta = 0.0f, delta; float _x = current->m_data[0].location.x, _y = current->m_data[0].location.y; while( true ) { delta = interval / r; theta += delta; r += ( interval * delta ) / ( 2 * PI ); destinationObs.setPosition( _x + r * cosf( theta ), _y + r * sinf( theta ) ); if( !getCollisionObject( &destinationObs ) ) break; } // Reset our destination current->m_data[0].location.x = _x + r * cosf( theta ); current->m_data[0].location.y = _y + r * sinf( theta ); return( false ); } case COLLISION_OTHER: { // Path around it. CEntityOrder avoidance; avoidance.m_type = CEntityOrder::ORDER_GOTO_COLLISION; CVector2D right; right.x = m_ahead.y; right.y = -m_ahead.x; CVector2D avoidancePosition; // Which is the shortest diversion, going left or right? // (Weight a little towards the right, to stop both units dodging the same way) if( ( collide->m_bounds->m_pos - m_bounds->m_pos ).dot( right ) < 1 ) { // Turn right. avoidancePosition = collide->m_bounds->m_pos + right * ( collide->m_bounds->m_radius + m_bounds->m_radius * 2.5f ); } else { // Turn left. avoidancePosition = collide->m_bounds->m_pos - right * ( collide->m_bounds->m_radius + m_bounds->m_radius * 2.5f ); } // Create a short path representing this detour avoidance.m_data[0].location = avoidancePosition; if( current->m_type == CEntityOrder::ORDER_GOTO_COLLISION ) m_orderQueue.pop_front(); m_orderQueue.push_front( avoidance ); return( false ); } case WOULD_LEAVE_MAP: // Just stop here, repath if necessary. m_orderQueue.pop_front(); //entf_clear(ENTF_IS_RUNNING); //entf_clear(ENTF_SHOULD_RUN); return( false ); default: return( false ); } } // Handles processing common to (at the moment) gather and melee attack actions bool CEntity::processContactAction( CEntityOrder* current, size_t UNUSED(timestep_millis), int transition, SEntityAction* action ) { if( !current->m_data[0].entity || !current->m_data[0].entity->m_extant ) return( false ); current->m_data[0].location = current->m_data[0].entity->m_position; float Distance = (current->m_data[0].location - m_position).length(); if( Distance < action->m_MaxRange ) { (int&)current->m_type = transition; entf_clear(ENTF_IS_RUNNING); return( true ); } processChooseMovement( Distance ); // The pathfinder will push its result back into this unit's queue and // add back the current order at the end with the transition type. (int&)current->m_type = transition; g_Pathfinder.requestContactPath( me, current ); return( true ); } bool CEntity::processContactActionNoPathing( CEntityOrder* current, size_t timestep_millis, const CStr& animation, CScriptEvent* contactEvent, SEntityAction* action ) { if( m_fsm_cyclepos != NOT_IN_CYCLE ) { size_t nextpos = m_fsm_cyclepos + timestep_millis * 2; if( ( m_fsm_cyclepos <= m_fsm_anipos ) && ( nextpos > m_fsm_anipos ) ) { // Start playing. // Start the animation. Actual damage/gather will be done in a // few hundred ms, at the 'action point' of the animation we're // now setting. entf_clear(ENTF_IS_RUNNING); // TODO: this is set to be looping, because apparently it otherwise // plays one frame of 'idle' after e.g. attacks. But this way means // animations sometimes play ~1.5 times then repeat, which looks // broken too. //m_actor->GetModel()->SetAnimation( m_fsm_animation, true, 1000.0f * m_fsm_animation->m_AnimDef->GetDuration() / (float)action->m_Speed, m_actor->GetRandomAnimation( "idle" ) ); m_actor->GetModel()->SetAnimation( m_fsm_animation, false, 1000.0f * m_fsm_animation->m_AnimDef->GetDuration() / (float)action->m_Speed ); } if( ( m_fsm_cyclepos <= m_fsm_anipos2 ) && ( nextpos > m_fsm_anipos2 ) ) { // Load the ammunition. m_actor->ShowAmmunition(); } if( ( m_fsm_cyclepos <= action->m_Speed ) && ( nextpos > action->m_Speed ) ) { m_actor->HideAmmunition(); if(!DispatchEvent( contactEvent )) { // Cancel current order popOrder(); entf_clear(ENTF_IS_RUNNING); entf_clear(ENTF_SHOULD_RUN); m_actor->SetEntitySelection( L"idle" ); m_actor->SetRandomAnimation( "idle" ); return( false ); } } if( nextpos >= ( action->m_Speed * 2 ) ) { // End of cycle. m_fsm_cyclepos = NOT_IN_CYCLE; return( false ); } // Otherwise, increment position. m_fsm_cyclepos = nextpos; return( false ); } // Target's dead (or exhausted), or we cancelled? Then our work here is done. if( !current->m_data[0].entity || !current->m_data[0].entity->m_extant ) { //TODO: eventually when stances/formations are implemented, if applicable (e.g. not //heal or if defensive stance), the unit should expand and continue the order. popOrder(); entf_clear(ENTF_IS_RUNNING); entf_clear(ENTF_SHOULD_RUN); return( false ); } CVector2D delta = current->m_data[0].entity->m_position - m_position; float deltaLength = delta.length(); float adjRange = action->m_MaxRange + m_bounds->m_radius + current->m_data[0].entity->m_bounds->m_radius; if( action->m_MinRange > 0.0f ) { float adjMinRange = action->m_MinRange + m_bounds->m_radius + current->m_data[0].entity->m_bounds->m_radius; if( delta.within( adjMinRange ) ) { // Too close... do nothing. entf_clear(ENTF_IS_RUNNING); entf_clear(ENTF_SHOULD_RUN); return( false ); } } if( !delta.within( adjRange ) ) { // Too far away at the moment, chase after the target... // We're aiming to end up at a location just inside our maximum range // (is this good enough?) delta = delta.normalize() * ( adjRange - m_bounds->m_radius ); processChooseMovement(deltaLength); current->m_data[0].location = (CVector2D)current->m_data[0].entity->m_position - delta; HEntity collide; switch( processGotoHelper( current, timestep_millis, collide ) ) { case ALREADY_AT_DESTINATION: case REACHED_DESTINATION: case COLLISION_WITH_DESTINATION: // Not too far any more... break; case NORMAL: // May or may not be close enough, check... // (Assuming the delta above will never take us within minimum range) delta = current->m_data[0].entity->m_position - m_position; if( delta.within( adjRange ) ) break; // Otherwise, continue chasing return( false ); default: // Path around it. CEntityOrder avoidance; avoidance.m_type = CEntityOrder::ORDER_GOTO_COLLISION; CVector2D right; right.x = m_ahead.y; right.y = -m_ahead.x; CVector2D avoidancePosition; // Which is the shortest diversion, going left or right? // (Weight a little towards the right, to stop both units dodging the same way) if( ( collide->m_bounds->m_pos - m_bounds->m_pos ).dot( right ) < 1 ) { // Turn right. avoidancePosition = collide->m_bounds->m_pos + right * ( collide->m_bounds->m_radius + m_bounds->m_radius * 2.5f ); } else { // Turn left. avoidancePosition = collide->m_bounds->m_pos - right * ( collide->m_bounds->m_radius + m_bounds->m_radius * 2.5f ); } // Create a short path representing this detour avoidance.m_data[0].location = avoidancePosition; if( current->m_type == CEntityOrder::ORDER_GOTO_COLLISION ) m_orderQueue.pop_front(); m_orderQueue.push_front( avoidance ); return( false ); } } else { // Close enough, but turn to face them. m_orientation.Y = atan2( delta.x, delta.y ); m_ahead = delta.normalize(); entf_clear(ENTF_IS_RUNNING); } // Pick our animation, calculate the time to play it, and start the timer. m_actor->SetEntitySelection( animation ); m_fsm_animation = m_actor->GetRandomAnimation( animation ); // Here's the idea - we want to be at that animation's event point // when the timer reaches action->m_Speed. The timer increments by 2 every millisecond. // animation->m_actionpos is the time offset into that animation that event // should happen. So... m_fsm_anipos = (size_t)( action->m_Speed * ( 1.0f - 2 * m_fsm_animation->m_ActionPos ) ); // But... if( m_fsm_anipos < 0 ) // (FIXME: m_fsm_anipos is unsigned, so this will never be true...) { // We ought to have started it in the past. Oh well. // Here's what we'll do: play it now, and advance it to // the point it should be by now. m_actor->GetModel()->SetAnimation( m_fsm_animation, true, 1000.0f * m_fsm_animation->m_AnimDef->GetDuration() / (float)action->m_Speed, m_actor->GetRandomAnimation( "idle" ) ); m_actor->GetModel()->Update( action->m_Speed * ( m_fsm_animation->m_ActionPos / 1000.0f - 0.0005f ) ); } else { // If we've just transitioned, play idle. Otherwise, let the previous animation complete, if it // hasn't already. if( entf_get(ENTF_TRANSITION) ) { // (don't change actor's entity-selection) m_actor->SetRandomAnimation( "idle" ); } } // Load time needs to be animation->m_ActionPos2 ms after the start of the animation. m_fsm_anipos2 = m_fsm_anipos + (size_t)( action->m_Speed * m_fsm_animation->m_ActionPos2 * 2 ); if( m_fsm_anipos2 < 0 ) // (FIXME: m_fsm_anipos2 is unsigned, so this will never be true...) { // Load now. m_actor->ShowAmmunition(); } m_fsm_cyclepos = 0; return( false ); } bool CEntity::processGeneric( CEntityOrder* current, size_t timestep_millis ) { int id = current->m_data[1].data; if( m_actions.find( id ) == m_actions.end() ) { return false; // we've been tasked as part of a group but we can't do this action } SEntityAction& action = m_actions[id]; return( processContactAction( current, timestep_millis, CEntityOrder::ORDER_GENERIC_NOPATHING, &action ) ); } bool CEntity::processGenericNoPathing( CEntityOrder* current, size_t timestep_millis ) { int id = current->m_data[1].data; if( m_actions.find( id ) == m_actions.end() ) { return false; // we've been tasked as part of a group but we can't do this action } SEntityAction& action = m_actions[id]; CEventGeneric evt( current->m_data[0].entity, id ); if( !m_actor ) return( false ); return( processContactActionNoPathing( current, timestep_millis, action.m_Animation, &evt, &action ) ); } bool CEntity::processGoto( CEntityOrder* current, size_t UNUSED(timestep_millis) ) { // float timestep=timestep_millis/1000.0f; // janwas: currently unused CVector2D pos( m_position.X, m_position.Z ); CVector2D path_to = current->m_data[0].location; m_orderQueue.pop_front(); float Distance = ( path_to - pos ).length(); // Let's just check we're going somewhere... if( Distance < 0.1f ) { //entf_clear(ENTF_IS_RUNNING); //entf_clear(ENTF_SHOULD_RUN); return( false ); } processChooseMovement( Distance ); // The pathfinder will push its result back into this unit's queue. g_Pathfinder.requestPath( me, path_to ); return( true ); } bool CEntity::processGotoWaypoint( CEntityOrder* current, size_t UNUSED(timestep_milli), bool contact ) { CVector2D pos( m_position.X, m_position.Z ); CVector2D path_to = current->m_data[0].location; m_orderQueue.pop_front(); float Distance = ( path_to - pos ).length(); // Let's just check we're going somewhere... if( Distance < 0.1f ) { entf_clear(ENTF_IS_RUNNING); //entf_clear(ENTF_SHOULD_RUN); return( false ); } processChooseMovement( Distance ); g_Pathfinder.requestLowLevelPath( me, path_to, contact ); return( true ); } bool CEntity::processPatrol( CEntityOrder* current, size_t UNUSED(timestep_millis) ) { // float timestep=timestep_millis/1000.0f; // janwas: currently unused CEntityOrder this_segment; CEntityOrder repeat_patrol; // Duplicate the patrol order, push one copy onto the start of our order queue // (that's the path we'll be taking next) and one copy onto the end of the // queue (to keep us patrolling) this_segment.m_type = CEntityOrder::ORDER_GOTO; this_segment.m_data[0] = current->m_data[0]; repeat_patrol.m_type = CEntityOrder::ORDER_PATROL; repeat_patrol.m_data[0] = current->m_data[0]; m_orderQueue.pop_front(); m_orderQueue.push_front( this_segment ); m_orderQueue.push_back( repeat_patrol ); return( true ); } bool CEntity::processProduce( CEntityOrder* order ) { int type = order->m_data[1].data; CStrW name = order->m_data[0].string; CEventStartProduction evt( type, name ); if( DispatchEvent( &evt ) && evt.GetTime() >= 0 ) { m_productionQueue->AddItem( type, name, evt.GetTime() ); } return( false ); }