Phase 17: map & fog VTT overhaul + player-facing projection
- Pure geometry library src/lib/map/* (grid, distance 5e/pf2e/euclid, AoE circle/cone/line/square, polygon rasterize + point-in-polygon, brush, fog set algebra, player projection) with unit tests. - Schema: map tokens gain size/kind/characterId/hp/conditions/gmOnly; maps gain drawings/gridUnit(feet)/gridType/fogVersion + point/drawing sub-schemas. Dexie v8 additive migration; mapsRepo.get + transactional mutate. - Editor split into a shared MapCanvas renderer + MapEditor tool state machine: fog via brush/rectangle/polygon (+ size, reveal-all/hide-all), measurement (system-aware distance in feet), AoE templates (circle/cone/line/square preview), drawing annotations (freehand/line/arrow/rect/circle/text, GM-only), pings, and richer tokens (NxN size, character link with live HP ring, condition dots, edit popover). - Player-facing projection: toPlayerProjection strips GM-only content; PlayerMapView renders it read-only with opaque fog; uiStore.activeMapId + 'Show to players'; the player view now shows the live battle map. enemyStatus extracted to src/lib/combat/playerProjection.ts (reused, and for Phase 18). 9 geometry unit tests; maps e2e covers upload→token→reveal-all→player projection. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,31 +1,29 @@
|
||||
import type { Campaign, Combatant } from '@/lib/schemas';
|
||||
import type { Campaign } from '@/lib/schemas';
|
||||
import { getSystem } from '@/lib/rules';
|
||||
import { localSync } from '@/lib/sync';
|
||||
import { enemyStatus } from '@/lib/combat/playerProjection';
|
||||
import { toPlayerProjection } from '@/lib/map';
|
||||
import { useUiStore } from '@/stores/uiStore';
|
||||
import { useCharacters } from '@/features/characters/hooks';
|
||||
import { useEncounters } from '@/features/combat/hooks';
|
||||
import { useCalendar } from '@/features/world/hooks';
|
||||
import { useCalendar, useMaps } from '@/features/world/hooks';
|
||||
import { Page, EmptyState, RequireCampaign } from '@/components/ui/Page';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { cn } from '@/lib/cn';
|
||||
import { PlayerMapView } from './PlayerMapView';
|
||||
|
||||
export function PlayerViewPage() {
|
||||
return <RequireCampaign>{(c) => <PlayerView campaign={c} />}</RequireCampaign>;
|
||||
}
|
||||
|
||||
/** Hide exact enemy HP from players — show a status band instead. */
|
||||
function enemyStatus(c: Combatant): { label: string; cls: string } {
|
||||
if (c.hp.current <= 0) return { label: 'Down', cls: 'text-danger' };
|
||||
const frac = c.hp.max > 0 ? c.hp.current / c.hp.max : 1;
|
||||
if (frac <= 0.5) return { label: 'Bloodied', cls: 'text-warning' };
|
||||
return { label: 'Healthy', cls: 'text-success' };
|
||||
}
|
||||
|
||||
function PlayerView({ campaign }: { campaign: Campaign }) {
|
||||
const characters = useCharacters(campaign.id);
|
||||
const encounters = useEncounters(campaign.id);
|
||||
const calendar = useCalendar(campaign.id);
|
||||
const maps = useMaps(campaign.id);
|
||||
const activeEncounterId = useUiStore((s) => s.activeEncounterId);
|
||||
const activeMapId = useUiStore((s) => s.activeMapId);
|
||||
const activeMap = maps.find((mp) => mp.id === activeMapId) ?? null;
|
||||
const sys = getSystem(campaign.system);
|
||||
|
||||
const pcs = characters.filter((c) => c.kind === 'pc');
|
||||
@@ -48,6 +46,13 @@ function PlayerView({ campaign }: { campaign: Campaign }) {
|
||||
<Button variant="secondary" onClick={enterFullscreen} className="print:hidden">⛶ Fullscreen</Button>
|
||||
</div>
|
||||
|
||||
{activeMap && (
|
||||
<section className="mb-6">
|
||||
<h2 className="mb-2 text-sm font-semibold uppercase tracking-wide text-muted">Battle map</h2>
|
||||
<PlayerMapView map={toPlayerProjection(activeMap)} image={activeMap.image} maxWidth={1100} />
|
||||
</section>
|
||||
)}
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
{/* Party */}
|
||||
<section>
|
||||
|
||||
Reference in New Issue
Block a user