// ============================================================================== // 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 {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] }; const PRETTY_ROLES = { [STRINGS.OMNISCIENT_TYPE]: 'Omnicient', [STRINGS.OBSERVER_TYPE]: 'Observer' }; 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: [] }; // Bind some class methods to this instance. this.clearOrderBuildingPath = this.clearOrderBuildingPath.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 prettyRole(role) { if (PRETTY_ROLES.hasOwnProperty(role)) return PRETTY_ROLES[role]; return role; } static gameTitle(game) { let title = `${game.game_id} | ${game.phase} | ${game.status} | ${ContentGame.prettyRole(game.role)} | ${game.map_name}`; if (game.daide_port) title += ` | DAIDE ${game.daide_port}`; 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; } clearOrderBuildingPath() { this.setState({ 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 ); } setSelectedVia(moveType, powerName, orderPath, location) { if (!moveType || !['M', 'V'].includes(moveType)) return; extendOrderBuilding( powerName, moveType, orderPath, location, this.onOrderBuilding, this.onOrderBuilt, this.getPage().error ); } onSelectLocation(possibleLocations, powerName, orderType, orderPath) { this.getPage().dialog(onClose => ( { this.setSelectedLocation(location, powerName, orderType, orderPath); onClose(); }} onClose={() => { this.clearOrderBuildingPath(); onClose(); }}/> )); } onSelectVia(location, powerName, orderPath) { this.getPage().dialog(onClose => ( { setTimeout(() => { this.setSelectedVia(moveType, powerName, orderPath, location); onClose(); }, 0); }} onClose={() => { this.clearOrderBuildingPath(); onClose(); }}/> )); } // ] 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) .then(() => { 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, notification) { 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.`} ); if (notification.power_name) { this.reloadPowerServerOrders(notification.power_name); } 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.notifiedLocalStateChange); networkGame.addOnPowerOrdersFlag(this.notifiedLocalStateChange); 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 for given power. * @param {string} powerName - name of power to update */ reloadPowerServerOrders(powerName) { const serverOrders = this.props.data.getServerOrders(); const engine = this.props.data; const allOrders = this.__get_orders(engine); if (!allOrders.hasOwnProperty(powerName)) { this.getPage().error(`Unknown power ${powerName}.`); return; } allOrders[powerName] = serverOrders[powerName]; this.__store_orders(allOrders); this.setState({orders: allOrders}); } /** * Reset local orders and replace them with current server orders for current selected power. */ reloadServerOrders() { const currentPowerName = this.getCurrentPowerName(); if (currentPowerName) { this.reloadPowerServerOrders(currentPowerName); } } /** * 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, including empty orders set. * Equivalent request is clearOrders(). */ 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 = []; 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: [], }); } 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 (