Why a Facing Arc?

stonedtrollstonedtroll
4 min read

When a token turns, it’s helpful—especially in tactical or role-playing scenarios—to see exactly which direction it’s “looking.” A subtle 20º arc projected in front of the token can communicate much more clearly than a bare rotation value.

But how do I hook into Foundry’s event system, check permissions, and still keep my code clean and loosely coupled?

FoundryHookAdapter (Infrastructure Layer)

Detects raw Foundry events and converts them to domain events. By hiding Foundry specifics here, the rest of the app remains blissfully ignorant of Foundry’s peculiarities.

💡
Bear in mind that when Foundry fires the updateToken hook, the Token’s document hasn’t yet been fully updated. If you read document.rotation at that moment, you may still get the old value. Always rely first on the changedState.rotation from the hook payload—and only fall back to document.rotation as a default if no change was provided.
// When a token is updated in Foundry:
handleTokenUpdate() {
  // Captures the update from Foundry's updateToken hook
  // Creates a TokenUpdateEvent with rotation data
  eventBus.emit('token:update', {
    tokenId: document.id,
    currentState: { rotation: 45, x: 100, y: 100, ... },
    changedState: { rotation: 90 },  // New rotation
    userId: 'abc123'
  });
}

TokenRotationCoordinator (Application Layer)

Listens for token updates and coordinates overlay rendering when rotation changes.

  1. Detect rotation change

  2. Fetch overlay types that respond to token rotation

  3. Check who’s allowed to see them

  4. Build a render context (dimensions, angles, positions)

  5. Fire off render requests

// Subscribed to 'token:update' events
handleTokenUpdate(event: TokenUpdateEvent) {
  // Check if rotation changed
  if (event.changedState.rotation !== undefined) {
    // Get overlays that respond to rotation (like facing-arc)
    const rotationOverlays = overlayRegistry.getTypesForEvent('tokenRotate');

    // Check permissions - can this user see overlays on this token?
    const viewableOverlays = await permissionCoordinator.getViewableOverlaysForToken();

    // Build render context with rotation data
    const context: OverlayRenderContext = {
      rotation: {
        currentRotation: 45,
        newRotation: 90,
        tokenXCoordinate: 100,
        tokenYCoordinate: 100,
        tokenWidth: 50,
        tokenHeight: 50
      }
    };

OverlayRenderingService (Presentation Layer)

Manages the PixiJS graphics containers and coordinates rendering.

  • Retrieves the overlay definition (FacingArcDefinition)

  • Instantiates—or reuses—a PIXI.Graphics object

  • Positions it at the token’s centre

  • Hands off drawing to the renderer

renderTokenOverlay(overlayTypeId: 'facing-arc', tokenId: string, context: OverlayRenderContext) {
  // Get the overlay definition from registry
  const overlayDef = overlayRegistry.get('facing-arc'); // Returns FacingArcDefinition

  // Get the renderer from the definition
  const renderer = overlayDef.getRenderer(); // Returns FacingArcRenderer instance

  // Get or create PIXI graphics object for this token+overlay
  const graphics = getOrCreateGraphicsInstance(`${tokenId}-facing-arc`);

  // Position graphics at token centre
  const centerX = context.rotation.tokenXCoordinate + context.rotation.tokenWidth / 2;
  const centerY = context.rotation.tokenYCoordinate + context.rotation.tokenHeight / 2;
  graphics.position.set(centerX, centerY);

  // Delegate actual drawing to the renderer
  renderer.render(graphics, context);
}

FacingArcDefinition (Infrastructure Layer)

Defines the facing arc overlay configuration.

export const FacingArcDefinition: OverlayDefinition = {
  id: 'facing-arc',
  renderTriggers: ['tokenRotate', 'tokenMove'],
};

Because it lives in a registry, adding new overlays later (boundary, aura, custom shapes…) is just a matter of dropping in another definition.

FacingArcRenderer (Presentation Layer)

Actually draws the facing arc using PixiJS.

render(graphics: PIXI.Graphics, context: OverlayRenderContext) {
  // Clear previous drawing
  graphics.clear();

  // Get rotation from context
  const rotation = context.rotation?.currentRotation ?? 0;
  const radius = Math.max(context.rotation.tokenWidth, context.rotation.tokenHeight) / 2;

  // Calculate arc angles (20 degree arc)
  const arcAngle = 20 * Math.PI / 180;
  const facingAngle = ((rotation - 90) * (Math.PI / 180));
  const startAngle = facingAngle - arcAngle / 2;
  const endAngle = facingAngle + arcAngle / 2;

  // Draw the arc line
  graphics.lineStyle({
    width: 8,
    color: 0x8A6A1C,
    alpha: 0.8,
    cap: PIXI.LINE_CAP.ROUND
  });

  graphics.arc(0, 0, radius, startAngle, endAngle, false);
}

Complete Flow Sequence

  1. User rotates token in Foundry UI

  2. Foundry fires updateToken hook

  3. FoundryHookAdapter catches hook, emits 'token:update' event

  4. TokenRotationCoordinator receives event, checks if rotation changed

  5. Coordinator queries OverlayRegistry for overlays with 'tokenRotate' trigger

  6. Coordinator checks permissions via OverlayPermissionCoordinator

  7. Coordinator builds render context with rotation data

  8. Coordinator calls overlayRenderingService.renderTokenOverlay()

  9. OverlayRenderingService gets FacingArcDefinition from registry

  10. Service calls definition.getRenderer() to get FacingArcRenderer

  11. Service creates/positions PIXI Graphics object

  12. Service calls renderer.render(graphics, context)

  13. FacingArcRenderer draws the arc at the new rotation angle

  14. Facing arc appears on screen at token’s new rotation

Key Design Patterns

  • Event-Driven: Loose coupling via an EventBus

  • Clean Architecture: Clear separation between infrastructure, application, and presentation layers

  • Strategy Pattern: Different renderers implement a common interface for each overlay type

  • Registry Pattern: Dynamic registration and lookup of overlay definitions

  • Permission System: Fine-grained control over who can see each overlay

This modular architecture makes it trivial to extend—simply drop in a new overlay definition and renderer, and the core flow remains untouched. Happy coding!

0
Subscribe to my newsletter

Read articles from stonedtroll directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

stonedtroll
stonedtroll