From 891fb62a77b9a86f2bc71cc02a82089412982b2f Mon Sep 17 00:00:00 2001 From: notoraptor Date: Thu, 1 Aug 2019 15:53:23 -0400 Subject: Refactored SVG map into a React component - Create link to diplomacy map folder into web/src/diplomacy/maps - Remove old web/src/gui/map folder. - [web] Handle click only on current map. - [web/game] Remove useless `wait` state. - Remove unused nodejs modules. - [web] Use queue to handle game notifications in sequential order. - Make all calls to setState() asynchronous in Page and ContentGame components. - Make sure notifications are handled in the order in which they come. --- diplomacy/web/src/gui/maps/standard/SvgStandard.js | 743 +++++++++++++++++++++ 1 file changed, 743 insertions(+) create mode 100644 diplomacy/web/src/gui/maps/standard/SvgStandard.js (limited to 'diplomacy/web/src/gui/maps/standard/SvgStandard.js') diff --git a/diplomacy/web/src/gui/maps/standard/SvgStandard.js b/diplomacy/web/src/gui/maps/standard/SvgStandard.js new file mode 100644 index 0000000..87c6217 --- /dev/null +++ b/diplomacy/web/src/gui/maps/standard/SvgStandard.js @@ -0,0 +1,743 @@ +// ============================================================================== +// 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 . +// ============================================================================== +/** Generated using svg_to_react.py with parameters: +Namespace(actionable=['MouseLayer'], input='src/diplomacy/maps/svg/standard.svg', name='SvgStandard', output='src/gui/maps/', remove=None) +**/ +import React from 'react'; +import PropTypes from 'prop-types'; +import "./SvgStandard.css"; +import {Game} from "../../../diplomacy/engine/game"; +import {MapData} from "../../utils/map_data"; +import {Unit} from "./unit"; +import {SupplyCenter} from "./supplyCenter"; +import {Hold} from "./hold"; +import {Move} from "./move"; +import {SupportMove} from "./supportMove"; +import {SupportHold} from "./supportHold"; +import {Convoy} from "./convoy"; +import {Build} from "./build"; +import {Disband} from "./disband"; +import {UTILS} from "../../../diplomacy/utils/utils"; +import {Diplog} from "../../../diplomacy/utils/diplog"; +import {extendOrderBuilding} from "../../utils/order_building"; + +function setInfluence(classes, mapData, loc, power_name) { + loc = loc.toUpperCase().substr(0, 3); + if (!['LAND', 'COAST'].includes(mapData.getProvince(loc).type)) + return; + const id = '_' + loc.toLowerCase(); + if (!classes.hasOwnProperty(id)) + throw new Error(`Unable to find SVG path for loc ${id}`); + classes[id] = power_name ? power_name.toLowerCase() : 'nopower'; +} + +function getClickedID(event) { + let node = event.target; + if (!node.id && node.parentNode.id && node.parentNode.tagName === 'g') + node = node.parentNode; + let id = node.id; + return id ? id.substr(0, 3) : null; +} + +function parseLocation(txt) { + if (txt.length > 2 && txt[1] === ' ' && ['A', 'F'].includes(txt[0])) + return txt.substr(2); + return txt; +} + +export class SvgStandard extends React.Component { + constructor(props) { + super(props); + this.onClick = this.onClick.bind(this); + this.onHover = this.onHover.bind(this); + } + onClick(event) { + if (this.props.orderBuilding) + return this.handleClickedID(getClickedID(event)); + } + onHover(event) { + return this.handleHoverID(getClickedID(event)); + } + handleClickedID(id) { + const orderBuilding = this.props.orderBuilding; + if (!orderBuilding.builder) + return this.props.onError('No orderable locations.'); + const province = this.props.mapData.getProvince(id); + if (!province) + return; + + const stepLength = orderBuilding.builder.steps.length; + if (orderBuilding.path.length >= stepLength) + throw new Error(`Order building: current steps count (${orderBuilding.path.length}) should be less than` + + ` expected steps count (${stepLength}) (${orderBuilding.path.join(', ')}).`); + + const lengthAfterClick = orderBuilding.path.length + 1; + let validLocations = []; + const testedPath = [orderBuilding.type].concat(orderBuilding.path); + const value = UTILS.javascript.getTreeValue(this.props.game.ordersTree, testedPath); + if (value !== null) { + const checker = orderBuilding.builder.steps[lengthAfterClick - 1]; + try { + const possibleLocations = checker(province, orderBuilding.power); + for (let possibleLocation of possibleLocations) { + possibleLocation = possibleLocation.toUpperCase(); + if (value.includes(possibleLocation)) + validLocations.push(possibleLocation); + } + } catch (error) { + return this.props.onError(error); + } + } + if (!validLocations.length) + return this.props.onError('Disallowed.'); + + if (validLocations.length > 1 && orderBuilding.type === 'S' && orderBuilding.path.length >= 2) { + // We are building a support order and we have a multiple choice for a location. + // Let's check if next location to choose is a coast. To have a coast: + // - all possible locations must start with same 3 characters. + // - we expect at least province name in possible locations (e.g. 'SPA' for 'SPA/NC'). + // If we have a coast, we will remove province name from possible locations. + let isACoast = true; + let validLocationsNoProvinceName = []; + for (let i = 0; i < validLocations.length; ++i) { + let location = validLocations[i]; + if (i > 0) { + // Compare 3 first letters with previous location. + if (validLocations[i - 1].substring(0, 3).toUpperCase() !== validLocations[i].substring(0, 3).toUpperCase()) { + // No same prefix with previous location. We does not have a coast. + isACoast = false; + break; + } + } + if (location.length !== 3) + validLocationsNoProvinceName.push(location); + } + if (validLocations.length === validLocationsNoProvinceName.length) { + // We have not found province name. + isACoast = false; + } + if (isACoast) { + // We want to choose location in a coastal province. Let's remove province name. + validLocations = validLocationsNoProvinceName; + } + } + + if (validLocations.length > 1) { + if (this.props.onSelectLocation) { + return this.props.onSelectLocation(validLocations, orderBuilding.power, orderBuilding.type, orderBuilding.path); + } else { + Diplog.warn(`Forced to select first valid location.`); + validLocations = [validLocations[0]]; + } + } + let orderBuildingType = orderBuilding.type; + if (lengthAfterClick === stepLength && orderBuildingType === 'M') { + const moveOrderPath = ['M'].concat(orderBuilding.path, validLocations[0]); + const moveTypes = UTILS.javascript.getTreeValue(this.props.game.ordersTree, moveOrderPath); + if (moveTypes !== null) { + if (moveTypes.length === 2 && this.props.onSelectVia) { + // This move can be done either regularly or VIA a fleet. Let user choose. + return this.props.onSelectVia(validLocations[0], orderBuilding.power, orderBuilding.path); + } else { + orderBuildingType = moveTypes[0]; + } + } + } + extendOrderBuilding( + orderBuilding.power, orderBuildingType, orderBuilding.path, validLocations[0], + this.props.onOrderBuilding, this.props.onOrderBuilt, this.props.onError + ); + } + handleHoverID(id) { + if (this.props.onHover) { + const province = this.props.mapData.getProvince(id); + if (province) { + this.props.onHover(province.name, this.getRelatedOrders(province.name)); + } + } + } + getRelatedOrders(name) { + const orders = []; + if (this.props.orders) { + for (let powerOrders of Object.values(this.props.orders)) { + if (powerOrders) { + for (let order of powerOrders) { + const pieces = order.split(/ +/); + if (pieces[1].slice(0, 3) === name.toUpperCase().slice(0, 3)) + orders.push(order); + } + } + } + } + return orders; + } + getNeighbors(extraLocation) { + const selectedPath = [this.props.orderBuilding.type].concat(this.props.orderBuilding.path); + if (extraLocation) + selectedPath.push(extraLocation); + const possibleNeighbors = UTILS.javascript.getTreeValue(this.props.game.ordersTree, selectedPath); + const neighbors = possibleNeighbors ? possibleNeighbors.map(neighbor => parseLocation(neighbor)) : []; + return neighbors.length ? neighbors: null; + } + render() { + const classes = {"_ank":"nopower","_arm":"nopower","_con":"nopower","_mos":"nopower","_sev":"nopower","_stp":"nopower","_syr":"nopower","_ukr":"nopower","_lvn":"nopower","_war":"nopower","_pru":"nopower","_sil":"nopower","_ber":"nopower","_kie":"nopower","_ruh":"nopower","_mun":"nopower","_rum":"nopower","_bul":"nopower","_gre":"nopower","_smy":"nopower","_alb":"nopower","_ser":"nopower","_bud":"nopower","_gal":"nopower","_vie":"nopower","_boh":"nopower","_tyr":"nopower","_tri":"nopower","_fin":"nopower","_swe":"nopower","_nwy":"nopower","_den":"nopower","_hol":"nopower","_bel":"nopower","_swi":"impassable","_ven":"nopower","_pie":"nopower","_tus":"nopower","_rom":"nopower","_apu":"nopower","_nap":"nopower","_bur":"nopower","_mar":"nopower","_gas":"nopower","_pic":"nopower","_par":"nopower","_bre":"nopower","_spa":"nopower","_por":"nopower","_naf":"nopower","_tun":"nopower","_lon":"nopower","_wal":"nopower","_lvp":"nopower","_yor":"nopower","_edi":"nopower","_cly":"nopower","unplayable":"neutral","unplayable_water":"water","_nat":"water","_nrg":"water","_bar":"water","_bot":"water","_bal":"water","denmark_water":"water","_ska":"water","_hel":"water","_nth":"water","_eng":"water","_iri":"water","_mid":"water","_wes":"water","_gol":"water","_tyn":"water","_adr":"water","_ion":"water","_aeg":"water","_eas":"water","constantinople_water":"water","_bla":"water","BriefLabelLayer":"labeltext24","CurrentNote":"currentnotetext","CurrentNote2":"currentnotetext","CurrentPhase":"currentphasetext","MouseLayer":"invisibleContent"}; + const game = this.props.game; + const mapData = this.props.mapData; + const orders = this.props.orders; + + //// Current phase. + const current_phase = (game.phase[0] === '?' || game.phase === 'COMPLETED') ? 'FINAL' : game.phase; + + //// Notes. + const nb_centers = []; + for (let power of Object.values(game.powers)) { + if (!power.isEliminated()) + nb_centers.push([power.name.substr(0, 3), power.centers.length]); + } + // Sort nb_centers by descending number of centers. + nb_centers.sort((a, b) => { + return -(a[1] - b[1]) || a[0].localeCompare(b[0]); + }); + const nb_centers_per_power = nb_centers.map((couple) => (couple[0] + ': ' + couple[1])).join(' '); + const note = game.note; + + //// Adding units, supply centers, influence and orders. + const scs = new Set(mapData.supplyCenters); + const renderedUnits = []; + const renderedDislodgedUnits = []; + const renderedSupplyCenters = []; + const renderedOrders = []; + const renderedOrders2 = []; + const renderedHighestOrders = []; + for (let power of Object.values(game.powers)) { + for (let unit of power.units) { + renderedUnits.push(); + } + for (let unit of Object.keys(power.retreats)) { + renderedDislodgedUnits.push(); + } + for (let center of power.centers) { + renderedSupplyCenters.push(); + setInfluence(classes, mapData, center, power.name); + scs.delete(center); + } + if (!power.isEliminated()) { + for (let loc of power.influence) { + if (!mapData.supplyCenters.has(loc)) + setInfluence(classes, mapData, loc, power.name); + } + } + + if (orders) { + const powerOrders = (orders && orders.hasOwnProperty(power.name) && orders[power.name]) || []; + for (let order of powerOrders) { + const tokens = order.split(/ +/); + if (!tokens || tokens.length < 3) + continue; + const unit_loc = tokens[1]; + if (tokens[2] === 'H') { + renderedOrders.push(); + } else if (tokens[2] === '-') { + const destLoc = tokens[tokens.length - (tokens[tokens.length - 1] === 'VIA' ? 2 : 1)]; + renderedOrders.push(); + } else if (tokens[2] === 'S') { + const destLoc = tokens[tokens.length - 1]; + if (tokens.includes('-')) { + const srcLoc = tokens[4]; + renderedOrders2.push(); + } else { + renderedOrders2.push(); + } + } else if (tokens[2] === 'C') { + const srcLoc = tokens[4]; + const destLoc = tokens[tokens.length - 1]; + if ((srcLoc !== destLoc) && (tokens.includes('-'))) { + renderedOrders2.push(); + } + } else if (tokens[2] === 'B') { + renderedHighestOrders.push(); + } else if (tokens[2] === 'D') { + renderedHighestOrders.push(); + } else if (tokens[2] === 'R') { + const srcLoc = tokens[1]; + const destLoc = tokens[3]; + renderedOrders.push(); + } else { + throw new Error(`Unknown error to render (${order}).`); + } + } + } + } + // Adding remaining supply centers. + for (let remainingCenter of scs) { + renderedSupplyCenters.push(); + } + + if (this.props.orderBuilding && this.props.orderBuilding.path.length) { + const clicked = parseLocation(this.props.orderBuilding.path[0]); + const province = this.props.mapData.getProvince(clicked); + if (!province) + throw new Error(('Unknown clicked province ' + clicked)); + const clickedID = province.getID(classes); + if (!clicked) + throw new Error(`Unknown path (${clickedID}) for province (${clicked}).`); + classes[clickedID] = 'provinceRed'; + const neighbors = this.getNeighbors(); + if (neighbors) { + for (let neighbor of neighbors) { + const neighborProvince = this.props.mapData.getProvince(neighbor); + if (!neighborProvince) + throw new Error('Unknown neighbor province ' + neighbor); + const neighborID = neighborProvince.getID(classes); + if (!neighborID) + throw new Error(`Unknown neoghbor path (${neighborID}) for province (${neighbor}).`); + classes[neighborID] = neighborProvince.isWater() ? 'provinceBlue' : 'provinceGreen'; + } + } + } + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {renderedSupplyCenters} + + + + {renderedOrders2} + + + {renderedOrders} + + + + {renderedUnits} + + + {renderedDislodgedUnits} + + + {renderedHighestOrders} + + + SWI + ADR + AEG + ALB + ANK + APU + ARM + BAL + BAR + BEL + BER + BLA + BOH + BRE + BUD + BUL + BUR + CLY + CON + DEN + EAS + EDI + ENG + FIN + GAL + GAS + GRE + BOT + LYO + HEL + HOL + ION + IRI + KIE + LON + LVN + LVP + MAR + MAO + MOS + MUN + NAF + NAP + NAO + NTH + NWY + NWG + PAR + PIC + PIE + POR + PRU + ROM + RUH + RUM + SER + SEV + SIL + SKA + SMY + SPA + STP + SWE + SYR + TRI + TUN + TUS + TYR + TYS + UKR + VEN + VIE + WAL + WAR + WES + YOR + + + + {nb_centers_per_power ? nb_centers_per_power : ''} + + + {note ? note : ''} + + + {current_phase} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } +} +SvgStandard.propTypes = { + game: PropTypes.instanceOf(Game).isRequired, + mapData: PropTypes.instanceOf(MapData).isRequired, + orders: PropTypes.object, + onHover: PropTypes.func, + onError: PropTypes.func.isRequired, + onSelectLocation: PropTypes.func, + onSelectVia: PropTypes.func, + onOrderBuilding: PropTypes.func, + onOrderBuilt: PropTypes.func, + orderBuilding: PropTypes.object +}; -- cgit v1.2.3