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",
|
"name": "skilldisplay",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^16.8.6",
|
"react": "^16.8.6",
|
||||||
|
@ -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
|
||||||
}
|
}
|
@ -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 || ''}
|
||||||
/>
|
/>
|
||||||
|
177
src/App.js
177
src/App.js
@ -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;
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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;
|
||||||
}
|
}
|
@ -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);
|
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user