2023-12-17 07:55:25 +03:00
|
|
|
|
// © Головин Г.Г., Визуализация игрового процесса, 2023
|
|
|
|
|
'use strict';
|
|
|
|
|
// размер клеточки, отступ
|
|
|
|
|
const size = 30, gap = 2;
|
|
|
|
|
// массив статусов игры
|
|
|
|
|
const statuses = ["ИГРА","УРОВЕНЬ","ПАУЗА","ЗАВЕРШЕНО"];
|
|
|
|
|
// массив цветов для фигур
|
|
|
|
|
const colors = [
|
|
|
|
|
'rgba(240,240,240,1)',
|
|
|
|
|
'rgba(150,0,0,1)',
|
|
|
|
|
'rgba(0,0,150,1)',
|
|
|
|
|
'rgba(150,150,0,1)',
|
|
|
|
|
'rgba(0,150,150,1)',
|
|
|
|
|
'rgba(0,150,0,1)',
|
|
|
|
|
'rgba(150,0,150,1)',
|
|
|
|
|
'rgba(150,80,80,1)'];
|
|
|
|
|
// прозрачность фигур
|
|
|
|
|
colors.setDefault = function() {
|
|
|
|
|
this.alpha = 0;
|
|
|
|
|
}
|
|
|
|
|
colors.setDefault();
|
|
|
|
|
// получаем цвет, добавляем прозрачность только фигурам
|
|
|
|
|
colors.get = function(...numbers) {
|
|
|
|
|
for (let num of numbers)
|
|
|
|
|
if (num <= 0) return this[0];
|
|
|
|
|
else if (num < colors.length)
|
|
|
|
|
return this[num].replace('1)', (100-this.alpha)/100 + ')');
|
|
|
|
|
return this[0];
|
|
|
|
|
}
|
|
|
|
|
// массив для кубиков
|
|
|
|
|
let field3D = [];
|
|
|
|
|
// центральная точка для поворотов
|
|
|
|
|
const t0 = {}; t0.reCalc = function() {
|
|
|
|
|
this.x = columns*(size+gap)/2;
|
|
|
|
|
this.y = rows*(size+gap);
|
|
|
|
|
this.z = (size+gap)*2;
|
|
|
|
|
};
|
|
|
|
|
t0.reCalc();
|
|
|
|
|
// угол поворота игрового поля с кубиками
|
2023-12-31 00:01:26 +03:00
|
|
|
|
let deg = {}; deg.setDefault = function() {
|
2023-12-17 07:55:25 +03:00
|
|
|
|
this.x=-1;
|
|
|
|
|
this.y=0;
|
|
|
|
|
this.z=0;
|
|
|
|
|
};
|
|
|
|
|
deg.setDefault();
|
|
|
|
|
// параллельная проекция: центр и экран наблюдателя
|
2023-12-31 00:01:26 +03:00
|
|
|
|
let d1, tv1 = {}; tv1.reCalc = function() {
|
2023-12-17 07:55:25 +03:00
|
|
|
|
this.x=columns*(size+gap)/2;
|
|
|
|
|
this.y=(size+gap)*2;
|
|
|
|
|
this.z=(size+gap)*2;
|
|
|
|
|
d1 = Math.max(rows,columns)*(size+gap);
|
|
|
|
|
}
|
|
|
|
|
tv1.reCalc();
|
|
|
|
|
// перспективная проекция: центр и экран наблюдателя
|
2023-12-31 00:01:26 +03:00
|
|
|
|
let d2, d2max, tv2 = {}; tv2.reCalc = function() {
|
2023-12-17 07:55:25 +03:00
|
|
|
|
this.x = columns*(size+gap)/2;
|
|
|
|
|
this.y = rows*(size+gap)/2;
|
|
|
|
|
this.z = (size+gap)*2;
|
|
|
|
|
this.show=false;
|
2023-12-31 00:01:26 +03:00
|
|
|
|
d2 = Math.max(rows,columns)*(size+gap); d2max = d2*2;
|
2023-12-17 07:55:25 +03:00
|
|
|
|
};
|
|
|
|
|
tv2.reCalc();
|
|
|
|
|
// стакан с игрой
|
|
|
|
|
const container = {};
|
|
|
|
|
container.canvas = document.getElementById('container');
|
|
|
|
|
container.context = container.canvas.getContext('2d');
|
|
|
|
|
container.changeSize = function() {
|
|
|
|
|
this.canvas.width = columns*size+(columns+1)*gap;
|
|
|
|
|
this.canvas.height = rows*size+(rows+1)*gap;
|
|
|
|
|
};
|
|
|
|
|
container.changeSize();
|
|
|
|
|
// перетаскивание центральной точки мышью
|
|
|
|
|
container.msBtnPressed = false;
|
|
|
|
|
container.canvas.onmouseup = ()=> container.msBtnPressed=false;
|
|
|
|
|
container.canvas.onmousedown = (caller)=> {
|
|
|
|
|
container.msBtnPressed=true;
|
|
|
|
|
container.canvas.onmousemove(caller);
|
|
|
|
|
}
|
|
|
|
|
container.canvas.onmousemove = (caller)=> {
|
|
|
|
|
if (vol==VOLUME.PERSPECTIVE && container.msBtnPressed && tv2.show) {
|
|
|
|
|
tv2.x = caller.offsetX;
|
|
|
|
|
tv2.y = caller.offsetY;
|
|
|
|
|
repaint(false);
|
|
|
|
|
refreshParams();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// следующая фигура
|
|
|
|
|
const next = {};
|
|
|
|
|
next.canvas = document.getElementById('next');
|
|
|
|
|
next.context = next.canvas.getContext('2d');
|
|
|
|
|
next.changeSize = function() {
|
|
|
|
|
this.canvas.width = 2*size+3*gap;
|
|
|
|
|
this.canvas.height = 4*size+5*gap;
|
|
|
|
|
};
|
|
|
|
|
next.changeSize();
|
|
|
|
|
// состояние игры
|
|
|
|
|
const statusView = {};
|
|
|
|
|
statusView.level = document.getElementById('levelView');
|
|
|
|
|
statusView.score = document.getElementById('scoreView');
|
|
|
|
|
statusView.nextLevel = document.getElementById('nextLevelView');
|
|
|
|
|
statusView.speed = document.getElementById('speedometer')
|
|
|
|
|
statusView.status = document.getElementById('statusView');
|
|
|
|
|
statusView.refresh = function() {
|
|
|
|
|
this.level.innerText = level;
|
|
|
|
|
this.score.innerText = score;
|
|
|
|
|
this.nextLevel.innerText = nextLevel;
|
|
|
|
|
this.speed.value = speedometer();
|
|
|
|
|
this.status.innerText = statuses[status];
|
|
|
|
|
}
|
|
|
|
|
// обновление изображения
|
|
|
|
|
function repaint(refresh3D=true) {
|
|
|
|
|
// состояние игры
|
|
|
|
|
statusView.refresh();
|
|
|
|
|
// стакан с игрой
|
|
|
|
|
if (vol==VOLUME.FLAT) {
|
|
|
|
|
drawCells(container.canvas, container.context, field, currentFigure.type);
|
|
|
|
|
} else {
|
|
|
|
|
if (refresh3D || field3D.length==0)
|
|
|
|
|
prepare3D();
|
|
|
|
|
repaint3D();
|
|
|
|
|
}
|
|
|
|
|
// следующая фигура
|
|
|
|
|
drawCells(next.canvas, next.context, nextFigure.shape);
|
|
|
|
|
}
|
|
|
|
|
// рисуем массив клеточек
|
|
|
|
|
function drawCells(canvas, context, array, color) {
|
|
|
|
|
// очищаем весь холст целиком
|
|
|
|
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
|
// обходим массив, рисуем клеточки
|
|
|
|
|
for (let y=0; y<array.length; y++)
|
|
|
|
|
for (let x=0; x<array[y].length; x++) {
|
|
|
|
|
context.fillStyle = colors.get(array[y][x], color);
|
|
|
|
|
context.fillRect(gap+x*(size+gap), gap+y*(size+gap), size, size);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// обходим поле, создаём кубики
|
|
|
|
|
function prepare3D() {
|
|
|
|
|
field3D = [];
|
|
|
|
|
for (let y=0; y<field.length; y++)
|
|
|
|
|
for (let x=0; x<field[y].length; x++) {
|
|
|
|
|
let cube = new Cube(gap+x*(size+gap),gap+y*(size+gap),size+gap,size+gap);
|
|
|
|
|
// в пустых кубиках оставляем только заднюю стенку
|
|
|
|
|
if (field[y][x] == 0) {
|
|
|
|
|
for (let i=0;i<3;i++) cube.faces.shift();
|
|
|
|
|
for (let i=0;i<2;i++) cube.faces.pop();
|
|
|
|
|
}
|
|
|
|
|
cube.rotate(deg, t0);
|
|
|
|
|
cube.color = field[y][x] < 11 ? field[y][x] : currentFigure.type;
|
|
|
|
|
field3D.push(cube);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// рисуем массив кубиков
|
|
|
|
|
function repaint3D() {
|
|
|
|
|
// проекции граней кубиков
|
|
|
|
|
let proj = [];
|
|
|
|
|
// получаем массив проекций
|
|
|
|
|
for (let cube of field3D) {
|
|
|
|
|
let cProj;
|
|
|
|
|
if (vol==VOLUME.PARALLEL)
|
|
|
|
|
cProj = cube.projection('parallel',tv1,d1);
|
|
|
|
|
else
|
|
|
|
|
cProj = cube.projection('perspective',tv2,d2);
|
|
|
|
|
for (let face of cProj)
|
|
|
|
|
face.color = cube.color;
|
|
|
|
|
proj = proj.concat(cProj);
|
|
|
|
|
}
|
|
|
|
|
// сортируем грани по удалённости от центра проекции
|
|
|
|
|
proj.sort((a,b) => b.dist-a.dist);
|
|
|
|
|
// удаляем смежные стенки между соседними кубиками
|
|
|
|
|
for (let i=0, j=1; i<proj.length-1; j=++i+1)
|
|
|
|
|
while (j<proj.length && Cube.pEquidistant(proj[i],proj[j]))
|
|
|
|
|
if (Cube.pAdjacent(proj[i],proj[j])) {
|
|
|
|
|
proj.splice(j,1);
|
|
|
|
|
proj.splice(i,1);
|
|
|
|
|
i--; j=proj.length;
|
|
|
|
|
} else j++;
|
|
|
|
|
if (vol==VOLUME.PARALLEL)
|
|
|
|
|
// сортируем грани разных кубиков по удалённости и внутри одного кубика по наклону
|
|
|
|
|
proj.sort((a,b)=>Math.abs(b.dist-a.dist)>size ? b.dist-a.dist : b.clock-a.clock);
|
|
|
|
|
// обновляем стакан с игрой
|
|
|
|
|
drawCubes(container.canvas, container.context, proj);
|
|
|
|
|
// центральная точка перспективной проекции
|
|
|
|
|
if (vol==VOLUME.PERSPECTIVE && tv2.show) centerPoint(container.context);
|
|
|
|
|
}
|
|
|
|
|
// обходим массив, рисуем грани по точкам
|
|
|
|
|
function drawCubes(canvas, context, array) {
|
|
|
|
|
// очищаем весь холст целиком
|
|
|
|
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
|
// рисуем только видимые грани
|
|
|
|
|
const visible = function(face) {
|
|
|
|
|
// если есть хотя бы одна точка, которую можно нарисовать
|
|
|
|
|
for (let j = 0; j < face.length; j++)
|
|
|
|
|
if (face[j].x>-size && face[j].x<canvas.width+size
|
|
|
|
|
&& face[j].y>-size && face[j].y<canvas.height+size)
|
|
|
|
|
return true;
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
// обходим массив граней куба
|
|
|
|
|
for (let i = 0; i < array.length; i++) {
|
|
|
|
|
// рисуем только видимые грани
|
|
|
|
|
if (!visible(array[i])) continue;
|
|
|
|
|
// обходим массив точек и соединяем их линиями
|
|
|
|
|
context.beginPath();
|
|
|
|
|
for (let j = 0; j < array[i].length; j++)
|
|
|
|
|
if (j==0) context.moveTo(array[i][j].x, array[i][j].y);
|
|
|
|
|
else context.lineTo(array[i][j].x, array[i][j].y);
|
|
|
|
|
context.closePath();
|
|
|
|
|
// рисуем линии на холсте
|
|
|
|
|
context.lineWidth = 1.7;
|
|
|
|
|
context.lineJoin = 'round';
|
|
|
|
|
context.fillStyle = colors.get(array[i].color);
|
|
|
|
|
context.strokeStyle = '#ffff';
|
|
|
|
|
context.fill();
|
|
|
|
|
context.stroke();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// центральная точка перспективной проекции
|
|
|
|
|
function centerPoint(context) {
|
|
|
|
|
context.beginPath();
|
|
|
|
|
context.lineWidth = 3.2;
|
|
|
|
|
context.strokeStyle = '#66bb6a';
|
|
|
|
|
context.arc(tv2.x, tv2.y, 6.5, 0, 2*Math.PI);
|
|
|
|
|
context.stroke();
|
|
|
|
|
}
|
|
|
|
|
// скорость падения фигуры
|
|
|
|
|
const speedometer = function() {
|
|
|
|
|
let speed = 0;
|
|
|
|
|
return function() {
|
|
|
|
|
if (rapidFall)
|
|
|
|
|
speed = Math.min(START_DELAY, speed + 80);
|
|
|
|
|
else
|
|
|
|
|
speed = Math.max(0, speed - 80);
|
|
|
|
|
return Math.max(speed, START_DELAY - stepDelay);
|
|
|
|
|
}
|
|
|
|
|
}();
|