diff --git a/prototype/src/App.tsx b/prototype/src/App.tsx
index 0223f28..ff0dfaa 100644
--- a/prototype/src/App.tsx
+++ b/prototype/src/App.tsx
@@ -141,7 +141,7 @@ export default function App(): JSX.Element {
/>
{ob.moving && (
- mobile
+ transit
)}
@@ -186,7 +186,7 @@ export default function App(): JSX.Element {
blue line — pose broadcast (5 Hz, ~46 B)
green line — bulk sync (sealed partitions)
flash — broadcast event delivered
- red circle — mobile obstacle
+ red circle — transit object crossing the area
diff --git a/prototype/src/sim.ts b/prototype/src/sim.ts
index 287b94a..f57f815 100644
--- a/prototype/src/sim.ts
+++ b/prototype/src/sim.ts
@@ -65,12 +65,52 @@ function rnd(): number {
return ((seedState >>> 0) % 100000) / 100000;
}
-const WAYPOINTS: Vec[] = [
+// 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 },
+];
+function routeOf(d: Drone): Vec[] {
+ return d.id % 2 === 0 ? PERIMETER : CROSS;
+}
+
+// Transit objects enter at one edge, cross the whole area, and respawn at
+// a fresh edge once they leave — a stream of through-traffic instead of
+// obstacles bouncing until they wedge into a corner.
+const TRANSIT_MARGIN = 120;
+function spawnTransit(id: number): Obstacle {
+ const speed = 26 + rnd() * 30;
+ const along = 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 } },
+ ];
+ const { pos, vel } = table[edge];
+ return { id, pos, radius: 22 + rnd() * 16, moving: true, vel };
+}
+function outOfTransit(ob: Obstacle): boolean {
+ return (
+ ob.pos.x < -TRANSIT_MARGIN ||
+ ob.pos.x > AREA + TRANSIT_MARGIN ||
+ ob.pos.y < -TRANSIT_MARGIN ||
+ ob.pos.y > AREA + TRANSIT_MARGIN
+ );
+}
export function makeWorld(droneCount: number): World {
const drones: Drone[] = Array.from({ length: droneCount }, (_, i) => {
@@ -83,7 +123,7 @@ export function makeWorld(droneCount: number): World {
},
vel: { x: 0, y: 0 },
heading: angle,
- waypoint: i % WAYPOINTS.length,
+ waypoint: i % PERIMETER.length,
battery: 90 + rnd() * 10,
channel: CHANNELS[i % CHANNELS.length],
};
@@ -92,8 +132,9 @@ export function makeWorld(droneCount: number): World {
{ 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: 3, pos: { x: 200, y: 800 }, radius: 30, moving: true, vel: { x: 14, y: -6 } },
- { id: 4, pos: { x: 820, y: 620 }, radius: 26, moving: true, vel: { x: -10, y: 9 } },
+ spawnTransit(3),
+ spawnTransit(4),
+ spawnTransit(5),
];
return { t: 0, drones, obstacles, links: new Map(), totalBytes: 0, broadcasts: 0 };
}
@@ -103,12 +144,13 @@ function linkKey(a: number, b: number): string {
}
function steer(d: Drone, world: World, dt: number): Drone {
- const wp = WAYPOINTS[(d.waypoint + d.id) % WAYPOINTS.length];
+ const route = routeOf(d);
+ const wp = route[(d.waypoint + d.id) % route.length];
let ax = wp.x - d.pos.x;
let ay = wp.y - d.pos.y;
const wpDist = Math.hypot(ax, ay);
let waypoint = d.waypoint;
- if (wpDist < 90) waypoint = (d.waypoint + 1) % WAYPOINTS.length;
+ if (wpDist < 90) waypoint = (d.waypoint + 1) % route.length;
ax /= wpDist || 1;
ay /= wpDist || 1;
@@ -157,18 +199,15 @@ function steer(d: Drone, world: World, dt: number): Drone {
export function tick(world: World, dt: number): World {
const drones = world.drones.map((d) => steer(d, world, dt));
- const obstacles = world.obstacles.map((ob) =>
- ob.moving
- ? {
- ...ob,
- pos: { x: ob.pos.x + ob.vel.x * dt, y: ob.pos.y + ob.vel.y * dt },
- vel: {
- x: ob.pos.x < 60 || ob.pos.x > AREA - 60 ? -ob.vel.x : ob.vel.x,
- y: ob.pos.y < 60 || ob.pos.y > AREA - 60 ? -ob.vel.y : ob.vel.y,
- },
- }
- : ob,
- );
+ const obstacles = world.obstacles.map((ob) => {
+ if (!ob.moving) return ob;
+ const moved = {
+ ...ob,
+ pos: { x: ob.pos.x + ob.vel.x * dt, y: ob.pos.y + ob.vel.y * dt },
+ };
+ // Once a transit object leaves the area, respawn it at a random edge
+ return outOfTransit(moved) ? spawnTransit(ob.id) : moved;
+ });
const links = new Map(world.links);
let totalBytes = world.totalBytes;