diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/DeliverPayloadAIUpdate.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/DeliverPayloadAIUpdate.h index f028aabe93a..4dc5ad10018 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/DeliverPayloadAIUpdate.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/DeliverPayloadAIUpdate.h @@ -254,6 +254,9 @@ class DeliverPayloadData Coord3D m_dropVariance; UnsignedInt m_dropDelay; Bool m_fireWeapon; + + UnsignedInt m_fireWeaponSlots; + Bool m_selfDestructObject; Int m_visibleNumBones; ///< The number of visible bones to process. Real m_diveStartDistance; @@ -278,6 +281,7 @@ class DeliverPayloadData m_dropVariance.zero(); m_dropDelay = 0; m_fireWeapon = false; + m_fireWeaponSlots = 1u << PRIMARY_WEAPON; m_visibleNumBones = 0; m_diveStartDistance = 0.0f; m_diveEndDistance = 0.0f; @@ -326,6 +330,7 @@ class DeliverPayloadAIUpdate : public AIUpdateInterface const Coord3D& getDropOffset() const { return m_data.m_dropOffset; } const Coord3D& getDropVariance() const { return m_data.m_dropVariance; } Bool isFireWeapon() const { return m_data.m_fireWeapon; } + Bool shouldFireWeaponSlot(WeaponSlotType wslot) const { return (m_data.m_fireWeaponSlots & (1 << wslot)) != 0; } Int getVisibleItemsDelivered() const { return m_visibleItemsDelivered; } void setVisibleItemsDelivered( Int num ) { m_visibleItemsDelivered = num; } @@ -333,7 +338,7 @@ class DeliverPayloadAIUpdate : public AIUpdateInterface Bool isOffMap() const; Real calcMinTurnRadius(Real* timeToTravelThatDist) const; - void deliverPayload( const Coord3D *moveToPos, const Coord3D *targetPos, const DeliverPayloadData *data ); + void deliverPayload( const Coord3D *moveToPos, const Coord3D *targetPos, const DeliverPayloadData *data, const Coord3D *decalOffset = nullptr ); void deliverPayloadViaModuleData( const Coord3D *moveToPos ); const DeliverPayloadData* getData() { return &m_data; } diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/OCLUpdate.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/OCLUpdate.h index 3e7e0546a09..f9aa8c1e36b 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/OCLUpdate.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/OCLUpdate.h @@ -54,6 +54,9 @@ class OCLUpdateModuleData : public UpdateModuleData Bool m_isCreateAtEdge; ///< Otherwise, it is created on top of myself Bool m_isFactionTriggered; ///< Faction has to be present before update will happen + Bool m_isDirectionalDelivery; ///< Deliver payload aligned to source object + Bool m_isDirectionalDeliveryFurthestEdge; ///< always get the furthest edge matching angle + OCLUpdateModuleData(); static void buildFieldParse(MultiIniFieldParse& p); diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/TerrainLogic.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/TerrainLogic.h index 2b734de82d0..5608e0682c6 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/TerrainLogic.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/TerrainLogic.h @@ -234,6 +234,10 @@ class TerrainLogic : public Snapshot, virtual void getMaximumPathfindExtent( Region3D *extent ) const { DEBUG_CRASH(("not implemented")); } ///< @todo This should not be a stub - this should own this functionality virtual Coord3D findClosestEdgePoint( const Coord3D *closestTo ) const ; virtual Coord3D findFarthestEdgePoint( const Coord3D *farthestFrom ) const ; + + virtual Coord3D findEdgePointForAngle(const Coord3D* pos, Real angle, bool farthest = FALSE, bool closest = FALSE) const; + + virtual Bool isClearLineOfSight(const Coord3D& pos, const Coord3D& posOther) const; virtual AsciiString getSourceFilename( void ) { return m_filenameString; } diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Map/TerrainLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Map/TerrainLogic.cpp index 4e3fc7054b3..26d697481f5 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Map/TerrainLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Map/TerrainLogic.cpp @@ -2169,7 +2169,117 @@ Coord3D TerrainLogic::findFarthestEdgePoint( const Coord3D *farthestFrom ) const } +//------------------------------------------------------------------------------------------------- +/** Returns the ground aligned point on the bounding box closest to the given point*/ +//------------------------------------------------------------------------------------------------- +Coord3D TerrainLogic::findEdgePointForAngle(const Coord3D* pos, Real angle, bool farthest/* = FALSE*/, bool closest/* = FALSE*/) const +{ + Region3D mapExtent; + getExtent(&mapExtent); + + // Map bounds (XY only) + const Real minX = mapExtent.lo.x; + const Real minY = mapExtent.lo.y; + const Real maxX = mapExtent.hi.x; + const Real maxY = mapExtent.hi.y; + + const Real x0 = pos->x; + const Real y0 = pos->y; + + // Direction from angle + const Real dx = std::cos(angle); + const Real dy = std::sin(angle); + + // Small epsilon for float comparisons + const Real eps = static_cast(1e-6); + + struct Hit + { + Real t; + Real x; + Real y; + }; + + Hit hits[4]; + int hitCount = 0; + + auto addHit = [&](Real t) + { + if (t < 0) return; // only forward along the ray + + const Real x = x0 + t * dx; + const Real y = y0 + t * dy; + + // Keep only points that lie on the rectangle boundary + if (x >= minX - eps && x <= maxX + eps && + y >= minY - eps && y <= maxY + eps) + { + // Avoid duplicates (corner hits can be found twice) + for (int i = 0; i < hitCount; ++i) + { + if (std::abs(hits[i].x - x) < eps && + std::abs(hits[i].y - y) < eps) + { + return; + } + } + + hits[hitCount++] = { t, x, y }; + } + }; + + // Intersect ray P(t) = (x0,y0) + t(dx,dy), t >= 0 + // with the 4 rectangle edges + + // x = minX + if (std::abs(dx) > eps) + addHit((minX - x0) / dx); + + // x = maxX + if (std::abs(dx) > eps) + addHit((maxX - x0) / dx); + + // y = minY + if (std::abs(dy) > eps) + addHit((minY - y0) / dy); + + // y = maxY + if (std::abs(dy) > eps) + addHit((maxY - y0) / dy); + + // No hit should only happen if pos is invalid or outside map + if (hitCount == 0) + return *pos; + + // Default behavior: + // - if closest == true -> nearest hit + // - if farthest == true -> farthest hit + // - otherwise -> nearest forward hit + int best = 0; + if (farthest) + { + for (int i = 1; i < hitCount; ++i) + { + if (hits[i].t > hits[best].t) + best = i; + } + } + else // closest or default + { + for (int i = 1; i < hitCount; ++i) + { + if (hits[i].t < hits[best].t) + best = i; + } + } + + Coord3D result = *pos; + result.x = hits[best].x; + result.y = hits[best].y; + // preserve original Z (XY only matters) + return result; +} //------------------------------------------------------------------------------------------------- diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/ObjectCreationList.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/ObjectCreationList.cpp index 311742793e3..fa6fbd12cef 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/ObjectCreationList.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/ObjectCreationList.cpp @@ -259,6 +259,7 @@ class DeliverPayloadNugget : public ObjectCreationNugget m_payload.clear(); m_putInContainerName.clear(); m_transportName.clear(); + m_targetOffset = { 0.0, 0.0 }; } virtual Object* create(const Object *primaryObj, const Coord3D *primary, const Coord3D *secondary, Real angle, UnsignedInt lifetimeFrames = 0 ) const @@ -283,7 +284,9 @@ class DeliverPayloadNugget : public ObjectCreationNugget //resultant vectors to the initial vectors, we can calculate the delta positions for each plane. Real CCWx = 0.0f, CCWy = 0.0f, CWx = 0.0f, CWy = 0.0f; - if( m_formationSize > 1 ) + Real targetOffsetX = 0.0f, targetOffsetY = 0.0f; + + if (m_formationSize > 1 || m_targetOffset.x != 0.0f || m_targetOffset.y != 0.0f) { //Get the delta x and y values from the target to the origin. Real dx = primary->x - secondary->x; @@ -296,16 +299,19 @@ class DeliverPayloadNugget : public ObjectCreationNugget dx /= length; dy /= length; + targetOffsetX = dx * m_targetOffset.x - dy * m_targetOffset.y; + targetOffsetY = dy * m_targetOffset.x + dx * m_targetOffset.y; + //Rotate 90 degrees CCW. Real radians = 90.0f * PI / 180.0f; - Real s = Sin( radians ); - Real c = Cos( radians ); + Real s = Sin(radians); + Real c = Cos(radians); CCWx = dx * c + dy * -s + dx; CCWy = dx * s + dy * c + dy; //Rotate 90 degrees CW - s = Sin( -radians ); - c = Cos( -radians ); + s = Sin(-radians); + c = Cos(-radians); CWx = dx * c + dy * -s + dx; CWy = dx * s + dy * c + dy; } @@ -331,6 +337,9 @@ class DeliverPayloadNugget : public ObjectCreationNugget offset.y = CWy * offsetMultiplier; } + offset.x += targetOffsetX; + offset.y += targetOffsetY; + Coord3D startPos = *primary; Coord3D moveToPos = *secondary; startPos.add( &offset ); @@ -407,9 +416,9 @@ class DeliverPayloadNugget : public ObjectCreationNugget static NameKeyType key_DeliverPayloadAIUpdate = NAMEKEY("DeliverPayloadAIUpdate"); DeliverPayloadAIUpdate *ai = (DeliverPayloadAIUpdate*)transport->findUpdateModule(key_DeliverPayloadAIUpdate); - if( ai ) + if (ai) { - if( m_startAtMaxSpeed && createOwner ) + if (m_startAtMaxSpeed && createOwner) { PhysicsBehavior* physics = transport->getPhysics(); if (physics) @@ -420,15 +429,24 @@ class DeliverPayloadNugget : public ObjectCreationNugget startingForce.x *= factor; startingForce.y *= factor; startingForce.z *= factor; - physics->applyMotiveForce( &startingForce ); + physics->applyMotiveForce(&startingForce); } } // only the first guy in each formation gets a delivery decal DeliverPayloadData data = m_data; - if (formationIndex != 0) + if (formationIndex != 0) { data.m_deliveryDecalRadius = 0; - ai->deliverPayload( &moveToPos, &targetPos, &data ); + } + + if (targetOffsetX != 0.0 || targetOffsetY != 0.0) { + Coord3D decalOffset = { -targetOffsetX, -targetOffsetY }; + ai->deliverPayload(&moveToPos, &targetPos, &data, &decalOffset); + } + else { + ai->deliverPayload(&moveToPos, &targetPos, &data); + } + if( m_startAtPreferredHeight && createOwner ) { startPos.z = TheTerrainLogic->getGroundHeight(startPos.x, startPos.y) + ai->getCurLocomotor()->getPreferredHeight(); @@ -538,6 +556,8 @@ class DeliverPayloadNugget : public ObjectCreationNugget { "WeaponErrorRadius", INI::parseReal, nullptr, offsetof( DeliverPayloadNugget, m_errorRadius ) }, { "DelayDeliveryMax", INI::parseDurationUnsignedInt, nullptr, offsetof( DeliverPayloadNugget, m_delayDeliveryFramesMax ) }, + { "TargetOffset", INI::parseCoord2D, nullptr, offsetof( DeliverPayloadNugget, m_targetOffset ) }, + //Payload information (it's all created now and stored inside) { "Payload", parsePayload, nullptr, 0 }, { "PutInContainer", INI::parseAsciiString, nullptr, offsetof( DeliverPayloadNugget, m_putInContainerName) }, @@ -579,6 +599,8 @@ class DeliverPayloadNugget : public ObjectCreationNugget Bool m_startAtPreferredHeight; Bool m_startAtMaxSpeed; + Coord2D m_targetOffset; + //AI specific data passed over to DeliverPayloadAIUpdate::deliver() DeliverPayloadData m_data; }; diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/DeliverPayloadAIUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/DeliverPayloadAIUpdate.cpp index eee98cc06cc..648af6297d7 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/DeliverPayloadAIUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/DeliverPayloadAIUpdate.cpp @@ -52,6 +52,20 @@ //------------------------------------------------------------------------------------------------- +// duplicate from TurretAI +static void parseTWS(INI* ini, void* /*instance*/, void* store, const void* /*userData*/) +{ + UnsignedInt* tws = (UnsignedInt*)store; + const char* token = ini->getNextToken(); + while (token != nullptr) + { + WeaponSlotType wslot = (WeaponSlotType)INI::scanIndexList(token, TheWeaponSlotTypeNames); + *tws |= (1 << wslot); + token = ini->getNextTokenOrNull(); + } +} +// --- + const FieldParse* DeliverPayloadData::getFieldParse() { static const FieldParse dataFieldParse[] = @@ -80,6 +94,7 @@ const FieldParse* DeliverPayloadData::getFieldParse() //Weapon based payload { "FireWeapon", INI::parseBool, nullptr, offsetof( DeliverPayloadData, m_fireWeapon ) }, + { "FireWeaponSlots", parseTWS, nullptr, offsetof(DeliverPayloadData, m_fireWeaponSlots) }, //Specify an additional weaponslot to be fired while strafing { "DiveStartDistance", INI::parseReal, nullptr, offsetof( DeliverPayloadData, m_diveStartDistance ) }, @@ -239,7 +254,8 @@ UpdateSleepTime DeliverPayloadAIUpdate::update( void ) void DeliverPayloadAIUpdate::deliverPayload( const Coord3D *moveToPos, const Coord3D *targetPos, - const DeliverPayloadData *data + const DeliverPayloadData *data, + const Coord3D *decalOffset /* = NULL*/ ) { @@ -255,8 +271,18 @@ void DeliverPayloadAIUpdate::deliverPayload( m_data = *data; m_deliveryDecal.clear(); - m_data.m_deliveryDecalTemplate.createRadiusDecal(*targetPos, - m_data.m_deliveryDecalRadius, getObject()->getControllingPlayer(), m_deliveryDecal); + + if (decalOffset != nullptr) { + Coord3D decalPos = *targetPos; + decalPos.add(decalOffset); + m_data.m_deliveryDecalTemplate.createRadiusDecal(decalPos, + m_data.m_deliveryDecalRadius, getObject()->getControllingPlayer(), m_deliveryDecal); + } + else { + m_data.m_deliveryDecalTemplate.createRadiusDecal(*targetPos, + m_data.m_deliveryDecalRadius, getObject()->getControllingPlayer(), m_deliveryDecal); + } + if( m_data.m_diveStartDistance <= 0.0f ) { @@ -434,6 +460,7 @@ void DeliverPayloadAIUpdate::xfer( Xfer *xfer ) xfer->xferCoord3D(&data.m_dropVariance); xfer->xferUnsignedInt(&data.m_dropDelay); xfer->xferBool(&data.m_fireWeapon); + xfer->xferUnsignedInt(&data.m_fireWeaponSlots); xfer->xferBool(&data.m_selfDestructObject); xfer->xferInt(&data.m_visibleNumBones); xfer->xferReal(&data.m_diveStartDistance); @@ -715,7 +742,17 @@ StateReturnType DeliveringState::update() // Kick a dude out every so often pos.x += ai->getDropOffset().x; pos.y += ai->getDropOffset().y; pos.z += ai->getDropOffset().z; - owner->fireCurrentWeapon( &pos ); + + for (int i = 0; i < WEAPONSLOT_COUNT; i++) { + WeaponSlotType wslot = (WeaponSlotType)i; + if (ai->shouldFireWeaponSlot(wslot)) { + owner->setWeaponLock(wslot, LOCKED_TEMPORARILY); + owner->fireCurrentWeapon(&pos); + } + } + //owner->fireCurrentWeapon( &pos ); + + TheGameLogic->destroyObject( item ); } else diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/OCLUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/OCLUpdate.cpp index 52fcf359865..e645fd70266 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/OCLUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/OCLUpdate.cpp @@ -97,6 +97,8 @@ OCLUpdateModuleData::OCLUpdateModuleData() { "MaxDelay", INI::parseDurationUnsignedInt, nullptr, offsetof( OCLUpdateModuleData, m_maxDelay ) }, { "CreateAtEdge", INI::parseBool, nullptr, offsetof( OCLUpdateModuleData, m_isCreateAtEdge ) }, { "FactionTriggered", INI::parseBool, nullptr, offsetof( OCLUpdateModuleData, m_isFactionTriggered ) }, + { "DirectionalDelivery", INI::parseBool, nullptr, offsetof( OCLUpdateModuleData, m_isDirectionalDelivery) }, + { "DirectionalDeliveryFurthestEdge", INI::parseBool, nullptr, offsetof( OCLUpdateModuleData, m_isDirectionalDeliveryFurthestEdge) }, { nullptr, nullptr, nullptr, 0 } }; p.add(dataFieldParse); @@ -190,10 +192,18 @@ UpdateSleepTime OCLUpdate::update( void ) setNextCreationFrame(); Coord3D creationCoord; - if( getOCLUpdateModuleData()->m_isCreateAtEdge ) - creationCoord = TheTerrainLogic->findClosestEdgePoint( getObject()->getPosition() ); + if (data->m_isCreateAtEdge) { + if (data->m_isDirectionalDelivery) { + creationCoord = TheTerrainLogic->findEdgePointForAngle(getObject()->getPosition(), getObject()->getOrientation(), data->m_isDirectionalDeliveryFurthestEdge, FALSE); + } + else { + creationCoord = TheTerrainLogic->findClosestEdgePoint(getObject()->getPosition()); + } + } else + { creationCoord = *getObject()->getPosition(); + } // If this is faction triggered, search through the faction specific OCLs to find the match if (data->m_isFactionTriggered)