aboutsummaryrefslogtreecommitdiff
path: root/diplomacy/web/src/gui/map/dom_order_builder.js
diff options
context:
space:
mode:
authornotoraptor <notoraptor@users.noreply.github.com>2019-07-17 15:16:43 -0400
committerPhilip Paquette <pcpaquette@gmail.com>2019-07-17 15:16:43 -0400
commit2701df1e3b03c7c605ccf212a02987d53fbd0609 (patch)
treed3637573d8585e32914c33cbd03ec0baf9c68ae3 /diplomacy/web/src/gui/map/dom_order_builder.js
parente9872eea32d4f66b9c7ca8c14d530c18f6c18506 (diff)
[web] Make button "Delete all" remove only orders from current se… (#49)
- 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.
Diffstat (limited to 'diplomacy/web/src/gui/map/dom_order_builder.js')
-rw-r--r--diplomacy/web/src/gui/map/dom_order_builder.js278
1 files changed, 278 insertions, 0 deletions
diff --git a/diplomacy/web/src/gui/map/dom_order_builder.js b/diplomacy/web/src/gui/map/dom_order_builder.js
new file mode 100644
index 0000000..14ba743
--- /dev/null
+++ b/diplomacy/web/src/gui/map/dom_order_builder.js
@@ -0,0 +1,278 @@
+// ==============================================================================
+// Copyright (C) 2019 - Philip Paquette, Steven Bocco
+//
+// This program is free software: you can redistribute it and/or modify it under
+// the terms of the GNU Affero General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or (at your option) any
+// later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+// details.
+//
+// You should have received a copy of the GNU Affero General Public License along
+// with this program. If not, see <https://www.gnu.org/licenses/>.
+// ==============================================================================
+import {UTILS} from "../../diplomacy/utils/utils";
+import $ from "jquery";
+import {extendOrderBuilding} from "../utils/order_building";
+import {Diplog} from "../../diplomacy/utils/diplog";
+
+function parseLocation(txt) {
+ if (txt.length > 2 && txt[1] === ' ' && ['A', 'F'].includes(txt[0]))
+ return txt.substr(2);
+ return txt;
+}
+
+export class DOMOrderBuilder {
+
+ constructor(svgElement, onOrderBuilding, onOrderBuilt, onSelectLocation, onSelectVia, onError) {
+ this.svg = svgElement;
+ this.cbOrderBuilding = onOrderBuilding;
+ this.cbOrderBuilt = onOrderBuilt;
+ this.cbSelectLocation = onSelectLocation;
+ this.cbSelectVia = onSelectVia;
+ this.cbError = onError;
+
+ this.game = null;
+ this.mapData = null;
+ this.orderBuilding = null;
+
+ this.provinceColors = {};
+ this.clickedID = null;
+ this.clickedNeighbors = [];
+
+ this.onProvinceClick = this.onProvinceClick.bind(this);
+ this.onLabelClick = this.onLabelClick.bind(this);
+ this.onUnitClick = this.onUnitClick.bind(this);
+ }
+
+ saveProvinceColors() {
+ // Get province colors.
+ const elements = this.svg.getElementsByTagName('path');
+ for (let element of elements) {
+ this.provinceColors[element.id] = element.getAttribute('class');
+ }
+ }
+
+ provinceNameToMapID(name) {
+ return `_${name.toLowerCase()}___${this.svg.parentNode.id}`;
+ }
+
+ mapID(id) {
+ return `${id}___${this.svg.parentNode.id}`;
+ }
+
+ onOrderBuilding(svgPath, powerName, orderPath) {
+ this.cbOrderBuilding(powerName, orderPath);
+ }
+
+ onOrderBuilt(svgPath, powerName, orderString) {
+ this.cbOrderBuilt(powerName, orderString);
+ }
+
+ onError(svgPath, error) {
+ this.cbError(error.toString());
+ }
+
+ handleSvgPath(svgPath) {
+ const orderBuilding = this.orderBuilding;
+ if (!orderBuilding.builder)
+ return this.onError(svgPath, 'No orderable locations.');
+
+ const province = this.mapData.getProvince(svgPath.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.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.onError(svgPath, error);
+ }
+ }
+ if (!validLocations.length)
+ return this.onError(svgPath, '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.cbSelectLocation) {
+ return this.cbSelectLocation(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.game.ordersTree, moveOrderPath);
+ if (moveTypes !== null) {
+ if (moveTypes.length === 2) {
+ // This move can be done either regularly or VIA a fleet. Let user choose.
+ return this.cbSelectVia(validLocations[0], orderBuilding.power, orderBuilding.path);
+ } else {
+ orderBuildingType = moveTypes[0];
+ }
+ }
+ }
+ this.clickedID = svgPath.id;
+
+ this.cleanBuildingView();
+ if (lengthAfterClick < stepLength)
+ this.renderBuildingView(validLocations[0]);
+ extendOrderBuilding(
+ orderBuilding.power, orderBuildingType, orderBuilding.path, validLocations[0],
+ this.cbOrderBuilding, this.cbOrderBuilt, this.cbError
+ );
+
+ }
+
+ getPathFromProvince(province) {
+ let path = this.svg.getElementById(this.provinceNameToMapID(province.name));
+ if (!path) {
+ for (let alias of province.aliases) {
+ path = this.svg.getElementById(this.provinceNameToMapID(alias));
+ if (path)
+ break;
+ }
+ }
+ return path;
+ }
+
+ onProvinceClick(event) {
+ this.handleSvgPath(event.target);
+ }
+
+ onLabelClick(event) {
+ const province = this.mapData.getProvince(event.target.textContent);
+ if (province) {
+ const path = this.getPathFromProvince(province);
+ if (path)
+ this.handleSvgPath(path);
+ }
+ }
+
+ onUnitClick(event) {
+ const province = this.mapData.getProvince(event.target.getAttribute('diplomacyUnit'));
+ if (province) {
+ let path = this.getPathFromProvince(province);
+ if (!path && province.isCoast())
+ path = this.svg.getElementById(this.provinceNameToMapID(province.parent.name));
+ if (path) {
+ this.handleSvgPath(path);
+ }
+ }
+ }
+
+ cleanBuildingView() {
+ if (this.clickedID) {
+ const path = this.svg.getElementById(this.clickedID);
+ if (path)
+ path.setAttribute('class', this.provinceColors[this.clickedID]);
+ }
+ for (let neighborName of this.clickedNeighbors) {
+ const province = this.mapData.getProvince(neighborName);
+ if (!province)
+ continue;
+ const path = this.getPathFromProvince(province);
+ if (path)
+ path.setAttribute('class', this.provinceColors[path.id]);
+ }
+ this.clickedNeighbors = [];
+ }
+
+ renderBuildingView(extraLocation) {
+ if (this.clickedID) {
+ const path = this.svg.getElementById(this.clickedID);
+ if (path)
+ path.setAttribute('class', 'provinceRed');
+ }
+ const selectedPath = [this.orderBuilding.type].concat(this.orderBuilding.path);
+ if (extraLocation)
+ selectedPath.push(extraLocation);
+ const possibleNeighbors = UTILS.javascript.getTreeValue(this.game.ordersTree, selectedPath);
+ if (!possibleNeighbors)
+ return;
+ this.clickedNeighbors = possibleNeighbors.map(neighbor => parseLocation(neighbor));
+ if (this.clickedNeighbors.length) {
+ for (let neighbor of this.clickedNeighbors) {
+ let neighborProvince = this.mapData.getProvince(neighbor);
+ if (!neighborProvince)
+ throw new Error('Unknown neighbor province ' + neighbor);
+ let path = this.getPathFromProvince(neighborProvince);
+ if (!path && neighborProvince.isCoast())
+ path = this.getPathFromProvince(neighborProvince.parent);
+ if (!path)
+ throw new Error(`Unable to find SVG path related to province ${neighborProvince.name}.`);
+ path.setAttribute('class', neighborProvince.isWater() ? 'provinceBlue' : 'provinceGreen');
+ }
+ }
+ }
+
+ update(game, mapData, orderBuilding) {
+ this.game = game;
+ this.mapData = mapData;
+ this.orderBuilding = orderBuilding;
+ this.saveProvinceColors();
+ // If there is a building path, then we are building, so we don't clean anything.
+ this.cleanBuildingView();
+ if (this.orderBuilding.path.length)
+ this.renderBuildingView();
+ // I don't yet know why I should place this here. Maybe because unit are re-rendered manually at every reloading ?
+ $(`#${this.svg.parentNode.id} svg use[diplomacyUnit]`).click(this.onUnitClick);
+ }
+
+ init(game, mapData, orderBuilding) {
+ $(`#${this.svg.parentNode.id} svg path`).click(this.onProvinceClick);
+ $(`#${this.mapID('BriefLabelLayer')} text`).click(this.onLabelClick);
+ this.update(game, mapData, orderBuilding);
+ }
+
+}