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:
parent
38f3e88f72
commit
64bfab91f5
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "skilldisplay",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"react": "^16.8.6",
|
||||
|
@ -1,21 +1,30 @@
|
||||
export default function listenActWebSocket(callback) {
|
||||
const url = new URLSearchParams(window.location.search)
|
||||
const wsUri = `${url.get('HOST_PORT')}BeforeLogLineRead` || undefined
|
||||
const ws = new WebSocket(wsUri)
|
||||
ws.onerror = () => listenActWebSocket()
|
||||
ws.onmessage = function (e, m) { //PING
|
||||
if (e.data === '.') return ws.send('.') //PONG
|
||||
const handleCodes = new Set([
|
||||
'00',
|
||||
'01',
|
||||
'02',
|
||||
'21',
|
||||
'22',
|
||||
'33'
|
||||
])
|
||||
|
||||
const obj = JSON.parse(e.data)
|
||||
if(obj.msgtype === 'SendCharName')
|
||||
{
|
||||
return callback(obj.msg)
|
||||
}
|
||||
else if(obj.msgtype === 'Chat')
|
||||
{
|
||||
const code = obj.msg.substring(0, 2) //first 2 numbers POG
|
||||
export default function listenActWebSocket( callback ) {
|
||||
const url = new URLSearchParams(window.location.search)
|
||||
const wsUri = `${url.get("HOST_PORT")}BeforeLogLineRead` || undefined
|
||||
const ws = new WebSocket(wsUri)
|
||||
ws.onerror = () => ws.close()
|
||||
ws.onclose = () => setTimeout(() => { listenActWebSocket( callback ) }, 1000)
|
||||
ws.onmessage = function(e, m) {
|
||||
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
|
||||
}
|
@ -26,14 +26,14 @@ const ogcdOverrides = new Set([
|
||||
114 //bard MB
|
||||
])
|
||||
|
||||
export default function Action({ action_id }) {
|
||||
export default function Action({ actionId, additionalClasses }) {
|
||||
const [apiData, setApiData] = React.useState()
|
||||
|
||||
React.useEffect(() => {
|
||||
let current = true
|
||||
void (async () => {
|
||||
const data = await (
|
||||
await fetch(`https://xivapi.com/Action/${action_id}`, { mode: 'cors' })
|
||||
await fetch(`https://xivapi.com/Action/${actionId}`, { mode: 'cors' })
|
||||
).json()
|
||||
if (current) {
|
||||
setApiData(data)
|
||||
@ -43,7 +43,7 @@ export default function Action({ action_id }) {
|
||||
return () => {
|
||||
current = false
|
||||
}
|
||||
}, [action_id])
|
||||
}, [actionId])
|
||||
|
||||
if (apiData === undefined || !apiData.Icon) {
|
||||
return null
|
||||
@ -51,7 +51,7 @@ export default function Action({ action_id }) {
|
||||
|
||||
return (
|
||||
<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}`}
|
||||
alt={apiData.Name || ''}
|
||||
/>
|
||||
|
177
src/App.js
177
src/App.js
@ -2,73 +2,148 @@ import React from 'react'
|
||||
import listenActWebSocket from './ACTWebsocket'
|
||||
import './css/App.css'
|
||||
import Action from './Action'
|
||||
import RotationContainer from './Rotation'
|
||||
import ReactDOM from 'react-dom'
|
||||
|
||||
class App extends React.Component {
|
||||
state = {
|
||||
me: 0,
|
||||
actionlist: [],
|
||||
actionindex: 1,
|
||||
lastAddedTimestamp: '',
|
||||
lastAddedAction: -1,
|
||||
}
|
||||
export default function App() {
|
||||
// NOTE: unlike class state, useState doesn't do object merging; instead, it directly holds values
|
||||
const [actionList, setActionList] = React.useState([])
|
||||
const [encounterList, setEncounterList] = React.useState([])
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
React.useEffect(() => {
|
||||
|
||||
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) {
|
||||
if(data.charID) {
|
||||
this.setState({me: data.charID})
|
||||
return
|
||||
} //the ME data we need
|
||||
// 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
|
||||
|
||||
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) => {
|
||||
const actionindex = (state.actionindex >= 32)?1:state.actionindex+1
|
||||
const lastAddedTimestamp = log[1]
|
||||
const lastAddedAction = action
|
||||
const actionlist = state.actionlist.concat({index,action});
|
||||
// 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
|
||||
|
||||
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() {
|
||||
this.setState((state) => {
|
||||
const actionlist = state.actionlist.slice(1)
|
||||
|
||||
return {actionlist}
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
let actions = []
|
||||
|
||||
for (const action of this.state.actionlist) {
|
||||
actions.push(<Action key={action.index} action_id={action.action} />)
|
||||
}
|
||||
|
||||
return <div className="actions">{actions}</div>
|
||||
}
|
||||
return (
|
||||
<div className='container'>
|
||||
<div className='actions'>
|
||||
{actionList.map(({ action, key }) => (
|
||||
<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;
|
||||
|
@ -1,25 +1,27 @@
|
||||
.action-icon {
|
||||
animation-duration: 10s;
|
||||
animation-name: action-move;
|
||||
animation-timing-function: linear;
|
||||
animation-fill-mode: forwards;
|
||||
position: absolute;
|
||||
.action-move {
|
||||
animation-duration: 10s;
|
||||
animation-name: action-move;
|
||||
animation-timing-function: linear;
|
||||
animation-fill-mode: forwards;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.gcd {
|
||||
width: 3rem;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.ogcd {
|
||||
width: 2rem;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
@keyframes action-move {
|
||||
from {
|
||||
transform: translateX(calc(100vw - 3rem));
|
||||
}
|
||||
from {
|
||||
transform: translateX(calc(100vw - 3rem));
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(-3rem);
|
||||
}
|
||||
to {
|
||||
transform: translateX(-3rem);
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
.actions {
|
||||
margin-top: 1em;
|
||||
padding-top: 1rem;
|
||||
background: linear-gradient(180deg,
|
||||
rgba(0,0,0,0) calc(25% - 1px),
|
||||
rgba(255,255,255,0.5) calc(25%),
|
||||
@ -11,13 +11,13 @@
|
||||
rgba(255,255,255,0.5) calc(75%),
|
||||
rgba(0,0,0,0) calc(75% + 1px)
|
||||
);
|
||||
height: 3em;
|
||||
|
||||
position: absolute;
|
||||
top:0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
margin: auto;
|
||||
height: 3rem;
|
||||
padding-bottom: 1rem;
|
||||
background-color: rgba(20, 20, 20, 0.3);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
@ -1,16 +1,15 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
html {
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background-color: rgba(20, 20, 20, 0.3);
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user