├── .gitignore ├── src ├── Settings │ ├── BallSettings.js │ ├── SystemSettings.js │ ├── BarSettings.js │ └── BlockSettings.js ├── Models │ ├── Bar.js │ ├── Block.js │ ├── BaseRectangle.js │ └── Ball.js ├── Scenes │ ├── GameOverScene.js │ └── GameScene.js ├── Repositories │ └── KeyController.js ├── index.js └── Util │ ├── Collision.js │ └── Validator.js ├── Dockerfile ├── docker-compose.yml ├── public ├── css │ └── style.css └── index.html ├── .eslintrc.js ├── .editorconfig ├── readme.md ├── package.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | ignore 3 | /nw/* 4 | !/nw/public 5 | /public/js/*.bundle.js 6 | -------------------------------------------------------------------------------- /src/Settings/BallSettings.js: -------------------------------------------------------------------------------- 1 | export default { 2 | RADIUS: 10, 3 | COLOR: 0x00FF00, 4 | }; 5 | -------------------------------------------------------------------------------- /src/Settings/SystemSettings.js: -------------------------------------------------------------------------------- 1 | export default { 2 | WIDTH: 640, 3 | HEIGHT: 480, 4 | }; 5 | -------------------------------------------------------------------------------- /src/Models/Bar.js: -------------------------------------------------------------------------------- 1 | import BaseRectangle from './BaseRectangle'; 2 | 3 | export default class Bar extends BaseRectangle { 4 | } 5 | -------------------------------------------------------------------------------- /src/Models/Block.js: -------------------------------------------------------------------------------- 1 | import BaseRectangle from './BaseRectangle'; 2 | 3 | export default class Block extends BaseRectangle { 4 | } 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14 2 | 3 | WORKDIR /app 4 | COPY package.json ./ 5 | RUN yarn 6 | 7 | COPY . ./app 8 | 9 | CMD yarn dev 10 | 11 | EXPOSE 3000 12 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | app: 5 | build: . 6 | ports: 7 | - 3000:3000 8 | volumes: 9 | - .:/app 10 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | min-height: 100vh; 6 | background: #1A1A1A; 7 | color: #FFF; 8 | font-size: 15px; 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "ecmaVersion": 12, 9 | "sourceType": "module" 10 | }, 11 | "globals": { 12 | "Phaser": true 13 | }, 14 | "rules": { 15 | "semi": "error", 16 | "lines-between-class-members": "error", 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/Scenes/GameOverScene.js: -------------------------------------------------------------------------------- 1 | import SystemSettings from '../Settings/SystemSettings'; 2 | 3 | // ----------------------------------------------------- 4 | 5 | export default class GameOverScene extends Phaser.Scene { 6 | constructor () { 7 | super({ key: 'GameOver', active: false }); 8 | } 9 | 10 | create() { 11 | this.add.text(SystemSettings.WIDTH / 2, SystemSettings.HEIGHT / 2, 'GAME OVER', {fontSize: '48px'}).setOrigin(0.5); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Repositories/KeyController.js: -------------------------------------------------------------------------------- 1 | export default class KeyController { 2 | /** 3 | * キー入力のセットアップ用 4 | * @param {Phaser.Scene} scene 5 | * @returns {KeyController} 6 | */ 7 | setup(scene) { 8 | this.keys = scene.input.keyboard.addKeys({ 9 | left: 'left', 10 | right: 'right' 11 | }); 12 | return this; 13 | } 14 | 15 | /** 16 | * 17 | * @param {string} keyName 18 | * @returns {boolean} 19 | */ 20 | isDown(keyName) { 21 | return this.keys[keyName].isDown; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Settings/BarSettings.js: -------------------------------------------------------------------------------- 1 | export default { 2 | X_SIZE : 100, 3 | Y_SIZE : 15, 4 | X_SPEED: 5, 5 | COLOR : 0xFFCCCC, 6 | 7 | /** 8 | * キャンバスの横サイズからBarの初期X座標を返す 9 | * @param {number} canvasWidth 10 | * @returns {number} 11 | */ 12 | getReferenceXPos (canvasWidth) { 13 | return canvasWidth / 2; 14 | }, 15 | 16 | /** 17 | * キャンバスの縦サイズからBarの初期Y座標を返す 18 | * @param {number} canvasHeight 19 | * @returns {number} 20 | */ 21 | getReferenceYPos (canvasHeight) { 22 | return canvasHeight * 13 / 15; 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [vcbuild.bat] 12 | end_of_line = crlf 13 | 14 | [Makefile] 15 | indent_size = 8 16 | indent_style = tab 17 | 18 | [{deps}/**] 19 | charset = unset 20 | end_of_line = unset 21 | indent_size = unset 22 | indent_style = unset 23 | trim_trailing_whitespace = unset 24 | 25 | [{test/fixtures,deps,tools/node_modules,tools/gyp,tools/icu,tools/msvs}/**] 26 | insert_final_newline = false 27 | -------------------------------------------------------------------------------- /src/Settings/BlockSettings.js: -------------------------------------------------------------------------------- 1 | export default { 2 | X_NUM : 10, 3 | Y_NUM : 5, 4 | X_SIZE: 50, 5 | Y_SIZE: 20, 6 | COLOR : 0xCCCCCC, 7 | 8 | /** 9 | * キャンバスの横サイズからBlockの初期X座標を返す 10 | * @param {number} canvasWidth 11 | * @returns {number} 12 | */ 13 | getReferenceXPos (canvasWidth) { 14 | return (canvasWidth - (this.X_NUM * (this.X_SIZE + 1))) / 2; 15 | }, 16 | 17 | /** 18 | * キャンバスの縦サイズからBlockの初期Y座標を返す 19 | * @param {number} canvasHeight 20 | * @returns {number} 21 | */ 22 | getReferenceYPos (canvasHeight) { 23 | return canvasHeight / 15; 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | NODE GAME SAMPLE 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # node game practice 2 | 3 | Phaserでお試しで作ったブロック崩し 4 | 5 | https://irokaru.github.io/node-game-practice/ 6 | 7 | ## 環境 8 | 9 | - node 14.12.0 10 | - npm 6.14.8 11 | - yarn 1.22.5 12 | 13 | ## はじめに 14 | 15 | とりあえずパッケージインストール 16 | 17 | ```bash 18 | # node でやる場合 19 | yarn 20 | 21 | # docker-compose でやる場合 22 | docker-compose build 23 | ``` 24 | 25 | ## 開発を始める 26 | 27 | ```bash 28 | # node でやる場合 29 | yarn dev 30 | 31 | # docker-compose でやる場合 32 | docker-compose up -d 33 | ``` 34 | 35 | ## Githubにデプロイする 36 | 37 | ```bash 38 | # node でやる場合 39 | yarn deploy 40 | 41 | # docker-compose でやる場合 42 | docker-compose exec app bash 43 | cd app 44 | yarn deploy 45 | ``` 46 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Phaser from 'phaser'; 2 | 3 | import SystemSettings from './Settings/SystemSettings'; 4 | 5 | import GameScene from './Scenes/GameScene'; 6 | import GameOverScene from './Scenes/GameOverScene'; 7 | 8 | // ----------------------------------------------------- 9 | 10 | const config = { 11 | type: Phaser.AUTO, 12 | parent: 'game', 13 | width: SystemSettings.WIDTH, 14 | height: SystemSettings.HEIGHT, 15 | scene: [GameScene, GameOverScene], 16 | physics: { 17 | default: 'arcade', 18 | arcade: { 19 | gravity: { y: 0 } 20 | } 21 | }, 22 | }; 23 | 24 | // ----------------------------------------------------- 25 | 26 | const game = new Phaser.Game(config); 27 | window.game = game; 28 | window.addEventListener('resize', () => game.scale.refresh()); 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-game-sample", 3 | "version": "0.0.1", 4 | "main": "src/index.js", 5 | "author": "irokaru", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+ssh://git@github.com:irokaru/node-game-practice.git" 9 | }, 10 | "license": "MIT", 11 | "scripts": { 12 | "dev": "webpack-dev-server --mode development --host 0.0.0.0", 13 | "build": "webpack --mode production", 14 | "nwbuild": "nwbuild -p linux64,win64,osx64 -v0.46.2 -o nw/ nw/public/", 15 | "deploy": "npm run build && gh-pages -d public", 16 | "lint": "eslint src" 17 | }, 18 | "devDependencies": { 19 | "babel-core": "^6.26.3", 20 | "babel-loader": "^7.1.5", 21 | "babel-preset-env": "^1.7.0", 22 | "eslint": "^7.10.0", 23 | "gh-pages": "^3.1.0", 24 | "husky": "^4.3.0", 25 | "lint-staged": "^10.4.0", 26 | "nw": "^0.46.1", 27 | "nw-builder": "^3.5.7", 28 | "phaser-assets-webpack-plugin": "^1.0.9", 29 | "tile-extrude-webpack-plugin": "^1.0.0", 30 | "webpack": "^4.27.0", 31 | "webpack-cli": "^3.1.2", 32 | "webpack-dev-server": "^3.1.10", 33 | "write-file-webpack-plugin": "^4.5.1" 34 | }, 35 | "dependencies": { 36 | "phaser": "^3.24.1" 37 | }, 38 | "husky": { 39 | "hooks": { 40 | "pre-commit": "eslint src" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const webpack = require('webpack'); 4 | const path = require('path'); 5 | 6 | const WriteFilePlugin = require('write-file-webpack-plugin'); 7 | const PhaserAssetsWebpackPlugin = require('phaser-assets-webpack-plugin'); 8 | 9 | module.exports = (_env, argv) => ({ 10 | mode: 'production', 11 | entry: { 12 | app: './src/index.js', 13 | vendor: ['phaser'] 14 | }, 15 | output: { 16 | path: path.resolve(__dirname, 'public/js'), 17 | filename: '[name].bundle.js' 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.js$/, 23 | exclude: '/node_modules/', 24 | include: path.resolve(__dirname, 'src/'), 25 | loader: 'babel-loader', 26 | options: { 27 | presets: [ 28 | ['env', { targets: { node: 'current' } }] 29 | ] 30 | } 31 | } 32 | ] 33 | }, 34 | plugins: [ 35 | new WriteFilePlugin(), 36 | // new PhaserAssetsWebpackPlugin([], { useAbsoluteUrl: false }), 37 | ], 38 | devServer: { 39 | contentBase: path.resolve(__dirname, 'public'), 40 | port: 3000, 41 | }, 42 | externals: {}, 43 | optimization: { 44 | splitChunks: { 45 | name: 'vendor', 46 | chunks: 'initial' 47 | } 48 | }, 49 | performance: { hints: false } 50 | }); 51 | -------------------------------------------------------------------------------- /src/Models/BaseRectangle.js: -------------------------------------------------------------------------------- 1 | export default class BaseRectangle extends Phaser.GameObjects.Rectangle { 2 | /** 3 | * 矩形を生成する 4 | * @param {Phaser.Scene} scene 5 | * @param {number} x 6 | * @param {number} y 7 | * @param {number} width 8 | * @param {number} height 9 | * @param {number} fillColor 10 | * @param {number} fillAlpha 11 | * @returns {BaseRectangle} 12 | */ 13 | constructor(scene, x, y, width, height, fillColor, fillAlpha) { 14 | super(scene, x, y, width, height, fillColor, fillAlpha); 15 | scene.add.existing(this); 16 | 17 | this.xMin = this.width / 2; 18 | this.xMax = scene.game.canvas.width - this.width / 2; 19 | this.yMin = this.height / 2; 20 | this.yMax = scene.game.canvas.height - this.height / 2; 21 | 22 | return this; 23 | } 24 | 25 | /** 26 | * Rectangleを相対移動させる 27 | * @param {number} x 28 | * @param {number} y 29 | * @param {boolean} fitting 30 | * @returns {Bar} 31 | */ 32 | moveRelative(x = 0, y = 0, fitting = false) { 33 | this.x += x; 34 | this.y += y; 35 | 36 | if (fitting) { 37 | this.fitInCanvas(); 38 | } 39 | 40 | return this; 41 | } 42 | 43 | /** 44 | * 上限値、下限値に基づいて座標を画面内に収める 45 | * @returns {Bar} 46 | */ 47 | fitInCanvas() { 48 | if (this.x < this.xMin) { 49 | this.x = this.xMin; 50 | } 51 | if (this.x > this.xMax) { 52 | this.x = this.xMax; 53 | } 54 | return this; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Util/Collision.js: -------------------------------------------------------------------------------- 1 | export default class Collision { 2 | /** 3 | * 矩形同士の当たり判定 4 | * @param {Phaser.GameObjects.Rectangle} rect1 5 | * @param {Phaser.GameObjects.Rectangle} rect2 6 | * @returns {boolean} 7 | */ 8 | static rect2rect(rect1, rect2) { 9 | if (rect1.x - rect2.x < rect1.width / 2 + rect2.width / 2 && 10 | rect1.y - rect2.y < rect1.height / 2 + rect2.height / 2) { 11 | return true; 12 | } 13 | 14 | return false; 15 | } 16 | 17 | /** 18 | * 矩形と円の当たり判定 19 | * 円が矩形のどの位置と当たったかを返す 20 | * @param {Phaser.GameObjects.Rectangle} rect 21 | * @param {Phaser.GameObjects.Arc} circle 22 | * @returns {string} 23 | */ 24 | static rect2circle(rect, circle) { 25 | let x = circle.x; 26 | let y = circle.y; 27 | let edge = ''; 28 | 29 | if (circle.x < rect.getLeftCenter().x) { 30 | x = rect.getLeftCenter().x; 31 | edge = 'left'; 32 | } else if (circle.x > rect.getRightCenter().x) { 33 | x = rect.getRightCenter().x; 34 | edge = 'right'; 35 | } 36 | 37 | if (circle.y < rect.getTopCenter().y) { 38 | y = rect.getTopCenter().y; 39 | edge = 'top'; 40 | } else if (circle.y > rect.getBottomCenter().y) { 41 | y = rect.getBottomCenter().y; 42 | edge ='bottom'; 43 | } 44 | 45 | const dx = circle.x - x; 46 | const dy = circle.y - y; 47 | const dist = Math.sqrt((dx*dx) + (dy*dy)); 48 | 49 | if (dist < circle.radius) { 50 | return edge; 51 | } 52 | 53 | return ''; 54 | } 55 | 56 | /** 57 | * 水平方向にぶつかったかどうかを返す 58 | * @param {string} collisionResult 59 | * @returns {boolean} 60 | */ 61 | static isHorizon(collisionResult) { 62 | return collisionResult === 'right' || collisionResult === 'left'; 63 | } 64 | 65 | /** 66 | * 垂直方向にぶつかったかどうかを返す 67 | * @param {string} collisionResult 68 | * @returns {boolean} 69 | */ 70 | static isVertical(collisionResult) { 71 | return collisionResult === 'top' || collisionResult === 'bottom'; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Models/Ball.js: -------------------------------------------------------------------------------- 1 | export default class Ball extends Phaser.GameObjects.Arc { 2 | /** 3 | * ボールを生成する 4 | * @param {Phaser.Scene} scene 5 | * @param {number} x 6 | * @param {number} y 7 | * @param {number} radius 8 | * @returns {Ball} 9 | */ 10 | constructor(scene, x, y, radius, fillColor, fillAlpha) { 11 | super(scene, x, y, radius, 0, 360, false, fillColor, fillAlpha); 12 | scene.add.existing(this); 13 | 14 | this.setVector(0, 0); 15 | 16 | this.xMin = this.radius; 17 | this.xMax = scene.game.canvas.width - this.radius; 18 | this.yMin = this.radius; 19 | this.yMax = scene.game.canvas.height - this.radius; 20 | 21 | return this; 22 | } 23 | 24 | /** 25 | * ベクトルを設定する 26 | * @param {number} x 27 | * @param {number} y 28 | */ 29 | setVector(x = 0, y = 0) { 30 | this.vector = {x: x, y: y}; 31 | return this; 32 | } 33 | 34 | /** 35 | * ベクトルに合わせて動く 36 | * @returns {Ball} 37 | */ 38 | moveByVector() { 39 | return this.moveRelative(this.vector.x, this.vector.y, true); 40 | } 41 | 42 | /** 43 | * Barを相対移動させる 44 | * @param {number} x 45 | * @param {number} y 46 | * @param {boolean} fitting 47 | * @returns {Bar} 48 | */ 49 | moveRelative(x = 0, y = 0, fitting = false) { 50 | this.x += x; 51 | this.y += y; 52 | 53 | if (fitting) { 54 | this.fitInCanvas(); 55 | } 56 | 57 | return this; 58 | } 59 | 60 | /** 61 | * 上限値、下限値に基づいて次回移動時に壁にぶつかるかどうかを検証する 62 | * @returns {boolean} 63 | */ 64 | collisionWallX() { 65 | const x = this.x + this.vector.x; 66 | 67 | if (x < this.xMin || this.xMax < x) { 68 | return true; 69 | } 70 | return false; 71 | } 72 | 73 | /** 74 | * 上限値、下限値に基づいて次回移動時に壁にぶつかるかどうかを検証する 75 | * @returns {boolean} 76 | */ 77 | collisionWallY() { 78 | const y = this.y + this.vector.y; 79 | 80 | if (y < this.yMin || this.yMax < y) { 81 | return true; 82 | } 83 | return false; 84 | } 85 | 86 | /** 87 | * 上限値、下限値に基づいて座標を画面内に収める 88 | * @returns {Bar} 89 | */ 90 | fitInCanvas() { 91 | if (this.x < this.xMin) { 92 | this.x = this.xMin; 93 | } 94 | if (this.x > this.xMax) { 95 | this.x = this.xMax; 96 | } 97 | if (this.y < this.yMin) { 98 | this.y = this.yMin; 99 | } 100 | if (this.y > this.yMax) { 101 | this.y = this.yMax; 102 | } 103 | return this; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Scenes/GameScene.js: -------------------------------------------------------------------------------- 1 | import Ball from '../Models/Ball'; 2 | import Bar from '../Models/Bar'; 3 | import Block from '../Models/Block'; 4 | 5 | import KeyController from '../Repositories/KeyController'; 6 | 7 | import BallSettings from '../Settings/BallSettings'; 8 | import BlockSettings from '../Settings/BlockSettings'; 9 | import BarSettings from '../Settings/BarSettings'; 10 | import SystemSettings from '../Settings/SystemSettings'; 11 | 12 | import Collision from '../Util/Collision'; 13 | 14 | // ----------------------------------------------------- 15 | 16 | const keyInput = new KeyController(); 17 | 18 | export default class GameScene extends Phaser.Scene { 19 | constructor () { 20 | super({ key: 'Game', active: false }); 21 | this.$ = {}; 22 | } 23 | 24 | create() { 25 | // create blocks 26 | this.$.blocks = this.add.group(); 27 | const blockRefX = BlockSettings.getReferenceXPos(SystemSettings.WIDTH); 28 | const blockRefY = BlockSettings.getReferenceYPos(SystemSettings.HEIGHT); 29 | 30 | for (let y = 0; y < BlockSettings.Y_NUM; y++) { 31 | for (let x = 0; x < BlockSettings.X_NUM; x++) { 32 | const xPos = x * (BlockSettings.X_SIZE + 1) + BlockSettings.X_SIZE / 2 + blockRefX; 33 | const yPos = y * (BlockSettings.Y_SIZE + 1) + BlockSettings.Y_SIZE / 2 + blockRefY; 34 | 35 | const block = new Block(this, xPos, yPos, BlockSettings.X_SIZE, BlockSettings.Y_SIZE, BlockSettings.COLOR); 36 | this.$.blocks.add(block); 37 | } 38 | } 39 | 40 | // create bar 41 | const barXpos = BarSettings.getReferenceXPos(SystemSettings.WIDTH); 42 | const barYpos = BarSettings.getReferenceYPos(SystemSettings.HEIGHT); 43 | this.$.bar = new Bar(this, barXpos, barYpos, BarSettings.X_SIZE, BarSettings.Y_SIZE, BarSettings.COLOR); 44 | 45 | // create ball 46 | const ballXpos = BarSettings.getReferenceXPos(SystemSettings.WIDTH); 47 | const ballYpos = BarSettings.getReferenceYPos(SystemSettings.HEIGHT) - 150; 48 | this.$.ball = new Ball(this, ballXpos, ballYpos, BallSettings.RADIUS, BallSettings.COLOR).setVector(3, 3); 49 | 50 | // key setup 51 | keyInput.setup(this); 52 | } 53 | 54 | update() { 55 | const bar = this.$.bar; 56 | const ball = this.$.ball; 57 | 58 | const adjustBallPos = { 59 | top : (blockObj) => blockObj.getTopCenter().y - ball.radius - 1, 60 | bottom: (blockObj) => blockObj.getBottomCenter().y + ball.radius + 1, 61 | left : (blockObj) => blockObj.getLeftCenter().x - ball.radius - 1, 62 | right : (blockObj) => blockObj.getRightCenter().x + ball.radius + 1, 63 | }; 64 | 65 | // controll bar 66 | if (keyInput.isDown('left')) { 67 | bar.moveRelative(-BarSettings.X_SPEED, 0, true); 68 | } 69 | if (keyInput.isDown('right')) { 70 | bar.moveRelative(BarSettings.X_SPEED, 0, true); 71 | } 72 | 73 | // bound ball 74 | if (ball.collisionWallX()) { 75 | ball.setVector(-ball.vector.x, ball.vector.y); 76 | } 77 | if (ball.collisionWallY()) { 78 | ball.setVector(ball.vector.x, -ball.vector.y); 79 | } 80 | 81 | const barCollision = Collision.rect2circle(bar, ball); 82 | if (Collision.isVertical(barCollision)) { 83 | ball.y = adjustBallPos[barCollision](bar); 84 | ball.setVector(ball.vector.x, -ball.vector.y); 85 | } else if (Collision.isHorizon(barCollision)) { 86 | ball.x = adjustBallPos[barCollision](bar); 87 | ball.setVector(-ball.vector.x, ball.vector.y); 88 | } else { 89 | ball.moveByVector(); 90 | } 91 | 92 | for (const block of this.$.blocks.getChildren()) { 93 | const blockCollision = Collision.rect2circle(block, ball); 94 | 95 | if (Collision.isVertical(blockCollision)) { 96 | ball.y = adjustBallPos[blockCollision](block); 97 | ball.setVector(ball.vector.x, -ball.vector.y); 98 | } else if (Collision.isHorizon(blockCollision)) { 99 | ball.x = adjustBallPos[blockCollision](block); 100 | ball.setVector(-ball.vector.x, ball.vector.y); 101 | } 102 | 103 | if (blockCollision !== '') { 104 | block.destroy(); 105 | break; 106 | } 107 | } 108 | 109 | // gameover check 110 | if (this.$.blocks.getChildren().length === 0) { 111 | this.scene.start('GameOver'); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Util/Validator.js: -------------------------------------------------------------------------------- 1 | export default class Validator { 2 | constructor() { 3 | this.$ = this.constructor; 4 | this._data = {}; 5 | this._rules = {}; 6 | 7 | this.$._validateType = { 8 | number: (val) => Validator.isNumber(val), 9 | int: (val) => Validator.isInteger(val), 10 | integer: (val) => Validator.isInteger(val), 11 | string: (val) => Validator.isString(val), 12 | numstring: (val) => Validator.isNumberOnString(val), 13 | intstring: (val) => Validator.isIntegerOnString(val), 14 | bool: (val) => Validator.isBoolean(val), 15 | boolean: (val) => Validator.isBoolean(val), 16 | array: (val) => Validator.isArray(val), 17 | object: (val) => Validator.isObject(val), 18 | callback: (callbacked, args) => Validator.callback(callbacked, args), 19 | }; 20 | 21 | this.$._strPatterns = { 22 | japanese: (val) => Validator.isJapanese(val), 23 | email: (val) => Validator.isEmail(val), 24 | url: (val) => Validator.isUrl(val), 25 | }; 26 | 27 | this.$._validateMin = { 28 | number: (val, limit, gt=true) => Validator.minNumber(val, limit, gt), 29 | int: (val, limit, gt=true) => Validator.minNumber(val, limit, gt), 30 | integer: (val, limit, gt=true) => Validator.minNumber(val, limit, gt), 31 | string: (val, limit, gt=true) => Validator.minLength(val, limit, gt), 32 | numstring: (val, limit, gt=true) => Validator.minLength(val, limit, gt), 33 | intstring: (val, limit, gt=true) => Validator.minLength(val, limit, gt), 34 | bool: () => false, 35 | boolean: () => false, 36 | array: (val, limit, gt=true) => Validator.minArrayLength(val, limit, gt), 37 | object: (val, limit, gt=true) => Validator.minObjectLength(val, limit, gt), 38 | }; 39 | 40 | this.$._validateMax = { 41 | number: (val, limit, lt=true) => Validator.maxNumber(val, limit, lt), 42 | int: (val, limit, lt=true) => Validator.maxNumber(val, limit, lt), 43 | integer: (val, limit, lt=true) => Validator.maxNumber(val, limit, lt), 44 | string: (val, limit, lt=true) => Validator.maxLength(val, limit, lt), 45 | numstring: (val, limit, lt=true) => Validator.maxLength(val, limit, lt), 46 | intstring: (val, limit, lt=true) => Validator.maxLength(val, limit, lt), 47 | bool: () => false, 48 | boolean: () => false, 49 | array: (val, limit, lt=true) => Validator.maxArrayLength(val, limit, lt), 50 | object: (val, limit, lt=true) => Validator.maxObjectLength(val, limit, lt), 51 | }; 52 | } 53 | 54 | /** 55 | * バリデーションのルールを設定する 56 | * @param {object} data 57 | * @param {object} rules 58 | * @returns {this|null} 59 | */ 60 | rules(data, rules) { 61 | if (!Validator.isObject(data) || !Validator.isObject(rules)) { 62 | return null; 63 | } 64 | 65 | if (!Validator.minObjectLength(rules, 1)) { 66 | return null; 67 | } 68 | 69 | // check type 70 | for (const value of Object.values(rules)) { 71 | if (!Validator.inArray(value.type, Object.keys(this.$._validateType))) { 72 | return null; 73 | } 74 | } 75 | 76 | this._data = data; 77 | this._rules = rules; 78 | 79 | return this; 80 | } 81 | 82 | /** 83 | * バリデーションの結果を返す(正しいデータであればtrueが返る) 84 | * @returns {boolean} 85 | */ 86 | exec() { 87 | return Object.entries(this.errors()).length ? false : true; 88 | } 89 | 90 | /** 91 | * バリデーションのエラーを返す 92 | * @param {object} err 93 | * @returns {object} 94 | */ 95 | errors(err = {}) { 96 | for (let [key, rule] of Object.entries(this._rules)) { 97 | const name = rule.name || ''; 98 | const type = rule.type; 99 | const value = this._data[key]; 100 | 101 | err = Validator._resetErrorAsKey(err, key); 102 | 103 | // skip for not exist nullable value 104 | if (!Validator.hasKeyInObject(this._data, key)) { 105 | if (!Validator._checkNullable(rule)) { 106 | const msg = name ? `${name}がありません` : `${key}がありません`; 107 | err = Validator._setError(err, key, msg); 108 | } 109 | continue; 110 | } 111 | 112 | // check type 113 | if (type !== 'callback' && !this.$._validateType[type](value)) { 114 | const msg = name ? `${name}の型が不正です` : `${key}の型が不正です`; 115 | err = Validator._setError(err, key, msg); 116 | continue; 117 | } 118 | 119 | // callback validate 120 | if (type === 'callback') { 121 | err = this._callbackExec(rule, key, value, err); 122 | continue; 123 | } 124 | 125 | // check min 126 | if (Validator.hasKeyInObject(rule, 'min')) { 127 | if (!this.$._validateMin[type](value, rule.min)) { 128 | err = Validator._setError(err, key, Validator._getMinErrorMsg(value, rule.min, name||key)); 129 | } 130 | } 131 | 132 | // check max 133 | if (Validator.hasKeyInObject(rule, 'max')) { 134 | if (!this.$._validateMax[type](value, rule.max)) { 135 | err = Validator._setError(err, key, Validator._getMaxErrorMsg(value, rule.max, name||key)); 136 | } 137 | } 138 | 139 | // check string pattern 140 | if (Validator.hasKeyInObject(rule, 'pattern') && type === 'string') { 141 | if (!this.$._strPatterns[rule.pattern](value)) { 142 | const msg = `正しい形式で入力してください`; 143 | err = Validator._setError(err, key, msg); 144 | } 145 | } 146 | } 147 | 148 | return err; 149 | } 150 | 151 | // ------------------------------------------------------------ 152 | 153 | /** 154 | * 空っぽではないかを判定する 155 | * @param {unknown} val 156 | * @returns {boolean} 157 | */ 158 | static isNotNull(val) { 159 | return val !== undefined && val !== null && val !== ''; 160 | } 161 | 162 | // ------------------------------------------------------------ 163 | 164 | /** 165 | * 数字かどうかを判定する 166 | * @param {unknown} val 167 | * @returns {boolean} 168 | */ 169 | static isNumber(val) { 170 | return typeof val === 'number' && isFinite(val); 171 | } 172 | 173 | /** 174 | * 整数かどうかを判定する 175 | * @param {unknown} val 176 | * @returns {boolean} 177 | */ 178 | static isInteger(val) { 179 | return typeof val === 'number' && Number.isInteger(val); 180 | } 181 | 182 | /** 183 | * 数値が指定値以上か(超過か)どうかを判定する 184 | * @param {number} val 185 | * @param {number} limit 186 | * @param {boolean} gt 187 | * @returns {boolean} 188 | */ 189 | static minNumber(val, limit, gt=true) { 190 | if (!Validator.isNumber(val) || !Validator.isNumber(limit) || !Validator.isBoolean(gt)) { 191 | return false; 192 | } 193 | return gt ? (val >= limit) : (val > limit); 194 | } 195 | 196 | /** 197 | * 数値が指定値以下か(未満か)どうかを判定する 198 | * @param {number} val 199 | * @param {number} limit 200 | * @param {boolean} lt 201 | * @returns {boolean} 202 | */ 203 | static maxNumber(val, limit, lt=true) { 204 | if (!Validator.isNumber(val) || !Validator.isNumber(limit) || !Validator.isBoolean(lt)) { 205 | return false; 206 | } 207 | return lt ? (val <= limit) : (val < limit); 208 | } 209 | 210 | /** 211 | * 数値が指定値の範囲内かどうかを判定する 212 | * @param {number} val 213 | * @param {number} min 214 | * @param {number} max 215 | * @param {boolean} gt 216 | * @param {boolean} lt 217 | * @returns {boolean} 218 | */ 219 | static betweenNumber(val, min, max, gt=true, lt=true) { 220 | return Validator.minNumber(val, min, gt) && Validator.maxNumber(val, max, lt); 221 | } 222 | 223 | // ------------------------------------------------------------ 224 | 225 | /** 226 | * 文字列かどうかを判定する 227 | * @param {unknown} val 228 | * @returns {boolean} 229 | */ 230 | static isString(val) { 231 | return typeof val === 'string'; 232 | } 233 | 234 | /** 235 | * 文字列が整数かどうかを判定する 236 | * @param {string} val 237 | * @returns {boolean} 238 | */ 239 | static isIntegerOnString(val) { 240 | if (!Validator.isString(val)) { 241 | return false; 242 | } 243 | return val.match(/^-?[0-9]*$|^-?[0-9]*\.0*$/) !== null; 244 | } 245 | 246 | /** 247 | * 文字列が数字かどうかを判定する 248 | * @param {string} val 249 | * @returns {boolean} 250 | */ 251 | static isNumberOnString(val) { 252 | if (!Validator.isString(val)) { 253 | return false; 254 | } 255 | const num = Number(val); 256 | return typeof num === 'number' && Number.isFinite(num); 257 | } 258 | 259 | /** 260 | * 日本語のみで構成された文字列かどうかを判定する 261 | * @param {string} val 262 | * @returns {boolean} 263 | */ 264 | static isJapanese(val) { 265 | if (!Validator.isString(val)) { 266 | return false; 267 | } 268 | return val.match(/^[\u30a0-\u30ff\u3040-\u309f\u3005-\u3006\u30e0-\u9fcf]+$/) !== null; 269 | } 270 | 271 | /** 272 | * Eメールかどうかを判定する 273 | * @param {string} val 274 | * @returns {boolean} 275 | */ 276 | static isEmail(val) { 277 | if (!Validator.isString(val)) { 278 | return false; 279 | } 280 | return val.match(/^[A-Za-z0-9]+[\w.-]*@[A-Za-z0-9]+[\w.-]+\.[\w.-]+$/) !== null; 281 | } 282 | 283 | /** 284 | * URLかどうかを判定する 285 | * @param {string} val 286 | * @returns {boolean} 287 | */ 288 | static isUrl(val) { 289 | if (!Validator.isString(val)) { 290 | return false; 291 | } 292 | return val.match(/^https?(:\/\/[-_.!~*¥'()a-zA-Z0-9;¥/?:¥@&=+¥$,%#]+)$/) !== null; 293 | } 294 | 295 | /** 296 | * 文字数が指定値以上か(超過か)どうかを判定する 297 | * @param {string} val 298 | * @param {number} limit 299 | * @param {boolean} gt 300 | * @returns {boolean} 301 | */ 302 | static minLength(val, limit, gt=true) { 303 | if (!Validator.isString(val) || !Validator.isInteger(limit) || limit < 0 || !Validator.isBoolean(gt)) { 304 | return false; 305 | } 306 | return gt ? (val.length >= limit) : (val.length > limit); 307 | } 308 | 309 | /** 310 | * 文字数が指定値以下か(未満か)どうかを判定する 311 | * @param {string} val 312 | * @param {number} limit 313 | * @param {boolean} lt 314 | * @returns {boolean} 315 | */ 316 | static maxLength(val, limit, lt=true) { 317 | if (!Validator.isString(val) || !Validator.isInteger(limit) || limit < 0 || !Validator.isBoolean(lt)) { 318 | return false; 319 | } 320 | return lt ? (val.length <= limit) : (val.length < limit); 321 | } 322 | 323 | /** 324 | * 文字数が指定値の範囲内かどうかを判定する 325 | * @param {string} val 326 | * @param {number} min 327 | * @param {number} max 328 | * @param {boolean} gt 329 | * @param {boolean} lt 330 | * @returns {boolean} 331 | */ 332 | static betweenLength(val, min, max, gt=true, lt=true) { 333 | return Validator.minLength(val, min, gt) && Validator.maxLength(val, max, lt); 334 | } 335 | 336 | // ------------------------------------------------------------ 337 | 338 | /** 339 | * 真偽値かどうかを判定する 340 | * @param {unknown} val 341 | * @returns {boolean} 342 | */ 343 | static isBoolean(val) { 344 | return val === true || val === false || toString.call(val) === "[object Boolean]"; 345 | } 346 | 347 | // ------------------------------------------------------------ 348 | 349 | /** 350 | * 配列かどうかを判定する 351 | * @param {unknown} val 352 | * @returns {boolean} 353 | */ 354 | static isArray(val) 355 | { 356 | return Array.isArray(val); 357 | } 358 | 359 | /** 360 | * 配列の要素数が指定値以上か(超過か)どうかを判定する 361 | * @param {array} val 362 | * @param {number} limit 363 | * @param {boolean} gt 364 | * @returns {boolean} 365 | */ 366 | static minArrayLength(val, limit, gt=true) { 367 | if (!Validator.isArray(val) || 368 | !Validator.isInteger(limit) || limit < 0 || !Validator.isBoolean(gt)) { 369 | return false; 370 | } 371 | 372 | return gt ? (val.length >= limit) : (val.length > limit); 373 | } 374 | 375 | /** 376 | * 配列の要素数が指定値以下か(未満か)どうかを判定する 377 | * @param {array} val 378 | * @param {number} limit 379 | * @param {boolean} lt 380 | * @returns {boolean} 381 | */ 382 | static maxArrayLength(val, limit, lt=true) { 383 | if (!Validator.isArray(val) || 384 | !Validator.isInteger(limit) || limit < 0 || !Validator.isBoolean(lt)) { 385 | return false; 386 | } 387 | 388 | return lt ? (val.length <= limit) : (val.length < limit); 389 | } 390 | 391 | /** 392 | * 配列の要素数が指定値の範囲内かどうかを判定する 393 | * @param {array} val 394 | * @param {number} min 395 | * @param {number} max 396 | * @param {boolean} gt 397 | * @param {boolean} lt 398 | * @returns {boolean} 399 | */ 400 | static betweenArrayLength(val, min, max, gt=true, lt=true) { 401 | return Validator.minArrayLength(val, min, gt) && Validator.maxArrayLength(val, max, lt); 402 | } 403 | 404 | /** 405 | * 配列内に指定した要素が存在するかを判定する 406 | * @param {unknown} value 407 | * @param {array} array 408 | * @param {boolean} fromIndex 409 | * @returns {boolean|number} 410 | */ 411 | static inArray(value, array, fromIndex=false) { 412 | if (!Validator.isArray(array) || !Validator.isBoolean(fromIndex)) { 413 | return false; 414 | } 415 | const ret = [].indexOf.call(array, value); 416 | return fromIndex ? ret : (ret !== -1); 417 | } 418 | 419 | // ------------------------------------------------------------ 420 | 421 | /** 422 | * オブジェクトかどうかを判定する 423 | * @param {unknown} val 424 | * @returns {boolean} 425 | */ 426 | static isObject(val) { 427 | return typeof val === 'object' && val !== null && !Validator.isArray(val); 428 | } 429 | 430 | /** 431 | * オブジェクトの中にキーが有るかどうかを判定する 432 | * @param {object} obj 433 | * @param {string} key 434 | * @returns {boolean} 435 | */ 436 | static hasKeyInObject(obj, key) { 437 | if (!Validator.isObject(obj) || !Validator.isString(key)) { 438 | return false; 439 | } 440 | return Object.prototype.hasOwnProperty.call(obj, key); 441 | } 442 | 443 | /** 444 | * オブジェクトの要素数が指定値以上か(超過か)どうかを判定する 445 | * @param {object} val 446 | * @param {number} limit 447 | * @param {boolean} gt 448 | * @returns {boolean} 449 | */ 450 | static minObjectLength(val, limit, gt=true) { 451 | if (!Validator.isObject(val) || 452 | !Validator.isInteger(limit) || limit < 0 || !Validator.isBoolean(gt)) { 453 | return false; 454 | } 455 | 456 | return gt ? (Object.keys(val).length >= limit) : (Object.keys(val).length > limit); 457 | } 458 | 459 | /** 460 | * オブジェクトの要素数が指定値以下か(未満か)どうかを判定する 461 | * @param {object} val 462 | * @param {number} limit 463 | * @param {boolean} lt 464 | * @returns {boolean} 465 | */ 466 | static maxObjectLength(val, limit, lt=true) { 467 | if (!Validator.isObject(val) || 468 | !Validator.isInteger(limit) || limit < 0 || !Validator.isBoolean(lt)) { 469 | return false; 470 | } 471 | 472 | return lt ? (Object.keys(val).length <= limit) : (Object.keys(val).length < limit); 473 | } 474 | 475 | /** 476 | * オブジェクトの要素数が指定値の範囲内かどうかを判定する 477 | * @param {object} val 478 | * @param {number} min 479 | * @param {number} max 480 | * @param {boolean} gt 481 | * @param {boolean} lt 482 | * @returns {boolean} 483 | */ 484 | static betweenObjectLength(val, min, max, gt=true, lt=true) { 485 | return Validator.minObjectLength(val, min, gt) && Validator.maxObjectLength(val, max, lt); 486 | } 487 | 488 | // ------------------------------------------------------------ 489 | 490 | /** 491 | * 独自指定したバリデーションを利用して判定する 492 | * バリデーションの結果はエラー内容の配列であること 493 | * @param {function} callbacked 494 | * @param {array} args 495 | * @returns {array} 496 | */ 497 | static callback(callbacked, args) { 498 | return callbacked(...args); 499 | } 500 | 501 | // ------------------------------------------------------------ 502 | 503 | /** 504 | * nullableが有効かどうかを返す 505 | * @param {object} rule 506 | * @returns {boolean} 507 | */ 508 | static _checkNullable(rule) { 509 | if (!Validator.isObject(rule) || !Validator.hasKeyInObject(rule, 'nullable') || 510 | !Validator.isBoolean(rule.nullable)) { 511 | return false; 512 | } 513 | return rule.nullable; 514 | } 515 | 516 | /** 517 | * エラーの内容を追加する 518 | * @param {object} err 519 | * @param {string} key 520 | * @param {string} msg 521 | * @returns {object} 522 | */ 523 | static _setError(err, key, msg) { 524 | if (!Validator.hasKeyInObject(err, key)) { 525 | err = Validator._resetErrorAsKey(err, key, true); 526 | } 527 | err[key].push(msg); 528 | return err; 529 | } 530 | 531 | /** 532 | * エラー用オブジェクトの中身をキーを基にリセットする 533 | * @param {object} err 534 | * @param {string} key 535 | * @param {boolean} force 536 | * @returns {object} 537 | */ 538 | static _resetErrorAsKey(err, key, force = false) { 539 | if (Validator.hasKeyInObject(err, key) || force) { 540 | err[key] = []; 541 | } 542 | return err; 543 | } 544 | 545 | /** 546 | * コールバック用バリデーションを実行する 547 | * @param {object} rule 548 | * @param {string} key 549 | * @param {unknown} value 550 | * @param {object} err 551 | * @returns {object} 552 | */ 553 | _callbackExec(rule, key, value, err) { 554 | const name = rule.name || ''; 555 | const type = rule.type; 556 | const func = rule.callback || null; 557 | 558 | if (func === null) { 559 | err = Validator._setError(err, key, 'not set callback function'); 560 | return err; 561 | } 562 | 563 | const callbackErrors = this.$._validateType[type](func, [value, name||key]); 564 | 565 | if (!Validator.isArray(callbackErrors)) { 566 | err = Validator._setError(err, key, 'function return type is not array'); 567 | return err; 568 | } 569 | 570 | if (Validator.minArrayLength(callbackErrors, 1)) { 571 | for (const error of callbackErrors) { 572 | err = Validator._setError(err, key, error); 573 | } 574 | } 575 | 576 | return err; 577 | } 578 | 579 | /** 580 | * min用エラー文を生成する 581 | * @param {unknown} value 582 | * @param {number} limit 583 | * @param {string} name 584 | * @returns {string} 585 | */ 586 | static _getMinErrorMsg(value, limit, name='') { 587 | if (Validator.isString(value)) { 588 | return name ? `${name}は${limit}文字以上にしてください` : `${limit}文字以上にしてください`; 589 | } else if (Validator.isNumber(value)) { 590 | return name ? `${name}は${limit}以上にしてください` : `${limit}以上にしてください`; 591 | } else if (Validator.isArray(value) || Validator.isObject(value)) { 592 | return name ? `${name}は${limit}要素以上にしてください` : `${limit}要素以上にしてください`; 593 | } else if (Validator.isBoolean(value)) { 594 | return name ? `${name}は真偽値です` : `真偽値です`; 595 | } 596 | return ''; 597 | } 598 | 599 | /** 600 | * max用エラー文を生成する 601 | * @param {unknown} value 602 | * @param {number} limit 603 | * @param {string} name 604 | * @returns {string} 605 | */ 606 | static _getMaxErrorMsg(value, limit, name='') { 607 | if (Validator.isString(value)) { 608 | return name ? `${name}は${limit}文字以内にしてください` : `${limit}文字以内にしてください`; 609 | } else if (Validator.isNumber(value)) { 610 | return name ? `${name}は${limit}以下にしてください` : `${limit}以下にしてください`; 611 | } else if (Validator.isArray(value) || Validator.isObject(value)) { 612 | return name ? `${name}は${limit}要素以下にしてください` : `${limit}要素以下にしてください`; 613 | } else if (Validator.isBoolean(value)) { 614 | return name ? `${name}は真偽値です` : `真偽値です`; 615 | } 616 | return ''; 617 | } 618 | } 619 | --------------------------------------------------------------------------------