Added rotation handling (beta!) Don't expand them if you don't wish to show them, report any performance issues and such to me

This commit is contained in:
Rawrington 2019-07-12 14:17:13 +01:00
parent 38f3e88f72
commit 64bfab91f5
7 changed files with 206 additions and 121 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "skilldisplay", "name": "skilldisplay",
"version": "0.2.0", "version": "0.3.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"react": "^16.8.6", "react": "^16.8.6",

View File

@ -1,21 +1,30 @@
export default function listenActWebSocket(callback) { const handleCodes = new Set([
const url = new URLSearchParams(window.location.search) '00',
const wsUri = `${url.get('HOST_PORT')}BeforeLogLineRead` || undefined '01',
const ws = new WebSocket(wsUri) '02',
ws.onerror = () => listenActWebSocket() '21',
ws.onmessage = function (e, m) { //PING '22',
if (e.data === '.') return ws.send('.') //PONG '33'
])
const obj = JSON.parse(e.data) export default function listenActWebSocket( callback ) {
if(obj.msgtype === 'SendCharName') const url = new URLSearchParams(window.location.search)
{ const wsUri = `${url.get("HOST_PORT")}BeforeLogLineRead` || undefined
return callback(obj.msg) const ws = new WebSocket(wsUri)
} ws.onerror = () => ws.close()
else if(obj.msgtype === 'Chat') ws.onclose = () => setTimeout(() => { listenActWebSocket( callback ) }, 1000)
{ ws.onmessage = function(e, m) {
const code = obj.msg.substring(0, 2) //first 2 numbers POG if (e.data === ".") return ws.send(".") //PING
if(code === '21' || code === '22') return callback(obj.msg) //NetworkAbility or NetworkAoeAbility const obj = JSON.parse(e.data);
} if (obj.msgtype === "SendCharName") {
} return callback(obj.msg, null)
} else if (obj.msgtype === "Chat") {
const code = obj.msg.substring(0, 2) //first 2 numbers POG
if (handleCodes.has(code)) return callback(obj.msg, code) //NetworkAbility or NetworkAoeAbility
}
}
return ws
} }

View File

@ -26,14 +26,14 @@ const ogcdOverrides = new Set([
114 //bard MB 114 //bard MB
]) ])
export default function Action({ action_id }) { export default function Action({ actionId, additionalClasses }) {
const [apiData, setApiData] = React.useState() const [apiData, setApiData] = React.useState()
React.useEffect(() => { React.useEffect(() => {
let current = true let current = true
void (async () => { void (async () => {
const data = await ( const data = await (
await fetch(`https://xivapi.com/Action/${action_id}`, { mode: 'cors' }) await fetch(`https://xivapi.com/Action/${actionId}`, { mode: 'cors' })
).json() ).json()
if (current) { if (current) {
setApiData(data) setApiData(data)
@ -43,7 +43,7 @@ export default function Action({ action_id }) {
return () => { return () => {
current = false current = false
} }
}, [action_id]) }, [actionId])
if (apiData === undefined || !apiData.Icon) { if (apiData === undefined || !apiData.Icon) {
return null return null
@ -51,7 +51,7 @@ export default function Action({ action_id }) {
return ( return (
<img <img
className={(gcdOverrides.has(action_id) || (!ogcdOverrides.has(action_id) && apiData.ActionCategory.ID !== 4)) ? 'action-icon gcd' : 'action-icon ogcd'} className={(gcdOverrides.has(actionId) || (!ogcdOverrides.has(actionId) && apiData.ActionCategory.ID !== 4)) ? `gcd ${additionalClasses}` : `ogcd ${additionalClasses}`}
src={`https://xivapi.com/${apiData.Icon}`} src={`https://xivapi.com/${apiData.Icon}`}
alt={apiData.Name || ''} alt={apiData.Name || ''}
/> />

View File

@ -2,73 +2,148 @@ import React from 'react'
import listenActWebSocket from './ACTWebsocket' import listenActWebSocket from './ACTWebsocket'
import './css/App.css' import './css/App.css'
import Action from './Action' import Action from './Action'
import RotationContainer from './Rotation'
import ReactDOM from 'react-dom'
class App extends React.Component { export default function App() {
state = { // NOTE: unlike class state, useState doesn't do object merging; instead, it directly holds values
me: 0, const [actionList, setActionList] = React.useState([])
actionlist: [], const [encounterList, setEncounterList] = React.useState([])
actionindex: 1,
lastAddedTimestamp: '',
lastAddedAction: -1,
}
constructor(props) { React.useEffect(() => {
super(props)
listenActWebSocket(this.handleLogEvent.bind(this)) // These values are only used internally by the handler,
} // we don't need to notify React that they were updated,
// or keep their updates synchronized with actionList updates.
//
// This means we don't have to keep them in State (or Reducer)!
let selfId
let lastTimestamp = ''
let lastAction = -1
let currentZone = 'Unknown'
handleLogEvent(data) { // we need keys to persist for each push, even if we shorten the array later,
if(data.charID) { // so we store the key with the action; can't just use array index due to CSS
this.setState({me: data.charID}) let lastKey = 1
return
} //the ME data we need
const me = this.state.me // listenActWebSocket should be changed to return the websocket,
// and this effect should return a function that disconnects the websocket
//
// like "return () => { ws.close() }"
let ws = listenActWebSocket((data, code) => {
const openNewEncounter = (timestamp) => {
setEncounterList(encounterList => {
if(encounterList[0] && encounterList[0].rotation && encounterList[0].rotation.length <= 0) {
encounterList.shift()
}
if(me === 0) return //we need data on the character first encounterList.unshift({
name: currentZone,
rotation: []
})
let log = data.split('|') return encounterList.slice(0,3)
})
}
if(parseInt(log[2],16) !== me) return //we only care about our actions if (data.charID) {
selfId = data.charID
openNewEncounter()
return
}
const action = parseInt(log[4],16) switch(code) {
case '00':
const [, , refCode, , message] = data.split('|')
if(refCode === '0038' && message === 'end') openNewEncounter()
return
case '01':
const [, , , zoneName] = data.split('|')
currentZone = zoneName
return
case '02':
const [, , logCharIdHex] = data.split('|')
selfId = parseInt(logCharIdHex, 16)
openNewEncounter()
return
case '33':
const [, , , controlCode] = data.split('|')
if(controlCode === '40000012' || controlCode === '40000010') openNewEncounter()
return
default:
break
}
if(action <= 8) return //things we don't care about i.e. sprint auto-attacks //if it's not any of these it must be Network(AOE)Ability, the bulk of what we want to handle
if(this.state.lastAddedTimestamp === log[1] && this.state.lastAddedAction === action) return //no double aoe stuff if (selfId === undefined) return
const index = this.state.actionindex const [, logTimestamp, logCharIdHex, , logActionIdHex] = data.split('|')
this.setState((state) => { // microoptimization: since selfId updates way less often,
const actionindex = (state.actionindex >= 32)?1:state.actionindex+1 // save selfId as data.charID.toString(16), that way you don't need to
const lastAddedTimestamp = log[1] // parse logCharIdHex every time
const lastAddedAction = action if (parseInt(logCharIdHex, 16) !== selfId) return
const actionlist = state.actionlist.concat({index,action});
return {actionindex,lastAddedTimestamp,lastAddedAction,actionlist} // we do a mathematical comparison with action though so can't optimize this away
const action = parseInt(logActionIdHex, 16)
if (
action <= 8 ||
(logTimestamp === lastTimestamp && action === lastAction)
)
return
if((Date.now() - Date.parse(lastTimestamp)) > 120000) openNewEncounter()//last action > 120s ago
lastTimestamp = logTimestamp
lastAction = action
const key = (lastKey % 256) + 1
lastKey = key
ReactDOM.unstable_batchedUpdates(() => {
setActionList(actionList => actionList.concat({ action, key }))
setEncounterList(encounterList => {
if(!encounterList[0]) {
encounterList[0] = {
name: currentZone,
rotation: []
}
}
encounterList[0].rotation.push( action )
return encounterList
})
})
// This _probably_ should be done as a separate React.useEffect instead,
// which runs as an effect whenever the value of actionList changes.
// The problem there is, it would have to detect whether the list grew
// since the last time it was called, otherwise it'd react (heh) to its own
// updates.
//
// Easier to pair it with the previous set.
setTimeout(() => {
setActionList(actionList => actionList.slice(1))
}, 10000)
}) })
setTimeout(this.purgeAction.bind(this), 10000) return () => { ws.close() }
} }, [])
purgeAction() { return (
this.setState((state) => { <div className='container'>
const actionlist = state.actionlist.slice(1) <div className='actions'>
{actionList.map(({ action, key }) => (
return {actionlist} <Action key={key} actionId={action} additionalClasses='action-move' />
}) ))}
} </div>
{encounterList.map((encounter, i) => (
render() { <RotationContainer key={i} encounterId={i} name={encounter.name} actionList={encounter.rotation} />
let actions = [] ))}
</div>
for (const action of this.state.actionlist) { )
actions.push(<Action key={action.index} action_id={action.action} />)
}
return <div className="actions">{actions}</div>
}
} }
export default App;

View File

@ -1,25 +1,27 @@
.action-icon { .action-move {
animation-duration: 10s; animation-duration: 10s;
animation-name: action-move; animation-name: action-move;
animation-timing-function: linear; animation-timing-function: linear;
animation-fill-mode: forwards; animation-fill-mode: forwards;
position: absolute; position: absolute;
} }
.gcd { .gcd {
width: 3rem; width: 3rem;
vertical-align: top;
} }
.ogcd { .ogcd {
width: 2rem; width: 2rem;
vertical-align: top;
} }
@keyframes action-move { @keyframes action-move {
from { from {
transform: translateX(calc(100vw - 3rem)); transform: translateX(calc(100vw - 3rem));
} }
to { to {
transform: translateX(-3rem); transform: translateX(-3rem);
} }
} }

View File

@ -1,5 +1,5 @@
.actions { .actions {
margin-top: 1em; padding-top: 1rem;
background: linear-gradient(180deg, background: linear-gradient(180deg,
rgba(0,0,0,0) calc(25% - 1px), rgba(0,0,0,0) calc(25% - 1px),
rgba(255,255,255,0.5) calc(25%), rgba(255,255,255,0.5) calc(25%),
@ -11,13 +11,13 @@
rgba(255,255,255,0.5) calc(75%), rgba(255,255,255,0.5) calc(75%),
rgba(0,0,0,0) calc(75% + 1px) rgba(0,0,0,0) calc(75% + 1px)
); );
height: 3em; height: 3rem;
padding-bottom: 1rem;
position: absolute; background-color: rgba(20, 20, 20, 0.3);
top:0; display: inline-block;
bottom: 0; }
left: 0;
right: 0; .container {
display: flex;
margin: auto; flex-direction: column;
} }

View File

@ -1,16 +1,15 @@
body { body {
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif; sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
font-size: 16px; font-size: 16px;
} }
html { html {
margin: 0; margin: 0;
height: 100vh; height: 100vh;
overflow: hidden; overflow: hidden;
background-color: rgba(20, 20, 20, 0.3);
} }