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!