Xonix на Javascript с картинками

// requestAnimationFrame/cancelAnimationFrame polyfill:
(function() {
    var tLast = 0;
    var vendors = ['webkit', 'moz'];
    for(var i = 0; i < vendors.length && !window.requestAnimationFrame; ++i) {
        var v = vendors[i];
        window.requestAnimationFrame = window[v+'RequestAnimationFrame'];
        window.cancelAnimationFrame = window[v+'CancelAnimationFrame'] ||
            window[v+'CancelRequestAnimationFrame'];
    }
    if (!window.requestAnimationFrame)
        window.requestAnimationFrame = function(callback, element) {
            var tNow = Date.now();
            var dt = Math.max(0, 17 - tNow + tLast);
            var id = setTimeout(function() { callback(tNow + dt); }, dt);
            tLast = tNow + dt;
            return id;
        };
    if (!window.cancelAnimationFrame)
        window.cancelAnimationFrame = function(id) {
            clearTimeout(id);
        };
}());

(function() {

    window.picxonix = function(v1, v2) {
        if (typeof v1 != 'string') {
            return init(v1, v2);
        }
        switch (v1) {
            case 'level': // начать новый уровень
                loadLevel(v2);
                break;
            case 'end': // закончить уровень
                endLevel(v2);
                break;
            case 'play': // пауза/возобновление игры
                setPlayMode(v2);
                break;
            case 'cursorDir': // угол движения курсора
                typeof v2 == 'string'? setDir(v2) : setDirToward(v2);
                break;
            case 'cursorSpeed': // скорость движения курсора
                setCursorSpeed(v2);
                break;
            case 'enemySpeed': // скорость движения точек
                setEnemySpeed(v2);
                break;
            case 'enemySpawn': // увеличить число сухопутных точек
                spawn();
                break;
            case 'state': // текущее состояние уровня
                return buildLevelState();
            default:
        }
        return 0;
    }

    var cfgMain = {
        width: 600,
        height: 400,
        sizeCell: 10,
        colorFill: '#000000',
        colorBorder: '#00aaaa',
        colorBall: '#ffffff',
        colorBallIn: '#000000',
        colorWarder: '#000000',
        colorWarderIn: '#f80000',
        colorCursor: '#aa00aa',
        colorCursorIn: '#00aaaa',
        colorTrail: '#a800a8',
        timeoutCollision: 1000,
        callback: null,
        callbackOnFrame: false
    };
    var cfgLevel = {
        nBalls: 1,
        nWarders: 1,
        speedCursor: 5,
        speedEnemy: 5
    };
    // cell attributes:
    var CA_CLEAR = 1 << 0;
    var CA_TRAIL = 1 << 1;
    // размеры:
    var sizeCell;
    var width, height;
    // ресурсы:
    var elContainer;
    var ctxPic;
    var ctxMain;
    var imgPic;
    var imgBall;
    var imgWarder;
    var imgCursor;
    // объекты игры:
    var dirset;
    var cellset;
    var cursor;
    var aBalls = [], aWarders = [];
    var nBalls = 0, nWarders = 0;
    // текущее состояние уровня:
    var idFrame = 0;
    var tLevel = 0;
    var tLastFrame = 0;
    var tLocked = 0;
    var bCollision = false;
    var bConquer = false;
    var dirhash = {
        'left': 180, 'right': 0, 'up': 270, 'down': 90, 'stop': false
    };

    function init(el, opts) {
        if (elContainer || !el || !el.appendChild) return false;
        elContainer = el;
        // установка общих настроек игры:
        merge(cfgMain, opts);
        if (!cfgMain.sizeCell) return false;
        sizeCell = cfgMain.sizeCell;
        if (typeof cfgMain.callback != 'function') cfgMain.callback = null;
        // установка настроек уровня:
        if (opts.speedCursor ^ opts.speedEnemy) {
            opts.speedCursor = opts.speedEnemy = Math.max(opts.speedCursor || 0, opts.speedEnemy || 0);
        }
        merge(cfgLevel, opts);
        setLevelData(cfgMain.width, cfgMain.height);
        var oWrap = document.createElement('div');
        oWrap.style.position = 'relative';
        // создаем канвас фона (картинки):
        (function() {
            var canvas = document.createElement('canvas');
            ctxPic = canvas.getContext('2d');
            canvas.width = width;
            canvas.height = height;
            canvas.style.position = 'absolute';
            canvas.style.left = canvas.style.top = (2*sizeCell) + 'px';
            ctxPic.fillStyle = cfgMain.colorTrail;
            ctxPic.fillRect(0, 0, width, height);
            oWrap.appendChild(canvas);
        }());
        // создаем канвас игрового поля:
        (function() {
            var canvas = document.createElement('canvas');
            ctxMain = canvas.getContext('2d');
            canvas.width = width+ 4*sizeCell;
            canvas.height = height+ 4*sizeCell;
            canvas.style.position = 'absolute';
            canvas.style.left = canvas.style.top = 0;
            fillCanvas();
            ctxMain.fillStyle = cfgMain.colorFill;
            ctxMain.fillRect(2*sizeCell, 2*sizeCell, width, height);
            oWrap.appendChild(canvas);
        }());
        elContainer.appendChild(oWrap);
        // создаем временный канвас:
        var canvas = document.createElement('canvas');
        var ctxTmp = canvas.getContext('2d');
        canvas.width = sizeCell;
        canvas.height = sizeCell;
        // создаем изображение морской точки:
        var r = sizeCell / 2, q = sizeCell / 4;
        ctxTmp.clearRect(0, 0, sizeCell, sizeCell);
        ctxTmp.beginPath();
        ctxTmp.arc(r, r, r, 0, Math.PI * 2, false);
        ctxTmp.fillStyle = cfgMain.colorBall;
        ctxTmp.fill();
        if (cfgMain.colorBallIn) {
            ctxTmp.beginPath();
            ctxTmp.arc(r, r, q, 0, Math.PI * 2, false);
            ctxTmp.fillStyle = cfgMain.colorBallIn;
            ctxTmp.fill();
        }
        imgBall = new Image();
        imgBall.src = ctxTmp.canvas.toDataURL();
        function prepareSquare(colorOut, colorIn) {
            ctxTmp.clearRect(0, 0, sizeCell, sizeCell);
            ctxTmp.fillStyle = colorOut;
            ctxTmp.fillRect(0, 0, sizeCell, sizeCell);
            if (colorIn) {
                ctxTmp.fillStyle = colorIn;
                ctxTmp.fillRect(q, q, sizeCell - r, sizeCell - r);
            }
        }
        // создаем изображение сухопутной точки:
        prepareSquare(cfgMain.colorWarder, cfgMain.colorWarderIn);
        imgWarder = new Image();
        imgWarder.src = ctxTmp.canvas.toDataURL();
        // создаем изображение курсора:
        prepareSquare(cfgMain.colorCursor, cfgMain.colorCursorIn);
        imgCursor = new Image();
        imgCursor.src = ctxTmp.canvas.toDataURL();
        return {width: width+ 4*sizeCell, height: height+ 4*sizeCell};
    }

    function loadLevel(data) {
        if (tLevel || tLastFrame || !data || !data.image) return;
        if (!data.image) return;
        var img = new Image();
        img.onload = function() {
            applyLevel(img, data);
        };
        img.src = data.image;
    }

    function applyLevel(img, data) {
        imgPic = img;
        merge(cfgLevel, data, true);
        setLevelData(img.width, img.height);
        ctxMain.canvas.width = width+ 4*sizeCell;
        ctxMain.canvas.height = height+ 4*sizeCell;
        fillCanvas();
        cellset.reset();
        ctxPic.canvas.width = width;
        ctxPic.canvas.height = height;
        ctxPic.drawImage(imgPic, 0, 0, width, height, 0, 0, width, height);
        var pos = cellset.placeCursor();
        cursor.reset(pos[0], pos[1]);
        aBalls = []; aWarders = [];
        var i, aPos;
        aPos = cellset.placeBalls(nBalls);
        for (i = 0; i < nBalls; i++)
            aBalls.push(new Enemy(aPos[i][0], aPos[i][1], false));
        aPos = cellset.placeWarders(nWarders);
        for (i = 0; i < nWarders; i++)
            aWarders.push(new Enemy(aPos[i][0], aPos[i][1], true, 45));
        tLevel = Date.now();
        tLastFrame = 0;
        startLoop();
    }

    function endLevel(bClear) {
        if (tLastFrame) return;
        tLevel = 0;
        if (!bClear) return;
        fillCanvas();
        ctxMain.clearRect(2*sizeCell, 2*sizeCell, width, height);
    }

    function setLevelData(w, h) {
        if (w) width = w - w % (2*sizeCell);
        if (h) height = h - h % (2*sizeCell);
        if (cfgLevel.nBalls) nBalls = cfgLevel.nBalls;
        if (cfgLevel.nWarders) nWarders = cfgLevel.nWarders;
    }

    function setPlayMode(bOn) {
        if (bOn ^ !tLastFrame) return;
        tLastFrame? endLoop() : startLoop();
    }

    function setDir(key) {
        if (!tLastFrame) return;
        if (key in dirhash) cursor.setDir(dirhash[key]);
    }

    function setDirToward(pos) {
        if (!tLastFrame || !pos || pos.length < 2) return;
        var xc = Math.floor(pos[0] / sizeCell) - 2,
            yc = Math.floor(pos[1] / sizeCell) - 2;
        var b = cellset.isPosValid(xc, yc);
        if (!b) return;
        var posCr = cursor.pos(), dirCr = cursor.getDir(), dir = false;
        if (dirCr === false) {
            var dx = xc - posCr[0], dy = yc - posCr[1],
                dc = Math.abs(dx) - Math.abs(dy);
            if (dc == 0) return;
            dir = dirset.find(dx, dy);
            if (dir % 90 != 0) {
                var dir1 = dir-45, dir2 = dir+45;
                dir = dir1 % 180 == 0 ^ dc < 0? dir1 : dir2;
            }
        }
        else {
            var delta = dirCr % 180? xc - posCr[0] : yc - posCr[1];
            if (!delta) return;
            dir = (delta > 0? 0 : 180) + (dirCr % 180? 0 : 90);
        }
        cursor.setDir(dir);
    }

    function setCursorSpeed(v) {
        if (v > 0) cfgLevel.speedCursor = v;
    }

    function setEnemySpeed(v) {
        if (v > 0) cfgLevel.speedEnemy = v;
    }

    function startLoop() {
        if (!tLevel) return;
        idFrame = requestAnimationFrame(loop);
    }

    function endLoop() {
        if (idFrame) cancelAnimationFrame(idFrame);
        tLastFrame = idFrame = 0;
    }

    // Главный цикл анимации
    function loop(now) {
        var dt = tLastFrame? (now - tLastFrame) / 1000 : 0;
        bCollision = bConquer = false;
        if (!tLastFrame || update(dt)) {
            render();
            tLastFrame = now;
        }
        if (bCollision) {
            lock();
            cfgMain.callback && cfgMain.callback(1);
            return;
        }
        if (bConquer) {
            bConquer = false;
            tLastFrame = 0;
            cellset.conquer();
            if (cfgMain.callback && cfgMain.callback(2))
                return;
        }
        else
            cfgMain.callback && cfgMain.callbackOnFrame && cfgMain.callback(0);
        startLoop();
    }

    function update(dt) {
        var distCursor = Math.round(dt * cfgLevel.speedCursor),
            distEnemy = Math.round(dt * cfgLevel.speedEnemy);
        if (!(distCursor >= 1 || distEnemy >= 1)) return false;
        cursor.update(distCursor);
        var i;
        for (i = 0; i < nBalls; i++) aBalls[i].update(distEnemy);
        for (i = 0; i < nWarders; i++) aWarders[i].update(distEnemy);
        return true;
    }

    function render() {
        cellset.render();
        cursor.render();
        var i;
        for (i = 0; i < nBalls; i++) aBalls[i].render();
        for (i = 0; i < nWarders; i++) aWarders[i].render();
    }

    function lock() {
        tLastFrame = 0;
        bCollision = false;
        var posCr = cursor.pos();
        cellset.add2Trail(posCr[0], posCr[1], false);
        setTimeout(unlock, cfgMain.timeoutCollision);
    }

    function unlock() {
        if (!tLevel) return;
        cellset.clearTrail();
        var pos = cellset.placeCursor();
        cursor.reset(pos[0], pos[1], true);
        var aPos = cellset.placeWarders(nWarders);
        for (var i = 0; i < nWarders; i++)
            aWarders[i].reset(aPos[i][0], aPos[i][1]);
        startLoop();
    }

    function spawn() {
        if (!tLevel) return;
        var pos = cellset.placeSpawned();
        if (!pos) return;
        aWarders.push(new Enemy(pos[0], pos[1], true));
        nWarders++;
    }

    function buildLevelState() {
        return {
            play: Boolean(tLastFrame),
            posCursor: cursor.pos(),
            warders: nWarders,
            speedCursor: cfgLevel.speedCursor,
            speedEnemy: cfgLevel.speedEnemy,
            cleared: cellset.getPercentage()
        };
    }

    function fillCanvas() {
        ctxMain.fillStyle = cfgMain.colorBorder;
        ctxMain.fillRect(0, 0, width+ 4*sizeCell, height+ 4*sizeCell);
    }

    function drawCellImg(img, x, y) {
        ctxMain.drawImage(img,
            0, 0, sizeCell, sizeCell,
            (x+2)*sizeCell, (y+2)*sizeCell, sizeCell, sizeCell
        );
    }

    function clearCellArea(x, y, w, h) {
        ctxMain.clearRect(
            (x+2)*sizeCell, (y+2)*sizeCell, (w || 1)* sizeCell, (h || 1)* sizeCell
        );
    }

    function fillCellArea(color, x, y, w, h) {
        ctxMain.fillStyle = color;
        ctxMain.fillRect(
            (x+2)*sizeCell, (y+2)*sizeCell, (w || 1)* sizeCell, (h || 1)* sizeCell
        );
    }

    // Множество доступных направлений:
    dirset = {
        vecs: {
            0: [1, 0], 45: [1, 1], 90: [0, 1], 135: [-1, 1], 180: [-1, 0], 225: [-1, -1], 270: [0, -1], 315: [1, -1]
        },
        get: function(v) {
            return v in this.vecs? this.vecs[v] : [0, 0];
        },
        find: function(x, y) {
            x = x == 0? 0 : (x > 0? 1 : -1);
            y = y == 0? 0 : (y > 0? 1 : -1);
            for (var v in this.vecs) {
                var vec = this.vecs[v];
                if (vec[0] == x && vec[1] == y) return parseInt(v);
            }
            return false;
        }
    };

    // Матрица ячеек игрового поля:
    cellset = {
        nW: 0,
        nH: 0,
        nWx: 0,
        nCleared: 0,
        dirTrail: 0,
        iPreTrail: 0,
        aCells: [],
        aTrail: [],
        aTrailNodes: [],
        aTrailRects: [],
        reset: function() {
            var nW = this.nW = Math.floor(width / sizeCell);
            var nH = this.nH = Math.floor(height / sizeCell);
            var n = (this.nWx = nW+4)* (nH+4);
            this.nCleared = 0;
            this.aCells = [];
            var aAll = [];
            for (var i = 0; i < n; i++) {
                var pos = this.pos(i), x = pos[0], y = pos[1];
                this.aCells.push(x >= 0 && x < nW && y >= 0 && y < nH? 0 : CA_CLEAR);
                aAll.push(i);
            }
            fillCellArea(cfgMain.colorFill, 0, 0, nW, nH);
        },
        render: function() {
            if (this.aTrailRects.length) {
                for (var i = this.aTrailRects.length-1; i >= 0; i--) {
                    fillCellArea.apply(null, [cfgMain.colorFill].concat(this.aTrailRects[i]));
                }
                this.aTrailRects = [];
            }
        },
        isPosIn: function(x, y) {
            return x >= 0 && x < this.nW && y >= 0 && y < this.nH;
        },
        isPosValid: function(x, y) {
            return x >= -2 && x < this.nW+2 && y >= -2 && y < this.nH+2;
        },
        find: function(x, y) {
            return this.isPosValid(x, y) ? (this.nWx)*(y+2) + x+2 : -1;
        },
        pos: function(i) {
            return [i % this.nWx - 2, Math.floor(i / this.nWx)-2];
        },
        posMap: function(arr) {
            var _this = this;
            return arr.map(function(v) { return _this.pos(v) });
        },
        value: function(x, y) {
            var i = this.find(x,y);
            return i >= 0? this.aCells[i] : 0;
        },
        set: function(x, y, v) {
            var i = this.find(x,y);
            if (i >= 0) this.aCells[i] = v;
            return i;
        },
        setOn: function(x, y, v) {
            var i = this.find(x,y);
            if (i >= 0) this.aCells[i] |= v;
            return i;
        },
        setOff: function(x, y, v) {
            var i = this.find(x,y);
            if (i >= 0) this.aCells[i] &= ~v;
            return i;
        },
        placeCursor: function() {
            return [Math.floor(this.nW/2), -2];
        },
        placeBalls: function(n) {
            var a = [], ret = [];
            for (var i = 0; i < n; i++) {
                var k;
                do k = Math.floor(Math.random() * this.nW * this.nH);
                while (a.indexOf(k) >= 0);
                a.push(k);
                var x = k % this.nW, y = Math.floor(k / this.nW);
                ret.push([x, y]);
            }
            return ret;
        },
        placeWarders: function(n) {
            var z;
            var aPos = [
                [Math.floor(this.nW/2), this.nH+1],
                [-1, this.nH+1], [this.nW, this.nH+1], [-1, -2], [this.nW, -2],
                [-1, z = Math.floor(this.nH/2)], [this.nW, z],
                [z = Math.floor(this.nW/4), this.nH+1], [3*z, this.nH+1]
            ];
            var i0 = (n+ 1)% 2;
            return aPos.slice(i0, Math.min(n+ i0, 9));
        },
        placeSpawned: function() {
            if (nWarders >= 9) return false;
            function dist(pos1, pos2) {
                return Math.pow(pos1[0]- pos2[0], 2) + Math.pow(pos1[1]- pos2[1], 2);
            }
            function find(pos0) {
                var n = nWarders;
                for (var l = 0; l < x0; l++) {
                    for (var dx = -1; dx <= 1; dx+= 2) {
                        var p = [pos0[0]+ l* dx, pos0[1]];
                        for (var i = 0; i < n && dist(aWarders[i].pos(), p) >= 4; i++) ;
                        if (i >= n) return p;
                    }
                }
                return pos0;
            }
            var x0 = Math.floor(this.nW/2);
            var aPos = [[x0, this.nH+1], [x0, -2]];
            var posCr = cursor.pos();
            var posSt = dist(aPos[0], posCr) > dist(aPos[1], posCr)? aPos[0] : aPos[1];
            var ret = find(posSt);
            return ret;
        },
        applyRelDirs: function(x, y, dir, aDeltas) {
            var ret = [];
            for (var n = aDeltas.length, i = 0; i < n; i++) {
                var d = (dir + aDeltas[i] + 360) % 360;
                var vec = dirset.get(d), xt, yt;
                ret.push([xt = x + vec[0], yt = y + vec[1], d, this.value(xt, yt)]);
            }
            return ret;
        },
        add2Trail: function(x, y, dir) {
            var i = this.setOn(x, y, CA_TRAIL);
            if (i < 0) return;
            var n = this.aTrail.length;
            if (!n || dir !== this.dirTrail) {
                var iNode = n? this.aTrail[n-1] : i;
                if (!n || iNode != this.aTrailNodes[this.aTrailNodes.length-1])
                    this.aTrailNodes.push(iNode);
                if (!n) {
                    var aPos = this.applyRelDirs(x, y, dir, [180]);
                    this.iPreTrail = this.find(aPos[0][0], aPos[0][1]);
                }
            }
            this.aTrail.push(i);
            this.dirTrail = dir;
        },
        lastTrailLine: function() {
            var pos0 = this.pos(this.aTrailNodes[this.aTrailNodes.length-1]),
                pos = this.pos(this.aTrail[this.aTrail.length-1]);
            return [
                Math.min(pos[0], pos0[0]), Math.min(pos[1], pos0[1]),
                Math.abs(pos[0] - pos0[0])+1, Math.abs(pos[1] - pos0[1])+1
            ];
        },
        clearTrail: function() {
            this.aTrailRects = this._buildTrailRects();
            for (var n = this.aTrail.length, i = 0; i < n; i++) {
                this.aCells[this.aTrail[i]] &= ~CA_TRAIL;
            }
            this.aTrail = []; this.aTrailNodes = [];
        },
        getPreTrail: function() {
            return this.iPreTrail;
        },
        conquer: function() {
            var nTrail = this.aTrail.length;
            if (!nTrail) return;
            if (nTrail > 1)
                this.aTrailNodes.push(this.aTrail[nTrail-1]);
            var aConqRects = this._conquer() || this._buildTrailRects();
            this.aTrail = []; this.aTrailNodes = [];
            if (!aConqRects || !aConqRects.length) return;
            for (var n = aConqRects.length, i = 0; i < n; i++) {
                var rect = aConqRects[i];
                var x0 = rect[0], y0 = rect[1], w = rect[2], h = rect[3];
                for (var x = 0; x < w; x++) {
                    for (var y = 0; y < h; y++) {
                        if (this.value(x + x0, y + y0, CA_CLEAR) & CA_CLEAR) continue;
                        this.set(x + x0, y + y0, CA_CLEAR);
                        this.nCleared++;
                    }
                }
            }
            for (i = 0; i < n; i++) {
                clearCellArea.apply(null, aConqRects[i]);
            }
            aConqRects = [];
        },
        getPercentage: function() {
            return this.nCleared / (this.nW * this.nH) * 100;
        },
        _conquer: function() {
            var nTrail = this.aTrail.length, nNodes = this.aTrailNodes.length;
            var dz = Math.abs(this.aTrailNodes[0] - this.aTrailNodes[nNodes-1]);
            var aOutlineset = [], bClosedTrail = false;
            if (bClosedTrail = nNodes >= 4 && dz == 1 || dz == this.nWx) {
                aOutlineset.push([this.aTrailNodes, 1]);
            }
            var bAddTrail = false;
            var posPre = this.pos(this.iPreTrail), posCr = cursor.pos();
            var aDeltas = [-90, 90];
            for (var d = 0; d < 2; d++) {
                var dd = aDeltas[d];
                var k = 0;
                var sum = 0, bSum = false, bEndAtNode = false;
                for (var l = 0; l < nTrail && sum < nTrail; l++) {
                    var iStart = this.aTrail[l];
                    var pos = this.pos(iStart);
                    var pos0 = l? this.pos(this.aTrail[l - 1]) : posPre;
                    var x = pos[0], y = pos[1];
                    var dir = (dirset.find(x - pos0[0], y - pos0[1]) + dd + 360) % 360;
                    var aDirs = bEndAtNode? [] : [dir];
                    if (this.aTrailNodes.indexOf(iStart) >= 0) {
                        var pos2 = l < nTrail - 1? this.pos(this.aTrail[l + 1]) : posCr;
                        dir = (dirset.find(pos2[0] - x, pos2[1] - y) + dd + 360) % 360;
                        if (dir != aDirs[0]) aDirs.push(dir);
                    }
                    if (this.aTrail[l] == this.aTrailNodes[k+1]) ++k;
                    var ret = 0;
                    for (var nDs = aDirs.length, j = 0; j < nDs && !ret; j++) {
                        dir = aDirs[j];
                        var vec = dirset.get(dir);
                        var xt = x + vec[0], yt = y + vec[1];
                        var v = this.value(xt, yt);
                        if (v & CA_CLEAR || v & CA_TRAIL) continue;
                        ret = this._outline(xt, yt, dir);
                        if (!ret || ret.length < 3) return false;
                    }
                    bEndAtNode = false;
                    if (!ret) continue;
                    var len = ret[0], aNodes = ret[1], bClosed = ret[2], iEnd = aNodes[aNodes.length-1];
                    if (bClosed) {
                        aOutlineset.push([aNodes, len]);
                        bSum = true;
                        continue;
                    }
                    var aXtra = [iStart];
                    for (var i = l+1; i < nTrail && this.aTrail[i] != iEnd; i++) {
                        if (this.aTrail[i] == this.aTrailNodes[k+1])
                            aXtra.push(this.aTrailNodes[++k]);
                    }
                    if (i >= nTrail) continue;
                    aOutlineset.push([aNodes.concat(aXtra.reverse()), len + i - l]);
                    sum += i - l + 1;
                    l = (bEndAtNode = this.aTrail[i] == this.aTrailNodes[k+1])? i-1 : i;
                }
                if (!sum && !bSum && !bClosedTrail) return false;
                if (sum < nTrail && !bClosedTrail) bAddTrail = true;
            }
            if (!aOutlineset.length)
                return false;
            aOutlineset.sort(function (el1, el2) { return el1[1] - el2[1]; });
            var aRects = [], n = aOutlineset.length, b = false;
            for (i = 0; i < n; i++) {
                if (i == n- 1 && !b) break;
                ret = this._buildConquerRects(aOutlineset[i][0]);
                if (ret)
                    aRects = aRects.concat(ret);
                else
                    b = true;
            }
            if (!aRects.length)
                return false;
            return bAddTrail? aRects.concat(this._buildTrailRects()) : aRects;
        },
        _outline: function(x0, y0, dir) {
            var aNodes = [], aUniqNodes = [], aUsedDirs = [], aBackDirs = [];
            var x = x0, y = y0,
                lim = 6 * (this.nW + this.nH), n = 0, bClosed = false;
            function isClear(arr) {
                return arr[3] & CA_CLEAR;
            }
            do {
                bClosed = n && x == x0 && y == y0;
                var iCurr = this.find(x,y), iUniq = aUniqNodes.indexOf(iCurr);
                var aCurrUsed = iUniq >= 0? aUsedDirs[iUniq] : [];
                var aCurrBack = iUniq >= 0? aBackDirs[iUniq] : [];
                var aPosOpts = this.applyRelDirs(x,y, dir, [-90, 90, 0]);
                var aTestDirs = [180+45, -45, 45, 180-45, -45, 45];
                var aPassIdx = [], aPassWeight = [];
                for (var i = 0; i < 3; i++) {
                    var d = aPosOpts[i][2];
                    if (aCurrUsed.indexOf(d) >= 0) continue;
                    if (isClear(aPosOpts[i])) continue;
                    var aTestOpts = this.applyRelDirs(x,y, dir, aTestDirs.slice(i*2,i*2+2));
                    var b1 = isClear(aTestOpts[0]), b2 = isClear(aTestOpts[1]);
                    var b = b1 || b2 || (i == 2? isClear(aPosOpts[0]) || isClear(aPosOpts[1]) : isClear(aPosOpts[2]));
                    if (!b) continue;
                    aPassIdx.push(i);
                    aPassWeight.push(
                        (b1 && b2? 0 : b1 || b2? 1 : 2) + (aCurrBack.indexOf(d) >= 0? 3 : 0)
                    );
                }
                var nPass = aPassIdx.length;
                var min = false, idx = false;
                for (i = 0; i < nPass; i++) {
                    if (!i || aPassWeight[i] < min) {
                        min = aPassWeight[i]; idx = aPassIdx[i];
                    }
                }
                var pos = nPass? aPosOpts[idx] : this.applyRelDirs(x,y, dir, [180])[0];
                var dir0 = dir;
                x = pos[0]; y = pos[1]; dir = pos[2];
                if (pos[2] == dir0) continue;
                nPass? aNodes.push(iCurr) : aNodes.push(iCurr, iCurr);
                dir0 = (dir0 + 180) % 360;
                if (iUniq < 0) {
                    aUniqNodes.push(iCurr);
                    aUsedDirs.push([dir]);
                    aBackDirs.push([dir0]);
                }
                else {
                    aUsedDirs[iUniq].push(dir);
                    aBackDirs[iUniq].push(dir0);
                }
            }
            while (n++ < lim && !(this.value(x, y) & CA_TRAIL));
            if (!(n < lim)) return false;
            if (bClosed) {
                aNodes.push(iCurr);
                if (aNodes[0] != (iCurr = this.find(x0,y0))) aNodes.unshift(iCurr);
                var nNodes = aNodes.length;
                if (nNodes % 2 && aNodes[0] == aNodes[nNodes-1]) aNodes.pop();
            }
            else
                aNodes.push(this.find(x,y));
            return [n+1, aNodes, bClosed];
        },
        _buildTrailRects: function() {
            if (this.aTrailNodes.length == 1)
                this.aTrailNodes.push(this.aTrailNodes[0]);
            var aRects = [];
            for (var n = this.aTrailNodes.length, i = 0; i < n-1; i++) {
                var pos1 = this.pos(this.aTrailNodes[i]), pos2 = this.pos(this.aTrailNodes[i+1]);
                var x0 = Math.min(pos1[0], pos2[0]), y0 = Math.min(pos1[1], pos2[1]);
                var w = Math.max(pos1[0], pos2[0]) - x0 + 1, h = Math.max(pos1[1], pos2[1]) - y0 + 1;
                var rect = [x0, y0, w, h];
                aRects.push(rect);
            }
            return aRects;
        },
        _buildConquerRects: function(aOutline) {
            if (aOutline.length < 4) return false;
            var aNodes = this.posMap(aOutline);
            var n = aNodes.length;
            if (n > 4 && n % 2 != 0) {
                var b1 = aNodes[0][0] == aNodes[n-1][0], b2;
                if (b1 ^ aNodes[0][1] == aNodes[n-1][1]) {
                    b2 = aNodes[n-2][0] == aNodes[n-1][0];
                    if (!(b2 ^ b1) && b2 ^ aNodes[n-2][1] == aNodes[n-1][1])
                        aNodes.pop();
                    b2 = aNodes[0][0] == aNodes[1][0];
                    if (!(b2 ^ b1) && b2 ^ aNodes[0][1] == aNodes[1][1])
                        aNodes.shift();
                }
                b1 = aNodes[0][0] == aNodes[1][0]; b2 = aNodes[1][0] == aNodes[2][0];
                if (!(b1 ^ b2) && b1 ^ aNodes[0][1] == aNodes[1][1] && b2 ^ aNodes[1][1] == aNodes[2][1])
                    aNodes.shift();
            }
            if (aNodes.length % 2 != 0) return false;
            var aRects = [];
            for (var l = 0; l < 10 && aNodes.length > 4; l++) {
                n = aNodes.length;
                var dim1 = 0, dim2 = 0, iBase = 0, iCo = 0;
                var posB1, posB2, posT1, posT2;
                for (var i = 0; i < n; i++) {
                    posB1 = aNodes[i]; posB2 = aNodes[(i+1)%n];
                    posT1 = aNodes[(i-1+n)%n]; posT2 = aNodes[(i+2)%n];
                    var dir = dirset.find(posT1[0]-posB1[0], posT1[1]-posB1[1]);
                    if (dir != dirset.find(posT2[0]-posB2[0], posT2[1]-posB2[1])) continue;
                    var dirTest = Math.floor((dirset.find(posB2[0]-posB1[0], posB2[1]-posB1[1])+ dir) / 2);
                    var vec = dirset.get(dirTest - dirTest% 45);
                    if (this.value([posB1[0]+ vec[0], posB1[1]+ vec[1]]) & CA_CLEAR) continue;
                    var b = false, t, w, k;
                    if ((t = Math.abs(posB1[0]-posB2[0])) > dim1) {
                        b = true; k = 0; w = t;
                    }
                    if ((t = Math.abs(posB1[1]-posB2[1])) > dim1) {
                        b = true; k = 1; w = t;
                    }
                    if (!b) continue;
                    var k2 = (k+1)%2;
                    vec = dirset.get(dir);
                    var sgn = vec[k2];
                    var co2 = posB1[k2];
                    var left = Math.min(posB1[k], posB2[k]), right = Math.max(posB1[k], posB2[k]);
                    var min = Math.min(sgn* (posT1[k2]- co2), sgn* (posT2[k2]- co2));
                    for (var j = i% 2; j < n; j+= 2) {
                        if (j == i) continue;
                        var pos = aNodes[j], pos2 = aNodes[(j+1)%n], h;
                        if (pos[k2] == pos2[k2] && (h = sgn*(pos[k2]- co2)) >= 0 && h < min &&
                            pos[k] > left && pos[k] < right && pos2[k] > left && pos2[k] < right)
                            break;
                    }
                    if (j < n) continue;
                    dim1 = w; dim2 = sgn*min;
                    iBase = i; iCo = k;
                }
                var iB2 = (iBase+1)%n, iT1 = (iBase-1+n)%n, iT2 = (iBase+2)%n;
                posB1 = aNodes[iBase];
                posB2 = aNodes[iB2];
                posT1 = aNodes[iT1];
                posT2 = aNodes[iT2];
                var aDim = [0, 0], pos0 = [];
                var iCo2 = (iCo+1)%2;
                aDim[iCo] = dim1;
                aDim[iCo2] = dim2;
                pos0[iCo] = Math.min(posB1[iCo], posB2[iCo]);
                pos0[iCo2] = Math.min(posB1[iCo2], posB2[iCo2]) + (aDim[iCo2] < 0? aDim[iCo2]: 0);
                var rect = [pos0[0], pos0[1], Math.abs(aDim[0])+1, Math.abs(aDim[1])+1];
                var bC = Math.abs(posT1[iCo2] - posB1[iCo2]) == Math.abs(dim2);
                if (this._containBall(rect)) return false;
                aRects.push(rect);
                if (bC) {
                    posB2[iCo2] += dim2;
                    aNodes.splice(iBase,1);
                    aNodes.splice(iT1 < iBase? iT1 : iT1-1, 1);
                }
                else {
                    posB1[iCo2] += dim2;
                    aNodes.splice(iT2,1);
                    aNodes.splice(iB2 < iT2? iB2 : iB2-1, 1);
                }
            }
            var aX = aNodes.map(function(v) {return v[0]});
            var aY = aNodes.map(function(v) {return v[1]});
            var x0 = Math.min.apply(null, aX);
            var y0 = Math.min.apply(null, aY);
            rect = [x0, y0, Math.max.apply(null, aX)-x0+1, Math.max.apply(null, aY)-y0+1];
            if (this._containBall(rect)) return false;
            aRects.push(rect);
            return aRects;
        },
        // проверяем, содержит ли прямоуг. область морскую точку:
        _containBall: function(rect) {
            var x1 = rect[0], x2 = x1+ rect[2] - 1;
            var y1 = rect[1], y2 = y1+ rect[3] - 1;
            for (var i = 0; i < nBalls; i++) {
                var o = aBalls[i], x = o.x, y = o.y;
                if (x >= x1 && x <= x2 && y >= y1 && y <= y2) return true;
            }
            return false;
        }
    };

    // Курсор:
    cursor = {
        x: 0, // текущая x координата
        y: 0, // текущая y координата
        x0: 0, // предыдущая x координата
        y0: 0, // предыдущая y координата
        dir: false, // текущий угол движения (в градусах)
        state: false, // текущий режим курсора (true - режим следа)
        state0: false, // предыдущий режим курсора
        // сброс позиции курсора:
        reset: function(x, y, bUnlock) {
            var bPre = bUnlock && cellset.value(this.x, this.y) & CA_CLEAR;
            this.x0 = bPre? this.x : x;
            this.y0 = bPre? this.y : y;
            this.x = x;
            this.y = y;
            this.dir = this.state = this.state0 = false;
        },
        // обновление позиции - перемещение на заданное расстояние:
        update: function(dist) {
            if (this.dir === false) return;
            var x = this.x, y = this.y;
            var vec = dirset.get(this.dir), vecX = vec[0], vecY = vec[1];
            var bEnd =  false;
            for (var n = 0; n < dist; n++) {
                if (cellset.find(x + vecX, y + vecY) < 0) {
                    this.dir = false; break;
                }
                x += vecX; y += vecY;
                if (cellset.value(x, y) & CA_TRAIL) {
                    bCollision = true; break;
                }
                var b = cellset.value(x, y) & CA_CLEAR;
                if (this.state && b) {
                    bEnd = true; break;
                }
                this.state = !b;
                if (this.state) cellset.add2Trail(x, y, this.dir);
            }
            this.x = x;
            this.y = y;
            if (!bEnd) return;
            if (cellset.getPreTrail() == cellset.find(x,y))
                bCollision = true;
            else {
                this.dir = this.state = false;
                bConquer = true;
            }
        },
        // рендеринг текущей позиции:
        render: function() {
            if (this.x0 == this.x && this.y0 == this.y) {
                if (tLastFrame) return;
            }
            else {
                if (this.state0) {
                    var rect = cellset.lastTrailLine();
                    fillCellArea.apply(null, [cfgMain.colorTrail].concat(rect));
                }
                else {
                    if (cellset.isPosIn(this.x0, this.y0))
                        clearCellArea(this.x0, this.y0);
                    else
                        fillCellArea(cfgMain.colorBorder, this.x0, this.y0);
                }
                this.x0 = this.x; this.y0 = this.y;
            }
            this.state0 = this.state;
            drawCellImg(imgCursor, this.x, this.y);
        },
        // получить текущую позицию:
        pos: function() {
            return [this.x, this.y];
        },
        // получить текущий угол движения:
        getDir: function() {
            return this.dir;
        },
        // изменить угол движения:
        setDir: function(dir) {
            if (dir === this.dir) return;
            if (this.state && this.dir !== false && Math.abs(dir - this.dir) == 180)
                return;
            this.dir = dir;
        }
    };

    // Конструктор класса точки (морской и сухопутной):
    function Enemy(x, y, type, dir) {
        this.x = x;
        this.y = y;
        this.x0 = x;
        this.y0 = y;
        var aDirs = [45, 135, 225, 315];
        this.dir = dir === undefined? aDirs[Math.floor(Math.random()*4)] : dir; // текущий угол движения
        this.type = Boolean(type); // (boolean) тип точки (false - морская, true - сухопутная)
    }
    // Методы класса точки:
    Enemy.prototype = {
        // сброс позиции:
        reset: function(x, y) {
            this.x = x;
            this.y = y;
        },
        // обновление позиции - перемещение на заданное расстояние:
        update: function(dist) {
            var ret = this._calcPath(this.x, this.y, dist, this.dir);
            this.x = ret.x;
            this.y = ret.y;
            this.dir = ret.dir;
        },
        // рендеринг текущей позиции:
        render: function() {
            if (this.x0 == this.x && this.y0 == this.y) {
                if (tLastFrame) return;
            }
            else {
                if (this.type && cellset.isPosIn(this.x0, this.y0))
                    clearCellArea(this.x0, this.y0);
                else
                    fillCellArea(this.type? cfgMain.colorBorder : cfgMain.colorFill, this.x0, this.y0);
                this.x0 = this.x; this.y0 = this.y;
            }
            drawCellImg(this.type? imgWarder : imgBall, this.x, this.y);
        },
        // получить текущую позицию:
        pos: function() {
            return [this.x, this.y];
        },
        // вычислить путь движения (перемещения):
        _calcPath: function(x, y, dist, dir) {
            var vec = dirset.get(dir), vecX = vec[0], vecY = vec[1];
            var posCr = cursor.pos();
            var xC = posCr[0], yC = posCr[1],
                vC = cellset.value(xC, yC), bC = !this.type ^ vC & CA_CLEAR;
            if (bC && Math.abs(x - xC) <= 1 && Math.abs(y - yC) <= 1 ||
                !this.type && this._isCollision(x, y, dir)) {
                bCollision = true;
            }
            for (var n = 0; n < dist && !bCollision; n++) {
                var xt = x + vecX, yt = y + vecY;
                var dirB = this._calcBounce(x, y, dir, xt, yt);
                if (dirB !== false)
                    return this._calcPath(x, y, dist - n, dirB);
                if (bC && Math.abs(xt - xC) <= 1 && Math.abs(yt - yC) <= 1 ||
                    !this.type && this._isCollision(xt, yt, dir))
                    bCollision = true;
                if (!this.type && !cellset.isPosIn(xt, yt))
                    break;
                x = xt; y = yt;
            }
            return {x: x, y: y, dir: dir};
        },
        // вычислить отскок точки от границы поля (если есть):
        _calcBounce: function(x, y, dir, xt, yt) {
            var ret = cellset.applyRelDirs(x,y, dir, [-45, 45]);
            var b1 = this.type ^ ret[0][3] & CA_CLEAR,
                b2 = this.type ^ ret[1][3] & CA_CLEAR;
            return b1 ^ b2?
                (b1? dir + 90 : dir + 270) % 360 :
                this.type ^ cellset.value(xt, yt) & CA_CLEAR || b1 && b2?
                    (dir+180) % 360 : false;
        },
        // проверить столкновение точки с курсором:
        _isCollision: function(x, y, dir) {
            if (cellset.value(x, y) & CA_TRAIL) return true;
            var aDirs = [-45, 45, -90, 90];
            for (var i = 0; i < 4; i++) {
                var d = (dir + aDirs[i] + 360) % 360, vec = dirset.get(d);
                if (cellset.value(x + vec[0], y + vec[1]) & CA_TRAIL) return true;
            }
            return false;
        }
    };
    

    function merge(dest, src, bFilter) {
        if (!src) return dest;
        for(var key in dest) {
            if (!dest.hasOwnProperty(key) || !src.hasOwnProperty(key)) continue;
            var v = src[key];
            if ((!bFilter || v) && (typeof v != 'number' || v >= 0))
                dest[key] = v;
        }
        return dest;
    }

})();

© Habrahabr.ru