From 4356140f9ac3c1f8ca4df511b2e2b6745d9a9e7d Mon Sep 17 00:00:00 2001 From: pWn3d Date: Sat, 29 Aug 2026 14:06:59 +0200 Subject: [PATCH 1/7] implement destructible road bridges --- .../Include/GameClient/TerrainRoads.h | 13 +++ .../GameClient/Terrain/TerrainRoads.cpp | 7 ++ .../Source/GameLogic/Map/TerrainLogic.cpp | 97 ++++++++++++++++--- 3 files changed, 102 insertions(+), 15 deletions(-) diff --git a/Core/GameEngine/Include/GameClient/TerrainRoads.h b/Core/GameEngine/Include/GameClient/TerrainRoads.h index ceb71b709db..cab95917607 100644 --- a/Core/GameEngine/Include/GameClient/TerrainRoads.h +++ b/Core/GameEngine/Include/GameClient/TerrainRoads.h @@ -98,6 +98,8 @@ class TerrainRoadType : public MemoryPoolObject Real getTransitionEffectsHeight( void ) { return m_transitionEffectsHeight; } Int getNumFXPerType( void ) { return m_numFXPerType; } Real getBridgeHoleAreaPercentage( void ) { return m_bridgeHoleAreaPercentage; } + Bool isDestroyable( void ) { return m_isDestroyable; } + AsciiString getBridgeObjectName( void ) { return m_bridgeObjectName; } // friend access methods to be used by the road collection only! void friend_setName( AsciiString name ) { m_name = name; } @@ -127,6 +129,9 @@ 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; } /// get the parsing table for INI const FieldParse *getRoadFieldParse( void ) { return m_terrainRoadFieldParseTable; } @@ -190,6 +195,14 @@ 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 }; //------------------------------------------------------------------------------------------------- diff --git a/Core/GameEngine/Source/GameClient/Terrain/TerrainRoads.cpp b/Core/GameEngine/Source/GameClient/Terrain/TerrainRoads.cpp index f889e0f0cce..51fde35427c 100644 --- a/Core/GameEngine/Source/GameClient/Terrain/TerrainRoads.cpp +++ b/Core/GameEngine/Source/GameClient/Terrain/TerrainRoads.cpp @@ -85,6 +85,8 @@ 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 ) }, { nullptr, nullptr, nullptr, 0 }, @@ -219,6 +221,8 @@ TerrainRoadType::TerrainRoadType( void ) m_radarColor.blue = 0.0f; m_transitionEffectsHeight = 0.0f; m_numFXPerType = 0; + m_bridgeHoleAreaPercentage = 0.0f; + m_isDestroyable = FALSE; } @@ -406,6 +410,9 @@ 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() ); for( Int state = BODY_PRISTINE; state < BODYDAMAGETYPE_COUNT; state++ ) { diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Map/TerrainLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Map/TerrainLogic.cpp index c52bd16cbf7..e901ecba182 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" @@ -223,6 +225,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; @@ -242,13 +247,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; @@ -270,15 +296,56 @@ 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; + + // + // the object template geometry is just a placeholder since the span is drawn by the bridge + // buffer; 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; + GeometryInfo geom = bridge->getGeometryInfo(); + geom.setMajorRadius( span.length() / 2.0f ); + geom.setMinorRadius( m_bridgeInfo.bridgeWidth / 2.0f ); + bridge->setGeometryInfo( geom ); + + // 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; @@ -316,14 +383,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; } //------------------------------------------------------------------------------------------------- From e3de8fc2881a6654f1b076f7a2cd1df470d54e1f Mon Sep 17 00:00:00 2001 From: pWn3d Date: Sun, 30 Aug 2026 16:52:15 +0200 Subject: [PATCH 2/7] fix some road bridge stuff, deck height and bridge height --- .../Include/GameClient/TerrainRoads.h | 8 ++++++++ .../Source/GameClient/Terrain/TerrainRoads.cpp | 3 +++ .../Source/GameLogic/AI/AIPathfind.cpp | 18 ++++++++++++++++-- .../Source/Common/Thing/ThingTemplate.cpp | 3 ++- .../Source/GameLogic/Object/Object.cpp | 7 ++++++- 5 files changed, 35 insertions(+), 4 deletions(-) diff --git a/Core/GameEngine/Include/GameClient/TerrainRoads.h b/Core/GameEngine/Include/GameClient/TerrainRoads.h index a4359a2762c..b18f510ecc6 100644 --- a/Core/GameEngine/Include/GameClient/TerrainRoads.h +++ b/Core/GameEngine/Include/GameClient/TerrainRoads.h @@ -100,6 +100,7 @@ class TerrainRoadType : public MemoryPoolObject Real getBridgeHoleAreaPercentage() { return m_bridgeHoleAreaPercentage; } Bool isDestroyable( void ) { return m_isDestroyable; } AsciiString getBridgeObjectName( void ) { return m_bridgeObjectName; } + Real getBridgeDeckHeight( void ) { return m_bridgeDeckHeight; } // friend access methods to be used by the road collection only! void friend_setName( AsciiString name ) { m_name = name; } @@ -132,6 +133,7 @@ class TerrainRoadType : public MemoryPoolObject 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; } /// get the parsing table for INI const FieldParse *getRoadFieldParse() { return m_terrainRoadFieldParseTable; } @@ -203,6 +205,12 @@ class TerrainRoadType : public MemoryPoolObject // 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 }; //------------------------------------------------------------------------------------------------- diff --git a/Core/GameEngine/Source/GameClient/Terrain/TerrainRoads.cpp b/Core/GameEngine/Source/GameClient/Terrain/TerrainRoads.cpp index 636adb72634..add00bab9e1 100644 --- a/Core/GameEngine/Source/GameClient/Terrain/TerrainRoads.cpp +++ b/Core/GameEngine/Source/GameClient/Terrain/TerrainRoads.cpp @@ -87,6 +87,7 @@ const FieldParse TerrainRoadType::m_terrainBridgeFieldParseTable[] = { "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 ) }, { nullptr, nullptr, nullptr, 0 }, @@ -223,6 +224,7 @@ TerrainRoadType::TerrainRoadType() m_numFXPerType = 0; m_bridgeHoleAreaPercentage = 0.0f; m_isDestroyable = FALSE; + m_bridgeDeckHeight = 0.0f; } @@ -413,6 +415,7 @@ TerrainRoadType *TerrainRoadCollection::newBridge( AsciiString name ) bridge->friend_setBridgeHoleAreaPercentage( defaultBridge->getBridgeHoleAreaPercentage() ); bridge->friend_setDestroyable( defaultBridge->isDestroyable() ); bridge->friend_setBridgeObjectName( defaultBridge->getBridgeObjectName() ); + bridge->friend_setBridgeDeckHeight( defaultBridge->getBridgeDeckHeight() ); 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/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/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)); } } From dc040a809070510102f982689bb535f9e962b0ac Mon Sep 17 00:00:00 2001 From: pWn3d Date: Sun, 30 Aug 2026 20:27:00 +0200 Subject: [PATCH 3/7] bugfix(bridges): stop destroyed bridges blocking shots and effects A collapsed bridge kept behaving like an intact one in three places, all of which only showed up now that road bridges can be destroyed from Roads.ini. getLayerForDestination handed out a rubbled bridge's layer. Its sibling getHighestLayerForDestination already filtered that case; this one relied on isPointOnBridge's hole test, which does nothing unless the road type defines BridgeHoleAreaPercentage. FXList and ObjectCreationList use the returned layer to place effects, so explosions and OCL spawns were lifted to the deck plane of a bridge that no longer existed. The span object's collision box is sized to the whole bridge at construction so area weapons aimed at the deck connect. ActiveBody only flattens Z when a structure rubbles, leaving the full major/minor radii behind, which keeps the wreck inside every FROM_BOUNDINGSPHERE range and splash query. Collapse the box instead, and rebuild it from the bridge on repair - the template geometry is only a placeholder, so restoring from it would be wrong. ActiveBody also sets OBJECT_STATUS_NO_COLLISIONS on the way down and never clears it, so a repaired span has to ask for collisions back. Projectiles tracking their pathfind layer now ask for healthy bridges only, so a flat trajectory crossing the plane of a wrecked deck no longer detonates on it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UF9NnxNqrmYH1XhNnyCqeb --- .../Include/GameLogic/TerrainLogic.h | 1 + .../Source/GameLogic/Map/TerrainLogic.cpp | 71 ++++++++++++++++--- .../Behavior/DumbProjectileBehavior.cpp | 6 +- .../Update/AIUpdate/MissileAIUpdate.cpp | 12 ++-- .../Object/Update/DrawBridgeUpdate.cpp | 3 + 5 files changed, 75 insertions(+), 18 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/TerrainLogic.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/TerrainLogic.h index b011a51de9c..f79654b8ef8 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 updateSpanObjectGeometry(); // size the span object's collision box, or collapse it when the span is rubble }; //------------------------------------------------------------------------------------------------- diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Map/TerrainLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Map/TerrainLogic.cpp index 267bbaf1ad2..bbae4fa73d3 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Map/TerrainLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Map/TerrainLogic.cpp @@ -301,17 +301,7 @@ m_bridgeInfo(theInfo) if( bridgeTemplate->isDestroyable() == FALSE ) return; - // - // the object template geometry is just a placeholder since the span is drawn by the bridge - // buffer; 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; - GeometryInfo geom = bridge->getGeometryInfo(); - geom.setMajorRadius( span.length() / 2.0f ); - geom.setMinorRadius( m_bridgeInfo.bridgeWidth / 2.0f ); - bridge->setGeometryInfo( geom ); + updateSpanObjectGeometry(); // if defined, set hole area for destroyable bridges/drawbridges if (bridgeTemplate->getBridgeHoleAreaPercentage() > 0.0f) { @@ -547,6 +537,54 @@ void Bridge::setDrawBridgeStage(bool open) { m_bridgeInfo.drawBridgeOpened = open; } +//------------------------------------------------------------------------------------------------- +/** updateSpanObjectGeometry - match the span object's collision box to the state of the bridge. */ +//------------------------------------------------------------------------------------------------- +void Bridge::updateSpanObjectGeometry() +{ + Object *bridgeObj = TheGameLogic->findObjectByID( m_bridgeInfo.bridgeObjectID ); + if( bridgeObj == nullptr ) + return; + + // + // landmark bridges are authored with a box that matches their model, and DrawBridgeUpdate owns + // it, so leave them alone + // + if( bridgeObj->getTemplate()->isBridge() ) + return; + + GeometryInfo geom = bridgeObj->getTemplate()->getTemplateGeometryInfo(); + if( bridgeObj->getBodyModule()->getDamageState() == BODY_RUBBLE ) + { + // + // nothing is left of the span to shoot at or bump into. the box is as long as the whole + // bridge, so leaving it behind makes every shot passing near the wreck detonate on it. + // + geom.set( GEOMETRY_BOX, TRUE, 0.0f, 0.0f, 0.0f ); + } + else + { + // + // the object template geometry is just a placeholder since the span is drawn by the bridge + // buffer; 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 span has to ask for them again + // + bridgeObj->clearStatus( MAKE_OBJECT_STATUS_MASK( OBJECT_STATUS_NO_COLLISIONS ) ); + } + + // setGeometryInfo, not setGeometryInfoZ -- the partition footprint has to change with the box + bridgeObj->setGeometryInfo( geom ); +} + //------------------------------------------------------------------------------------------------- /** isPointOnBridge - see if point is on bridge. */ //------------------------------------------------------------------------------------------------- @@ -1002,6 +1040,7 @@ void Bridge::updateDamageState() m_bridgeInfo.curDamageState = damageState; if (damageState == BODY_RUBBLE) { TheAI->pathfinder()->changeBridgeState(m_layer, false); + updateSpanObjectGeometry(); m_bridgeInfo.damageStateChanged = true; Object *obj; for (obj = TheGameLogic->getFirstObject(); obj; obj=obj->getNextObject()) { @@ -1038,6 +1077,10 @@ void Bridge::updateDamageState() BridgeBehaviorInterface *bbi = BridgeBehavior::getBridgeBehaviorInterfaceFromObject( bridge ); if( bbi == nullptr || bbi->isScaffoldPresent() == FALSE ) TheAI->pathfinder()->changeBridgeState(m_layer, true); + + // the span is shootable again as soon as it stops being rubble, even while the + // scaffolding still keeps the deck closed + updateSpanObjectGeometry(); m_bridgeInfo.damageStateChanged = true; } } @@ -1839,6 +1882,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/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/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..9f8c48afb32 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()); + // a procedural span's box comes from the bridge, not from its placeholder template + if (bridge != nullptr) + bridge->updateSpanObjectGeometry(); m_openingFrame = 0U; // when rapid toggling is possible m_closingDamageFrame = TheGameLogic->getFrame() + data->m_closingDamageTime; From c9143eae951193a82f1f025ce471a352c830dc1d Mon Sep 17 00:00:00 2001 From: pWn3d Date: Sun, 30 Aug 2026 20:33:46 +0200 Subject: [PATCH 4/7] bugfix(bridges): inherit RadarColor from DefaultBridge newBridge copies every other field from the DefaultBridge block but never copied the radar colour, and TerrainRoadType starts out black, so any bridge block that omits RadarColor drew black on the radar instead of picking up the default. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UF9NnxNqrmYH1XhNnyCqeb --- Core/GameEngine/Include/GameClient/TerrainRoads.h | 1 + Core/GameEngine/Source/GameClient/Terrain/TerrainRoads.cpp | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Core/GameEngine/Include/GameClient/TerrainRoads.h b/Core/GameEngine/Include/GameClient/TerrainRoads.h index b18f510ecc6..f585b438a0b 100644 --- a/Core/GameEngine/Include/GameClient/TerrainRoads.h +++ b/Core/GameEngine/Include/GameClient/TerrainRoads.h @@ -134,6 +134,7 @@ class TerrainRoadType : public MemoryPoolObject 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_setRadarColor( RGBColor color ) { m_radarColor = color; } /// get the parsing table for INI const FieldParse *getRoadFieldParse() { return m_terrainRoadFieldParseTable; } diff --git a/Core/GameEngine/Source/GameClient/Terrain/TerrainRoads.cpp b/Core/GameEngine/Source/GameClient/Terrain/TerrainRoads.cpp index add00bab9e1..5f2221b263c 100644 --- a/Core/GameEngine/Source/GameClient/Terrain/TerrainRoads.cpp +++ b/Core/GameEngine/Source/GameClient/Terrain/TerrainRoads.cpp @@ -416,6 +416,9 @@ TerrainRoadType *TerrainRoadCollection::newBridge( AsciiString name ) bridge->friend_setDestroyable( defaultBridge->isDestroyable() ); bridge->friend_setBridgeObjectName( defaultBridge->getBridgeObjectName() ); bridge->friend_setBridgeDeckHeight( defaultBridge->getBridgeDeckHeight() ); + + // 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++ ) { From 241eda4978681e8bec5441398e338a7b473be190 Mon Sep 17 00:00:00 2001 From: pWn3d Date: Sun, 30 Aug 2026 20:34:00 +0200 Subject: [PATCH 5/7] bugfix(bridges): clear the deck of a destroyed bridge for good A destroyed bridge still behaved like a solid deck in several ways that only show once bridges can actually be destroyed and rebuilt. The hole punched at death was described by an optional polygon, so a road type without BridgeHoleAreaPercentage got no hole at all and every isPointOnBridge test kept reporting the wreck as intact. A holed bridge with no polygon now loses the whole deck, and the hole is punched for non-repairable bridges too, since the deck is just as gone either way. Nothing ever cleared the flag again, so repairing now closes the hole as well. Only procedural spans had their collision box collapsed on death. A landmark bridge keeps a box the size of the whole structure and the pathfinder stamps that into the ground, so the wreck went on blocking everything underneath. Collapse it for both kinds, and take the object out of the pathfind map and put it back around the change, otherwise the old footprint survives - the rubble re-stamp in ActiveBody runs before this and re-stamps the full box. Restoring the box afterwards has to stay split: a landmark bridge is authored with real geometry, a procedural span carries a placeholder and has to be sized from the bridge instead. Landmark bridges also never got OBJECT_STATUS_NO_COLLISIONS cleared on repair, since only the span path did that. Rider handling is unaffected: the kill pass in updateDamageState runs before onDie punches the hole, handleObjectsOnBridgeOnDie walks the bridge corners directly rather than through isPointOnBridge, and findBridgeAt ignores holes. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UF9NnxNqrmYH1XhNnyCqeb --- .../Include/GameLogic/TerrainLogic.h | 2 +- .../Source/GameLogic/Map/TerrainLogic.cpp | 77 ++++++++++++------- .../Object/Behavior/BridgeBehavior.cpp | 16 ++-- .../Object/Update/DrawBridgeUpdate.cpp | 4 +- 4 files changed, 61 insertions(+), 38 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/TerrainLogic.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/TerrainLogic.h index f79654b8ef8..b9e88bef5d9 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/TerrainLogic.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/TerrainLogic.h @@ -210,7 +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 updateSpanObjectGeometry(); // size the span object's collision box, or collapse it when the span is rubble + void updateBridgeObjectGeometry(); // size the bridge object's collision box, or collapse it when the deck is gone }; //------------------------------------------------------------------------------------------------- diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Map/TerrainLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Map/TerrainLogic.cpp index bbae4fa73d3..27e0a434ed1 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Map/TerrainLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Map/TerrainLogic.cpp @@ -301,7 +301,7 @@ m_bridgeInfo(theInfo) if( bridgeTemplate->isDestroyable() == FALSE ) return; - updateSpanObjectGeometry(); + updateBridgeObjectGeometry(); // if defined, set hole area for destroyable bridges/drawbridges if (bridgeTemplate->getBridgeHoleAreaPercentage() > 0.0f) { @@ -538,51 +538,56 @@ void Bridge::setDrawBridgeStage(bool open) { } //------------------------------------------------------------------------------------------------- -/** updateSpanObjectGeometry - match the span object's collision box to the state of the bridge. */ +/** updateBridgeObjectGeometry - match the bridge object's collision box to the state of the deck. */ //------------------------------------------------------------------------------------------------- -void Bridge::updateSpanObjectGeometry() +void Bridge::updateBridgeObjectGeometry() { Object *bridgeObj = TheGameLogic->findObjectByID( m_bridgeInfo.bridgeObjectID ); if( bridgeObj == nullptr ) return; // - // landmark bridges are authored with a box that matches their model, and DrawBridgeUpdate owns - // it, so leave them alone + // 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. // - if( bridgeObj->getTemplate()->isBridge() ) - return; + const Bool deckIsGone = (bridgeObj->getBodyModule()->getDamageState() == BODY_RUBBLE) || hasHole(); GeometryInfo geom = bridgeObj->getTemplate()->getTemplateGeometryInfo(); - if( bridgeObj->getBodyModule()->getDamageState() == BODY_RUBBLE ) + if( deckIsGone ) { - // - // nothing is left of the span to shoot at or bump into. the box is as long as the whole - // bridge, so leaving it behind makes every shot passing near the wreck detonate on it. - // geom.set( GEOMETRY_BOX, TRUE, 0.0f, 0.0f, 0.0f ); } else { - // - // the object template geometry is just a placeholder since the span is drawn by the bridge - // buffer; 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 ); + 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 span has to ask for them again + // back on, so a repaired bridge has to ask for them again // bridgeObj->clearStatus( MAKE_OBJECT_STATUS_MASK( OBJECT_STATUS_NO_COLLISIONS ) ); } - // setGeometryInfo, not setGeometryInfoZ -- the partition footprint has to change with the box + // + // 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 ); } //------------------------------------------------------------------------------------------------- @@ -599,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); @@ -1040,7 +1055,7 @@ void Bridge::updateDamageState() m_bridgeInfo.curDamageState = damageState; if (damageState == BODY_RUBBLE) { TheAI->pathfinder()->changeBridgeState(m_layer, false); - updateSpanObjectGeometry(); + updateBridgeObjectGeometry(); m_bridgeInfo.damageStateChanged = true; Object *obj; for (obj = TheGameLogic->getFirstObject(); obj; obj=obj->getNextObject()) { @@ -1078,9 +1093,15 @@ void Bridge::updateDamageState() if( bbi == nullptr || bbi->isScaffoldPresent() == FALSE ) TheAI->pathfinder()->changeBridgeState(m_layer, true); - // the span is shootable again as soon as it stops being rubble, even while the - // scaffolding still keeps the deck closed - updateSpanObjectGeometry(); + // + // 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; } } diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/BridgeBehavior.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/BridgeBehavior.cpp index d3147ef36cf..04ec01b21eb 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/BridgeBehavior.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/BridgeBehavior.cpp @@ -984,6 +984,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 +1006,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/Update/DrawBridgeUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/DrawBridgeUpdate.cpp index 9f8c48afb32..78c8c95c9b0 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/DrawBridgeUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/DrawBridgeUpdate.cpp @@ -182,9 +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()); - // a procedural span's box comes from the bridge, not from its placeholder template + // the box comes from the bridge, not from a procedural span's placeholder template if (bridge != nullptr) - bridge->updateSpanObjectGeometry(); + bridge->updateBridgeObjectGeometry(); m_openingFrame = 0U; // when rapid toggling is possible m_closingDamageFrame = TheGameLogic->getFrame() + data->m_closingDamageTime; From ac4591b11957702337b38521dc89013d97c4d67b Mon Sep 17 00:00:00 2001 From: pWn3d Date: Wed, 2 Sep 2026 18:27:56 +0200 Subject: [PATCH 6/7] Sectional Bridge animations --- .../Include/GameClient/TerrainRoads.h | 32 ++ .../GameClient/Terrain/TerrainRoads.cpp | 21 ++ .../Include/GameLogic/Module/BridgeBehavior.h | 1 + .../Object/Behavior/BridgeBehavior.cpp | 101 +++++- .../W3DDevice/GameClient/W3DBridgeBuffer.h | 35 +- .../W3DDevice/GameClient/W3DBridgeBuffer.cpp | 307 +++++++++++++++++- 6 files changed, 468 insertions(+), 29 deletions(-) diff --git a/Core/GameEngine/Include/GameClient/TerrainRoads.h b/Core/GameEngine/Include/GameClient/TerrainRoads.h index f585b438a0b..3cfe9fa07b6 100644 --- a/Core/GameEngine/Include/GameClient/TerrainRoads.h +++ b/Core/GameEngine/Include/GameClient/TerrainRoads.h @@ -101,6 +101,13 @@ class TerrainRoadType : public MemoryPoolObject 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; } @@ -134,6 +141,13 @@ class TerrainRoadType : public MemoryPoolObject 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 @@ -212,6 +226,24 @@ class TerrainRoadType : public MemoryPoolObject // 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 5f2221b263c..147839f90e6 100644 --- a/Core/GameEngine/Source/GameClient/Terrain/TerrainRoads.cpp +++ b/Core/GameEngine/Source/GameClient/Terrain/TerrainRoads.cpp @@ -88,6 +88,13 @@ const FieldParse TerrainRoadType::m_terrainBridgeFieldParseTable[] = { "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 }, @@ -225,6 +232,13 @@ TerrainRoadType::TerrainRoadType() 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; } @@ -416,6 +430,13 @@ TerrainRoadType *TerrainRoadCollection::newBridge( AsciiString name ) 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() ); 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/Source/GameLogic/Object/Behavior/BridgeBehavior.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/BridgeBehavior.cpp index 04ec01b21eb..d61c0084df4 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, @@ -852,14 +914,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 +935,7 @@ UpdateSleepTime BridgeBehavior::update() // launch the fx list - FXList::doFXPos( (*fxIt).fx, &pos ); + FXList::doFXPos( (*fxIt).fx, &pos, pMtx ); } @@ -923,8 +991,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() ); 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); From 0d213e4636c98599ecff66dcafef26f151e2fc55 Mon Sep 17 00:00:00 2001 From: pWn3d Date: Thu, 3 Sep 2026 16:51:02 +0200 Subject: [PATCH 7/7] road bridges push on repair --- .../Source/GameLogic/Object/Behavior/BridgeBehavior.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/BridgeBehavior.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/BridgeBehavior.cpp index d61c0084df4..fe41332c486 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/BridgeBehavior.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/BridgeBehavior.cpp @@ -691,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();