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
|
||||
* system): every gate is within level-1 tether (tether.level1Radius)
|
||||
* 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
|
||||
* anchoring object); stations are NOT anchors (the player must be
|
||||
* able to build out from a world), but they DO get clearance;
|
||||
* (soft rule — the gate sits on the DESTINATION SIDE of the system
|
||||
* center whenever some anchor can reach that side (maxProj + tether
|
||||
* > 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
|
||||
* anchor is guaranteed to exist);
|
||||
* - 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++;
|
||||
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
|
||||
// target side — within 90° of the system→star bearing (some anchor
|
||||
// must satisfy BOTH the tether and the facing).
|
||||
// FACING (soft, from the CENTER): the gate sits on the DESTINATION
|
||||
// side of the system center — dot(gate, destDir) > 0 — whenever
|
||||
// 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;
|
||||
for (const a of anchors) {
|
||||
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°).
|
||||
* Every candidate is exactly `range` from its anchor, so
|
||||
* 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
|
||||
* deliberately excluded: the build console lives on a world, so every
|
||||
* 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
|
||||
// clearance search for tight systems.
|
||||
// Every candidate is exactly `range` from an anchor, so the level-N
|
||||
// tether rule (hard) is met by construction; the direction rule (soft)
|
||||
// is honored by the tier order: on-ray → aimed → near-aimed.
|
||||
// tether rule (hard) is met by construction. The facing rule (soft —
|
||||
// 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 add = (tier, order, px, py) =>
|
||||
cands.push({ tier, order, side: px * ux + py * uy > 0 ? 0 : 1, px, py });
|
||||
anchors.forEach((a, ai) => {
|
||||
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
|
||||
if (h <= range) {
|
||||
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++) {
|
||||
const phi = -1.3089 + (2.6179 * k) / 31; // ±75° around the target direction
|
||||
const dx = ux * Math.cos(phi) - uy * Math.sin(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 d2c = c.px * c.px + c.py * c.py;
|
||||
|
|
@ -622,9 +635,12 @@ function layoutGates(seed, record, planets, freeSpace, isHome, targets) {
|
|||
if (!chosen) {
|
||||
// Every candidate failed clearance (nearly impossible — an anchor's
|
||||
// tether circle is 5120 px across, the discs under a thousand): take
|
||||
// the first anchor's aimed point anyway — the tether and facing rules
|
||||
// outrank cosmetics.
|
||||
chosen = cands.find((c) => c.tier === 2) ?? cands[0];
|
||||
// the first DESTINATION-SIDE aimed point anyway — the tether and
|
||||
// facing rules outrank cosmetics.
|
||||
chosen =
|
||||
cands.find((c) => c.tier === 2 && c.side === 0) ??
|
||||
cands.find((c) => c.tier === 2) ??
|
||||
cands[0];
|
||||
console.warn(
|
||||
`[orbit] ${record.id}: gate ${i + 1} fell back to its first aimed candidate (clearance)`,
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue