555 lines
71 KiB
JavaScript
555 lines
71 KiB
JavaScript
|
const nope = (x) => { };
|
||
|
const getSize = (value, options) => value * options.pixelSize;
|
||
|
const drawBorderedRect = (position, context, options) => {
|
||
|
const borderWidth = options.borderWidth;
|
||
|
if (!borderWidth) {
|
||
|
return;
|
||
|
}
|
||
|
const actualLineWidth = getSize(borderWidth, options);
|
||
|
context.lineWidth = actualLineWidth;
|
||
|
context.strokeStyle = 'white';
|
||
|
context.strokeRect(getSize(position.x, options) - Math.round(actualLineWidth / 2), getSize(position.y, options) - Math.round(actualLineWidth / 2), getSize(position.width, options) + actualLineWidth, getSize(position.height, options) + actualLineWidth);
|
||
|
};
|
||
|
class TetrissimoLevel {
|
||
|
constructor(order, speed) {
|
||
|
this.order = order;
|
||
|
this.movementsPerSecond = speed;
|
||
|
}
|
||
|
}
|
||
|
TetrissimoLevel.level1 = new TetrissimoLevel(1, 0.5);
|
||
|
TetrissimoLevel.level2 = new TetrissimoLevel(2, 0.75);
|
||
|
TetrissimoLevel.level3 = new TetrissimoLevel(2, 1);
|
||
|
TetrissimoLevel.level4 = new TetrissimoLevel(2, 1.5);
|
||
|
TetrissimoLevel.level5 = new TetrissimoLevel(2, 2);
|
||
|
TetrissimoLevel.level6 = new TetrissimoLevel(2, 3);
|
||
|
TetrissimoLevel.level7 = new TetrissimoLevel(2, 4);
|
||
|
TetrissimoLevel.level8 = new TetrissimoLevel(2, 6);
|
||
|
TetrissimoLevel.level9 = new TetrissimoLevel(2, 8);
|
||
|
TetrissimoLevel.level10 = new TetrissimoLevel(2, 10);
|
||
|
class TetrissimoGame {
|
||
|
constructor(ui) {
|
||
|
this.startedAt = null;
|
||
|
this.pausedAt = null;
|
||
|
this.finishedAt = null;
|
||
|
this.state = 'not-started';
|
||
|
this.fps = 60;
|
||
|
this.lastFrameTimes = [];
|
||
|
this.pieces = [];
|
||
|
this.currentLevel = TetrissimoLevel.level1;
|
||
|
this.msUntilNextTick = 0;
|
||
|
this.lastTick = new Date();
|
||
|
this.lastUpdate = new Date();
|
||
|
this.ui = ui;
|
||
|
}
|
||
|
getState() {
|
||
|
return this.state;
|
||
|
}
|
||
|
setTargetFps(fps) {
|
||
|
this.fps = fps;
|
||
|
}
|
||
|
setLevel(level) {
|
||
|
this.currentLevel = level;
|
||
|
}
|
||
|
getCurrentFps() {
|
||
|
switch (this.state) {
|
||
|
case 'running':
|
||
|
if (this.lastFrameTimes.length <= 1) {
|
||
|
return 0;
|
||
|
}
|
||
|
const firstFrame = this.lastFrameTimes[0];
|
||
|
const lastFrame = this.lastFrameTimes[this.lastFrameTimes.length - 1];
|
||
|
const elapsedMs = lastFrame - firstFrame;
|
||
|
const avgMsPerFrame = (elapsedMs / this.lastFrameTimes.length);
|
||
|
return 1000 / avgMsPerFrame;
|
||
|
case 'paused':
|
||
|
case 'not-started':
|
||
|
case 'finished':
|
||
|
return 0;
|
||
|
default:
|
||
|
nope(this.state);
|
||
|
throw new Error(`Invalid game state "${this.state}"`);
|
||
|
}
|
||
|
}
|
||
|
performPieceAction(action) {
|
||
|
if (this.state !== 'running') {
|
||
|
return;
|
||
|
}
|
||
|
const piece = this.pieces.filter(piece => piece.state === 'in-play')[0];
|
||
|
if (!piece) {
|
||
|
// nothing to do, no pieces in play
|
||
|
return;
|
||
|
}
|
||
|
const movementIncrement = 1;
|
||
|
switch (action.type) {
|
||
|
case 'moveLeft':
|
||
|
this.moveIfValid(piece, -movementIncrement, 0);
|
||
|
break;
|
||
|
case 'moveRight':
|
||
|
this.moveIfValid(piece, movementIncrement, 0);
|
||
|
break;
|
||
|
case 'moveDown':
|
||
|
if (!this.moveIfValid(piece, 0, movementIncrement)) {
|
||
|
// make fixed to bottom of play area
|
||
|
piece.setFixed();
|
||
|
}
|
||
|
break;
|
||
|
case 'rotateLeft':
|
||
|
// TODO ensure it's a valid move
|
||
|
piece.rotateLeft();
|
||
|
break;
|
||
|
case 'rotateRight':
|
||
|
piece.rotateRight();
|
||
|
break;
|
||
|
default:
|
||
|
nope(action.type);
|
||
|
throw new Error(`invalid action "${action.type}"`);
|
||
|
}
|
||
|
}
|
||
|
start() {
|
||
|
this.pausedAt = null;
|
||
|
switch (this.state) {
|
||
|
case 'running':
|
||
|
// no-op
|
||
|
break;
|
||
|
case 'paused':
|
||
|
case 'not-started':
|
||
|
this.lastTick = new Date();
|
||
|
this.lastUpdate = new Date();
|
||
|
this.startedAt = new Date();
|
||
|
this.state = 'running';
|
||
|
this.update();
|
||
|
break;
|
||
|
case 'finished':
|
||
|
throw new Error('Cannot start a finished game');
|
||
|
default:
|
||
|
nope(this.state);
|
||
|
throw new Error(`Invalid game state "${this.state}"`);
|
||
|
}
|
||
|
}
|
||
|
pause() {
|
||
|
switch (this.state) {
|
||
|
case 'running':
|
||
|
case 'paused':
|
||
|
this.pausedAt = new Date();
|
||
|
this.state = 'paused';
|
||
|
this.lastFrameTimes = [];
|
||
|
break;
|
||
|
case 'not-started':
|
||
|
case 'finished':
|
||
|
throw new Error(`Cannot pause game while it's in state "${this.state}"`);
|
||
|
default:
|
||
|
nope(this.state);
|
||
|
throw new Error(`Invalid game state "${this.state}"`);
|
||
|
}
|
||
|
}
|
||
|
update() {
|
||
|
const tickStart = Date.now();
|
||
|
this.lastFrameTimes.push(tickStart);
|
||
|
if (this.lastFrameTimes.length > 20) {
|
||
|
this.lastFrameTimes.shift();
|
||
|
}
|
||
|
switch (this.state) {
|
||
|
case 'running':
|
||
|
this.performUpdate();
|
||
|
break;
|
||
|
case 'paused':
|
||
|
case 'not-started':
|
||
|
case 'finished':
|
||
|
throw new Error(`Cannot advance game while in state "${this.state}"`);
|
||
|
default:
|
||
|
nope(this.state);
|
||
|
throw new Error(`Invalid game state "${this.state}"`);
|
||
|
}
|
||
|
const elapsed = Date.now() - tickStart;
|
||
|
const nextTickTimeout = Math.max(0, (1000 / this.fps) - elapsed);
|
||
|
setTimeout(() => {
|
||
|
switch (this.state) {
|
||
|
case 'running':
|
||
|
this.update();
|
||
|
break;
|
||
|
case 'paused':
|
||
|
case 'not-started':
|
||
|
case 'finished':
|
||
|
break;
|
||
|
default:
|
||
|
nope(this.state);
|
||
|
throw new Error(`Invalid game state "${this.state}"`);
|
||
|
}
|
||
|
}, nextTickTimeout);
|
||
|
}
|
||
|
performUpdate() {
|
||
|
const sinceLastUpdate = Date.now() - this.lastUpdate.getTime();
|
||
|
this.msUntilNextTick -= sinceLastUpdate;
|
||
|
this.lastUpdate = new Date();
|
||
|
if (this.msUntilNextTick <= 0) {
|
||
|
this.tick();
|
||
|
}
|
||
|
// detect collision
|
||
|
// is every column filled? remove column
|
||
|
// is every row filled? end game
|
||
|
ui.update();
|
||
|
}
|
||
|
moveIfValid(piece, xDelta, yDelta) {
|
||
|
const translation = {
|
||
|
xDelta,
|
||
|
yDelta,
|
||
|
};
|
||
|
if (this.ui.canMovePiece(piece, translation)) {
|
||
|
piece.move(translation);
|
||
|
return true;
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
;
|
||
|
tick() {
|
||
|
const inPlayPieces = this.pieces.filter(piece => piece.state === 'in-play');
|
||
|
if (!inPlayPieces.length) {
|
||
|
const newPiece = TetrissimoPiece.T.clone();
|
||
|
newPiece.state = 'in-play';
|
||
|
this.pieces.push(newPiece);
|
||
|
this.ui.addPiece(newPiece);
|
||
|
}
|
||
|
else {
|
||
|
inPlayPieces.forEach((piece) => {
|
||
|
if (!this.moveIfValid(piece, 0, 1)) {
|
||
|
piece.setFixed();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
this.msUntilNextTick = Math.round(1000 / this.currentLevel.movementsPerSecond);
|
||
|
this.lastTick = new Date();
|
||
|
}
|
||
|
}
|
||
|
class TetrissimoUI {
|
||
|
constructor(canvas, playArea,
|
||
|
// nextBox: TetrissimoNextBox,
|
||
|
hud) {
|
||
|
this.pieces = [];
|
||
|
this.playArea = playArea;
|
||
|
// this.nextBox = nextBox;
|
||
|
this.hud = hud;
|
||
|
const context = canvas.getContext('2d');
|
||
|
if (!context) {
|
||
|
throw new Error('failed to get 2d context');
|
||
|
}
|
||
|
this.options = {
|
||
|
borderWidth: 1,
|
||
|
canvas: canvas,
|
||
|
context,
|
||
|
gridLines: 4,
|
||
|
hud: {
|
||
|
width: 32,
|
||
|
height: 12,
|
||
|
},
|
||
|
playArea: {
|
||
|
width: 68,
|
||
|
height: 128,
|
||
|
},
|
||
|
padding: 2,
|
||
|
pixelSize: 4,
|
||
|
};
|
||
|
}
|
||
|
setOptions(options) {
|
||
|
Object.keys(options).forEach((key) => {
|
||
|
this.options[key] = options[key];
|
||
|
});
|
||
|
}
|
||
|
addPiece(piece) {
|
||
|
this.pieces.push(piece);
|
||
|
}
|
||
|
getPiecePosition(piece) {
|
||
|
const origin = this.getPieceOrigin(piece);
|
||
|
const offset = piece.getOffset();
|
||
|
return {
|
||
|
x: origin.x + offset.x,
|
||
|
y: origin.y + offset.y,
|
||
|
};
|
||
|
}
|
||
|
canMovePiece(piece, delta) {
|
||
|
// check each block in layout's edge to see if it's in a valid area?s
|
||
|
const layout = piece.getLayout();
|
||
|
const position = this.getPiecePosition(piece);
|
||
|
for (let i = 0; i < layout.length; i++) {
|
||
|
const blockTopEdge = position.y + (i * TetrissimoPiece.blockLength) + (delta.yDelta * TetrissimoPiece.blockLength);
|
||
|
for (let j = 0; j < layout[i].length; j++) {
|
||
|
if (layout[i][j] === 0) {
|
||
|
continue;
|
||
|
}
|
||
|
const blockLeftEdge = position.x + (j * TetrissimoPiece.blockLength) + (delta.xDelta * TetrissimoPiece.blockLength);
|
||
|
const isCollision = this.hasCollision({
|
||
|
x: blockLeftEdge,
|
||
|
y: blockTopEdge,
|
||
|
width: TetrissimoPiece.blockLength,
|
||
|
height: TetrissimoPiece.blockLength,
|
||
|
});
|
||
|
if (isCollision) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
hasCollision(rect) {
|
||
|
const playAreaLeftEdge = this.options.padding;
|
||
|
const playAreaRightEdge = playAreaLeftEdge + this.options.playArea.width;
|
||
|
const playAreaTopEdge = this.options.padding;
|
||
|
const playAreaBottomEdge = playAreaTopEdge + this.options.playArea.height;
|
||
|
const leftEdge = rect.x;
|
||
|
const rightEdge = rect.x + rect.width;
|
||
|
const topEdge = rect.y;
|
||
|
const bottomEdge = rect.y + rect.height;
|
||
|
const isInPlayArea = leftEdge >= playAreaLeftEdge &&
|
||
|
rightEdge <= playAreaRightEdge &&
|
||
|
bottomEdge <= playAreaBottomEdge &&
|
||
|
topEdge >= playAreaTopEdge;
|
||
|
if (!isInPlayArea) {
|
||
|
return true;
|
||
|
}
|
||
|
const fixedPieces = this.pieces.filter(piece => piece.state === 'fixed');
|
||
|
for (const piece of fixedPieces) {
|
||
|
const position = this.getPiecePosition(piece);
|
||
|
const layout = piece.getLayout();
|
||
|
for (let i = 0; i < layout.length; i++) {
|
||
|
const blockTopEdge = position.y + (i * TetrissimoPiece.blockLength);
|
||
|
const blockBottomEdge = blockTopEdge + TetrissimoPiece.blockLength;
|
||
|
for (let j = 0; j < layout[i].length; j++) {
|
||
|
if (layout[i][j] === 0) {
|
||
|
continue;
|
||
|
}
|
||
|
const blockLeftEdge = position.x + (j * TetrissimoPiece.blockLength);
|
||
|
const blockRightEdge = blockLeftEdge + TetrissimoPiece.blockLength;
|
||
|
const isCollision = leftEdge >= blockLeftEdge &&
|
||
|
rightEdge <= blockRightEdge &&
|
||
|
bottomEdge <= blockBottomEdge &&
|
||
|
topEdge >= blockTopEdge;
|
||
|
if (isCollision) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
update() {
|
||
|
const options = this.options;
|
||
|
const context = this.options.context;
|
||
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
||
|
// fill background
|
||
|
context.fillStyle = 'rgb(224,158,107)';
|
||
|
context.fillRect(0, 0, canvas.width, canvas.height);
|
||
|
// draw some grid lines
|
||
|
if (typeof (options.gridLines) === 'number') {
|
||
|
context.strokeStyle = 'rgba(255, 255, 255, 0.25)';
|
||
|
context.lineWidth = 1;
|
||
|
const increment = options.pixelSize * options.gridLines;
|
||
|
for (let i = 0; i < options.canvas.width; i += increment) {
|
||
|
// draw vertical line
|
||
|
context.beginPath();
|
||
|
context.moveTo(i, 0);
|
||
|
context.lineTo(i, options.canvas.height);
|
||
|
context.stroke();
|
||
|
}
|
||
|
for (let i = 0; i < options.canvas.height; i += increment) {
|
||
|
// draw horizontal lines
|
||
|
context.beginPath();
|
||
|
context.moveTo(0, i);
|
||
|
context.lineTo(options.canvas.width, i);
|
||
|
context.stroke();
|
||
|
}
|
||
|
}
|
||
|
const totalWidth = Math.floor(canvas.width / options.pixelSize);
|
||
|
const padding = options.padding;
|
||
|
// draw HUD in top right corner
|
||
|
this.hud.render(context, Object.assign(Object.assign({}, options), { x: totalWidth - padding - options.hud.width, y: padding }));
|
||
|
// draw play area
|
||
|
this.playArea.render(context, Object.assign(Object.assign({}, options), { x: padding, y: padding }));
|
||
|
this.pieces.forEach((piece, i) => {
|
||
|
piece.render(context, Object.assign(Object.assign({}, options), this.getPieceOrigin(piece)));
|
||
|
});
|
||
|
}
|
||
|
getPieceOrigin(piece) {
|
||
|
const length = TetrissimoPiece.blockLength;
|
||
|
// TODO rotation will cause problems, need a "PieceDefinition" or something with a static layout
|
||
|
let translateX = Math.floor((piece.getBlockWidth() * length) / 2);
|
||
|
// we can only perform movements in multiples of the blockLength, so we must clamp it
|
||
|
// to ensure it's not offset
|
||
|
translateX -= (translateX % length);
|
||
|
const padding = this.options.padding;
|
||
|
return {
|
||
|
x: padding + Math.round(this.options.playArea.width / 2) - translateX,
|
||
|
y: padding,
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
class TetrissimoNextBox {
|
||
|
render(context, options) {
|
||
|
}
|
||
|
}
|
||
|
class TetrissimoHUD {
|
||
|
render(context, options) {
|
||
|
const position = {
|
||
|
x: options.x,
|
||
|
y: options.y,
|
||
|
width: options.hud.width,
|
||
|
height: options.hud.height,
|
||
|
};
|
||
|
drawBorderedRect(position, context, options);
|
||
|
}
|
||
|
}
|
||
|
class TetrissimoPlayArea {
|
||
|
render(context, options) {
|
||
|
const position = {
|
||
|
x: options.x,
|
||
|
y: options.y,
|
||
|
width: options.playArea.width,
|
||
|
height: options.playArea.height,
|
||
|
};
|
||
|
drawBorderedRect(position, context, options);
|
||
|
}
|
||
|
}
|
||
|
class TetrissimoPiece {
|
||
|
constructor(name, layout, color) {
|
||
|
this.offset = { x: 0, y: 0 };
|
||
|
this.state = 'in-play';
|
||
|
this.name = name;
|
||
|
this.originalLayout = layout.map(row => row.concat([]));
|
||
|
this.layout = layout.concat([]);
|
||
|
this.color = color;
|
||
|
}
|
||
|
getOffset() {
|
||
|
return this.offset;
|
||
|
}
|
||
|
getLayout() {
|
||
|
return this.layout;
|
||
|
}
|
||
|
move(translation) {
|
||
|
if (this.state !== 'in-play') {
|
||
|
return;
|
||
|
}
|
||
|
this.offset.x += (translation.xDelta * TetrissimoPiece.blockLength);
|
||
|
this.offset.y += (translation.yDelta * TetrissimoPiece.blockLength);
|
||
|
}
|
||
|
rotateLeft() {
|
||
|
if (this.state !== 'in-play') {
|
||
|
return;
|
||
|
}
|
||
|
// clockwise turn
|
||
|
const newLayout = [];
|
||
|
for (let i = 0; i < this.layout.length; i++) {
|
||
|
const col = this.layout.length - 1 - i;
|
||
|
for (let j = 0; j < this.layout[i].length; j++) {
|
||
|
const row = j;
|
||
|
if (!newLayout[row]) {
|
||
|
newLayout[row] = [];
|
||
|
}
|
||
|
newLayout[row][col] = this.layout[i][j];
|
||
|
}
|
||
|
}
|
||
|
this.layout = newLayout;
|
||
|
}
|
||
|
rotateRight() {
|
||
|
if (this.state !== 'in-play') {
|
||
|
return;
|
||
|
}
|
||
|
const newLayout = [];
|
||
|
for (let i = 0; i < this.layout.length; i++) {
|
||
|
const col = i;
|
||
|
for (let j = 0; j < this.layout[i].length; j++) {
|
||
|
const row = this.layout[i].length - 1 - j;
|
||
|
if (!newLayout[row]) {
|
||
|
newLayout[row] = [];
|
||
|
}
|
||
|
newLayout[row][col] = this.layout[i][j];
|
||
|
}
|
||
|
}
|
||
|
this.layout = newLayout;
|
||
|
}
|
||
|
setFixed() {
|
||
|
this.state = 'fixed';
|
||
|
}
|
||
|
getBlockWidth() {
|
||
|
return this.layout
|
||
|
.map(arr => arr.filter(value => value === 1).length)
|
||
|
.reduce((max, len) => Math.max(max, len), 0);
|
||
|
}
|
||
|
getBlockHeight() {
|
||
|
const cols = [];
|
||
|
for (let i = 0; i < this.layout.length; i++) {
|
||
|
for (let j = 0; j < this.layout[i].length; j++) {
|
||
|
if (!cols[j]) {
|
||
|
cols[j] = [];
|
||
|
}
|
||
|
cols[j].push(this.layout[i][j]);
|
||
|
}
|
||
|
}
|
||
|
return cols
|
||
|
.map(arr => arr.filter(value => value === 1).length)
|
||
|
.reduce((max, len) => Math.max(max, len), 0);
|
||
|
}
|
||
|
clone() {
|
||
|
return new TetrissimoPiece(this.name, this.layout, this.color);
|
||
|
}
|
||
|
render(context, options) {
|
||
|
this.layout.forEach((columns, rowNum) => {
|
||
|
columns.forEach((value, colNum) => {
|
||
|
switch (value) {
|
||
|
case 0:
|
||
|
break;
|
||
|
case 1:
|
||
|
context.fillStyle = this.color;
|
||
|
const length = TetrissimoPiece.blockLength;
|
||
|
const offsetX = this.offset.x;
|
||
|
const offsetY = this.offset.y;
|
||
|
const x = options.x + offsetX + (colNum * length);
|
||
|
const y = options.y + offsetY + (rowNum * length);
|
||
|
const actualLength = getSize(length, options);
|
||
|
context.fillRect(getSize(x, options), getSize(y, options), actualLength, actualLength);
|
||
|
break;
|
||
|
default:
|
||
|
nope(value);
|
||
|
throw new Error(`Invalid piece cell value "${value}"`);
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
TetrissimoPiece.blockLength = 4;
|
||
|
TetrissimoPiece.J = new TetrissimoPiece('J', [
|
||
|
[0, 1],
|
||
|
[0, 1],
|
||
|
[1, 1],
|
||
|
], 'black');
|
||
|
TetrissimoPiece.S = new TetrissimoPiece('S', [
|
||
|
[0, 1, 1],
|
||
|
[1, 1, 0],
|
||
|
], 'green');
|
||
|
TetrissimoPiece.T = new TetrissimoPiece('T', [
|
||
|
[1, 1, 1],
|
||
|
[0, 1, 0],
|
||
|
], 'blue');
|
||
|
TetrissimoPiece.Z = new TetrissimoPiece('Z', [
|
||
|
[1, 1, 0,],
|
||
|
[0, 1, 1,],
|
||
|
], 'yellow');
|
||
|
TetrissimoPiece.I = new TetrissimoPiece('I', [
|
||
|
[1, 1, 1, 1],
|
||
|
], 'magenta');
|
||
|
TetrissimoPiece.L = new TetrissimoPiece('L', [
|
||
|
[1],
|
||
|
[1],
|
||
|
[1, 1],
|
||
|
], 'cyan');
|
||
|
TetrissimoPiece.O = new TetrissimoPiece('O', [
|
||
|
[1, 1],
|
||
|
[1, 1],
|
||
|
], 'red');
|
||
|
// ---------------------------------------------
|
||
|
const hud = new TetrissimoHUD();
|
||
|
const playArea = new TetrissimoPlayArea();
|
||
|
const canvas = document.getElementsByTagName('canvas')[0];
|
||
|
if (!canvas) {
|
||
|
throw new Error('no canvas');
|
||
|
}
|
||
|
const ui = new TetrissimoUI(canvas, playArea, hud);
|
||
|
const game = new TetrissimoGame(ui);
|
||
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGV0cmlzc2ltby5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInRldHJpc3NpbW8udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBRUEsTUFBTSxJQUFJLEdBQUcsQ0FBQyxDQUFRLEVBQVEsRUFBRSxHQUFFLENBQUMsQ0FBQztBQXFEcEMsTUFBTSxPQUFPLEdBQUcsQ0FBQyxLQUFhLEVBQUUsT0FBMkMsRUFBVSxFQUFFLENBQUMsS0FBSyxHQUFHLE9BQU8sQ0FBQyxTQUFTLENBQUM7QUFFbEgsTUFBTSxnQkFBZ0IsR0FBRyxDQUN4QixRQUE0QyxFQUM1QyxPQUFpQyxFQUNqQyxPQUErQixFQUN4QixFQUFFO0lBQ1QsTUFBTSxXQUFXLEdBQUcsT0FBTyxDQUFDLFdBQVcsQ0FBQztJQUN4QyxJQUFJLENBQUMsV0FBVyxFQUFFO1FBQ2pCLE9BQU87S0FDUDtJQUVELE1BQU0sZUFBZSxHQUFHLE9BQU8sQ0FBQyxXQUFXLEVBQUUsT0FBTyxDQUFDLENBQUM7SUFDdEQsT0FBTyxDQUFDLFNBQVMsR0FBRyxlQUFlLENBQUM7SUFDcEMsT0FBTyxDQUFDLFdBQVcsR0FBRyxPQUFPLENBQUM7SUFDOUIsT0FBTyxDQUFDLFVBQVUsQ0FDakIsT0FBTyxDQUFDLFFBQVEsQ0FBQyxDQUFDLEVBQUUsT0FBTyxDQUFDLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxlQUFlLEdBQUcsQ0FBQyxDQUFDLEVBQzlELE9BQU8sQ0FBQyxRQUFRLENBQUMsQ0FBQyxFQUFFLE9BQU8sQ0FBQyxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsZUFBZSxHQUFHLENBQUMsQ0FBQyxFQUM5RCxPQUFPLENBQUMsUUFBUSxDQUFDLEtBQUssRUFBRSxPQUFPLENBQUMsR0FBRyxlQUFlLEVBQ2xELE9BQU8sQ0FBQyxRQUFRLENBQUMsTUFBTSxFQUFFLE9BQU8sQ0FBQyxHQUFHLGVBQWUsQ0FDbkQsQ0FBQztBQUNILENBQUMsQ0FBQztBQUVGLE1BQU0sZUFBZTtJQWVwQixZQUFtQixLQUFhLEVB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
|