From 2701df1e3b03c7c605ccf212a02987d53fbd0609 Mon Sep 17 00:00:00 2001 From: notoraptor Date: Wed, 17 Jul 2019 15:16:43 -0400 Subject: [web] Make button "Delete all" remove only orders from current se… (#49) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Make button "Delete all" remove only orders from current selected power. - Reorganize code - [web] Remove bugged and useless function gameReloaded() from game page. - This function caused error `engine.getPhaseType is not a function` for games with deadlines. - Move function saveGameToDisk into its own file. - [web] Add documentation to methods involved in orders management to help understand what happens. - Move methods getServerOrders() from game GUI component to game engine object. - Rename method onSetNoOrders to onSetEmptyOrdersSet. - Rename property in PowerActionsForm: onNoOrders => onPass. - [web] Update sending orders to send request clearOrders when local orders list is null. - Renamed local file: - components/power_order => power_orders - forms/power_actions_form => power_order_creation_form - Move power orders buttons bar to a separate file: - components/power_orders_actions_bar - [web] Improve messages about local/server defined orders. --- diplomacy/web/src/gui/pages/content_game.jsx | 1303 ++++++++++++++++++++++++++ 1 file changed, 1303 insertions(+) create mode 100644 diplomacy/web/src/gui/pages/content_game.jsx (limited to 'diplomacy/web/src/gui/pages/content_game.jsx') diff --git a/diplomacy/web/src/gui/pages/content_game.jsx b/diplomacy/web/src/gui/pages/content_game.jsx new file mode 100644 index 0000000..f1bc76c --- /dev/null +++ b/diplomacy/web/src/gui/pages/content_game.jsx @@ -0,0 +1,1303 @@ +// ============================================================================== +// 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 {Row} from "../components/layouts"; +import {Tabs} from "../components/tabs"; +import {Map} from "../map/map"; +import {extendOrderBuilding, ORDER_BUILDER, POSSIBLE_ORDERS} from "../utils/order_building"; +import {PowerOrderCreationForm} from "../forms/power_order_creation_form"; +import {MessageForm} from "../forms/message_form"; +import {UTILS} from "../../diplomacy/utils/utils"; +import {Message} from "../../diplomacy/engine/message"; +import {PowerOrders} from "../components/power_orders"; +import {MessageView} from "../components/message_view"; +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"; +import {PageContext} from "../components/page_context"; +import PropTypes from 'prop-types'; +import {Help} from "../components/help"; +import {Tab} from "../components/tab"; +import {Button} from "../components/button"; +import {saveGameToDisk} from "../utils/saveGameToDisk"; +import {Game} from '../../diplomacy/engine/game'; +import {PowerOrdersActionBar} from "../components/power_orders_actions_bar"; + +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] +}; + +export class ContentGame extends React.Component { + + 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, + 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.onRemoveAllCurrentPowerOrders = this.onRemoveAllCurrentPowerOrders.bind(this); + this.onRemoveOrder = this.onRemoveOrder.bind(this); + this.onSelectLocation = this.onSelectLocation.bind(this); + this.onSelectVia = this.onSelectVia.bind(this); + this.onSetEmptyOrdersSet = this.onSetEmptyOrdersSet.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 getServerWaitFlags(engine) { + const wait = {}; + const controllablePowers = engine.getControllablePowers(); + for (let powerName of controllablePowers) { + wait[powerName] = engine.powers[powerName].wait; + } + return wait; + } + + static getOrderBuilding(powerName, orderType, orderPath) { + return { + type: orderType, + path: orderPath, + power: powerName, + builder: orderType && ORDER_BUILDER[orderType] + }; + } + + getPage() { + return this.context; + } + + closeFancyBox() { + this.setState({ + fancy_title: null, + fancy_function: null, + on_fancy_close: null, + orderBuildingPath: [] + }); + } + + // [ Methods used to handle current map. + + 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 + }); + } + + // ] + + getMapInfo() { + return this.getPage().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().load(`game: ${engine.game_id}`, ); + } + + 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()}`); + }); + } + + // [ Network game notifications. + + /** + * Return True if given network game is the game currently displayed on the interface. + * @param {NetworkGame} networkGame - network game to check + * @returns {boolean} + */ + networkGameIsDisplayed(networkGame) { + return this.getPage().getName() === `game: ${networkGame.local.game_id}`; + } + + notifiedNetworkGame(networkGame, notification) { + if (this.networkGameIsDisplayed(networkGame)) { + const msg = `Game (${networkGame.local.game_id}) received notification ${notification.name}.`; + this.getPage().load( + `game: ${networkGame.local.game_id}`, + , + {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.getPage().disconnectGame(networkGame.local.game_id); + if (this.networkGameIsDisplayed(networkGame)) { + const page = this.getPage(); + page.loadGames( + {error: `${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().load( + `game: ${networkGame.local.game_id}`, + , + {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().load( + `game: ${networkGame.local.game_id}`, + , + {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, tabPastMessages: null, tabCurrentMessages: null}); + } + + 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.getPage(); + networkGame.sendGameMessage({message: message}) + .then(() => { + page.load( + `game: ${engine.game_id}`, + , + {success: `Message sent: ${JSON.stringify(message)}`} + ); + }) + .catch(error => page.error(error.toString())); + } + + onProcessGame() { + const page = this.getPage(); + this.props.data.client.process() + .then(() => page.success('Game processed.')) + .catch(err => { + page.error(err.toString()); + }); + } + + /** + * Get name of current power selected on the game page. + * @returns {null|string} + */ + getCurrentPowerName() { + const engine = this.props.data; + const controllablePowers = engine.getControllablePowers(); + return this.state.power || (controllablePowers.length && controllablePowers[0]); + } + + __get_wait(engine) { + return this.state.wait ? this.state.wait : ContentGame.getServerWaitFlags(engine); + } + + // [ Methods involved in orders management. + + /** + * Return a dictionary of local orders for given game engine. + * Returned dictionary maps each power name to either: + * - a dictionary of orders, mapping a location to an Order object with boolean flag `local` correctly set + * to determine if that order is a new local order or is a copy of an existing server order for this power. + * - null or empty dictionary, if there are no local orders defined for this power. + * @param {Game} engine - game engine from which we must get local orders + * @returns {{}} + * @private + */ + __get_orders(engine) { + const orders = engine.getServerOrders(); + 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; + } + + /** + * Save given orders into local storage. + * @param orders - orders to save + * @private + */ + __store_orders(orders) { + 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); + } + } + + /** + * Reset local orders and replace them with current server orders. + */ + reloadServerOrders() { + // TODO: This method should reset orders to server version only for current powers, not for all powers as she currently does. + const serverOrders = this.props.data.getServerOrders(); + this.__store_orders(serverOrders); + this.setState({orders: serverOrders}); + } + + /** + * Remove given order from local orders of given power name. + * @param {string} powerName - power name + * @param {Order} order - order to remove + */ + 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}); + } + } + + /** + * Remove all local orders for current selected power + */ + onRemoveAllCurrentPowerOrders() { + const currentPowerName = this.getCurrentPowerName(); + if (currentPowerName) { + const engine = this.props.data; + const allOrders = this.__get_orders(engine); + if (!allOrders.hasOwnProperty(currentPowerName)) { + this.getPage().error(`Unknown power ${currentPowerName}.`); + return; + } + allOrders[currentPowerName] = null; + this.__store_orders(allOrders); + this.setState({orders: allOrders}); + } + } + + /** + * Set an empty local orders set for given power name. + * @param {string} powerName - power name + */ + onSetEmptyOrdersSet(powerName) { + const orders = this.__get_orders(this.props.data); + orders[powerName] = {}; + this.__store_orders(orders); + this.setState({orders: orders}); + } + + /** + * Send local orders to server. + */ + setOrders() { + const serverOrders = this.props.data.getServerOrders(); + 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. + same = localPowerOrders === null; + // Otherwise, we have local orders set (even empty local orders). + } else if (serverPowerOrders.length === 0) { + // Empty orders set on server. + // If we have empty orders set locally, then it's same thing. + same = localPowerOrders && localPowerOrders.length === 0; + // Otherwise, we have either local non-empty orders set or local null order. + } 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(); + same = true; + for (let i = 0; i < localPowerOrders.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}: ${localPowerOrders ? JSON.stringify(localPowerOrders) : null}`); + let requestCall = null; + if (localPowerOrders) { + requestCall = this.props.data.client.setOrders({power_name: powerName, orders: localPowerOrders}); + } else { + requestCall = this.props.data.client.clearOrders({power_name: powerName}); + } + requestCall + .then(() => { + this.getPage().success('Orders sent.'); + }) + .catch(err => { + this.getPage().error(err.toString()); + }) + .then(() => { + this.reloadServerOrders(); + }); + } + } + + // ] + + onOrderBuilding(powerName, path) { + const pathToSave = path.slice(1); + this.getPage().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.getPage().success(`Built order: ${orderString}`); + this.__store_orders(allOrders); + this.setState(state); + } + + 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) { + this.setState({ + historyPhaseIndex: newPhaseIndex, + historyCurrentLoc: null, + historyCurrentOrders: null + }); + } + + onChangePastPhase(event) { + this.__change_past_phase(event.target.value); + } + + onChangePastPhaseIndex(increment) { + const selectObject = document.getElementById('select-past-phase'); + if (selectObject) { + // 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}); + } + + onClickMessage(message) { + if (!message.read) { + message.read = true; + let protagonist = message.sender; + if (message.recipient === 'GLOBAL') + protagonist = message.recipient; + this.getPage().load(`game: ${this.props.data.game_id}`, ); + 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 + }); + } + + // [ Rendering methods. + + renderOrders(engine, currentPowerName) { + const serverOrders = this.props.data.getServerOrders(); + const orders = this.__get_orders(engine); + const wait = this.__get_wait(engine); + + const render = []; + render.push(); + return render; + } + + renderPastMessages(engine, role) { + const messageChannels = engine.getMessageChannels(role, true); + const tabNames = []; + for (let powerName of Object.keys(engine.powers)) if (powerName !== role) + tabNames.push(powerName); + tabNames.sort(); + tabNames.push('GLOBAL'); + const titles = tabNames.map(tabName => (tabName === 'GLOBAL' ? tabName : tabName.substr(0, 3))); + 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, role) { + const messageChannels = engine.getMessageChannels(role, true); + const tabNames = []; + for (let powerName of Object.keys(engine.powers)) if (powerName !== role) + tabNames.push(powerName); + tabNames.sort(); + tabNames.push('GLOBAL'); + const titles = tabNames.map(tabName => (tabName === 'GLOBAL' ? tabName : tabName.substr(0, 3))); + const currentTabId = this.state.tabCurrentMessages || tabNames[0]; + const highlights = this.state.messageHighlights; + const unreadMarked = new Set(); + + 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 !== 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)}/>)} +
+ ); + } + + renderMapForResults(gameEngine, showOrders) { + return ; + } + + renderMapForMessages(gameEngine, showOrders) { + return ; + } + + renderMapForCurrent(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 ; + } + + __get_engine_to_display(initialEngine) { + const pastPhases = initialEngine.state_history.values().map(state => state.name); + pastPhases.push(initialEngine.phase); + 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 = ( + pastPhases[phaseIndex] === initialEngine.phase ? + initialEngine : initialEngine.cloneAt(pastPhases[phaseIndex]) + ); + return {engine, pastPhases, phaseIndex}; + } + + __form_phases(pastPhases, phaseIndex) { + return ( +
+