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:
Entities – Mutable objects with identity
Value Objects – Immutable domain concepts
Interfaces – Contracts for infrastructure and external services
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
Entity | Value Object | Interface | Type | |
What it represents | A thing with identity | A concept with value | A contract for a service | A data structure |
Has identity? | O | X | X | X |
Mutable? | O | X | X | X |
Behaviour? | Rich methods | Domain-specific | Method signatures only | None |
Example | MoveableToken | Vector2 , Distance | LineOfSightChecker | SceneConfig , 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.
Subscribe to my newsletter
Read articles from stonedtroll directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
