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

stonedtrollstonedtroll
4 min read

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.

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