Never place a chevron on the flat bottom threshold

pickChevrons() spaced chevrons evenly starting from whichever ring
block the flood-fill happened to discover first - an arbitrary point
depending on where the ring was punched, with no relationship to the
ring's actual top/bottom. That let spacing land a chevron directly on
the bottom row you walk through, which a real Stargate never has.

For a vertical ring, chevron spacing is now anchored at the true
topmost block and the bottom row is excluded from the candidate list
entirely before spacing runs. Horizontal rings are unaffected - they
have no "bottom" to avoid, so they still use the full ring.
This commit is contained in:
Michael Burgess
2026-08-09 14:29:42 -04:00
parent 712af607d7
commit 508a3d9b51
2 changed files with 37 additions and 9 deletions
@@ -266,6 +266,8 @@ public class GateStructureScanner {
* Picks {@code chevronCount} chevrons evenly spaced around the ring's actual geometric
* perimeter - walks the ring in order (each frame block should have exactly two ring
* neighbours) and takes evenly-spaced indices from that walk, not raw scan-discovery order.
* For a vertical ring, spacing is anchored at the topmost point and the flat bottom
* threshold row is never a candidate - a real Stargate never has a chevron underfoot.
*/
private List<GateStructure.ChevronSlot> pickChevrons(List<Block> frameBlocks, BlockFace[] inPlane) {
Map<Long, Block> plane = new HashMap<>();
@@ -278,16 +280,40 @@ public class GateStructureScanner {
List<Block> ordered = walkRing(frameBlocks, plane, inPlane);
List<Block> ring = (ordered != null && ordered.size() >= 4) ? ordered : frameBlocks;
int count = Math.max(0, Math.min(chevronCount, ring.size()));
boolean vertical = false;
for (BlockFace f : inPlane) if (f == BlockFace.UP || f == BlockFace.DOWN) vertical = true;
List<Block> candidates;
if (vertical) {
int n = ring.size();
int[] rows = new int[n];
int minRow = Integer.MAX_VALUE, maxRow = Integer.MIN_VALUE, topIdx = 0;
for (int i = 0; i < n; i++) {
rows[i] = project(ring.get(i), inPlane)[0]; // row is world Y for a vertical ring
if (rows[i] < minRow) minRow = rows[i];
if (rows[i] > maxRow) { maxRow = rows[i]; topIdx = i; }
}
candidates = new ArrayList<>();
for (int i = 0; i < n; i++) {
int idx = (topIdx + i) % n;
if (rows[idx] == minRow) continue; // skip the flat bottom threshold
candidates.add(ring.get(idx));
}
if (candidates.isEmpty()) candidates = ring; // degenerate shape fallback
} else {
candidates = ring; // a horizontal ring has no "bottom" to avoid
}
int count = Math.max(0, Math.min(chevronCount, candidates.size()));
List<GateStructure.ChevronSlot> slots = new ArrayList<>(count);
if (count == 0) return slots;
double step = ring.size() / (double) count;
double step = candidates.size() / (double) count;
Set<Integer> chosen = new HashSet<>();
for (int i = 0; i < count; i++) {
int idx = (int) Math.round(i * step) % ring.size();
while (chosen.contains(idx)) idx = (idx + 1) % ring.size();
int idx = (int) Math.round(i * step) % candidates.size();
while (chosen.contains(idx)) idx = (idx + 1) % candidates.size();
chosen.add(idx);
Block b = ring.get(idx);
Block b = candidates.get(idx);
slots.add(new GateStructure.ChevronSlot(b, b.getType()));
}
return slots;