PekAblo - Move Mechanics Problems

in LeoFinance11 days ago

If anyone can help me out, and point me in the right direction please. I can make the character move with the keyboard, but that doesn't animate. Mouse Clicks do nothing, if you know and can help, let me know.

gaulent.js

https://github.com/PaulMoon410/pekablo

Play the errors here

https://geocities.ws/peakecoin/games/pekablo/


(function() {
// --- ARROW KEY & MOBILE CONTROL ---
function handleMoveKey(key) {
    if (game.state !== 'playing') return;
    let dx = 0, dy = 0;
    const step = s * 0.5;
    if (key === 'ArrowUp' || key === 'w') dy = -step;
    else if (key === 'ArrowDown' || key === 's') dy = step;
    else if (key === 'ArrowLeft' || key === 'a') dx = -step;
    else if (key === 'ArrowRight' || key === 'd') dx = step;
    else return;
    let nx = hero.x + dx;
    let ny = hero.y + dy;
    if (!isWayWall(nx, ny)) {
        hero.x = nx;
        hero.y = ny;
        hero.to_x = nx;
        hero.to_y = ny;
        if (typeof hero.setState === 'function' && hero.run) hero.setState(hero.run);
        if (typeof hero.currentState === 'object' && typeof hero.step === 'number' && hero.currentState.steps) {
            hero.step = (hero.step + 1) % hero.currentState.steps;
            hero.sprite = hero.currentState;
        }
    } else {
        if (typeof hero.setState === 'function' && hero.stay) hero.setState(hero.stay);
    }
}

function handleAttackKey() {
    if (game.state !== 'playing') return;
    if (typeof hero.tryAttack === 'function') hero.tryAttack();
}

document.addEventListener('keydown', function(e) {
    // Prevent default scrolling on mobile for arrow keys and space
    if (["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"," ","w","a","s","d"].includes(e.key)) {
        e.preventDefault && e.preventDefault();
    }
    if (["ArrowUp","ArrowDown","ArrowLeft","ArrowRight","w","a","s","d"].includes(e.key)) {
        handleMoveKey(e.key);
    } else if (e.key === ' ') {
        handleAttackKey();
    }
});

// Support for on-screen controls via custom events
window.handleMoveKey = handleMoveKey;
window.handleAttackKey = handleAttackKey;

var imageCount = 0;
function loadImage(url, angles, steps, offsetX) {
    imageCount++;
    var image = new Image();
    image.onload = function() {
        imageCount--;
        image.offsetX = offsetX ? ((image.height / angles) >> 2) : 0;
    };
    image.src = url;
    if (typeof angles !== "undefined" && typeof steps !== "undefined") {
        image.angles = angles;
        image.steps = steps;
    }
    return image;
}

function load(img, callback) {
    if (img.complete) callback();
    else img.addEventListener("load", callback, false);
}

var level = {
    floor: {
        prefix: "dttool/output/1/",
        map: [
            // 30x30 large level, mostly open with some obstacles
            ...Array(30).fill().map((_, y) => Array(30).fill(756)),
        ],
        header: {
            372: false,
            660: false,
            756: false,
            1140: false,
            1908: false
        }
    },
    wall: {
        prefix: "dttool/output/0/",
        map: [
            // 30x30, border walls and some internal obstacles
            ...Array(30).fill().map((_, y) => Array(30).fill(0)),
        ],
        header: {
            276: {orientation: 8, main_index: 5, sub_index: 2, direction: 1, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0]},
            372: {orientation: 2, main_index: 5, sub_index: 0, direction: 2, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1]},
            468: {orientation: 1, main_index: 5, sub_index: 0, direction: 1, walk: [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0]},
            564: {orientation: 2, main_index: 5, sub_index: 0, direction: 2, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1]},
            660: {orientation: 2, main_index: 5, sub_index: 0, direction: 2, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1]},
            756: {orientation: 1, main_index: 5, sub_index: 0, direction: 1, walk: [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0]},
            852: {orientation: 1, main_index: 5, sub_index: 0, direction: 1, walk: [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0]},
            948: {orientation: 3, main_index: 5, sub_index: 0, direction: 3, walk: [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,1,1,1,1]},
            1044: {orientation: 4, main_index: 5, sub_index: 0, direction: 3, walk: [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,1,1,1,1]},
            1140: {orientation: 7, main_index: 5, sub_index: 0, direction: 4, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,1,0,0,0]},
            1236: {orientation: 9, main_index: 5, sub_index: 0, direction: 2, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1]},
            1332: {orientation: 9, main_index: 5, sub_index: 1, direction: 2, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0]},
            1428: {orientation: 8, main_index: 5, sub_index: 0, direction: 1, walk: [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},
            1524: {orientation: 8, main_index: 5, sub_index: 1, direction: 1, walk: [0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0]},
            1620: {orientation: 9, main_index: 5, sub_index: 0, direction: 2, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1]},
            1716: {orientation: 9, main_index: 5, sub_index: 1, direction: 2, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0]},
            1812: {orientation: 8, main_index: 5, sub_index: 1, direction: 1, walk: [0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0]},
            1908: {orientation: 8, main_index: 5, sub_index: 0, direction: 1, walk: [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},
            2004: {orientation: 6, main_index: 5, sub_index: 0, direction: 2, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,1,1,1,1]},
            2100: {orientation: 5, main_index: 5, sub_index: 0, direction: 1, walk: [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,1,0,0,0,1,1,0,0,0]},
            2196: {orientation: 6, main_index: 5, sub_index: 0, direction: 2, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,1,1,1,1]},
            2292: {orientation: 5, main_index: 5, sub_index: 0, direction: 1, walk: [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,1,0,0,0,1,1,0,0,0]},
            2388: {orientation: 2, main_index: 5, sub_index: 0, direction: 2, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1]},
            2484: {orientation: 1, main_index: 5, sub_index: 0, direction: 1, walk: [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0]},
            2580: {orientation: 3, main_index: 5, sub_index: 0, direction: 3, walk: [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,1,1,1,1]},
            2676: {orientation: 4, main_index: 5, sub_index: 0, direction: 3, walk: [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,1,1,1,1]},
            2772: {orientation: 12, main_index: 5, sub_index: 0, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,1,0,0,0]}
        }
    },
    object: {
        prefix: "dttool/output/2/",
        map: [
            // 30x30, sparse objects
            ...Array(30).fill().map(() => Array(30).fill(0)),
        ],
        header: {
            276: {orientation: 2, main_index: 9, sub_index: 12, direction: 2, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,1,1,0,0,0]},
            372: {orientation: 2, main_index: 9, sub_index: 11, direction: 2, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1]},
            468: {orientation: 12, main_index: 50, sub_index: 0, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,1,0,0,0,0,0,0,0]},
            564: {orientation: 12, main_index: 9, sub_index: 33, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,1,1,1,0,1,1,1,1,0]},
            660: {orientation: 1, main_index: 9, sub_index: 33, direction: 1, walk: [1,1,1,1,0,1,1,1,1,0,1,1,1,1,0,1,1,1,1,0,1,1,1,1,0]},
            756: {orientation: 7, main_index: 9, sub_index: 33, direction: 4, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,1,1,1,1,0]},
            852: {orientation: 12, main_index: 9, sub_index: 32, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0]},
            948: {orientation: 12, main_index: 9, sub_index: 31, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,1,1,1,0,0]},
            1044: {orientation: 1, main_index: 9, sub_index: 10, direction: 1, walk: [0,0,0,0,0,1,1,0,0,0,1,1,0,0,0,1,1,0,0,0,1,1,0,0,0]},
            1140: {orientation: 1, main_index: 9, sub_index: 9, direction: 1, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,1,0,0,0]},
            1236: {orientation: 1, main_index: 9, sub_index: 8, direction: 1, walk: [1,1,0,0,0,1,1,0,0,0,1,1,0,0,0,1,1,0,0,0,1,1,0,0,0]},
            1332: {orientation: 12, main_index: 9, sub_index: 11, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0]},
            1428: {orientation: 12, main_index: 9, sub_index: 10, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0]},
            1524: {orientation: 12, main_index: 9, sub_index: 9, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},
            1620: {orientation: 12, main_index: 9, sub_index: 8, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},
            1716: {orientation: 12, main_index: 9, sub_index: 7, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0]},
            1812: {orientation: 12, main_index: 9, sub_index: 6, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0]},
            1908: {orientation: 12, main_index: 9, sub_index: 5, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0]},
            2004: {orientation: 12, main_index: 9, sub_index: 4, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0]},
            2100: {orientation: 12, main_index: 9, sub_index: 3, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0]},
            2196: {orientation: 12, main_index: 9, sub_index: 2, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0]},
            2292: {orientation: 12, main_index: 9, sub_index: 1, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0]},
            2388: {orientation: 12, main_index: 9, sub_index: 0, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0]},
            2484: {orientation: 2, main_index: 9, sub_index: 10, direction: 2, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},
            2580: {orientation: 2, main_index: 9, sub_index: 9, direction: 2, walk: [0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1]},
            2676: {orientation: 2, main_index: 9, sub_index: 8, direction: 2, walk: [0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]},
            2772: {orientation: 2, main_index: 9, sub_index: 7, direction: 2, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0]},
            2868: {orientation: 2, main_index: 9, sub_index: 6, direction: 2, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0]},
            2964: {orientation: 1, main_index: 9, sub_index: 7, direction: 1, walk: [0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0]},
            3060: {orientation: 1, main_index: 9, sub_index: 6, direction: 1, walk: [0,0,0,0,0,1,1,0,0,0,1,1,0,0,0,1,1,0,0,0,1,1,0,0,0]},
            3156: {orientation: 2, main_index: 9, sub_index: 5, direction: 2, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0]},
            3252: {orientation: 2, main_index: 9, sub_index: 4, direction: 2, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,1,1,1,1,0]},
            3348: {orientation: 1, main_index: 9, sub_index: 5, direction: 1, walk: [0,0,0,0,0,1,1,0,0,0,1,1,0,0,0,1,1,0,0,0,1,1,0,0,0]},
            3444: {orientation: 1, main_index: 9, sub_index: 4, direction: 1, walk: [0,0,0,0,0,1,1,0,0,0,1,1,0,0,0,1,1,0,0,0,1,1,0,0,0]},
            3540: {orientation: 12, main_index: 9, sub_index: 30, direction: 3, walk: [0,0,0,0,0,0,0,1,0,0,0,1,1,0,0,0,1,1,0,0,0,0,0,0,0]},
            3636: {orientation: 12, main_index: 9, sub_index: 29, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,1,1,1,0,0,0,0,0,0]},
            3732: {orientation: 1, main_index: 9, sub_index: 3, direction: 1, walk: [0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,1,0,0,0,1,1,0,0,0]},
            3828: {orientation: 1, main_index: 9, sub_index: 2, direction: 1, walk: [1,1,0,0,0,1,1,0,0,0,1,1,0,0,0,1,1,0,0,0,1,1,0,0,0]},
            3924: {orientation: 2, main_index: 9, sub_index: 3, direction: 2, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,1,1,1,1,1]},
            4020: {orientation: 2, main_index: 9, sub_index: 2, direction: 2, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1]},
            4116: {orientation: 2, main_index: 9, sub_index: 1, direction: 2, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1]},
            4212: {orientation: 2, main_index: 9, sub_index: 0, direction: 2, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1]},
            4308: {orientation: 1, main_index: 9, sub_index: 1, direction: 1, walk: [0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,1,0,0,0,1,1,0,0,0]},
            4404: {orientation: 1, main_index: 9, sub_index: 0, direction: 1, walk: [1,1,0,0,0,1,1,0,0,0,1,1,0,0,0,1,1,0,0,0,1,1,0,0,0]},
            4500: {orientation: 12, main_index: 9, sub_index: 28, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0]},
            4596: {orientation: 12, main_index: 9, sub_index: 27, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0]},
            4692: {orientation: 12, main_index: 9, sub_index: 24, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0]},
            4788: {orientation: 12, main_index: 9, sub_index: 23, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0]},
            4884: {orientation: 12, main_index: 9, sub_index: 22, direction: 3, walk: [0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0]},
            4980: {orientation: 12, main_index: 9, sub_index: 21, direction: 3, walk: [0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0]},
            5076: {orientation: 12, main_index: 9, sub_index: 20, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0]},
            5172: {orientation: 12, main_index: 9, sub_index: 17, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,1,0,0,0,1,1,0,0,0]},
            5268: {orientation: 12, main_index: 9, sub_index: 18, direction: 3, walk: [0,0,0,0,0,1,1,0,0,0,1,1,0,0,0,1,1,0,0,0,1,1,0,0,0]},
            5364: {orientation: 12, main_index: 9, sub_index: 19, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,1,1,1,0,0]},
            5460: {orientation: 12, main_index: 9, sub_index: 16, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,1,0,0,0,1,1,0,0,0]},
            5556: {orientation: 12, main_index: 9, sub_index: 15, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0]},
            5652: {orientation: 12, main_index: 9, sub_index: 13, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,1,0]},
            5748: {orientation: 12, main_index: 9, sub_index: 12, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0]},
            5844: {orientation: 12, main_index: 9, sub_index: 14, direction: 3, walk: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,1,1,1,1,0]}
        }
    }
};

for (var layer in level) {
    level[layer].tiles = {};
    for (var tileId in level[layer].header) {
        if (!level[layer].tiles[tileId]) {
            level[layer].tiles[tileId] = loadImage(level[layer].prefix + tileId + ".png");
        }
    }
}

var floor = document.getElementById("floor").getContext("2d");
floor.w = floor.canvas.width;
floor.h = floor.canvas.height;
var tw = 160;
var th = tw / 2;
var s = tw * 0.705;
var a = Math.PI / 4;
var visible = 12; // show more tiles on large map
var asin = Math.sin(a);
var acos = Math.sin(a);

var barrelSprite = loadImage("sprite/barrel64.png");
var coinSprite = loadImage("sprite/coins10.png");
var potionSprite = loadImage("sprite/potions.png");
var gateSprite = loadImage("sprite/house.png");

var monsterMap = {
    SK: {
        A1: loadImage("monsters/SK/A1/map.png", 8, 16, true),
        NU: loadImage("monsters/SK/NU/map.png", 8, 8, true),
        WL: loadImage("monsters/SK/WL/map.png", 8, 8, true),
        DD: loadImage("monsters/SK/DD/map.png", 8, 1),
        attackOffset: 10
    },
    FS: {
        A1: loadImage("monsters/FS/A1/map.png", 8, 17, true),
        NU: loadImage("monsters/FS/NU/map.png", 8, 12, true),
        WL: loadImage("monsters/FS/WL/map.png", 8, 14, true),
        DD: loadImage("monsters/FS/DD/map.png", 8, 1)
    },
    SI: {
        A1: loadImage("monsters/SI/A1/map.png", 8, 16, true),
        NU: loadImage("monsters/SI/NU/map.png", 8, 8, true),
        WL: loadImage("monsters/SI/WL/map.png", 8, 9, true),
        DD: loadImage("monsters/SI/DD/map.png", 8, 1)
    },
    BA: {
        A1: loadImage("monsters/BA/A1/map.png", 16, 9, true),
        NU: loadImage("monsters/BA/NU/map.png", 16, 8, true),
        WL: loadImage("monsters/BA/WL/map.png", 16, 8, true)
    }
};

var STAGES = [
    {
        name: "The Gatehouse",
        intro: "Clear the scavengers haunting the gatehouse.",
        monsters: [
            {type: "SK", count: 3, health: 700, damage: 22},
            {type: "FS", count: 2, health: 900, damage: 28}
        ],
        potionCount: 2,
        barrelCount: 3,
        healOnStart: 0,
        gateLabel: "Descend"
    },
    {
        name: "The Bone Hall",
        intro: "A deeper chamber wakes. Survive the second trial.",
        monsters: [
            {type: "SK", count: 3, health: 900, damage: 28},
            {type: "FS", count: 3, health: 1200, damage: 32},
            {type: "SI", count: 2, health: 1000, damage: 36}
        ],
        potionCount: 2,
        barrelCount: 4,
        healOnStart: 250,
        gateLabel: "Challenge the Warden"
    },
    {
        name: "Warden's Ring",
        intro: "Defeat the crypt warden and claim the relic.",
        monsters: [
            {type: "SI", count: 2, health: 1200, damage: 38},
            {type: "FS", count: 2, health: 1300, damage: 40},
            {type: "SK", count: 1, health: 1600, damage: 45, title: "Bone Captain"},
            {type: "SI", count: 1, health: 3200, damage: 65, resistance: 180, speed: 10, title: "Crypt Warden", lootCoins: 900}
        ],
        potionCount: 3,
        barrelCount: 4,
        healOnStart: 300,
        gateLabel: "Claim Victory"
    }
];

var monsters = [];
var deathmobs = [];
var barrels = [];
var coins = [];
var potions = [];
var walls = [];
var interactives = [];
var stageKills = 0;

var game = {
    state: "loading",
    stageIndex: -1,
    message: "Loading assets...",
    messageUntil: 0,
    showMap: false,
    questText: "",
    gate: null
};

function now() {
    return new Date().getTime();
}

// DEBUG: Draw walkability overlay
function drawWalkabilityOverlay(ctx) {
    if (!window.hero) return;
    ctx.save();
    for (let y = 0; y < level.floor.map.length; y++) {
        for (let x = 0; x < level.floor.map[0].length; x++) {
            let wx = x * s;
            let wy = y * s;
            if (isWayWall(wx + s/2, wy + s/2)) {
                ctx.fillStyle = 'rgba(255,0,0,0.2)';
                ctx.fillRect(wx, wy, s, s);
            }
        }
    }
    // Draw hero position
    ctx.fillStyle = 'lime';
    ctx.beginPath();
    ctx.arc(hero.x, hero.y, 6, 0, 2 * Math.PI);
    ctx.fill();
    ctx.restore();
}

function setMessage(text, duration) {
    game.message = text;
    game.messageUntil = now() + (duration || 2200);
}

function clearMessage() {
    if (game.messageUntil && now() > game.messageUntil) {
        game.message = "";
        game.messageUntil = 0;
    }
}

function remove(arr, value) {
    var index = arr.indexOf(value);
    if (index >= 0) arr.splice(index, 1);
}

function distance(ax, ay, bx, by) {
    var dx = ax - bx;
    var dy = ay - by;
    return Math.sqrt(dx * dx + dy * dy);
}

function isWayWall(x, y) {
    var block_x = Math.floor(x / s);
    var block_y = Math.floor(y / s);
    if (block_x < 0 || block_y < 0 || block_x >= level.wall.map[0].length || block_y >= level.wall.map.length) return true;
    // Border walls
    if (block_x === 0 || block_y === 0 || block_x === level.wall.map[0].length - 1 || block_y === level.wall.map.length - 1) return true;
    // Internal walls
    if (level.wall.map[block_y][block_x] !== 0) return true;
    // Objects block movement
    if (level.object.map[block_y][block_x] !== 0) return true;
    return false;
}

function getFloorTile(x, y) {
    if (!level.floor.map[y]) return null;
    if (!level.floor.map[y][x]) return null;
    var f = level.floor.map[y][x];
    return level.floor.tiles[f];
}

function randomFreePoint(minDistance) {
    for (var attempt = 0; attempt < 500; attempt++) {
        var point = {
            x: s * (1 + Math.random() * (level.floor.map[0].length - 2)),
            y: s * (1 + Math.random() * (level.floor.map.length - 2))
        };
        if (!isWayWall(point.x, point.y)) {
            if (!minDistance || distance(point.x, point.y, hero.x, hero.y) >= minDistance) return point;
        }
    }
    return {x: s * 3, y: s * 3};
}

function Shape(sprite, x, y) {
    this.x = x;
    this.y = y;
    this.offset_x = 0;
    this.offset_y = 0;
    this.sprite = sprite;
    this.isAboveHero = function() {
        var maxlen = tw * visible / 2;
        return Math.abs(this.x - hero.x) <= maxlen && Math.abs(this.y - hero.y) <= maxlen;
    };
}

function BaseWall(sprite, header, x, y) {
    Shape.call(this, sprite, x, y);
    this.header = header;
    this.isAboveHero = function() { return true; };
    this.offset_x -= 14;
    this.offset_y += 82;
}

function Wall(index, x, y) {
    BaseWall.call(this, level.wall.tiles[index], level.wall.header[index], x, y);
    switch (this.header.orientation) {
        case 2:
        case 6:
            this.offset_x += 16;
            break;
        case 5:
            this.offset_x -= 16;
            break;
        case 3:
            for (var inx in level.wall.header) {
                var h = level.wall.header[inx];
                if (h.orientation === 4 && h.main_index === this.header.main_index && h.sub_index === this.header.sub_index) {
                    walls.push(new Wall(inx, x, y));
                    break;
                }
            }
            this.offset_x += 16;
            break;
        case 4:
            this.offset_x -= 16;
            break;
    }
}

function WallObject(index, x, y) {
    BaseWall.call(this, level.object.tiles[index], level.object.header[index], x, y);
    this.offset_x += 16;
    var self = this;
    load(this.sprite, function() {
        if (self.sprite.width < 160) self.offset_x -= (160 - self.sprite.width) / 2;
    });
}

function DeathMob(mob) {
    Shape.call(this, mob.death, mob.x, mob.y);
    this.step = 0;
    this.angle = mob.angle;
    this.used = false;
    this.use = function() {
        if (this.used) return;
        this.used = true;
        var bonusCoins = mob.lootCoins || Math.floor(120 + Math.random() * 180);
        coins.push(new Coin(this.x + 28, this.y + 20, bonusCoins));
        if (Math.random() > 0.45) potions.push(new PotionHealth(this.x + 54, this.y));
        if (Math.random() > 0.75) coins.push(new Coin(this.x - 22, this.y + 8, Math.floor(bonusCoins / 2)));
        setMessage("Loot gathered from " + (mob.title || "the fallen") + ".", 1800);
    };
}

function Barrel(x, y) {
    Shape.call(this, barrelSprite, x, y);
    this.use = function(mob) {
        if (mob.doAttack) mob.doAttack(this);
    };
    this.damage = function() {
        remove(barrels, this);
        if (Math.random() > 0.35) coins.push(new Coin(this.x, this.y, Math.floor(45 + Math.random() * 60)));
        if (Math.random() > 0.8) potions.push(new PotionHealth(this.x + 12, this.y));
    };
}

function Coin(x, y, amount) {
    Shape.call(this, coinSprite, x, y);
    this.coins = amount || Math.floor(Math.random() * 100 + 50);
    this.use = function(mob) {
        remove(coins, this);
        mob.coins += this.coins;
        setMessage("Collected " + this.coins + " gold.", 1200);
    };
}

function Potion(x, y) {
    Shape.call(this, potionSprite, x, y);
    this.sprite.steps = 6;
    this.sprite.angles = 4;
    this.use = function(mob) {
        if (mob.addToBelt(this)) {
            remove(potions, this);
            setMessage("Potion stored on your belt.", 1200);
        }
    };
}

function PotionHealth(x, y) {
    Potion.call(this, x, y);
    this.step = 0;
    this.angle = 0;
    this.health = 850;
    this.drink = function(mob) {
        mob.health = Math.min(mob.origin_health, mob.health + this.health);
        setMessage("You recover " + this.health + " vitality.", 1200);
    };
}

function AncientGate(x, y) {
    Shape.call(this, gateSprite, x, y);
    this.offset_y += 24;
    this.active = false;
    this.label = "Ward Locked";
    this.use = function() {
        if (game.state !== "playing") return;
        if (!this.active) {
            setMessage("The gate remains sealed. Finish the trial first.", 1800);
            return;
        }
        if (game.stageIndex === STAGES.length - 1) {
            game.state = "won";
            setMessage("The relic is yours. The gauntlet is broken.", 4000);
            return;
        }
        advanceStage();
    };
}

function Mob(x, y, name) {
    this.to_x = x;
    this.to_y = y;
    this.name = name;
    this.title = name;
    this.stay = monsterMap[name].NU;
    this.run = monsterMap[name].WL;
    this.death = monsterMap[name].DD;
    this.currentState = this.stay;
    this.step = 0;
    this.angle = 0;
    this.st = 8;
    this.lootCoins = 150;
    Shape.call(this, this.currentState, x, y);
    this.rotate = function(sx, sy) {
        var l = this.currentState.angles;
        this.angle = Math.round((Math.atan2(sy, sx) / Math.PI + 2.75) * l / 2 + l / 2) % l;
    };
    this.rotateTo = function(point) {
        this.rotate(point.x - this.x, point.y - this.y);
    };
    this.setState = function(state) {
        if (this.currentState !== state) {
            this.currentState = state;
            this.step = -1;
        }
    };
    this.nextStep = function() {
        var dx = this.to_x - this.x;
        var dy = this.to_y - this.y;
        var dist = Math.sqrt(dx * dx + dy * dy);
        if (dist > 1) {
            var maxStep = this.st;
            var tx = 0, ty = 0;
            for (var st = 0; st <= maxStep; st += 1) {
                var sx = st * dx / dist;
                var sy = st * dy / dist;
                if (isWayWall(this.x + sx, this.y + sy)) {
                    tx = sx;
                    ty = sy;
                } else break;
            }
            this.rotate(tx, ty);
            if (Math.abs(tx) > 0.5 || Math.abs(ty) > 0.5) {
                this.x += tx;
                this.y += ty;
                this.setState(this.run);
            } else {
                this.setState(this.stay);
                this.x += tx;
                this.y += ty;
                this.to_x = this.x;
                this.to_y = this.y;
            }
        } else {
            this.setState(this.stay);
            this.to_x = this.x;
            this.to_y = this.y;
        }
        this.step = (this.step + 1) % this.currentState.steps;
        this.sprite = this.currentState;
    };
    this.origin_health = this.health = 1000;
    this.resistance = 10;
    this.use = function(mob) {
        if (mob.doAttack) mob.doAttack(this);
    };
    this.damage = function(damage) {
        var nextHealth = this.health - damage * 1000 / (1000 - this.resistance);
        if (nextHealth <= 0) {
            this.health = 0;
            remove(monsters, this);
            if (this.death) deathmobs.push(new DeathMob(this));
            stageKills++;
            if (monsters.length === 0 && game.gate) {
                game.gate.active = true;
                game.gate.label = STAGES[game.stageIndex].gateLabel;
                hero.health = Math.min(hero.origin_health, hero.health + 180);
                setMessage("The seal gives way. Approach the gate.", 2600);
            }
        } else {
            this.health = nextHealth;
        }
    };
}

function AgressiveMob(x, y, name) {
    Mob.call(this, x, y, name);
    this.attack = monsterMap[name].A1;
    this.attackOffset = monsterMap[name].attackOffset || 0;
    this.normalOffset = 0;
    this._nextStep = this.nextStep;
    this.nextStep = function() {
        if (this.currentState === this.attack) {
            if (this.step === this.attack.steps - 1) {
                this.currentState = this.stay;
                this.step = -1;
                if (this.attacked) {
                    this.attacked.damage(this.getDamage());
                    this.attacked = null;
                }
            }
            this.step = (this.step + 1) % this.currentState.steps;
            this.sprite = this.currentState;
        } else this._nextStep();
        this.offset_y = this.currentState === this.attack ? this.attackOffset : this.normalOffset;
    };
    this.currentDamage = 30;
    this.getDamage = function() {
        return this.currentDamage;
    };
    this.attacked = null;
    this.doAttack = function(mob) {
        if (this.attacked !== mob) {
            this.rotateTo(mob);
            this.setState(this.attack);
            this.attacked = mob;
        }
    };
}

function HeroBarbarian(x, y) {
    AgressiveMob.call(this, x, y, "BA");
    this.attackOffset = 40;
    this.normalOffset = 10;
    this.health = this.origin_health = 1500;
    this.belt = {items: [], size: 10};
    this.st = 16;
    this.coins = 0;
    this.resistance = 140;
    this.title = "The Wardenbreaker";
    this.addToBelt = function(potion) {
        for (var i = 0; i < this.belt.size; i++) {
            if (typeof this.belt.items[i] === "undefined") {
                this.belt.items[i] = potion;
                return true;
            }
        }
        setMessage("Your belt is full.", 1200);
        return false;
    };
    this.criticalDamage = 0.35;
    this.currentDamage = 130;
    this.getDamage = function() {
        return this.currentDamage * (Math.random() <= this.criticalDamage ? 3.5 : 1);
    };
    // Override nextStep to always move toward to_x, to_y
    // Save AgressiveMob's nextStep as _superNextStep
    this._superNextStep = this.nextStep;
    this.nextStep = function() {
        var dx = this.to_x - this.x;
        var dy = this.to_y - this.y;
        if (Math.abs(dx) > 1 || Math.abs(dy) > 1) {
            this._superNextStep();
        } else {
            this.setState(this.stay);
            this.to_x = this.x;
            this.to_y = this.y;
        }
        this.step = (this.step + 1) % this.currentState.steps;
        this.sprite = this.currentState;
    };
    this.damage = function(damage) {
        var nextHealth = this.health - damage * 1000 / (1000 - this.resistance);
        this.health = Math.max(0, nextHealth);
        if (this.health <= 0 && game.state === "playing") {
            this.to_x = this.x;
            this.to_y = this.y;
            game.state = "lost";
            setMessage("You fall beneath the gatehouse stones. Press R to try again.", 5000);
        }
    };
    this.resetAdventure = function() {
        this.x = s * 3;
        this.y = s * 3;
        this.to_x = this.x;
        this.to_y = this.y;
        this.health = this.origin_health;
        this.coins = 0;
        this.belt.items = [];
        this.currentDamage = 130;
    };
}

var hero = new HeroBarbarian(s * 3, s * 3);

function buildWalls() {
    walls.length = 0;
    // Border walls
    for (var y = 0; y < level.wall.map.length; y++) {
        for (var x = 0; x < level.wall.map[y].length; x++) {
            if (x === 0 || y === 0 || x === level.wall.map[y].length - 1 || y === level.wall.map.length - 1) {
                level.wall.map[y][x] = 948; // Use a wall tile
            } else if ((x % 7 === 0 && y % 7 > 1 && y % 7 < 6) || (y % 9 === 0 && x % 9 > 1 && x % 9 < 7)) {
                level.wall.map[y][x] = 372; // Internal obstacles
            } else {
                level.wall.map[y][x] = 0;
            }
            if (level.wall.map[y][x] > 0) walls.push(new Wall(level.wall.map[y][x], x * s, y * s));
        }
    }
    // Place a few objects
    for (var i = 0; i < 20; i++) {
        var ox = Math.floor(2 + Math.random() * (level.object.map[0].length - 4));
        var oy = Math.floor(2 + Math.random() * (level.object.map.length - 4));
        level.object.map[oy][ox] = 5844;
        walls.push(new WallObject(5844, ox * s, oy * s));
    }
}

function spawnMonster(config) {
    var point = randomFreePoint(s * 1.8);
    var mob = new AgressiveMob(point.x, point.y, config.type);
    mob.title = config.title || config.type;
    mob.origin_health = mob.health = config.health || mob.health;
    mob.currentDamage = config.damage || mob.currentDamage;
    mob.resistance = typeof config.resistance === "number" ? config.resistance : mob.resistance;
    mob.st = config.speed || mob.st;
    mob.lootCoins = config.lootCoins || Math.floor(130 + Math.random() * 170);
    monsters.push(mob);
    return mob;
}

function spawnBarrels(count) {
    for (var i = 0; i < count; i++) {
        var point = randomFreePoint(s * 1.2);
        barrels.push(new Barrel(point.x, point.y));
    }
}

function spawnPotions(count) {
    for (var i = 0; i < count; i++) {
        var point = randomFreePoint(s * 1.1);
        potions.push(new PotionHealth(point.x, point.y));
    }
}

function setupStage(index) {
    var stage = STAGES[index];
    monsters.length = 0;
    deathmobs.length = 0;
    barrels.length = 0;
    coins.length = 0;
    potions.length = 0;
    interactives.length = 0;
    stageKills = 0;

    hero.to_x = hero.x;
    hero.to_y = hero.y;
    hero.health = Math.min(hero.origin_health, hero.health + stage.healOnStart);
    hero.x = s * 3;
    hero.y = s * 3;
    hero.to_x = hero.x;
    hero.to_y = hero.y;

    for (var groupIndex = 0; groupIndex < stage.monsters.length; groupIndex++) {
        var group = stage.monsters[groupIndex];
        for (var count = 0; count < group.count; count++) spawnMonster(group);
    }

    spawnBarrels(stage.barrelCount);
    spawnPotions(stage.potionCount);

    game.gate = new AncientGate(s * 8.3, s * 2.3);
    game.gate.label = "Ward Locked";
    interactives.push(game.gate);
    game.questText = stage.intro;
    setMessage(stage.name + ": " + stage.intro, 3000);
}

function startGame() {
    hero.resetAdventure();
    game.state = "playing";
    game.stageIndex = 0;
    setupStage(0);
}

function advanceStage() {
    game.stageIndex++;
    if (game.stageIndex >= STAGES.length) {
        game.state = "won";
        setMessage("The relic is yours. The gauntlet is broken.", 4000);
        return;
    }
    setupStage(game.stageIndex);
}

function loadZb(order, click) {
    var tmp_zb = [];
    var zb = [];
    var all = [monsters, potions, barrels, interactives, click ? [] : [hero], click ? [] : walls];
    for (var t in all) {
        for (var m in all[t]) {
            if (all[t][m].isAboveHero()) tmp_zb.push(all[t][m]);
        }
    }
    tmp_zb.sort(function(objA, objB) {
        var compare = (objB.x + objB.offset_x) + (objB.y + objB.offset_y) - (objA.x + objA.offset_x) - (objA.y + objA.offset_y);
        return order ? compare : -compare;
    });
    var ordered = [coins, deathmobs, tmp_zb];
    for (var i in ordered) for (var j in ordered[i]) zb.push(ordered[i][j]);
    return zb;
}

function processClick() {
    var zb = loadZb(true, true);
    var cx = (floor.click_x - floor.click_y) * acos;
    var cy = (floor.click_x + floor.click_y) / 2 * asin;
    for (var i in zb) {
        var entity = zb[i];
        var sprite = entity.sprite;
        var sx = (entity.x - entity.y) * acos + entity.offset_x;
        var sy = (entity.x + entity.y) / 2 * asin + entity.offset_y;

        var spriteWidth = sprite.angles ? sprite.width / sprite.angles : sprite.width;
        var spriteHeight = sprite.steps ? sprite.height / sprite.steps : sprite.height;
        if (cx >= sx - spriteWidth / 2 && cx <= sx + spriteWidth / 2 && cy >= sy - spriteHeight && cy <= sy) {
            entity.use(hero);
            return true;
        }
    }
    return false;
}

floor.canvas.onclick = function(e) {
        console.log('Canvas clicked');
    if (game.state === "won" || game.state === "lost") {
        startGame();
        return;
    }
    if (game.state !== "playing") return;

    // Get canvas bounding rect for correct mouse position
    var rect = floor.canvas.getBoundingClientRect();
    var mx = (e.clientX - rect.left) - floor.w / 2 + th;
    var my = (e.clientY - rect.top) - floor.h / 2;

    // Inverse isometric transform to get world coordinates
    var mrx = hero.x * acos - hero.y * asin;
    var mry = (hero.x * asin + hero.y * acos) / 2;
    var wx = (mx + mrx) / acos;
    var wy = (2 * my + mry * 2 - mx - mrx) / (2 * asin);

    floor.click_x = wx;
    floor.click_y = wy;
    console.log('[CLICK]', {
        hero_x: hero.x,
        hero_y: hero.y,
        click_x: wx,
        click_y: wy,
        to_x: hero.to_x,
        to_y: hero.to_y
    });
    processClick(); // still allow interaction, but always set move target
    hero.to_x = floor.click_x;
    hero.to_y = floor.click_y;
    if (typeof hero.setState === 'function' && hero.run) hero.setState(hero.run);
    console.log('[MOVE TARGET]', {to_x: hero.to_x, to_y: hero.to_y});
};

window.onkeydown = function(e) {
    var beltKeys = [49,50,51,52,53,54,55,56,57,48];
    var beltIndex = beltKeys.indexOf(e.keyCode);
    if (beltIndex >= 0) {
        if (hero.belt.items[beltIndex] instanceof PotionHealth) {
            hero.belt.items[beltIndex].drink(hero);
            delete hero.belt.items[beltIndex];
        }
        return false;
    }
    if (e.keyCode === 9 || e.keyCode === 77) {
        game.showMap = !game.showMap;
        return false;
    }
    if (e.keyCode === 82) {
        startGame();
        return false;
    }
};

function renderObjects() {
    var zb = loadZb(false);
    for (var z in zb) {
        var entity = zb[z];
        floor.save();
        var sx = (entity.x - entity.y) * acos + entity.offset_x;
        var sy = (entity.x + entity.y) / 2 * asin + entity.offset_y;
        var tile = entity.sprite;
        var renderW = tile.width;
        var renderH = tile.height;
        if (tile.steps && tile.angles) {
            renderW /= tile.steps;
            renderH /= tile.angles;
            floor.drawImage(tile, renderW * entity.step, renderH * entity.angle, renderW, renderH, Math.round(sx - renderW / 2 - tile.offsetX), Math.round(sy - renderH), renderW, renderH);
        } else {
            if (entity instanceof AncientGate && !entity.active) floor.globalAlpha = 0.45;
            floor.drawImage(tile, Math.round(sx - tile.width / 2) + 1, Math.round(sy - tile.height) + 1);
        }
        floor.restore();

        if (entity.health && entity.origin_health && entity !== hero) {
            floor.save();
            floor.globalAlpha = 0.75;
            sy -= 92;
            var maxLine = Math.floor(entity.origin_health / 20);
            var currentLine = Math.max(0, Math.floor(entity.health / 20));
            floor.fillStyle = "black";
            floor.fillRect(sx - maxLine / 2 - 1, sy, maxLine + 2, 6);
            floor.fillStyle = entity.title === "Crypt Warden" ? "#d9c26e" : "#b22222";
            floor.fillRect(sx - maxLine / 2, sy + 1, currentLine, 4);
            floor.restore();
        }

        if (entity instanceof AncientGate || entity.title === "Crypt Warden" || entity.title === "Bone Captain") {
            floor.save();
            floor.textAlign = "center";
            floor.fillStyle = "#d8c392";
            floor.font = "14px Verdana";
            floor.fillText(entity.label || entity.title, sx, sy - 120);
            floor.restore();
        }
    }
}

function renderFloor() {
    floor.save();
    floor.translate(floor.w / 2 - th, floor.h / 2);
    var fdx = Math.floor(hero.x / s);
    var fdy = Math.floor(hero.y / s);
    var miny = Math.max(0, fdy - visible);
    var maxy = Math.min(level.floor.map.length - 1, fdy + visible);
    var minx = Math.max(0, fdx - visible);
    var maxx = Math.min(level.floor.map[0].length - 1, fdx + visible);
    var mrx = hero.x * acos - hero.y * asin;
    var mry = hero.x * asin + hero.y * acos;
    mry = mry / 2;
    floor.translate(-mrx, -mry);
    for (var y = miny; y <= maxy; y++) {
        for (var x = minx; x <= maxx; x++) {
            var tile = getFloorTile(x, y);
            if (tile) {
                var tx = (x - y) * th;
                var ty = (x + y) * th / 2;
                floor.drawImage(tile, tx, ty, tile.width + 0.707, tile.height + 0.707);
            }
        }
    }
    floor.translate(th, 0);
    // DEBUG: draw walkability overlay
    drawWalkabilityOverlay(floor);
    renderObjects();
    floor.restore();
}

function renderMap() {
    floor.save();
    floor.translate(floor.w - 180, 160);
    var sc = 0.25;
    floor.scale(1 * sc, 0.5 * sc);
    floor.rotate(Math.PI * 0.25);
    floor.translate(-hero.x, -hero.y);
    floor.fillStyle = "rgba(0,0,0,0.55)";
    var wallOffset = [];
    for (var y = 4; y >= 0; y--) for (var x = 0; x <= 4; x++) wallOffset.push({x: x * s / 5, y: y * s / 5});
    for (var i in walls) {
        var wall = walls[i];
        var walk = wall.header.walk;
        if (wall.header.orientation === 4) continue;
        for (var j = 0; j < 25; j++) if (walk[j] === 1) floor.fillRect(wall.x + wallOffset[j].x, wall.y + wallOffset[j].y, s / 5, s / 5);
    }
    floor.fillStyle = "rgba(216,195,146,0.95)";
    floor.fillRect(hero.x, hero.y, s / 5, s / 5);
    if (game.gate) {
        floor.fillStyle = game.gate.active ? "rgba(70,200,110,0.85)" : "rgba(210,90,70,0.75)";
        floor.fillRect(game.gate.x, game.gate.y, s / 5, s / 5);
    }
    floor.restore();
}

function renderHeroHealth() {
    var radius = 68;
    var padding = 22;
    floor.save();
    floor.globalAlpha = 0.55;
    floor.fillStyle = "black";
    floor.beginPath();
    floor.arc(radius + padding, floor.h - radius - padding, radius + 5, 0, Math.PI * 2);
    floor.closePath();
    floor.fill();
    floor.fillStyle = "#851414";
    var percent = hero.health / hero.origin_health;
    var angleFrom = Math.PI * (0.5 - percent);
    var angleTo = Math.PI * (0.5 + percent);
    floor.beginPath();
    floor.arc(radius + padding, floor.h - radius - padding, radius, angleFrom, angleTo);
    floor.closePath();
    floor.fill();
    floor.restore();

    floor.save();
    floor.fillStyle = "#d8c392";
    floor.font = "bold 14px Verdana";
    floor.fillText("HP", 18, floor.h - 26);
    floor.fillText(Math.floor(hero.health) + " / " + hero.origin_health, 62, floor.h - 26);
    floor.restore();
}

function renderHeroBelt() {
    floor.save();
    var tile = potionSprite;
    var slotW = tile.width / tile.steps;
    var slotH = tile.height / tile.angles;
    for (var i = 0; i < hero.belt.size; i++) {
        floor.globalAlpha = 0.8;
        floor.drawImage(tile, slotW * 2, slotH * 3, slotW, slotH, 188 + slotW * i, 606, slotW, slotH);
        floor.globalAlpha = 1;
        var potion = hero.belt.items[i];
        if (potion) floor.drawImage(tile, slotW * potion.step, slotH * potion.angle, slotW, slotH, 188 + slotW * i, 606, slotW, slotH);
        floor.fillStyle = "#d8c392";
        floor.font = "11px Verdana";
        floor.fillText((i + 1) % 10, 200 + slotW * i, 655);
    }
    floor.restore();
}

function renderHud() {
    floor.save();
    floor.fillStyle = "rgba(0,0,0,0.55)";
    floor.fillRect(18, 16, 310, 128);
    floor.strokeStyle = "rgba(216,195,146,0.5)";
    floor.strokeRect(18, 16, 310, 128);
    floor.fillStyle = "#d8c392";
    floor.font = "bold 22px Verdana";
    floor.fillText("PeakeCoin Gauntlet", 34, 44);
    floor.font = "14px Verdana";
    var stage = STAGES[game.stageIndex] || {name: "Preparing..."};
    floor.fillText("Stage: " + stage.name, 34, 70);
    floor.fillText("Enemies left: " + monsters.length, 34, 92);
    floor.fillText("Gold: " + hero.coins, 34, 114);
    floor.fillText("Objective: " + (game.gate && game.gate.active ? game.gate.label : "Defeat every foe"), 34, 136);
    floor.restore();
}

function renderMessage() {
    if (!game.message) return;
    floor.save();
    floor.fillStyle = "rgba(0,0,0,0.65)";
    floor.fillRect(210, 18, 560, 38);
    floor.strokeStyle = "rgba(216,195,146,0.5)";
    floor.strokeRect(210, 18, 560, 38);
    floor.fillStyle = "#f0e0b6";
    floor.font = "15px Verdana";
    floor.textAlign = "center";
    floor.fillText(game.message, 490, 43);
    floor.restore();
}

function renderHelp() {
    floor.save();
    floor.fillStyle = "rgba(0,0,0,0.45)";
    floor.fillRect(680, 530, 282, 108);
    floor.strokeStyle = "rgba(216,195,146,0.4)";
    floor.strokeRect(680, 530, 282, 108);
    floor.fillStyle = "#d8c392";
    floor.font = "13px Verdana";
    floor.fillText("Click to move. Click nearby targets to attack or loot.", 694, 555);
    floor.fillText("1-0 drink belt potions. M or Tab toggles the map.", 694, 578);
    floor.fillText("R restarts the run at any time.", 694, 601);
    floor.fillText("Clear each trial, then touch the gate to continue.", 694, 624);
    floor.restore();
}

function renderOverlay() {
    if (game.state === "playing") return;
    floor.save();
    floor.fillStyle = "rgba(0,0,0,0.55)";
    floor.fillRect(0, 0, floor.w, floor.h);
    floor.fillStyle = "#d8c392";
    floor.textAlign = "center";
    floor.font = "bold 42px Verdana";
    var title = game.state === "won" ? "Victory" : (game.state === "lost" ? "Defeat" : "Loading");
    floor.fillText(title, floor.w / 2, 220);
    floor.font = "18px Verdana";
    if (game.state === "won") {
        floor.fillText("You emerged with " + hero.coins + " gold and shattered the gauntlet.", floor.w / 2, 270);
        floor.fillText("Click or press R to begin another run.", floor.w / 2, 305);
    } else if (game.state === "lost") {
        floor.fillText("The wardens hold the chamber. Click or press R to try again.", floor.w / 2, 270);
    } else {
        floor.fillText("Gathering sprites and stonework...", floor.w / 2, 270);
    }
    floor.restore();
}

function updateMonsters() {
    if (game.state !== "playing") return;
    for (var i = 0; i < monsters.length; i++) monsters[i].nextStep();
}

setInterval(function() {
    if (game.state !== "playing") return;
    if (monsters.length === 0) return;
    var randomMob = monsters[Math.ceil(Math.random() * (monsters.length - 1))];
    if (randomMob && typeof randomMob.attacked !== "object") {
        randomMob.to_x = randomMob.x + (Math.random() * s - s / 2);
        randomMob.to_y = randomMob.y + (Math.random() * s - s / 2);
    }
    for (var i = 0; i < monsters.length; i++) {
        var mob = monsters[i];
        var attackDist = mob.title === "Crypt Warden" ? 128 : 104;
        if (mob.attack && mob.isAboveHero()) {
            if (Math.abs(hero.x - mob.x) < attackDist && Math.abs(hero.y - mob.y) < attackDist) {
                mob.doAttack(hero);
                mob.to_x = mob.x;
                mob.to_y = mob.y;
            } else {
                mob.to_x = hero.x;
                mob.to_y = hero.y;
            }
        }
    }
}, 220);

setInterval(function() {
    if (game.state === "playing") hero.health = Math.min(hero.origin_health, hero.health + 18);
}, 2500);

setInterval(function() {
    clearMessage();
    if (imageCount > 0) {
        floor.fillStyle = "black";
        floor.fillRect(0, 0, floor.w, floor.h);
        renderOverlay();
        return;
    }
    if (game.state === "loading") startGame();
    if (game.state === "playing") hero.nextStep();
    updateMonsters();
    floor.fillStyle = "black";
    floor.fillRect(0, 0, floor.w, floor.h);
    renderFloor();
    renderHeroHealth();
    renderHeroBelt();
    renderHud();
    renderHelp();
    renderMessage();
    if (game.showMap) renderMap();
    renderOverlay();
}, 66);

buildWalls();
})();



🪙 PeakeCoin Ecosystem


⚙️ HiveP.I.M.P. — Market Stabilization Layer

::contentReference[oaicite:0]{index=0}

Operated by @hivepimp, P.I.M.P. stabilizes PEK markets and supports liquidity on Hive Engine.
Community participation strengthens long-term market health.
👉 https://peakecoin.info


🤖 PeakeBot — Autonomous Trading System (RC-AWARE)

::contentReference[oaicite:1]{index=1}

Independent multi-token trading bot featuring:

  • RC-aware execution
  • Adaptive delay logic
  • Self-regulating trade cycles

📊 Trading bot details:
👉 https://geocities.ws/p/e/peakecoin/trading-bot/peakebot_v0_01.html
💻 Open-source repositories:
👉 https://github.com/paulmoon410


🎰 PeakeSino — The PeakeCoin Casino (Beta)

::contentReference[oaicite:2]{index=2}

Blockchain-powered games using PEK as the native in-game currency.
Built on Hive with provable fairness and community-driven growth.

🃏 Play the beta games here:
👉 https://geocities.ws/peakecoin/pek_casino/beta_games/index.html


🙏 Acknowledgements

Thanks to and please follow:
@enginewitty @ecoinstant @neoxian @txracer @thecrazygm @holdonia @aggroed

For their continued support, guidance, and help expanding the PeakeCoin ecosystem.


Sort:  

Congratulations @strangedad! You have completed the following achievement on the Hive blockchain And have been rewarded with New badge(s)

You received more than 7000 upvotes.
Your next target is to reach 8000 upvotes.

You can view your badges on your board and compare yourself to others in the Ranking
If you no longer want to receive notifications, reply to this comment with the word STOP

Check out our last posts:

Our Hive Power Delegations to the March PUM Winners
Feedback from the April Hive Power Up Day
Hive Power Up Month Challenge - March 2026 Winners List