├── .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 |
--------------------------------------------------------------------------------