Step 1: The HTML UI Overlay

You need the physical HTML for the dialog box. Open your main .astro file (or index.html) where your <canvas> lives.

Add this exact code directly below your <canvas> tag:

HTML

<canvas id="game-canvas" width="800" height="600"></canvas>

<div id="dialog-overlay" style="display: none; position: absolute; top: 0; left: 0; width: 800px; height: 600px; background: rgba(0,0,0,0.6); justify-content: center; align-items: center; z-index: 10;">
    
    <div style="background: #2b2b2b; border: 4px solid #d4af37; padding: 20px; width: 400px; text-align: center; color: #f2e8c9; font-family: 'Times New Roman', serif; box-shadow: 0px 0px 15px #000;">
        
        <p id="dialog-text" style="font-size: 18px; line-height: 1.5; margin-bottom: 25px;">
            Default Text
        </p>
        
        <button id="dialog-btn" style="background: #4a3b22; border: 2px solid #d4af37; color: #f2e8c9; padding: 10px 30px; font-size: 16px; cursor: pointer; font-family: 'Times New Roman', serif;">
            OK
        </button>
    </div>
</div>

Step 2: Create ui.js

Create a brand new file named ui.js. This will handle locking the game and injecting the custom text into the HTML.

Paste this exact code into ui.js:

JavaScript

// ui.js

export function showDialog(game, message, onConfirm) {
    const overlay = document.getElementById("dialog-overlay");
    const textEl = document.getElementById("dialog-text");
    const btn = document.getElementById("dialog-btn");

    // 1. Lock the game controls
    game.isUiOpen = true; 
    
    // 2. Clear any paths just to be safe
    game.activePath = []; 
    game.stagedPath = []; 

    // 3. Inject the unique message and show the box
    textEl.innerHTML = message;
    overlay.style.display = "flex";

    // 4. Handle the OK button click
    btn.onclick = () => {
        overlay.style.display = "none";
        game.isUiOpen = false; // Unlock the game
        
        if (onConfirm) {
            onConfirm(); 
        }
    };
}

Step 3: Update objects.js

We need to give your object factory a backdoor so you can pass custom messages to individual objects. Open objects.js and find your createMapObject function.

Replace your entire createMapObject function with this:

JavaScript

// NEW: Added customData = {} as the 4th parameter
export function createMapObject(type, gridX, gridY, customData = {}) {
    const blueprint = ObjectLibrary[type];
    
    return {
        type,
        blueprint,
        gridX,
        gridY,
        anchorPixelX: gridX * TILE_SIZE + TILE_SIZE / 2,
        anchorPixelY: gridY * TILE_SIZE + TILE_SIZE / 2,
        images: [],
        shadowImages: [],
        animFrame: 0,
        
        // NEW: Store the unique custom data inside the object instance
        customData: customData
    };
}

Step 4: Lock the Inputs (input.js)

We must prevent the player from clicking around or using the keyboard while the dialog box is open. Open input.js.

Add this single line to the very top of ALL THREE input functions:

JavaScript

export function onKeyDown(event, game, draw) {
    if (game.isUiOpen) return; // <-- ADD THIS LINE
    // ... rest of your code ...
}

export function onMouseDown(event, canvas, game, draw) {
    if (game.isUiOpen) return; // <-- ADD THIS LINE
    // ... rest of your code ...
}

export function onMouseMove(event, canvas, game) {
    if (game.isUiOpen) return; // <-- ADD THIS LINE
    // ... rest of your code ...
}

Step 5: Wire it together in main.js

This is the final step where the magic happens. We need to import the UI, set up the game state, and trigger the box when the hero arrives.

5a. At the very top of main.js, add the import:

JavaScript

import { showDialog } from './ui.js';

5b. Add isUiOpen to your game state: Find wherever you define your game object (likely a createGameState() function or just const game = { ... }). Add this line inside it:

JavaScript

    isUiOpen: false, // <-- Add this inside your game state object

5c. Add the Object Lookup Helper: Paste this function anywhere in main.js (outside of your other functions). It securely looks up which object owns a specific interaction tile.

JavaScript

function getInteractiveObjectAt(game, col, row) {
    for (const obj of game.mapObjects) {
        if (obj.blueprint.interaction) {
            const ix = obj.gridX + obj.blueprint.interaction.x;
            const iy = obj.gridY + obj.blueprint.interaction.y;
            
            if (ix === col && iy === row) {
                return obj; // Found the exact object!
            }
        }
    }
    return null;
}

5d. The Movement Trigger: Find the part of your code where the hero is moving and “snaps” to the target tile (usually an if (distance <= game.hero.speed) block inside your update or gameLoop function).

Replace that specific block with this exact code:

JavaScript

    if (distance <= game.hero.speed) {
        // Snap perfectly to the center
        game.hero.pixelX = targetPixelX;
        game.hero.pixelY = targetPixelY;
        
        // Update our logical grid position
        game.hero.cellX = targetCell.x;
        game.hero.cellY = targetCell.y;

        // Eat the breadcrumb
        game.activePath.shift(); 

        // --- NEW: THE INTERACTION TRIGGER ---
        // Check if the tile we just arrived at is marked as interactive in the matrix
        const cellState = game.passabilityMatrix[game.hero.cellY]?.[game.hero.cellX];
        
        if (cellState === "interactive") {
            // Figure out WHICH object we just stepped on
            const activeObject = getInteractiveObjectAt(game, game.hero.cellX, game.hero.cellY);
            
            if (activeObject) {
                // Get the unique custom message, or use a default one
                const message = activeObject.customData.message || `You have visited a ${activeObject.type}.`;
                
                // Show the HoMM3 Dialog Box!
                showDialog(game, message, () => {
                    // This runs when the user clicks "OK"
                    console.log(`Finished interacting with ${activeObject.type}`);
                });
            }
        }
        
        // Reset the cursor if that was the last step
        if (game.activePath.length === 0) {
            canvas.classList.remove("cursor-blocked");
            canvas.classList.add("cursor-walkable");
        }
    }

Step 6: Test it out!

Go to where you manually push objects into your map in main.js, and pass a custom message to one of them:

JavaScript

game.mapObjects.push(
    createMapObject("mountain_1", 5, 5, { 
        message: "You discovered a hidden cache of gold in the rocks!" 
    })
);

Click the green interaction tile of that mountain. Your hero will walk there using your A* pathfinder. The exact moment they stop on the tile, the game will freeze, and the HoMM3 dialog box will pop up with your unique message!