Skip to main content

Overlay Visibility & Domain Design: How I Keep It Clean and Playable

Updated 4 min readMarkdown
Part of seriesMaleficar Manoeuvres
Overlay Visibility & Domain Design: How I Keep It Clean and Playable

When I think about overlays in a tabletop-style virtual environment—like range rings, blocked-status indicators, and vision cones—visibility becomes crucial. I want to make sure what players see on the screen reflects what their characters could logically perceive in the game world.

Overlay Visibility – Requirements

For Players

As a player, I only want overlays to show up when they make sense—meaning, I should only see an overlay if my character would be able to see the token it’s attached to. This helps preserve immersion and makes decision-making more intuitive.

If I have a token selected, I want its overlays to show—of course. But for overlays on other tokens, I only want to see them if:

  • The other token is visible on the canvas

  • It’s not hidden in the scene configuration

  • It lies within the selected token’s vision polygon

  • There are no walls or barriers blocking line of sight

When no token is selected, I want the system to fall back on visibility from all tokens I control—so overlays still show accurately based on what my characters could see collectively.

For GMs

As a GM, it’s a different story. I want to see everything—all overlays, all the time. Full visibility means I can keep an eye on the battlefield, check for inconsistencies, and help players when needed.

Domain Layer – My Approach

Now, to support this behaviour in code, I’ve been thinking carefully about how to model the domain. Here’s how I’ve structured things.

I use four main layers in my domain model:

  1. Entities – Mutable objects with identity

  2. Value Objects – Immutable domain concepts

  3. Interfaces – Contracts for infrastructure and external services

  4. Types – Lightweight, structural definitions (like enums, DTOs, configs)

Entities – Things That Change and Persist

Entities are objects that have a unique ID and mutable state. For example:

export class MoveableToken {
    constructor(
        public readonly id: string,
        private position: Vector2,
        private rotation: number,
        private collisionShape: CollisionShape
    ) {}

    move(newPosition: Vector2): void {
        this.position = newPosition;
    }

    rotate(newRotation: number): void {
        this.rotation = newRotation;
    }
}

What makes this an entity:

  • It has identity (id)

  • Its state can change (position, rotation)

  • I consider two entities the same if they share the same ID—not necessarily the same data

Value Objects – Immutable by Design

Value objects are all about immutability and equality by value:

export class Vector2 {
    constructor(
        public readonly x: number,
        public readonly y: number
    ) {}

    add(other: Vector2): Vector2 {
        return new Vector2(this.x + other.x, this.y + other.y);
    }

    equals(other: Vector2): boolean {
        return this.x === other.x && this.y === other.y;
    }
}

I reach for value objects when I want:

  • Domain expressiveness

  • Predictability

  • Thread safety

  • Easy equality comparison

Interfaces – Contracts for Flexibility

Interfaces help me define what needs to be done, not how.

export interface LineOfSightChecker {
    isBlocked(origin: Vector2, destination: Vector2): boolean;
}

export interface TokenOwnershipProvider {
    getOwnedTokenIds(userId: string): string[];
}

This makes it easy for me to swap implementations, mock them during tests, or inject platform-specific logic without tying it to my domain.

Types – Pure Data

I use types and enums to represent pure data without behaviour:

export enum GridType {
    Gridless = 0,
    Square = 1,
    Hex = 2
}

export interface SceneConfig {
    gridType: GridType;
    gridSize: number;
    useElevation: boolean;
    sceneCeiling: number;
    enable3DPathfinding: boolean;
    maxStepHeight: number;
}

Types are lightweight and ideal for:

  • Configs

  • DTOs

  • Enums and constraints

  • Representing shape without logic

Choosing the Right Tool for the Job

Purpose

EntityValue ObjectInterfaceType
What it representsA thing with identityA concept with valueA contract for a serviceA data structure
Has identity?OXXX
Mutable?OXXX
Behaviour?Rich methodsDomain-specificMethod signatures onlyNone
ExampleMoveableTokenVector2, DistanceLineOfSightCheckerSceneConfig, GridType

Why This Structure Works for Me

This approach keeps my codebase:

  • Testable – Logic lives in pure functions and classes

  • Modular – Interfaces separate infrastructure from domain

  • Extensible – Easy to extend or replace parts of the system

  • Readable – Clear boundaries make it easier to reason about

By thinking in terms of entities, value objects, interfaces, and types, I avoid the dreaded "everything is just a class" pitfall. It also helps me align gameplay logic (like overlay visibility) with clean architectural decisions.

I think this is a strong foundation for building features that are both fun to use and maintainable to develop. If you’ve got similar patterns—or different ones—I’d love to hear about them.

No comments yet. Be the first to comment.