@@ -1390,20 +1390,17 @@ local function BuildMpCache()
13901390 end
13911391 end
13921392
1393- -- Per-node tree-path distance from the component root. Forward pass: parent
1394- -- precedes child in `order`, so the parent's distance is already final.
1395- -- Topology-stable, so free to accumulate here. Drives the twig-pulse stagger
1396- -- so thorn pulses chain across edges instead of restarting per cable.
1397- local distFromRoot = {}
1393+ -- Length of each node's tree edge to its DFS parent. Topology-stable, so
1394+ -- computed once here; the per-tick flow-phase pass (ComputeMaxPotentials)
1395+ -- accumulates these with a sign from the current flow orientation.
1396+ local parentEdgeLen = {}
13981397 for i = 1 , # order do
13991398 local u = order [i ]
14001399 local pi = parentInTree [u ]
1401- if not pi then
1402- distFromRoot [u ] = 0
1403- else
1400+ if pi then
14041401 local e = edges [pi .key ]
14051402 local dx , dz = e .px - e .cx , e .pz - e .cz
1406- distFromRoot [u ] = distFromRoot [ pi . parent ] + sqrt (dx * dx + dz * dz )
1403+ parentEdgeLen [u ] = sqrt (dx * dx + dz * dz )
14071404 end
14081405 end
14091406
@@ -1459,7 +1456,7 @@ local function BuildMpCache()
14591456 mpCache .subPmaxNonWind = subPmaxNonWind
14601457 mpCache .subWindCount = subWindCount
14611458 mpCache .subWindBase = subWindBase
1462- mpCache .distFromRoot = distFromRoot
1459+ mpCache .parentEdgeLen = parentEdgeLen
14631460 mpCache .valid = true
14641461end
14651462
@@ -1588,6 +1585,33 @@ local function ComputeMaxPotentials(flowMode)
15881585 end
15891586 end
15901587
1588+ -- Flow-phase potential φ, one per node: φ(child) − φ(parent) = ±len, signed by
1589+ -- the current flow orientation. On an MST (acyclic) this is always consistent,
1590+ -- so φ rises by edge length along every flow edge — the twig wave then follows
1591+ -- flow, with merges/splits aligning (no discontinuity). Recomputed per tick
1592+ -- (flow reorients); just adds/subtracts the precomputed parentEdgeLen.
1593+ local parentEdgeLen = mpCache .parentEdgeLen
1594+ local flowPhase = {}
1595+ for i = 1 , # order do
1596+ local u = order [i ]
1597+ local pi = parentInTree [u ]
1598+ if not pi then
1599+ flowPhase [u ] = 0
1600+ else
1601+ local p = pi .parent
1602+ local len = parentEdgeLen [u ] or 0
1603+ local edge = edges [pi .key ]
1604+ -- edge.parentID is the flow-source end. Aligned with the DFS parent
1605+ -- (flow p→u) → u is downstream, add; reversed (flow u→p) → subtract.
1606+ if edge and edge .parentID == p then
1607+ flowPhase [u ] = flowPhase [p ] + len
1608+ else
1609+ flowPhase [u ] = flowPhase [p ] - len
1610+ end
1611+ end
1612+ end
1613+ mpCache .flowPhase = flowPhase
1614+
15911615 if debugLog and # debugLog > 0 then
15921616 Spring .Echo (" [OD-cables] capacities:" )
15931617 for i = 1 , # debugLog do Spring .Echo (debugLog [i ]) end
@@ -1697,7 +1721,7 @@ local function SendAll()
16971721 end
16981722
16991723 -- Bin edges by ally, in one pass.
1700- local distFromRoot = mpCache .distFromRoot
1724+ local flowPhase = mpCache .flowPhase
17011725 local perAlly = {}
17021726 for key , edge in pairs (edges ) do
17031727 local ally = allyOfUnit [edge .parentID ] or allyOfUnit [edge .childID ]
@@ -1707,7 +1731,7 @@ local function SendAll()
17071731 pa = {
17081732 keys = {}, pxs = {}, pzs = {}, cxs = {}, czs = {},
17091733 caps = {}, maxxed = {}, flows = {}, effs = {},
1710- rootAs = {}, rootBs = {}, n = 0 ,
1734+ phaseAs = {}, phaseBs = {}, n = 0 ,
17111735 }
17121736 perAlly [ally ] = pa
17131737 end
@@ -1719,10 +1743,10 @@ local function SendAll()
17191743 pa .caps [i ] = capacities [key ] or 0
17201744 pa .maxxed [i ] = (spGetUnitRulesParam (edge .parentID , " OD_gridMaxxed" ) or spGetUnitRulesParam (edge .childID , " OD_gridMaxxed" )) == 1
17211745 pa .flows [i ] = flows [key ] or 0
1722- -- Per-end distance-from-root for the twig pulse. v0 = parent end,
1723- -- v1 = child end; keyed by node so flow reorientation just swaps them.
1724- pa .rootAs [i ] = distFromRoot [edge .parentID ] or 0
1725- pa .rootBs [i ] = distFromRoot [edge .childID ] or 0
1746+ -- Per-end flow phase for the twig pulse. v0 = parent end, v1 = child
1747+ -- end; keyed by node so flow reorientation just swaps them.
1748+ pa .phaseAs [i ] = flowPhase [edge .parentID ] or 0
1749+ pa .phaseBs [i ] = flowPhase [edge .childID ] or 0
17261750 -- Cached per-grid lookup; fall back to child end if parent's grid
17271751 -- is unknown. 0 → magenta in the shader (unit_mex_overdrive's
17281752 -- "no grid" sentinel).
@@ -1742,7 +1766,7 @@ local function SendAll()
17421766 keys = pa .keys , pxs = pa .pxs , pzs = pa .pzs ,
17431767 cxs = pa .cxs , czs = pa .czs ,
17441768 caps = pa .caps , maxxed = pa .maxxed , flows = pa .flows , effs = pa .effs ,
1745- rootAs = pa .rootAs , rootBs = pa .rootBs ,
1769+ phaseAs = pa .phaseAs , phaseBs = pa .phaseBs ,
17461770 })
17471771 alliesWithEdges [ally ] = true
17481772 end
@@ -2348,10 +2372,10 @@ local function GenerateOrganicTree()
23482372 local eff = (e .eff or 0 ) * (e .maxxed and - 1 or 1 )
23492373 local flow = e .flow or 0
23502374 -- vertGrid.z (ex-bubblePhase, now dead — GS drops gridData.z) carries each
2351- -- end's distance-from-root : v0 = rootA , v1 = rootB . GS lerps it to twig
2352- -- roots to chain pulses across edges.
2353- local rootA = e .rootA or 0
2354- local rootB = e .rootB or 0
2375+ -- end's flow phase : v0 = phaseA , v1 = phaseB . GS lerps it to twig roots so
2376+ -- the pulse follows flow continuously across edges.
2377+ local phaseA = e .phaseA or 0
2378+ local phaseB = e .phaseB or 0
23552379 local isOwn = (fullview and 2 ) or (e .isOwnAlly and 1 ) or 0
23562380 -- Coverage SSBO slot index, or -1 to disable bit updates / lookups
23572381 -- on the GS side. Stored as float here for VBO layout simplicity;
@@ -2362,12 +2386,12 @@ local function GenerateOrganicTree()
23622386 -- Vertex 0: parent end (10 floats: pos2 + data3 + grid4 + slot)
23632387 verts [k + 1 ] = e .px ; verts [k + 2 ] = e .pz
23642388 verts [k + 3 ] = cap ; verts [k + 4 ] = appearTime ; verts [k + 5 ] = witherTime
2365- verts [k + 6 ] = eff ; verts [k + 7 ] = flow ; verts [k + 8 ] = rootA ; verts [k + 9 ] = isOwn
2389+ verts [k + 6 ] = eff ; verts [k + 7 ] = flow ; verts [k + 8 ] = phaseA ; verts [k + 9 ] = isOwn
23662390 verts [k + 10 ] = slot
2367- -- Vertex 1: child end (same per-edge payload, except rootB for its end)
2391+ -- Vertex 1: child end (same per-edge payload, except phaseB for its end)
23682392 verts [k + 11 ] = e .cx ; verts [k + 12 ] = e .cz
23692393 verts [k + 13 ] = cap ; verts [k + 14 ] = appearTime ; verts [k + 15 ] = witherTime
2370- verts [k + 16 ] = eff ; verts [k + 17 ] = flow ; verts [k + 18 ] = rootB ; verts [k + 19 ] = isOwn
2394+ verts [k + 16 ] = eff ; verts [k + 17 ] = flow ; verts [k + 18 ] = phaseB ; verts [k + 19 ] = isOwn
23712395 verts [k + 20 ] = slot
23722396 k = k + 20
23732397 end
@@ -2612,10 +2636,10 @@ function OnCableTreeFull(data)
26122636 e .eff = data .effs and data .effs [i ] or 0
26132637 e .px , e .pz = data .pxs [i ], data .pzs [i ]
26142638 e .cx , e .cz = data .cxs [i ], data .czs [i ]
2615- -- Refresh with px/cx so the per-end root distances track the
2639+ -- Refresh with px/cx so the per-end flow phases track the
26162640 -- current parent/child orientation.
2617- e .rootA = data .rootAs and data .rootAs [i ] or 0
2618- e .rootB = data .rootBs and data .rootBs [i ] or 0
2641+ e .phaseA = data .phaseAs and data .phaseAs [i ] or 0
2642+ e .phaseB = data .phaseBs and data .phaseBs [i ] or 0
26192643 end
26202644 e .isOwnAlly = ownAlly
26212645 -- Late-bind a coverage slot if missing (e.g., gadget was reloaded
@@ -2649,8 +2673,8 @@ function OnCableTreeFull(data)
26492673 maxxed = data .maxxed [i ],
26502674 flow = newFlow ,
26512675 eff = data .effs and data .effs [i ] or 0 ,
2652- rootA = data .rootAs and data .rootAs [i ] or 0 ,
2653- rootB = data .rootBs and data .rootBs [i ] or 0 ,
2676+ phaseA = data .phaseAs and data .phaseAs [i ] or 0 ,
2677+ phaseB = data .phaseBs and data .phaseBs [i ] or 0 ,
26542678 isOwnAlly = ownAlly ,
26552679 appearFrame = af ,
26562680 witherFrame = nil ,
0 commit comments