From 6187faf20384b0c5a4966343b2d4ca47f8b11e45 Mon Sep 17 00:00:00 2001 From: Philip Paquette Date: Wed, 26 Sep 2018 07:48:55 -0400 Subject: Release v1.0.0 - Diplomacy Game Engine - AGPL v3+ License --- diplomacy/web/src/gui/core/content.jsx | 51 + diplomacy/web/src/gui/core/fancybox.jsx | 59 + diplomacy/web/src/gui/core/forms.jsx | 116 ++ diplomacy/web/src/gui/core/layouts.jsx | 55 + diplomacy/web/src/gui/core/page.jsx | 434 +++++++ diplomacy/web/src/gui/core/table.jsx | 112 ++ diplomacy/web/src/gui/core/tabs.jsx | 96 ++ diplomacy/web/src/gui/core/widgets.jsx | 102 ++ .../gui/diplomacy/contents/content_connection.jsx | 91 ++ .../src/gui/diplomacy/contents/content_game.jsx | 1235 ++++++++++++++++++++ .../src/gui/diplomacy/contents/content_games.jsx | 140 +++ .../src/gui/diplomacy/forms/connection_form.jsx | 123 ++ .../web/src/gui/diplomacy/forms/create_form.jsx | 95 ++ .../web/src/gui/diplomacy/forms/find_form.jsx | 70 ++ .../web/src/gui/diplomacy/forms/join_form.jsx | 77 ++ .../web/src/gui/diplomacy/forms/message_form.jsx | 53 + .../src/gui/diplomacy/forms/power_actions_form.jsx | 120 ++ .../gui/diplomacy/forms/select_location_form.jsx | 36 + .../src/gui/diplomacy/forms/select_via_form.jsx | 35 + .../web/src/gui/diplomacy/map/dom_order_builder.js | 278 +++++ .../web/src/gui/diplomacy/map/dom_past_map.js | 112 ++ diplomacy/web/src/gui/diplomacy/map/map.jsx | 94 ++ diplomacy/web/src/gui/diplomacy/map/renderer.js | 615 ++++++++++ .../web/src/gui/diplomacy/utils/dipStorage.jsx | 140 +++ .../src/gui/diplomacy/utils/inline_game_view.jsx | 129 ++ diplomacy/web/src/gui/diplomacy/utils/map_data.js | 98 ++ diplomacy/web/src/gui/diplomacy/utils/order.js | 24 + .../web/src/gui/diplomacy/utils/order_building.js | 211 ++++ .../web/src/gui/diplomacy/utils/power_view.jsx | 59 + diplomacy/web/src/gui/diplomacy/utils/province.js | 117 ++ .../web/src/gui/diplomacy/widgets/message_view.jsx | 57 + .../web/src/gui/diplomacy/widgets/power_order.jsx | 79 ++ 32 files changed, 5113 insertions(+) create mode 100644 diplomacy/web/src/gui/core/content.jsx create mode 100644 diplomacy/web/src/gui/core/fancybox.jsx create mode 100644 diplomacy/web/src/gui/core/forms.jsx create mode 100644 diplomacy/web/src/gui/core/layouts.jsx create mode 100644 diplomacy/web/src/gui/core/page.jsx create mode 100644 diplomacy/web/src/gui/core/table.jsx create mode 100644 diplomacy/web/src/gui/core/tabs.jsx create mode 100644 diplomacy/web/src/gui/core/widgets.jsx create mode 100644 diplomacy/web/src/gui/diplomacy/contents/content_connection.jsx create mode 100644 diplomacy/web/src/gui/diplomacy/contents/content_game.jsx create mode 100644 diplomacy/web/src/gui/diplomacy/contents/content_games.jsx create mode 100644 diplomacy/web/src/gui/diplomacy/forms/connection_form.jsx create mode 100644 diplomacy/web/src/gui/diplomacy/forms/create_form.jsx create mode 100644 diplomacy/web/src/gui/diplomacy/forms/find_form.jsx create mode 100644 diplomacy/web/src/gui/diplomacy/forms/join_form.jsx create mode 100644 diplomacy/web/src/gui/diplomacy/forms/message_form.jsx create mode 100644 diplomacy/web/src/gui/diplomacy/forms/power_actions_form.jsx create mode 100644 diplomacy/web/src/gui/diplomacy/forms/select_location_form.jsx create mode 100644 diplomacy/web/src/gui/diplomacy/forms/select_via_form.jsx create mode 100644 diplomacy/web/src/gui/diplomacy/map/dom_order_builder.js create mode 100644 diplomacy/web/src/gui/diplomacy/map/dom_past_map.js create mode 100644 diplomacy/web/src/gui/diplomacy/map/map.jsx create mode 100644 diplomacy/web/src/gui/diplomacy/map/renderer.js create mode 100644 diplomacy/web/src/gui/diplomacy/utils/dipStorage.jsx create mode 100644 diplomacy/web/src/gui/diplomacy/utils/inline_game_view.jsx create mode 100644 diplomacy/web/src/gui/diplomacy/utils/map_data.js create mode 100644 diplomacy/web/src/gui/diplomacy/utils/order.js create mode 100644 diplomacy/web/src/gui/diplomacy/utils/order_building.js create mode 100644 diplomacy/web/src/gui/diplomacy/utils/power_view.jsx create mode 100644 diplomacy/web/src/gui/diplomacy/utils/province.js create mode 100644 diplomacy/web/src/gui/diplomacy/widgets/message_view.jsx create mode 100644 diplomacy/web/src/gui/diplomacy/widgets/power_order.jsx (limited to 'diplomacy/web/src/gui') diff --git a/diplomacy/web/src/gui/core/content.jsx b/diplomacy/web/src/gui/core/content.jsx new file mode 100644 index 0000000..416ba9e --- /dev/null +++ b/diplomacy/web/src/gui/core/content.jsx @@ -0,0 +1,51 @@ +// ============================================================================== +// 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 . +// ============================================================================== +import React from 'react'; +import PropTypes from 'prop-types'; + +export class Content extends React.Component { + // PROPERTIES: + // page: pointer to parent Page object + // data: data for current content + + // Each derived class must implement this static method. + static builder(page, data) { + return { + // page title (string) + title: `${data ? 'with data' : 'without data'}`, + // page navigation links: array of couples + // (navigation title, navigation callback ( onClick=() => callback() )) + navigation: [], + // page content: React component (e.g. , or
...
, etc). + component: null + }; + } + + getPage() { + return this.props.page; + } + + componentDidMount() { + window.scrollTo(0, 0); + } +} + + +Content.propTypes = { + page: PropTypes.object.isRequired, + data: PropTypes.object +}; diff --git a/diplomacy/web/src/gui/core/fancybox.jsx b/diplomacy/web/src/gui/core/fancybox.jsx new file mode 100644 index 0000000..4d1013d --- /dev/null +++ b/diplomacy/web/src/gui/core/fancybox.jsx @@ -0,0 +1,59 @@ +// ============================================================================== +// 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 . +// ============================================================================== +import React from 'react'; +import {Button} from "./widgets"; +import PropTypes from 'prop-types'; + +const TIMES = '\u00D7'; + +export class FancyBox extends React.Component { + // open-tag () + // PROPERTIES + // title + // onClose + render() { + return ( +
+
{ + if (!event) + event = window.event; + if (event.hasOwnProperty('cancelBubble')) + event.cancelBubble = true; + if (event.stopPropagation) + event.stopPropagation(); + }}> +
+
{this.props.title}
+
+
+
+
+
{this.props.children}
+
+
+
+ ); + } +} + + +FancyBox.propTypes = { + title: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired, + children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]) +}; diff --git a/diplomacy/web/src/gui/core/forms.jsx b/diplomacy/web/src/gui/core/forms.jsx new file mode 100644 index 0000000..76d188c --- /dev/null +++ b/diplomacy/web/src/gui/core/forms.jsx @@ -0,0 +1,116 @@ +// ============================================================================== +// 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 . +// ============================================================================== +import React from "react"; +import {Button} from "./widgets"; +import {UTILS} from "../../diplomacy/utils/utils"; + +export class Forms { + static createOnChangeCallback(component, callback) { + return (event) => { + const value = UTILS.html.isCheckBox(event.target) ? event.target.checked : event.target.value; + const fieldName = UTILS.html.isRadioButton(event.target) ? event.target.name : event.target.id; + const update = {[fieldName]: value}; + const state = Object.assign({}, component.state, update); + if (callback) + callback(state); + component.setState(state); + }; + } + + static createOnSubmitCallback(component, callback, resetState) { + return (event) => { + if (callback) + callback(Object.assign({}, component.state)); + if (resetState) + component.setState(resetState); + event.preventDefault(); + }; + } + + static createOnResetCallback(component, onChangeCallback, resetState) { + return (event) => { + if (onChangeCallback) + onChangeCallback(resetState); + component.setState(resetState); + if (event && event.preventDefault) + event.preventDefault(); + }; + } + + static getValue(fieldValues, fieldName, defaultValue) { + return fieldValues.hasOwnProperty(fieldName) ? fieldValues[fieldName] : defaultValue; + } + + static createReset(title, large, onReset) { + return +
+ {content.navigation.map((nav, index) => { + const navTitle = nav[0]; + const navAction = nav[1]; + return {navTitle}; + })} +
+ + )} + + + )) || ( +
{title}
+ )} + {content.component} + {this.state.onFancyBox && ( + + {this.state.onFancyBox()} + + )} + + ); + } +} diff --git a/diplomacy/web/src/gui/core/table.jsx b/diplomacy/web/src/gui/core/table.jsx new file mode 100644 index 0000000..cb729e7 --- /dev/null +++ b/diplomacy/web/src/gui/core/table.jsx @@ -0,0 +1,112 @@ +// ============================================================================== +// 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 . +// ============================================================================== +//// Tables. + +import React from "react"; +import PropTypes from 'prop-types'; + +class DefaultWrapper { + constructor(data) { + this.data = data; + this.get = this.get.bind(this); + } + + get(fieldName) { + return this.data[fieldName]; + } +} + +function defaultWrapper(data) { + return new DefaultWrapper(data); +} + +export class Table extends React.Component { + // className + // caption + // columns : {name: [title, order]} + // data: [objects with expected column names] + // wrapper: (optional) function to use to wrap one data entry into an object before accessing fields. + // Must return an instance with a method get(name). + // If provided: wrapper(data_entry).get(field_name) + // else: data_entry[field_name] + + constructor(props) { + super(props); + if (!this.props.wrapper) + this.props.wrapper = defaultWrapper; + } + + static getHeader(columns) { + const header = []; + for (let entry of Object.entries(columns)) { + const name = entry[0]; + const title = entry[1][0]; + const order = entry[1][1]; + header.push([order, name, title]); + } + header.sort((a, b) => { + let t = a[0] - b[0]; + if (t === 0) + t = a[1].localeCompare(b[1]); + if (t === 0) + t = a[2].localeCompare(b[2]); + return t; + }); + return header; + } + + static getHeaderLine(header) { + return ( + + {header.map((column, colIndex) => {column[2]})} + + ); + } + + static getBodyRow(header, row, rowIndex, wrapper) { + const wrapped = wrapper(row); + return ( + {header.map((headerColumn, colIndex) => {wrapped.get(headerColumn[1])})} + ); + } + + static getBodyLines(header, data, wrapper) { + return ({data.map((row, rowIndex) => Table.getBodyRow(header, row, rowIndex, wrapper))}); + } + + render() { + const header = Table.getHeader(this.props.columns); + return ( +
+ + + {Table.getHeaderLine(header)} + {Table.getBodyLines(header, this.props.data, this.props.wrapper)} +
{this.props.caption} ({this.props.data.length})
+
+ ); + } +} + +Table.propTypes = { + wrapper: PropTypes.func, + columns: PropTypes.object, + className: PropTypes.string, + caption: PropTypes.string, + data: PropTypes.array +}; diff --git a/diplomacy/web/src/gui/core/tabs.jsx b/diplomacy/web/src/gui/core/tabs.jsx new file mode 100644 index 0000000..6123219 --- /dev/null +++ b/diplomacy/web/src/gui/core/tabs.jsx @@ -0,0 +1,96 @@ +// ============================================================================== +// 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 . +// ============================================================================== +import React from "react"; +import {Action} from "./widgets"; +import PropTypes from 'prop-types'; + +export class Tab extends React.Component { + render() { + const style = { + display: this.props.display ? 'block' : 'none' + }; + const id = this.props.id ? {id: this.props.id} : {}; + return ( +
+ {this.props.children} +
+ ); + } +} + +Tab.propTypes = { + display: PropTypes.bool, + className: PropTypes.string, + id: PropTypes.string, + children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]) +}; + +Tab.defaultProps = { + display: false, + className: '', + id: '' +}; + +export class Tabs extends React.Component { + /** PROPERTIES + * active: index of active menu (must be > menu.length). + * highlights: dictionary mapping a menu indice to a highlight message + * onChange: callback(index): receive index of menu to display. + * **/ + + generateTabAction(tabTitle, tabId, isActive, onChange, highlight) { + return onChange(tabId)} + highlight={highlight} + key={tabId}/>; + } + + render() { + if (!this.props.menu.length) + throw new Error(`No tab menu given.`); + if (this.props.menu.length !== this.props.titles.length) + throw new Error(`Menu length (${this.props.menu.length}) != titles length (${this.props.titles.length})`); + if (this.props.active && !this.props.menu.includes(this.props.active)) + throw new Error(`Invalid active tab name, got ${this.props.active}, expected one of: ${this.props.menu.join(', ')}`); + const active = this.props.active || this.props.menu[0]; + return ( +
+ + {this.props.children} +
+ ); + } +} + +Tabs.propTypes = { + menu: PropTypes.arrayOf(PropTypes.string).isRequired, // tab names + titles: PropTypes.arrayOf(PropTypes.string).isRequired, // tab titles + onChange: PropTypes.func.isRequired, // callback(tab name) + children: PropTypes.array.isRequired, + active: PropTypes.string, // current active tab name + highlights: PropTypes.object, // {tab name => highligh message (optional)} +}; + +Tabs.defaultProps = { + highlights: {} +}; diff --git a/diplomacy/web/src/gui/core/widgets.jsx b/diplomacy/web/src/gui/core/widgets.jsx new file mode 100644 index 0000000..62a5eb4 --- /dev/null +++ b/diplomacy/web/src/gui/core/widgets.jsx @@ -0,0 +1,102 @@ +// ============================================================================== +// 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 . +// ============================================================================== +import React from "react"; +import PropTypes from 'prop-types'; + +export class Button extends React.Component { + /** Bootstrap button. + * Bootstrap classes: + * - btn + * - btn-primary + * - mx-1 (margin-left 1px, margin-right 1px) + * Props: title (str), onClick (function). + * **/ + // title + // onClick + // pickEvent = false + // large = false + // small = false + + constructor(props) { + super(props); + this.onClick = this.onClick.bind(this); + } + + onClick(event) { + if (this.props.onClick) + this.props.onClick(this.props.pickEvent ? event : null); + } + + render() { + return ( + + ); + } +} + +Button.propTypes = { + title: PropTypes.string.isRequired, + onClick: PropTypes.func.isRequired, + color: PropTypes.string, + large: PropTypes.bool, + small: PropTypes.bool, + pickEvent: PropTypes.bool, + disabled: PropTypes.bool +}; + +Button.defaultPropTypes = { + disabled: false +}; + + +export class Action extends React.Component { + // title + // isActive + // onClick + // See Button parameters. + + render() { + return ( +
+
+ {this.props.title} + {this.props.highlight !== null + && this.props.highlight !== undefined + && {this.props.highlight}} +
+
+ ); + } +} + +Action.propTypes = { + title: PropTypes.string.isRequired, + onClick: PropTypes.func.isRequired, + highlight: PropTypes.any, + isActive: PropTypes.bool +}; + +Action.defaultProps = { + highlight: null, + isActive: false +}; diff --git a/diplomacy/web/src/gui/diplomacy/contents/content_connection.jsx b/diplomacy/web/src/gui/diplomacy/contents/content_connection.jsx new file mode 100644 index 0000000..8aa7fb1 --- /dev/null +++ b/diplomacy/web/src/gui/diplomacy/contents/content_connection.jsx @@ -0,0 +1,91 @@ +// ============================================================================== +// 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 . +// ============================================================================== +import React from 'react'; +import {Content} from "../../core/content"; +import {Connection} from "../../../diplomacy/client/connection"; +import {ConnectionForm} from "../forms/connection_form"; +import {DipStorage} from "../utils/dipStorage"; + +export class ContentConnection extends Content { + constructor(props) { + super(props); + this.connection = null; + this.onSubmit = this.onSubmit.bind(this); + } + + static builder(page, data) { + return { + title: 'Connection', + navigation: [], + component: + }; + } + + onSubmit(data) { + const page = this.getPage(); + for (let fieldName of ['hostname', 'port', 'username', 'password', 'showServerFields']) + if (!data.hasOwnProperty(fieldName)) + return page.error(`Missing ${fieldName}, got ${JSON.stringify(data)}`); + page.info('Connecting ...'); + if (this.connection) { + this.connection.currentConnectionProcessing.stop(); + } + this.connection = new Connection(data.hostname, data.port, window.location.protocol.toLowerCase() === 'https:'); + // Page is passed as logger object (with methods info(), error(), success()) when connecting. + this.connection.connect(this.getPage()) + .then(() => { + page.connection = this.connection; + this.connection = null; + page.success(`Successfully connected to server ${data.username}:${data.port}`); + page.connection.authenticate(data.username, data.password, false) + .catch((error) => { + page.error(`Unable to sign in, trying to create an account, error: ${error}`); + return page.connection.authenticate(data.username, data.password, true); + }) + .then((channel) => { + page.channel = channel; + return channel.getAvailableMaps(); + }) + .then(availableMaps => { + page.availableMaps = availableMaps; + const userGameIndices = DipStorage.getUserGames(page.channel.username); + if (userGameIndices && userGameIndices.length) { + return page.channel.getGamesInfo({games: userGameIndices}); + } else { + return null; + } + }) + .then((gamesInfo) => { + if (gamesInfo) { + this.getPage().success('Found ' + gamesInfo.length + ' user games.'); + this.getPage().updateMyGames(gamesInfo); + } + page.loadGames(null, {success: `Account ${data.username} connected.`}); + }) + .catch((error) => { + page.error('Error while authenticating: ' + error + ' Please re-try.'); + }); + }) + .catch((error) => { + page.error('Error while connecting: ' + error + ' Please re-try.'); + }); + } + + render() { + return
; + } +} diff --git a/diplomacy/web/src/gui/diplomacy/contents/content_game.jsx b/diplomacy/web/src/gui/diplomacy/contents/content_game.jsx new file mode 100644 index 0000000..81a689d --- /dev/null +++ b/diplomacy/web/src/gui/diplomacy/contents/content_game.jsx @@ -0,0 +1,1235 @@ +// ============================================================================== +// 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 . +// ============================================================================== +import React from "react"; +import Scrollchor from 'react-scrollchor'; +import {SelectLocationForm} from "../forms/select_location_form"; +import {SelectViaForm} from "../forms/select_via_form"; +import {Order} from "../utils/order"; +import {Button} from "../../core/widgets"; +import {Bar, Row} from "../../core/layouts"; +import {Content} from "../../core/content"; +import {Tab, Tabs} from "../../core/tabs"; +import {Map} from "../map/map"; +import {extendOrderBuilding, ORDER_BUILDER, POSSIBLE_ORDERS} from "../utils/order_building"; +import {PowerActionsForm} from "../forms/power_actions_form"; +import {MessageForm} from "../forms/message_form"; +import {UTILS} from "../../../diplomacy/utils/utils"; +import {Message} from "../../../diplomacy/engine/message"; +import {PowerOrder} from "../widgets/power_order"; +import {MessageView} from "../widgets/message_view"; +import {STRINGS} from "../../../diplomacy/utils/strings"; +import {Diplog} from "../../../diplomacy/utils/diplog"; +import {Table} from "../../core/table"; +import {PowerView} from "../utils/power_view"; +import {FancyBox} from "../../core/fancybox"; +import {DipStorage} from "../utils/dipStorage"; + +const HotKey = require('react-shortcut'); + +/* Order management in game page. + * When editing orders locally, we have to compare it to server orders + * to determine when we need to update orders on server side. There are + * 9 comparison cases, depending on orders: + * SERVER LOCAL DECISION + * null null 0 (same) + * null {} 1 (different, user wants to send "no orders" on server) + * null {orders} 1 (different, user defines new orders locally) + * {} null 0 (assumed same: user is not allowed to "delete" a "no orders": he can only add new orders) + * {} {} 0 (same) + * {} {orders} 1 (different, user defines new orders locally and wants to overwrite the "no-orders" on server) + * {orders} null 1 (different, user wants to delete all server orders, will result to "no-orders") + * {orders} {} 1 (different, user wants to delete all server orders, will result to "no-orders") + * {orders} {orders} same if we have exactly same orders on both server and local + * */ + +const TABLE_POWER_VIEW = { + name: ['Power', 0], + controller: ['Controller', 1], + order_is_set: ['With orders', 2], + wait: ['Waiting', 3] +}; + +function Help() { + return ( +
+

When building an order, press ESC to reset build.

+

Press letter associated to an order type to start building an order of this type. +
Order type letter is indicated in order type name after order type radio button. +

+

In Phase History tab, use keyboard left and right arrows to navigate in past phases.

+
+ ); +} + +export class ContentGame extends Content { + + constructor(props) { + super(props); + // Load local orders from local storage (if available). + const savedOrders = this.props.data.client ? DipStorage.getUserGameOrders( + this.props.data.client.channel.username, + this.props.data.game_id, + this.props.data.phase + ) : null; + let orders = null; + if (savedOrders) { + orders = {}; + for (let entry of Object.entries(savedOrders)) { + let powerOrders = null; + const powerName = entry[0]; + if (entry[1]) { + powerOrders = {}; + for (let orderString of entry[1]) { + const order = new Order(orderString, true); + powerOrders[order.loc] = order; + } + } + orders[powerName] = powerOrders; + } + } + this.schedule_timeout_id = null; + this.state = { + tabMain: null, + tabPastMessages: null, + tabCurrentMessages: null, + messageHighlights: {}, + historyPhaseIndex: null, + historyShowOrders: true, + historySubView: 0, + historyCurrentLoc: null, + historyCurrentOrders: null, + wait: null, // {power name => bool} + orders: orders, // {power name => {loc => {local: bool, order: str}}} + power: null, + orderBuildingType: null, + orderBuildingPath: [], + fancy_title: null, + fancy_function: null, + on_fancy_close: null, + }; + + // Bind some class methods to this instance. + this.closeFancyBox = this.closeFancyBox.bind(this); + this.displayFirstPastPhase = this.displayFirstPastPhase.bind(this); + this.displayLastPastPhase = this.displayLastPastPhase.bind(this); + this.displayLocationOrders = this.displayLocationOrders.bind(this); + this.getMapInfo = this.getMapInfo.bind(this); + this.notifiedGamePhaseUpdated = this.notifiedGamePhaseUpdated.bind(this); + this.notifiedLocalStateChange = this.notifiedLocalStateChange.bind(this); + this.notifiedNetworkGame = this.notifiedNetworkGame.bind(this); + this.notifiedNewGameMessage = this.notifiedNewGameMessage.bind(this); + this.notifiedPowersControllers = this.notifiedPowersControllers.bind(this); + this.onChangeCurrentPower = this.onChangeCurrentPower.bind(this); + this.onChangeMainTab = this.onChangeMainTab.bind(this); + this.onChangeOrderType = this.onChangeOrderType.bind(this); + this.onChangePastPhase = this.onChangePastPhase.bind(this); + this.onChangePastPhaseIndex = this.onChangePastPhaseIndex.bind(this); + this.onChangeShowPastOrders = this.onChangeShowPastOrders.bind(this); + this.onChangeTabCurrentMessages = this.onChangeTabCurrentMessages.bind(this); + this.onChangeTabPastMessages = this.onChangeTabPastMessages.bind(this); + this.onClickMessage = this.onClickMessage.bind(this); + this.onDecrementPastPhase = this.onDecrementPastPhase.bind(this); + this.onIncrementPastPhase = this.onIncrementPastPhase.bind(this); + this.onOrderBuilding = this.onOrderBuilding.bind(this); + this.onOrderBuilt = this.onOrderBuilt.bind(this); + this.onProcessGame = this.onProcessGame.bind(this); + this.onRemoveAllOrders = this.onRemoveAllOrders.bind(this); + this.onRemoveOrder = this.onRemoveOrder.bind(this); + this.onSelectLocation = this.onSelectLocation.bind(this); + this.onSelectVia = this.onSelectVia.bind(this); + this.onSetNoOrders = this.onSetNoOrders.bind(this); + this.reloadServerOrders = this.reloadServerOrders.bind(this); + this.renderOrders = this.renderOrders.bind(this); + this.sendMessage = this.sendMessage.bind(this); + this.setOrders = this.setOrders.bind(this); + this.setSelectedLocation = this.setSelectedLocation.bind(this); + this.setSelectedVia = this.setSelectedVia.bind(this); + this.setWaitFlag = this.setWaitFlag.bind(this); + this.vote = this.vote.bind(this); + } + + static gameTitle(game) { + let title = `${game.game_id} | ${game.phase} | ${game.status} | ${game.role} | ${game.map_name}`; + const remainingTime = game.deadline_timer; + if (remainingTime === undefined) + title += ` (deadline: ${game.deadline} sec)`; + else if (remainingTime) + title += ` (remaining ${remainingTime} sec)`; + return title; + } + + static saveGameToDisk(game, page) { + if (game.client) { + game.client.save() + .then((savedData) => { + const domLink = document.createElement('a'); + domLink.setAttribute( + 'href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(JSON.stringify(savedData))); + domLink.setAttribute('download', `${game.game_id}.json`); + domLink.style.display = 'none'; + document.body.appendChild(domLink); + domLink.click(); + document.body.removeChild(domLink); + }) + .catch(exc => page.error(`Error while saving game: ${exc.toString()}`)); + } else { + page.error(`Cannot save this game.`); + } + } + + static builder(page, data) { + return { + title: ContentGame.gameTitle(data), + navigation: [ + ['Help', () => page.loadFancyBox('Help', () => )], + ['Load a game from disk', page.loadGameFromDisk], + ['Save game to disk', () => ContentGame.saveGameToDisk(data)], + [`${UTILS.html.UNICODE_SMALL_LEFT_ARROW} Games`, page.loadGames], + [`${UTILS.html.UNICODE_SMALL_LEFT_ARROW} Leave game`, () => page.leaveGame(data.game_id)], + [`${UTILS.html.UNICODE_SMALL_LEFT_ARROW} Logout`, page.logout] + ], + component: + }; + } + + static getServerWaitFlags(engine) { + const wait = {}; + const controllablePowers = engine.getControllablePowers(); + for (let powerName of controllablePowers) { + wait[powerName] = engine.powers[powerName].wait; + } + return wait; + } + + static getServerOrders(engine) { + const orders = {}; + const controllablePowers = engine.getControllablePowers(); + for (let powerName of controllablePowers) { + const powerOrders = {}; + let countOrders = 0; + const power = engine.powers[powerName]; + for (let orderString of power.orders) { + const serverOrder = new Order(orderString, false); + powerOrders[serverOrder.loc] = serverOrder; + ++countOrders; + } + orders[powerName] = (countOrders || power.order_is_set) ? powerOrders : null; + } + return orders; + } + + static getOrderBuilding(powerName, orderType, orderPath) { + return { + type: orderType, + path: orderPath, + power: powerName, + builder: orderType && ORDER_BUILDER[orderType] + }; + } + + closeFancyBox() { + this.setState({ + fancy_title: null, + fancy_function: null, + on_fancy_close: null, + orderBuildingPath: [] + }); + } + + setSelectedLocation(location, powerName, orderType, orderPath) { + if (!location) + return; + extendOrderBuilding( + 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) { + if (!moveType || !['M', 'V'].includes(moveType)) + return; + extendOrderBuilding( + 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 = () => ( this.setSelectedLocation(location, powerName, orderType, orderPath)}/>); + this.setState({ + fancy_title: title, + fancy_function: func, + on_fancy_close: this.closeFancyBox + }); + } + + onSelectVia(location, powerName, orderPath) { + const title = `Select move type for move order: ${orderPath.join(' ')}`; + const func = () => ( + this.setSelectedVia(moveType, powerName, orderPath, location)}/>); + this.setState({ + fancy_title: title, + fancy_function: func, + on_fancy_close: this.closeFancyBox + }); + } + + __get_orders(engine) { + const orders = ContentGame.getServerOrders(engine); + if (this.state.orders) { + for (let powerName of Object.keys(orders)) { + const serverPowerOrders = orders[powerName]; + const localPowerOrders = this.state.orders[powerName]; + if (localPowerOrders) { + for (let localOrder of Object.values(localPowerOrders)) { + localOrder.local = ( + !serverPowerOrders + || !serverPowerOrders.hasOwnProperty(localOrder.loc) + || serverPowerOrders[localOrder.loc].order !== localOrder.order + ); + } + } + orders[powerName] = localPowerOrders; + } + } + return orders; + } + + __get_wait(engine) { + return this.state.wait ? this.state.wait : ContentGame.getServerWaitFlags(engine); + } + + getMapInfo() { + return this.props.page.availableMaps[this.props.data.map_name]; + } + + clearScheduleTimeout() { + if (this.schedule_timeout_id) { + clearInterval(this.schedule_timeout_id); + this.schedule_timeout_id = null; + } + } + + updateDeadlineTimer() { + const engine = this.props.data; + --engine.deadline_timer; + if (engine.deadline_timer <= 0) { + engine.deadline_timer = 0; + this.clearScheduleTimeout(); + } + this.getPage().setTitle(ContentGame.gameTitle(engine)); + } + + reloadDeadlineTimer(networkGame) { + networkGame.querySchedule() + .then(dataSchedule => { + const schedule = dataSchedule.schedule; + const server_current = schedule.current_time; + const server_end = schedule.time_added + schedule.delay; + const server_remaining = server_end - server_current; + this.props.data.deadline_timer = server_remaining * schedule.time_unit; + if (!this.schedule_timeout_id) + this.schedule_timeout_id = setInterval(() => this.updateDeadlineTimer(), schedule.time_unit * 1000); + }) + .catch(() => { + if (this.props.data.hasOwnProperty('deadline_timer')) + delete this.props.data.deadline_timer; + this.clearScheduleTimeout(); + // this.getPage().error(`Error while updating deadline timer: ${error.toString()}`); + }); + } + + networkGameIsDisplayed(networkGame) { + return this.getPage().pageIsGame(networkGame.local); + } + + notifiedNetworkGame(networkGame, notification) { + if (this.networkGameIsDisplayed(networkGame)) { + const msg = `Game (${networkGame.local.game_id}) received notification ${notification.name}.`; + this.props.page.loadGame(networkGame.local, {info: msg}); + this.reloadDeadlineTimer(networkGame); + } + } + + notifiedPowersControllers(networkGame, notification) { + if (networkGame.local.isPlayerGame() && ( + !networkGame.channel.game_id_to_instances.hasOwnProperty(networkGame.local.game_id) + || !networkGame.channel.game_id_to_instances[networkGame.local.game_id].has(networkGame.local.role) + )) { + // This power game is now invalid. + this.props.page.disconnectGame(networkGame.local.game_id); + if (this.networkGameIsDisplayed(networkGame)) { + this.props.page.loadGames(null, + {error: `Player game ${networkGame.local.game_id}/${networkGame.local.role} was kicked. Deadline over?`}); + } + } else { + this.notifiedNetworkGame(networkGame, notification); + } + } + + notifiedGamePhaseUpdated(networkGame, notification) { + networkGame.getAllPossibleOrders() + .then(allPossibleOrders => { + networkGame.local.setPossibleOrders(allPossibleOrders); + if (this.networkGameIsDisplayed(networkGame)) { + this.getPage().loadGame( + networkGame.local, {info: `Game update (${notification.name}) to ${networkGame.local.phase}.`} + ); + this.__store_orders(null); + this.setState({orders: null, wait: null, messageHighlights: {}}); + this.reloadDeadlineTimer(networkGame); + } + }) + .catch(error => this.getPage().error('Error when updating possible orders: ' + error.toString())); + } + + notifiedLocalStateChange(networkGame) { + networkGame.getAllPossibleOrders() + .then(allPossibleOrders => { + networkGame.local.setPossibleOrders(allPossibleOrders); + if (this.networkGameIsDisplayed(networkGame)) { + this.getPage().loadGame( + networkGame.local, {info: `Possible orders re-loaded.`} + ); + this.reloadDeadlineTimer(networkGame); + } + }) + .catch(error => this.getPage().error('Error when updating possible orders: ' + error.toString())); + } + + notifiedNewGameMessage(networkGame, notification) { + let protagonist = notification.message.sender; + if (notification.message.recipient === 'GLOBAL') + protagonist = notification.message.recipient; + const messageHighlights = Object.assign({}, this.state.messageHighlights); + if (!messageHighlights.hasOwnProperty(protagonist)) + messageHighlights[protagonist] = 1; + else + ++messageHighlights[protagonist]; + this.setState({messageHighlights: messageHighlights}); + this.notifiedNetworkGame(networkGame, notification); + } + + bindCallbacks(networkGame) { + if (!networkGame.callbacksBound) { + networkGame.addOnClearedCenters(this.notifiedLocalStateChange); + networkGame.addOnClearedOrders(this.notifiedLocalStateChange); + networkGame.addOnClearedUnits(this.notifiedLocalStateChange); + networkGame.addOnPowersControllers(this.notifiedPowersControllers); + networkGame.addOnGameMessageReceived(this.notifiedNewGameMessage); + networkGame.addOnGameProcessed(this.notifiedGamePhaseUpdated); + networkGame.addOnGamePhaseUpdate(this.notifiedGamePhaseUpdated); + networkGame.addOnGameStatusUpdate(this.notifiedNetworkGame); + networkGame.addOnOmniscientUpdated(this.notifiedNetworkGame); + networkGame.addOnPowerOrdersUpdate(this.notifiedNetworkGame); + networkGame.addOnPowerOrdersFlag(this.notifiedNetworkGame); + networkGame.addOnPowerVoteUpdated(this.notifiedNetworkGame); + networkGame.addOnPowerWaitFlag(this.notifiedNetworkGame); + networkGame.addOnVoteCountUpdated(this.notifiedNetworkGame); + networkGame.addOnVoteUpdated(this.notifiedNetworkGame); + networkGame.callbacksBound = true; + networkGame.local.markAllMessagesRead(); + } + } + + onChangeCurrentPower(event) { + this.setState({power: event.target.value}); + } + + onChangeMainTab(tab) { + this.setState({tabMain: tab}); + } + + onChangeTabCurrentMessages(tab) { + this.setState({tabCurrentMessages: tab}); + } + + onChangeTabPastMessages(tab) { + this.setState({tabPastMessages: tab}); + } + + sendMessage(networkGame, recipient, body) { + const engine = networkGame.local; + const message = new Message({ + phase: engine.phase, + sender: engine.role, + recipient: recipient, + message: body + }); + const page = this.props.page; + networkGame.sendGameMessage({message: message}) + .then(() => { + page.loadGame(engine, {success: `Message sent: ${JSON.stringify(message)}`}); + }) + .catch(error => page.error(error.toString())); + } + + __store_orders(orders) { + // Save local orders into local storage. + const username = this.props.data.client.channel.username; + const gameID = this.props.data.game_id; + const gamePhase = this.props.data.phase; + if (!orders) { + return DipStorage.clearUserGameOrders(username, gameID); + } + for (let entry of Object.entries(orders)) { + const powerName = entry[0]; + let powerOrdersList = null; + if (entry[1]) { + powerOrdersList = Object.values(entry[1]).map(order => order.order); + } + DipStorage.clearUserGameOrders(username, gameID, powerName); + DipStorage.addUserGameOrders(username, gameID, gamePhase, powerName, powerOrdersList); + } + } + + reloadServerOrders() { + const serverOrders = ContentGame.getServerOrders(this.props.data); + this.__store_orders(serverOrders); + this.setState({orders: serverOrders}); + } + + setOrders() { + const serverOrders = ContentGame.getServerOrders(this.props.data); + const orders = this.__get_orders(this.props.data); + + for (let entry of Object.entries(orders)) { + const powerName = entry[0]; + const localPowerOrders = entry[1] ? Object.values(entry[1]).map(orderEntry => orderEntry.order) : null; + const serverPowerOrders = serverOrders[powerName] ? Object.values(serverOrders[powerName]).map(orderEntry => orderEntry.order) : null; + let same = false; + + if (serverPowerOrders === null) { + // No orders set on server. + if (localPowerOrders === null) + same = true; + // Otherwise, we have local orders set (even empty local orders). + } else if (serverPowerOrders.length === 0) { + // Empty orders set on server. + // If local orders are null or empty, then we assume + // it's the same thing as empty order set on server. + if (localPowerOrders === null || !localPowerOrders.length) + same = true; + // Otherwise, we have local non-empty orders set. + } else { + // Orders set on server. Identical to local orders only if we have exactly same orders on server and locally. + if (localPowerOrders && localPowerOrders.length === serverPowerOrders.length) { + localPowerOrders.sort(); + serverPowerOrders.sort(); + const length = localPowerOrders.length; + same = true; + for (let i = 0; i < length; ++i) { + if (localPowerOrders[i] !== serverPowerOrders[i]) { + same = false; + break; + } + } + } + } + + if (same) { + Diplog.warn(`Orders not changed for ${powerName}.`); + continue; + } + Diplog.info('Sending orders for ' + powerName + ': ' + JSON.stringify(localPowerOrders)); + this.props.data.client.setOrders({power_name: powerName, orders: localPowerOrders || []}) + .then(() => { + this.props.page.success('Orders sent.'); + }) + .catch(err => { + this.props.page.error(err.toString()); + }) + .then(() => { + this.reloadServerOrders(); + }); + } + } + + onProcessGame() { + this.props.data.client.process() + .then(() => this.props.page.success('Game processed.')) + .catch(err => { + this.props.page.error(err.toString()); + }); + } + + onRemoveOrder(powerName, order) { + const orders = this.__get_orders(this.props.data); + if (orders.hasOwnProperty(powerName) + && orders[powerName].hasOwnProperty(order.loc) + && orders[powerName][order.loc].order === order.order) { + delete orders[powerName][order.loc]; + if (!UTILS.javascript.count(orders[powerName])) + orders[powerName] = null; + this.__store_orders(orders); + this.setState({orders: orders}); + } + } + + onRemoveAllOrders() { + const orders = {}; + const controllablePowers = this.props.data.getControllablePowers(); + for (let powerName of controllablePowers) { + orders[powerName] = null; + } + this.__store_orders(orders); + this.setState({orders: orders}); + } + + onOrderBuilding(powerName, path) { + const pathToSave = path.slice(1); + this.props.page.success(`Building order ${pathToSave.join(' ')} ...`); + this.setState({orderBuildingPath: pathToSave}); + } + + 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); + return; + } + const engine = this.props.data; + const localOrder = new Order(orderString, true); + const allOrders = this.__get_orders(engine); + if (!allOrders.hasOwnProperty(powerName)) { + Diplog.warn(`Unknown power ${powerName}.`); + this.setState(state); + return; + } + + if (!allOrders[powerName]) + allOrders[powerName] = {}; + allOrders[powerName][localOrder.loc] = localOrder; + state.orders = allOrders; + this.props.page.success(`Built order: ${orderString}`); + this.__store_orders(allOrders); + this.setState(state); + } + + onSetNoOrders(powerName) { + const orders = this.__get_orders(this.props.data); + orders[powerName] = {}; + this.__store_orders(orders); + this.setState({orders: orders}); + } + + onChangeOrderType(form) { + this.setState({ + orderBuildingType: form.order_type, + orderBuildingPath: [], + fancy_title: null, + fancy_function: null, + on_fancy_close: null + }); + } + + vote(decision) { + const engine = this.props.data; + const networkGame = engine.client; + const controllablePowers = engine.getControllablePowers(); + const currentPowerName = this.state.power || (controllablePowers.length ? controllablePowers[0] : null); + if (!currentPowerName) + throw new Error(`Internal error: unable to detect current selected power name.`); + networkGame.vote({power_name: currentPowerName, vote: decision}) + .then(() => this.getPage().success(`Vote set to ${decision} for ${currentPowerName}`)) + .catch(error => { + Diplog.error(error.stack); + this.getPage().error(`Error while setting vote for ${currentPowerName}: ${error.toString()}`); + }); + } + + setWaitFlag(waitFlag) { + const engine = this.props.data; + const networkGame = engine.client; + const controllablePowers = engine.getControllablePowers(); + const currentPowerName = this.state.power || (controllablePowers.length ? controllablePowers[0] : null); + if (!currentPowerName) + throw new Error(`Internal error: unable to detect current selected power name.`); + networkGame.setWait(waitFlag, {power_name: currentPowerName}) + .then(() => this.getPage().success(`Wait flag set to ${waitFlag} for ${currentPowerName}`)) + .catch(error => { + Diplog.error(error.stack); + this.getPage().error(`Error while setting wait flag for ${currentPowerName}: ${error.toString()}`); + }); + } + + __change_past_phase(newPhaseIndex, subView) { + this.setState({ + historyPhaseIndex: newPhaseIndex, + historySubView: (subView ? subView : 0), + historyCurrentLoc: null, + historyCurrentOrders: null + }); + } + + onChangePastPhase(event) { + this.__change_past_phase(event.target.value); + } + + onChangePastPhaseIndex(increment) { + const selectObject = document.getElementById('select-past-phase'); + if (selectObject) { + if (!this.state.historyShowOrders) { + // We must change map sub-view before showed phase index. + const currentSubView = this.state.historySubView; + const newSubView = currentSubView + (increment ? 1 : -1); + if (newSubView === 0 || newSubView === 1) { + // Sub-view correctly updated. We don't yet change showed phase. + return this.setState({historySubView: newSubView}); + } + // Sub-view badly updated (either from 0 to -1, or from 1 to 2). We must change phase. + } + // Let's simply increase or decrease index of showed past phase. + const index = selectObject.selectedIndex; + const newIndex = index + (increment ? 1 : -1); + if (newIndex >= 0 && newIndex < selectObject.length) { + selectObject.selectedIndex = newIndex; + this.__change_past_phase(parseInt(selectObject.options[newIndex].value, 10), (increment ? 0 : 1)); + } + } + } + + onIncrementPastPhase(event) { + this.onChangePastPhaseIndex(true); + if (event && event.preventDefault) + event.preventDefault(); + } + + onDecrementPastPhase(event) { + this.onChangePastPhaseIndex(false); + if (event && event.preventDefault) + event.preventDefault(); + } + + displayFirstPastPhase() { + this.__change_past_phase(0, 0); + } + + displayLastPastPhase() { + this.__change_past_phase(-1, 1); + } + + onChangeShowPastOrders(event) { + this.setState({historyShowOrders: event.target.checked, historySubView: 0}); + } + + renderOrders(engine, currentPowerName) { + const serverOrders = ContentGame.getServerOrders(this.props.data); + const orders = this.__get_orders(engine); + const wait = this.__get_wait(engine); + + const render = []; + render.push(); + return render; + } + + onClickMessage(message) { + if (!message.read) { + message.read = true; + let protagonist = message.sender; + if (message.recipient === 'GLOBAL') + protagonist = message.recipient; + this.getPage().loadGame(this.props.data); + if (this.state.messageHighlights.hasOwnProperty(protagonist) && this.state.messageHighlights[protagonist] > 0) { + const messageHighlights = Object.assign({}, this.state.messageHighlights); + --messageHighlights[protagonist]; + this.setState({messageHighlights: messageHighlights}); + } + } + } + + displayLocationOrders(loc, orders) { + this.setState({ + historyCurrentLoc: loc || null, + historyCurrentOrders: orders && orders.length ? orders : null + }); + } + + renderPastMessages(engine) { + const messageChannels = engine.getMessageChannels(); + let tabNames = null; + if (engine.isPlayerGame()) { + tabNames = []; + for (let powerName of Object.keys(engine.powers)) if (powerName !== engine.role) + tabNames.push(powerName); + tabNames.sort(); + tabNames.push('GLOBAL'); + } else { + tabNames = Object.keys(messageChannels); + } + const currentTabId = this.state.tabPastMessages || tabNames[0]; + + return ( +
+ {/* Messages. */} + + {tabNames.map(protagonist => ( + + {(!messageChannels.hasOwnProperty(protagonist) || !messageChannels[protagonist].length ? + (
No + messages{engine.isPlayerGame() ? ` with ${protagonist}` : ''}.
) : + messageChannels[protagonist].map((message, index) => ( + + )) + )} +
+ ))} +
+
+ ); + } + + renderCurrentMessages(engine) { + const messageChannels = engine.getMessageChannels(); + let tabNames = null; + let highlights = null; + if (engine.isPlayerGame()) { + tabNames = []; + for (let powerName of Object.keys(engine.powers)) if (powerName !== engine.role) + tabNames.push(powerName); + tabNames.sort(); + tabNames.push('GLOBAL'); + highlights = this.state.messageHighlights; + } else { + tabNames = Object.keys(messageChannels); + let totalHighlights = 0; + for (let count of Object.values(this.state.messageHighlights)) + totalHighlights += count; + highlights = {messages: totalHighlights}; + } + const unreadMarked = new Set(); + const currentTabId = this.state.tabCurrentMessages || tabNames[0]; + + return ( +
+ {/* Messages. */} + + {tabNames.map(protagonist => ( + + {(!messageChannels.hasOwnProperty(protagonist) || !messageChannels[protagonist].length ? + (
No + messages{engine.isPlayerGame() ? ` with ${protagonist}` : ''}.
) : + (messageChannels[protagonist].map((message, index) => { + let id = null; + if (!message.read && !unreadMarked.has(protagonist)) { + if (engine.isOmniscientGame() || message.sender !== engine.role) { + unreadMarked.add(protagonist); + id = `${protagonist}-unread`; + } + } + return ; + })) + )} +
+ ))} +
+ {/* Link to go to first unread received message. */} + {unreadMarked.has(currentTabId) && ( + + Go to 1st unread message + + )} + {/* Send form. */} + {engine.isPlayerGame() && ( + + this.sendMessage(engine.client, currentTabId, form.message)}/>)} +
+ ); + } + + renderPastMap(gameEngine, showOrders) { + return ; + } + + renderCurrentMap(gameEngine, powerName, orderType, orderPath) { + const rawOrders = this.__get_orders(gameEngine); + const orders = {}; + for (let entry of Object.entries(rawOrders)) { + orders[entry[0]] = []; + if (entry[1]) { + for (let orderObject of Object.values(entry[1])) + orders[entry[0]].push(orderObject.order); + } + } + return ; + } + + renderTabPhaseHistory(toDisplay, initialEngine) { + const pastPhases = initialEngine.state_history.values().map(state => state.name); + if (initialEngine.phase === 'COMPLETED') { + pastPhases.push('COMPLETED'); + } + let phaseIndex = 0; + if (initialEngine.displayed) { + if (this.state.historyPhaseIndex === null || this.state.historyPhaseIndex >= pastPhases.length) { + phaseIndex = pastPhases.length - 1; + } else { + if (this.state.historyPhaseIndex < 0) { + phaseIndex = pastPhases.length + this.state.historyPhaseIndex; + } else { + phaseIndex = this.state.historyPhaseIndex; + } + } + } + const engine = ( + phaseIndex === initialEngine.state_history.size() ? + initialEngine : initialEngine.cloneAt(initialEngine.state_history.keyFromIndex(phaseIndex)) + ); + let orders = {}; + let orderResult = null; + if (engine.order_history.contains(engine.phase)) + orders = engine.order_history.get(engine.phase); + if (engine.result_history.contains(engine.phase)) + orderResult = engine.result_history.get(engine.phase); + let countOrders = 0; + for (let powerOrders of Object.values(orders)) { + if (powerOrders) + countOrders += powerOrders.length; + } + const powerNames = Object.keys(orders); + powerNames.sort(); + + const getOrderResult = (order) => { + if (orderResult) { + const pieces = order.split(/ +/); + const unit = `${pieces[0]} ${pieces[1]}`; + if (orderResult.hasOwnProperty(unit)) { + const resultsToParse = orderResult[unit]; + if (!resultsToParse.length) + resultsToParse.push(''); + const results = []; + for (let r of resultsToParse) { + if (results.length) + results.push(', '); + results.push({r || 'OK'}); + } + return ({results}); + } + } + return ''; + }; + + const orderView = [ + (
+