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) {
if (e.data === ".") return ws.send(".") //PING
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 const code = obj.msg.substring(0, 2) //first 2 numbers POG
if(code === '21' || code === '22') return callback(obj.msg) //NetworkAbility or NetworkAoeAbility 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: '', React.useEffect(() => {
lastAddedAction: -1,
// 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'
// we need keys to persist for each push, even if we shorten the array later,
// so we store the key with the action; can't just use array index due to CSS
let lastKey = 1
// 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()
} }
constructor(props) { encounterList.unshift({
super(props) name: currentZone,
rotation: []
})
listenActWebSocket(this.handleLogEvent.bind(this)) return encounterList.slice(0,3)
})
} }
handleLogEvent(data) { if (data.charID) {
if(data.charID) { selfId = data.charID
this.setState({me: data.charID}) openNewEncounter()
return return
} //the ME data we need }
const me = this.state.me 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(me === 0) return //we need data on the character first //if it's not any of these it must be Network(AOE)Ability, the bulk of what we want to handle
let log = data.split('|') if (selfId === undefined) return
if(parseInt(log[2],16) !== me) return //we only care about our actions const [, logTimestamp, logCharIdHex, , logActionIdHex] = data.split('|')
const action = parseInt(log[4],16) // microoptimization: since selfId updates way less often,
// save selfId as data.charID.toString(16), that way you don't need to
// parse logCharIdHex every time
if (parseInt(logCharIdHex, 16) !== selfId) return
if(action <= 8) return //things we don't care about i.e. sprint auto-attacks // we do a mathematical comparison with action though so can't optimize this away
const action = parseInt(logActionIdHex, 16)
if(this.state.lastAddedTimestamp === log[1] && this.state.lastAddedAction === action) return //no double aoe stuff if (
action <= 8 ||
(logTimestamp === lastTimestamp && action === lastAction)
)
return
const index = this.state.actionindex if((Date.now() - Date.parse(lastTimestamp)) > 120000) openNewEncounter()//last action > 120s ago
this.setState((state) => { lastTimestamp = logTimestamp
const actionindex = (state.actionindex >= 32)?1:state.actionindex+1 lastAction = action
const lastAddedTimestamp = log[1]
const lastAddedAction = action
const actionlist = state.actionlist.concat({index,action});
return {actionindex,lastAddedTimestamp,lastAddedAction,actionlist} 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
})
}) })
setTimeout(this.purgeAction.bind(this), 10000) // 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
purgeAction() { // since the last time it was called, otherwise it'd react (heh) to its own
this.setState((state) => { // updates.
const actionlist = state.actionlist.slice(1) //
// Easier to pair it with the previous set.
return {actionlist} setTimeout(() => {
setActionList(actionList => actionList.slice(1))
}, 10000)
}) })
}
render() { return () => { ws.close() }
let actions = [] }, [])
for (const action of this.state.actionlist) { return (
actions.push(<Action key={action.index} action_id={action.action} />) <div className='container'>
} <div className='actions'>
{actionList.map(({ action, key }) => (
return <div className="actions">{actions}</div> <Action key={key} actionId={action} additionalClasses='action-move' />
} ))}
</div>
{encounterList.map((encounter, i) => (
<RotationContainer key={i} encounterId={i} name={encounter.name} actionList={encounter.rotation} />
))}
</div>
)
} }
export default App;

View File

@ -1,4 +1,4 @@
.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;
@ -8,10 +8,12 @@
.gcd { .gcd {
width: 3rem; width: 3rem;
vertical-align: top;
} }
.ogcd { .ogcd {
width: 2rem; width: 2rem;
vertical-align: top;
} }
@keyframes action-move { @keyframes action-move {

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

@ -12,5 +12,4 @@ html {
margin: 0; margin: 0;
height: 100vh; height: 100vh;
overflow: hidden; overflow: hidden;
background-color: rgba(20, 20, 20, 0.3);
} }