mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 06:00:41 +00:00
805f0968b1
## Description: Relates to #3725 Adds a new **Impassable** terrain type that enables non-rectangular maps and creates impassable barriers on the map. Painted with pure black (`#000`) in the map editor's `image.png`. **Encoding:** Impassable terrain is encoded in the binary format as `isLand=1, magnitude=31` (previously unused). The Go map generator detects `#000` pixels and produces this encoding. The map generator's minimap downscaling gives impassable highest priority (Impassable > Water > Land). Thumbnails render impassable as transparent so the map picker background shows through. **Rendering:** Impassable tiles render as the map background colour (`rgb(60, 60, 60)`, matching `gl.clearColor` in `Renderer.ts`), making them visually indistinguishable from the area outside the map quad. This enables maps to appear non-rectangular. **Gameplay restrictions:** Impassable terrain cannot be: - Owned (`conquer()` throws) - Attacked (`AttackExecution` skips impassable tiles in both `tick()` and `addNeighbors()`) - Nuked (targeting rejected in `nukeSpawn()`, blast radius filtered in `tilesToDestroy()`) - Spawned on (nations, human players, and structures all reject impassable tiles) - Converted to water (guarded in `WaterManager` and `setWater()`) **Nuke trajectories:** Nuke trajectories cannot cross impassable terrain, matching the existing map-border enforcement. This is checked at launch time in `NukeExecution.tick()`. The client-side trajectory preview turns red with a red X where the arc crosses impassable terrain (reusing the existing SAM-intercept visual pipeline in `NukeTrajectory.ts`). The nuke ghost preview is completely hidden when hovering over impassable terrain (same as hovering outside the map). https://github.com/user-attachments/assets/ff131146-9749-41e0-892a-617e5cd16c54 Impassable terrain is transparent on the thumbnail: <img width="213" height="152" alt="Screenshot 2026-06-18 211640" src="https://github.com/user-attachments/assets/ede16f8c-9239-4ab1-be5d-0ba81cce5e9e" /> Tested with water nukes, made sure there is no water depth gradient near the impassable terrain, just like at the world border: <img width="774" height="771" alt="Screenshot 2026-06-18 212348" src="https://github.com/user-attachments/assets/4429069d-911b-48e8-91e3-7307d42c9397" /> Models used: GLM 5.2 and MiMo 2.5 Pro 😄 ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin
135 lines
4.2 KiB
TypeScript
135 lines
4.2 KiB
TypeScript
import {
|
|
buildNukeTrajectory,
|
|
computeNukeControlPoints,
|
|
computeTrajectoryThresholds,
|
|
type SAMInfo,
|
|
} from "../src/client/render/gl/utils/NukeTrajectory";
|
|
|
|
// A large map height so the parabola arc isn't clamped.
|
|
const MAP_H = 1000;
|
|
|
|
// Helper: build control points for a straight horizontal trajectory.
|
|
function horizontalCp(srcX: number, dstX: number) {
|
|
return computeNukeControlPoints(srcX, 500, dstX, 500, MAP_H, true);
|
|
}
|
|
|
|
describe("NukeTrajectory impassable terrain blocking", () => {
|
|
test("tSamIntercept is 1.0 when no SAMs and no blocked terrain", () => {
|
|
const cp = horizontalCp(100, 800);
|
|
const th = computeTrajectoryThresholds(cp, 100, 500, 800, 500, []);
|
|
expect(th.tSamIntercept).toBe(1.0);
|
|
});
|
|
|
|
test("tSamIntercept < 1.0 when trajectory crosses impassable terrain", () => {
|
|
const cp = horizontalCp(100, 800);
|
|
// Block tiles at x=400..500 (midway through the arc).
|
|
const isBlocked = (x: number) => x >= 400 && x <= 500;
|
|
const th = computeTrajectoryThresholds(
|
|
cp,
|
|
100,
|
|
500,
|
|
800,
|
|
500,
|
|
[],
|
|
isBlocked,
|
|
);
|
|
expect(th.tSamIntercept).toBeLessThan(1.0);
|
|
// The block is roughly at the midpoint of the curve (t ≈ 0.5).
|
|
expect(th.tSamIntercept).toBeGreaterThan(0.3);
|
|
expect(th.tSamIntercept).toBeLessThan(0.7);
|
|
});
|
|
|
|
test("tSamIntercept is 1.0 when blocked terrain is not on the trajectory", () => {
|
|
const cp = horizontalCp(100, 800);
|
|
// Block tiles far away from the trajectory.
|
|
const isBlocked = (x: number) => x >= 0 && x <= 50;
|
|
const th = computeTrajectoryThresholds(
|
|
cp,
|
|
100,
|
|
500,
|
|
800,
|
|
500,
|
|
[],
|
|
isBlocked,
|
|
);
|
|
// The source is at x=100, so blocking x=0..50 shouldn't affect the arc.
|
|
// (The arc starts at x=100 and goes to x=800, it never touches x<100.)
|
|
expect(th.tSamIntercept).toBe(1.0);
|
|
});
|
|
|
|
test("blocked terrain takes precedence (min of SAM and blocked)", () => {
|
|
const cp = horizontalCp(100, 800);
|
|
// SAM at x=600 with range covering a wide area.
|
|
const sams: SAMInfo[] = [{ x: 600, y: 500, rangeSq: 200 * 200 }];
|
|
// Block at x=300 (earlier than the SAM at x=600).
|
|
const isBlocked = (x: number) => x >= 300 && x <= 350;
|
|
const th = computeTrajectoryThresholds(
|
|
cp,
|
|
100,
|
|
500,
|
|
800,
|
|
500,
|
|
sams,
|
|
isBlocked,
|
|
);
|
|
// The block at x=300 should be hit first (lower t) than the SAM at x=600.
|
|
expect(th.tSamIntercept).toBeLessThan(0.5);
|
|
});
|
|
|
|
test("blocked scan covers the untargetable mid-air zone (not skipped like SAMs)", () => {
|
|
// With a long trajectory, there's an untargetable zone in the middle.
|
|
const cp = horizontalCp(100, 800);
|
|
const th = computeTrajectoryThresholds(cp, 100, 500, 800, 500, []);
|
|
// Verify there IS an untargetable zone.
|
|
expect(th.tUntargetableStart).toBeGreaterThanOrEqual(0);
|
|
expect(th.tUntargetableEnd).toBeGreaterThan(th.tUntargetableStart);
|
|
|
|
// Block a tile in the middle of the untargetable zone.
|
|
const blockT = (th.tUntargetableStart + th.tUntargetableEnd) / 2;
|
|
// Sample the Bezier at that t to find the x coordinate.
|
|
const { p0x, p1x, p2x, p3x } = cp;
|
|
const T = 1 - blockT;
|
|
const blockX = Math.floor(
|
|
T * T * T * p0x +
|
|
3 * T * T * blockT * p1x +
|
|
3 * T * blockT * blockT * p2x +
|
|
blockT * blockT * blockT * p3x,
|
|
);
|
|
const isBlocked = (x: number) => x === blockX;
|
|
|
|
const th2 = computeTrajectoryThresholds(
|
|
cp,
|
|
100,
|
|
500,
|
|
800,
|
|
500,
|
|
[],
|
|
isBlocked,
|
|
);
|
|
// The blocked tile is in the untargetable zone, but unlike SAMs, the
|
|
// impassable scan should still detect it.
|
|
expect(th2.tSamIntercept).toBeLessThan(1.0);
|
|
});
|
|
|
|
test("buildNukeTrajectory passes isBlocked through", () => {
|
|
const data = buildNukeTrajectory(
|
|
100,
|
|
500,
|
|
800,
|
|
500,
|
|
MAP_H,
|
|
true,
|
|
[],
|
|
(x: number) => x >= 400 && x <= 500,
|
|
);
|
|
expect(data.tSamIntercept).toBeLessThan(1.0);
|
|
});
|
|
|
|
test("buildNukeTrajectory works without isBlocked (backwards compatible)", () => {
|
|
const data = buildNukeTrajectory(100, 500, 800, 500, MAP_H, true, []);
|
|
expect(data.tSamIntercept).toBe(1.0);
|
|
expect(data.p0x).toBe(100);
|
|
expect(data.p3x).toBe(800);
|
|
});
|
|
});
|