diff --git a/frontend/assets/img/icons/bitcoin.svg b/frontend/assets/img/icons/bitcoin.svg new file mode 100644 index 000000000..ca5d37fc1 --- /dev/null +++ b/frontend/assets/img/icons/bitcoin.svg @@ -0,0 +1,95 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/assets/img/icons/snek.png b/frontend/assets/img/icons/snek.png new file mode 100644 index 000000000..26ed2bea5 Binary files /dev/null and b/frontend/assets/img/icons/snek.png differ diff --git a/frontend/projects/ui/src/app/app.component.html b/frontend/projects/ui/src/app/app.component.html index 59bb22384..dfdc85b72 100644 --- a/frontend/projects/ui/src/app/app.component.html +++ b/frontend/projects/ui/src/app/app.component.html @@ -62,6 +62,17 @@ +
Score: {{ score }} + + + + +
+ +
+
+ + + + High Score: {{ highScore }} + + Byeeeeeee! + + + diff --git a/frontend/projects/ui/src/app/modals/snake/snake.page.scss b/frontend/projects/ui/src/app/modals/snake/snake.page.scss new file mode 100644 index 000000000..c07d3a2b7 --- /dev/null +++ b/frontend/projects/ui/src/app/modals/snake/snake.page.scss @@ -0,0 +1,6 @@ +.canvas-center { + padding-top: 20px; + display: flex; + align-items: center; + justify-content: center; +} \ No newline at end of file diff --git a/frontend/projects/ui/src/app/modals/snake/snake.page.ts b/frontend/projects/ui/src/app/modals/snake/snake.page.ts new file mode 100644 index 000000000..b997bf366 --- /dev/null +++ b/frontend/projects/ui/src/app/modals/snake/snake.page.ts @@ -0,0 +1,260 @@ +import { Component, HostListener } from '@angular/core' +import { ModalController } from '@ionic/angular' +import { pauseFor } from '@start9labs/shared' +import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' + +@Component({ + selector: 'snake', + templateUrl: './snake.page.html', + styleUrls: ['./snake.page.scss'], +}) +export class SnakePage { + speed = 40 + width = 40 + height = 26 + grid + + startingLength = 4 + + score = 0 + highScore = 0 + + xDown: number + yDown: number + canvas: HTMLCanvasElement + image: HTMLImageElement + context + + snake + bitcoin + + moveQueue: String[] = [] + + constructor( + private readonly modalCtrl: ModalController, + private readonly patch: PatchDbService, + ) {} + + ngOnInit() { + if (this.patch.data.ui.gaming?.snake?.['high-score']) { + this.highScore = this.patch.data.ui.gaming?.snake?.['high-score'] + } + } + + async dismiss() { + return this.modalCtrl.dismiss({ highScore: this.highScore }) + } + + @HostListener('document:keydown', ['$event']) + keyEvent(e: KeyboardEvent) { + this.moveQueue.push(e.key) + } + + @HostListener('touchstart', ['$event']) + touchStart(e: TouchEvent) { + this.handleTouchStart(e) + } + + @HostListener('touchmove', ['$event']) + touchMove(e: TouchEvent) { + this.handleTouchMove(e) + } + + @HostListener('window:resize', ['$event']) + sizeChange(event) { + this.init() + } + + ionViewDidEnter() { + this.init() + + this.image = new Image() + this.image.onload = () => { + requestAnimationFrame(async () => await this.loop()) + } + this.image.src = '../../../../../../assets/img/icons/bitcoin.svg' + } + + init() { + this.canvas = document.getElementById('game') as HTMLCanvasElement + this.canvas.style.border = '1px solid #e0e0e0' + this.context = this.canvas.getContext('2d') + const container = document.getElementsByClassName('canvas-center')[0] + this.grid = Math.floor(container.clientWidth / this.width) + this.snake = { + x: this.grid * (Math.floor(this.width / 2) - this.startingLength), + y: this.grid * Math.floor(this.height / 2), + // snake velocity. moves one grid length every frame in either the x or y direction + dx: this.grid, + dy: 0, + // keep track of all grids the snake body occupies + cells: [], + // length of the snake. grows when eating an bitcoin + maxCells: this.startingLength, + } + this.bitcoin = { + x: this.getRandomInt(0, this.width) * this.grid, + y: this.getRandomInt(0, this.height) * this.grid, + } + + this.canvas.width = this.grid * this.width + this.canvas.height = this.grid * this.height + this.context.imageSmoothingEnabled = false + } + + getTouches(evt: TouchEvent) { + return evt.touches + } + + handleTouchStart(evt) { + const firstTouch = this.getTouches(evt)[0] + this.xDown = firstTouch.clientX + this.yDown = firstTouch.clientY + } + + handleTouchMove(evt) { + if (!this.xDown || !this.yDown) { + return + } + + var xUp = evt.touches[0].clientX + var yUp = evt.touches[0].clientY + + var xDiff = this.xDown - xUp + var yDiff = this.yDown - yUp + + if (Math.abs(xDiff) > Math.abs(yDiff)) { + /*most significant*/ + if (xDiff > 0) { + this.moveQueue.push('ArrowLeft') + } else { + this.moveQueue.push('ArrowRight') + } + } else { + if (yDiff > 0) { + this.moveQueue.push('ArrowUp') + } else { + this.moveQueue.push('ArrowDown') + } + } + /* reset values */ + this.xDown = null + this.yDown = null + } + + // game loop + async loop() { + await pauseFor(this.speed) + + requestAnimationFrame(async () => await this.loop()) + + this.context.clearRect(0, 0, this.canvas.width, this.canvas.height) + + // move snake by it's velocity + this.snake.x += this.snake.dx + this.snake.y += this.snake.dy + + if (this.moveQueue.length) { + const move = this.moveQueue.shift() + // left arrow key + if (move === 'ArrowLeft' && this.snake.dx === 0) { + this.snake.dx = -this.grid + this.snake.dy = 0 + } + // up arrow key + else if (move === 'ArrowUp' && this.snake.dy === 0) { + this.snake.dy = -this.grid + this.snake.dx = 0 + } + // right arrow key + else if (move === 'ArrowRight' && this.snake.dx === 0) { + this.snake.dx = this.grid + this.snake.dy = 0 + } + // down arrow key + else if (move === 'ArrowDown' && this.snake.dy === 0) { + this.snake.dy = this.grid + this.snake.dx = 0 + } + } + + // edge death + if ( + this.snake.x < 0 || + this.snake.y < 0 || + this.snake.x >= this.canvas.width || + this.snake.y >= this.canvas.height + ) { + this.death() + } + + // keep track of where snake has been. front of the array is always the head + this.snake.cells.unshift({ x: this.snake.x, y: this.snake.y }) + + // remove cells as we move away from them + if (this.snake.cells.length > this.snake.maxCells) { + this.snake.cells.pop() + } + + // draw bitcoin + this.context.fillStyle = '#ff4961' + this.context.drawImage( + this.image, + this.bitcoin.x - 1, + this.bitcoin.y - 1, + this.grid + 2, + this.grid + 2, + ) + + // draw snake one cell at a time + this.context.fillStyle = '#2fdf75' + + const firstCell = this.snake.cells[0] + + for (let index = 0; index < this.snake.cells.length; index++) { + const cell = this.snake.cells[index] + + // drawing 1 px smaller than the grid creates a grid effect in the snake body so you can see how long it is + this.context.fillRect(cell.x, cell.y, this.grid - 1, this.grid - 1) + + // snake ate bitcoin + if (cell.x === this.bitcoin.x && cell.y === this.bitcoin.y) { + this.score++ + if (this.score > this.highScore) this.highScore = this.score + this.snake.maxCells++ + + this.bitcoin.x = this.getRandomInt(0, this.width) * this.grid + this.bitcoin.y = this.getRandomInt(0, this.height) * this.grid + } + + if (index > 0) { + // check collision with all cells after this one (modified bubble sort) + // snake occupies same space as a body part. reset game + if ( + firstCell.x === this.snake.cells[index].x && + firstCell.y === this.snake.cells[index].y + ) { + this.death() + } + } + } + } + + death() { + this.snake.x = + this.grid * (Math.floor(this.width / 2) - this.startingLength) + this.snake.y = this.grid * Math.floor(this.height / 2) + this.snake.cells = [] + this.snake.maxCells = this.startingLength + this.snake.dx = this.grid + this.snake.dy = 0 + + this.bitcoin.x = this.getRandomInt(0, 25) * this.grid + this.bitcoin.y = this.getRandomInt(0, 25) * this.grid + this.score = 0 + } + + getRandomInt(min, max) { + return Math.floor(Math.random() * (max - min)) + min + } +} diff --git a/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.html b/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.html index 5f69f031c..744c10c40 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.html +++ b/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.html @@ -14,7 +14,7 @@ - Add alternative marketplace + Add alt marketplace diff --git a/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.ts b/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.ts index a940cda31..aef0b4831 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.ts @@ -291,7 +291,7 @@ function getMarketplaceValueSpec(): ValueSpecObject { url: { type: 'string', name: 'URL', - description: 'The fully-qualified URL of the alternative marketplace.', + description: 'The fully-qualified URL of the alt marketplace.', nullable: false, masked: false, copyable: false, diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.module.ts b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.module.ts index 2efdac950..2ec7b74d4 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.module.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.module.ts @@ -6,6 +6,7 @@ import { ServerShowPage } from './server-show.page' import { FormsModule } from '@angular/forms' import { TextSpinnerComponentModule } from '@start9labs/shared' import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' +import { SnakePageModule } from 'src/app/modals/snake/snake.module' const routes: Routes = [ { @@ -22,6 +23,7 @@ const routes: Routes = [ RouterModule.forChild(routes), TextSpinnerComponentModule, BadgeMenuComponentModule, + SnakePageModule, ], declarations: [ServerShowPage], }) diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts index 822bf7e1e..866d3c206 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts @@ -17,6 +17,7 @@ import { wizardModal } from 'src/app/components/install-wizard/install-wizard.co import { exists, isEmptyObject } from '@start9labs/shared' import { EOSService } from 'src/app/services/eos.service' import { ServerStatus } from 'src/app/services/patch-db/data-model' +import { SnakePage } from 'src/app/modals/snake/snake.page' @Component({ selector: 'server-show', diff --git a/frontend/projects/ui/src/app/services/api/mock-patch.ts b/frontend/projects/ui/src/app/services/api/mock-patch.ts index 76cf2a969..02e039e07 100644 --- a/frontend/projects/ui/src/app/services/api/mock-patch.ts +++ b/frontend/projects/ui/src/app/services/api/mock-patch.ts @@ -16,6 +16,7 @@ export const mockPatchData: DataModel = { 'ack-welcome': '1.0.0', marketplace: undefined, dev: undefined, + gaming: undefined, }, 'server-info': { id: 'embassy-abcdefgh', diff --git a/frontend/projects/ui/src/app/services/patch-db/data-model.ts b/frontend/projects/ui/src/app/services/patch-db/data-model.ts index bb8c8ddda..e661f32f6 100644 --- a/frontend/projects/ui/src/app/services/patch-db/data-model.ts +++ b/frontend/projects/ui/src/app/services/patch-db/data-model.ts @@ -16,6 +16,13 @@ export interface UIData { 'ack-welcome': string // EOS version marketplace: UIMarketplaceData dev: DevData + gaming: + | { + snake: { + 'high-score': number + } + } + | undefined } export interface UIMarketplaceData {