Xonix на Javascript с картинками31.07.2015 19:18
// 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