feat(prototype): adapt the patrol field to the display aspect ratio
The field was a fixed square, leaving wide monitors with dead side margins. Field width now follows the window aspect (capped at 2.2:1, quantized to avoid rebuilds on minor resizes); routes, obstacles and transit spawns stretch with it, and the grid keeps 100 m cells.
This commit is contained in:
+44
-28
@@ -45,7 +45,14 @@ export interface World {
|
||||
broadcasts: number;
|
||||
}
|
||||
|
||||
export const AREA = 1000; // meters, square side
|
||||
export const AREA = 1000; // meters, vertical extent of the field
|
||||
export const MAX_ASPECT = 2.2;
|
||||
// Horizontal extent follows the display aspect ratio (set via makeWorld),
|
||||
// so wide monitors get a genuinely wider patrol area, not empty margins.
|
||||
let areaX = AREA;
|
||||
export function areaWidth(): number {
|
||||
return areaX;
|
||||
}
|
||||
export const LINK_RANGE = 320;
|
||||
const CRUISE = 28;
|
||||
const SEPARATION = 55;
|
||||
@@ -68,21 +75,26 @@ function rnd(): number {
|
||||
// Two interleaved patrol routes: even drones circle the perimeter, odd
|
||||
// drones fly an X through the center. Paths cross mid-field, so the mesh
|
||||
// keeps bridging between the front and the back of the formation instead
|
||||
// of stretching into disconnected segments along one loop.
|
||||
const PERIMETER: Vec[] = [
|
||||
{ x: 120, y: 120 },
|
||||
{ x: AREA - 120, y: 150 },
|
||||
{ x: AREA - 150, y: AREA - 120 },
|
||||
{ x: 150, y: AREA - 150 },
|
||||
];
|
||||
const CROSS: Vec[] = [
|
||||
{ x: 150, y: 150 },
|
||||
{ x: AREA - 150, y: AREA - 150 },
|
||||
{ x: AREA - 150, y: 150 },
|
||||
{ x: 150, y: AREA - 150 },
|
||||
];
|
||||
// of stretching into disconnected segments along one loop. Routes are
|
||||
// derived from the current field width, so they stretch on wide displays.
|
||||
function perimeterRoute(): Vec[] {
|
||||
return [
|
||||
{ x: 120, y: 120 },
|
||||
{ x: areaX - 120, y: 150 },
|
||||
{ x: areaX - 150, y: AREA - 120 },
|
||||
{ x: 150, y: AREA - 150 },
|
||||
];
|
||||
}
|
||||
function crossRoute(): Vec[] {
|
||||
return [
|
||||
{ x: 150, y: 150 },
|
||||
{ x: areaX - 150, y: AREA - 150 },
|
||||
{ x: areaX - 150, y: 150 },
|
||||
{ x: 150, y: AREA - 150 },
|
||||
];
|
||||
}
|
||||
function routeOf(d: Drone): Vec[] {
|
||||
return d.id % 2 === 0 ? PERIMETER : CROSS;
|
||||
return d.id % 2 === 0 ? perimeterRoute() : crossRoute();
|
||||
}
|
||||
|
||||
// Transit objects enter at one edge, cross the whole area, and respawn at
|
||||
@@ -91,14 +103,15 @@ function routeOf(d: Drone): Vec[] {
|
||||
const TRANSIT_MARGIN = 120;
|
||||
function spawnTransit(id: number): Obstacle {
|
||||
const speed = 26 + rnd() * 30;
|
||||
const along = 120 + rnd() * (AREA - 240);
|
||||
const alongX = 120 + rnd() * (areaX - 240);
|
||||
const alongY = 120 + rnd() * (AREA - 240);
|
||||
const skew = (rnd() - 0.5) * speed * 0.9; // diagonal-ish crossings
|
||||
const edge = Math.floor(rnd() * 4);
|
||||
const table: { pos: Vec; vel: Vec }[] = [
|
||||
{ pos: { x: along, y: -TRANSIT_MARGIN + 20 }, vel: { x: skew, y: speed } },
|
||||
{ pos: { x: along, y: AREA + TRANSIT_MARGIN - 20 }, vel: { x: skew, y: -speed } },
|
||||
{ pos: { x: -TRANSIT_MARGIN + 20, y: along }, vel: { x: speed, y: skew } },
|
||||
{ pos: { x: AREA + TRANSIT_MARGIN - 20, y: along }, vel: { x: -speed, y: skew } },
|
||||
{ pos: { x: alongX, y: -TRANSIT_MARGIN + 20 }, vel: { x: skew, y: speed } },
|
||||
{ pos: { x: alongX, y: AREA + TRANSIT_MARGIN - 20 }, vel: { x: skew, y: -speed } },
|
||||
{ pos: { x: -TRANSIT_MARGIN + 20, y: alongY }, vel: { x: speed, y: skew } },
|
||||
{ pos: { x: areaX + TRANSIT_MARGIN - 20, y: alongY }, vel: { x: -speed, y: skew } },
|
||||
];
|
||||
const { pos, vel } = table[edge];
|
||||
return { id, pos, radius: 22 + rnd() * 16, moving: true, vel };
|
||||
@@ -106,32 +119,35 @@ function spawnTransit(id: number): Obstacle {
|
||||
function outOfTransit(ob: Obstacle): boolean {
|
||||
return (
|
||||
ob.pos.x < -TRANSIT_MARGIN ||
|
||||
ob.pos.x > AREA + TRANSIT_MARGIN ||
|
||||
ob.pos.x > areaX + TRANSIT_MARGIN ||
|
||||
ob.pos.y < -TRANSIT_MARGIN ||
|
||||
ob.pos.y > AREA + TRANSIT_MARGIN
|
||||
);
|
||||
}
|
||||
|
||||
export function makeWorld(droneCount: number): World {
|
||||
export function makeWorld(droneCount: number, aspect = 1): World {
|
||||
areaX = AREA * Math.max(1, Math.min(MAX_ASPECT, aspect));
|
||||
const drones: Drone[] = Array.from({ length: droneCount }, (_, i) => {
|
||||
const angle = (i / droneCount) * Math.PI * 2;
|
||||
return {
|
||||
id: i,
|
||||
pos: {
|
||||
x: AREA / 2 + Math.cos(angle) * (150 + rnd() * 120),
|
||||
x: areaX / 2 + Math.cos(angle) * (150 + rnd() * 120),
|
||||
y: AREA / 2 + Math.sin(angle) * (150 + rnd() * 120),
|
||||
},
|
||||
vel: { x: 0, y: 0 },
|
||||
heading: angle,
|
||||
waypoint: i % PERIMETER.length,
|
||||
waypoint: i % 4,
|
||||
battery: 90 + rnd() * 10,
|
||||
channel: CHANNELS[i % CHANNELS.length],
|
||||
};
|
||||
});
|
||||
// Static obstacles sit at fixed fractions of the field, so they spread
|
||||
// out instead of clustering left when the field widens
|
||||
const obstacles: Obstacle[] = [
|
||||
{ id: 0, pos: { x: 340, y: 420 }, radius: 60, moving: false, vel: { x: 0, y: 0 } },
|
||||
{ id: 1, pos: { x: 700, y: 260 }, radius: 45, moving: false, vel: { x: 0, y: 0 } },
|
||||
{ id: 2, pos: { x: 560, y: 720 }, radius: 70, moving: false, vel: { x: 0, y: 0 } },
|
||||
{ id: 0, pos: { x: areaX * 0.34, y: 420 }, radius: 60, moving: false, vel: { x: 0, y: 0 } },
|
||||
{ id: 1, pos: { x: areaX * 0.7, y: 260 }, radius: 45, moving: false, vel: { x: 0, y: 0 } },
|
||||
{ id: 2, pos: { x: areaX * 0.56, y: 720 }, radius: 70, moving: false, vel: { x: 0, y: 0 } },
|
||||
spawnTransit(3),
|
||||
spawnTransit(4),
|
||||
spawnTransit(5),
|
||||
@@ -189,7 +205,7 @@ function steer(d: Drone, world: World, dt: number): Drone {
|
||||
vel: { x: vx, y: vy },
|
||||
heading,
|
||||
pos: {
|
||||
x: Math.max(20, Math.min(AREA - 20, d.pos.x + vx * dt)),
|
||||
x: Math.max(20, Math.min(areaX - 20, d.pos.x + vx * dt)),
|
||||
y: Math.max(20, Math.min(AREA - 20, d.pos.y + vy * dt)),
|
||||
},
|
||||
battery: Math.max(0, d.battery - dt * 0.05),
|
||||
|
||||
Reference in New Issue
Block a user