diff options
author | notoraptor <notoraptor@users.noreply.github.com> | 2019-07-17 15:16:43 -0400 |
---|---|---|
committer | Philip Paquette <pcpaquette@gmail.com> | 2019-07-17 15:16:43 -0400 |
commit | 2701df1e3b03c7c605ccf212a02987d53fbd0609 (patch) | |
tree | d3637573d8585e32914c33cbd03ec0baf9c68ae3 /diplomacy/web/src/gui/map/dom_order_builder.js | |
parent | e9872eea32d4f66b9c7ca8c14d530c18f6c18506 (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.js | 278 |
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); + } + +} |