Phase 4: dynamic vision, walls & doors

- src/lib/map/vision.ts (pure, tested): blockingSegments (walls + closed doors),
  segment intersection, computeVisibleCells (raycast viewer→cell-centre, sight radius).
- BattleMap += dynamicVision + sightRadiusFeet (Dexie v12, backfilled).
- MapEditor: new "Walls" tool (draw wall polylines; click a door to open/close),
  a "Vision" toggle (auto-enables fog and reveals from the party), sight-radius field,
  and "Reveal from party". Moving a PC token accumulates revealed cells via LoS when
  Vision is on. Doors/walls render for the GM (amber=closed, green=open).
- Networked players need no new protocol: revealed cells already travel in the snapshot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 15:02:49 +02:00
parent 36f0ea66b3
commit f61fabadb5
8 changed files with 226 additions and 6 deletions
+18
View File
@@ -53,6 +53,24 @@ test('maps: import a Universal VTT (.dd2vtt) map', async ({ page }) => {
await expect(page.getByRole('button', { name: 'Export .uvtt' })).toBeVisible();
});
test('maps: walls tool + dynamic vision controls', async ({ page }) => {
await page.getByRole('button', { name: '+ New campaign' }).first().click();
await page.locator('input[data-autofocus]').fill('Vision');
await page.getByRole('button', { name: 'Create' }).click();
await page.getByRole('link', { name: 'Dashboard' }).click();
await page.getByRole('link', { name: 'Maps' }).click();
await page.locator('input[type="file"]').setInputFiles('e2e/fixtures/sample.dd2vtt');
await expect(page.getByTestId('map-viewport')).toBeVisible();
await page.getByRole('button', { name: 'walls', exact: true }).click();
await expect(page.getByRole('button', { name: /Reveal from party/ })).toBeVisible();
await expect(page.getByRole('button', { name: /Clear walls/ })).toBeVisible();
const vision = page.locator('label', { hasText: 'Vision' }).locator('input[type=checkbox]');
await vision.check();
await expect(vision).toBeChecked();
});
test('maps: zoom controls scale the canvas and fit resets', async ({ page }) => {
await page.getByRole('button', { name: '+ New campaign' }).first().click();
await page.locator('input[data-autofocus]').fill('Zoom');