Fix gate facing so gates sit on the destination side of system center
- Sort gate candidates by destination-side before tier, so an on-ray point behind the center loses to any destination-side aimed/scan point - Add a new soft "facing from the center" check in tests: when some anchor's tether circle reaches the destination side (maxProj + tether > 0), the gate must lie there; a single far planet may still take the best aimed point - Prefer a destination-side aimed candidate in the clearance fallback
This commit is contained in:
parent
963b2b5bc2
commit
a846562782
|
|
@ -21,9 +21,14 @@
|
||||||
* - ANCHORED systems (a planet — or the home world in the starting
|
* - ANCHORED systems (a planet — or the home world in the starting
|
||||||
* system): every gate is within level-1 tether (tether.level1Radius)
|
* system): every gate is within level-1 tether (tether.level1Radius)
|
||||||
* of a planet anchor and FACES its destination star on the 2-D map
|
* of a planet anchor and FACES its destination star on the 2-D map
|
||||||
* (soft rule — within 90° of the system→star bearing from the
|
* (soft rule — the gate sits on the DESTINATION SIDE of the system
|
||||||
* anchoring object); stations are NOT anchors (the player must be
|
* center whenever some anchor can reach that side (maxProj + tether
|
||||||
* able to build out from a world), but they DO get clearance;
|
* > 0), and from the anchoring object it is within 90° of the
|
||||||
|
* system→star bearing; when the only anchor sits farther than the
|
||||||
|
* tether opposite the destination — a single far planet — the gate
|
||||||
|
* takes the best aimed point instead); stations are NOT anchors (the
|
||||||
|
* player must be able to build out from a world), but they DO get
|
||||||
|
* clearance;
|
||||||
* - every non-barren system holds at least ONE planet (the gate's
|
* - every non-barren system holds at least ONE planet (the gate's
|
||||||
* anchor is guaranteed to exist);
|
* anchor is guaranteed to exist);
|
||||||
* - BARREN systems (objectCount → 0 — the network's dead-end leaves):
|
* - BARREN systems (objectCount → 0 — the network's dead-end leaves):
|
||||||
|
|
@ -243,9 +248,21 @@ const norm = (a) => ((a % (2 * Math.PI)) + 3 * Math.PI) % (2 * Math.PI) - Math.P
|
||||||
tether++;
|
tether++;
|
||||||
if (!tWhy) tWhy = `${sys.id}: gate ${j.id} is ${Math.round(bestD)} px from its nearest anchor (> ${TETHER})`;
|
if (!tWhy) tWhy = `${sys.id}: gate ${j.id} is ${Math.round(bestD)} px from its nearest anchor (> ${TETHER})`;
|
||||||
}
|
}
|
||||||
// FACING (soft): from the anchoring object, the gate is on the
|
// FACING (soft, from the CENTER): the gate sits on the DESTINATION
|
||||||
// target side — within 90° of the system→star bearing (some anchor
|
// side of the system center — dot(gate, destDir) > 0 — whenever
|
||||||
// must satisfy BOTH the tether and the facing).
|
// geometry allows it: some anchor's tether circle reaches that side
|
||||||
|
// (maxProj + tether > 0). When every anchor sits farther than the
|
||||||
|
// tether opposite the destination (a single far planet), the gate
|
||||||
|
// takes the best aimed point — the tether rule (hard) outranks the
|
||||||
|
// facing rule (soft).
|
||||||
|
const maxProj = Math.max(...anchors.map((a) => a.x * Math.cos(th) + a.y * Math.sin(th)));
|
||||||
|
if (maxProj + TETHER > 0 && j.x * Math.cos(th) + j.y * Math.sin(th) <= 1e-6) {
|
||||||
|
facing++;
|
||||||
|
if (!fWhy) fWhy = `${sys.id}: gate ${j.id} faces AWAY from its destination (wrong side of the center)`;
|
||||||
|
}
|
||||||
|
// FACING (soft, from the anchor): from the anchoring object, the
|
||||||
|
// gate is on the target side — within 90° of the system→star
|
||||||
|
// bearing (some anchor must satisfy BOTH the tether and the facing).
|
||||||
let onSide = false;
|
let onSide = false;
|
||||||
for (const a of anchors) {
|
for (const a of anchors) {
|
||||||
const d = Math.hypot(j.x - a.x, j.y - a.y);
|
const d = Math.hypot(j.x - a.x, j.y - a.y);
|
||||||
|
|
|
||||||
|
|
@ -458,7 +458,13 @@ function bestRotation(place, targetAngles) {
|
||||||
* target star, (3) a forward-hemisphere scan of the circle (±75°).
|
* target star, (3) a forward-hemisphere scan of the circle (±75°).
|
||||||
* Every candidate is exactly `range` from its anchor, so
|
* Every candidate is exactly `range` from its anchor, so
|
||||||
* tether-reachability holds by construction and the facing deviation
|
* tether-reachability holds by construction and the facing deviation
|
||||||
* never exceeds 90° (in practice a few degrees). Anchors are PLANETS
|
* never exceeds 90° (in practice a few degrees). Candidates on the
|
||||||
|
* DESTINATION SIDE of the system center sort first — before every
|
||||||
|
* tier — so the gate faces its destination FROM THE CENTER (the rule
|
||||||
|
* the map reads) and is never placed on the far side; an on-ray
|
||||||
|
* intersection behind the center loses to any destination-side
|
||||||
|
* aimed/scan point.
|
||||||
|
* Anchors are PLANETS
|
||||||
* only (plus the home world at home) — free-space stations are
|
* only (plus the home world at home) — free-space stations are
|
||||||
* deliberately excluded: the build console lives on a world, so every
|
* deliberately excluded: the build console lives on a world, so every
|
||||||
* gate must be tether-reachable from a planet the player can build
|
* gate must be tether-reachable from a planet the player can build
|
||||||
|
|
@ -575,25 +581,32 @@ function layoutGates(seed, record, planets, freeSpace, isHome, targets) {
|
||||||
// (32 points, ± up to 75° from the target direction) — the
|
// (32 points, ± up to 75° from the target direction) — the
|
||||||
// clearance search for tight systems.
|
// clearance search for tight systems.
|
||||||
// Every candidate is exactly `range` from an anchor, so the level-N
|
// Every candidate is exactly `range` from an anchor, so the level-N
|
||||||
// tether rule (hard) is met by construction; the direction rule (soft)
|
// tether rule (hard) is met by construction. The facing rule (soft —
|
||||||
// is honored by the tier order: on-ray → aimed → near-aimed.
|
// the gate faces its destination FROM THE SYSTEM CENTER) is honored
|
||||||
|
// by the sort: destination-side candidates (side 0) come FIRST —
|
||||||
|
// before every tier — so a gate is never placed on the far side of
|
||||||
|
// the center (an on-ray intersection behind the center loses to any
|
||||||
|
// destination-side aimed/scan point); then on-ray → aimed →
|
||||||
|
// near-aimed.
|
||||||
const cands = [];
|
const cands = [];
|
||||||
|
const add = (tier, order, px, py) =>
|
||||||
|
cands.push({ tier, order, side: px * ux + py * uy > 0 ? 0 : 1, px, py });
|
||||||
anchors.forEach((a, ai) => {
|
anchors.forEach((a, ai) => {
|
||||||
const proj = a.x * ux + a.y * uy; // signed distance along the ray
|
const proj = a.x * ux + a.y * uy; // signed distance along the ray
|
||||||
const h = Math.abs(a.x * uy - a.y * ux); // perpendicular distance
|
const h = Math.abs(a.x * uy - a.y * ux); // perpendicular distance
|
||||||
if (h <= range) {
|
if (h <= range) {
|
||||||
const off = Math.sqrt(Math.max(0, range * range - h * h));
|
const off = Math.sqrt(Math.max(0, range * range - h * h));
|
||||||
cands.push({ tier: 1, order: h * 1e6 + ai * 1000, px: (proj + off) * ux, py: (proj + off) * uy });
|
add(1, h * 1e6 + ai * 1000, (proj + off) * ux, (proj + off) * uy);
|
||||||
}
|
}
|
||||||
cands.push({ tier: 2, order: h * 1e6 + ai * 1000, px: a.x + range * ux, py: a.y + range * uy });
|
add(2, h * 1e6 + ai * 1000, a.x + range * ux, a.y + range * uy);
|
||||||
for (let k = 0; k < 32; k++) {
|
for (let k = 0; k < 32; k++) {
|
||||||
const phi = -1.3089 + (2.6179 * k) / 31; // ±75° around the target direction
|
const phi = -1.3089 + (2.6179 * k) / 31; // ±75° around the target direction
|
||||||
const dx = ux * Math.cos(phi) - uy * Math.sin(phi);
|
const dx = ux * Math.cos(phi) - uy * Math.sin(phi);
|
||||||
const dy = ux * Math.sin(phi) + uy * Math.cos(phi);
|
const dy = ux * Math.sin(phi) + uy * Math.cos(phi);
|
||||||
cands.push({ tier: 3, order: Math.abs(phi) * 1e6 + h + ai * 1e-3, px: a.x + range * dx, py: a.y + range * dy });
|
add(3, Math.abs(phi) * 1e6 + h + ai * 1e-3, a.x + range * dx, a.y + range * dy);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
cands.sort((p, q) => p.tier - q.tier || p.order - q.order);
|
cands.sort((p, q) => p.side - q.side || p.tier - q.tier || p.order - q.order);
|
||||||
|
|
||||||
const ok = (c) => {
|
const ok = (c) => {
|
||||||
const d2c = c.px * c.px + c.py * c.py;
|
const d2c = c.px * c.px + c.py * c.py;
|
||||||
|
|
@ -622,9 +635,12 @@ function layoutGates(seed, record, planets, freeSpace, isHome, targets) {
|
||||||
if (!chosen) {
|
if (!chosen) {
|
||||||
// Every candidate failed clearance (nearly impossible — an anchor's
|
// Every candidate failed clearance (nearly impossible — an anchor's
|
||||||
// tether circle is 5120 px across, the discs under a thousand): take
|
// tether circle is 5120 px across, the discs under a thousand): take
|
||||||
// the first anchor's aimed point anyway — the tether and facing rules
|
// the first DESTINATION-SIDE aimed point anyway — the tether and
|
||||||
// outrank cosmetics.
|
// facing rules outrank cosmetics.
|
||||||
chosen = cands.find((c) => c.tier === 2) ?? cands[0];
|
chosen =
|
||||||
|
cands.find((c) => c.tier === 2 && c.side === 0) ??
|
||||||
|
cands.find((c) => c.tier === 2) ??
|
||||||
|
cands[0];
|
||||||
console.warn(
|
console.warn(
|
||||||
`[orbit] ${record.id}: gate ${i + 1} fell back to its first aimed candidate (clearance)`,
|
`[orbit] ${record.id}: gate ${i + 1} fell back to its first aimed candidate (clearance)`,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue