diff options
Diffstat (limited to 'diplomacy')
21 files changed, 662 insertions, 241 deletions
diff --git a/diplomacy/web/package-lock.json b/diplomacy/web/package-lock.json index 6f65557..077f9fe 100644 --- a/diplomacy/web/package-lock.json +++ b/diplomacy/web/package-lock.json @@ -926,14 +926,6 @@ "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-9.0.1.tgz", "integrity": "sha512-6It2EVfGskxZCQhuykrfnALg7oVeiI6KclWSmGDqB0AiInVrTGB9Jp9i4/Ad21u9Jde/voVQz6eFX/eSg/UsPA==" }, - "@githubprimer/octicons-react": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@githubprimer/octicons-react/-/octicons-react-8.5.0.tgz", - "integrity": "sha512-Tb9Nu4usHON3EGiyCnppNQoJmSJvhUw+iD1URpOsoc69YWv96jIcxUAU4/Tw/D6ydzOubf3H9kPdLKVqz8XOnQ==", - "requires": { - "prop-types": "^15.6.1" - } - }, "@hapi/address": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.0.0.tgz", @@ -1198,6 +1190,14 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==" }, + "@primer/octicons-react": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@primer/octicons-react/-/octicons-react-9.1.1.tgz", + "integrity": "sha512-+ZgALoxUOYUeEnqqN6ZqSfRP6LDRgfmErhY4ZIuGlw5Ocjj7AI87J68dD/wYqWl4IW7xE6rmLvpC3kU3iGmAfQ==", + "requires": { + "prop-types": "^15.6.1" + } + }, "@svgr/babel-plugin-add-jsx-attribute": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz", @@ -10079,6 +10079,11 @@ "whatwg-fetch": "3.0.0" } }, + "react-confirm-alert": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-confirm-alert/-/react-confirm-alert-2.4.1.tgz", + "integrity": "sha512-Sc2N1paCTCS5HWEAhik2IQa9/vwSQLAoCT5uccjPH/VyTaBAkRPZPx9sUqFTy3q5VnnGwCPsoz7fnw54x79d/w==" + }, "react-dev-utils": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-9.0.1.tgz", diff --git a/diplomacy/web/package.json b/diplomacy/web/package.json index 5e1d505..aaf89c8 100644 --- a/diplomacy/web/package.json +++ b/diplomacy/web/package.json @@ -4,13 +4,14 @@ "homepage": ".", "private": true, "dependencies": { - "@githubprimer/octicons-react": "^8.5.0", + "@primer/octicons-react": "^9.1.1", "bootstrap": "^4.3.1", "fancybox": "^3.0.1", "jquery": "^3.4.1", "popper.js": "^1.15.0", "prop-types": "^15.7.2", "react": "^16.8.6", + "react-confirm-alert": "^2.4.1", "react-dom": "^16.8.6", "react-helmet": "^5.2.1", "react-inlinesvg": "^0.8.4", diff --git a/diplomacy/web/src/diplomacy/utils/utils.js b/diplomacy/web/src/diplomacy/utils/utils.js index f398cd2..8e54515 100644 --- a/diplomacy/web/src/diplomacy/utils/utils.js +++ b/diplomacy/web/src/diplomacy/utils/utils.js @@ -37,6 +37,10 @@ export const UTILS = { return id; }, + createGameID: function (username) { + return `${username}_${new Date().getTime().toString(10)}`; + }, + date: function () { const d = new Date(); return d.toLocaleString() + '.' + d.getMilliseconds(); diff --git a/diplomacy/web/src/gui/components/fancybox.jsx b/diplomacy/web/src/gui/components/fancyBox.js index 66a1efe..1e8c07c 100644 --- a/diplomacy/web/src/gui/components/fancybox.jsx +++ b/diplomacy/web/src/gui/components/fancyBox.js @@ -21,31 +21,19 @@ import {Button} from "./button"; const TIMES = '\u00D7'; export class FancyBox extends React.Component { - // open-tag (<FancyBox></FancyBox>) - // PROPERTIES - // title - // onClose render() { return ( - <div className={'fancy-wrapper'} onClick={this.props.onClose}> - <div className={'fancy-box container'} onClick={(event) => { - if (!event) - event = window.event; - if (event.hasOwnProperty('cancelBubble')) - event.cancelBubble = true; - if (event.stopPropagation) - event.stopPropagation(); - }}> - <div className={'row fancy-bar'}> - <div className={'col-11 align-self-center fancy-title'}>{this.props.title}</div> - <div className={'col-1 fancy-button'}> - <Button title={TIMES} color={'danger'} onClick={this.props.onClose}/> - </div> - </div> - <div className={'row'}> - <div className={'col fancy-content'}>{this.props.children}</div> + <div className="fancy-box"> + <div className="fancy-bar p-1 d-flex flex-row"> + <div + className="flex-grow-1 fancy-title d-flex flex-column justify-content-center pr-0 pr-sm-1">{this.props.title}</div> + <div className="fancy-button"> + <Button title={TIMES} color={'danger'} onClick={this.props.onClose}/> </div> </div> + <div> + <div className="col fancy-content">{this.props.children}</div> + </div> </div> ); } @@ -55,5 +43,5 @@ export class FancyBox extends React.Component { FancyBox.propTypes = { title: PropTypes.string.isRequired, onClose: PropTypes.func.isRequired, - children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]) + children: PropTypes.any.isRequired }; diff --git a/diplomacy/web/src/gui/components/help.jsx b/diplomacy/web/src/gui/components/help.jsx index 1ec1a54..f3f469f 100644 --- a/diplomacy/web/src/gui/components/help.jsx +++ b/diplomacy/web/src/gui/components/help.jsx @@ -1,13 +1,23 @@ import React from "react"; +import PropTypes from 'prop-types'; +import {FancyBox} from "./fancyBox"; -export function Help() { - return ( - <div> - <p>When building an order, press <strong>ESC</strong> to reset build.</p> - <p>Press letter associated to an order type to start building an order of this type. - <br/> Order type letter is indicated in order type name after order type radio button. - </p> - <p>In Phase History tab, use keyboard left and right arrows to navigate in past phases.</p> - </div> - ); +export class Help extends React.Component { + render() { + return ( + <FancyBox title={'Help'} onClose={this.props.onClose}> + <div> + <p>When building an order, press <strong>ESC</strong> to reset build.</p> + <p>Press letter associated to an order type to start building an order of this type. + <br/> Order type letter is indicated in order type name after order type radio button. + </p> + <p>In Phase History tab, use keyboard left and right arrows to navigate in past phases.</p> + </div> + </FancyBox> + ); + } } + +Help.propTypes = { + onClose: PropTypes.func.isRequired +}; diff --git a/diplomacy/web/src/gui/components/navigation.jsx b/diplomacy/web/src/gui/components/navigation.jsx index 5d961bc..051f8ea 100644 --- a/diplomacy/web/src/gui/components/navigation.jsx +++ b/diplomacy/web/src/gui/components/navigation.jsx @@ -1,5 +1,5 @@ import React from "react"; -import Octicon, {Person} from "@githubprimer/octicons-react"; +import Octicon, {Person} from "@primer/octicons-react"; import PropTypes from "prop-types"; export class Navigation extends React.Component { diff --git a/diplomacy/web/src/gui/forms/create_form.jsx b/diplomacy/web/src/gui/forms/create_form.jsx deleted file mode 100644 index 6753519..0000000 --- a/diplomacy/web/src/gui/forms/create_form.jsx +++ /dev/null @@ -1,95 +0,0 @@ -// ============================================================================== -// Copyright (C) 2019 - Philip Paquette, Steven Bocco -// -// This program is free software: you can redistribute it and/or modify it under -// the terms of the GNU Affero General Public License as published by the Free -// Software Foundation, either version 3 of the License, or (at your option) any -// later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -// details. -// -// You should have received a copy of the GNU Affero General Public License along -// with this program. If not, see <https://www.gnu.org/licenses/>. -// ============================================================================== -import React from 'react'; -import {Forms} from "../components/forms"; -import {STRINGS} from "../../diplomacy/utils/strings"; -import PropTypes from "prop-types"; - -export class CreateForm extends React.Component { - constructor(props) { - super(props); - this.state = this.initState(); - } - - initState() { - const state = { - game_id: '', - power_name: '', - n_controls: 7, - deadline: 300, - registration_password: '' - }; - for (let rule of STRINGS.PUBLIC_RULES) - state[`rule_${rule.toLowerCase()}`] = false; - return state; - } - - render() { - const onChange = Forms.createOnChangeCallback(this, this.props.onChange); - const onSubmit = Forms.createOnSubmitCallback(this, this.props.onSubmit); - return ( - <form> - {Forms.createRow( - Forms.createColLabel('game_id', 'Game ID (optional)'), - <input id={'game_id'} className={'form-control'} type={'text'} - value={Forms.getValue(this.state, 'game_id')} onChange={onChange}/> - )} - {Forms.createRow( - Forms.createColLabel('power_name', 'power:'), - <select id={'power_name'} className={'form-control custom-select'} - value={Forms.getValue(this.state, 'power_name')} onChange={onChange}> - {Forms.createSelectOptions(STRINGS.ALL_POWER_NAMES, true)} - </select> - )} - {Forms.createRow( - Forms.createColLabel('n_controls', 'number of required players:'), - <input id={'n_controls'} className={'form-control'} type={'number'} - value={Forms.getValue(this.state, 'n_controls')} onChange={onChange}/> - )} - {Forms.createRow( - Forms.createColLabel('deadline', 'deadline (in seconds)'), - <input id={'deadline'} className={'form-control'} type={'number'} - value={Forms.getValue(this.state, 'deadline')} - onChange={onChange}/> - )} - {Forms.createRow( - Forms.createColLabel('registration_password', 'registration password'), - <input id={'registration_password'} className={'form-control'} type={'password'} - value={Forms.getValue(this.state, 'registration_password')} onChange={onChange}/> - )} - <div><strong>RULES:</strong></div> - <div className={'mb-4'}> - {STRINGS.PUBLIC_RULES.map((rule, index) => ( - <div key={index} className={'form-check-inline'}> - {Forms.createCheckbox( - `rule_${rule.toLowerCase()}`, - rule, - Forms.getValue(this.state, `rule_${rule.toLowerCase()}`), - onChange)} - </div> - ))} - </div> - {Forms.createRow('', Forms.createSubmit('create a game', true, onSubmit))} - </form> - ); - } -} - -CreateForm.propTypes = { - onChange: PropTypes.func, - onSubmit: PropTypes.func -}; diff --git a/diplomacy/web/src/gui/forms/select_location_form.jsx b/diplomacy/web/src/gui/forms/select_location_form.jsx index ca7be09..1725c9b 100644 --- a/diplomacy/web/src/gui/forms/select_location_form.jsx +++ b/diplomacy/web/src/gui/forms/select_location_form.jsx @@ -17,20 +17,27 @@ import React from "react"; import PropTypes from "prop-types"; import {Button} from "../components/button"; +import {FancyBox} from "../components/fancyBox"; export class SelectLocationForm extends React.Component { render() { + const title = `Select location to continue building order: ${this.props.path.join(' ')}`; return ( - <div> - {this.props.locations.map((location, index) => ( - <Button key={index} title={location} large={true} onClick={() => this.props.onSelect(location)}/> - ))} - </div> + <FancyBox title={title} onClose={this.props.onClose}> + <div> + {this.props.locations.map((location, index) => ( + <Button key={index} title={location} large={true} + onClick={() => this.props.onSelect(location)}/> + ))} + </div> + </FancyBox> ); } } SelectLocationForm.propTypes = { locations: PropTypes.arrayOf(PropTypes.string).isRequired, - onSelect: PropTypes.func.isRequired // onSelect(location) + onSelect: PropTypes.func.isRequired, // onSelect(location) + onClose: PropTypes.func.isRequired, + path: PropTypes.array.isRequired }; diff --git a/diplomacy/web/src/gui/forms/select_via_form.jsx b/diplomacy/web/src/gui/forms/select_via_form.jsx index b779b8d..8b340af 100644 --- a/diplomacy/web/src/gui/forms/select_via_form.jsx +++ b/diplomacy/web/src/gui/forms/select_via_form.jsx @@ -17,19 +17,25 @@ import React from "react"; import PropTypes from "prop-types"; import {Button} from "../components/button"; +import {FancyBox} from "../components/fancyBox"; export class SelectViaForm extends React.Component { render() { return ( - <div> - <Button title={'regular move (M)'} large={true} onClick={() => this.props.onSelect('M')}/> - <Button title={'move via (V)'} large={true} onClick={() => this.props.onSelect('V')}/> - </div> + <FancyBox title={`Select move type for move order: ${this.props.path.join(' ')}`} + onClose={this.props.onClose}> + <div> + <Button title={'regular move (M)'} large={true} onClick={() => this.props.onSelect('M')}/> + <Button title={'move via (V)'} large={true} onClick={() => this.props.onSelect('V')}/> + </div> + </FancyBox> ); } } SelectViaForm.propTypes = { - onSelect: PropTypes.func.isRequired + path: PropTypes.array.isRequired, + onSelect: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired }; diff --git a/diplomacy/web/src/gui/pages/content_game.jsx b/diplomacy/web/src/gui/pages/content_game.jsx index f37bdca..457d901 100644 --- a/diplomacy/web/src/gui/pages/content_game.jsx +++ b/diplomacy/web/src/gui/pages/content_game.jsx @@ -33,7 +33,6 @@ import {STRINGS} from "../../diplomacy/utils/strings"; import {Diplog} from "../../diplomacy/utils/diplog"; import {Table} from "../components/table"; import {PowerView} from "../utils/power_view"; -import {FancyBox} from "../components/fancybox"; import {DipStorage} from "../utils/dipStorage"; import Helmet from 'react-helmet'; import {Navigation} from "../components/navigation"; @@ -116,14 +115,11 @@ export class ContentGame extends React.Component { orders: orders, // {power name => {loc => {local: bool, order: str}}} power: null, orderBuildingType: null, - orderBuildingPath: [], - fancy_title: null, - fancy_function: null, - on_fancy_close: null, + orderBuildingPath: [] }; // Bind some class methods to this instance. - this.closeFancyBox = this.closeFancyBox.bind(this); + this.clearOrderBuildingPath = this.clearOrderBuildingPath.bind(this); this.displayFirstPastPhase = this.displayFirstPastPhase.bind(this); this.displayLastPastPhase = this.displayLastPastPhase.bind(this); this.displayLocationOrders = this.displayLocationOrders.bind(this); @@ -202,11 +198,8 @@ export class ContentGame extends React.Component { return this.context; } - closeFancyBox() { + clearOrderBuildingPath() { this.setState({ - fancy_title: null, - fancy_function: null, - on_fancy_close: null, orderBuildingPath: [] }); } @@ -220,11 +213,6 @@ export class ContentGame extends React.Component { powerName, orderType, orderPath, location, this.onOrderBuilding, this.onOrderBuilt, this.getPage().error ); - this.setState({ - fancy_title: null, - fancy_function: null, - on_fancy_close: null - }); } setSelectedVia(moveType, powerName, orderPath, location) { @@ -234,33 +222,35 @@ export class ContentGame extends React.Component { powerName, moveType, orderPath, location, this.onOrderBuilding, this.onOrderBuilt, this.getPage().error ); - this.setState({ - fancy_title: null, - fancy_function: null, - on_fancy_close: null - }); } onSelectLocation(possibleLocations, powerName, orderType, orderPath) { - const title = `Select location to continue building order: ${orderPath.join(' ')} ... (press ESC or close button to cancel building)`; - const func = () => (<SelectLocationForm locations={possibleLocations} - onSelect={(location) => this.setSelectedLocation(location, powerName, orderType, orderPath)}/>); - this.setState({ - fancy_title: title, - fancy_function: func, - on_fancy_close: this.closeFancyBox - }); + this.getPage().dialog(onClose => ( + <SelectLocationForm path={orderPath} + locations={possibleLocations} + onSelect={(location) => { + this.setSelectedLocation(location, powerName, orderType, orderPath); + onClose(); + }} + onClose={() => { + this.clearOrderBuildingPath(); + onClose(); + }}/> + )); } onSelectVia(location, powerName, orderPath) { - const title = `Select move type for move order: ${orderPath.join(' ')}`; - const func = () => ( - <SelectViaForm onSelect={(moveType) => this.setSelectedVia(moveType, powerName, orderPath, location)}/>); - this.setState({ - fancy_title: title, - fancy_function: func, - on_fancy_close: this.closeFancyBox - }); + this.getPage().dialog(onClose => ( + <SelectViaForm path={orderPath} + onSelect={(moveType) => { + this.setSelectedVia(moveType, powerName, orderPath, location); + onClose(); + }} + onClose={() => { + this.clearOrderBuildingPath(); + onClose(); + }}/> + )); } // ] @@ -682,9 +672,6 @@ export class ContentGame extends React.Component { onOrderBuilt(powerName, orderString) { const state = Object.assign({}, this.state); state.orderBuildingPath = []; - state.fancy_title = null; - state.fancy_function = null; - state.on_fancy_close = null; if (!orderString) { Diplog.warn('No order built.'); this.setState(state); @@ -712,9 +699,6 @@ export class ContentGame extends React.Component { this.setState({ orderBuildingType: form.order_type, orderBuildingPath: [], - fancy_title: null, - fancy_function: null, - on_fancy_close: null }); } @@ -1162,7 +1146,7 @@ export class ContentGame extends React.Component { const engine = this.props.data; const title = ContentGame.gameTitle(engine); const navigation = [ - ['Help', () => page.loadFancyBox('Help', () => <Help/>)], + ['Help', () => page.dialog(onClose => <Help onClose={onClose}/>)], ['Load a game from disk', page.loadGameFromDisk], ['Save game to disk', () => saveGameToDisk(engine, page.error)], [`${UTILS.html.UNICODE_SMALL_LEFT_ARROW} Games`, () => page.loadGames()], @@ -1292,10 +1276,6 @@ export class ContentGame extends React.Component { currentTabOrderCreation )) || ''} </Tabs> - {this.state.fancy_title && ( - <FancyBox title={this.state.fancy_title} onClose={this.state.on_fancy_close}> - {this.state.fancy_function()} - </FancyBox>)} </main> ); } diff --git a/diplomacy/web/src/gui/pages/content_games.jsx b/diplomacy/web/src/gui/pages/content_games.jsx index 31bd1af..5250f03 100644 --- a/diplomacy/web/src/gui/pages/content_games.jsx +++ b/diplomacy/web/src/gui/pages/content_games.jsx @@ -18,15 +18,15 @@ import React from "react"; import {Tabs} from "../components/tabs"; import {Table} from "../components/table"; import {FindForm} from "../forms/find_form"; -import {CreateForm} from "../forms/create_form"; import {InlineGameView} from "../utils/inline_game_view"; -import {STRINGS} from "../../diplomacy/utils/strings"; import {Helmet} from "react-helmet"; import {Navigation} from "../components/navigation"; import {PageContext} from "../components/page_context"; import {ContentGame} from "./content_game"; import PropTypes from 'prop-types'; import {Tab} from "../components/tab"; +import {GameCreationWizard} from "../wizards/gameCreation/gameCreationWizard"; +import {Diplog} from "../../diplomacy/utils/diplog"; const TABLE_LOCAL_GAMES = { game_id: ['Game ID', 0], @@ -71,23 +71,6 @@ export class ContentGames extends React.Component { } onCreate(form) { - for (let key of Object.keys(form)) { - if (form[key] === '') - form[key] = null; - } - if (form.n_controls !== null) - form.n_controls = parseInt(form.n_controls, 10); - if (form.deadline !== null) - form.deadline = parseInt(form.deadline, 10); - form.rules = ['POWER_CHOICE']; - for (let rule of STRINGS.PUBLIC_RULES) { - const rule_id = `rule_${rule.toLowerCase()}`; - if (form.hasOwnProperty(rule_id)) { - if (form[rule_id]) - form.rules.push(rule); - delete form[rule_id]; - } - } let networkGame = null; this.getPage().channel.createGame(form) .then((game) => { @@ -116,6 +99,28 @@ export class ContentGames extends React.Component { return new InlineGameView(this.getPage(), gameData); } + gameCreationButton() { + return ( + <button type="button" + className="btn btn-danger btn-sm mx-0 mx-sm-4" + onClick={() => this.getPage().dialog(onClose => ( + <GameCreationWizard availableMaps={this.getPage().availableMaps} + onCancel={onClose} + username={this.getPage().channel.username} + onSubmit={(form) => { + onClose(); + Diplog.info(`Creating game:`); + for (let entry of Object.entries(form)) { + Diplog.info(`${entry[0]}: ${entry[1] ? entry[1].toString() : entry[1]}`); + } + this.onCreate(form); + }}/> + ))}> + <strong>create a game</strong> + </button> + ); + } + render() { const title = 'Games'; const page = this.getPage(); @@ -133,14 +138,10 @@ export class ContentGames extends React.Component { <Helmet> <title>{title} | Diplomacy</title> </Helmet> - <Navigation title={title} username={page.channel.username} navigation={navigation}/> - <Tabs menu={['create', 'find', 'my-games']} titles={['Create', 'Find', 'My Games']} + <Navigation title={title} afterTitle={this.gameCreationButton()} + username={page.channel.username} navigation={navigation}/> + <Tabs menu={['find', 'my-games']} titles={['Find', 'My Games']} onChange={this.changeTab} active={tab}> - {tab === 'create' ? ( - <Tab id="tab-games-create" display={true}> - <CreateForm onSubmit={this.onCreate}/> - </Tab> - ) : ''} {tab === 'find' ? ( <Tab id="tab-games-find" display={true}> <FindForm onSubmit={this.onFind}/> diff --git a/diplomacy/web/src/gui/pages/page.jsx b/diplomacy/web/src/gui/pages/page.jsx index cd36f6c..a9ff9ac 100644 --- a/diplomacy/web/src/gui/pages/page.jsx +++ b/diplomacy/web/src/gui/pages/page.jsx @@ -20,12 +20,13 @@ import React from "react"; import {ContentConnection} from "./content_connection"; import {UTILS} from "../../diplomacy/utils/utils"; import {Diplog} from "../../diplomacy/utils/diplog"; -import {FancyBox} from "../components/fancybox"; import {DipStorage} from "../utils/dipStorage"; import {PageContext} from "../components/page_context"; import {ContentGames} from "./content_games"; import {loadGameFromDisk} from "../utils/load_game_from_disk"; import {ContentGame} from "./content_game"; +import {confirmAlert} from 'react-confirm-alert'; +import 'react-confirm-alert/src/react-confirm-alert.css'; export class Page extends React.Component { @@ -35,9 +36,6 @@ export class Page extends React.Component { this.channel = null; this.availableMaps = null; this.state = { - // fancybox, - fancyTitle: null, - onFancyBox: null, // Page messages error: null, info: null, @@ -54,7 +52,6 @@ export class Page extends React.Component { this.success = this.success.bind(this); this.logout = this.logout.bind(this); this.loadGameFromDisk = this.loadGameFromDisk.bind(this); - this.unloadFancyBox = this.unloadFancyBox.bind(this); this._post_remove = this._post_remove.bind(this); this._add_to_my_games = this._add_to_my_games.bind(this); this._remove_from_my_games = this._remove_from_my_games.bind(this); @@ -80,14 +77,22 @@ export class Page extends React.Component { this.__disconnect(error); } - //// Methods to load a global fancybox. + /** + * @callback OnClose + */ - loadFancyBox(title, callback) { - this.setState({fancyTitle: title, onFancyBox: callback}); - } + /** + * @callback DialogBuilder + * @param {OnClose} onClose + */ - unloadFancyBox() { - this.setState({fancyTitle: null, onFancyBox: null}); + /** + * open a dialog box + * @param {DialogBuilder} builder - a callback to generate dialog GUI. Will be executed with a `onClose` callback + * parameter to call when dialog must be closed: `builder(onClose)`. + */ + dialog(builder) { + confirmAlert({customUI: ({onClose}) => builder(onClose)}); } //// Methods to load a page. @@ -275,7 +280,6 @@ export class Page extends React.Component { } } - disconnectGame(gameID) { const game = this.getGame(gameID); if (game) { @@ -363,11 +367,6 @@ export class Page extends React.Component { </div> </div> {this.state.body || Page.defaultPage()} - {this.state.onFancyBox && ( - <FancyBox title={this.state.fancyTitle} onClose={this.unloadFancyBox}> - {this.state.onFancyBox()} - </FancyBox> - )} </div> </PageContext.Provider> ); diff --git a/diplomacy/web/src/gui/wizards/gameCreation/gameCreationWizard.js b/diplomacy/web/src/gui/wizards/gameCreation/gameCreationWizard.js new file mode 100644 index 0000000..daaa461 --- /dev/null +++ b/diplomacy/web/src/gui/wizards/gameCreation/gameCreationWizard.js @@ -0,0 +1,109 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {Panels} from "./panelList"; +import {PanelChooseMap} from "./panelChooseMap"; +import {PanelChoosePlayers} from "./panelChoosePlayers"; +import {PanelChoosePower} from "./panelChoosePower"; +import {PanelChooseSettings} from "./panelChooseSettings"; +import {Maps} from "./mapList"; +import {UTILS} from "../../../diplomacy/utils/utils"; + +export class GameCreationWizard extends React.Component { + constructor(props) { + super(props); + this.state = { + panel: Panels.CHOOSE_MAP, + game_id: UTILS.createGameID(this.props.username), + power_name: null, + n_controls: -1, + deadline: 0, + registration_password: '', + + map: Maps[0], + no_press: false + }; + this.backward = this.backward.bind(this); + this.forward = this.forward.bind(this); + this.updateParams = this.updateParams.bind(this); + } + + updateParams(params) { + this.setState(params); + } + + goToPanel(panelID) { + if (panelID < Panels.CHOOSE_MAP) + this.props.onCancel(); + else if (panelID > Panels.CHOOSE_SETTINGS) { + const rules = ['POWER_CHOICE']; + if (this.state.no_press) + rules.push('NO_PRESS'); + if (!this.state.deadline) { + rules.push('NO_DEADLINE'); + rules.push('REAL_TIME'); + } + this.props.onSubmit({ + game_id: this.state.game_id, + map_name: this.state.map.name, + power_name: this.state.power_name, + n_controls: this.state.n_controls, + deadline: this.state.deadline, + registration_password: this.state.registration_password || null, + rules: rules + }); + } else + this.setState({panel: panelID, registration_password: ''}); + } + + backward(step) { + this.goToPanel(this.state.panel - (step ? step : 1)); + } + + forward(step) { + this.goToPanel(this.state.panel + (step ? step : 1)); + } + + renderPanel() { + switch (this.state.panel) { + case Panels.CHOOSE_MAP: + return <PanelChooseMap forward={this.forward} + params={this.state} + onUpdateParams={this.updateParams} + cancel={this.props.onCancel}/>; + case Panels.CHOOSE_PLAYERS: + return <PanelChoosePlayers backward={this.backward} + forward={this.forward} + onUpdateParams={this.updateParams} + nbPowers={this.props.availableMaps[this.state.map.name].powers.length} + cancel={this.props.onCancel}/>; + case Panels.CHOOSE_POWER: + return <PanelChoosePower backward={this.backward} + forward={this.forward} + onUpdateParams={this.updateParams} + powers={this.props.availableMaps[this.state.map.name].powers} + cancel={this.props.onCancel}/>; + case Panels.CHOOSE_SETTINGS: + return <PanelChooseSettings backward={this.backward} + forward={this.forward} + onUpdateParams={this.updateParams} + username={this.props.username} + params={this.state} + cancel={this.props.onCancel}/>; + default: + return ''; + } + } + + render() { + return ( + <div className="game-creation-wizard">{this.renderPanel()}</div> + ); + } +} + +GameCreationWizard.propTypes = { + onCancel: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, + availableMaps: PropTypes.object.isRequired, + username: PropTypes.string.isRequired +}; diff --git a/diplomacy/web/src/gui/wizards/gameCreation/mapList.js b/diplomacy/web/src/gui/wizards/gameCreation/mapList.js new file mode 100644 index 0000000..5d2c00a --- /dev/null +++ b/diplomacy/web/src/gui/wizards/gameCreation/mapList.js @@ -0,0 +1,41 @@ +class VariantInfo { + constructor(variantName, variantTitle) { + this.name = variantName; + this.title = variantTitle; + this.map = null; + } + + svgName() { + return this.map.name; + } +} + +class MapInfo { + constructor(mapName, mapTitle, variants) { + this.name = mapName; + this.title = mapTitle; + this.variants = null; + if (variants) { + this.variants = []; + for (let variant of variants) { + variant.map = this; + this.variants.push(variant); + } + } + } + + svgName() { + return this.name; + } +} + +export const Maps = [ + new MapInfo('standard', 'Standard', [ + new VariantInfo('standard', 'Default'), + new VariantInfo('standard_age_of_empires', 'Age of empires'), + new VariantInfo('standard_age_of_empires_2', 'Age of empires II'), + new VariantInfo('standard_fleet_rome', 'Fleet at Rome'), + new VariantInfo('standard_france_austria', 'France VS Austria'), + new VariantInfo('standard_germany_italy', 'Germany VS Italy') + ]), +]; diff --git a/diplomacy/web/src/gui/wizards/gameCreation/panelChooseMap.js b/diplomacy/web/src/gui/wizards/gameCreation/panelChooseMap.js new file mode 100644 index 0000000..5c40f1c --- /dev/null +++ b/diplomacy/web/src/gui/wizards/gameCreation/panelChooseMap.js @@ -0,0 +1,94 @@ +import React from "react"; +import {Maps} from "./mapList"; +import {FancyBox} from "../../components/fancyBox"; +import PropTypes from "prop-types"; + +export class PanelChooseMap extends React.Component { + render() { + const mapImg = require(`../../../maps/svg/${this.props.params.map.svgName()}.svg`); + const mapEntries = []; + let count = 0; + for (let mapInfo of Maps) { + ++count; + if (!mapInfo.variants) { + mapEntries.push( + <div key={count} className="mb-1"> + <button type="button" + className="btn btn-secondary btn-sm btn-block" + onMouseOver={() => this.props.onUpdateParams({map: mapInfo})} + onClick={() => this.props.forward()}> + {mapInfo.title} + </button> + </div> + ); + } else { + const dropDownID = `collapse-${count}-${mapInfo.name}`; + const variants = mapInfo.variants.slice(); + const defaultVariant = variants[0]; + mapEntries.push( + <div key={count}> + <div className="mb-1 d-flex flex-row justify-content-center"> + <button type="button" + className="btn btn-secondary btn-sm flex-grow-1 mr-1" + onMouseOver={() => this.props.onUpdateParams({map: defaultVariant})} + onClick={() => this.props.forward()}> + {mapInfo.title} ({defaultVariant.title}) + </button> + <button type="button" + className="btn btn-outline-secondary btn-sm collapsed" + data-toggle="collapse" + data-target={`#${dropDownID}`} + aria-expanded={false} + aria-controls={dropDownID}> + <span className="unroll"><strong>+</strong></span> + <span className="roll"><strong>-</strong></span> + </button> + </div> + <div className="collapse" id={dropDownID}> + <div> + {(() => { + const views = []; + for (let i = 1; i < variants.length; ++i) { + const variantInfo = variants[i]; + views.push( + <div key={variantInfo.name} className="mb-1"> + <button type="button" + className="btn btn-outline-secondary btn-sm btn-block" + onMouseOver={() => this.props.onUpdateParams({map: variantInfo})} + onClick={() => this.props.forward()}> + {variantInfo.title} + </button> + </div> + ); + } + return views; + })()} + </div> + </div> + </div> + ); + } + } + return ( + <FancyBox title={'Choose a map'} onClose={this.props.cancel}> + <div className="row panel-choose-map"> + <div className="col-md"> + <div className="map-list p-1 ml-0 ml-sm-1"> + {mapEntries} + </div> + </div> + <div className="col-md"> + <img className="img-fluid" src={mapImg} alt={this.props.params.map.title}/> + </div> + </div> + </FancyBox> + ); + } +} + +PanelChooseMap.propTypes = { + forward: PropTypes.func.isRequired, + cancel: PropTypes.func.isRequired, + params: PropTypes.object.isRequired, + onUpdateParams: PropTypes.func.isRequired +}; diff --git a/diplomacy/web/src/gui/wizards/gameCreation/panelChoosePlayers.js b/diplomacy/web/src/gui/wizards/gameCreation/panelChoosePlayers.js new file mode 100644 index 0000000..84a47a0 --- /dev/null +++ b/diplomacy/web/src/gui/wizards/gameCreation/panelChoosePlayers.js @@ -0,0 +1,61 @@ +import React from "react"; +import {FancyBox} from "../../components/fancyBox"; +import PropTypes from "prop-types"; +import Octicon, {ArrowLeft} from "@primer/octicons-react"; + +export class PanelChoosePlayers extends React.Component { + render() { + return ( + <FancyBox title={'Number of human players'} onClose={this.props.cancel}> + <div className="row"> + <div className="col-sm"> + <button type="button" className="btn btn-secondary btn-sm btn-block inline" onClick={() => { + this.props.onUpdateParams({n_controls: 0}); + this.props.forward(2); + }}>None - just bots + </button> + </div> + <div className="col-sm"> + <button type="button" className="btn btn-secondary btn-sm btn-block inline" onClick={() => { + this.props.onUpdateParams({n_controls: this.props.nbPowers}); + this.props.forward(); + }}>All humans - no bots + </button> + </div> + </div> + <div className="d-flex flex-row justify-content-center my-2"> + {(() => { + const choice = []; + for (let i = 0; i < this.props.nbPowers; ++i) { + choice.push( + <button key={i} type="button" + className={`btn btn-secondary btn-sm flex-grow-1 ${i === 0 ? '' : 'ml-sm-1'}`} + onClick={() => { + this.props.onUpdateParams({n_controls: i + 1}); + this.props.forward(); + }}> + {i + 1} + </button> + ); + } + return choice; + })()} + </div> + <div> + <button type="button" className="btn btn-secondary btn-sm px-3" + onClick={() => this.props.backward()}> + <Octicon icon={ArrowLeft}/> + </button> + </div> + </FancyBox> + ); + } +} + +PanelChoosePlayers.propTypes = { + backward: PropTypes.func.isRequired, + forward: PropTypes.func.isRequired, + cancel: PropTypes.func.isRequired, + onUpdateParams: PropTypes.func.isRequired, + nbPowers: PropTypes.number.isRequired +}; diff --git a/diplomacy/web/src/gui/wizards/gameCreation/panelChoosePower.js b/diplomacy/web/src/gui/wizards/gameCreation/panelChoosePower.js new file mode 100644 index 0000000..dc400bd --- /dev/null +++ b/diplomacy/web/src/gui/wizards/gameCreation/panelChoosePower.js @@ -0,0 +1,62 @@ +import React from "react"; +import {FancyBox} from "../../components/fancyBox"; +import PropTypes from "prop-types"; +import Octicon, {ArrowLeft} from "@primer/octicons-react"; + +export class PanelChoosePower extends React.Component { + render() { + return ( + <FancyBox title={'Choose your power'} onClose={this.props.cancel}> + <div className="row"> + <div className="col-sm"> + <button type="button" className="btn btn-secondary btn-sm btn-block inline" onClick={() => { + this.props.onUpdateParams({power_name: null}); + this.props.forward(); + }}>I just want to observe + </button> + </div> + <div className="col-sm"> + <button type="button" className="btn btn-secondary btn-sm btn-block inline" onClick={() => { + const powerName = this.props.powers[Math.floor(Math.random() * this.props.powers.length)]; + this.props.onUpdateParams({power_name: powerName}); + this.props.forward(); + }}>Choose randomly for me + </button> + </div> + </div> + <div className="d-flex flex-row justify-content-center my-2"> + {(() => { + const choice = []; + for (let i = 0; i < this.props.powers.length; ++i) { + choice.push( + <button key={i} type="button" + className={`btn btn-secondary btn-sm flex-grow-1 ${i === 0 ? '' : 'ml-sm-1'}`} + onClick={() => { + this.props.onUpdateParams({power_name: this.props.powers[i]}); + this.props.forward(); + }}> + {this.props.powers[i]} + </button> + ); + } + return choice; + })()} + </div> + <div> + <button type="button" className="btn btn-secondary btn-sm px-3" + onClick={() => this.props.backward()}> + <Octicon icon={ArrowLeft}/> + </button> + </div> + </FancyBox> + ); + } +} + +PanelChoosePower.propTypes = { + backward: PropTypes.func.isRequired, + forward: PropTypes.func.isRequired, + cancel: PropTypes.func.isRequired, + onUpdateParams: PropTypes.func.isRequired, + powers: PropTypes.arrayOf(PropTypes.string).isRequired +}; diff --git a/diplomacy/web/src/gui/wizards/gameCreation/panelChooseSettings.js b/diplomacy/web/src/gui/wizards/gameCreation/panelChooseSettings.js new file mode 100644 index 0000000..e509158 --- /dev/null +++ b/diplomacy/web/src/gui/wizards/gameCreation/panelChooseSettings.js @@ -0,0 +1,113 @@ +import React from "react"; +import {FancyBox} from "../../components/fancyBox"; +import PropTypes from "prop-types"; +import {UTILS} from "../../../diplomacy/utils/utils"; +import Octicon, {ArrowLeft} from "@primer/octicons-react"; + +const DEADLINES = [ + [0, '(no deadline)'], + [60, '1 min'], + [60 * 5, '5 min'], + [60 * 30, '30 min'], + [60 * 60 * 2, '2 hrs'], + [60 * 60 * 24, '24 hrs'], +]; + +export class PanelChooseSettings extends React.Component { + constructor(props) { + super(props); + this.onCheckNoPress = this.onCheckNoPress.bind(this); + this.onSelectDeadline = this.onSelectDeadline.bind(this); + this.onSetRegistrationPassword = this.onSetRegistrationPassword.bind(this); + this.onSetGameID = this.onSetGameID.bind(this); + } + + onCheckNoPress(event) { + this.props.onUpdateParams({no_press: event.target.checked}); + } + + onSelectDeadline(event) { + this.props.onUpdateParams({deadline: parseInt(event.target.value)}); + } + + onSetRegistrationPassword(event) { + this.props.onUpdateParams({registration_password: event.target.value}); + } + + onSetGameID(event) { + let gameID = event.target.value; + if (!gameID) + gameID = UTILS.createGameID(this.props.username); + this.props.onUpdateParams({game_id: gameID}); + } + + render() { + return ( + <FancyBox title={'Other settings'} onClose={this.props.cancel}> + <div> + <form> + <div className="form-group row align-items-center mb-2"> + <label className="col-md col-form-label" htmlFor="deadline">Deadline</label> + <div className="col-md"> + <select id="deadline" className="custom-select custom-select-sm" + value={this.props.params.deadline} + onChange={this.onSelectDeadline}> + {DEADLINES.map((deadline, index) => ( + <option key={index} value={deadline[0]}>{deadline[1]}</option> + ))} + </select> + </div> + </div> + <div className="form-group row mb-2"> + <label className="col-md col-form-label" htmlFor="registration-password">Login + password</label> + <div className="col-md"> + <input type="password" className="form-control form-control-sm" + id="registration-password" + value={this.props.params.registration_password} + onChange={this.onSetRegistrationPassword} placeholder="(no password)"/> + </div> + </div> + <div className="form-group row mb-2"> + <label className="col-md col-form-label" htmlFor="game-id">Game ID</label> + <div className="col-md"> + <input type="text" className="form-control form-control-sm" + id="game-id" + value={this.props.params.game_id} + onChange={this.onSetGameID}/> + </div> + </div> + <div className="custom-control custom-checkbox mb-5"> + <input type="checkbox" className="custom-control-input" id="no-press" + checked={this.props.params.no_press} onChange={this.onCheckNoPress}/> + <label className="custom-control-label" htmlFor="no-press">No messages allowed</label> + </div> + </form> + </div> + <div className="row"> + <div className="col-sm"> + <button type="button" className="btn btn-secondary btn-sm btn-block" + onClick={() => this.props.backward()}> + <Octicon icon={ArrowLeft}/> + </button> + </div> + <div className="col-sm"> + <button type="button" className="btn btn-success btn-sm btn-block inline" + onClick={() => this.props.forward()}> + <strong>create the game</strong> + </button> + </div> + </div> + </FancyBox> + ); + } +} + +PanelChooseSettings.propTypes = { + backward: PropTypes.func.isRequired, + forward: PropTypes.func.isRequired, + cancel: PropTypes.func.isRequired, + params: PropTypes.object.isRequired, + onUpdateParams: PropTypes.func.isRequired, + username: PropTypes.string.isRequired +}; diff --git a/diplomacy/web/src/gui/wizards/gameCreation/panelList.js b/diplomacy/web/src/gui/wizards/gameCreation/panelList.js new file mode 100644 index 0000000..0b6100c --- /dev/null +++ b/diplomacy/web/src/gui/wizards/gameCreation/panelList.js @@ -0,0 +1,6 @@ +export const Panels = { + CHOOSE_MAP: 0, + CHOOSE_PLAYERS: 1, + CHOOSE_POWER: 2, + CHOOSE_SETTINGS: 3 +}; diff --git a/diplomacy/web/src/index.css b/diplomacy/web/src/index.css index f270135..8b14a97 100644 --- a/diplomacy/web/src/index.css +++ b/diplomacy/web/src/index.css @@ -261,12 +261,9 @@ span.power-name { .fancy-box .fancy-bar { background-color: rgb(240, 240, 240); border-bottom: 1px solid silver; - padding-top: 10px; - padding-bottom: 10px; } .fancy-box .fancy-button { - text-align: right; } .fancy-box .fancy-content { @@ -419,6 +416,37 @@ span.power-name { background-color: rgb(230, 230, 230); } +button .unroll { + display: none; +} + +button .roll { + display: inline; +} + +button.collapsed .unroll { + display: inline; +} + +button.collapsed .roll { + display: none; +} + +.panel-choose-map .map-list { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + overflow: auto; + border: 1px solid gray; +} + +.inline { + white-space: nowrap; +} + + /** Page login. **/ /** Page games. **/ /** Page game. **/ diff --git a/diplomacy/web/src/maps b/diplomacy/web/src/maps new file mode 120000 index 0000000..0aaf789 --- /dev/null +++ b/diplomacy/web/src/maps @@ -0,0 +1 @@ +../../maps/
\ No newline at end of file |