bunch of fixes after testing round

This commit is contained in:
Fabian Dill
2021-05-14 01:25:41 +02:00
parent b82d6cec31
commit b2f3fd56f4
44 changed files with 18 additions and 15781 deletions

View File

@@ -1,4 +0,0 @@
{
"presets": ["@babel/preset-react", "@babel/preset-env"],
"plugins": ["@babel/plugin-proposal-class-properties"]
}

View File

@@ -1,40 +0,0 @@
module.exports = {
env: {
browser: true,
es6: true,
},
extends: [
'plugin:react/recommended',
'airbnb',
],
parser: 'babel-eslint',
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly',
},
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 2018,
sourceType: 'module',
},
plugins: [
'react',
],
rules: {
"react/jsx-filename-extension": 0,
"react/jsx-one-expression-per-line": 0,
"react/destructuring-assignment": 0,
"react/jsx-curly-spacing": [2, { "when": "always" }],
"react/prop-types": 0,
"react/no-access-state-in-setstate": 0,
"react/button-has-type": 0,
"max-len": [2, { code: 120 }],
"operator-linebreak": [2, "after"],
"no-console": [2, { allow: ["error", "warn"] }],
"linebreak-style": 0,
"jsx-a11y/no-static-element-interactions": 0,
"jsx-a11y/click-events-have-key-events": 0,
},
};

2
data/web/.gitignore vendored
View File

@@ -1,2 +0,0 @@
node_modules
*.map

14170
data/web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,50 +0,0 @@
{
"name": "web-ui",
"version": "1.0.0",
"description": "",
"main": "index.jsx",
"scripts": {
"build": "webpack --config webpack.config.js",
"dev": "webpack --config webpack.dev.js"
},
"author": "LegendaryLinux",
"license": "MIT",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@fortawesome/react-fontawesome": "^0.1.14",
"crypto-browserify": "^3.12.0",
"crypto-js": "^4.0.0",
"css-loader": "^5.1.3",
"lodash-es": "^4.17.21",
"prop-types": "^15.7.2",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-redux": "^7.2.2",
"react-router-dom": "^5.2.0",
"redux": "^4.0.5",
"redux-devtools-extension": "^2.13.9",
"sass-loader": "^10.1.1",
"style-loader": "^2.0.0",
"webpack-cli": "^4.5.0"
},
"devDependencies": {
"@babel/core": "^7.13.10",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/preset-env": "^7.13.10",
"@babel/preset-react": "^7.12.13",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.2.2",
"buffer": "^6.0.3",
"eslint": "^7.22.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"file-loader": "^6.2.0",
"node-sass": "^5.0.0",
"stream-browserify": "^3.0.0",
"webpack": "^5.27.1"
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Berserker Multiworld Web GUI</title>
<script type="application/ecmascript" src="assets/index.bundle.js"></script>
</head>
<body>
<div id="app">
<!-- Populated by React/JSX -->
</div>
</body>
</html>

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 242 KiB

View File

@@ -1,10 +0,0 @@
import React from 'react';
import '../../../styles/HeaderBar/components/HeaderBar.scss';
const HeaderBar = () => (
<div id="header-bar">
Multiworld WebUI
</div>
);
export default HeaderBar;

View File

@@ -1,8 +0,0 @@
const APPEND_MESSAGE = 'APPEND_MESSAGE';
const appendMessage = (content) => ({
type: APPEND_MESSAGE,
content,
});
export default appendMessage;

View File

@@ -1,8 +0,0 @@
const SET_MONITOR_FONT_SIZE = 'SET_MONITOR_FONT_SIZE';
const setMonitorFontSize = (fontSize) => ({
type: SET_MONITOR_FONT_SIZE,
fontSize,
});
export default setMonitorFontSize;

View File

@@ -1,8 +0,0 @@
const SET_SHOW_RELEVANT = 'SET_SHOW_RELEVANT';
const setShowRelevant = (showRelevant) => ({
type: SET_SHOW_RELEVANT,
showRelevant,
});
export default setShowRelevant;

View File

@@ -1,8 +0,0 @@
const SET_SIMPLE_FONT = 'SET_SIMPLE_FONT';
const setSimpleFont = (simpleFont) => ({
type: SET_SIMPLE_FONT,
simpleFont,
});
export default setSimpleFont;

View File

@@ -1,42 +0,0 @@
import _assign from 'lodash-es/assign';
const initialState = {
fontSize: 18,
simpleFont: false,
showRelevantOnly: false,
messageLog: [],
};
const appendToLog = (log, item) => {
const trimmedLog = log.slice(-349);
trimmedLog.push(item);
return trimmedLog;
};
const monitorReducer = (state = initialState, action) => {
switch (action.type) {
case 'SET_MONITOR_FONT_SIZE':
return _assign({}, state, {
fontSize: action.fontSize,
});
case 'SET_SIMPLE_FONT':
return _assign({}, state, {
simpleFont: action.simpleFont,
});
case 'SET_SHOW_RELEVANT':
return _assign({}, state, {
showRelevantOnly: action.showRelevant,
});
case 'APPEND_MESSAGE':
return _assign({}, state, {
messageLog: appendToLog(state.messageLog, action.content),
});
default:
return state;
}
};
export default monitorReducer;

View File

@@ -1,13 +0,0 @@
import React from 'react';
import '../../../styles/Monitor/components/Monitor.scss';
import MonitorControls from '../containers/MonitorControls';
import MonitorWindow from '../containers/MonitorWindow';
const Monitor = () => (
<div id="monitor">
<MonitorControls />
<MonitorWindow />
</div>
);
export default Monitor;

View File

@@ -1,218 +0,0 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import _forEach from 'lodash-es/forEach';
import WebSocketUtils from '../../global/WebSocketUtils';
import '../../../styles/Monitor/containers/MonitorControls.scss';
// Redux actions
import setMonitorFontSize from '../Redux/actions/setMonitorFontSize';
import setShowRelevant from '../Redux/actions/setShowRelevant';
import setSimpleFont from '../Redux/actions/setSimpleFont';
const mapReduxStateToProps = (reduxState) => ({
fontSize: reduxState.monitor.fontSize,
webSocket: reduxState.webUI.webSocket,
availableDevices: reduxState.webUI.availableDevices,
snesDevice: reduxState.gameState.connections.snesDevice,
snesConnected: reduxState.gameState.connections.snesConnected,
serverAddress: reduxState.gameState.connections.serverAddress,
serverConnected: reduxState.gameState.connections.serverConnected,
simpleFont: reduxState.monitor.simpleFont,
});
const mapDispatchToProps = (dispatch) => ({
updateFontSize: (fontSize) => {
dispatch(setMonitorFontSize(fontSize));
},
doToggleRelevance: (showRelevantOnly) => {
dispatch(setShowRelevant(showRelevantOnly));
},
doSetSimpleFont: (simpleFont) => {
dispatch(setSimpleFont(simpleFont));
},
});
class MonitorControls extends Component {
constructor(props) {
super(props);
this.state = {
deviceId: null,
serverAddress: this.props.serverAddress,
};
}
componentDidMount() {
setTimeout(() => {
if (this.props.webSocket) {
// Poll for available devices
this.pollSnesDevices();
}
}, 500);
}
componentDidUpdate(prevProps) {
// If there is only one SNES device available, connect to it automatically
if (
prevProps.availableDevices.length !== this.props.availableDevices.length &&
this.props.availableDevices.length === 1
) {
// eslint-disable-next-line react/no-did-update-set-state
this.setState({ deviceId: this.props.availableDevices[0] }, () => {
if (!this.props.snesConnected) {
this.connectToSnes();
}
});
}
// If we have moved from a disconnected state (default) into a connected state, request the game information
if (
(
(prevProps.snesConnected !== this.props.snesConnected) || // SNES status changed
(prevProps.serverConnected !== this.props.serverConnected) // OR server status changed
) && ((this.props.serverConnected) && (this.props.snesConnected)) // AND both are connected
) {
this.props.webSocket.send(WebSocketUtils.formatSocketData('webStatus', 'gameInfo'));
this.props.webSocket.send(WebSocketUtils.formatSocketData('webStatus', 'checkData'));
}
}
increaseTextSize = () => {
if (this.props.fontSize >= 25) return;
this.props.updateFontSize(this.props.fontSize + 1);
};
decreaseTextSize = () => {
if (this.props.fontSize <= 10) return;
this.props.updateFontSize(this.props.fontSize - 1);
};
generateSnesOptions = () => {
const options = [];
// No available devices, show waiting for devices
if (this.props.availableDevices.length === 0) {
options.push(<option key="0" value="-1">Waiting for devices...</option>);
return options;
}
// More than one available device, list all options
options.push(<option key="-1" value="-1">Select a device</option>);
_forEach(this.props.availableDevices, (device) => {
options.push(<option key={ device } value={ device }>{device}</option>);
});
return options;
}
updateDeviceId = (event) => this.setState({ deviceId: event.target.value }, this.connectToSnes);
pollSnesDevices = () => {
if (!this.props.webSocket) { return; }
this.props.webSocket.send(WebSocketUtils.formatSocketData('webStatus', 'devices'));
}
connectToSnes = () => {
if (!this.props.webSocket) { return; }
this.props.webSocket.send(WebSocketUtils.formatSocketData('webConfig', { deviceId: this.state.deviceId }));
}
updateServerAddress = (event) => this.setState({ serverAddress: event.target.value ? event.target.value : null });
connectToServer = (event) => {
if (event.key !== 'Enter') { return; }
// If the user presses enter on an empty textbox, disconnect from the server
if (!event.target.value) {
this.props.webSocket.send(WebSocketUtils.formatSocketData('webControl', 'disconnect'));
return;
}
this.props.webSocket.send(
WebSocketUtils.formatSocketData('webConfig', { serverAddress: this.state.serverAddress }),
);
}
toggleRelevance = (event) => {
this.props.doToggleRelevance(event.target.checked);
};
setSimpleFont = (event) => this.props.doSetSimpleFont(event.target.checked);
render() {
return (
<div id="monitor-controls">
<div id="connection-status">
<div id="snes-connection">
<table>
<tbody>
<tr>
<td>SNES Device:</td>
<td>
<select
onChange={ this.updateDeviceId }
disabled={ this.props.availableDevices.length === 0 }
value={ this.state.deviceId }
>
{this.generateSnesOptions()}
</select>
</td>
</tr>
<tr>
<td>Status:</td>
<td>
<span className={ this.props.snesConnected ? 'connected' : 'not-connected' }>
{this.props.snesConnected ? 'Connected' : 'Not Connected'}
</span>
</td>
</tr>
</tbody>
</table>
</div>
<div id="server-connection">
<table>
<tbody>
<tr>
<td>Server:</td>
<td>
<input
defaultValue={ this.props.serverAddress }
onKeyUp={ this.updateServerAddress }
onKeyDown={ this.connectToServer }
/>
</td>
</tr>
<tr>
<td>Status:</td>
<td>
<span className={ this.props.serverConnected ? 'connected' : 'not-connected' }>
{this.props.serverConnected ? 'Connected' : 'Not Connected'}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div id="accessibility">
<div>
Text Size:
<button disabled={ this.props.fontSize <= 10 } onClick={ this.decreaseTextSize }>-</button>
{ this.props.fontSize }
<button disabled={ this.props.fontSize >= 25 } onClick={ this.increaseTextSize }>+</button>
</div>
<div>
Only show my items <input type="checkbox" onChange={ this.toggleRelevance } />
</div>
<div>
Use alternate font
<input
type="checkbox"
onChange={ this.setSimpleFont }
defaultChecked={ this.props.simpleFont }
/>
</div>
</div>
</div>
);
}
}
export default connect(mapReduxStateToProps, mapDispatchToProps)(MonitorControls);

View File

@@ -1,96 +0,0 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import md5 from 'crypto-js/md5';
import WebSocketUtils from '../../global/WebSocketUtils';
import '../../../styles/Monitor/containers/MonitorWindow.scss';
// Redux actions
import appendMessage from '../Redux/actions/appendMessage';
const mapReduxStateToProps = (reduxState) => ({
fontSize: reduxState.monitor.fontSize,
webSocket: reduxState.webUI.webSocket,
messageLog: reduxState.monitor.messageLog,
showRelevantOnly: reduxState.monitor.showRelevantOnly,
});
const mapDispatchToProps = (dispatch) => ({
doAppendMessage: (message) => dispatch(appendMessage(
<div
key={ `${md5(message)}${Math.floor((Math.random() * 1000000))}` }
className="user-command relevant"
>
{message}
</div>,
)),
});
class MonitorWindow extends Component {
constructor(props) {
super(props);
this.monitorRef = React.createRef();
this.commandRef = React.createRef();
this.commandInputRef = React.createRef();
}
componentDidMount() {
// Adjust the monitor height to match user's viewport
this.adjustMonitorHeight();
// Resize the monitor as the user adjusts the window size
window.addEventListener('resize', this.adjustMonitorHeight);
}
componentDidUpdate() {
this.monitorRef.current.style.fontSize = `${this.props.fontSize}px`;
this.adjustMonitorHeight();
}
componentWillUnmount() {
// If one day we have different components occupying the main viewport, let us not attempt to
// perform actions on an unmounted component
window.removeEventListener('resize', this.adjustMonitorHeight);
}
adjustMonitorHeight = () => {
const monitorDimensions = this.monitorRef.current.getBoundingClientRect();
const commandDimensions = this.commandRef.current.getBoundingClientRect();
// Set monitor height
const newMonitorHeight = window.innerHeight - monitorDimensions.top - commandDimensions.height - 30;
this.monitorRef.current.style.height = `${newMonitorHeight}px`;
this.scrollToBottom();
};
scrollToBottom = () => {
this.monitorRef.current.scrollTo(0, this.monitorRef.current.scrollHeight);
};
sendCommand = (event) => {
// If the user didn't press enter, or the command is empty, do nothing
if (event.key !== 'Enter' || !event.target.value) return;
this.props.doAppendMessage(event.target.value);
this.scrollToBottom();
this.props.webSocket.send(WebSocketUtils.formatSocketData('webCommand', event.target.value));
this.commandInputRef.current.value = '';
};
render() {
return (
<div id="monitor-window-wrapper">
<div
id="monitor-window"
ref={ this.monitorRef }
className={ `${this.props.showRelevantOnly ? 'relevant-only' : null}` }
>
{ this.props.messageLog }
</div>
<div id="command-wrapper" ref={ this.commandRef }>
Command: <input onKeyDown={ this.sendCommand } ref={ this.commandInputRef } />
</div>
</div>
);
}
}
export default connect(mapReduxStateToProps, mapDispatchToProps)(MonitorWindow);

View File

@@ -1,8 +0,0 @@
const SET_AVAILABLE_DEVICES = 'SET_AVAILABLE_DEVICES';
const setAvailableDevices = (devices) => ({
type: SET_AVAILABLE_DEVICES,
devices,
});
export default setAvailableDevices;

View File

@@ -1,8 +0,0 @@
const SET_WEBSOCKET = 'SET_WEBSOCKET';
const setWebSocket = (webSocket) => ({
type: SET_WEBSOCKET,
webSocket,
});
export default setWebSocket;

View File

@@ -1,25 +0,0 @@
import _assign from 'lodash-es/assign';
const initialState = {
webSocket: null,
availableDevices: [],
};
const webUIReducer = (state = initialState, action) => {
switch (action.type) {
case 'SET_WEBSOCKET':
return _assign({}, state, {
webSocket: action.webSocket,
});
case 'SET_AVAILABLE_DEVICES':
return _assign({}, state, {
availableDevices: action.devices,
});
default:
return state;
}
};
export default webUIReducer;

View File

@@ -1,109 +0,0 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import HeaderBar from '../../HeaderBar/components/HeaderBar';
import Monitor from '../../Monitor/components/Monitor';
import WidgetArea from '../../WidgetArea/containers/WidgetArea';
import MonitorTools from '../../global/MonitorTools';
import '../../../styles/WebUI/containers/WebUI.scss';
// Redux actions
import setWebSocket from '../Redux/actions/setWebSocket';
import WebSocketUtils from '../../global/WebSocketUtils';
import updateGameState from '../../global/Redux/actions/updateGameState';
import appendMessage from '../../Monitor/Redux/actions/appendMessage';
const mapReduxStateToProps = (reduxState) => ({
connections: reduxState.gameState.connections,
simpleFont: reduxState.monitor.simpleFont,
});
const mapDispatchToProps = (dispatch) => ({
doSetWebSocket: (webSocket) => dispatch(setWebSocket(webSocket)),
handleIncomingMessage: (message) => dispatch(WebSocketUtils.handleIncomingMessage(message)),
doUpdateGameState: (gameState) => dispatch(updateGameState(gameState)),
appendMonitorMessage: (message) => dispatch(appendMessage(message)),
});
class WebUI extends Component {
constructor(props) {
super(props);
this.webSocket = null;
this.maxConnectionAttempts = 20;
this.webUiRef = React.createRef();
this.state = {
connectionAttempts: 0,
};
}
componentDidMount() {
this.webSocketConnect();
}
webSocketConnect = () => {
this.props.appendMonitorMessage(MonitorTools.createTextDiv(
`Attempting to connect to MultiClient (attempt ${this.state.connectionAttempts + 1})...`,
));
this.setState({ connectionAttempts: this.state.connectionAttempts + 1 }, () => {
if (this.state.connectionAttempts >= 20) {
this.props.appendMonitorMessage(MonitorTools.createTextDiv(
'Unable to connect to MultiClient. Maximum of 20 attempts exceeded.',
));
return;
}
const getParams = new URLSearchParams(document.location.search.substring(1));
const port = getParams.get('port');
if (!port) { throw new Error('Unable to determine socket port from GET parameters'); }
const webSocketAddress = `ws://localhost:${port}`;
try {
this.props.webSocket.close();
this.props.doSetWebSocket(null);
} catch (error) {
// Ignore errors caused by attempting to close an invalid WebSocket object
}
const webSocket = new WebSocket(webSocketAddress);
webSocket.onerror = () => {
this.props.doUpdateGameState({
connections: {
snesDevice: this.props.connections.snesDevice,
snesConnected: false,
serverAddress: this.props.connections.serverAddress,
serverConnected: false,
},
});
if (this.state.connectionAttempts < this.maxConnectionAttempts) {
setTimeout(this.webSocketConnect, 5000);
}
};
// Dispatch a custom event when websocket messages are received
webSocket.onmessage = (message) => {
this.props.handleIncomingMessage(message);
};
// Store the webSocket object in the Redux store so other components can access it
webSocket.onopen = () => {
this.props.doSetWebSocket(webSocket);
webSocket.send(WebSocketUtils.formatSocketData('webStatus', 'connections'));
this.props.appendMonitorMessage(MonitorTools.createTextDiv('Connected to MultiClient.'));
this.setState({ connectionAttempts: 0 });
};
});
};
render() {
return (
<div id="web-ui" ref={ this.webUiRef } className={ this.props.simpleFont ? 'simple-font' : null }>
<HeaderBar />
<div id="content-middle">
<Monitor />
<WidgetArea />
</div>
</div>
);
}
}
export default connect(mapReduxStateToProps, mapDispatchToProps)(WebUI);

View File

@@ -1,117 +0,0 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import '../../../styles/WidgetArea/containers/WidgetArea.scss';
const mapReduxStateToProps = (reduxState) => ({
clientVersion: reduxState.gameState.clientVersion,
forfeitMode: reduxState.gameState.forfeitMode,
remainingMode: reduxState.gameState.remainingMode,
hintCost: reduxState.gameState.hintCost,
checkPoints: reduxState.gameState.checkPoints,
hintPoints: reduxState.gameState.hintPoints,
totalChecks: reduxState.gameState.totalChecks,
lastCheck: reduxState.gameState.lastCheck,
});
class WidgetArea extends Component {
constructor(props) {
super(props);
this.state = {
collapsed: false,
};
}
saveNotes = (event) => {
localStorage.setItem('notes', event.target.value);
};
// eslint-disable-next-line react/no-access-state-in-setstate
toggleCollapse = () => this.setState({ collapsed: !this.state.collapsed });
render() {
return (
<div id="widget-area" className={ `${this.state.collapsed ? 'collapsed' : null}` }>
{
this.state.collapsed ? (
<div id="widget-button-row">
<button className="collapse-button" onClick={ this.toggleCollapse }></button>
</div>
) : null
}
{
this.state.collapsed ? null : (
<div id="widget-area-contents">
<div id="game-info">
<div id="game-info-title">
Game Info:
<button className="collapse-button" onClick={ this.toggleCollapse }></button>
</div>
<table>
<tbody>
<tr>
<th>Client Version:</th>
<td>{this.props.clientVersion}</td>
</tr>
<tr>
<th>Forfeit Mode:</th>
<td>{this.props.forfeitMode}</td>
</tr>
<tr>
<th>Remaining Mode:</th>
<td>{this.props.remainingMode}</td>
</tr>
</tbody>
</table>
</div>
<div id="check-data">
<div id="check-data-title">Checks:</div>
<table>
<tbody>
<tr>
<th>Total Checks:</th>
<td>{this.props.totalChecks}</td>
</tr>
<tr>
<th>Last Check:</th>
<td>{this.props.lastCheck}</td>
</tr>
</tbody>
</table>
</div>
<div id="hint-data">
<div id="hint-data-title">
Hint Data:
</div>
<table>
<tbody>
<tr>
<th>Hint Cost:</th>
<td>{this.props.hintCost}</td>
</tr>
<tr>
<th>Check Points:</th>
<td>{this.props.checkPoints}</td>
</tr>
<tr>
<th>Current Points:</th>
<td>{this.props.hintPoints}</td>
</tr>
</tbody>
</table>
</div>
<div id="notes">
<div id="notes-title">
<div>Notes:</div>
</div>
<textarea defaultValue={ localStorage.getItem('notes') } onKeyUp={ this.saveNotes } />
</div>
More tools Coming Soon
</div>
)
}
</div>
);
}
}
export default connect(mapReduxStateToProps)(WidgetArea);

View File

@@ -1,69 +0,0 @@
import React from 'react';
import md5 from 'crypto-js/md5';
const finderSpan = (finder, possessive = false, ownItem = false) => (
<span className={ `finder-span ${ownItem ? 'mine' : null}` }>{finder}{possessive ? "'s" : null}</span>
);
const recipientSpan = (recipient, possessive = false, ownItem = false) => (
<span className={ `recipient-span ${ownItem ? 'mine' : null}` }>{recipient}{possessive ? "'s" : null}</span>
);
const itemSpan = (item, unique) => <span className={ `item-span ${unique ? 'unique' : ''}` }>{item}</span>;
const locationSpan = (location) => <span className="location-span">{location}</span>;
const entranceSpan = (entrance) => <span className="entrance-span">{entrance}</span>;
class MonitorTools {
/** Convert plaintext into a React-friendly div */
static createTextDiv = (text) => (
<div key={ `${md5(text)}${Math.floor((Math.random() * 1000000))}` }>
{text}
</div>
);
/** Sent an item to another player */
static sentItem = (finder, recipient, item, location, iAmFinder = false, iAmRecipient = false, unique = false) => (
<div
key={ `${md5(finder + recipient + item + location)}${Math.floor((Math.random() * 1000000))}` }
className={ (iAmFinder || iAmRecipient) ? 'relevant' : null }
>
{finderSpan(finder, false, iAmFinder)} found {recipientSpan(recipient, true, iAmRecipient)}&nbsp;
{itemSpan(item, unique)} at {locationSpan(location)}
</div>
)
/** Received item from another player */
static receivedItem = (finder, item, location, itemIndex, queueLength, unique = false) => (
<div
key={ `${md5(finder + item + location)}${Math.floor((Math.random() * 1000000))}` }
className="relevant"
>
({itemIndex}/{queueLength}) {finderSpan(finder, false)} found your&nbsp;
{itemSpan(item, unique)} at {locationSpan(location)}
</div>
)
/** Player found their own item (local or remote player) */
static foundItem = (finder, item, location, iAmFinder = false, unique = false) => (
<div
key={ `${md5(finder + item + location)}${Math.floor((Math.random() * 1000000))}` }
className={ iAmFinder ? 'relevant' : null }
>
{finderSpan(finder, false, iAmFinder)} found their own {itemSpan(item, unique)} at {locationSpan(location)}
</div>
)
/** Hint message */
static hintMessage = (finder, recipient, item, location, found, iAmFinder = false, iAmRecipient = false,
entranceLocation = null) => (
<div
key={ `${md5(finder + recipient + item + location)}${Math.floor((Math.random() * 1000000))}` }
className={ (iAmFinder || iAmRecipient) ? 'relevant' : null }
>
{recipientSpan(recipient, true, iAmRecipient)} {itemSpan(item)} can be found in&nbsp;
{finderSpan(finder, true, iAmFinder)} world at {locationSpan(location)}
{ entranceLocation ? [', which is at ', entranceSpan(entranceLocation)] : null }&nbsp;
({found ? '✔' : '❌'})
</div>
)
}
export default MonitorTools;

View File

@@ -1,8 +0,0 @@
const UPDATE_GAME_STATE = 'UPDATE_GAME_STATE';
const updateGameState = (gameState) => ({
type: UPDATE_GAME_STATE,
gameState,
});
export default updateGameState;

View File

@@ -1,30 +0,0 @@
import _assign from 'lodash-es/assign';
const initialState = {
clientVersion: null,
forfeitMode: null,
remainingMode: null,
connections: {
snesDevice: '',
snesConnected: false,
serverAddress: null,
serverConnected: false,
},
totalChecks: 0,
lastCheck: null,
hintCost: null,
checkPoints: null,
hintPoints: 0,
};
const gameStateReducer = (state = initialState, action) => {
switch (action.type) {
case 'UPDATE_GAME_STATE':
return _assign({}, state, action.gameState);
default:
return state;
}
};
export default gameStateReducer;

View File

@@ -1,106 +0,0 @@
import MonitorTools from './MonitorTools';
// Redux actions
import appendMessage from '../Monitor/Redux/actions/appendMessage';
import updateGameState from './Redux/actions/updateGameState';
import setAvailableDevices from '../WebUI/Redux/actions/setAvailableDevices';
class WebSocketUtils {
static formatSocketData = (eventType, content) => JSON.stringify({
type: eventType,
content,
});
/**
* Handle incoming websocket data and return appropriate data for dispatch
* @param message
* @returns Object
*/
static handleIncomingMessage = (message) => {
try {
const data = JSON.parse(message.data);
switch (data.type) {
// Client sent snes and server connection statuses
case 'connections':
return updateGameState({
connections: {
snesDevice: data.content.snesDevice ? data.content.snesDevice : '',
snesConnected: parseInt(data.content.snes, 10) === 3,
serverAddress: data.content.serverAddress ? data.content.serverAddress.replace(/^.*\/\//, '') : null,
serverConnected: parseInt(data.content.server, 10) === 1,
},
});
case 'availableDevices':
return setAvailableDevices(data.content.devices);
// Client unable to automatically connect to multiworld server
case 'serverAddress':
return appendMessage(MonitorTools.createTextDiv(
'Unable to automatically connect to multiworld server. Please enter an address manually.',
));
case 'itemSent':
return appendMessage(MonitorTools.sentItem(data.content.finder, data.content.recipient,
data.content.item, data.content.location, parseInt(data.content.iAmFinder, 10) === 1,
parseInt(data.content.iAmRecipient, 10) === 1, parseInt(data.content.itemIsUnique, 10) === 1));
case 'itemReceived':
return appendMessage(MonitorTools.receivedItem(data.content.finder, data.content.item,
data.content.location, data.content.itemIndex, data.content.queueLength,
parseInt(data.content.itemIsUnique, 10) === 1));
case 'itemFound':
return appendMessage(MonitorTools.foundItem(data.content.finder, data.content.item, data.content.location,
parseInt(data.content.iAmFinder, 10) === 1, parseInt(data.content.itemIsUnique, 10) === 1));
case 'hint':
return appendMessage(MonitorTools.hintMessage(data.content.finder, data.content.recipient,
data.content.item, data.content.location, parseInt(data.content.found, 10) === 1,
parseInt(data.content.iAmFinder, 10) === 1, parseInt(data.content.iAmRecipient, 10) === 1,
data.content.entranceLocation));
case 'gameInfo':
return updateGameState({
clientVersion: data.content.clientVersion,
forfeitMode: data.content.forfeitMode,
remainingMode: data.content.remainingMode,
hintCost: parseInt(data.content.hintCost, 10),
checkPoints: parseInt(data.content.checkPoints, 10),
});
case 'locationCheck':
return updateGameState({
totalChecks: parseInt(data.content.totalChecks, 10),
lastCheck: data.content.lastCheck,
hintPoints: parseInt(data.content.hintPoints, 10),
});
// The client prints several types of messages to the console
case 'critical':
case 'error':
case 'warning':
case 'info':
case 'chat':
return appendMessage(MonitorTools.createTextDiv(
(typeof (data.content) === 'string') ? data.content : JSON.stringify(data.content),
));
default:
console.warn(`Unknown message type received: ${data.type}`);
console.warn(data.content);
return { type: 'NO_OP' };
}
} catch (error) {
console.error(message);
console.error(error);
// The returned value from this function will be dispatched to Redux. If an error occurs,
// Redux and the SPA in general should live on. Dispatching something with the correct format
// but that matches no known Redux action will cause the state to update to itself, which is
// treated as a no-op.
return { type: 'NO_OP' };
}
};
}
export default WebSocketUtils;

View File

@@ -1,28 +0,0 @@
import React from 'react';
import ReactDom from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, combineReducers } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly';
import WebUI from './WebUI/containers/WebUI';
import '../styles/index.scss';
// Redux reducers
import webUI from './WebUI/Redux/reducers/webUIReducer';
import gameState from './global/Redux/reducers/gameStateReducer';
import monitor from './Monitor/Redux/reducers/monitorReducer';
const store = createStore(combineReducers({
webUI,
gameState,
monitor,
}), composeWithDevTools());
const App = () => (
<Provider store={ store }>
<WebUI />
</Provider>
);
window.onload = () => {
ReactDom.render(<App />, document.getElementById('app'));
};

View File

@@ -1,4 +0,0 @@
#header-bar{
font-size: 3.4em;
min-width: 1036px;
}

View File

@@ -1,4 +0,0 @@
#monitor{
flex-grow: 1;
min-width: 800px;
}

View File

@@ -1,43 +0,0 @@
#monitor-controls{
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 0.5em;
padding-bottom: 0.5em;
#connection-status{
display: flex;
flex-direction: row;
#snes-connection, #server-connection{
margin-right: 1em;
table{
td{
padding-right: 0.5em;
}
}
}
.connected{
color: #008000;
}
.not-connected{
color: #ff0000;
}
button {
border-radius: 3px;
}
}
#accessibility{
display: flex;
flex-direction: column;
button{
border-radius: 4px;
margin: 0.5em;
}
}
}

View File

@@ -1,55 +0,0 @@
#monitor-window-wrapper{
#monitor-window{
display: flex;
flex-direction: column;
justify-content: flex-start;
background-color: #414042;
color: #dce7df;
overflow-y: auto;
margin-bottom: 10px;
div{
width: calc(100% - 14px);
padding: 7px;
border-bottom: 1px solid #000000;
&.user-command{
color: #ffffff;
background-color: #575757;
}
}
&.relevant-only{
div:not(.relevant){
visibility: collapse;
}
}
.item-span{
color: #67e9ff;
&.unique{
color: #ff884e;
text-shadow: #000000 2px 2px;
}
}
.location-span{ color: #f5e63c; }
.entrance-span{ color: #73ae38; }
.finder-span{ color: #f96cb8; }
.recipient-span{ color: #9b8aff; }
.mine{ color: #ffa500; }
}
#command-wrapper{
display: flex;
flex-direction: row;
justify-content: flex-start;
height: 25px;
line-height: 25px;
input{
margin-left: 0.5em;
flex-grow: 1;
}
}
}

View File

@@ -1,19 +0,0 @@
@font-face{
font-family: HyliaSerif;
src: local('HyliaSerif'), url('../../../assets/HyliaSerif.otf')
}
#web-ui{
width: calc(100% - 1.5em);
padding: 0.75em;
font-family: HyliaSerif, sans-serif;
&.simple-font{
font-family: sans-serif;
}
#content-middle{
display: flex;
flex-direction: row;
}
}

View File

@@ -1,60 +0,0 @@
#widget-area{
margin-left: 0.5em;
margin-right: 0.5em;
padding: 0.25em;
border: 2px solid #6a6a6a;
&:not(.collapsed){
width: calc(20% - 1.5em - 4px);
}
#widget-button-row{
width: 100%;
text-align: right;
.collapse-button{
width: 35px;
}
}
#widget-area-contents{
display: flex;
flex-direction: column;
table{
th{
text-align: left;
}
td{
padding-left: 1em;
}
}
#game-info{
margin-bottom: 1em;
#game-info-title{
display: flex;
flex-direction: row;
justify-content: space-between;
}
}
#check-data{
margin-bottom: 1em;
}
#hint-data{
margin-bottom: 1em;
}
#notes{
display: flex;
flex-direction: column;
textarea{
height: 10em;
}
}
}
}

View File

@@ -1,6 +0,0 @@
body {
background-color: #131313;
color: #eae703;
letter-spacing: 2px;
margin: 0;
}

View File

@@ -1,54 +0,0 @@
const path = require('path');
module.exports = {
mode: 'production',
watch: false,
resolve: {
fallback: {
crypto: require.resolve('crypto-browserify'),
buffer: require.resolve('buffer/'),
stream: require.resolve('stream-browserify'),
},
},
entry: {
index: './src/js/index.js',
},
module: {
rules: [
{
test: /\.(js|jsx|es6)$/,
loader: 'babel-loader',
options: {
compact: true,
minified: true,
},
},
{
test: /\.((css)|(s[a|c]ss))$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' },
{ loader: 'sass-loader' },
],
},
{
test: /\.(otf)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'fonts/',
publicPath: 'assets/fonts/',
},
},
],
},
],
},
output: {
path: `${path.resolve(__dirname)}/public/assets`,
publicPath: '/',
filename: '[name].bundle.js',
},
};

View File

@@ -1,52 +0,0 @@
const path = require('path');
module.exports = {
mode: 'development',
watch: true,
resolve: {
fallback: {
crypto: require.resolve('crypto-browserify'),
},
},
entry: {
index: './src/js/index.js',
},
module: {
rules: [
{
test: /\.(js|jsx|es6)$/,
loader: 'babel-loader',
options: {
compact: false,
minified: false,
},
},
{
test: /\.((css)|(s[a|c]ss))$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' },
{ loader: 'sass-loader' },
],
},
{
test: /\.(otf)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'fonts/',
publicPath: 'assets/fonts/',
},
},
],
},
],
},
output: {
path: `${path.resolve(__dirname)}/public/assets`,
publicPath: '/',
filename: '[name].bundle.js',
},
};