diff --git a/Core/GameEngine/Include/GameClient/TerrainRoads.h b/Core/GameEngine/Include/GameClient/TerrainRoads.h index 65765515cd7..3cfe9fa07b6 100644 --- a/Core/GameEngine/Include/GameClient/TerrainRoads.h +++ b/Core/GameEngine/Include/GameClient/TerrainRoads.h @@ -98,6 +98,16 @@ class TerrainRoadType : public MemoryPoolObject Real getTransitionEffectsHeight() { return m_transitionEffectsHeight; } Int getNumFXPerType() { return m_numFXPerType; } Real getBridgeHoleAreaPercentage() { return m_bridgeHoleAreaPercentage; } + Bool isDestroyable( void ) { return m_isDestroyable; } + AsciiString getBridgeObjectName( void ) { return m_bridgeObjectName; } + Real getBridgeDeckHeight( void ) { return m_bridgeDeckHeight; } + UnsignedInt getBridgeCollapseDuration( void ) { return m_bridgeCollapseDuration; } + Real getBridgeCollapseDrop( void ) { return m_bridgeCollapseDrop; } + Real getBridgeCollapseTilt( void ) { return m_bridgeCollapseTilt; } + Real getBridgeCollapseStagger( void ) { return m_bridgeCollapseStagger; } + UnsignedInt getBridgeRebuildDuration( void ) { return m_bridgeRebuildDuration; } + Real getBridgeCollapseSingleSpanRoll( void ) { return m_bridgeCollapseSingleSpanRoll; } + Real getBridgeCollapseSingleSpanDrop( void ) { return m_bridgeCollapseSingleSpanDrop; } // friend access methods to be used by the road collection only! void friend_setName( AsciiString name ) { m_name = name; } @@ -127,6 +137,18 @@ class TerrainRoadType : public MemoryPoolObject void friend_setRepairedToFXString( BodyDamageType state, Int index, AsciiString s ) { m_repairedToFXString[ state ][ index ] = s; } void friend_setTransitionEffectsHeight( Real height ) { m_transitionEffectsHeight = height; } void friend_setNumFXPerType( Int num ) { m_numFXPerType = num; } + void friend_setBridgeHoleAreaPercentage( Real percentage ) { m_bridgeHoleAreaPercentage = percentage; } + void friend_setDestroyable( Bool destroyable ) { m_isDestroyable = destroyable; } + void friend_setBridgeObjectName( AsciiString name ) { m_bridgeObjectName = name; } + void friend_setBridgeDeckHeight( Real height ) { m_bridgeDeckHeight = height; } + void friend_setBridgeCollapseDuration( UnsignedInt frames ) { m_bridgeCollapseDuration = frames; } + void friend_setBridgeCollapseDrop( Real drop ) { m_bridgeCollapseDrop = drop; } + void friend_setBridgeCollapseTilt( Real tilt ) { m_bridgeCollapseTilt = tilt; } + void friend_setBridgeCollapseStagger( Real stagger ) { m_bridgeCollapseStagger = stagger; } + void friend_setBridgeRebuildDuration( UnsignedInt frames ) { m_bridgeRebuildDuration = frames; } + void friend_setBridgeCollapseSingleSpanRoll( Real roll ) { m_bridgeCollapseSingleSpanRoll = roll; } + void friend_setBridgeCollapseSingleSpanDrop( Real drop ) { m_bridgeCollapseSingleSpanDrop = drop; } + void friend_setRadarColor( RGBColor color ) { m_radarColor = color; } /// get the parsing table for INI const FieldParse *getRoadFieldParse() { return m_terrainRoadFieldParseTable; } @@ -190,6 +212,38 @@ class TerrainRoadType : public MemoryPoolObject Int m_numFXPerType; ///< for *each* fx/ocl we will make this many of them on the bridge area Real m_bridgeHoleAreaPercentage; ///< if bridge is openable/destroyable, how much % of length becomes open + + // + // non landmark bridges are drawn procedurally and are represented in the logic by an object + // created from m_bridgeObjectName; when destroyable they also get the 4 targetable towers + // that landmark bridges have + // + Bool m_isDestroyable; ///< true if this bridge can be destroyed + AsciiString m_bridgeObjectName; ///< object representing the bridge span in the logic + + // + // the deck plane is the driving surface; a thick deck, girders or arches hang below it and + // take away room from anything passing underneath + // + Real m_bridgeDeckHeight; ///< thickness of the deck below the driving surface + + // + // a sectional bridge deck is baked into a shared vertex buffer and has no skeleton, so it + // cannot play a model animation. these drive a procedural fold-and-drop of the span sections + // instead. all default to zero, which reproduces the old instant model swap exactly. + // + UnsignedInt m_bridgeCollapseDuration; ///< frames the collapse animation runs, 0 disables it + Real m_bridgeCollapseDrop; ///< world units a fully collapsed section falls + Real m_bridgeCollapseTilt; ///< radians a fully collapsed section folds by + Real m_bridgeCollapseStagger; ///< 0..1 of the duration spent rippling out from mid span + UnsignedInt m_bridgeRebuildDuration; ///< frames the rebuild animation runs, 0 disables it + + // + // a bridge short enough to resolve to a single span section has nothing to fold against, so + // it banks sideways and sinks instead of hinging. a zero drop falls back to the value above. + // + Real m_bridgeCollapseSingleSpanRoll; ///< radians a one-span deck banks by, sign picks the side + Real m_bridgeCollapseSingleSpanDrop; ///< world units a one-span deck sinks, 0 means use the normal drop }; //------------------------------------------------------------------------------------------------- diff --git a/Core/GameEngine/Source/GameClient/Terrain/TerrainRoads.cpp b/Core/GameEngine/Source/GameClient/Terrain/TerrainRoads.cpp index 09e90bcc55a..147839f90e6 100644 --- a/Core/GameEngine/Source/GameClient/Terrain/TerrainRoads.cpp +++ b/Core/GameEngine/Source/GameClient/Terrain/TerrainRoads.cpp @@ -85,6 +85,16 @@ const FieldParse TerrainRoadType::m_terrainBridgeFieldParseTable[] = { "TransitionToOCL", parseTransitionToOCL, nullptr, 0 }, { "TransitionToFX", parseTransitionToFX, nullptr, 0 }, { "BridgeHoleAreaPercentage", INI::parsePercentToReal, nullptr, offsetof( TerrainRoadType, m_bridgeHoleAreaPercentage) }, + { "Destroyable", INI::parseBool, nullptr, offsetof( TerrainRoadType, m_isDestroyable ) }, + { "BridgeObjectName", INI::parseAsciiString, nullptr, offsetof( TerrainRoadType, m_bridgeObjectName ) }, + { "BridgeDeckHeight", INI::parseReal, nullptr, offsetof( TerrainRoadType, m_bridgeDeckHeight ) }, + { "BridgeCollapseDuration", INI::parseDurationUnsignedInt,nullptr, offsetof( TerrainRoadType, m_bridgeCollapseDuration ) }, + { "BridgeCollapseDrop", INI::parseReal, nullptr, offsetof( TerrainRoadType, m_bridgeCollapseDrop ) }, + { "BridgeCollapseTilt", INI::parseAngleReal, nullptr, offsetof( TerrainRoadType, m_bridgeCollapseTilt ) }, + { "BridgeCollapseStagger", INI::parsePercentToReal, nullptr, offsetof( TerrainRoadType, m_bridgeCollapseStagger ) }, + { "BridgeRebuildDuration", INI::parseDurationUnsignedInt,nullptr, offsetof( TerrainRoadType, m_bridgeRebuildDuration ) }, + { "BridgeCollapseSingleSpanRoll", INI::parseAngleReal, nullptr, offsetof( TerrainRoadType, m_bridgeCollapseSingleSpanRoll ) }, + { "BridgeCollapseSingleSpanDrop", INI::parseReal, nullptr, offsetof( TerrainRoadType, m_bridgeCollapseSingleSpanDrop ) }, { nullptr, nullptr, nullptr, 0 }, @@ -219,6 +229,16 @@ TerrainRoadType::TerrainRoadType() m_radarColor.blue = 0.0f; m_transitionEffectsHeight = 0.0f; m_numFXPerType = 0; + m_bridgeHoleAreaPercentage = 0.0f; + m_isDestroyable = FALSE; + m_bridgeDeckHeight = 0.0f; + m_bridgeCollapseDuration = 0; + m_bridgeCollapseDrop = 0.0f; + m_bridgeCollapseTilt = 0.0f; + m_bridgeCollapseStagger = 0.0f; + m_bridgeRebuildDuration = 0; + m_bridgeCollapseSingleSpanRoll = 0.0f; + m_bridgeCollapseSingleSpanDrop = 0.0f; } @@ -406,6 +426,20 @@ TerrainRoadType *TerrainRoadCollection::newBridge( AsciiString name ) bridge->friend_setTransitionEffectsHeight( defaultBridge->getTransitionEffectsHeight() ); bridge->friend_setNumFXPerType( defaultBridge->getNumFXPerType() ); + bridge->friend_setBridgeHoleAreaPercentage( defaultBridge->getBridgeHoleAreaPercentage() ); + bridge->friend_setDestroyable( defaultBridge->isDestroyable() ); + bridge->friend_setBridgeObjectName( defaultBridge->getBridgeObjectName() ); + bridge->friend_setBridgeDeckHeight( defaultBridge->getBridgeDeckHeight() ); + bridge->friend_setBridgeCollapseDuration( defaultBridge->getBridgeCollapseDuration() ); + bridge->friend_setBridgeCollapseDrop( defaultBridge->getBridgeCollapseDrop() ); + bridge->friend_setBridgeCollapseTilt( defaultBridge->getBridgeCollapseTilt() ); + bridge->friend_setBridgeCollapseStagger( defaultBridge->getBridgeCollapseStagger() ); + bridge->friend_setBridgeRebuildDuration( defaultBridge->getBridgeRebuildDuration() ); + bridge->friend_setBridgeCollapseSingleSpanRoll( defaultBridge->getBridgeCollapseSingleSpanRoll() ); + bridge->friend_setBridgeCollapseSingleSpanDrop( defaultBridge->getBridgeCollapseSingleSpanDrop() ); + + // a block that omits RadarColor drew black on the radar without this + bridge->friend_setRadarColor( defaultBridge->getRadarColor() ); for( Int state = BODY_PRISTINE; state < BODYDAMAGETYPE_COUNT; state++ ) { diff --git a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp index c5e8e92a420..bb46b3396a9 100644 --- a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp +++ b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp @@ -38,6 +38,7 @@ #include "Common/ThingFactory.h" #include "GameClient/Line2D.h" +#include "GameClient/TerrainRoads.h" #include "GameLogic/AI.h" #include "GameLogic/GameLogic.h" @@ -4369,6 +4370,12 @@ void Pathfinder::classifyObjectFootprint( Object *obj, Bool insert ) return; // It is important to not abuse bridge towers. } + if (obj->isKindOf(KINDOF_BRIDGE) && !obj->getTemplate()->isBridge()) { + // Procedural bridge span. Its deck is a pathfind layer of its own, and the ground below it + // is governed by the bridge clearance, not by this object's placeholder box. + return; + } + if (obj->getTemplate()->getFenceWidth() > 0.0f) { if (!obj->isKindOf(KINDOF_DEFENSIVE_WALL)) @@ -4861,6 +4868,10 @@ static void calculateBridgeHeights(IRegion2D bounds, PathfindCell** map) if (cellHiX > bounds.hi.x) cellHiX = bounds.hi.x; if (cellHiY > bounds.hi.y) cellHiY = bounds.hi.y; + // a thick deck, girders or arches hang below the driving surface and take away room + TerrainRoadType *bridgeTemplate = TheTerrainRoads->findBridge( bridge->getBridgeTemplateName() ); + Real deckHeight = bridgeTemplate ? bridgeTemplate->getBridgeDeckHeight() : 0.0f; + for (Int i = cellLoX; i < cellHiX; ++i) { for (Int j = cellLoY; j < cellHiY; ++j) { Real worldX = ((Real)i + 0.5f) * PATHFIND_CELL_SIZE_F; @@ -4878,12 +4889,15 @@ static void calculateBridgeHeights(IRegion2D bounds, PathfindCell** map) continue; } + // the deck plane through the two bridge points is what a unit passing below clears Real bridgeZ = TheTerrainLogic->getLayerHeight(worldX, worldY, layer); - Real waterZ, groundZ; + + // isUnderwater leaves waterZ untouched where there is no water, so seed both + Real waterZ = 0.0f, groundZ = 0.0f; TheTerrainLogic->isUnderwater(worldX, worldY, &waterZ, &groundZ); Real baseZ = (waterZ > groundZ) ? waterZ : groundZ; - Real gap = bridgeZ - baseZ; + Real gap = bridgeZ - deckHeight - baseZ; if (gap < 0.0f) gap = 0.0f; Int encoded = (Int)(gap / 10.0f); diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BridgeBehavior.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BridgeBehavior.h index ba3efdb3d42..af20c5710d3 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BridgeBehavior.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BridgeBehavior.h @@ -51,6 +51,7 @@ struct TimeAndLocationInfo { UnsignedInt delay; ///< how long to wait to execute this AsciiString boneName; ///< which bone to execute at + Real spanFraction; ///< 0..1 along the bridge centreline, <0 means unset }; // ------------------------------------------------------------------------------------------------ struct BridgeFXInfo diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/TerrainLogic.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/TerrainLogic.h index b011a51de9c..b9e88bef5d9 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/TerrainLogic.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/TerrainLogic.h @@ -210,6 +210,7 @@ class Bridge : public MemoryPoolObject Bool hasHoleArea(); // check if this bridge has defined a hole area for damaged/drawbridge state Bool hasHole(); // Check if bridge currently has a hole (destroyed/drawbridge open) void setDrawBridgeStage(bool open); // change if bridge is open/closed + void updateBridgeObjectGeometry(); // size the bridge object's collision box, or collapse it when the deck is gone }; //------------------------------------------------------------------------------------------------- diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Thing/ThingTemplate.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Thing/ThingTemplate.cpp index b4b79c89e12..40a0a78f5ff 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Thing/ThingTemplate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Thing/ThingTemplate.cpp @@ -1076,7 +1076,8 @@ void ThingTemplate::parseRequiredBridgeHeight(INI* ini, void* instance, void* st self->m_requiredBridgeHeight = -1; } else { - self->m_requiredBridgeHeight = std::clamp(static_cast(value / 10.0f), static_cast(0), static_cast(15)); + // rounded up, see Object::getRequiredBridgeHeight. 0 still means "never blocked". + self->m_requiredBridgeHeight = std::clamp(static_cast(REAL_TO_INT_CEIL(value / 10.0f)), static_cast(0), static_cast(15)); } } diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Map/TerrainLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Map/TerrainLogic.cpp index a7835089ccd..27e0a434ed1 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Map/TerrainLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Map/TerrainLogic.cpp @@ -33,6 +33,8 @@ #include "Common/DataChunk.h" #include "Common/GameState.h" #include "Common/MapObject.h" +#include "Common/Player.h" +#include "Common/PlayerList.h" #include "Common/Radar.h" #include "Common/ThingFactory.h" #include "Common/ThingTemplate.h" @@ -224,6 +226,9 @@ m_bridgeInfo(theInfo) // save the template name m_templateName = bridgeTemplateName; + // set up front, several paths below bail out early + m_next = nullptr; + //Coord3D fromLeft, fromRight, toLeft, toRight; /// The 4 corners of the rectangle that the bridge covers. m_bounds.lo.x = m_bridgeInfo.fromLeft.x; m_bounds.lo.y = m_bridgeInfo.fromLeft.y; @@ -243,13 +248,34 @@ m_bridgeInfo(theInfo) m_bridgeInfo.curDamageState = BODY_PRISTINE; + // + // the bridge definition decides which object represents us in the logic, and whether we get + // the targetable towers, so it has to be resolved before the object is created + // + TerrainRoadType *bridgeTemplate = TheTerrainRoads->findBridge( bridgeTemplateName ); + if( bridgeTemplate == nullptr ) { + DEBUG_LOG(( "*** Bridge Template Not Found '%s'.", bridgeTemplateName.str() )); + return; + } - static const ThingTemplate* genericBridgeTemplate = TheThingFactory->findTemplate("GenericBridge"); - if (!genericBridgeTemplate) { - DEBUG_LOG(("*** GenericBridge template not found.")); + AsciiString bridgeObjectName = bridgeTemplate->getBridgeObjectName(); + if( bridgeObjectName.isEmpty() ) + bridgeObjectName = "GenericBridge"; + const ThingTemplate* bridgeObjectTemplate = TheThingFactory->findTemplate( bridgeObjectName ); + if (!bridgeObjectTemplate) { + DEBUG_LOG(("*** Bridge object template '%s' not found.", bridgeObjectName.str())); return; } - Object *bridge = TheThingFactory->newObject(genericBridgeTemplate, nullptr); + + // + // a destroyable bridge takes damage and its towers can be captured, both of which need a real + // team; indestructible bridges keep the teamless object they have always had + // + Team *team = nullptr; + if( bridgeTemplate->isDestroyable() ) + team = ThePlayerList->getNeutralPlayer()->getDefaultTeam(); + + Object *bridge = TheThingFactory->newObject(bridgeObjectTemplate, team); Coord3D center; center.x = (m_bridgeInfo.fromLeft.x + m_bridgeInfo.toRight.x)/2.0f; center.y = (m_bridgeInfo.fromLeft.y + m_bridgeInfo.toRight.y)/2.0f; @@ -271,15 +297,46 @@ m_bridgeInfo(theInfo) v.y = m_bridgeInfo.toLeft.y - m_bridgeInfo.toRight.y; v.normalize(); - // get the template of the bridge - TerrainRoadType *bridgeTemplate = TheTerrainRoads->findBridge( bridgeTemplateName ); - if( bridgeTemplate == nullptr ) { - DEBUG_LOG(( "*** Bridge Template Not Found '%s'.", bridgeTemplateName.str() )); + // indestructible bridges are never shot at or opened, so there is nothing left to set up + if( bridgeTemplate->isDestroyable() == FALSE ) return; + + updateBridgeObjectGeometry(); + + // if defined, set hole area for destroyable bridges/drawbridges + if (bridgeTemplate->getBridgeHoleAreaPercentage() > 0.0f) { + Real factor = std::clamp(bridgeTemplate->getBridgeHoleAreaPercentage(), 0.0f, 1.0f); + + // midpoints of the two long edges of the bridge rectangle + Coord3D midLeft, midRight; + midLeft.x = (m_bridgeInfo.fromLeft.x + m_bridgeInfo.toLeft.x) / 2.0f; + midLeft.y = (m_bridgeInfo.fromLeft.y + m_bridgeInfo.toLeft.y) / 2.0f; + midLeft.z = (m_bridgeInfo.fromLeft.z + m_bridgeInfo.toLeft.z) / 2.0f; + midRight.x = (m_bridgeInfo.fromRight.x + m_bridgeInfo.toRight.x) / 2.0f; + midRight.y = (m_bridgeInfo.fromRight.y + m_bridgeInfo.toRight.y) / 2.0f; + midRight.z = (m_bridgeInfo.fromRight.z + m_bridgeInfo.toRight.z) / 2.0f; + + // shrink the rectangle along the span axis about those midpoints, full width preserved + m_bridgeInfo.fromLeftHole.set(midLeft.x + (m_bridgeInfo.fromLeft.x - midLeft.x) * factor, + midLeft.y + (m_bridgeInfo.fromLeft.y - midLeft.y) * factor, + midLeft.z + (m_bridgeInfo.fromLeft.z - midLeft.z) * factor); + m_bridgeInfo.toLeftHole.set(midLeft.x + (m_bridgeInfo.toLeft.x - midLeft.x) * factor, + midLeft.y + (m_bridgeInfo.toLeft.y - midLeft.y) * factor, + midLeft.z + (m_bridgeInfo.toLeft.z - midLeft.z) * factor); + m_bridgeInfo.fromRightHole.set(midRight.x + (m_bridgeInfo.fromRight.x - midRight.x) * factor, + midRight.y + (m_bridgeInfo.fromRight.y - midRight.y) * factor, + midRight.z + (m_bridgeInfo.fromRight.z - midRight.z) * factor); + m_bridgeInfo.toRightHole.set(midRight.x + (m_bridgeInfo.toRight.x - midRight.x) * factor, + midRight.y + (m_bridgeInfo.toRight.y - midRight.y) * factor, + midRight.z + (m_bridgeInfo.toRight.z - midRight.z) * factor); + } + else { + m_bridgeInfo.fromLeftHole.zero(); + m_bridgeInfo.toLeftHole.zero(); + m_bridgeInfo.fromRightHole.zero(); + m_bridgeInfo.toRightHole.zero(); } -#define no_BRIDGE_TOWERS // since they aren't destructable, don't need towers. -#if BRIDGE_TOWERS // initialize each of the tower positions to that of the bridge info bounding rect Coord3D towerPos[ BRIDGE_MAX_TOWERS ]; towerPos[ BRIDGE_TOWER_FROM_LEFT ] = m_bridgeInfo.fromLeft; @@ -317,14 +374,14 @@ m_bridgeInfo(theInfo) } tower = createTower( &pos, type, towerTemplate, bridge ); - - // store the tower object ID - m_bridgeInfo.towerObjectID[ i ] = tower->getID(); + if( tower ) + { + // store the tower object ID + m_bridgeInfo.towerObjectID[ i ] = tower->getID(); + } } -#endif - m_next = nullptr; } //------------------------------------------------------------------------------------------------- @@ -480,6 +537,59 @@ void Bridge::setDrawBridgeStage(bool open) { m_bridgeInfo.drawBridgeOpened = open; } +//------------------------------------------------------------------------------------------------- +/** updateBridgeObjectGeometry - match the bridge object's collision box to the state of the deck. */ +//------------------------------------------------------------------------------------------------- +void Bridge::updateBridgeObjectGeometry() +{ + Object *bridgeObj = TheGameLogic->findObjectByID( m_bridgeInfo.bridgeObjectID ); + if( bridgeObj == nullptr ) + return; + + // + // once the deck is rubble, or a drawbridge stands open, there is nothing left to shoot at, + // bump into or block the ground below. a landmark bridge keeps a box the size of the whole + // structure and the pathfinder stamps that into the ground, so it has to go with the deck. + // + const Bool deckIsGone = (bridgeObj->getBodyModule()->getDamageState() == BODY_RUBBLE) || hasHole(); + + GeometryInfo geom = bridgeObj->getTemplate()->getTemplateGeometryInfo(); + if( deckIsGone ) + { + geom.set( GEOMETRY_BOX, TRUE, 0.0f, 0.0f, 0.0f ); + } + else + { + if( bridgeObj->getTemplate()->isBridge() == FALSE ) + { + // + // a procedural span is drawn by the bridge buffer, so its template geometry is only a + // placeholder; size it to the actual span so area weapons aimed at the deck hit us + // + Coord2D span; + span.x = m_bridgeInfo.to.x - m_bridgeInfo.from.x; + span.y = m_bridgeInfo.to.y - m_bridgeInfo.from.y; + geom.setMajorRadius( span.length() / 2.0f ); + geom.setMinorRadius( m_bridgeInfo.bridgeWidth / 2.0f ); + } + + // + // ActiveBody turns collisions off for good when a structure rubbles and never turns them + // back on, so a repaired bridge has to ask for them again + // + bridgeObj->clearStatus( MAKE_OBJECT_STATUS_MASK( OBJECT_STATUS_NO_COLLISIONS ) ); + } + + // + // the pathfind footprint is derived from the geometry when the object is stamped, so it has to + // come out and go back in around the change. setGeometryInfo, not setGeometryInfoZ, so that + // the partition footprint follows the box too. + // + TheAI->pathfinder()->removeObjectFromPathfindMap( bridgeObj ); + bridgeObj->setGeometryInfo( geom ); + TheAI->pathfinder()->addObjectToPathfindMap( bridgeObj ); +} + //------------------------------------------------------------------------------------------------- /** isPointOnBridge - see if point is on bridge. */ //------------------------------------------------------------------------------------------------- @@ -494,7 +604,17 @@ Bool Bridge::isPointOnBridge(const Coord3D *pLoc, bool ignoreHole) unsigned char flags{ 0U }; // If bridge has hole and point is in hole area -> not on bridge - if (!ignoreHole && hasHole() && hasHoleArea()) { + if (!ignoreHole && hasHole()) { + + // + // BridgeHoleAreaPercentage is optional, and without it there are no hole corners to test + // against. a bridge that is destroyed or standing open has lost the whole deck, so say so + // rather than reporting it as intact. + // + if (!hasHoleArea()) { + return false; + } + Vector3 left1(m_bridgeInfo.fromLeftHole.x, m_bridgeInfo.fromLeftHole.y, m_bridgeInfo.fromLeftHole.z); Vector3 right1(m_bridgeInfo.fromRightHole.x, m_bridgeInfo.fromRightHole.y, m_bridgeInfo.fromRightHole.z); Vector3 left2(m_bridgeInfo.toLeftHole.x, m_bridgeInfo.toLeftHole.y, m_bridgeInfo.toLeftHole.z); @@ -935,6 +1055,7 @@ void Bridge::updateDamageState() m_bridgeInfo.curDamageState = damageState; if (damageState == BODY_RUBBLE) { TheAI->pathfinder()->changeBridgeState(m_layer, false); + updateBridgeObjectGeometry(); m_bridgeInfo.damageStateChanged = true; Object *obj; for (obj = TheGameLogic->getFirstObject(); obj; obj=obj->getNextObject()) { @@ -971,6 +1092,16 @@ void Bridge::updateDamageState() BridgeBehaviorInterface *bbi = BridgeBehavior::getBridgeBehaviorInterfaceFromObject( bridge ); if( bbi == nullptr || bbi->isScaffoldPresent() == FALSE ) TheAI->pathfinder()->changeBridgeState(m_layer, true); + + // + // the deck is whole again, so drop the hole that onDie punched in it. this has + // to happen before the box is rebuilt, since the box follows the hole. + // + setDrawBridgeStage(false); + + // the bridge is shootable again as soon as it stops being rubble, even while + // the scaffolding still keeps the deck closed + updateBridgeObjectGeometry(); m_bridgeInfo.damageStateChanged = true; } } @@ -1772,6 +1903,12 @@ PathfindLayerEnum TerrainLogic::getLayerForDestination(const Coord3D *pos) } while (pBridge ) { + // a collapsed bridge has no deck left to stand on, shoot at or put an effect on + if (pBridge->peekBridgeInfo()->curDamageState == BODY_RUBBLE) { + pBridge = pBridge->getNext(); + continue; + } + // filter out destroyed bridges or open draw bridges if (pBridge->isPointOnBridge(pos, false) ) { Real delta = fabs(pos->z-pBridge->getBridgeHeight(pos, nullptr)); diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/BridgeBehavior.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/BridgeBehavior.cpp index d3147ef36cf..fe41332c486 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/BridgeBehavior.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/BridgeBehavior.cpp @@ -107,12 +107,15 @@ BridgeBehaviorModuleData::~BridgeBehaviorModuleData() // ------------------------------------------------------------------------------------------------ /** Parse time and location info in the form of: - * Delay:#### */ + * Delay:#### */ // ------------------------------------------------------------------------------------------------ static void parseTimeAndLocationInfo( INI *ini, void *instance, TimeAndLocationInfo *timeAndLocationInfo ) { + // no explicit placement along the span unless one is given below + timeAndLocationInfo->spanFraction = -1.0f; + // delay label const char *token = ini->getNextToken( ini->getSepsColon() ); if( stricmp( token, "Delay" ) != 0 ) @@ -126,22 +129,31 @@ static void parseTimeAndLocationInfo( INI *ini, void *instance, // delay value ini->parseDurationUnsignedInt( ini, instance, &timeAndLocationInfo->delay, nullptr ); - // get optional bone label - token = ini->getNextTokenOrNull( ini->getSepsColon() ); - if( token ) + // get the optional location labels + while( (token = ini->getNextTokenOrNull( ini->getSepsColon() )) != nullptr ) { - // token must be a label for bone location - if( stricmp( token, "Bone" ) != 0 ) + if( stricmp( token, "Bone" ) == 0 ) { - DEBUG_CRASH(( "Expected 'Bone' token, found '%s'", token )); - throw INI_INVALID_DATA; + // read bone name and store + timeAndLocationInfo->boneName = ini->getNextAsciiString(); } + else if( stricmp( token, "SpanFraction" ) == 0 ) + { - // read bone name and store - timeAndLocationInfo->boneName = ini->getNextAsciiString(); + // 0 is the 'from' bank, 1 is the 'to' bank + ini->parseReal( ini, instance, &timeAndLocationInfo->spanFraction, nullptr ); + + } + else + { + + DEBUG_CRASH(( "Expected 'Bone' or 'SpanFraction' token, found '%s'", token )); + throw INI_INVALID_DATA; + + } } @@ -149,6 +161,56 @@ static void parseTimeAndLocationInfo( INI *ini, void *instance, //------------------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------------ +/** Position a fraction of the way along the bridge centreline -- 0 at the 'from' bank, 1 at the + * 'to' bank -- and optionally a matrix oriented along the span so debris lines up with the deck. + * + * Unlike getRandomSurfacePosition this draws no random numbers. That is deliberate: adding + * SpanFraction entries must not shift the logic RNG sequence, or existing replays would diverge. */ +// ------------------------------------------------------------------------------------------------ +static Bool getSpanPositionAndMatrix( const BridgeInfo *bridgeInfo, Real fraction, + Coord3D *pos, Matrix3D *mtx ) +{ + + // sanity + if( bridgeInfo == nullptr || pos == nullptr ) + return FALSE; + + if( fraction < 0.0f ) fraction = 0.0f; + if( fraction > 1.0f ) fraction = 1.0f; + + // the midpoints of the two ends give us the centreline + Coord3D from, to; + from.x = (bridgeInfo->fromLeft.x + bridgeInfo->fromRight.x) * 0.5f; + from.y = (bridgeInfo->fromLeft.y + bridgeInfo->fromRight.y) * 0.5f; + from.z = (bridgeInfo->fromLeft.z + bridgeInfo->fromRight.z) * 0.5f; + to.x = (bridgeInfo->toLeft.x + bridgeInfo->toRight.x) * 0.5f; + to.y = (bridgeInfo->toLeft.y + bridgeInfo->toRight.y) * 0.5f; + to.z = (bridgeInfo->toLeft.z + bridgeInfo->toRight.z) * 0.5f; + + pos->x = from.x + (to.x - from.x) * fraction; + pos->y = from.y + (to.y - from.y) * fraction; + pos->z = from.z + (to.z - from.z) * fraction; + + if( mtx ) + { + + Vector3 dir( to.x - from.x, to.y - from.y, to.z - from.z ); + if( dir.Length2() < 0.0001f ) + return FALSE; + dir.Normalize(); // buildTransformMatrix requires a normalized direction + + Vector3 p( pos->x, pos->y, pos->z ); + mtx->buildTransformMatrix( p, dir ); + + } + + return TRUE; + +} + +// ------------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------------ /*static*/ void BridgeBehaviorModuleData::parseFX( INI *ini, void *instance, void *store, @@ -629,6 +691,14 @@ void BridgeBehavior::onBodyDamageStateChange( const DamageInfo* damageInfo, if( newState != BODY_RUBBLE ) m_deathFrame = 0; + // + // the deck is back, so start shoving anything that ended up standing inside it. this is the + // trigger for sectional bridges: the tower healing path in BridgeTowerBehavior only fires + // onRepaired() at one exact moment, and any other route back out of rubble would miss it. + // + if( oldState == BODY_RUBBLE && newState != BODY_RUBBLE ) + m_repairedFrame = TheGameLogic->getFrame(); + // first resolve any fx stuff if we need to if( m_fxResolved == FALSE ) resolveFX(); @@ -852,14 +922,20 @@ UpdateSleepTime BridgeBehavior::update() if( deathTime == (*fxIt).timeAndLocationInfo.delay ) { Coord3D pos; + Matrix3D spanMtx; + const Matrix3D *pMtx = nullptr; // - // if a bone name is present, we'll use the bone position, otherwise we'll pick a - // spot somewhere on the bridge surface + // a bone name wins, then an explicit spot along the span, otherwise we pick a + // random spot somewhere on the bridge surface as we always did // boneName = (*fxIt).timeAndLocationInfo.boneName; + const Real spanFraction = (*fxIt).timeAndLocationInfo.spanFraction; if( boneName.isEmpty() == FALSE ) us->getSingleLogicalBonePosition( boneName.str(), &pos, nullptr ); + else if( spanFraction >= 0.0f && bridgeInfo && + getSpanPositionAndMatrix( bridgeInfo, spanFraction, &pos, &spanMtx ) ) + pMtx = &spanMtx; else if ( bridge && bridgeTemplate && bridgeInfo)//we have valid Terrain data for the bridge getRandomSurfacePosition( bridgeTemplate, bridgeInfo, &pos ); else @@ -867,7 +943,7 @@ UpdateSleepTime BridgeBehavior::update() // launch the fx list - FXList::doFXPos( (*fxIt).fx, &pos ); + FXList::doFXPos( (*fxIt).fx, &pos, pMtx ); } @@ -923,8 +999,11 @@ UpdateSleepTime BridgeBehavior::update() else { - // get random place on bridge - if ( bridge && bridgeTemplate && bridgeInfo )//we have valid Terrain data for the bridge + // an explicit spot along the span, else a random place on bridge + const Real spanFraction = (*oclIt).timeAndLocationInfo.spanFraction; + if( spanFraction >= 0.0f && bridgeInfo ) + getSpanPositionAndMatrix( bridgeInfo, spanFraction, &pos, nullptr ); + else if ( bridge && bridgeTemplate && bridgeInfo )//we have valid Terrain data for the bridge getRandomSurfacePosition( bridgeTemplate, bridgeInfo, &pos ); else pos.set( *getObject()->getPosition() ); @@ -984,6 +1063,15 @@ void BridgeBehavior::onDie( const DamageInfo *damageInfo ) // kill the towers associated with us auto moduleData = getBridgeBehaviorModuleData(); + // + // the deck is gone whether or not it can be rebuilt, so punch the hole for either kind. this + // runs before handleObjectsOnBridgeOnDie, which walks the bridge corners directly and so is + // not affected by the hole. + // + Bridge* deadBridge = TheTerrainLogic->findBridgeAt(getObject()->getPosition()); + if (deadBridge) + deadBridge->setDrawBridgeStage(true); + if (!moduleData->m_restoreable) { Object* tower; for (Int i = 0; i < BRIDGE_MAX_TOWERS; ++i) @@ -997,13 +1085,6 @@ void BridgeBehavior::onDie( const DamageInfo *damageInfo ) } } else { - // for destroy/repairable bridges set it to have a hole at death - Bridge* bridge = TheTerrainLogic->findBridgeAt(getObject()->getPosition()); - if (bridge) - { - bridge->setDrawBridgeStage(true); - } - // Set tower owner back to neutral Object* tower; for (Int i = 0; i < BRIDGE_MAX_TOWERS; ++i) diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/DumbProjectileBehavior.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/DumbProjectileBehavior.cpp index b9110ff4351..5e4e4df08e3 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/DumbProjectileBehavior.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/DumbProjectileBehavior.cpp @@ -736,8 +736,10 @@ UpdateSleepTime DumbProjectileBehavior::update() // note that we want to use getHighestLayerForDestination() here, so that anything even slightly // below the bridge translates into GROUND. (getLayerForDestination just does a "closest" check) + // a collapsed bridge has no deck left to stop us, so ask for healthy bridges only + const Bool onlyHealthyBridges = TRUE; PathfindLayerEnum oldLayer = getObject()->getLayer(); - PathfindLayerEnum newLayer = TheTerrainLogic->getHighestLayerForDestination(getObject()->getPosition()); + PathfindLayerEnum newLayer = TheTerrainLogic->getHighestLayerForDestination(getObject()->getPosition(), onlyHealthyBridges); getObject()->setLayer(newLayer); if (oldLayer != LAYER_GROUND && newLayer == LAYER_GROUND) @@ -745,7 +747,7 @@ UpdateSleepTime DumbProjectileBehavior::update() // see if we' still in the bridge's xy area Coord3D tmp = *getObject()->getPosition(); tmp.z = 9999.0f; - PathfindLayerEnum testLayer = TheTerrainLogic->getHighestLayerForDestination(&tmp); + PathfindLayerEnum testLayer = TheTerrainLogic->getHighestLayerForDestination(&tmp, onlyHealthyBridges); if (testLayer == oldLayer) { // ensure we are slightly above the bridge, to account for fudge & sloppy art diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp index ff8c6e0ea42..37bc83ec6c0 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp @@ -6934,7 +6934,12 @@ Short Object::getRequiredBridgeHeight() const { return 0; } else { + // + // A cell stores floor(clearance / 10), so a cell reporting 1 only guarantees 10 units of + // room. Round the requirement up, otherwise a 12 unit tall object would ask for 1 and be + // let through a 10 unit gap it does not fit under. + // Real geometryHeight = getGeometryInfo().getMaxHeightAbovePosition(); - return std::clamp(static_cast(geometryHeight / 10.0f), static_cast(1), static_cast(15)); + return std::clamp(static_cast(REAL_TO_INT_CEIL(geometryHeight / 10.0f)), static_cast(1), static_cast(15)); } } diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/MissileAIUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/MissileAIUpdate.cpp index 7e290978200..d163d586966 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/MissileAIUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/MissileAIUpdate.cpp @@ -745,8 +745,8 @@ void MissileAIUpdate::doAttackState(Bool turnOK, Bool randomPath) targetPos.add(offset); if (!d->m_isTorpedo) { - // Make sure Z is above ground - PathfindLayerEnum layer = TheTerrainLogic->getHighestLayerForDestination(&targetPos); + // Make sure Z is above ground. A collapsed bridge is not something to clear. + PathfindLayerEnum layer = TheTerrainLogic->getHighestLayerForDestination(&targetPos, TRUE); Real minHeight = TheTerrainLogic->getLayerHeight(targetPos.x, targetPos.y, layer) + APPROACH_HEIGHT; targetPos.z = __max(targetPos.z, minHeight); } @@ -1027,8 +1027,10 @@ UpdateSleepTime MissileAIUpdate::update() // note that we want to use getHighestLayerForDestination() here, so that anything even slightly // below the bridge translates into GROUND. (getLayerForDestination just does a "closest" check) + // a collapsed bridge has no deck left to stop us, so ask for healthy bridges only + const Bool onlyHealthyBridges = TRUE; PathfindLayerEnum oldLayer = getObject()->getLayer(); - PathfindLayerEnum newLayer = TheTerrainLogic->getHighestLayerForDestination(getObject()->getPosition()); + PathfindLayerEnum newLayer = TheTerrainLogic->getHighestLayerForDestination(getObject()->getPosition(), onlyHealthyBridges); getObject()->setLayer(newLayer); if (projectileIsArmed() && oldLayer != LAYER_GROUND && newLayer == LAYER_GROUND) @@ -1036,7 +1038,7 @@ UpdateSleepTime MissileAIUpdate::update() // see if we' still in the bridge's xy area Coord3D tmp = *getObject()->getPosition(); tmp.z = 9999.0f; - PathfindLayerEnum testLayer = TheTerrainLogic->getHighestLayerForDestination(&tmp); + PathfindLayerEnum testLayer = TheTerrainLogic->getHighestLayerForDestination(&tmp, onlyHealthyBridges); if (testLayer == oldLayer) { // ensure we are slightly above the bridge, to account for fudge & sloppy art @@ -1104,7 +1106,7 @@ void MissileAIUpdate::projectileNowJammed() targetPosition.y += GameLogicRandomValue(-scatter, scatter); targetPosition.z = TheTerrainLogic->getLayerHeight( targetPosition.x, targetPosition.y, - TheTerrainLogic->getHighestLayerForDestination(&targetPosition) ); + TheTerrainLogic->getHighestLayerForDestination(&targetPosition, TRUE) ); getStateMachine()->setGoalObject(nullptr); // Projectiles are expressly forbidden from getting AIIdle. Who am I to argue. diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/DrawBridgeUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/DrawBridgeUpdate.cpp index 262af185e46..78c8c95c9b0 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/DrawBridgeUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/DrawBridgeUpdate.cpp @@ -182,6 +182,9 @@ bool DrawBridgeUpdate::setDrawBridgeState(bool opened, const Object* fromTower) else { obj->clearAndSetModelConditionState(MODELCONDITION_DOOR_1_OPENING, MODELCONDITION_DOOR_1_CLOSING); obj->setGeometryInfo(obj->getTemplate()->getTemplateGeometryInfo()); + // the box comes from the bridge, not from a procedural span's placeholder template + if (bridge != nullptr) + bridge->updateBridgeObjectGeometry(); m_openingFrame = 0U; // when rapid toggling is possible m_closingDamageFrame = TheGameLogic->getFrame() + data->m_closingDamageTime; diff --git a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DBridgeBuffer.h b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DBridgeBuffer.h index 3166e4622f2..ddd2c6d4970 100644 --- a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DBridgeBuffer.h +++ b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DBridgeBuffer.h @@ -77,6 +77,29 @@ typedef enum { SECTIONAL_BRIDGE = 1 } TBridgeType; +typedef enum { + BRIDGE_ANIM_NONE = 0, + BRIDGE_ANIM_COLLAPSE = 1, + BRIDGE_ANIM_REBUILD = 2 +} TBridgeAnimType; + +// +// Per span section deformation for the collapse/rebuild animation. The deck lives in a shared +// world space vertex buffer and has no skeleton, so the fold is applied to the finished world +// position rather than to the model matrix: the model->world mapping divides the bridge axis by +// the bridge length, so a rotation baked into the matrix would come out sheared. +// +struct BridgeSectionAnim +{ + Vector3 pivot; ///< world position the section hinges about + Vector3 along; ///< normalized bridge axis + Vector3 across; ///< normalized across-bridge axis + Vector3 up; ///< normalized up axis + Real angle; ///< fold angle about the across axis, radians + Real roll; ///< bank angle about the along axis, radians + Real drop; ///< world units the section sinks +}; + class BridgeInfo; /// The individual data for a bridge. class W3DBridge @@ -111,6 +134,9 @@ class W3DBridge AsciiString m_templateName; ///< Name of the bridge type. BodyDamageType m_curDamageState; Bool m_enabled; + TBridgeAnimType m_animType; ///< deck animation currently playing, if any + UnsignedInt m_animStartFrame; ///< logic frame the animation started on + BodyDamageType m_pendingDamageState; ///< model to load once a collapse animation lands protected: Int getModelVerticesFixed(VertexFormatXYZNDUV1 *destination_vb, Int curVertex, const Matrix3D &mtx, MeshClass *pMesh, RefRenderObjListIterator *pLightsIterator); @@ -118,7 +144,11 @@ class W3DBridge Int getModelVertices(VertexFormatXYZNDUV1 *destination_vb, Int curVertex, Real xOffset, Vector3 &vec, Vector3 &vecNormal, Vector3 &vecZ, Vector3 &offset, const Matrix3D &mtx, - MeshClass *pMesh, RefRenderObjListIterator *pLightsIterator); + MeshClass *pMesh, RefRenderObjListIterator *pLightsIterator, + const BridgeSectionAnim *anim = nullptr); + Real getAnimPhase(UnsignedInt now) const; ///< 0..1 progress of the current animation + Bool computeSectionAnim(Int section, Int numSpans, Real phase, Real xOffset, + const Vector3 &vec, BridgeSectionAnim *anim); public: W3DBridge(); @@ -130,6 +160,9 @@ class W3DBridge const Vector3* getEnd() const { return &m_end;} Bool load(BodyDamageType curDamageState); BodyDamageType getDamageState() {return m_curDamageState;}; + Bool isAnimating() const {return m_animType != BRIDGE_ANIM_NONE;}; + /// Advance the deck animation against the logic state. Returns true if the buffer needs a rebake. + Bool updateAnimation(BodyDamageType logicState, UnsignedInt now); void setDamageState(BodyDamageType state) { m_curDamageState = state;}; void getIndicesNVertices(UnsignedShort *destination_ib, VertexFormatXYZNDUV1 *destination_vb, Int *curIndexP, Int *curVertexP, RefRenderObjListIterator *pLightsIterator); Bool cullBridge(CameraClass * camera); ///< Culls the bridges. Returns true if visibility changed. diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DBridgeBuffer.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DBridgeBuffer.cpp index a69b827206a..55c97b8287a 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DBridgeBuffer.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DBridgeBuffer.cpp @@ -56,6 +56,7 @@ #include "Common/ThingTemplate.h" #include "GameClient/TerrainRoads.h" #include "GameLogic/Damage.h" +#include "GameLogic/GameLogic.h" #include "GameLogic/Module/BodyModule.h" #include "W3DDevice/GameLogic/W3DTerrainLogic.h" #include "W3DDevice/GameClient/TerrainTex.h" @@ -108,7 +109,10 @@ m_sectionMesh(nullptr), m_rightMesh(nullptr), m_visible(false), m_curDamageState(BODY_PRISTINE), -m_scale(1.0) +m_scale(1.0), +m_animType(BRIDGE_ANIM_NONE), +m_animStartFrame(0), +m_pendingDamageState(BODY_PRISTINE) { } @@ -396,6 +400,44 @@ void W3DBridge::getBridgeInfo(BridgeInfo *pInfo) +//============================================================================= +// rotateInBridgeFrame +//============================================================================= +/** Rotates a vector inside an orthonormal bridge frame: a pitch in the (along, up) +plane for the multi span fold, and a roll in the (across, up) plane for the single +span bank. Both are plain 2D rotations because the frame is orthonormal. + +The frame is built from the bridge end points rather than from vec/vecNormal/vecZ, +because those three are not orthogonal for a sloped bridge that does not run along +world X -- decomposing against them would skew the fold instead of rotating it. */ +//============================================================================= +static Vector3 rotateInBridgeFrame(const Vector3 &v, const BridgeSectionAnim *anim) +{ + Real a = Vector3::Dot_Product(v, anim->along); + Real c = Vector3::Dot_Product(v, anim->across); + Real u = Vector3::Dot_Product(v, anim->up); + + // pitch -- the fold, used when there are enough sections to fold against each other + if (anim->angle != 0.0f) { + Real ca = (Real)cos(anim->angle); + Real sa = (Real)sin(anim->angle); + Real na = a*ca - u*sa; + u = a*sa + u*ca; + a = na; + } + + // roll -- the sideways bank of a single span deck sinking + if (anim->roll != 0.0f) { + Real cr = (Real)cos(anim->roll); + Real sr = (Real)sin(anim->roll); + Real nc = c*cr - u*sr; + u = c*sr + u*cr; + c = nc; + } + + return anim->along * a + anim->across * c + anim->up * u; +} + //============================================================================= // W3DBridge::getModelVertices //============================================================================= @@ -404,7 +446,8 @@ void W3DBridge::getBridgeInfo(BridgeInfo *pInfo) Int W3DBridge::getModelVertices(VertexFormatXYZNDUV1 *destination_vb, Int curVertex, Real xOffset, Vector3 &vec, Vector3 &vecNormal, Vector3 &vecZ, Vector3 &offset, const Matrix3D &mtx, - MeshClass *pMesh, RefRenderObjListIterator *pLightsIterator) + MeshClass *pMesh, RefRenderObjListIterator *pLightsIterator, + const BridgeSectionAnim *anim) { if (pMesh == nullptr) return(0); @@ -444,6 +487,12 @@ Int W3DBridge::getModelVertices(VertexFormatXYZNDUV1 *destination_vb, Int curVer vLoc.Y += m_start.Y; vLoc.Z += m_start.Z; + // fold and sink this section if a collapse/rebuild animation is running + if (anim) { + vLoc = anim->pivot + rotateInBridgeFrame(vLoc - anim->pivot, anim); + vLoc.Z -= anim->drop; + } + curVb->x = vLoc.X; curVb->y = vLoc.Y; curVb->z = vLoc.Z; @@ -462,6 +511,10 @@ Int W3DBridge::getModelVertices(VertexFormatXYZNDUV1 *destination_vb, Int curVer curVb->diffuse = 0xFF000000; #else normal = (normal.X) * vec + normal.Y*vecNormal + normal.Z*vecZ; + // the normals have to turn with the geometry or a falling section lights wrongly + if (anim) { + normal = rotateInBridgeFrame(normal, anim); + } normal.Normalize(); TheTerrainRenderObject->doTheLight(&vb, lightRay, &normal, nullptr, 1.0f); curVb->nx = 0; //will these to keep AGP write buffer happy. @@ -504,6 +557,227 @@ Int W3DBridge::getModelVerticesFixed(VertexFormatXYZNDUV1 *destination_vb, Int c return(getModelVertices(destination_vb, curVertex, xOffset, vec, vecNormal, vecZ, m_start, mtx, pMesh, pLightsIterator)); } +//============================================================================= +// W3DBridge::getAnimPhase +//============================================================================= +/** Progress of the running deck animation, 0..1. + +Deliberately computed from absolute elapsed frames rather than an accumulated +delta: drawBridges runs more than once per rendered frame (the water reflection +pass calls it again), and an accumulator would advance the animation twice as +fast whenever the bridge is reflected. */ +//============================================================================= +Real W3DBridge::getAnimPhase(UnsignedInt now) const +{ + TerrainRoadType *bridge = TheTerrainRoads ? TheTerrainRoads->findBridge(m_templateName) : nullptr; + if (bridge == nullptr) + return 1.0f; + + UnsignedInt duration = (m_animType == BRIDGE_ANIM_REBUILD) ? + bridge->getBridgeRebuildDuration() : bridge->getBridgeCollapseDuration(); + + // a zero duration means the modder did not ask for an animation + if (duration == 0 || now < m_animStartFrame) + return 1.0f; + + Real t = (Real)(now - m_animStartFrame) / (Real)duration; + if (t > 1.0f) t = 1.0f; + + // the rebuild is simply the collapse played backwards + if (m_animType == BRIDGE_ANIM_REBUILD) + t = 1.0f - t; + + return t; +} + +//============================================================================= +// W3DBridge::computeSectionAnim +//============================================================================= +/** Builds the deformation for one span section. + +With several sections the failure ripples out from mid span: the centre sections +start folding immediately, the ones near the banks lag by up to BridgeCollapseStagger +of the total duration, and each half hinges about its outboard edge so the deck folds +inward. + +A bridge that resolves to a single section has nothing to fold against -- hinging it +about one edge just swings it like a trapdoor -- so it banks sideways about its own +centre and sinks instead. The stagger is meaningless there and is ignored. + +Returns false when this section has nothing to do, so untouched sections keep the +cheap path. */ +//============================================================================= +Bool W3DBridge::computeSectionAnim(Int section, Int numSpans, Real phase, Real xOffset, + const Vector3 &vec, BridgeSectionAnim *anim) +{ + if (anim == nullptr || numSpans < 1) + return false; + + TerrainRoadType *bridge = TheTerrainRoads ? TheTerrainRoads->findBridge(m_templateName) : nullptr; + if (bridge == nullptr) + return false; + + const Bool singleSpan = (numSpans == 1); + + Real drop = bridge->getBridgeCollapseDrop(); + Real tilt = bridge->getBridgeCollapseTilt(); + Real roll = bridge->getBridgeCollapseSingleSpanRoll(); + + if (singleSpan) { + // a zero single span drop just reuses the normal one + Real singleDrop = bridge->getBridgeCollapseSingleSpanDrop(); + if (singleDrop != 0.0f) + drop = singleDrop; + tilt = 0.0f; // no fold, it banks instead + if (drop == 0.0f && roll == 0.0f) + return false; + } else { + roll = 0.0f; // the fold does not bank + if (drop == 0.0f && tilt == 0.0f) + return false; + } + + Real sp; + Bool leftHalf = false; + if (singleSpan) { + + // one section, so there is nothing to stagger against + sp = phase; + + } else { + + Real stagger = bridge->getBridgeCollapseStagger(); + if (stagger < 0.0f) stagger = 0.0f; + if (stagger > 0.9f) stagger = 0.9f; // keep the divide below sane + + Real half = numSpans * 0.5f; + Real center = (Real)section + 0.5f; + leftHalf = center < half; + + // 0 at mid span, 1 at the banks + Real d = (Real)fabs(center - half) / half; + sp = (phase - d * stagger) / (1.0f - stagger); + + } + + if (sp <= 0.0f) + return false; // this section has not started moving yet + if (sp > 1.0f) sp = 1.0f; + + sp = sp * sp; // ease in, so it reads as falling and not sliding + + // + // build an orthonormal frame from the bridge end points. vec/vecNormal/vecZ cannot be used + // for this: vecZ is a rotation about world Y regardless of the bridge heading, so the three + // are not mutually perpendicular unless the bridge happens to run along world X. + // + Vector3 dir = m_end - m_start; + if (dir.Length2() < 0.0001f) + return false; + anim->along = dir; + anim->along.Normalize(); + + Vector3::Cross_Product(Vector3(0.0f, 0.0f, 1.0f), anim->along, &anim->across); + if (anim->across.Length2() < 0.0001f) + anim->across = Vector3(0.0f, 1.0f, 0.0f); // dead vertical bridge, pick anything sane + anim->across.Normalize(); + + Vector3::Cross_Product(anim->along, anim->across, &anim->up); + anim->up.Normalize(); + + anim->drop = sp * drop; + + Real pivotX; + if (singleSpan) { + + // bank about the middle of the section so it sinks rather than swinging off one edge + anim->angle = 0.0f; + anim->roll = sp * roll; + pivotX = (m_sectionMinX + m_sectionMaxX) * 0.5f; + + } else { + + // the two halves fold toward each other, hinging about the edge nearest the bank + anim->angle = sp * tilt * (leftHalf ? 1.0f : -1.0f); + anim->roll = 0.0f; + pivotX = leftHalf ? m_sectionMinX : m_sectionMaxX; + + } + + anim->pivot = m_start + vec * (pivotX + xOffset); + + return true; +} + +//============================================================================= +// W3DBridge::updateAnimation +//============================================================================= +/** Reconciles the buffer model with the logic damage state, running the deck +animation across the transition instead of popping between models. Returns true +if the shared vertex buffer needs rebuilding this frame. + +Client side only -- nothing here feeds back into the logic. The logic has already +flipped to BODY_RUBBLE by the time a collapse starts, so riders are dead and the +layer is closed while the deck is still visibly falling. */ +//============================================================================= +Bool W3DBridge::updateAnimation(BodyDamageType logicState, UnsignedInt now) +{ + TerrainRoadType *bridge = TheTerrainRoads ? TheTerrainRoads->findBridge(m_templateName) : nullptr; + UnsignedInt collapseDuration = bridge ? bridge->getBridgeCollapseDuration() : 0; + UnsignedInt rebuildDuration = bridge ? bridge->getBridgeRebuildDuration() : 0; + + // let a running animation finish before looking at the logic state again + if (m_animType == BRIDGE_ANIM_COLLAPSE) { + if (now >= m_animStartFrame + collapseDuration) { + // the deck has finished falling, so now show the wreck + m_animType = BRIDGE_ANIM_NONE; + BodyDamageType prevState = m_curDamageState; + m_curDamageState = m_pendingDamageState; + if (!load(m_pendingDamageState)) + load(prevState); + } + return true; + } + + if (m_animType == BRIDGE_ANIM_REBUILD) { + if (now >= m_animStartFrame + rebuildDuration) + m_animType = BRIDGE_ANIM_NONE; + return true; + } + + if (logicState == m_curDamageState) + return false; + + // + // healthy -> rubble. hold the model we are already showing and fold it; the broken model + // is swapped in when the fall lands. the BRIDGE_ANIM_NONE guard above matters here, or + // this would restart every frame since m_curDamageState deliberately lags the logic. + // + if (logicState == BODY_RUBBLE && collapseDuration > 0) { + m_animType = BRIDGE_ANIM_COLLAPSE; + m_animStartFrame = now; + m_pendingDamageState = logicState; + return true; + } + + // every other transition swaps the model straight away, as it always did + BodyDamageType prevState = m_curDamageState; + m_curDamageState = logicState; + if (!load(logicState)) { + // put the old model back + load(prevState); + m_curDamageState = logicState; + } + + // rubble -> healthy. the deck is whole geometry again, so unfold it into place. + if (prevState == BODY_RUBBLE && rebuildDuration > 0) { + m_animType = BRIDGE_ANIM_REBUILD; + m_animStartFrame = now; + } + + return true; +} + //============================================================================= // W3DBridge::getIndicesNVertices //============================================================================= @@ -587,10 +861,24 @@ void W3DBridge::getIndicesNVertices(UnsignedShort *destination_ib, VertexFormatX m_numPolygons += numI/3; Int i; + // + // the deck animation folds the span sections only -- the left and right end pieces sit on + // the abutments and stay put. + // + Real animPhase = 0.0f; + if (m_animType != BRIDGE_ANIM_NONE && TheGameLogic) + animPhase = getAnimPhase(TheGameLogic->getFrame()); + // draw the spans. for (i=0; igetFrame() : 0; for (Bridge *bridge = TheTerrainLogic->getFirstBridge(); bridge; bridge = bridge->getNext()) { BridgeInfo info; bridge->getBridgeInfo(&info); @@ -1125,16 +1414,8 @@ void W3DBridgeBuffer::drawBridges(CameraClass * camera, Bool wireframe, TextureC continue; } m_bridges[info.bridgeIndex].setEnabled(true); - if (m_bridges[info.bridgeIndex].getDamageState() != info.curDamageState) { + if (m_bridges[info.bridgeIndex].updateAnimation(info.curDamageState, now)) changed = true; - BodyDamageType curState = m_bridges[info.bridgeIndex].getDamageState(); - m_bridges[info.bridgeIndex].setDamageState(info.curDamageState); - if (!m_bridges[info.bridgeIndex].load(info.curDamageState)) { - // put the old model back. - m_bridges[info.bridgeIndex].load(curState); - m_bridges[info.bridgeIndex].setDamageState(info.curDamageState); - } - } } if (changed) { loadBridgesInVertexAndIndexBuffers(nullptr);