This commit is contained in:
AlxAI 2025-02-06 14:33:10 -08:00
parent e8530a146d
commit 93c073e2df
295 changed files with 86794 additions and 0 deletions

View file

@ -0,0 +1,52 @@
// ==============================================================================
// 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 React from "react";
import PropTypes from 'prop-types';
export class Action extends React.Component {
// title
// isActive
// onClick
// See Button parameters.
render() {
return (
<div className="action nav-item" onClick={this.props.onClick}>
<div
className={'nav-link' + (this.props.isActive ? ' active' : '') + (this.props.highlight !== null ? ' updated' : '')}>
{this.props.title}
{this.props.highlight !== null
&& this.props.highlight !== undefined
&& <span className={'update'}>{this.props.highlight}</span>}
</div>
</div>
);
}
}
Action.propTypes = {
title: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
highlight: PropTypes.any,
isActive: PropTypes.bool
};
Action.defaultProps = {
highlight: null,
isActive: false
};

View file

@ -0,0 +1,68 @@
// ==============================================================================
// 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 React from "react";
import PropTypes from "prop-types";
export class Button extends React.Component {
/** Bootstrap button.
* Bootstrap classes:
* - btn
* - btn-primary
* - mx-1 (margin-left 1px, margin-right 1px)
* Props: title (str), onClick (function).
* **/
// title
// onClick
// pickEvent = false
// large = false
// small = false
constructor(props) {
super(props);
this.onClick = this.onClick.bind(this);
}
onClick(event) {
if (this.props.onClick)
this.props.onClick(this.props.pickEvent ? event : null);
}
render() {
return (
<button
className={`btn btn-${this.props.color || 'secondary'}` + (this.props.large ? ' btn-block' : '') + (this.props.small ? ' btn-sm' : '')}
disabled={this.props.disabled}
onClick={this.onClick}>
<strong>{this.props.title}</strong>
</button>
);
}
}
Button.propTypes = {
title: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
color: PropTypes.string,
large: PropTypes.bool,
small: PropTypes.bool,
pickEvent: PropTypes.bool,
disabled: PropTypes.bool
};
Button.defaultPropTypes = {
disabled: false
};

View file

@ -0,0 +1,59 @@
// ==============================================================================
// 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 React from "react";
import {Button} from "./button";
import PropTypes from "prop-types";
export class DeleteButton extends React.Component {
constructor(props) {
super(props);
this.state = {step: 0};
this.onClick = this.onClick.bind(this);
}
onClick() {
this.setState({step: this.state.step + 1}, () => {
if (this.state.step === 2)
this.props.onClick();
});
}
render() {
let title = '';
let color = '';
if (this.state.step === 0) {
title = this.props.title;
color = 'secondary';
} else if (this.state.step === 1) {
title = this.props.confirmTitle;
color = 'danger';
} else if (this.state.step === 2) {
title = this.props.waitingTitle;
color = 'danger';
}
return (
<Button title={title} color={color} onClick={this.onClick} small={true} large={true}/>
);
}
}
DeleteButton.propTypes = {
title: PropTypes.string.isRequired,
confirmTitle: PropTypes.string.isRequired,
waitingTitle: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired
};

View file

@ -0,0 +1,47 @@
// ==============================================================================
// 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 React from 'react';
import PropTypes from 'prop-types';
import {Button} from "./button";
const TIMES = '\u00D7';
export class FancyBox extends React.Component {
render() {
return (
<div className="fancy-box">
<div className="fancy-bar p-1 d-flex flex-row">
<div
className="flex-grow-1 fancy-title d-flex flex-column justify-content-center pr-0 pr-sm-1">{this.props.title}</div>
<div className="fancy-button">
<Button title={TIMES} color={'danger'} onClick={this.props.onClose}/>
</div>
</div>
<div className="fancy-content p-2">
{this.props.children}
</div>
</div>
);
}
}
FancyBox.propTypes = {
title: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
children: PropTypes.any.isRequired
};

View file

@ -0,0 +1,116 @@
// ==============================================================================
// 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 React from "react";
import {UTILS} from "../../diplomacy/utils/utils";
import {Button} from "./button";
export class Forms {
static createOnChangeCallback(component, callback) {
return (event) => {
const value = UTILS.html.isCheckBox(event.target) ? event.target.checked : event.target.value;
const fieldName = UTILS.html.isRadioButton(event.target) ? event.target.name : event.target.id;
const update = {[fieldName]: value};
const state = Object.assign({}, component.state, update);
if (callback)
callback(state);
component.setState(state);
};
}
static createOnSubmitCallback(component, callback, resetState) {
return (event) => {
if (callback)
callback(Object.assign({}, component.state));
if (resetState)
component.setState(resetState);
event.preventDefault();
};
}
static createOnResetCallback(component, onChangeCallback, resetState) {
return (event) => {
if (onChangeCallback)
onChangeCallback(resetState);
component.setState(resetState);
if (event && event.preventDefault)
event.preventDefault();
};
}
static getValue(fieldValues, fieldName, defaultValue) {
return fieldValues.hasOwnProperty(fieldName) ? fieldValues[fieldName] : defaultValue;
}
static createReset(title, large, onReset) {
return <Button key={'reset'} title={title || 'reset'} onClick={onReset} pickEvent={true} large={large}/>;
}
static createSubmit(title, large, onSubmit) {
return <Button key={'submit'} title={title || 'submit'} onClick={onSubmit} pickEvent={true} large={large}/>;
}
static createButton(title, fn, color, large) {
const wrapFn = (event) => {
fn();
event.preventDefault();
};
return <Button large={large} key={title} color={color} title={title} onClick={wrapFn} pickEvent={true}/>;
}
static createCheckbox(id, title, value, onChange) {
const input = <input className={'form-check-input'} key={id} type={'checkbox'} id={id} checked={value}
onChange={onChange}/>;
const label = <label className={'form-check-label'} key={`label-${id}`} htmlFor={id}>{title}</label>;
return [input, label];
}
static createRadio(name, value, title, currentValue, onChange) {
const id = `[${name}][${value}]`;
const input = <input className={'form-check-input'} key={id} type={'radio'}
name={name} value={value} checked={currentValue === value}
id={id} onChange={onChange}/>;
const label = <label className={'form-check-label'} key={`label-${id}`} htmlFor={id}>{title || value}</label>;
return [input, label];
}
static createRow(label, input) {
return (
<div className={'form-group row'}>
{label}
<div className={'col'}>{input}</div>
</div>
);
}
static createLabel(htmFor, title, className) {
return <label className={className} htmlFor={htmFor}>{title}</label>;
}
static createColLabel(htmlFor, title) {
return Forms.createLabel(htmlFor, title, 'col');
}
static createSelectOptions(values, none) {
const options = values.slice();
const components = options.map((option, index) => <option key={index} value={option}>{option}</option>);
if (none) {
components.splice(0, 0, [<option key={-1} value={''}>{none === true ? '(none)' : `${none}`}</option>]);
}
return components;
}
}

View file

@ -0,0 +1,39 @@
// ==============================================================================
// 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 React from "react";
import PropTypes from 'prop-types';
import {FancyBox} from "./fancyBox";
export class Help extends React.Component {
render() {
return (
<FancyBox title={'Help'} onClose={this.props.onClose}>
<div>
<p>When building an order, press <strong>ESC</strong> to reset build.</p>
<p>Press letter associated to an order type to start building an order of this type.
<br/> Order type letter is indicated in order type name after order type radio button.
</p>
<p>In Phase History tab, use keyboard left and right arrows to navigate in past phases.</p>
</div>
</FancyBox>
);
}
}
Help.propTypes = {
onClose: PropTypes.func.isRequired
};

View file

@ -0,0 +1,55 @@
// ==============================================================================
// 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 React from 'react';
import PropTypes from 'prop-types';
class Div extends React.Component {
getClassName() {
return '';
}
render() {
return (
<div className={this.getClassName() + (this.props.className ? ' ' + this.props.className : '')}>
{this.props.children}
</div>
);
}
}
Div.propTypes = {
className: PropTypes.string,
children: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
};
export class Bar extends Div {
getClassName() {
return 'bar';
}
}
export class Row extends Div {
getClassName() {
return 'row';
}
}
export class Col extends Div {
getClassName() {
return 'col';
}
}

View file

@ -0,0 +1,67 @@
// ==============================================================================
// 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 React from "react";
import PropTypes from 'prop-types';
export class MessageView extends React.Component {
// message
render() {
const message = this.props.message;
const owner = this.props.owner;
const id = this.props.id ? {id: this.props.id} : {};
const messagesLines = message.message.replace('\r\n', '\n')
.replace('\r', '\n')
.replace('<br>', '\n')
.replace('<br/>', '\n')
.split('\n');
let onClick = null;
const classNames = ['game-message', 'row'];
if (owner === message.sender)
classNames.push('message-sender');
else {
classNames.push('message-recipient');
if (message.read || this.props.read)
classNames.push('message-read');
onClick = this.props.onClick ? {onClick: () => this.props.onClick(message)} : {};
}
return (
<div className={'game-message-wrapper' + (
this.props.phase && this.props.phase !== message.phase ? ' other-phase' : ' new-phase')}
{...id}>
<div className={classNames.join(' ')} {...onClick}>
<div className="message-header col-md-auto text-md-right text-center">
{message.phase}
</div>
<div className="message-content col-md">
{messagesLines.map((line, lineIndex) => <div key={lineIndex}>{
line.replace(/(<([^>]+)>)/ig, "")
}</div>)}
</div>
</div>
</div>
);
}
}
MessageView.propTypes = {
message: PropTypes.object,
phase: PropTypes.string,
owner: PropTypes.string,
onClick: PropTypes.func,
id: PropTypes.string,
read: PropTypes.bool
};

View file

@ -0,0 +1,77 @@
// ==============================================================================
// 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 React from "react";
import Octicon, {Person} from "@primer/octicons-react";
import PropTypes from "prop-types";
export class Navigation extends React.Component {
render() {
const hasNavigation = this.props.navigation && this.props.navigation.length;
if (hasNavigation) {
return (
<div className={'title row'}>
<div className={'col align-self-center'}>
<strong>{this.props.title}</strong>
{this.props.afterTitle ? this.props.afterTitle : ''}
</div>
<div className={'col-sm-1'}>
{(!hasNavigation && (
<div className={'float-right'}>
<strong>
<u className={'mr-2'}>{this.props.username}</u>
<Octicon icon={Person}/>
</strong>
</div>
)) || (
<div className="dropdown float-right">
<button className="btn btn-secondary dropdown-toggle" type="button"
id="dropdownMenuButton" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
{(this.props.username && (
<span>
<u className={'mr-2'}>{this.props.username}</u>
<Octicon icon={Person}/>
</span>
)) || 'Menu'}
</button>
<div className="dropdown-menu dropdown-menu-right"
aria-labelledby="dropdownMenuButton">
{this.props.navigation.map((nav, index) => {
const navTitle = nav[0];
const navAction = nav[1];
return <span key={index} className="dropdown-item"
onClick={navAction}>{navTitle}</span>;
})}
</div>
</div>
)}
</div>
</div>
);
}
return (
<div className={'title'}><strong>{this.props.title}</strong></div>
);
}
}
Navigation.propTypes = {
title: PropTypes.string.isRequired,
afterTitle: PropTypes.object,
navigation: PropTypes.array,
username: PropTypes.string,
};

View file

@ -0,0 +1,19 @@
// ==============================================================================
// 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 React from "react";
export const PageContext = React.createContext(null);

View file

@ -0,0 +1,77 @@
// ==============================================================================
// 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 React from "react";
import PropTypes from 'prop-types';
import {Button} from "./button";
export class PowerOrders extends React.Component {
render() {
const orderEntries = this.props.orders ? Object.entries(this.props.orders) : null;
let display = null;
if (orderEntries) {
if (orderEntries.length) {
orderEntries.sort((a, b) => a[1].order.localeCompare(b[1].order));
display = (
<div className={'container order-list'}>
{orderEntries.map((entry, index) => (
<div
className={`row order-entry entry-${1 + index % 2} ` + (entry[1].local ? 'local' : 'server')}
key={index}>
<div className={'col align-self-center order'}>
<span className={'order-string'}>{entry[1].order}</span>
{entry[1].local ? '' : <span className={'order-mark'}> [S]</span>}
</div>
<div className={'col remove-button'}>
<Button title={'-'} onClick={() => this.props.onRemove(this.props.name, entry[1])}/>
</div>
</div>
))}
</div>
);
} else if (this.props.serverCount === 0) {
display = (<div className={'empty-orders'}>Empty orders set</div>);
} else {
display = (<div className={'empty-orders'}>Local empty orders set</div>);
}
} else {
if (this.props.serverCount < 0) {
display = <div className={'no-orders'}>No orders!</div>;
} else {
display = <div className={'empty-orders'}>Asking to unset orders</div>;
}
}
return (
<div className={'power-orders'}>
<div className={'title'}>
<span className={'name'}>{this.props.name}</span>
<span className={this.props.wait ? 'wait' : 'no-wait'}>
{(this.props.wait ? ' ' : ' not') + ' waiting'}
</span>
</div>
{display}
</div>
);
}
}
PowerOrders.propTypes = {
wait: PropTypes.bool,
name: PropTypes.string,
orders: PropTypes.object,
serverCount: PropTypes.number,
onRemove: PropTypes.func,
};

View file

@ -0,0 +1,42 @@
// ==============================================================================
// 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 React from 'react';
import {Button} from "./button";
import {Bar} from "./layouts";
import PropTypes from 'prop-types';
export class PowerOrdersActionBar extends React.Component {
render() {
return (
<Bar className={'p-2'}>
<strong className={'mr-4'}>Orders:</strong>
<Button title={'reset'} onClick={this.props.onReset}/>
<Button title={'delete all'} onClick={this.props.onDeleteAll}/>
<Button color={'primary'} title={'update'} onClick={this.props.onUpdate}/>
{(this.props.onProcess &&
<Button color={'danger'} title={'process game'} onClick={this.props.onProcess}/>) || ''}
</Bar>
);
}
}
PowerOrdersActionBar.propTypes = {
onReset: PropTypes.func.isRequired,
onDeleteAll: PropTypes.func.isRequired,
onUpdate: PropTypes.func.isRequired,
onProcess: PropTypes.func
};

View file

@ -0,0 +1,45 @@
// ==============================================================================
// 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 React from "react";
import PropTypes from "prop-types";
export class Tab extends React.Component {
render() {
const style = {
display: this.props.display ? 'block' : 'none'
};
const id = this.props.id ? {id: this.props.id} : {};
return (
<div className={'tab mb-4 ' + this.props.className} style={style} {...id}>
{this.props.children}
</div>
);
}
}
Tab.propTypes = {
display: PropTypes.bool,
className: PropTypes.string,
id: PropTypes.string,
children: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
};
Tab.defaultProps = {
display: false,
className: '',
id: ''
};

View file

@ -0,0 +1,112 @@
// ==============================================================================
// 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/>.
// ==============================================================================
//// Tables.
import React from "react";
import PropTypes from 'prop-types';
class DefaultWrapper {
constructor(data) {
this.data = data;
this.get = this.get.bind(this);
}
get(fieldName) {
return this.data[fieldName];
}
}
function defaultWrapper(data) {
return new DefaultWrapper(data);
}
export class Table extends React.Component {
// className
// caption
// columns : {name: [title, order]}
// data: [objects with expected column names]
// wrapper: (optional) function to use to wrap one data entry into an object before accessing fields.
// Must return an instance with a method get(name).
// If provided: wrapper(data_entry).get(field_name)
// else: data_entry[field_name]
constructor(props) {
super(props);
if (!this.props.wrapper)
this.props.wrapper = defaultWrapper;
}
static getHeader(columns) {
const header = [];
for (let entry of Object.entries(columns)) {
const name = entry[0];
const title = entry[1][0];
const order = entry[1][1];
header.push([order, name, title]);
}
header.sort((a, b) => {
let t = a[0] - b[0];
if (t === 0)
t = a[1].localeCompare(b[1]);
if (t === 0)
t = a[2].localeCompare(b[2]);
return t;
});
return header;
}
static getHeaderLine(header) {
return (
<thead className={'thead-light'}>
<tr>{header.map((column, colIndex) => <th key={colIndex}>{column[2]}</th>)}</tr>
</thead>
);
}
static getBodyRow(header, row, rowIndex, wrapper) {
const wrapped = wrapper(row);
return (<tr key={rowIndex}>
{header.map((headerColumn, colIndex) => <td className={'align-middle'}
key={colIndex}>{wrapped.get(headerColumn[1])}</td>)}
</tr>);
}
static getBodyLines(header, data, wrapper) {
return (<tbody>{data.map((row, rowIndex) => Table.getBodyRow(header, row, rowIndex, wrapper))}</tbody>);
}
render() {
const header = Table.getHeader(this.props.columns);
return (
<div className={'table-responsive'}>
<table className={this.props.className}>
<caption>{this.props.caption} ({this.props.data.length})</caption>
{Table.getHeaderLine(header)}
{Table.getBodyLines(header, this.props.data, this.props.wrapper)}
</table>
</div>
);
}
}
Table.propTypes = {
wrapper: PropTypes.func,
columns: PropTypes.object,
className: PropTypes.string,
caption: PropTypes.string,
data: PropTypes.array
};

View file

@ -0,0 +1,69 @@
// ==============================================================================
// 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 React from "react";
import {Action} from "./action";
import PropTypes from 'prop-types';
export class Tabs extends React.Component {
/** PROPERTIES
* active: index of active menu (must be > menu.length).
* highlights: dictionary mapping a menu indice to a highlight message
* onChange: callback(index): receive index of menu to display.
* **/
generateTabAction(tabTitle, tabId, isActive, onChange, highlight) {
return <Action isActive={isActive}
title={tabTitle}
onClick={() => onChange(tabId)}
highlight={highlight}
key={tabId}/>;
}
render() {
if (!this.props.menu.length)
throw new Error(`No tab menu given.`);
if (this.props.menu.length !== this.props.titles.length)
throw new Error(`Menu length (${this.props.menu.length}) != titles length (${this.props.titles.length})`);
if (this.props.active && !this.props.menu.includes(this.props.active))
throw new Error(`Invalid active tab name, got ${this.props.active}, expected one of: ${this.props.menu.join(', ')}`);
const active = this.props.active || this.props.menu[0];
return (
<div className={'tabs mb-3'}>
<nav className={'tabs-bar nav nav-tabs justify-content-center mb-3'}>
{this.props.menu.map((tabName, index) => this.generateTabAction(
this.props.titles[index], tabName, active === tabName, this.props.onChange,
(this.props.highlights.hasOwnProperty(tabName) && this.props.highlights[tabName]) || null
))}
</nav>
{this.props.children}
</div>
);
}
}
Tabs.propTypes = {
menu: PropTypes.arrayOf(PropTypes.string).isRequired, // tab names
titles: PropTypes.arrayOf(PropTypes.string).isRequired, // tab titles
onChange: PropTypes.func.isRequired, // callback(tab name)
children: PropTypes.array.isRequired,
active: PropTypes.string, // current active tab name
highlights: PropTypes.object, // {tab name => highligh message (optional)}
};
Tabs.defaultProps = {
highlights: {}
};

View file

@ -0,0 +1,123 @@
// ==============================================================================
// 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 React from 'react';
import {Forms} from "../components/forms";
import {UTILS} from "../../diplomacy/utils/utils";
import PropTypes from "prop-types";
import {DipStorage} from "../utils/dipStorage";
export class ConnectionForm extends React.Component {
constructor(props) {
super(props);
// Load fields values from local storage.
const initialState = this.initState();
const savedState = DipStorage.getConnectionForm();
if (savedState) {
if (savedState.hostname)
initialState.hostname = savedState.hostname;
if (savedState.port)
initialState.port = savedState.port;
if (savedState.username)
initialState.username = savedState.username;
if (savedState.showServerFields)
initialState.showServerFields = savedState.showServerFields;
}
this.state = initialState;
this.updateServerFieldsView = this.updateServerFieldsView.bind(this);
this.onChange = this.onChange.bind(this);
}
initState() {
return {
hostname: window.location.hostname,
port: (window.location.protocol.toLowerCase() === 'https:') ? 8433 : 8432,
username: '',
password: '',
showServerFields: false
};
}
updateServerFieldsView() {
DipStorage.setConnectionshowServerFields(!this.state.showServerFields);
this.setState({showServerFields: !this.state.showServerFields});
}
onChange(newState) {
const initialState = this.initState();
if (newState.hostname !== initialState.hostname)
DipStorage.setConnectionHostname(newState.hostname);
else
DipStorage.setConnectionHostname(null);
if (newState.port !== initialState.port)
DipStorage.setConnectionPort(newState.port);
else
DipStorage.setConnectionPort(null);
if (newState.username !== initialState.username)
DipStorage.setConnectionUsername(newState.username);
else
DipStorage.setConnectionUsername(null);
if (this.props.onChange)
this.props.onChange(newState);
}
render() {
const onChange = Forms.createOnChangeCallback(this, this.onChange);
const onSubmit = Forms.createOnSubmitCallback(this, this.props.onSubmit);
return (
<form>
{Forms.createRow(
Forms.createColLabel('username', 'username:'),
<input className={'form-control'} type={'text'} id={'username'}
value={Forms.getValue(this.state, 'username')} onChange={onChange}/>
)}
{Forms.createRow(
Forms.createColLabel('password', 'password:'),
<input className={'form-control'} type={'password'} id={'password'}
value={Forms.getValue(this.state, 'password')} onChange={onChange}/>
)}
<div>
<div className={this.state.showServerFields ? 'mb-2' : 'mb-4'}>
<span className={'button-server'} onClick={this.updateServerFieldsView}>
server settings {this.state.showServerFields ? UTILS.html.UNICODE_BOTTOM_ARROW : UTILS.html.UNICODE_TOP_ARROW}
</span>
</div>
{this.state.showServerFields && (
<div className={'mb-4'}>
{Forms.createRow(
<label className={'col'} htmlFor={'hostname'}>hostname:</label>,
<input className={'form-control'} type={'text'} id={'hostname'}
value={Forms.getValue(this.state, 'hostname')} onChange={onChange}/>
)}
{Forms.createRow(
<label className={'col'} htmlFor={'port'}>port:</label>,
<input className={'form-control'} type={'number'} id={'port'}
value={Forms.getValue(this.state, 'port')}
onChange={onChange}/>
)}
</div>
)}
</div>
{Forms.createRow('', Forms.createSubmit('connect', true, onSubmit))}
</form>
);
}
}
ConnectionForm.propTypes = {
onChange: PropTypes.func,
onSubmit: PropTypes.func
};

View file

@ -0,0 +1,70 @@
// ==============================================================================
// 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 React from 'react';
import {Forms} from "../components/forms";
import {STRINGS} from "../../diplomacy/utils/strings";
import PropTypes from "prop-types";
export class FindForm extends React.Component {
constructor(props) {
super(props);
this.state = this.initState();
}
initState() {
return {
game_id: '',
status: '',
include_protected: false,
for_omniscience: false
};
}
render() {
const onChange = Forms.createOnChangeCallback(this, this.props.onChange);
const onSubmit = Forms.createOnSubmitCallback(this, this.props.onSubmit);
return (
<form>
{Forms.createRow(
Forms.createColLabel('game_id', 'game id (should contain):'),
<input className={'form-control'} id={'game_id'} type={'text'}
value={Forms.getValue(this.state, 'game_id')}
onChange={onChange}/>
)}
{Forms.createRow(
Forms.createColLabel('status', 'status:'),
(<select className={'form-control custom-select'}
id={'status'} value={Forms.getValue(this.state, 'status')} onChange={onChange}>
{Forms.createSelectOptions(STRINGS.ALL_GAME_STATUSES, true)}
</select>)
)}
<div className={'form-check'}>
{Forms.createCheckbox('include_protected', 'include protected games.', Forms.getValue(this.state, 'include_protected'), onChange)}
</div>
<div className={'form-check mb-4'}>
{Forms.createCheckbox('for_omniscience', 'for omniscience.', Forms.getValue(this.state, 'for_omniscience'), onChange)}
</div>
{Forms.createRow('', Forms.createSubmit('find games', true, onSubmit))}
</form>
);
}
}
FindForm.propTypes = {
onChange: PropTypes.func,
onSubmit: PropTypes.func
};

View file

@ -0,0 +1,80 @@
// ==============================================================================
// 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 React from 'react';
import {Forms} from "../components/forms";
import PropTypes from "prop-types";
export class JoinForm extends React.Component {
constructor(props) {
super(props);
this.state = this.initState();
}
initState() {
return {
[this.getPowerNameID()]: this.getDefaultPowerName(),
[this.getPasswordID()]: ''
};
}
getPowerNameID() {
return `power_name_${this.props.game_id}`;
}
getPasswordID() {
return `registration_password_${this.props.game_id}`;
}
getDefaultPowerName() {
return (this.props.powers && this.props.powers.length && this.props.powers[0]) || '';
}
render() {
const onChange = Forms.createOnChangeCallback(this, this.props.onChange);
const onSubmit = Forms.createOnSubmitCallback(this, this.props.onSubmit);
return (
<form className={'form-inline'}>
<div className={'form-group mr-2'}>
{Forms.createLabel(this.getPowerNameID(), 'Power:')}
<select id={this.getPowerNameID()} className={'from-control custom-select ml-2'}
value={Forms.getValue(this.state, this.getPowerNameID())} onChange={onChange}>
{Forms.createSelectOptions(this.props.availablePowers, true)}
</select>
</div>
{this.props.password_required ? (
<div className={'form-group mr-2'}>
{Forms.createLabel(this.getPasswordID(), '', 'sr-only')}
<input id={this.getPasswordID()} type={'password'} className={'form-control'}
placeholder={'registration password'}
value={Forms.getValue(this.state, this.getPasswordID())}
onChange={onChange}/>
</div>
) : ''}
{Forms.createSubmit('join', false, onSubmit)}
</form>
);
}
}
JoinForm.propTypes = {
game_id: PropTypes.string.isRequired,
password_required: PropTypes.bool.isRequired,
powers: PropTypes.arrayOf(PropTypes.string),
availablePowers: PropTypes.arrayOf(PropTypes.string),
onChange: PropTypes.func,
onSubmit: PropTypes.func
};

View file

@ -0,0 +1,53 @@
// ==============================================================================
// 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 React from 'react';
import {Forms} from "../components/forms";
import {UTILS} from "../../diplomacy/utils/utils";
import PropTypes from "prop-types";
export class MessageForm extends React.Component {
constructor(props) {
super(props);
this.state = this.initState();
}
initState() {
return {message: ''};
}
render() {
const onChange = Forms.createOnChangeCallback(this, this.props.onChange);
const onSubmit = Forms.createOnSubmitCallback(this, this.props.onSubmit, this.initState());
return (
<form>
<div className={'form-group'}>
{Forms.createLabel('message', '', 'sr-only')}
<textarea id={'message'} className={'form-control'}
value={Forms.getValue(this.state, 'message')} onChange={onChange}/>
</div>
{Forms.createSubmit(`send (${this.props.sender} ${UTILS.html.UNICODE_SMALL_RIGHT_ARROW} ${this.props.recipient})`, true, onSubmit)}
</form>
);
}
}
MessageForm.propTypes = {
sender: PropTypes.string,
recipient: PropTypes.string,
onChange: PropTypes.func,
onSubmit: PropTypes.func
};

View file

@ -0,0 +1,121 @@
// ==============================================================================
// 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 React from 'react';
import {Forms} from "../components/forms";
import {ORDER_BUILDER} from "../utils/order_building";
import {STRINGS} from "../../diplomacy/utils/strings";
import PropTypes from "prop-types";
import {Power} from "../../diplomacy/engine/power";
const HotKey = require('react-shortcut');
export class PowerOrderCreationForm extends React.Component {
constructor(props) {
super(props);
this.state = this.initState();
}
initState() {
return {order_type: this.props.orderType};
}
render() {
const onChange = Forms.createOnChangeCallback(this, this.props.onChange);
const onReset = Forms.createOnResetCallback(this, this.props.onChange, this.initState());
const onSetOrderType = (letter) => {
this.setState({order_type: letter}, () => {
if (this.props.onChange)
this.props.onChange(this.state);
});
};
let title = '';
let titleClass = 'mr-4';
const header = [];
const votes = [];
if (this.props.orderTypes.length) {
title = 'Create order:';
header.push(...this.props.orderTypes.map((orderLetter, index) => (
<div key={index} className={'form-check-inline'}>
{Forms.createRadio('order_type', orderLetter, ORDER_BUILDER[orderLetter].name, this.props.orderType, onChange)}
</div>
)));
header.push(Forms.createReset('reset', false, onReset));
} else if (this.props.power.order_is_set) {
title = 'Unorderable power.';
titleClass += ' neutral';
} else {
title = 'No orders available for this power.';
}
if (!this.props.power.order_is_set) {
header.push(Forms.createButton('pass', this.props.onPass));
}
if (this.props.role !== STRINGS.OMNISCIENT_TYPE) {
votes.push(<strong key={0} className={'ml-4 mr-2'}>Vote for draw:</strong>);
switch (this.props.power.vote) {
case 'yes':
votes.push(Forms.createButton('no', () => this.props.onVote('no'), 'danger'));
votes.push(Forms.createButton('neutral', () => this.props.onVote('neutral'), 'info'));
break;
case 'no':
votes.push(Forms.createButton('yes', () => this.props.onVote('yes'), 'success'));
votes.push(Forms.createButton('neutral', () => this.props.onVote('neutral'), 'info'));
break;
case 'neutral':
votes.push(Forms.createButton('yes', () => this.props.onVote('yes'), 'success'));
votes.push(Forms.createButton('no', () => this.props.onVote('no'), 'danger'));
break;
default:
votes.push(Forms.createButton('yes', () => this.props.onVote('yes'), 'success'));
votes.push(Forms.createButton('no', () => this.props.onVote('no'), 'danger'));
votes.push(Forms.createButton('neutral', () => this.props.onVote('neutral'), 'info'));
break;
}
}
return (
<div>
<div><strong key={'title'} className={titleClass}>{title}</strong></div>
<form className={'form-inline power-actions-form'}>
{header}
{Forms.createButton(
(this.props.power.wait ? 'no wait' : 'wait'),
this.props.onSetWaitFlag,
(this.props.power.wait ? 'success' : 'danger')
)}
{votes}
<HotKey keys={['escape']} onKeysCoincide={onReset}/>
{this.props.orderTypes.map((letter, index) => (
<HotKey key={index} keys={[letter.toLowerCase()]}
onKeysCoincide={() => onSetOrderType(letter)}/>
))}
</form>
</div>
);
}
}
PowerOrderCreationForm.propTypes = {
orderType: PropTypes.oneOf(Object.keys(ORDER_BUILDER)),
orderTypes: PropTypes.arrayOf(PropTypes.oneOf(Object.keys(ORDER_BUILDER))),
power: PropTypes.instanceOf(Power),
role: PropTypes.string,
onChange: PropTypes.func,
onSubmit: PropTypes.func,
onPass: PropTypes.func, // onPass(), to submit empty orders set (powers want to do nothing at this phase)
onVote: PropTypes.func, // onVote(voteString)
onSetWaitFlag: PropTypes.func, // onSetWaitFlag(),
};

View file

@ -0,0 +1,43 @@
// ==============================================================================
// 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 React from "react";
import PropTypes from "prop-types";
import {Button} from "../components/button";
import {FancyBox} from "../components/fancyBox";
export class SelectLocationForm extends React.Component {
render() {
const title = `Select location to continue building order: ${this.props.path.join(' ')}`;
return (
<FancyBox title={title} onClose={this.props.onClose}>
<div>
{this.props.locations.map((location, index) => (
<Button key={index} title={location} large={true}
onClick={() => this.props.onSelect(location)}/>
))}
</div>
</FancyBox>
);
}
}
SelectLocationForm.propTypes = {
locations: PropTypes.arrayOf(PropTypes.string).isRequired,
onSelect: PropTypes.func.isRequired, // onSelect(location)
onClose: PropTypes.func.isRequired,
path: PropTypes.array.isRequired
};

View file

@ -0,0 +1,45 @@
// ==============================================================================
// 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 React from "react";
import PropTypes from "prop-types";
import {Button} from "../components/button";
import {FancyBox} from "../components/fancyBox";
const HotKey = require('react-shortcut');
export class SelectViaForm extends React.Component {
render() {
return (
<FancyBox title={`Select move type for move order: ${this.props.path.join(' ')}`}
onClose={this.props.onClose}>
<div>
<Button title={'regular move (M)'} large={true} onClick={() => this.props.onSelect('M')}/>
<Button title={'move via (V)'} large={true} onClick={() => this.props.onSelect('V')}/>
<HotKey keys={['m']} onKeysCoincide={() => this.props.onSelect('M')}/>
<HotKey keys={['v']} onKeysCoincide={() => this.props.onSelect('V')}/>
</div>
</FancyBox>
);
}
}
SelectViaForm.propTypes = {
path: PropTypes.array.isRequired,
onSelect: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired
};

View file

@ -0,0 +1,19 @@
/**
==============================================================================
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/>.
==============================================================================
**/
/* text */ .SvgAncMed .smalllabeltext {text-anchor:middle;stroke-width:0.1;stroke:black;fill:black;font-family:serif;font-style:italic;font-size:14px;} .SvgAncMed .currentnoterect {fill:#c5dfea; stroke-width:0; stroke:black;} .SvgAncMed .currentnotetext {font-family:serif,sans-serif; font-size:12px; fill:black; stroke:black;} .SvgAncMed .currentphasetext {font-family:serif,sans-serif; font-size:18px; fill:black; stroke:black;} /* style class for invisible regions */ .SvgAncMed .invisibleContent {stroke:white;fill:white;fill-opacity:0.0;opacity:0.0} /* default region coloring, by power (nopower if region not owned) */ .SvgAncMed .provinceRed {fill:url(#patternRed); stroke: black; stroke-width:2} .SvgAncMed .provinceBrown {fill:url(#patternBrown); stroke: black; stroke-width:2} .SvgAncMed .provinceGreen {fill:url(#patternGreen); stroke: black; stroke-width:2} .SvgAncMed .provinceBlack {fill:url(#patternBlack); stroke: black; stroke-width:2} .SvgAncMed .provinceBlue {fill:url(#patternBlue); stroke: black; stroke-width:2} .SvgAncMed .nopower {fill:none;stroke:black;stroke-width:2} .SvgAncMed .water {fill:lightblue;stroke:black;stroke-width:2} .SvgAncMed .rome {fill:#c48f85;stroke:black;stroke-width:2} .SvgAncMed .carthage {fill:royalblue;stroke:black;stroke-width:2} .SvgAncMed .persia {fill:gray;stroke:black;stroke-width:2} .SvgAncMed .greece {fill:forestgreen;stroke:black;stroke-width:2} .SvgAncMed .egypt {fill:#b9a61c;stroke:black;stroke-width:2} /* unit colors fills, by power (units always have a power) */ .SvgAncMed .unitrome {fill:red;} .SvgAncMed .unitcarthage {fill:deepskyblue;} .SvgAncMed .unitpersia {fill:white;} .SvgAncMed .unitgreece {fill:#8db600;} .SvgAncMed .unitegypt {fill:yellow;} /* map accents */ .SvgAncMed .thickdash {fill:none;stroke:black;stroke-width:6;stroke-dasharray:2,1.5;} /* order drawing styles, stroke and fill colors should not be specified */ .SvgAncMed .supportorder {stroke-width:3; fill:none; stroke-dasharray:5,5;} .SvgAncMed .convoyorder {stroke-dasharray:15,5; stroke-width:3; fill:none;} .SvgAncMed .shadowdash {stroke-width:5; fill:none; stroke:black; opacity:0.45;} .SvgAncMed .varwidthorder {fill:none;} .SvgAncMed .varwidthshadow {fill:none; stroke:black;} .SvgAncMed .style1 {fill:none;stroke:#17A1F8;stroke-width:5;} .SvgAncMed .style2 {fill:none;stroke:black;stroke-width:2}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,21 @@
/**
==============================================================================
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/>.
==============================================================================
**/
export const Coordinates = {"ADR":{"unit":["472.5","201.0"],"disl":["479.5","194.0"]},"AEG":{"unit":["657.5","335.0"],"disl":["664.5","328.0"]},"ALE":{"unit":["705.5","504.0"],"disl":["712.5","497.0"]},"ANT":{"unit":["911.5","304.0"],"disl":["918.5","297.0"]},"APU":{"unit":["443.5","222.0"],"disl":["450.5","215.0"]},"ARA":{"unit":["971.5","416.0"],"disl":["978.5","409.0"]},"ARM":{"unit":["977.5","214.0"],"disl":["984.5","207.0"]},"ATH":{"unit":["581.5","300.0"],"disl":["588.5","293.0"]},"AUS":{"unit":["485.5","382.0"],"disl":["492.5","375.0"]},"BAL":{"unit":["153.5","240.0"],"disl":["160.5","233.0"]},"BAY":{"unit":["682.5","617.0"],"disl":["689.5","610.0"]},"BER":{"unit":["205.5","315.0"],"disl":["212.5","308.0"]},"BIT":{"unit":["759.5","209.0"],"disl":["766.5","202.0"]},"BLA":{"unit":["770.5","143.0"],"disl":["777.5","136.0"]},"BYZ":{"unit":["682.5","242.0"],"disl":["689.5","235.0"]},"CAP":{"unit":["865.5","269.0"],"disl":["872.5","262.0"]},"CAR":{"unit":["303.5","372.0"],"disl":["310.5","365.0"]},"CHE":{"unit":["881.5","59.0"],"disl":["888.5","52.0"]},"CIL":{"unit":["785.5","353.0"],"disl":["792.5","346.0"]},"CIR":{"unit":["236.5","442.0"],"disl":["243.5","435.0"]},"COR":{"unit":["307.5","215.0"],"disl":["314.5","208.0"]},"CRE":{"unit":["650.5","382.0"],"disl":["657.5","375.0"]},"CYP":{"unit":["816.5","374.0"],"disl":["823.5","367.0"]},"CYR":{"unit":["625.5","508.0"],"disl":["632.5","501.0"]},"DAC":{"unit":["621.5","138.0"],"disl":["628.5","131.0"]},"DAL":{"unit":["502.5","178.0"],"disl":["509.5","171.0"]},"DAM":{"unit":["973.5","308.0"],"disl":["980.5","301.0"]},"EGY":{"unit":["728.5","441.0"],"disl":["735.5","434.0"]},"EPI":{"unit":["544.5","267.0"],"disl":["551.5","260.0"]},"ETR":{"unit":["313.5","129.0"],"disl":["320.5","122.0"]},"GAL":{"unit":["774.5","264.0"],"disl":["781.5","257.0"]},"GAU":{"unit":["197.5","83.0"],"disl":["204.5","76.0"]},"GOP":{"unit":["843.5","446.0"],"disl":["850.5","439.0"]},"GOS":{"unit":["509.5","498.0"],"disl":["516.5","491.0"]},"GOT":{"unit":["475.5","428.0"],"disl":["482.5","421.0"]},"IBE":{"unit":["82.5","356.0"],"disl":["89.5","349.0"]},"ILL":{"unit":["536.5","157.0"],"disl":["543.5","150.0"]},"ION":{"unit":["508.5","306.0"],"disl":["515.5","299.0"]},"ISA":{"unit":["806.5","313.0"],"disl":["813.5","306.0"]},"JER":{"unit":["899.5","451.0"],"disl":["906.5","444.0"]},"LEP":{"unit":["468.5","533.0"],"disl":["475.5","526.0"]},"LIB":{"unit":["544.5","428.0"],"disl":["551.5","421.0"]},"LIG":{"unit":["256.5","215.0"],"disl":["263.5","208.0"]},"LUS":{"unit":["18.5","227.0"],"disl":["25.5","220.0"]},"MAC":{"unit":["611.5","231.0"],"disl":["618.5","224.0"]},"MAR":{"unit":["601.5","561.0"],"disl":["608.5","554.0"]},"MAS":{"unit":["253.5","127.0"],"disl":["260.5","120.0"]},"MAU":{"unit":["112.5","410.0"],"disl":["119.5","403.0"]},"MEM":{"unit":["783.5","554.0"],"disl":["790.5","547.0"]},"MES":{"unit":["535.5","378.0"],"disl":["542.5","371.0"]},"MIL":{"unit":["730.5","313.0"],"disl":["737.5","306.0"]},"MIN":{"unit":["709.5","370.0"],"disl":["716.5","363.0"]},"NAB":{"unit":["976.5","528.0"],"disl":["983.5","521.0"]},"NEA":{"unit":["457.5","274.0"],"disl":["464.5","267.0"]},"NUM":{"unit":["343.5","471.0"],"disl":["350.5","464.0"]},"PET":{"unit":["917.5","508.0"],"disl":["924.5","501.0"]},"PHA":{"unit":["400.5","553.0"],"disl":["407.5","546.0"]},"PUN":{"unit":["358.5","338.0"],"disl":["365.5","331.0"]},"RAV":{"unit":["391.5","169.0"],"disl":["398.5","162.0"]},"REE":{"unit":["950.5","594.0"],"disl":["957.5","587.0"]},"RHA":{"unit":["339.5","59.0"],"disl":["346.5","52.0"]},"ROM":{"unit":["368.5","188.0"],"disl":["375.5","181.0"]},"SAD":{"unit":["304.5","264.0"],"disl":["311.5","257.0"]},"SAG":{"unit":["58.5","302.0"],"disl":["65.5","295.0"]},"SAH":{"unit":["269.5","588.0"],"disl":["276.5","581.0"]},"SAM":{"unit":["517.5","56.0"],"disl":["524.5","49.0"]},"SIC":{"unit":["421.5","333.0"],"disl":["428.5","326.0"]},"SID":{"unit":["919.5","355.0"],"disl":["926.5","348.0"]},"SIN":{"unit":["871.5","490.0"],"disl":["878.5","483.0"]},"SIP":{"unit":["865.5","196.0"],"disl":["872.5","189.0"]},"SPA":{"unit":["583.5","322.0"],"disl":["590.5","315.0"]},"SYR":{"unit":["864.5","402.0"],"disl":["871.5","395.0"]},"TAR":{"unit":["128.5","221.0"],"disl":["135.5","214.0"]},"THA":{"unit":["307.5","419.0"],"disl":["314.5","412.0"]},"THB":{"unit":["879.5","591.0"],"disl":["886.5","584.0"]},"TYE":{"unit":["914.5","383.0"],"disl":["921.5","376.0"]},"TYN":{"unit":["395.5","268.0"],"disl":["402.5","261.0"]},"VEN":{"unit":["346.5","100.0"],"disl":["353.5","93.0"]},"VIN":{"unit":["410.5","71.0"],"disl":["417.5","64.0"]}};
export const SymbolSizes = {"Fleet":{"width":20.0,"height":20.0},"Army":{"width":20.0,"height":20.0},"Wing":{"width":20.0,"height":20.0},"DislodgedFleet":{"width":20.0,"height":20.0},"DislodgedArmy":{"width":20.0,"height":20.0},"DislodgedWing":{"width":20.0,"height":20.0},"FailedOrder":{"width":20.0,"height":20.0},"SupplyCenter":{"width":10.0,"height":10.0},"BuildUnit":{"width":30.0,"height":30.0},"RemoveUnit":{"width":30.0,"height":30.0},"WaivedBuild":{"width":25.0,"height":25.0},"HoldUnit":{"width":40.0,"height":40.0},"SupportHoldUnit":{"width":46.0,"height":46.0},"ConvoyTriangle":{"width":50.0,"height":43.2},"Stroke":{"width":3.0,"height":5.0}};
export const Colors = {"ROME":"#c48f85","CARTHAGE":"royalblue","PERSIA":"gray","GREECE":"forestgreen","EGYPT":"#b9a61c"};

View file

@ -0,0 +1,57 @@
// ==============================================================================
// 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 React from "react";
import {ARMY, centerSymbolAroundUnit, FLEET} from "./common";
import PropTypes from "prop-types";
export class Build extends React.Component {
render() {
const Coordinates = this.props.coordinates;
const SymbolSizes = this.props.symbolSizes;
const loc = this.props.loc;
const unit_type = this.props.unitType;
const build_symbol = 'BuildUnit';
const loc_x = Coordinates[loc].unit[0];
const loc_y = Coordinates[loc].unit[1];
const [build_loc_x, build_loc_y] = centerSymbolAroundUnit(Coordinates, SymbolSizes, loc, false, build_symbol);
const symbol = unit_type === 'A' ? ARMY : FLEET;
return (
<g>
<use x={build_loc_x}
y={build_loc_y}
height={SymbolSizes[build_symbol].height}
width={SymbolSizes[build_symbol].width}
href={`#${build_symbol}`}/>
<use x={loc_x}
y={loc_y}
height={SymbolSizes[symbol].height}
width={SymbolSizes[symbol].width}
href={`#${symbol}`}
className={`unit${this.props.powerName.toLowerCase()}`}/>
</g>
);
}
}
Build.propTypes = {
unitType: PropTypes.string.isRequired,
loc: PropTypes.string.isRequired,
powerName: PropTypes.string.isRequired,
coordinates: PropTypes.object.isRequired,
symbolSizes: PropTypes.object.isRequired
};

View file

@ -0,0 +1,83 @@
// ==============================================================================
// 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/>.
// ==============================================================================
export const ARMY = 'Army';
export const FLEET = 'Fleet';
export function offset(floatString, offset) {
return "" + (parseFloat(floatString) + offset);
}
export function setInfluence(classes, mapData, loc, power_name) {
const province = mapData.getProvince(loc);
if (!province)
throw new Error(`Unable to find province ${loc}`);
if (!['LAND', 'COAST'].includes(province.type))
return;
const id = province.getID(classes);
if (!id)
throw new Error(`Unable to find SVG path for loc ${id}`);
classes[id] = power_name ? power_name.toLowerCase() : 'nopower';
}
export 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;
}
export function parseLocation(txt) {
if (txt.length > 2 && txt[1] === ' ' && ['A', 'F'].includes(txt[0]))
return txt.substr(2);
return txt;
}
export function centerSymbolAroundUnit(coordinates, symbolSizes, loc, isDislodged, symbol) {
const key = isDislodged ? 'disl' : 'unit';
const unitKey = ARMY;
const [unit_x, unit_y] = coordinates[loc][key];
const unit_height = symbolSizes[unitKey].height;
const unit_width = symbolSizes[unitKey].width;
const symbol_height = symbolSizes[symbol].height;
const symbol_width = symbolSizes[symbol].width;
return [
`${(parseFloat(unit_x) + parseFloat(unit_width) / 2 - parseFloat(symbol_width) / 2)}`,
`${(parseFloat(unit_y) + parseFloat(unit_height) / 2 - parseFloat(symbol_height) / 2)}`
];
}
export function getUnitCenter(coordinates, symbolSizes, loc, isDislodged) {
const key = isDislodged ? 'disl' : 'unit';
const unitKey = ARMY;
const [unit_x, unit_y] = coordinates[loc][key];
const unit_height = symbolSizes[unitKey].height;
const unit_width = symbolSizes[unitKey].width;
return [
`${parseFloat(unit_x) + parseFloat(unit_width) / 2}`,
`${parseFloat(unit_y) + parseFloat(unit_height) / 2}`
];
}
export function plainStrokeWidth(symbolSizes) {
return parseFloat(symbolSizes.Stroke.height);
}
export function coloredStrokeWidth(symbolSizes) {
return parseFloat(symbolSizes.Stroke.width);
}

View file

@ -0,0 +1,103 @@
// ==============================================================================
// 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 React from "react";
import {ARMY, centerSymbolAroundUnit, coloredStrokeWidth, getUnitCenter} from "./common";
import {EquilateralTriangle} from "./equilateralTriangle";
import PropTypes from "prop-types";
export class Convoy extends React.Component {
render() {
const Coordinates = this.props.coordinates;
const SymbolSizes = this.props.symbolSizes;
const Colors = this.props.colors;
const loc = this.props.loc;
const src_loc = this.props.srcLoc;
const dest_loc = this.props.dstLoc;
const symbol = 'ConvoyTriangle';
let [symbol_loc_x, symbol_loc_y] = centerSymbolAroundUnit(Coordinates, SymbolSizes, src_loc, false, symbol);
const symbol_height = parseFloat(SymbolSizes[symbol].height);
const symbol_width = parseFloat(SymbolSizes[symbol].width);
const triangle = new EquilateralTriangle(
parseFloat(symbol_loc_x) + symbol_width / 2,
parseFloat(symbol_loc_y),
parseFloat(symbol_loc_x) + symbol_width,
parseFloat(symbol_loc_y) + symbol_height,
parseFloat(symbol_loc_x),
parseFloat(symbol_loc_y) + symbol_height
);
symbol_loc_y = '' + (parseFloat(symbol_loc_y) - symbol_height / 6);
const [loc_x, loc_y] = getUnitCenter(Coordinates, SymbolSizes, loc, false);
const [src_loc_x, src_loc_y] = getUnitCenter(Coordinates, SymbolSizes, src_loc, false);
let [dest_loc_x, dest_loc_y] = getUnitCenter(Coordinates, SymbolSizes, dest_loc, false);
const [src_loc_x_1, src_loc_y_1] = triangle.intersection(loc_x, loc_y);
const [src_loc_x_2, src_loc_y_2] = triangle.intersection(dest_loc_x, dest_loc_y);
const dest_delta_x = dest_loc_x - src_loc_x;
const dest_delta_y = dest_loc_y - src_loc_y;
const dest_vector_length = Math.sqrt(dest_delta_x * dest_delta_x + dest_delta_y * dest_delta_y);
const delta_dec = parseFloat(SymbolSizes[ARMY].width) / 2 + 2 * coloredStrokeWidth(SymbolSizes);
dest_loc_x = '' + Math.round((parseFloat(src_loc_x) + (dest_vector_length - delta_dec) / dest_vector_length * dest_delta_x) * 100.) / 100.;
dest_loc_y = '' + Math.round((parseFloat(src_loc_y) + (dest_vector_length - delta_dec) / dest_vector_length * dest_delta_y) * 100.) / 100.;
return (
<g stroke={Colors[this.props.powerName]}>
<line x1={loc_x}
y1={loc_y}
x2={src_loc_x_1}
y2={src_loc_y_1}
className={'shadowdash'}/>
<line x1={src_loc_x_2}
y1={src_loc_y_2}
x2={dest_loc_x}
y2={dest_loc_y}
className={'shadowdash'}/>
<line x1={loc_x}
y1={loc_y}
x2={src_loc_x_1}
y2={src_loc_y_1}
className={'convoyorder'}
stroke={Colors[this.props.powerName]}/>
<line x1={src_loc_x_2}
y1={src_loc_y_2}
x2={dest_loc_x}
y2={dest_loc_y}
className={'convoyorder'}
markerEnd={'url(#arrow)'}
stroke={Colors[this.props.powerName]}/>
<use
x={symbol_loc_x}
y={symbol_loc_y}
width={'' + symbol_width}
height={'' + symbol_height}
href={`#${symbol}`}
/>
</g>
);
}
}
Convoy.propTypes = {
loc: PropTypes.string.isRequired,
srcLoc: PropTypes.string.isRequired,
dstLoc: PropTypes.string.isRequired,
powerName: PropTypes.string.isRequired,
coordinates: PropTypes.object.isRequired,
symbolSizes: PropTypes.object.isRequired,
colors: PropTypes.object.isRequired
};

View file

@ -0,0 +1,47 @@
// ==============================================================================
// 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 React from "react";
import {centerSymbolAroundUnit} from "./common";
import PropTypes from "prop-types";
export class Disband extends React.Component {
render() {
const Coordinates = this.props.coordinates;
const SymbolSizes = this.props.symbolSizes;
const loc = this.props.loc;
const phaseType = this.props.phaseType;
const symbol = 'RemoveUnit';
const [loc_x, loc_y] = centerSymbolAroundUnit(Coordinates, SymbolSizes, loc, phaseType === 'R', symbol);
return (
<g>
<use x={loc_x}
y={loc_y}
height={SymbolSizes[symbol].height}
width={SymbolSizes[symbol].width}
href={`#${symbol}`}
/>
</g>
);
}
}
Disband.propTypes = {
loc: PropTypes.string.isRequired,
phaseType: PropTypes.string.isRequired,
coordinates: PropTypes.object.isRequired,
symbolSizes: PropTypes.object.isRequired
};

View file

@ -0,0 +1,122 @@
export class EquilateralTriangle {
/** Helper class that represent an equilateral triangle.;
Used to compute intersection of a line with a side of convoy symbol, which is an equilateral triangle. **/
constructor(x_top, y_top, x_right, y_right, x_left, y_left) {
this.x_A = x_top;
this.y_A = y_top;
this.x_B = x_right;
this.y_B = y_right;
this.x_C = x_left;
this.y_C = y_left;
this.h = this.y_B - this.y_A;
this.x_O = this.x_A;
this.y_O = this.y_A + 2 * this.h / 3;
this.line_AB_a = (this.y_B - this.y_A) / (this.x_B - this.x_A);
this.line_AB_b = this.y_B - this.x_B * this.line_AB_a;
this.line_AC_a = (this.y_C - this.y_A) / (this.x_C - this.x_A);
this.line_AC_b = this.y_C - this.x_C * this.line_AC_a;
}
__line_OM(x_M, y_M) {
const a = (y_M - this.y_O) / (x_M - this.x_O);
const b = y_M - a * x_M;
return [a, b];
}
__intersection_with_AB(x_M, y_M) {
const [a, b] = [this.line_AB_a, this.line_AB_b];
let x = null;
if (x_M === this.x_O) {
x = x_M;
} else {
const [u, v] = this.__line_OM(x_M, y_M);
if (a === u)
return [null, null];
x = (v - b) / (a - u);
}
const y = a * x + b;
if (this.x_A <= x && x <= this.x_B && this.y_A <= y && y <= this.y_B)
return [x, y];
return [null, null];
}
__intersection_with_AC(x_M, y_M) {
const [a, b] = [this.line_AC_a, this.line_AC_b];
let x = null;
if (x_M === this.x_O) {
x = x_M;
} else {
const [u, v] = this.__line_OM(x_M, y_M);
if (a === u)
return [null, null];
x = (v - b) / (a - u);
}
const y = a * x + b;
if (this.x_C <= x && x <= this.x_A && this.y_A <= y && y <= this.y_C)
return [x, y];
return [null, null];
}
__intersection_with_BC(x_M, y_M) {
const y = this.y_C;
let x = null;
if (x_M === this.x_O) {
x = x_M;
} else {
const [a, b] = this.__line_OM(x_M, y_M);
if (a === 0)
return [null, null];
x = (y - b) / a;
}
if (this.x_C <= x && x <= this.x_A)
return [x, y];
return [null, null];
}
intersection(x_M, y_M) {
if (this.x_O === x_M && this.y_O === y_M)
return [x_M, y_M];
if (this.x_O === x_M) {
if (y_M < this.y_O)
return [x_M, this.y_A];
else {
// vertical line intersects BC;
return [x_M, this.y_C];
}
} else if (this.y_O === y_M) {
let a = null;
let b = null;
if (x_M < this.x_O) {
// horizontal line intersects AC;
[a, b] = [this.line_AC_a, this.line_AC_b];
} else {
// horizontal line intersects AB;
[a, b] = [this.line_AB_a, this.line_AB_b];
}
const x = (y_M - b) / a;
return [x, y_M];
} else {
// get nearest point in intersections with AB, AC, BC;
const [p1_x, p1_y] = this.__intersection_with_AB(x_M, y_M);
const [p2_x, p2_y] = this.__intersection_with_AC(x_M, y_M);
const [p3_x, p3_y] = this.__intersection_with_BC(x_M, y_M);
const distances = [];
if (p1_x !== null) {
const d1 = Math.sqrt((p1_x - x_M) * (p1_x - x_M) + (p1_y - y_M) * (p1_y - y_M));
distances.push([d1, p1_x, p1_y]);
}
if (p2_x !== null) {
const d2 = Math.sqrt((p2_x - x_M) * (p2_x - x_M) + (p2_y - y_M) * (p2_y - y_M));
distances.push([d2, p2_x, p2_y]);
}
if (p3_x !== null) {
const d3 = Math.sqrt((p3_x - x_M) * (p3_x - x_M) + (p3_y - y_M) * (p3_y - y_M));
distances.push([d3, p3_x, p3_y]);
}
distances.sort();
const output = distances[0];
output.shift();
return output;
}
}
}

View file

@ -0,0 +1,47 @@
// ==============================================================================
// 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 React from "react";
import {centerSymbolAroundUnit} from "./common";
import PropTypes from "prop-types";
export class Hold extends React.Component {
render() {
const Coordinates = this.props.coordinates;
const Colors = this.props.colors;
const SymbolSizes = this.props.symbolSizes;
const symbol = 'HoldUnit';
const [loc_x, loc_y] = centerSymbolAroundUnit(Coordinates, SymbolSizes, this.props.loc, false, symbol);
return (
<g stroke={Colors[this.props.powerName]}>
<use
x={loc_x}
y={loc_y}
width={SymbolSizes[symbol].width}
height={SymbolSizes[symbol].height}
href={`#${symbol}`}/>
</g>
);
}
}
Hold.propTypes = {
loc: PropTypes.string.isRequired,
powerName: PropTypes.string.isRequired,
coordinates: PropTypes.object.isRequired,
symbolSizes: PropTypes.object.isRequired,
colors: PropTypes.object.isRequired
};

View file

@ -0,0 +1,67 @@
// ==============================================================================
// 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 React from "react";
import {ARMY, coloredStrokeWidth, getUnitCenter, plainStrokeWidth} from "./common";
import PropTypes from "prop-types";
export class Move extends React.Component {
render() {
const Coordinates = this.props.coordinates;
const SymbolSizes = this.props.symbolSizes;
const Colors = this.props.colors;
const src_loc = this.props.srcLoc;
const dest_loc = this.props.dstLoc;
const is_dislodged = this.props.phaseType === 'R';
const [src_loc_x, src_loc_y] = getUnitCenter(Coordinates, SymbolSizes, src_loc, is_dislodged);
let [dest_loc_x, dest_loc_y] = getUnitCenter(Coordinates, SymbolSizes, dest_loc, is_dislodged);
// Adjusting destination
const delta_x = dest_loc_x - src_loc_x;
const delta_y = dest_loc_y - src_loc_y;
const vector_length = Math.sqrt(delta_x * delta_x + delta_y * delta_y);
const delta_dec = parseFloat(SymbolSizes[ARMY].width) / 2 + 2 * coloredStrokeWidth(SymbolSizes);
dest_loc_x = '' + Math.round((parseFloat(src_loc_x) + (vector_length - delta_dec) / vector_length * delta_x) * 100.) / 100.;
dest_loc_y = '' + Math.round((parseFloat(src_loc_y) + (vector_length - delta_dec) / vector_length * delta_y) * 100.) / 100.;
return (
<g>
<line x1={src_loc_x}
y1={src_loc_y}
x2={dest_loc_x}
y2={dest_loc_y}
className={'varwidthshadow'}
strokeWidth={'' + plainStrokeWidth(SymbolSizes)}/>
<line x1={src_loc_x}
y1={src_loc_y}
x2={dest_loc_x}
y2={dest_loc_y}
className={'varwidthorder'}
markerEnd={'url(#arrow)'}
stroke={Colors[this.props.powerName]}
strokeWidth={'' + coloredStrokeWidth(SymbolSizes)}/>
</g>
);
}
}
Move.propTypes = {
srcLoc: PropTypes.string.isRequired,
dstLoc: PropTypes.string.isRequired,
powerName: PropTypes.string.isRequired,
phaseType: PropTypes.string.isRequired,
coordinates: PropTypes.object.isRequired,
symbolSizes: PropTypes.object.isRequired,
colors: PropTypes.object.isRequired
};

View file

@ -0,0 +1,72 @@
// ==============================================================================
// 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 React from "react";
import {centerSymbolAroundUnit, getUnitCenter} from "./common";
import PropTypes from "prop-types";
export class SupportHold extends React.Component {
render() {
const Coordinates = this.props.coordinates;
const SymbolSizes = this.props.symbolSizes;
const Colors = this.props.colors;
const loc = this.props.loc;
const dest_loc = this.props.dstLoc;
const symbol = 'SupportHoldUnit';
const [symbol_loc_x, symbol_loc_y] = centerSymbolAroundUnit(Coordinates, SymbolSizes, dest_loc, false, symbol);
const [loc_x, loc_y] = getUnitCenter(Coordinates, SymbolSizes, loc, false);
let [dest_loc_x, dest_loc_y] = getUnitCenter(Coordinates, SymbolSizes, dest_loc, false);
const delta_x = dest_loc_x - loc_x;
const delta_y = dest_loc_y - loc_y;
const vector_length = Math.sqrt(delta_x * delta_x + delta_y * delta_y);
const delta_dec = parseFloat(SymbolSizes[symbol].height) / 2;
dest_loc_x = '' + Math.round((parseFloat(loc_x) + (vector_length - delta_dec) / vector_length * delta_x) * 100.) / 100.;
dest_loc_y = '' + Math.round((parseFloat(loc_y) + (vector_length - delta_dec) / vector_length * delta_y) * 100.) / 100.;
return (
<g stroke={Colors[this.props.powerName]}>
<line x1={loc_x}
y1={loc_y}
x2={dest_loc_x}
y2={dest_loc_y}
className={'shadowdash'}/>
<line x1={loc_x}
y1={loc_y}
x2={dest_loc_x}
y2={dest_loc_y}
className={'supportorder'}
stroke={Colors[this.props.powerName]}/>
<use
x={symbol_loc_x}
y={symbol_loc_y}
width={SymbolSizes[symbol].width}
height={SymbolSizes[symbol].height}
href={`#${symbol}`}
/>
</g>
);
}
}
SupportHold.propTypes = {
loc: PropTypes.string.isRequired,
dstLoc: PropTypes.string.isRequired,
powerName: PropTypes.string.isRequired,
coordinates: PropTypes.object.isRequired,
symbolSizes: PropTypes.object.isRequired,
colors: PropTypes.object.isRequired
};

View file

@ -0,0 +1,61 @@
// ==============================================================================
// 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 React from "react";
import {ARMY, coloredStrokeWidth, getUnitCenter} from "./common";
import PropTypes from "prop-types";
export class SupportMove extends React.Component {
render() {
const Coordinates = this.props.coordinates;
const SymbolSizes = this.props.symbolSizes;
const Colors = this.props.colors;
const loc = this.props.loc;
const src_loc = this.props.srcLoc;
const dest_loc = this.props.dstLoc;
const [loc_x, loc_y] = getUnitCenter(Coordinates, SymbolSizes, loc, false);
const [src_loc_x, src_loc_y] = getUnitCenter(Coordinates, SymbolSizes, src_loc, false);
let [dest_loc_x, dest_loc_y] = getUnitCenter(Coordinates, SymbolSizes, dest_loc, false);
// Adjusting destination
const delta_x = dest_loc_x - src_loc_x;
const delta_y = dest_loc_y - src_loc_y;
const vector_length = Math.sqrt(delta_x * delta_x + delta_y * delta_y);
const delta_dec = parseFloat(SymbolSizes[ARMY].width) / 2 + 2 * coloredStrokeWidth(SymbolSizes);
dest_loc_x = '' + Math.round((parseFloat(src_loc_x) + (vector_length - delta_dec) / vector_length * delta_x) * 100.) / 100.;
dest_loc_y = '' + Math.round((parseFloat(src_loc_y) + (vector_length - delta_dec) / vector_length * delta_y) * 100.) / 100.;
return (
<g>
<path className={'shadowdash'}
d={`M ${loc_x},${loc_y} C ${src_loc_x},${src_loc_y} ${src_loc_x},${src_loc_y} ${dest_loc_x},${dest_loc_y}`}/>
<path className={'supportorder'}
markerEnd={'url(#arrow)'}
stroke={Colors[this.props.powerName]}
d={`M ${loc_x},${loc_y} C ${src_loc_x},${src_loc_y} ${src_loc_x},${src_loc_y} ${dest_loc_x},${dest_loc_y}`}/>
</g>
);
}
}
SupportMove.propTypes = {
loc: PropTypes.string.isRequired,
srcLoc: PropTypes.string.isRequired,
dstLoc: PropTypes.string.isRequired,
powerName: PropTypes.string.isRequired,
coordinates: PropTypes.object.isRequired,
symbolSizes: PropTypes.object.isRequired,
colors: PropTypes.object.isRequired
};

View file

@ -0,0 +1,50 @@
// ==============================================================================
// 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 React from "react";
import {ARMY, FLEET} from "./common";
import PropTypes from "prop-types";
export class Unit extends React.Component {
render() {
const Coordinates = this.props.coordinates;
const SymbolSizes = this.props.symbolSizes;
const split_unit = this.props.unit.split(/ +/);
const unit_type = split_unit[0];
const loc = split_unit[1];
const dislogged_type = this.props.isDislodged ? 'disl' : 'unit';
const symbol = unit_type === 'F' ? FLEET : ARMY;
const loc_x = Coordinates[loc][dislogged_type][0];
const loc_y = Coordinates[loc][dislogged_type][1];
return (
<use href={`#${this.props.isDislodged ? 'Dislodged' : ''}${symbol}`}
x={loc_x}
y={loc_y}
id={`${this.props.isDislodged ? 'dislodged_' : ''}unit_${loc}`}
width={SymbolSizes[symbol].width}
height={SymbolSizes[symbol].height}
className={`unit${this.props.powerName.toLowerCase()}`}/>
);
}
}
Unit.propTypes = {
unit: PropTypes.string.isRequired,
powerName: PropTypes.string.isRequired,
isDislodged: PropTypes.bool.isRequired,
coordinates: PropTypes.object.isRequired,
symbolSizes: PropTypes.object.isRequired
};

View file

@ -0,0 +1,19 @@
/**
==============================================================================
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/>.
==============================================================================
**/
/* Default Text */ .SvgModern text {font-size:10pt;fill:black;font-family:Arial;} /* Text */ .SvgModern /* .credits {font-size:8pt;font-family:Arial;} */ .SvgModern /* .titletext {font-size:14px;font-weight:bolder;font-family:Arial;} */ .SvgModern .labeltext {font-size:12px;fill:black;font-family:Arial;} .SvgModern .currentnoterect {fill:#c5dfea; stroke-width:0; stroke:black;} .SvgModern .currentnotetext {font-family:serif,sans-serif; font-size:12px; fill:black; stroke:black;} .SvgModern .currentphasetext {font-family:serif,sans-serif; font-size:14px; fill:black; stroke:black;} /* Mouse Layer */ .SvgModern .invisibleContent {stroke:#000000;fill:#000000;fill-opacity:0.0;opacity:0.0} .SvgModern .provinceRed {fill:url(#patternRed); stroke:black; stroke-width: 2} .SvgModern .provinceBrown {fill:url(#patternBrown); stroke:black; stroke-width: 2} .SvgModern .provinceGreen {fill:url(#patternGreen); stroke:black; stroke-width: 2} .SvgModern .provinceBlack {fill:url(#patternBlack); stroke:black; stroke-width: 2} .SvgModern .provinceBlue {fill:url(#patternBlue); stroke:black; stroke-width: 2} /* Nations */ .SvgModern .nopower {fill:#FFEEEE; stroke:black; stroke-linejoin:round; stroke-width: 2} .SvgModern .water {fill:#DDDDFF; stroke:black; stroke-linejoin:round; stroke-width: 2} .SvgModern .britain {fill:royalblue;stroke:black;; stroke-width: 2} .SvgModern .egypt {fill:#808000;stroke:black;; stroke-width: 2} .SvgModern .france {fill:#00FFFF;stroke:black;; stroke-width: 2} .SvgModern .germany {fill:darkgrey;stroke:black;; stroke-width: 2} .SvgModern .italy {fill:#80FF80;stroke:black;; stroke-width: 2} .SvgModern .poland {fill:#FF0000;stroke:black;; stroke-width: 2} .SvgModern .russia {fill:#008000;stroke:black;; stroke-width: 2} .SvgModern .spain {fill:#FF8080;stroke:black;; stroke-width: 2} .SvgModern .turkey {fill:#FFFF00;stroke:black;; stroke-width: 2} .SvgModern .ukraine {fill:#FF00FF;stroke:black;; stroke-width: 2} /* Unit Colors */ .SvgModern .unitbritain {fill:deepskyblue;stroke:black;fill-opacity:0.90;} .SvgModern .unitegypt {fill:#808000;stroke:black;fill-opacity:0.90;} .SvgModern .unitfrance {fill:#00FFFF;stroke:black;fill-opacity:0.90;} .SvgModern .unitgermany {fill:darkgrey;stroke:black;fill-opacity:0.90;} .SvgModern .unititaly {fill:#80FF80;stroke:black;fill-opacity:0.90;} .SvgModern .unitpoland {fill:#FF0000;stroke:black;fill-opacity:0.90;} .SvgModern .unitrussia {fill:#008000;stroke:black;fill-opacity:0.90;} .SvgModern .unitspain {fill:#FF8080;stroke:black;fill-opacity:0.90;} .SvgModern .unitturkey {fill:#FFFF00;stroke:black;fill-opacity:0.90;} .SvgModern .unitukraine {fill:#FF00FF;stroke:black;fill-opacity:0.90;} /* order drawing styles, stroke and fill colors should not be specified */ .SvgModern .supportorder {stroke-width:2; fill:none; stroke-dasharray:5,5;} .SvgModern .convoyorder {stroke-dasharray:15,5; stroke-width:2; fill:none;} .SvgModern .shadowdash {stroke-width:4; fill:none; stroke:black; opacity:0.45;} .SvgModern .varwidthorder {fill:none;} .SvgModern .varwidthshadow {fill:none; stroke:black;} .SvgModern .style1 {fill:darkGray}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,19 @@
/**
==============================================================================
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/>.
==============================================================================
**/
/* text */ .SvgPure svg { font-size: 100% } .SvgPure .labeltext {stroke-width:0.1; stroke:black; fill:black;} .SvgPure .currentnotetext {font-family:serif,sans-serif; font-size:1.5em; fill:black; stroke:black;} .SvgPure .currentphasetext {font-family:serif,sans-serif; fill:black; stroke:black;} /* invisible click rects fill:none does not work */ .SvgPure .invisibleContent {stroke:#000000; fill:#000000; fill-opacity:0.0; opacity:0.0} /* default region coloring, by power */ .SvgPure .provinceRed {fill:url(#patternRed); stroke: black; stroke-width: 2} .SvgPure .provinceBrown {fill:url(#patternBrown); stroke: black; stroke-width: 2} .SvgPure .provinceGreen {fill:url(#patternGreen); stroke: black; stroke-width: 2} .SvgPure .provinceBlack {fill:url(#patternBlack); stroke: black; stroke-width: 2} .SvgPure .provinceBlue {fill:url(#patternBlue); stroke: black; stroke-width: 2} .SvgPure .nopower {fill:antiquewhite; stroke:#000000; stroke-width:2} .SvgPure .water {fill:#c5dfea; stroke:#000000; stroke-width:2} .SvgPure .austria {fill:#c48f85; stroke:#000000; stroke-width:2} .SvgPure .england {fill:darkviolet; stroke:#000000; stroke-width:2} .SvgPure .france {fill:royalblue; stroke:#000000; stroke-width:2} .SvgPure .germany {fill:#a08a75; stroke:#000000; stroke-width:2} .SvgPure .italy {fill:forestgreen; stroke:#000000; stroke-width:2} .SvgPure .russia {fill:#757d91; stroke:#000000; stroke-width:2} .SvgPure .turkey {fill:#b9a61c; stroke:#000000; stroke-width:2} /* unit colors, by power note that underscores are not supported */ .SvgPure .unitaustria {fill:red; fill-opacity:0.85} .SvgPure .unitengland {fill:mediumpurple; fill-opacity:0.85} .SvgPure .unitfrance {fill:deepskyblue; fill-opacity:0.85} .SvgPure .unitgermany {fill:dimgray; fill-opacity:0.85} .SvgPure .unititaly {fill:olive; fill-opacity:0.85} .SvgPure .unitrussia {fill:white; fill-opacity:1.0} .SvgPure .unitturkey {fill:yellow; fill-opacity:0.85} /* order drawing styles, stroke and fill colors should not be specified */ .SvgPure .supportorder {stroke-width:6; fill:none; stroke-dasharray:5,5;} .SvgPure .convoyorder {stroke-dasharray:15,5; stroke-width:6; fill:none;} .SvgPure .shadowdash {stroke-width:10; fill:none; stroke:black; opacity:0.45;} .SvgPure .varwidthorder {fill:none;} .SvgPure .varwidthshadow {fill:none; stroke:black;}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,21 @@
/**
==============================================================================
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/>.
==============================================================================
**/
export const Coordinates = {"BER":{"unit":["362.5","864.0"],"disl":["374.5","852.0"]},"CON":{"unit":["412.5","98.0"],"disl":["424.5","86.0"]},"LON":{"unit":["878.5","514.0"],"disl":["890.5","502.0"]},"MOS":{"unit":["140.5","289.0"],"disl":["152.5","277.0"]},"PAR":{"unit":["707.5","815.0"],"disl":["719.5","803.0"]},"ROM":{"unit":["116.5","628.0"],"disl":["128.5","616.0"]},"VIE":{"unit":["750.5","195.0"],"disl":["762.5","183.0"]}};
export const SymbolSizes = {"Fleet":{"width":40.0,"height":40.0},"Army":{"width":40.0,"height":40.0},"Wing":{"width":40.0,"height":40.0},"DislodgedFleet":{"width":40.0,"height":40.0},"DislodgedArmy":{"width":40.0,"height":40.0},"DislodgedWing":{"width":40.0,"height":40.0},"FailedOrder":{"width":30.0,"height":30.0},"SupplyCenter":{"width":20.0,"height":20.0},"BuildUnit":{"width":60.0,"height":60.0},"RemoveUnit":{"width":50.0,"height":50.0},"WaivedBuild":{"width":40.0,"height":40.0},"HoldUnit":{"width":66.6,"height":66.6},"SupportHoldUnit":{"width":76.6,"height":76.6},"ConvoyTriangle":{"width":66.4,"height":57.4},"Stroke":{"width":6.0,"height":10.0}};
export const Colors = {"AUSTRIA":"#DA251D","ENGLAND":"#2D77B2","FRANCE":"#8E85B7","GERMANY":"#666666","ITALY":"#40A340","RUSSIA":"#EEF0E9","TURKEY":"#E9F507"};

View file

@ -0,0 +1,19 @@
/**
==============================================================================
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/>.
==============================================================================
**/
/* text */ .SvgStandard svg { font-size: 100% } .SvgStandard .labeltext24 {text-anchor:middle; stroke-width:0.1; stroke:black; fill:black; font-family:serif,sans-serif; font-style:italic; font-size:1.4em;} .SvgStandard .labeltext18 {text-anchor:middle; stroke-width:0.1; stroke:black; fill:black; font-family:serif,sans-serif; font-style:italic; font-size:1.1em;} .SvgStandard .currentnotetext {font-family:serif,sans-serif; font-size:1.5em; fill:black; stroke:black;} .SvgStandard .currentnoterect {fill:#c5dfea;} .SvgStandard .currentphasetext {font-family:serif,sans-serif; font-size:2.5em; fill:black; stroke:black;} .SvgStandard .labeltext24 text {cursor:default;} /* map and object features */ .SvgStandard .impassable {fill:#353433; stroke:#000000; stroke-width:1} /* invisible click rects fill:none does not work */ .SvgStandard .invisibleContent {stroke:#000000; fill:#000000; fill-opacity:0.0; opacity:0.0} /* default region coloring, by power */ .SvgStandard .provinceRed {fill:url(#patternRed)} .SvgStandard .provinceBrown {fill:url(#patternBrown)} .SvgStandard .provinceGreen {fill:url(#patternGreen)} .SvgStandard .provinceBlack {fill:url(#patternBlack)} .SvgStandard .provinceBlue {fill:url(#patternBlue)} .SvgStandard .nopower {fill:antiquewhite; stroke:#000000; stroke-width:1} .SvgStandard .water {fill:#c5dfea; stroke:#000000; stroke-width:1} .SvgStandard .neutral {fill:lightgray; stroke:#000000; stroke-width:1} .SvgStandard .austria {fill:#c48f85; stroke:#000000; stroke-width:1} .SvgStandard .england {fill:darkviolet; stroke:#000000; stroke-width:1} .SvgStandard .france {fill:royalblue; stroke:#000000; stroke-width:1} .SvgStandard .germany {fill:#a08a75; stroke:#000000; stroke-width:1} .SvgStandard .italy {fill:forestgreen; stroke:#000000; stroke-width:1} .SvgStandard .russia {fill:#757d91; stroke:#000000; stroke-width:1} .SvgStandard .turkey {fill:#b9a61c; stroke:#000000; stroke-width:1} /* unit colors, by power note that underscores are not supported */ .SvgStandard .unitaustria {fill:red; fill-opacity:0.85} .SvgStandard .unitengland {fill:mediumpurple; fill-opacity:0.85} .SvgStandard .unitfrance {fill:deepskyblue; fill-opacity:0.85} .SvgStandard .unitgermany {fill:dimgray; fill-opacity:0.85} .SvgStandard .unititaly {fill:olive; fill-opacity:0.85} .SvgStandard .unitrussia {fill:white; fill-opacity:1.0} .SvgStandard .unitturkey {fill:yellow; fill-opacity:0.85} /* order drawing styles, stroke and fill colors should not be specified */ .SvgStandard .supportorder {stroke-width:6; fill:none; stroke-dasharray:5,5;} .SvgStandard .convoyorder {stroke-dasharray:15,5; stroke-width:6; fill:none;} .SvgStandard .shadowdash {stroke-width:10; fill:none; stroke:black; opacity:0.45;} .SvgStandard .varwidthorder {fill:none;} .SvgStandard .varwidthshadow {fill:none; stroke:black;}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,99 @@
// ==============================================================================
// 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 React from 'react';
import {Connection} from "../../diplomacy/client/connection";
import {ConnectionForm} from "../forms/connection_form";
import {DipStorage} from "../utils/dipStorage";
import {Helmet} from "react-helmet";
import {Navigation} from "../components/navigation";
import {PageContext} from "../components/page_context";
export class ContentConnection extends React.Component {
constructor(props) {
super(props);
this.connection = null;
this.onSubmit = this.onSubmit.bind(this);
}
onSubmit(data) {
const page = this.context;
for (let fieldName of ['hostname', 'port', 'username', 'password', 'showServerFields'])
if (!data.hasOwnProperty(fieldName))
return page.error(`Missing ${fieldName}, got ${JSON.stringify(data)}`);
page.info('Connecting ...');
if (this.connection) {
this.connection.currentConnectionProcessing.stop();
}
this.connection = new Connection(data.hostname, data.port, window.location.protocol.toLowerCase() === 'https:');
this.connection.onReconnectionError = page.onReconnectionError;
// Page is passed as logger object (with methods info(), error(), success()) when connecting.
this.connection.connect(page)
.then(() => {
page.connection = this.connection;
this.connection = null;
page.success(`Successfully connected to server ${data.username}:${data.port}`);
page.connection.authenticate(data.username, data.password)
.then((channel) => {
page.channel = channel;
return channel.getAvailableMaps();
})
.then(availableMaps => {
for (let mapName of Object.keys(availableMaps))
availableMaps[mapName].powers.sort();
page.availableMaps = availableMaps;
const userGameIndices = DipStorage.getUserGames(page.channel.username);
if (userGameIndices && userGameIndices.length) {
return page.channel.getGamesInfo({games: userGameIndices});
} else {
return null;
}
})
.then((gamesInfo) => {
if (gamesInfo) {
page.success('Found ' + gamesInfo.length + ' user games.');
page.updateMyGames(gamesInfo);
}
page.loadGames({success: `Account ${data.username} connected.`});
})
.catch((error) => {
page.error('Error while authenticating: ' + error + ' Please re-try.');
});
})
.catch((error) => {
page.error('Error while connecting: ' + error + ' Please re-try.');
});
}
render() {
const title = 'Connection';
return (
<main>
<Helmet>
<title>{title} | Diplomacy</title>
</Helmet>
<Navigation title={title}/>
<ConnectionForm onSubmit={this.onSubmit}/>
</main>
);
}
componentDidMount() {
window.scrollTo(0, 0);
}
}
ContentConnection.contextType = PageContext;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,167 @@
// ==============================================================================
// 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 React from "react";
import {Tabs} from "../components/tabs";
import {Table} from "../components/table";
import {FindForm} from "../forms/find_form";
import {InlineGameView} from "../utils/inline_game_view";
import {Helmet} from "react-helmet";
import {Navigation} from "../components/navigation";
import {PageContext} from "../components/page_context";
import {ContentGame} from "./content_game";
import PropTypes from 'prop-types';
import {Tab} from "../components/tab";
import {GameCreationWizard} from "../wizards/gameCreation/gameCreationWizard";
const TABLE_LOCAL_GAMES = {
game_id: ['Game ID', 0],
deadline: ['Deadline', 1],
rights: ['Rights', 2],
rules: ['Rules', 3],
players: ['Players/Expected', 4],
status: ['Status', 5],
phase: ['Phase', 6],
join: ['Join', 7],
actions: ['Actions', 8],
};
export class ContentGames extends React.Component {
constructor(props) {
super(props);
this.state = {tab: null};
this.changeTab = this.changeTab.bind(this);
this.onFind = this.onFind.bind(this);
this.onCreate = this.onCreate.bind(this);
this.wrapGameData = this.wrapGameData.bind(this);
}
getPage() {
return this.context;
}
onFind(form) {
for (let field of ['game_id', 'status', 'include_protected', 'for_omniscience'])
if (!form[field])
form[field] = null;
this.getPage().channel.listGames(form)
.then((data) => {
this.getPage().success('Found ' + data.length + ' data.');
this.getPage().addGamesFound(data);
this.getPage().loadGames();
})
.catch((error) => {
this.getPage().error('Error when looking for distant games: ' + error);
});
}
onCreate(form) {
let networkGame = null;
this.getPage().channel.createGame(form)
.then((game) => {
this.getPage().addToMyGames(game.local);
networkGame = game;
return networkGame.getAllPossibleOrders();
})
.then(allPossibleOrders => {
networkGame.local.setPossibleOrders(allPossibleOrders);
this.getPage().load(
`game: ${networkGame.local.game_id}`,
<ContentGame data={networkGame.local}/>,
{success: 'Game created.'}
);
})
.catch((error) => {
this.getPage().error('Error when creating a game: ' + error);
});
}
changeTab(tabIndex) {
this.setState({tab: tabIndex});
}
wrapGameData(gameData) {
return new InlineGameView(this.getPage(), gameData, this.getPage().availableMaps);
}
gameCreationButton() {
return (
<button type="button"
className="btn btn-danger btn-sm mx-0 mx-sm-4"
onClick={() => this.getPage().dialog(onClose => (
<GameCreationWizard availableMaps={this.getPage().availableMaps}
onCancel={onClose}
username={this.getPage().channel.username}
onSubmit={(form) => {
onClose();
this.onCreate(form);
}}/>
))}>
<strong>create a game</strong>
</button>
);
}
render() {
const title = 'Games';
const page = this.getPage();
const navigation = [
['load a game from disk', page.loadGameFromDisk],
['logout', page.logout]
];
const myGames = this.props.myGames;
const gamesFound = this.props.gamesFound;
myGames.sort((a, b) => b.timestamp_created - a.timestamp_created);
gamesFound.sort((a, b) => b.timestamp_created - a.timestamp_created);
const tab = this.state.tab ? this.state.tab : (myGames.length ? 'my-games' : 'find');
return (
<main>
<Helmet>
<title>{title} | Diplomacy</title>
</Helmet>
<Navigation title={title} afterTitle={this.gameCreationButton()}
username={page.channel.username} navigation={navigation}/>
<Tabs menu={['find', 'my-games']} titles={['Find', 'My Games']}
onChange={this.changeTab} active={tab}>
{tab === 'find' ? (
<Tab id="tab-games-find" display={true}>
<FindForm onSubmit={this.onFind}/>
<Table className={"table table-striped"} caption={"Games"} columns={TABLE_LOCAL_GAMES}
data={gamesFound} wrapper={this.wrapGameData}/>
</Tab>
) : ''}
{tab === 'my-games' ? (
<Tab id={'tab-my-games'} display={true}>
<Table className={"table table-striped"} caption={"My games"} columns={TABLE_LOCAL_GAMES}
data={myGames} wrapper={this.wrapGameData}/>
</Tab>
) : ''}
</Tabs>
</main>
);
}
componentDidMount() {
window.scrollTo(0, 0);
}
}
ContentGames.contextType = PageContext;
ContentGames.propTypes = {
gamesFound: PropTypes.array.isRequired,
myGames: PropTypes.array.isRequired
};

View file

@ -0,0 +1,372 @@
// ==============================================================================
// 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/>.
// ==============================================================================
/** Main class to use to create app GUI. **/
import React from "react";
import {ContentConnection} from "./content_connection";
import {UTILS} from "../../diplomacy/utils/utils";
import {Diplog} from "../../diplomacy/utils/diplog";
import {DipStorage} from "../utils/dipStorage";
import {PageContext} from "../components/page_context";
import {ContentGames} from "./content_games";
import {loadGameFromDisk} from "../utils/load_game_from_disk";
import {ContentGame} from "./content_game";
import {confirmAlert} from 'react-confirm-alert';
import 'react-confirm-alert/src/react-confirm-alert.css';
export class Page extends React.Component {
constructor(props) {
super(props);
this.connection = null;
this.channel = null;
this.availableMaps = null;
this.state = {
// Page messages
error: null,
info: null,
success: null,
// Page content parameters
name: null,
body: null,
// Games.
games: {}, // Games found.
myGames: {} // Games locally stored.
};
this.error = this.error.bind(this);
this.info = this.info.bind(this);
this.success = this.success.bind(this);
this.logout = this.logout.bind(this);
this.loadGameFromDisk = this.loadGameFromDisk.bind(this);
this._post_remove = this._post_remove.bind(this);
this._add_to_my_games = this._add_to_my_games.bind(this);
this._remove_from_my_games = this._remove_from_my_games.bind(this);
this._remove_from_games = this._remove_from_games.bind(this);
this.onReconnectionError = this.onReconnectionError.bind(this);
}
static wrapMessage(message) {
return message ? `(${UTILS.date()}) ${message}` : '';
}
static __sort_games(games) {
// Sort games with not-joined games first, else compare game ID.
games.sort((a, b) => (((a.role ? 1 : 0) - (b.role ? 1 : 0)) || a.game_id.localeCompare(b.game_id)));
return games;
}
static defaultPage() {
return <ContentConnection/>;
}
setState(state) {
return new Promise(resolve => super.setState(state, resolve));
}
onReconnectionError(error) {
this.__disconnect(error);
}
/**
* @callback OnClose
*/
/**
* @callback DialogBuilder
* @param {OnClose} onClose
*/
/**
* open a dialog box
* @param {DialogBuilder} builder - a callback to generate dialog GUI. Will be executed with a `onClose` callback
* parameter to call when dialog must be closed: `builder(onClose)`.
*/
dialog(builder) {
confirmAlert({customUI: ({onClose}) => builder(onClose)});
}
//// Methods to load a page.
load(name, body, messages) {
const newState = {};
if (messages) {
for (let key of ['error', 'info', 'success'])
newState[key] = Page.wrapMessage(messages[key]);
}
Diplog.printMessages(newState);
newState.name = name;
newState.body = body;
return this.setState(newState);
}
loadGames(messages) {
return this.load(
'games',
<ContentGames myGames={this.getMyGames()} gamesFound={this.getGamesFound()}/>,
messages
);
}
loadGameFromDisk() {
return loadGameFromDisk()
.then((game) => this.load(
`game: ${game.game_id}`,
<ContentGame data={game}/>,
{success: `Game loaded from disk: ${game.game_id}`}
))
.catch(this.error);
}
getName() {
return this.state.name;
}
//// Methods to sign out channel and go back to connection page.
__disconnect(error) {
// Clear local data and go back to connection page.
this.connection.close();
this.connection = null;
this.channel = null;
this.availableMaps = null;
const message = Page.wrapMessage(error ? `${error.toString()}` : `Disconnected from channel and server.`);
Diplog.success(message);
return this.setState({
error: error ? message : null,
info: null,
success: error ? null : message,
name: null,
body: null,
// When disconnected, remove all games previously loaded.
games: {},
myGames: {}
});
}
logout() {
// Disconnect channel and go back to connection page.
if (this.channel) {
return this.channel.logout()
.then(() => this.__disconnect())
.catch(error => this.error(`Error while disconnecting: ${error.toString()}.`));
} else {
return this.__disconnect();
}
}
//// Methods to be used to set page title and messages.
error(message) {
message = Page.wrapMessage(message);
Diplog.error(message);
return this.setState({error: message});
}
info(message) {
message = Page.wrapMessage(message);
Diplog.info(message);
return this.setState({info: message});
}
success(message) {
message = Page.wrapMessage(message);
Diplog.success(message);
return this.setState({success: message});
}
warn(message) {
return this.info(message);
}
//// Methods to manage games.
updateMyGames(gamesToAdd) {
// Update state myGames with given games. This method does not update local storage.
const myGames = Object.assign({}, this.state.myGames);
let gamesFound = null;
for (let gameToAdd of gamesToAdd) {
myGames[gameToAdd.game_id] = gameToAdd;
if (this.state.games.hasOwnProperty(gameToAdd.game_id)) {
if (!gamesFound)
gamesFound = Object.assign({}, this.state.games);
gamesFound[gameToAdd.game_id] = gameToAdd;
}
}
if (!gamesFound)
gamesFound = this.state.games;
return this.setState({myGames: myGames, games: gamesFound});
}
getGame(gameID) {
if (this.state.myGames.hasOwnProperty(gameID))
return this.state.myGames[gameID];
return this.state.games[gameID];
}
getMyGames() {
return Page.__sort_games(Object.values(this.state.myGames));
}
getGamesFound() {
return Page.__sort_games(Object.values(this.state.games));
}
addGamesFound(gamesToAdd) {
const gamesFound = {};
for (let game of gamesToAdd) {
gamesFound[game.game_id] = (
this.state.myGames.hasOwnProperty(game.game_id) ?
this.state.myGames[game.game_id] : game
);
}
return this.setState({games: gamesFound});
}
leaveGame(gameID) {
if (this.state.myGames.hasOwnProperty(gameID)) {
const game = this.state.myGames[gameID];
if (game.client) {
return game.client.leave()
.then(() => this.disconnectGame(gameID))
.then(() => this.loadGames({info: `Game ${gameID} left.`}))
.catch(error => this.error(`Error when leaving game ${gameID}: ${error.toString()}`));
}
} else {
return this.loadGames({info: `No game to left.`});
}
return null;
}
_post_remove(gameID) {
return this.disconnectGame(gameID)
.then(() => {
const myGames = this._remove_from_my_games(gameID);
const games = this._remove_from_games(gameID);
return this.setState({games, myGames});
})
.then(() => this.loadGames({info: `Game ${gameID} deleted.`}));
}
removeGame(gameID) {
const game = this.getGame(gameID);
if (game) {
if (game.client) {
return game.client.remove()
.then(() => this._post_remove(gameID))
.catch(error => this.error(`Error when deleting game ${gameID}: ${error.toString()}`));
} else {
return this.channel.joinGame({game_id: gameID})
.then(networkGame => networkGame.remove())
.then(() => this._post_remove(gameID))
.catch(error => this.error(`Error when deleting game after joining it (${gameID}): ${error.toString()}`));
}
}
}
disconnectGame(gameID) {
const game = this.getGame(gameID);
if (game) {
if (game.client) {
game.client.clearAllCallbacks();
game.client.callbacksBound = false;
if (game.client.queue)
game.client.queue.append(null);
}
return this.channel.getGamesInfo({games: [gameID]})
.then(gamesInfo => this.updateMyGames(gamesInfo))
.catch(error => this.error(`Error while leaving game ${gameID}: ${error.toString()}`));
}
return null;
}
_add_to_my_games(game) {
const myGames = Object.assign({}, this.state.myGames);
const gamesFound = this.state.games.hasOwnProperty(game.game_id) ? Object.assign({}, this.state.games) : this.state.games;
myGames[game.game_id] = game;
if (gamesFound.hasOwnProperty(game.game_id))
gamesFound[game.game_id] = game;
return {myGames: myGames, games: gamesFound};
}
_remove_from_my_games(gameID) {
if (this.state.myGames.hasOwnProperty(gameID)) {
const games = Object.assign({}, this.state.myGames);
delete games[gameID];
DipStorage.removeUserGame(this.channel.username, gameID);
return games;
} else {
return this.state.myGames;
}
}
_remove_from_games(gameID) {
if (this.state.games.hasOwnProperty(gameID)) {
const games = Object.assign({}, this.state.games);
delete games[gameID];
return games;
} else {
return this.state.games;
}
}
addToMyGames(game) {
// Update state myGames with given game **and** update local storage.
DipStorage.addUserGame(this.channel.username, game.game_id);
return this.setState(this._add_to_my_games(game)).then(() => this.loadGames());
}
removeFromMyGames(gameID) {
const myGames = this._remove_from_my_games(gameID);
return this.setState({myGames}).then(() => this.loadGames());
}
hasMyGame(gameID) {
return this.state.myGames.hasOwnProperty(gameID);
}
//// Render method.
render() {
const successMessage = this.state.success || '-';
const infoMessage = this.state.info || '-';
const errorMessage = this.state.error || '-';
return (
<PageContext.Provider value={this}>
<div className="page container-fluid" id={this.state.contentName}>
<div className={'top-msg row'}>
<div title={successMessage !== '-' ? successMessage : ''}
className={'col-sm-4 msg success ' + (this.state.success ? 'with-msg' : 'no-msg')}
onClick={() => this.success()}>
{successMessage}
</div>
<div title={infoMessage !== '-' ? infoMessage : ''}
className={'col-sm-4 msg info ' + (this.state.info ? 'with-msg' : 'no-msg')}
onClick={() => this.info()}>
{infoMessage}
</div>
<div title={errorMessage !== '-' ? errorMessage : ''}
className={'col-sm-4 msg error ' + (this.state.error ? 'with-msg' : 'no-msg')}
onClick={() => this.error()}>
{errorMessage}
</div>
</div>
{this.state.body || Page.defaultPage()}
</div>
</PageContext.Provider>
);
}
}

View file

@ -0,0 +1,140 @@
// ==============================================================================
// 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/>.
// ==============================================================================
/* DipStorage scheme:
global
- connection
- username
- hostname
- port
- showServerFields
users
- (username)
- games
- (game_id)
- phase: string
- local_orders: {power_name => [orders]}
*/
let STORAGE = null;
export class DipStorage {
static load() {
if (!STORAGE) {
const global = window.localStorage.global;
const users = window.localStorage.users;
STORAGE = {
global: (global && JSON.parse(global)) || {
connection: {
username: null,
hostname: null,
port: null,
showServerFields: null
}
},
users: (users && JSON.parse(users)) || {}
};
}
}
static save() {
if (STORAGE) {
window.localStorage.global = JSON.stringify(STORAGE.global);
window.localStorage.users = JSON.stringify(STORAGE.users);
}
}
static getConnectionForm() {
DipStorage.load();
return Object.assign({}, STORAGE.global.connection);
}
static getUserGames(username) {
DipStorage.load();
if (STORAGE.users[username])
return Object.keys(STORAGE.users[username].games);
return null;
}
static getUserGameOrders(username, gameID, gamePhase) {
DipStorage.load();
if (STORAGE.users[username] && STORAGE.users[username].games[gameID]
&& STORAGE.users[username].games[gameID].phase === gamePhase)
return Object.assign({}, STORAGE.users[username].games[gameID].local_orders);
return null;
}
static setConnectionUsername(username) {
DipStorage.load();
STORAGE.global.connection.username = username;
DipStorage.save();
}
static setConnectionHostname(hostname) {
DipStorage.load();
STORAGE.global.connection.hostname = hostname;
DipStorage.save();
}
static setConnectionPort(port) {
DipStorage.load();
STORAGE.global.connection.port = port;
DipStorage.save();
}
static setConnectionshowServerFields(showServerFields) {
DipStorage.load();
STORAGE.global.connection.showServerFields = showServerFields;
DipStorage.save();
}
static addUserGame(username, gameID) {
DipStorage.load();
if (!STORAGE.users[username])
STORAGE.users[username] = {games: {}};
if (!STORAGE.users[username].games[gameID])
STORAGE.users[username].games[gameID] = {phase: null, local_orders: {}};
DipStorage.save();
}
static addUserGameOrders(username, gameID, gamePhase, powerName, orders) {
DipStorage.addUserGame(username, gameID);
if (STORAGE.users[username].games[gameID].phase !== gamePhase)
STORAGE.users[username].games[gameID] = {phase: null, local_orders: {}};
STORAGE.users[username].games[gameID].phase = gamePhase;
STORAGE.users[username].games[gameID].local_orders[powerName] = orders;
DipStorage.save();
}
static removeUserGame(username, gameID) {
DipStorage.load();
if (STORAGE.users[username] && STORAGE.users[username].games[gameID]) {
delete STORAGE.users[username].games[gameID];
DipStorage.save();
}
}
static clearUserGameOrders(username, gameID, powerName) {
DipStorage.addUserGame(username, gameID);
if (powerName) {
if (STORAGE.users[username].games[gameID].local_orders[powerName])
delete STORAGE.users[username].games[gameID].local_orders[powerName];
} else {
STORAGE.users[username].games[gameID] = {phase: null, local_orders: {}};
}
DipStorage.save();
}
}

View file

@ -0,0 +1,163 @@
// ==============================================================================
// 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 React from "react";
import {JoinForm} from "../forms/join_form";
import {STRINGS} from "../../diplomacy/utils/strings";
import {ContentGame} from "../pages/content_game";
import {Button} from "../components/button";
import {DeleteButton} from "../components/delete_button";
export class InlineGameView {
constructor(page, gameData, maps) {
this.page = page;
this.game = gameData;
this.maps = maps;
this.get = this.get.bind(this);
this.joinGame = this.joinGame.bind(this);
this.showGame = this.showGame.bind(this);
}
joinGame(formData) {
const form = {
power_name: formData[`power_name_${this.game.game_id}`],
registration_password: formData[`registration_password_${this.game.game_id}`]
};
if (!form.power_name)
form.power_name = null;
if (!form.registration_password)
form.registration_password = null;
form.game_id = this.game.game_id;
this.page.channel.joinGame(form)
.then((networkGame) => {
this.game = networkGame.local;
this.page.addToMyGames(this.game);
return networkGame.getAllPossibleOrders();
})
.then(allPossibleOrders => {
this.game.setPossibleOrders(allPossibleOrders);
this.page.load(
`game: ${this.game.game_id}`,
<ContentGame data={this.game}/>,
{success: 'Game joined.'}
);
})
.catch((error) => {
this.page.error('Error when joining game ' + this.game.game_id + ': ' + error);
});
}
showGame() {
this.page.load(`game: ${this.game.game_id}`, <ContentGame data={this.game}/>);
}
getJoinUI() {
if (this.game.role) {
// Game already joined.
return (
<div className={'games-form'}>
<Button key={'button-show-' + this.game.game_id} title={'show'} onClick={this.showGame}/>
<Button key={'button-leave-' + this.game.game_id} title={'leave'}
onClick={() => this.page.leaveGame(this.game.game_id)}/>
</div>
);
} else {
// Game not yet joined.
return <JoinForm key={this.game.game_id}
game_id={this.game.game_id}
powers={this.game.controlled_powers}
availablePowers={this.maps[this.game.map_name].powers}
password_required={this.game.registration_password}
onSubmit={this.joinGame}/>;
}
}
getActionButtons() {
const buttons = [];
// Button to add/remove game from "My games" list.
if (this.page.hasMyGame(this.game.game_id)) {
if (!this.game.client) {
// Game in My Games and not joined. We can remove it.
buttons.push(<Button key={`my-game-remove`} title={'Remove from My Games'}
small={true} large={true}
onClick={() => this.page.removeFromMyGames(this.game.game_id)}/>);
}
} else {
// Game not in My Games, we can add it.
buttons.push(<Button key={`my-game-add`} title={'Add to My Games'}
small={true} large={true}
onClick={() => this.page.addToMyGames(this.game)}/>);
}
// Button to delete game.
if ([STRINGS.MASTER_TYPE, STRINGS.OMNISCIENT_TYPE].includes(this.game.observer_level)) {
buttons.push(
<DeleteButton key={`game-delete-${this.game.game_id}`}
title={'Delete this game'}
confirmTitle={'Click again to confirm deletion'}
waitingTitle={'Deleting ...'}
onClick={() => this.page.removeGame(this.game.game_id)}/>
);
}
return buttons;
}
get(name) {
if (name === 'players') {
return `${this.game.n_players} / ${this.game.n_controls}`;
}
if (name === 'rights') {
const elements = [];
if (this.game.observer_level) {
let levelName = '';
if (this.game.observer_level === STRINGS.MASTER_TYPE)
levelName = 'master';
else if (this.game.observer_level === STRINGS.OMNISCIENT_TYPE)
levelName = 'omniscient';
else
levelName = 'observer';
elements.push((<p key={0}><strong>Observer right:</strong><br/>{levelName}</p>));
}
if (this.game.controlled_powers && this.game.controlled_powers.length) {
const powers = this.game.controlled_powers.slice();
powers.sort();
elements.push((
<div key={1}><strong>Currently handled power{powers.length === 1 ? '' : 's'}</strong></div>));
for (let power of powers)
elements.push((<div key={power}>{power}</div>));
}
return elements.length ? (<div>{elements}</div>) : '';
}
if (name === 'rules') {
if (this.game.rules)
return <div>{this.game.rules.map(rule => <div key={rule}>{rule}</div>)}</div>;
return '';
}
if (name === 'join')
return this.getJoinUI();
if (name === 'actions')
return this.getActionButtons();
if (name === 'game_id') {
const date = new Date(this.game.timestamp_created / 1000);
const dateString = `${date.toLocaleDateString()} - ${date.toLocaleTimeString()}`;
return <div>
<div><strong>{this.game.game_id}</strong></div>
<div>({dateString})</div>
<div><em>{this.game.map_name}</em></div>
</div>;
}
return this.game[name];
}
}

View file

@ -0,0 +1,101 @@
// ==============================================================================
// 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 $ from "jquery";
import {STRINGS} from "../../diplomacy/utils/strings";
import {Game} from "../../diplomacy/engine/game";
export function loadGameFromDisk() {
return new Promise((onLoad, onError) => {
const input = $(document.createElement('input'));
input.attr("type", "file");
input.trigger('click');
input.change(event => {
const file = event.target.files[0];
if (!file.name.match(/\.json$/i)) {
onError(`Invalid JSON filename ${file.name}`);
return;
}
const reader = new FileReader();
reader.onload = () => {
const savedData = JSON.parse(reader.result);
const gameObject = {};
gameObject.game_id = `(local) ${savedData.id}`;
gameObject.map_name = savedData.map;
gameObject.rules = savedData.rules;
gameObject.state_history = {};
gameObject.message_history = {};
gameObject.order_history = {};
gameObject.result_history = {};
// Load all saved phases (expect the latest one) to history fields.
for (let i = 0; i < savedData.phases.length - 1; ++i) {
const savedPhase = savedData.phases[i];
const gameState = savedPhase.state;
const phaseOrders = savedPhase.orders || {};
const phaseResults = savedPhase.results || {};
const phaseMessages = {};
if (savedPhase.messages) {
for (let message of savedPhase.messages) {
phaseMessages[message.time_sent] = message;
}
}
if (!gameState.name)
gameState.name = savedPhase.name;
gameObject.state_history[gameState.name] = gameState;
gameObject.message_history[gameState.name] = phaseMessages;
gameObject.order_history[gameState.name] = phaseOrders;
gameObject.result_history[gameState.name] = phaseResults;
}
// Load latest phase separately and use it later to define the current game phase.
const latestPhase = savedData.phases[savedData.phases.length - 1];
const latestGameState = latestPhase.state;
const latestPhaseOrders = latestPhase.orders || {};
const latestPhaseResults = latestPhase.results || {};
const latestPhaseMessages = {};
if (latestPhase.messages) {
for (let message of latestPhase.messages) {
latestPhaseMessages[message.time_sent] = message;
}
}
if (!latestGameState.name)
latestGameState.name = latestPhase.name;
// TODO: NB: What if latest phase in loaded JSON contains order results? Not sure if it is well handled.
gameObject.result_history[latestGameState.name] = latestPhaseResults;
gameObject.messages = [];
gameObject.role = STRINGS.OBSERVER_TYPE;
gameObject.status = STRINGS.COMPLETED;
gameObject.timestamp_created = 0;
gameObject.deadline = 0;
gameObject.n_controls = 0;
gameObject.registration_password = '';
const game = new Game(gameObject);
// Set game current phase and state using latest phase found in JSON file.
game.setPhaseData({
name: latestGameState.name,
state: latestGameState,
orders: latestPhaseOrders,
messages: latestPhaseMessages
});
onLoad(game);
};
reader.readAsText(file);
});
});
}

View file

@ -0,0 +1,100 @@
// ==============================================================================
// 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 {Province} from "./province";
export class MapData {
constructor(mapInfo, game) {
// mapInfo: {powers: [], supply_centers: [], aliases: {alias: name}, loc_type: {loc => type}, loc_abut: {loc => [abuts]}}
// game: a NetworkGame object.
this.game = game;
this.powers = new Set(mapInfo.powers);
this.supplyCenters = new Set(mapInfo.supply_centers);
this.aliases = Object.assign({}, mapInfo.aliases);
this.provinces = {};
for (let entry of Object.entries(mapInfo.loc_type)) {
const provinceName = entry[0];
const provinceType = entry[1];
this.provinces[provinceName] = new Province(provinceName, provinceType, this.supplyCenters.has(provinceName));
}
for (let entry of Object.entries(mapInfo.loc_abut)) {
this.getProvince(entry[0]).setNeighbors(entry[1].map(name => this.getProvince(name)));
}
for (let province of Object.values(this.provinces)) {
province.setCoasts(this.provinces);
}
for (let power of Object.values(this.game.powers)) {
for (let center of power.centers) {
this.getProvince(center).setController(power.name, 'C');
}
for (let loc of power.influence) {
this.getProvince(loc).setController(power.name, 'I');
}
for (let unit of power.units) {
this.__add_unit(unit, power.name);
}
for (let unit of Object.keys(power.retreats)) {
this.__add_retreat(unit, power.name);
}
}
for (let entry of Object.entries(this.aliases)) {
const alias = entry[0];
const provinceName = entry[1];
const province = this.getProvince(provinceName);
if (province)
province.aliases.push(alias);
}
}
__add_unit(unit, power_name) {
const splitUnit = unit.split(/ +/);
const unitType = splitUnit[0];
const location = splitUnit[1];
const province = this.getProvince(location);
province.setController(power_name, 'U');
province.unit = unitType;
}
__add_retreat(unit, power_name) {
const splitUnit = unit.split(/ +/);
const location = splitUnit[1];
const province = this.getProvince(location);
province.retreatController = power_name;
province.retreatUnit = unit;
}
getProvince(abbr) {
if (abbr === '')
return null;
if (abbr[0] === '_')
abbr = abbr.substr(1, 3);
if (!abbr)
return null;
if (this.provinces.hasOwnProperty(abbr))
return this.provinces[abbr];
if (this.provinces.hasOwnProperty(abbr.toUpperCase()))
return this.provinces[abbr.toUpperCase()];
if (this.provinces.hasOwnProperty(abbr.toLowerCase()))
return this.provinces[abbr.toLowerCase()];
if (this.aliases.hasOwnProperty(abbr))
return this.provinces[this.aliases[abbr]];
if (this.aliases.hasOwnProperty(abbr.toUpperCase()))
return this.provinces[this.aliases[abbr.toUpperCase()]];
if (this.aliases.hasOwnProperty(abbr.toLowerCase()))
return this.provinces[this.aliases[abbr.toLowerCase()]];
return null;
}
}

View file

@ -0,0 +1,24 @@
// ==============================================================================
// 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/>.
// ==============================================================================
export class Order {
constructor(orderString, isLocal) {
const pieces = orderString.split(/ +/);
this.loc = pieces[1];
this.order = orderString;
this.local = Boolean(isLocal);
}
}

View file

@ -0,0 +1,211 @@
// ==============================================================================
// 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/>.
// ==============================================================================
/*eslint no-unused-vars: ["error", { "args": "none" }]*/
function assertLength(expected, given) {
if (expected !== given)
throw new Error(`Length error: expected ${expected}, given ${given}.`);
}
export class ProvinceCheck {
static retreated(province, powerName) {
const retreatProvince = province.getRetreated(powerName);
if (!retreatProvince)
throw new Error(`No retreated location at province ${province.name}.`);
// No confusion possible, we select the only occupied location at this province.
return [retreatProvince.retreatUnit];
}
static present(province, powerName) {
let unit = null;
let presenceProvince = province.getOccupied(powerName);
if (presenceProvince) {
unit = `${presenceProvince.unit} ${presenceProvince.name}`;
} else {
presenceProvince = province.getRetreated(powerName);
if (!presenceProvince)
throw new Error(`No unit or retreat at province ${province.name}.`);
unit = presenceProvince.retreatUnit;
}
return [unit];
}
static occupied(province, powerName) {
const occupiedProvince = province.getOccupied(powerName);
if (!occupiedProvince)
throw new Error(`No occupied location at province ${province.name}.`);
// No confusion possible, we select the only occupied location at this province.
const unit = occupiedProvince.unit;
const name = occupiedProvince.name.toUpperCase();
return [`${unit} ${name}`];
}
static occupiedByAny(province, unusedPowerName) {
return ProvinceCheck.occupied(province, null);
}
static any(province, unusedPowerName) {
// There may be many locations available for a province (e.g. many coasts).
return province.getLocationNames();
}
static buildOrder(path) {
switch (path[0]) {
case 'H':
return ProvinceCheck.holdToString(path);
case 'M':
return ProvinceCheck.moveToString(path);
case 'V':
return ProvinceCheck.moveViaToString(path);
case 'S':
return ProvinceCheck.supportToString(path);
case 'C':
return ProvinceCheck.convoyToString(path);
case 'R':
return ProvinceCheck.retreatToString(path);
case 'D':
return ProvinceCheck.disbandToString(path);
case 'A':
return ProvinceCheck.buildArmyToString(path);
case 'F':
return ProvinceCheck.buildFleetToString(path);
default:
throw new Error('Unable to build order from path ' + JSON.stringify(path));
}
}
static holdToString(path) {
assertLength(2, path.length);
return `${path[1]} ${path[0]}`;
}
static moveToString(path) {
assertLength(3, path.length);
return `${path[1]} - ${path[2]}`;
}
static moveViaToString(path) {
return ProvinceCheck.moveToString(path) + ' VIA';
}
static supportToString(path) {
assertLength(4, path.length);
let order = `${path[1]} ${path[0]} ${path[2]}`;
if (path[2].substr(2) !== path[3])
order += ` - ${path[3]}`;
return order;
}
static convoyToString(path) {
assertLength(4, path.length);
return `${path[1]} ${path[0]} ${path[2]} - ${path[3]}`;
}
static retreatToString(path) {
assertLength(3, path.length);
return `${path[1]} ${path[0]} ${path[2]}`;
}
static disbandToString(path) {
assertLength(2, path.length);
return `${path[1]} ${path[0]}`;
}
static buildArmyToString(path) {
assertLength(2, path.length);
return `${path[0]} ${path[1]} B`;
}
static buildFleetToString(path) {
assertLength(2, path.length);
return `${path[0]} ${path[1]} B`;
}
}
export const ORDER_BUILDER = {
H: {
name: 'hold (H)',
steps: [ProvinceCheck.occupied]
},
M: {
name: 'move (M)',
steps: [ProvinceCheck.occupied, ProvinceCheck.any]
},
V: {
name: 'move VIA (V)',
steps: [ProvinceCheck.occupied, ProvinceCheck.any]
},
S: {
name: 'support (S)',
steps: [ProvinceCheck.occupied, ProvinceCheck.occupiedByAny, ProvinceCheck.any]
},
C: {
name: 'convoy (C)',
steps: [ProvinceCheck.occupied, ProvinceCheck.occupiedByAny, ProvinceCheck.any]
},
R: {
name: 'retreat (R)',
steps: [ProvinceCheck.retreated, ProvinceCheck.any]
},
D: {
name: 'disband (D)',
steps: [ProvinceCheck.present]
},
A: {
name: 'build army (A)',
steps: [ProvinceCheck.any]
},
F: {
name: 'build fleet (F)',
steps: [ProvinceCheck.any]
},
};
export const POSSIBLE_ORDERS = {
// Allowed orders for movement phase step.
M: ['H', 'M', 'V', 'S', 'C'],
// Allowed orders for retreat phase step.
R: ['R', 'D'],
// Allowed orders for adjustment phase step.
A: ['D', 'A', 'F'],
sorting: {
M: {M: 0, V: 1, S: 2, C: 3, H: 4},
R: {R: 0, D: 1},
A: {A: 0, F: 1, D: 2}
},
sortOrderTypes: function (arr, phaseType) {
arr.sort((a, b) => POSSIBLE_ORDERS.sorting[phaseType][a] - POSSIBLE_ORDERS.sorting[phaseType][b]);
}
};
export function extendOrderBuilding(powerName, orderType, currentOrderPath, location, onBuilding, onBuilt, onError) {
const selectedPath = [orderType].concat(currentOrderPath, location);
if (selectedPath.length - 1 < ORDER_BUILDER[orderType].steps.length) {
// Checker OK, update.
onBuilding(powerName, selectedPath);
} else {
try {
// Order created.
const orderString = ProvinceCheck.buildOrder(selectedPath);
onBuilt(powerName, orderString);
} catch (error) {
onError(error.toString());
}
}
}

View file

@ -0,0 +1,65 @@
// ==============================================================================
// 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 {STRINGS} from "../../diplomacy/utils/strings";
import React from "react";
function getName(power) {
if (power.isEliminated())
return <span className="dummy"><em><s>{power.name.toLowerCase()}</s> (eliminated)</em></span>;
return power.name;
}
function getController(power) {
if (power.isEliminated())
return <span className="dummy"><em>N/A</em></span>;
const controller = power.getController();
return <span className={controller === STRINGS.DUMMY ? 'dummy' : 'controller'}>{controller}</span>;
}
function getOrderFlag(power) {
if (power.isEliminated() || power.game.orderableLocations[power.name].length === 0)
return <span className="dummy"><em>N/A</em></span>;
const value = ['no', 'empty', 'yes'][power.order_is_set];
return <span className={value}>{value}</span>;
}
function getWaitFlag(power) {
if (power.isEliminated())
return <span className="dummy"><em>N/A</em></span>;
return <span className={power.wait ? 'wait' : 'no-wait'}>{power.wait ? 'yes' : 'no'}</span>;
}
const GETTERS = {
name: getName,
controller: getController,
order_is_set: getOrderFlag,
wait: getWaitFlag
};
export class PowerView {
constructor(power) {
this.power = power;
}
static wrap(power) {
return new PowerView(power);
}
get(key) {
return GETTERS[key](this.power);
}
}

View file

@ -0,0 +1,135 @@
// ==============================================================================
// 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/>.
// ==============================================================================
const ProvinceType = {
WATER: 'WATER',
COAST: 'COAST',
PORT: 'PORT',
LAND: 'LAND'
};
export class Province {
constructor(name, type, isSupplyCenter) {
this.name = name;
this.type = type;
this.coasts = {};
this.parent = null;
this.neighbors = {};
this.isSupplyCenter = isSupplyCenter;
this.controller = null; // null or power name.
this.controlType = null; // null, C (center), I (influence) or U (unit).
this.unit = null; // null, A or F
this.retreatController = null;
this.retreatUnit = null; // null or `{unit type} {loc}`
this.aliases = [];
}
compareControlType(a, b) {
const controlTypeLevels = {C: 0, I: 1, U: 2};
return controlTypeLevels[a] - controlTypeLevels[b];
}
__set_controller(controller, controlType) {
this.controller = controller;
this.controlType = controlType;
for (let coast of Object.values(this.coasts))
coast.setController(controller, controlType);
}
setController(controller, controlType) {
if (!['C', 'I', 'U'].includes(controlType))
throw new Error(`Invalid province control type (${controlType}), expected 'C', 'I' or 'U'.`);
if (this.controller && this.controller !== controller) {
const controlTypeComparison = this.compareControlType(controlType, this.controlType);
if (controlTypeComparison === 0)
throw new Error(`Found 2 powers (${this.controller}, ${controller}) trying to control same province ` +
`(${this.name}) with same control type (${controlType} VS ${this.controlType}).`);
if (controlTypeComparison > 0)
this.__set_controller(controller, controlType);
} else
this.__set_controller(controller, controlType);
}
setCoasts(provinces) {
const name = this.name.toUpperCase();
for (let entry of Object.entries(provinces)) {
const pieces = entry[0].split(/[^A-Za-z0-9]+/);
if (pieces.length > 1 && pieces[0].toUpperCase() === name) {
this.coasts[entry[0]] = entry[1];
entry[1].parent = this;
}
}
}
setNeighbors(neighborProvinces) {
for (let province of neighborProvinces)
this.neighbors[province.name] = province;
}
getLocationNames() {
const arr = Object.keys(this.coasts);
arr.splice(0, 0, this.name);
return arr;
}
getOccupied(powerName) {
if (!this.controller)
return null;
if (powerName && this.controller !== powerName)
return null;
if (this.unit)
return this;
for (let coast of Object.values(this.coasts))
if (coast.unit)
return coast;
return null;
}
getRetreated(powerName) {
if (this.retreatController === powerName)
return this;
for (let coast of Object.values(this.coasts))
if (coast.retreatController === powerName)
return coast;
return null;
}
isCoast() {
return this.type === ProvinceType.COAST;
}
isWater() {
return this.type === ProvinceType.WATER;
}
_id(id) {
return `_${id.toLowerCase()}`;
}
getID(identifiers) {
let id = this._id(this.name);
if (!identifiers[id]) {
for (let alias of this.aliases) {
id = this._id(alias);
if (identifiers[id])
break;
}
}
if (!identifiers[id] && this.isCoast())
id = this.parent.getID(identifiers);
return id;
}
}

View file

@ -0,0 +1,34 @@
// ==============================================================================
// 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/>.
// ==============================================================================
export function saveGameToDisk(game, onError) {
if (game.client) {
game.client.save()
.then((savedData) => {
const domLink = document.createElement('a');
domLink.setAttribute(
'href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(JSON.stringify(savedData)));
domLink.setAttribute('download', `${game.game_id}.json`);
domLink.style.display = 'none';
document.body.appendChild(domLink);
domLink.click();
document.body.removeChild(domLink);
})
.catch(exc => onError(`Error while saving game: ${exc.toString()}`));
} else {
onError(`Cannot save this game.`);
}
}

View file

@ -0,0 +1,31 @@
.game-creation-wizard {
display: flex;
flex-direction: column;
justify-content: center;
}
.game-creation-wizard .row {
margin-left: 0;
margin-right: 0;
}
.game-creation-wizard .fancy-box {
width: 75vw;
height: 75vh;
display: flex;
flex-direction: column;
}
.game-creation-wizard .fancy-content {
flex-grow: 1;
overflow: auto;
}
.panel-choose-map {
height: 100%;
}
.panel-choose-map .map-list {
border: 1px solid gray;
height: 100%;
}

View file

@ -0,0 +1,126 @@
// ==============================================================================
// 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 React from 'react';
import PropTypes from 'prop-types';
import {Panels} from "./panelList";
import {PanelChooseMap} from "./panelChooseMap";
import {PanelChoosePlayers} from "./panelChoosePlayers";
import {PanelChoosePower} from "./panelChoosePower";
import {PanelChooseSettings} from "./panelChooseSettings";
import {Maps} from "./mapList";
import {UTILS} from "../../../diplomacy/utils/utils";
import './gameCreationWizard.css';
export class GameCreationWizard extends React.Component {
constructor(props) {
super(props);
this.state = {
panel: Panels.CHOOSE_MAP,
game_id: UTILS.createGameID(this.props.username),
power_name: null,
n_controls: -1,
deadline: 0,
registration_password: '',
map: Maps[0],
no_press: false
};
this.backward = this.backward.bind(this);
this.forward = this.forward.bind(this);
this.updateParams = this.updateParams.bind(this);
}
updateParams(params) {
this.setState(params);
}
goToPanel(panelID) {
if (panelID < Panels.CHOOSE_MAP)
this.props.onCancel();
else if (panelID > Panels.CHOOSE_SETTINGS) {
const rules = ['POWER_CHOICE'];
if (this.state.no_press)
rules.push('NO_PRESS');
if (!this.state.deadline) {
rules.push('NO_DEADLINE');
rules.push('REAL_TIME');
}
this.props.onSubmit({
game_id: this.state.game_id,
map_name: this.state.map.name,
power_name: this.state.power_name,
n_controls: this.state.n_controls,
deadline: this.state.deadline,
registration_password: this.state.registration_password || null,
rules: rules
});
} else
this.setState({panel: panelID, registration_password: ''});
}
backward(step) {
this.goToPanel(this.state.panel - (step ? step : 1));
}
forward(step) {
this.goToPanel(this.state.panel + (step ? step : 1));
}
renderPanel() {
switch (this.state.panel) {
case Panels.CHOOSE_MAP:
return <PanelChooseMap forward={this.forward}
params={this.state}
onUpdateParams={this.updateParams}
cancel={this.props.onCancel}/>;
case Panels.CHOOSE_PLAYERS:
return <PanelChoosePlayers backward={this.backward}
forward={this.forward}
onUpdateParams={this.updateParams}
nbPowers={this.props.availableMaps[this.state.map.name].powers.length}
cancel={this.props.onCancel}/>;
case Panels.CHOOSE_POWER:
return <PanelChoosePower backward={this.backward}
forward={this.forward}
onUpdateParams={this.updateParams}
powers={this.props.availableMaps[this.state.map.name].powers}
cancel={this.props.onCancel}/>;
case Panels.CHOOSE_SETTINGS:
return <PanelChooseSettings backward={this.backward}
forward={this.forward}
onUpdateParams={this.updateParams}
username={this.props.username}
params={this.state}
cancel={this.props.onCancel}/>;
default:
return '';
}
}
render() {
return (
<div className="game-creation-wizard">{this.renderPanel()}</div>
);
}
}
GameCreationWizard.propTypes = {
onCancel: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
availableMaps: PropTypes.object.isRequired,
username: PropTypes.string.isRequired
};

View file

@ -0,0 +1,63 @@
// ==============================================================================
// 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/>.
// ==============================================================================
class VariantInfo {
constructor(variantName, variantTitle) {
this.name = variantName;
this.title = variantTitle;
this.map = null;
}
svgName() {
return this.map.name;
}
}
class MapInfo {
constructor(mapName, mapTitle, variants) {
this.name = mapName;
this.title = mapTitle;
this.variants = null;
if (variants) {
this.variants = [];
for (let variant of variants) {
variant.map = this;
this.variants.push(variant);
}
}
}
svgName() {
return this.name;
}
}
export const Maps = [
new MapInfo('standard', 'Standard', [
new VariantInfo('standard', 'Default'),
new VariantInfo('standard_age_of_empires', 'Age of empires'),
new VariantInfo('standard_age_of_empires_2', 'Age of empires II'),
new VariantInfo('standard_fleet_rome', 'Fleet at Rome'),
new VariantInfo('standard_france_austria', 'France VS Austria'),
new VariantInfo('standard_germany_italy', 'Germany VS Italy')
]),
new MapInfo('ancmed', 'Ancient Mediterranean', [
new VariantInfo('ancmed', 'Default'),
new VariantInfo('ancmed_age_of_empires', 'Age of empires')
]),
new MapInfo('modern', 'Modern'),
new MapInfo('pure', 'Pure'),
];

View file

@ -0,0 +1,113 @@
// ==============================================================================
// 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 React from "react";
import {Maps} from "./mapList";
import {FancyBox} from "../../components/fancyBox";
import PropTypes from "prop-types";
export class PanelChooseMap extends React.Component {
render() {
const mapImg = require(`../../../diplomacy/maps/svg/${this.props.params.map.svgName()}.svg`);
const mapEntries = [];
let count = 0;
for (let mapInfo of Maps) {
++count;
if (!mapInfo.variants) {
mapEntries.push(
<div key={count} className="mb-1 d-flex flex-row">
<button type="button"
className="btn btn-secondary btn-sm flex-grow-1 mr-1"
onMouseOver={() => this.props.onUpdateParams({map: mapInfo})}
onClick={() => this.props.forward()}>
{mapInfo.title}
</button>
<button type="button" className="btn btn-outline-secondary btn-sm" disabled={true}>
<strong>+</strong>
</button>
</div>
);
} else {
const dropDownID = `collapse-${count}-${mapInfo.name}`;
const variants = mapInfo.variants.slice();
const defaultVariant = variants[0];
mapEntries.push(
<div key={count}>
<div className="mb-1 d-flex flex-row">
<button type="button"
className="btn btn-secondary btn-sm flex-grow-1 mr-1"
onMouseOver={() => this.props.onUpdateParams({map: defaultVariant})}
onClick={() => this.props.forward()}>
{mapInfo.title} ({defaultVariant.title})
</button>
<button type="button"
className="btn btn-outline-secondary btn-sm collapsed"
data-toggle="collapse"
data-target={`#${dropDownID}`}
aria-expanded={false}
aria-controls={dropDownID}>
<span className="unroll"><strong>+</strong></span>
<span className="roll"><strong>-</strong></span>
</button>
</div>
<div className="collapse" id={dropDownID}>
<div>
{(() => {
const views = [];
for (let i = 1; i < variants.length; ++i) {
const variantInfo = variants[i];
views.push(
<div key={variantInfo.name} className="mb-1">
<button type="button"
className="btn btn-outline-secondary btn-sm btn-block"
onMouseOver={() => this.props.onUpdateParams({map: variantInfo})}
onClick={() => this.props.forward()}>
{variantInfo.title}
</button>
</div>
);
}
return views;
})()}
</div>
</div>
</div>
);
}
}
return (
<FancyBox title={'Choose a map'} onClose={this.props.cancel}>
<div className="row panel-choose-map">
<div className="col-md">
<div className="map-list p-1 ml-0 ml-sm-1">
{mapEntries}
</div>
</div>
<div className="col-md">
<img className="img-fluid" src={mapImg} alt={this.props.params.map.title}/>
</div>
</div>
</FancyBox>
);
}
}
PanelChooseMap.propTypes = {
forward: PropTypes.func.isRequired,
cancel: PropTypes.func.isRequired,
params: PropTypes.object.isRequired,
onUpdateParams: PropTypes.func.isRequired
};

View file

@ -0,0 +1,83 @@
// ==============================================================================
// 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 React from "react";
import {FancyBox} from "../../components/fancyBox";
import PropTypes from "prop-types";
import Octicon, {ArrowLeft} from "@primer/octicons-react";
export class PanelChoosePlayers extends React.Component {
render() {
return (
<FancyBox title={'Number of human players'} onClose={this.props.cancel}>
<div className="row">
<div className="col-sm">
<button type="button" className="btn btn-secondary btn-sm btn-block inline" onClick={() => {
this.props.onUpdateParams({n_controls: 0});
this.props.forward(2);
}}>None - just bots
</button>
</div>
<div className="col-sm">
<button type="button" className="btn btn-secondary btn-sm btn-block inline" onClick={() => {
this.props.onUpdateParams({n_controls: this.props.nbPowers});
this.props.forward();
}}>All humans - no bots
</button>
</div>
</div>
<div className="row">
<div className="col">
<div className="d-flex flex-row justify-content-center my-2">
{(() => {
const choice = [];
for (let i = 0; i < this.props.nbPowers; ++i) {
choice.push(
<button key={i} type="button"
className={`btn btn-secondary btn-sm flex-grow-1 ${i === 0 ? '' : 'ml-sm-1'}`}
onClick={() => {
this.props.onUpdateParams({n_controls: i + 1});
this.props.forward();
}}>
{i + 1}
</button>
);
}
return choice;
})()}
</div>
</div>
</div>
<div className="row">
<div className="col">
<button type="button" className="btn btn-secondary btn-sm px-3"
onClick={() => this.props.backward()}>
<Octicon icon={ArrowLeft}/>
</button>
</div>
</div>
</FancyBox>
);
}
}
PanelChoosePlayers.propTypes = {
backward: PropTypes.func.isRequired,
forward: PropTypes.func.isRequired,
cancel: PropTypes.func.isRequired,
onUpdateParams: PropTypes.func.isRequired,
nbPowers: PropTypes.number.isRequired
};

View file

@ -0,0 +1,85 @@
// ==============================================================================
// 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 React from "react";
import {FancyBox} from "../../components/fancyBox";
import PropTypes from "prop-types";
import Octicon, {ArrowLeft} from "@primer/octicons-react";
export class PanelChoosePower extends React.Component {
render() {
this.props.powers.sort();
return (
<FancyBox title={'Choose your power'} onClose={this.props.cancel}>
<div className="row">
<div className="col-sm">
<button type="button" className="btn btn-secondary btn-sm btn-block inline" onClick={() => {
this.props.onUpdateParams({power_name: null});
this.props.forward();
}}>I just want to observe
</button>
</div>
<div className="col-sm">
<button type="button" className="btn btn-secondary btn-sm btn-block inline" onClick={() => {
const powerName = this.props.powers[Math.floor(Math.random() * this.props.powers.length)];
this.props.onUpdateParams({power_name: powerName});
this.props.forward();
}}>Choose randomly for me
</button>
</div>
</div>
<div className="row">
<div className="col">
<div className="d-flex flex-row justify-content-center my-2">
{(() => {
const choice = [];
for (let i = 0; i < this.props.powers.length; ++i) {
choice.push(
<button key={i} type="button"
className={`btn btn-secondary btn-sm flex-grow-1 ${i === 0 ? '' : 'ml-sm-1'}`}
onClick={() => {
this.props.onUpdateParams({power_name: this.props.powers[i]});
this.props.forward();
}}>
{this.props.powers[i]}
</button>
);
}
return choice;
})()}
</div>
</div>
</div>
<div className="row">
<div className="col">
<button type="button" className="btn btn-secondary btn-sm px-3"
onClick={() => this.props.backward()}>
<Octicon icon={ArrowLeft}/>
</button>
</div>
</div>
</FancyBox>
);
}
}
PanelChoosePower.propTypes = {
backward: PropTypes.func.isRequired,
forward: PropTypes.func.isRequired,
cancel: PropTypes.func.isRequired,
onUpdateParams: PropTypes.func.isRequired,
powers: PropTypes.arrayOf(PropTypes.string).isRequired
};

View file

@ -0,0 +1,129 @@
// ==============================================================================
// 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 React from "react";
import {FancyBox} from "../../components/fancyBox";
import PropTypes from "prop-types";
import {UTILS} from "../../../diplomacy/utils/utils";
import Octicon, {ArrowLeft} from "@primer/octicons-react";
const DEADLINES = [
[0, '(no deadline)'],
[60, '1 min'],
[60 * 5, '5 min'],
[60 * 30, '30 min'],
[60 * 60 * 2, '2 hrs'],
[60 * 60 * 24, '24 hrs'],
];
export class PanelChooseSettings extends React.Component {
constructor(props) {
super(props);
this.onCheckNoPress = this.onCheckNoPress.bind(this);
this.onSelectDeadline = this.onSelectDeadline.bind(this);
this.onSetRegistrationPassword = this.onSetRegistrationPassword.bind(this);
this.onSetGameID = this.onSetGameID.bind(this);
}
onCheckNoPress(event) {
this.props.onUpdateParams({no_press: event.target.checked});
}
onSelectDeadline(event) {
this.props.onUpdateParams({deadline: parseInt(event.target.value)});
}
onSetRegistrationPassword(event) {
this.props.onUpdateParams({registration_password: event.target.value});
}
onSetGameID(event) {
let gameID = event.target.value;
if (!gameID)
gameID = UTILS.createGameID(this.props.username);
this.props.onUpdateParams({game_id: gameID});
}
render() {
return (
<FancyBox title={'Other settings'} onClose={this.props.cancel}>
<div>
<form>
<div className="form-group row align-items-center mb-2">
<label className="col-md col-form-label" htmlFor="deadline">Deadline</label>
<div className="col-md">
<select id="deadline" className="custom-select custom-select-sm"
value={this.props.params.deadline}
onChange={this.onSelectDeadline}>
{DEADLINES.map((deadline, index) => (
<option key={index} value={deadline[0]}>{deadline[1]}</option>
))}
</select>
</div>
</div>
<div className="form-group row mb-2">
<label className="col-md col-form-label" htmlFor="registration-password">Login
password</label>
<div className="col-md">
<input type="password" className="form-control form-control-sm"
id="registration-password"
value={this.props.params.registration_password}
onChange={this.onSetRegistrationPassword} placeholder="(no password)"/>
</div>
</div>
<div className="form-group row mb-2">
<label className="col-md col-form-label" htmlFor="game-id">Game ID</label>
<div className="col-md">
<input type="text" className="form-control form-control-sm"
id="game-id"
value={this.props.params.game_id}
onChange={this.onSetGameID}/>
</div>
</div>
<div className="custom-control custom-checkbox mb-5">
<input type="checkbox" className="custom-control-input" id="no-press"
checked={this.props.params.no_press} onChange={this.onCheckNoPress}/>
<label className="custom-control-label" htmlFor="no-press">No messages allowed</label>
</div>
</form>
</div>
<div className="row">
<div className="col-sm">
<button type="button" className="btn btn-secondary btn-sm btn-block"
onClick={() => this.props.backward()}>
<Octicon icon={ArrowLeft}/>
</button>
</div>
<div className="col-sm">
<button type="button" className="btn btn-success btn-sm btn-block inline"
onClick={() => this.props.forward()}>
<strong>create the game</strong>
</button>
</div>
</div>
</FancyBox>
);
}
}
PanelChooseSettings.propTypes = {
backward: PropTypes.func.isRequired,
forward: PropTypes.func.isRequired,
cancel: PropTypes.func.isRequired,
params: PropTypes.object.isRequired,
onUpdateParams: PropTypes.func.isRequired,
username: PropTypes.string.isRequired
};

View file

@ -0,0 +1,22 @@
// ==============================================================================
// 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/>.
// ==============================================================================
export const Panels = {
CHOOSE_MAP: 0,
CHOOSE_PLAYERS: 1,
CHOOSE_POWER: 2,
CHOOSE_SETTINGS: 3
};