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

Xonix — популярна гра часів DOS, клон відеоігри Qix.

На Javascript Xonix вже був портований кілька разів. Найкраща і найбільш наближена до оригіналу з існуючих реалізацій на сьогоднішній день, мабуть, ось ця. Її я спочатку намагався пристосувати для своєї реалізації/модифікації… Але, на жаль, код навіть після деобфускации так і не став зрозумілим (принаймні для мене). До того ж, наскільки я зміг зрозуміти, код там місцями не зовсім ефективний, або зовсім застарілий. Так що довелося все писати з нуля.

В результаті у мене вийшов ось такий «свій» Xonix, з картинками і відповідями.



Демо | Джерело

Код вийшов досить об'ємним, тому тут я буду пояснювати не всі, а тільки найбільш важливі (з моєї точки зору) моменти.

Як відомо, ігрове поле Xonix являє собою сітку з квадратних комірок. На початку гри (рівня) більшу частину поля займає прямокутна чорна область («море»), яку оточує з усіх боків світла рамка («суша»).

Основна відмінність моєї реалізації від класичного Ксоникса це те, що за чорною областю ховається картинка, унікальна для кожного рівня. Цим вона схожа на іншу модифікацію Ксоникса — Sexonix. Але в моїй картинка з'являється не просто так. Це частина питання, на яке слід дати відповідь. А вся гра таким чином перетворюється в вікторину.

Розміри картинки визначають розміри чорної області, які повинні бути кратними розміру комірки. У загальному випадку картинки для різних рівнів гри можуть мати різні розміри, тому розміри чорної області можуть змінюватися від рівня до рівня, на відміну від світлої рамки, яка на початку рівня завжди має фіксовану ширину в 2 комірки.

Рух усіх об'єктів (курсору точок) відбувається строго по осередках, так що в кожен момент часу кожен об'єкт займає рівно одну клітинку. Така структура ігрового поля значно спрощує реалізацію гри. Складність представляє лише визначення «завойованих» областей, що утворюються при перетині курсором «моря», але про це пізніше.

Рух об'єктів
Той факт, що ми можемо прийняти клітинку «піксель», рятує від більшості проблем з обчисленнями, які зазвичай зустрічаються в іграх з багатьма рухомими об'єктами: розрахунок руху, відскоків і зіткнень і т. п.

У Xonix у будь-якого об'єкта є всього 4 варіанти напрямку руху: для курсору вгору/вниз/вправо/вліво, для точки (обох типів) — то ж саме, тільки по діагоналі. Об'єднуємо ці варіанти в безліч можливих напрямків, які будемо задавати в градусах. Отримуємо 8 кутів руху: від 0 до 315 градусів з кроком 45. Кожному значенню кута ставимо у відповідність пару координат вектора напрямку руху. В результаті отримуємо таку структуру, яку будемо використовувати при розрахунку руху:

Фрагмент коду
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;
}
};

Метод
get
повертає для даного кута (в градусах) відповідний вектор руху. Метод
find
робить протилежне: для даного вектора руху (не обов'язково одиничного) повертає відповідний кут у градусах, або найближчий до нього з доступних.

Зіткнення точок одне з одним можна ігнорувати, просто дозволяємо їм «проходити» крізь один одного. Оскільки всі точки (одного типу) виглядають однаково, то з боку це нічим не буде відрізнятися від зіткнення і відскоку.

Для розрахунку зіткнень точок з курсором, відскоку точок від кордону «своїй» області і деяких інших речей нам знадобиться матриця станів всіх осередків — двовимірний масив
(n+4) * (m+4)
,
де
(n+4)
,
(m+4)
— відповідно ширина і висота ігрового поля в комірках, а перший елемент матриці відповідає клітинку в лівому верхньому куті ігрового поля.

Кожен елемент буде зберігати стан відповідної клітинки, що включає в себе 2 ознаки: тип галузі, до якої вона належить (море/суша), а також те, проходить по ній в даний момент «слід» від руху курсору по «морю». Зберігати цей стан буде в бітовому поле з двох бітів. Для цього нам потрібно оголосити дві константи-маски для кожної ознаки відповідно:

var CA_CLEAR = 1 << 0; // осередок очищена, тобто належить до суші
var CA_TRAIL = 1 << 1; // комірка - частина сліду руху курсору по "моря"

В цілях оптимізації масив станів зробимо не двовимірним, а одновимірним, так що всі елементи будуть зберігатися порядково, починаючи з першої (верхній) рядка. Для перетворення координат клітинки індекс масиву і зворотного перетворення будемо використовувати такі формули:

i = n * (y + 2) + x + 2;
x = i % n - 2;
y = Math.floor(i / n) - 2

Для розрахунку відскоку точки («морської» або «сухопутного») від кордону «своїй» області нам потрібно знати стан трьох клітинок, не рахуючи осередку самої точки. Нижче з допомогою псевдографіки показано положення цих осередків щодо поточної комірки точки при куті руху в 45 градусів.

OOO
OX1
O23

Осередок точки відзначена хрестиком, а шукані клітинки цифрами 1, 2, 3. Нулики просто зображують сусідні клітинки. Напрямок руху точки в даному випадку виходить південно-східний, оскільки вісь ординат (Y) сітки у нас спрямована вниз.

Відскік від кордону має місце, якщо хоча б в одній із зазначених трьох осередків тип області протилежним типом точки. Тобто якщо, наприклад, точка «морська», то одна з цих клітинок повинна бути «сухопутного». При цьому, якщо ця умова виконується тільки в одній з комірок 1 або 2 (але не обидва), то до куті руху додається відповідно
+90
або
-90
градусів. В іншому випадку кут руху змінюється на протилежний (
+180
градусів).

При будь-якому іншому куті руху логіка відскоку очевидно буде точно такий же.

Зіткнення точки з курсором призводить до припинення гри, після чого курсор й «сухопутні» точки повертаються у вихідні для початку гри позиції: курсор — в середню клітинки в другому рядку, «сухопутні» точки в залежності від їх кількості — в нижній і бічних частинах рамки. Положення «морських» точок не змінюється.

Визначення зіткнення «сухопутного» точки з курсором тривіально. Просто перевіряємо контакт точки з курсором, порівнюючи координати клітинок точки і курсору. Визначення зіткнення «морський» точки з курсором трохи складніше: нам треба перевірити не тільки контакт самої точки з курсором, але і дотик сліду руху курсору. Для цього використовуємо другий біт стану осередків: перевіряємо його у всіх сусідніх з точкою осередків.

Визначення «завойованих» областей
Як вже згадувалося, найскладніше в реалізації — визначення «завойованих» у «моря» областей. Це замкнуті області, що утворюються в результаті перетину курсором «моря», всередині яких немає «морських» точок. У більшості випадків при цьому утворюються дві замкнуті області, одержувані розподілом (слідом руху курсору) доступною «морський» області на дві частини, з яких «завойованою» стає лише одна або жодній (див. скріншот 1). Але в деяких, особливо складних випадках може утворитися відразу безліч замкнутих (див. скріншот 2) і в тому числі «завойованих» областей. Крім того, можлива ситуація, коли слід курсору сам по собі утворює замкнуту область (див. скріншот 3).

Скріншот 1

Скріншот 2

Скріншот 3

Отже, нам потрібно знайти всі такі замкнуті області, після чого визначити тип кожної з них («завойована»/«незавоеванная»).

Для знаходження замкнутих областей можна перебирати всі клітинки, використовуючи який-небудь загальний алгоритм порівняння сусідніх комірок. Наприклад, як це реалізовано тут.

Але є й інший спосіб (який я в підсумку і вибрав): використовувати контури замкнутих областей, що утворюються при перетині курсором «моря». Під контуром мається на увазі замкнена ламана товщиною в одну комірку. Якщо відомий контур замкнутій області, то залишається тільки знайти її вміст, тобто всі комірки всередині неї. Але як знайти ці контури?

Контури замкнутих областей

У загальному випадку про кожному вихідному контурі нам відомо тільки те, що він має часткове перетинання зі слідом руху курсору, тобто містить як мінімум одну клітинку сліду курсору. У більшості випадків, як показано на скріншоті 1, утворюється рівно два контури, кожний з яких містить весь слід курсору (всі його осередки). Але в окремих випадках, як на скріншоті 2, таких контурів може бути безліч, причому багато з них можуть містити лише частина клітинок сліду курсору. Крім того, слід враховувати випадки, коли весь слід курсору сам по собі утворює замкнутий контур (скріншот 3). Таким чином потрібно, виходячи з даних про сліді курсору, отримати всю інформацію про прилеглих до нього контурах замкнутих областей, перевіряючи при цьому якомога менше клітинок.

Розглядаючи різні варіанти розподілу контурів, можна виявити кілька закономірностей. По-перше, число контурів, що містять весь слід курсору цілком, завжди не більше 2. По-друге, якщо розбити всі контури на 2 групи по різні сторони від сліду курсору, то контури однієї групи не будуть мати спільних осередків сліду. Іншими словами, кожна клітинка сліду курсору належить тільки одному контуру з кожної сторони від сліду.

На основі перерахованого можна вивести алгоритм знаходження контурів. У загальних словах він виглядає так.

Перебираємо в циклі всі комірки, що належать сліду руху курсору, починаючи з першої (тобто з початку руху). Для кожної клітинки сліду перевіряємо по обидва боки від неї дві сусідні клітинки зліва і справа по напряму руху). Якщо осередок «морська», то це частина контуру однієї з шуканих областей. В іншому випадку продовжуємо перебір до тих пір, поки не знайдемо «морську» клітинку з даної сторони від сліду. Із знайденої клітинки можна витягнути весь примикає до неї контур. Для цього потрібно йти від цієї клітинки спочатку в напрямку від осередку сліду, а потім у такому напрямку, щоб хоча б з одного боку від поточної клітинки завжди була «сухопутна» клітинка (при цьому поточна комірка повинна бути «морський»). І так до тих пір, поки не зустрінеться осередок сліду, що означає, що контур замкнулося. Додаємо до пройденим осередкам частина клітинок сліду, необхідних для замикання ламаної, і отримуємо шуканий контур.

Після цього продовжуємо зовнішній цикл перебору осередків сліду, починаючи з тієї, на якій зупинилася описана вище процедура знаходження контуру з однієї комірки.

Необхідно відзначити, що процедури знаходження контурів з обох сторін від сліду повинні виконуватися незалежно один від одного. Це означає, що цикл перебору осередків сліду потрібно обернути в ще один цикл з 2-х ітерацій — по одній на кожну сторону.

Можлива ситуація, коли з якої-небудь із сторін не буде знайдено жодного контуру. Це означає, що слід руху прилягає впритул до кордону «морський» області. В такому разі ми будемо мати єдину замкнуту область складається тільки з самого сліду.

При наявності ситуації з скріншота 3 до всіх знайдених контурах необхідно додати контур, утворений усім слідом руху курсору.

Вміст і тип замкнутої області

Тепер, коли контури замкнутих областей знайдені, необхідно по кожному контуру визначити вміст відповідної області (всі містяться в ній осередки) і її тип («завойована» чи ні). Оскільки в Xonix курсор може рухатися тільки вертикально/горизонтально, то кожна замкнута область може бути розбита на декілька прямокутників різних розмірів. Таким чином задача визначення вмісту замкнутої області зводиться до знаходження складових її прямокутників. До речі, цим самим ми «вбиваємо» відразу двох «зайців»: полегшуємо підрахунок точок всередині замкнутих областей, а також зафарбовування (вірніше стирання) «завойованих» областей.

Для знаходження прямокутників достатньо знати вершини контуру області, тобто вершини многокутника.

Основна ідея полягає в тому, щоб на кожній ітерації відсікати від області виступаючий прямокутник з найбільшою шириною або висотою. Під виступаючим тут розуміється прямокутник, три вершини якого належать вихідного многоугольнику. На основі цього можна вивести алгоритм розбиття замкнутої області на прямокутники. Він виглядає наступним чином.

На першій ітерації знаходимо бік (відрізок) багатокутника з найбільшою довжиною, яка є частиною виступає прямокутника. Якщо таких кілька відрізків, вибираємо будь-який з них. Знайдений відрізок буде однією із сторін шуканого прямокутника. Тепер нам потрібно знайти інші його сторони. Для цього беремо відрізки, що виходять з обох кінців знайденої боку перпендикулярно до неї, і вибираємо найкоротший з них. Це буде друга сторона прямокутника. Щоб знайти третю сторону, потрібно знайти (ортогональну) проекцію другого кінця цього відрізка на другий з перпендикулярних до першої стороні відрізків. З'єднуємо знайдену точку з відповідним кінцем першої сторони і отримуємо третю сторону. Звідси отримуємо весь шуканий виступаючий прямокутник. Тепер нам потрібно відсікти його від вихідного багатокутника. Для цього потрібно видалити з багатокутника перший і другий знайдені відрізки і відповідні їм вершини, після чого додати знайдену точку-проекцію, з'єднавши її з вершиною, яка раніше з'єднувалася з другим знайденим відрізком. В результаті отримуємо багатокутник, має дві вершини менше, ніж вихідний.

На наступній ітерації будемо працювати з урізаним гратки за тією ж схемою, що і на першій ітерації… І так до тих пір, поки не отримаємо в результаті відсікання багатокутник з 4 вершин, тобто прямокутник.

Всі відтяті у розглянутому процесі прямокутники і будуть становити вміст замкнутої області.

Розглянемо процес розбиття на прямокутники на конкретному прикладі.
Нехай ми маємо багатокутник
ABCDEFGHIJKL
(див. рис. 1), що представляє собою контур області. Застосуємо покроково описаний алгоритм розбиття.

1. Знаходимо сторону багатокутника
ABCDEFGHIJKL
з найбільшою довжиною. Це відрізок
CD
з довжиною 4. Але він нам не підходить, оскільки не є частиною виступає прямокутника. Тому ігноруємо його і шукаємо далі. Знаходимо 3 відрізка з довжиною 3:
AL
,
FG
,
GH
.
GH
нам не підходить по тій же причині, що і
CD
. Так що залишаються відрізки
AL
,
FG
. Вибираємо будь-який з них. Нехай це буде
AL
. Перпендикулярні до нього відрізки —
AB
та
KL
, з них самий короткий —
AB
. Знаходимо проекцію точки
B
на відрізок
KL
— це точка
M
(див. рис. 2). Таким чином отримуємо отсекаемый прямокутник —
ABML
. Після його відсікання залишається багатокутник
CDEFGHIJKM
.

2. Знаходимо сторону багатокутника
CDEFGHIJKM
з найбільшою довжиною. Це відрізок
FG
з довжиною 3… Отсекаемый прямокутник —
FGNE
(див. рис. 2). Після його відсікання залишається багатокутник
CDNHIJKM
.

3. Знаходимо сторону багатокутника
CDNHIJKM
з найбільшою довжиною. Це вже знайомий нам відрізок
CD
з довжиною 4… Отсекаемый прямокутник —
CDNO
. Після його відсікання залишається багатокутник
OHIJKM
.

4. Знаходимо сторону багатокутника
OHIJKM
з найбільшою довжиною. Таких сторін два. Це відрізки
OH
та
HI
з довжиною 2. Вибираємо перший з них —
OH
… Отсекаемый прямокутник —
OHPM
. Після його відсікання залишається прямокутник
KPIJ
. Тепер відсікати вже нічого. Так що на цьому алгоритм завершується.

В результаті ми отримуємо 5 прямокутників, що складають вміст замкнутої області:
ABML
,
FGNE
,
CDNO
,
OHPM
та
KPIJ
(див. рис. 2).

Рис. 1

Рис. 2

Після того як замкнуті області знайдені, необхідно визначити тип кожної з них (чи є вона «завойованою» чи ні). Тип області визначається шляхом підрахунку «морських» точок всередині неї. Не обов'язково вважати всі точки всередині області, достатньо лише дізнатися чи є там хоча б одна точка. Якщо є, то ця область не «завойована» (і відповідно ми її не стираємо), тому як в «завойованою» області не повинно бути ні однієї точки.

Визначити, чи містить багатокутник довільної форми точку з заданої позицією (координатами) — в загальному випадку досить непросто. Але ми для того і розбивали багатокутник на прямокутники, щоб полегшити в тому числі цю задачу. На відміну від довільного багатокутника, визначення приналежності точки прямокутника — тривіальна задача. Потрібно лише перевірити належність кожної координати даної точки відповідного діапазону кордонів прямокутника.

Таким чином задача визначення типу замкнутої області зводиться до пошуку «морських» точок всередині кожного з прямокутників, що складають дану область.

Всі знайдені «завойовані» області підлягають стирання, яке реалізується ще тривиальнее: просто перемо (методом clearRect) всі прямокутники, що становлять дану область.

Анімація, управління грою та інше
Заголовок трохи обманює). Стаття вже підійшла до кінця, так що нічого з перерахованого тут на жаль не буде. Можу хіба що помітити, що код анімації я писав на основі цієї статті переклад, а також цієї переклад).

Для тих, кому це цікаво, нижче наведено весь код гри. Втім, цінність його сумнівна, оскільки коментарів там — кіт наплакав. Але основну логіку я, сподіваюся, пояснив.

Код гри
// 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 = {
ширина: 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;
}

})();

За сім закругляюсь. Дякую за увагу.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.