board-game-engine-react 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +24 -0
- package/.storybook/addons.js +1 -0
- package/.storybook/main.js +18 -0
- package/.storybook/webpack.config.js +24 -0
- package/README.md +4 -0
- package/babel.config.js +9 -0
- package/board-game-engine-react.test.js +12 -0
- package/dist/board-game-engine-react.css +2 -0
- package/dist/board-game-engine-react.js +20 -0
- package/dist/board-game-engine-react.min.css +1 -0
- package/dist/board-game-engine-react.min.js +1 -0
- package/jest.config.js +13 -0
- package/package.json +86 -0
- package/rollup.config.js +42 -0
- package/src/BoardGameEngineReact.js +12 -0
- package/src/abstract-choices/abstract-choices.js +45 -0
- package/src/board/grid.js +28 -0
- package/src/contexts/game-context.js +35 -0
- package/src/entity/entity.js +37 -0
- package/src/game/game.js +56 -0
- package/src/game-status/game-status.js +20 -0
- package/src/index.js +3 -0
- package/src/space/space.js +68 -0
- package/src/styles.scss +4 -0
- package/stories/BoardGameEngineReact.stories.js +52 -0
- package/stories/static/blue.png +0 -0
- package/stories/static/red.png +0 -0
- package/stories/styles.css +0 -0
package/.eslintrc.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
env: {
|
|
3
|
+
browser: true,
|
|
4
|
+
es6: true
|
|
5
|
+
},
|
|
6
|
+
extends: ['plugin:react/recommended', 'standard', 'plugin:storybook/recommended'],
|
|
7
|
+
globals: {
|
|
8
|
+
Atomics: 'readonly',
|
|
9
|
+
SharedArrayBuffer: 'readonly'
|
|
10
|
+
},
|
|
11
|
+
parserOptions: {
|
|
12
|
+
ecmaFeatures: {
|
|
13
|
+
jsx: true
|
|
14
|
+
},
|
|
15
|
+
ecmaVersion: 'latest',
|
|
16
|
+
sourceType: 'module'
|
|
17
|
+
},
|
|
18
|
+
plugins: [
|
|
19
|
+
'react'
|
|
20
|
+
],
|
|
21
|
+
rules: {
|
|
22
|
+
'operator-linebreak': ['error', 'before']
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@storybook/addon-controls/register';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** @type { import('@storybook/react-webpack5').StorybookConfig } */
|
|
2
|
+
const config = {
|
|
3
|
+
stories: ["../stories/**/*.stories.@(js|jsx|ts|tsx)"],
|
|
4
|
+
addons: [
|
|
5
|
+
"@storybook/addon-webpack5-compiler-swc",
|
|
6
|
+
"@storybook/addon-onboarding",
|
|
7
|
+
"@storybook/addon-essentials",
|
|
8
|
+
"@chromatic-com/storybook",
|
|
9
|
+
"@storybook/addon-interactions",
|
|
10
|
+
"@storybook/preset-scss"
|
|
11
|
+
],
|
|
12
|
+
framework: {
|
|
13
|
+
name: "@storybook/react-webpack5",
|
|
14
|
+
options: {},
|
|
15
|
+
},
|
|
16
|
+
staticDirs: ['../stories/static'],
|
|
17
|
+
};
|
|
18
|
+
export default config;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
|
|
3
|
+
const modulesToTranspile = [
|
|
4
|
+
'react-dialogue-tree'
|
|
5
|
+
]
|
|
6
|
+
|
|
7
|
+
module.exports = ({ config, mode }) => {
|
|
8
|
+
config.resolve.alias['react-yarn-visual-novel'] = mode === 'PRODUCTION'
|
|
9
|
+
? path.resolve(__dirname, '../dist/react-yarn-visual-novel.min.js')
|
|
10
|
+
: path.resolve(__dirname, '../src/index.js')
|
|
11
|
+
|
|
12
|
+
// brittle
|
|
13
|
+
const babelLoader = config.module.rules[config.module.rules.length - 1]
|
|
14
|
+
babelLoader.exclude.shift()
|
|
15
|
+
babelLoader.exclude.unshift(new RegExp(
|
|
16
|
+
`node_modules\/(?!${modulesToTranspile.join('|')}).+`)
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
config.resolve.fallback = {
|
|
20
|
+
fs: false,
|
|
21
|
+
path: false,
|
|
22
|
+
}
|
|
23
|
+
return config
|
|
24
|
+
}
|
package/README.md
ADDED
package/babel.config.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/* eslint-env jest */
|
|
2
|
+
|
|
3
|
+
import '@testing-library/jest-dom/extend-expect'
|
|
4
|
+
import React from 'react'
|
|
5
|
+
import { render, screen } from '@testing-library/react'
|
|
6
|
+
import userEvent from '@testing-library/user-event'
|
|
7
|
+
|
|
8
|
+
describe('board-game-engine-react', () => {
|
|
9
|
+
test('has a test', async () => {
|
|
10
|
+
return true
|
|
11
|
+
})
|
|
12
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
(function (global, factory) {
|
|
2
|
+
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('react')) :
|
|
3
|
+
typeof define === 'function' && define.amd ? define(['react'], factory) :
|
|
4
|
+
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.BoardGameEngineReact = factory(global.React));
|
|
5
|
+
})(this, (function (React) { 'use strict';
|
|
6
|
+
|
|
7
|
+
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
8
|
+
|
|
9
|
+
var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
|
|
10
|
+
|
|
11
|
+
function BoardGameEngineReact({}) {
|
|
12
|
+
return /*#__PURE__*/React__default["default"].createElement("div", {
|
|
13
|
+
className: "board-game-engine-react"
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
BoardGameEngineReact.propTypes = {};
|
|
17
|
+
|
|
18
|
+
return BoardGameEngineReact;
|
|
19
|
+
|
|
20
|
+
}));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
html,body,#storybook-root,.story{height:100%}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("react")):"function"==typeof define&&define.amd?define(["react"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).BoardGameEngineReact=t(e.React)}(this,function(e){"use strict";function t(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var n=t(e);function o({}){return n.default.createElement("div",{className:"board-game-engine-react"})}return o.propTypes={},o});
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
transform: {
|
|
3
|
+
'^.+\\.(js|jsx)$': 'babel-jest'
|
|
4
|
+
},
|
|
5
|
+
testEnvironment: 'jsdom',
|
|
6
|
+
transformIgnorePatterns: [
|
|
7
|
+
'/node_modules/(?!yarn-bound|@mnbroatch|react-dialogue-tree).+\\.js$'
|
|
8
|
+
],
|
|
9
|
+
moduleNameMapper: {
|
|
10
|
+
'^.+\\.(css|less|scss)$': 'babel-jest'
|
|
11
|
+
},
|
|
12
|
+
clearMocks: true
|
|
13
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "board-game-engine-react",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "React library for using board-game-engine",
|
|
5
|
+
"main": "dist/board-game-engine-react.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "storybook dev p 6006",
|
|
8
|
+
"test": "jest --verbose false",
|
|
9
|
+
"test:coverage": "jest --coverage --verbose false",
|
|
10
|
+
"build": "rollup -c",
|
|
11
|
+
"storybook": "storybook dev -p 6006",
|
|
12
|
+
"prepare": "npm run build",
|
|
13
|
+
"build-storybook": "storybook build"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/mnbroatch/board-game-engine-react.git"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"react",
|
|
21
|
+
"board game",
|
|
22
|
+
"boardgame",
|
|
23
|
+
"ui"
|
|
24
|
+
],
|
|
25
|
+
"author": "Matthew Broatch",
|
|
26
|
+
"license": "ISC",
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/mnbroatch/board-game-engine-react/issues"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/mnbroatch/board-game-engine-react",
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@babel/core": "^7.20.12",
|
|
33
|
+
"@babel/plugin-proposal-object-rest-spread": "",
|
|
34
|
+
"@babel/preset-env": "^7.26.0",
|
|
35
|
+
"@babel/preset-react": "",
|
|
36
|
+
"@chromatic-com/storybook": "^3.2.2",
|
|
37
|
+
"@rollup/plugin-babel": "",
|
|
38
|
+
"@rollup/plugin-commonjs": "",
|
|
39
|
+
"@rollup/plugin-json": "",
|
|
40
|
+
"@rollup/plugin-node-resolve": "^13.0.0",
|
|
41
|
+
"@storybook/addon-essentials": "^8.4.6",
|
|
42
|
+
"@storybook/addon-interactions": "^8.4.6",
|
|
43
|
+
"@storybook/addon-onboarding": "^8.4.6",
|
|
44
|
+
"@storybook/addon-webpack5-compiler-swc": "^1.0.5",
|
|
45
|
+
"@storybook/blocks": "^8.4.6",
|
|
46
|
+
"@storybook/preset-scss": "^1.0.3",
|
|
47
|
+
"@storybook/react": "^8.4.6",
|
|
48
|
+
"@storybook/react-webpack5": "^8.4.6",
|
|
49
|
+
"@storybook/test": "^8.4.6",
|
|
50
|
+
"@testing-library/jest-dom": "^5.16.1",
|
|
51
|
+
"@testing-library/react": "^13.4.0",
|
|
52
|
+
"@testing-library/user-event": "^13.5.0",
|
|
53
|
+
"assert": "^2.0.0",
|
|
54
|
+
"babel-jest": "^27.4.4",
|
|
55
|
+
"babel-loader": "^8.3.0",
|
|
56
|
+
"eslint": "",
|
|
57
|
+
"eslint-config-standard": "",
|
|
58
|
+
"eslint-plugin-import": "",
|
|
59
|
+
"eslint-plugin-node": "",
|
|
60
|
+
"eslint-plugin-promise": "",
|
|
61
|
+
"eslint-plugin-react": "",
|
|
62
|
+
"eslint-plugin-storybook": "^0.11.1",
|
|
63
|
+
"install": "^0.13.0",
|
|
64
|
+
"jest": "^27.4.3",
|
|
65
|
+
"markdown-to-jsx": "",
|
|
66
|
+
"node-sass": "^7.0.1",
|
|
67
|
+
"npm": "^8.3.1",
|
|
68
|
+
"prop-types": "^15.8.1",
|
|
69
|
+
"raw-loader": "",
|
|
70
|
+
"react-dom": "^18.2.0",
|
|
71
|
+
"react-syntax-highlighter": "",
|
|
72
|
+
"react-useinterval": "",
|
|
73
|
+
"rollup": "^2.63.0",
|
|
74
|
+
"rollup-plugin-node-resolve": "^5.2.0",
|
|
75
|
+
"rollup-plugin-scss": "^3.0.0",
|
|
76
|
+
"rollup-plugin-terser": "",
|
|
77
|
+
"sass-loader": "^12.4.0",
|
|
78
|
+
"source-loader": "",
|
|
79
|
+
"storybook": "^8.4.6"
|
|
80
|
+
},
|
|
81
|
+
"dependencies": {
|
|
82
|
+
"react": "^18.2.0",
|
|
83
|
+
"react-error-boundary": "^3.1.4",
|
|
84
|
+
"react-transition-group": "^4.4.5"
|
|
85
|
+
}
|
|
86
|
+
}
|
package/rollup.config.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import babel from '@rollup/plugin-babel'
|
|
2
|
+
import { terser } from 'rollup-plugin-terser'
|
|
3
|
+
import scss from 'rollup-plugin-scss'
|
|
4
|
+
import resolve from 'rollup-plugin-node-resolve'
|
|
5
|
+
|
|
6
|
+
const config = [
|
|
7
|
+
{
|
|
8
|
+
input: 'src/index.js',
|
|
9
|
+
external: ['react'],
|
|
10
|
+
output: {
|
|
11
|
+
format: 'umd',
|
|
12
|
+
file: 'dist/board-game-engine-react.js',
|
|
13
|
+
name: 'BoardGameEngineReact'
|
|
14
|
+
},
|
|
15
|
+
plugins: [
|
|
16
|
+
resolve({
|
|
17
|
+
}),
|
|
18
|
+
babel({
|
|
19
|
+
}),
|
|
20
|
+
scss()
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
input: 'src/index.js',
|
|
25
|
+
external: ['react'],
|
|
26
|
+
output: {
|
|
27
|
+
format: 'umd',
|
|
28
|
+
file: 'dist/board-game-engine-react.min.js',
|
|
29
|
+
name: 'BoardGameEngineReact'
|
|
30
|
+
},
|
|
31
|
+
plugins: [
|
|
32
|
+
resolve({
|
|
33
|
+
}),
|
|
34
|
+
babel({
|
|
35
|
+
}),
|
|
36
|
+
scss({ outputStyle: 'compressed' }),
|
|
37
|
+
terser()
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
export default config
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { useGame } from "../contexts/game-context.js";
|
|
3
|
+
|
|
4
|
+
export default function AbstractChoices () {
|
|
5
|
+
const { clickTarget, allClickable, undoStep, currentMoveTargets } = useGame()
|
|
6
|
+
|
|
7
|
+
const abstractChoices = [...allClickable].filter(c => c.abstract)
|
|
8
|
+
|
|
9
|
+
// spacer assumes only one row of choices.
|
|
10
|
+
// could save and store biggest height instead?
|
|
11
|
+
return (
|
|
12
|
+
<div style={{ position: 'relative' }}>
|
|
13
|
+
<button
|
|
14
|
+
style={{visibility: 'hidden'}}
|
|
15
|
+
className="button button--style-b button--x-small abstract-choices__choice"
|
|
16
|
+
>
|
|
17
|
+
Spacer
|
|
18
|
+
</button>
|
|
19
|
+
<div
|
|
20
|
+
style={{
|
|
21
|
+
position: 'absolute',
|
|
22
|
+
top: 0,
|
|
23
|
+
width: '100%',
|
|
24
|
+
}}>
|
|
25
|
+
{!!currentMoveTargets.length && (
|
|
26
|
+
<button
|
|
27
|
+
className="button button--style-c button--x-small abstract-choices__choice abstract-choices__choice--undo"
|
|
28
|
+
onClick={undoStep}
|
|
29
|
+
>
|
|
30
|
+
Undo
|
|
31
|
+
</button>
|
|
32
|
+
)}
|
|
33
|
+
{abstractChoices.map((choice, i) => (
|
|
34
|
+
<button
|
|
35
|
+
key={i}
|
|
36
|
+
className="button button--style-b button--x-small abstract-choices__choice"
|
|
37
|
+
onClick={() => clickTarget(choice)}
|
|
38
|
+
>
|
|
39
|
+
{choice.value}
|
|
40
|
+
</button>
|
|
41
|
+
))}
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Entity from "../entity/entity.js";
|
|
3
|
+
|
|
4
|
+
export default function Grid ({ grid }) {
|
|
5
|
+
const { width, height, spaces } = grid.attributes;
|
|
6
|
+
return (
|
|
7
|
+
<div
|
|
8
|
+
className="grid"
|
|
9
|
+
style={{
|
|
10
|
+
display: 'inline-grid',
|
|
11
|
+
width: '100%',
|
|
12
|
+
gridTemplateColumns: `repeat(${width}, 1fr)`,
|
|
13
|
+
gridTemplateRows: `repeat(${height}, 1fr)`,
|
|
14
|
+
}}
|
|
15
|
+
>
|
|
16
|
+
{spaces.map((space, index) => {
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
key={index}
|
|
20
|
+
className="grid__cell"
|
|
21
|
+
>
|
|
22
|
+
<Entity entity={space} />
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
})}
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React, { createContext, useContext, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
const GameContext = createContext({
|
|
4
|
+
clickTarget: () => {},
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
// do we need isSpectator
|
|
8
|
+
export function GameProvider({ gameConnection, children, isSpectator }) {
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (gameConnection.state._stateID === 0) {
|
|
11
|
+
gameConnection.reset()
|
|
12
|
+
}
|
|
13
|
+
}, [gameConnection.state._stateID]);
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<GameContext.Provider value={{
|
|
17
|
+
clickTarget: target => {
|
|
18
|
+
if (!isSpectator) {
|
|
19
|
+
gameConnection.doStep(target)
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
undoStep: () => { gameConnection.undoStep() },
|
|
23
|
+
allClickable: (gameConnection.optimisticWinner || isSpectator)
|
|
24
|
+
? new Set()
|
|
25
|
+
: gameConnection.allClickable,
|
|
26
|
+
currentMoveTargets: (gameConnection.optimisticWinner || isSpectator)
|
|
27
|
+
? []
|
|
28
|
+
: gameConnection.moveBuilder.targets,
|
|
29
|
+
}}>
|
|
30
|
+
{children}
|
|
31
|
+
</GameContext.Provider>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const useGame = () => useContext(GameContext);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { useGame } from "../../contexts/game-context.js";
|
|
3
|
+
import Grid from '../board/grid.js'
|
|
4
|
+
import Space from "../space/space.js";
|
|
5
|
+
|
|
6
|
+
export default function Entity ({ entity }) {
|
|
7
|
+
const { clickTarget, allClickable } = useGame()
|
|
8
|
+
const isClickable = allClickable.has(entity)
|
|
9
|
+
const attributes = entity.attributes
|
|
10
|
+
|
|
11
|
+
switch (attributes.type) {
|
|
12
|
+
case 'Grid':
|
|
13
|
+
return <Grid grid={entity} isClickable={isClickable} />
|
|
14
|
+
case 'Space':
|
|
15
|
+
return <Space space={entity} isClickable={isClickable} />
|
|
16
|
+
default:
|
|
17
|
+
return <div
|
|
18
|
+
onClick={(e) => {
|
|
19
|
+
if (isClickable) {
|
|
20
|
+
e.stopPropagation()
|
|
21
|
+
clickTarget(entity)
|
|
22
|
+
}
|
|
23
|
+
}}
|
|
24
|
+
className={[
|
|
25
|
+
'entity',
|
|
26
|
+
attributes.player && `player-${attributes.player}`,
|
|
27
|
+
allClickable.has(entity) && 'entity--clickable',
|
|
28
|
+
].filter(Boolean).join(' ')}
|
|
29
|
+
>
|
|
30
|
+
{entity.rule.displayProperties?.map((property, i) => (
|
|
31
|
+
<div key={i}>
|
|
32
|
+
{property}: {(entity.attributes[property])?.toString()}
|
|
33
|
+
</div>
|
|
34
|
+
))}
|
|
35
|
+
</div>
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/game/game.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import Entity from '../entity/entity.js'
|
|
3
|
+
import AbstractChoices from '../abstract-choices/abstract-choices.js'
|
|
4
|
+
import GameStatus from '../game-status/game-status.js'
|
|
5
|
+
import { GameProvider } from "../../contexts/game-context.js";
|
|
6
|
+
|
|
7
|
+
export default function Game ({ gameConnection }) {
|
|
8
|
+
console.log('555gameConnection', gameConnection)
|
|
9
|
+
const { G } = gameConnection.state
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<GameProvider
|
|
13
|
+
gameConnection={gameConnection}
|
|
14
|
+
isSpectator
|
|
15
|
+
>
|
|
16
|
+
<div className="game">
|
|
17
|
+
<AbstractChoices />
|
|
18
|
+
<div
|
|
19
|
+
className="shared-board"
|
|
20
|
+
style={{
|
|
21
|
+
width: '100%',
|
|
22
|
+
display: 'flex',
|
|
23
|
+
flexWrap: 'wrap',
|
|
24
|
+
justifyContent: 'center',
|
|
25
|
+
alignItems: 'center',
|
|
26
|
+
gap: '1em',
|
|
27
|
+
}}
|
|
28
|
+
>
|
|
29
|
+
{G.sharedBoard.entities.map((entity, i) => <Entity key={i} entity={entity} />)}
|
|
30
|
+
</div>
|
|
31
|
+
{G.personalBoards && (
|
|
32
|
+
<div className="personal-boards">
|
|
33
|
+
{G.personalBoards.map((board, i) => (
|
|
34
|
+
<div
|
|
35
|
+
key={i}
|
|
36
|
+
className="personal-board"
|
|
37
|
+
style={{
|
|
38
|
+
width: '100%',
|
|
39
|
+
display: 'grid',
|
|
40
|
+
gridAutoFlow: 'column',
|
|
41
|
+
gridAutoRows: '1fr',
|
|
42
|
+
gap: '1em',
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
{board.entities.map((entity, j) => (
|
|
46
|
+
<Entity key={j} entity={entity} />
|
|
47
|
+
))}
|
|
48
|
+
</div>
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
)}
|
|
52
|
+
<GameStatus gameConnection={gameConnection} />
|
|
53
|
+
</div>
|
|
54
|
+
</GameProvider>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
export default function GameStatus ({ gameConnection }) {
|
|
4
|
+
const players = gameConnection.client.matchData
|
|
5
|
+
const winner = gameConnection.state.ctx.gameover?.winner
|
|
6
|
+
const draw = gameConnection.state.ctx.gameover?.draw
|
|
7
|
+
let winnerString = ''
|
|
8
|
+
if (draw) {
|
|
9
|
+
winnerString = 'Draw!'
|
|
10
|
+
} else if (players && winner) {
|
|
11
|
+
winnerString = `${players[winner].name} Wins!`
|
|
12
|
+
} else if (winner) {
|
|
13
|
+
winnerString = `Player ${winner} Wins!`
|
|
14
|
+
}
|
|
15
|
+
return gameConnection.state.ctx.gameover && (
|
|
16
|
+
<div className="game-status">
|
|
17
|
+
{winnerString}
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
20
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useGame } from "../../contexts/game-context.js";
|
|
3
|
+
import Entity from '../entity/entity.js'
|
|
4
|
+
|
|
5
|
+
function calculateOptimalCols(numSquares) {
|
|
6
|
+
if (numSquares === 0) return 1;
|
|
7
|
+
|
|
8
|
+
let bestCols = 1;
|
|
9
|
+
let bestAspectRatio = Infinity;
|
|
10
|
+
|
|
11
|
+
for (let cols = 1; cols <= numSquares; cols++) {
|
|
12
|
+
const rows = Math.ceil(numSquares / cols);
|
|
13
|
+
const aspectRatio = Math.abs(cols - rows);
|
|
14
|
+
|
|
15
|
+
if (aspectRatio < bestAspectRatio) {
|
|
16
|
+
bestAspectRatio = aspectRatio;
|
|
17
|
+
bestCols = cols;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return bestCols;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function Space ({ space }) {
|
|
25
|
+
const { clickTarget, allClickable, currentMoveTargets } = useGame()
|
|
26
|
+
const { entities, entityId } = space.attributes
|
|
27
|
+
|
|
28
|
+
const clickable = [...allClickable].map(e => e.entityId).includes(entityId)
|
|
29
|
+
const targeted = currentMoveTargets?.map(e => e.entityId).includes(entityId)
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<a
|
|
33
|
+
className={[
|
|
34
|
+
'space',
|
|
35
|
+
clickable && 'space--clickable',
|
|
36
|
+
targeted && 'space--targeted'
|
|
37
|
+
].filter(Boolean).join(' ')}
|
|
38
|
+
onClick={() => clickTarget(space)}
|
|
39
|
+
style={{
|
|
40
|
+
display: 'inline-block',
|
|
41
|
+
flex: '1',
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<div
|
|
45
|
+
className="space__entity-grid"
|
|
46
|
+
style={{
|
|
47
|
+
display: 'flex',
|
|
48
|
+
height: '100%',
|
|
49
|
+
width: '100%',
|
|
50
|
+
flexWrap: 'wrap',
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
{Array.from({ length: entities.length }, (_, i) => (
|
|
54
|
+
<div
|
|
55
|
+
className="space__entity-grid__cell"
|
|
56
|
+
style={{
|
|
57
|
+
display: 'inline-block',
|
|
58
|
+
}}
|
|
59
|
+
key={i}
|
|
60
|
+
>
|
|
61
|
+
<Entity entity={entities[i]} />
|
|
62
|
+
</div>
|
|
63
|
+
))}
|
|
64
|
+
{!entities.length && space.attributes.name}
|
|
65
|
+
</div>
|
|
66
|
+
</a>
|
|
67
|
+
);
|
|
68
|
+
}
|
package/src/styles.scss
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import BoardGameEngineReact from '../src/index'
|
|
3
|
+
import { ErrorBoundary } from 'react-error-boundary'
|
|
4
|
+
import './styles.css'
|
|
5
|
+
|
|
6
|
+
const dialogue = `
|
|
7
|
+
title: Start
|
|
8
|
+
---
|
|
9
|
+
Red: Hello.
|
|
10
|
+
Blue: Hi!
|
|
11
|
+
Red: I'm here.
|
|
12
|
+
Blue: I'm here too!
|
|
13
|
+
===
|
|
14
|
+
`
|
|
15
|
+
|
|
16
|
+
export default {
|
|
17
|
+
title: 'BoardGameEngineReact',
|
|
18
|
+
component: BoardGameEngineReact,
|
|
19
|
+
className: 'board-game-engine-react',
|
|
20
|
+
args: {
|
|
21
|
+
dialogue,
|
|
22
|
+
},
|
|
23
|
+
argTypes: {
|
|
24
|
+
dialogue: {
|
|
25
|
+
control: 'text'
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const Template = (props) => {
|
|
31
|
+
return (
|
|
32
|
+
<div className="story">
|
|
33
|
+
<ErrorBoundary
|
|
34
|
+
resetKeys={[props.dialogue]}
|
|
35
|
+
fallbackRender={({ error }) => {
|
|
36
|
+
return (
|
|
37
|
+
<div>
|
|
38
|
+
Invalid Dialogue: {error.message}
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
<BoardGameEngineReact
|
|
44
|
+
{...props}
|
|
45
|
+
onDialogueEnd={() => { alert('onDialogueEnd called') }}
|
|
46
|
+
/>
|
|
47
|
+
</ErrorBoundary>
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const Basic = Template.bind({})
|
|
Binary file
|
|
Binary file
|
|
File without changes
|