feat: major cleanup + death alerts + idle detection + Discord webhooks
Cleanup: - Removed 109 stale asset files from static/assets/ (was 122, now 13) - Removed static/v2/ entirely (was duplicate of root assets) - Removed dead dashboard code: DashboardView, Layout, GlobalStats, CharacterCard, CharacterGrid, VitalBar, TabContainer, CombatTab, RaresTab, MapTab, InventoryTab, global.css, MapTransformContext - Removed recharts dependency (425KB chunk eliminated) - CSS reduced from 17KB to 10KB - Added deploy-frontend.sh script for one-command build+deploy - Updated CLAUDE.md with combat_stats, share_*, dungeon_map events and React frontend architecture Death alerts (frontend + backend): - Frontend: DeathNotification component with red banner + sawtooth sound when vitae goes from 0 to >0 - Backend: detects vitae transition in vitals handler, sends Discord webhook to #aclog with "☠️ CHARACTER died! (vitae: X%)" - Rate-limited: max 1 Discord alert per character per 5 minutes Idle detection (backend): - Background task runs every 60 seconds - Detects: vt_state "default"/"idle" OR kph=0 while in combat/hunt - Sends Discord webhook: "⚠️ CHARACTER appears idle (state: X, KPH: 0)" - Auto-clears alert when character becomes active again - No duplicate alerts for same idle period Discord integration: - DISCORD_ACLOG_WEBHOOK env var for webhook URL - Used by both death alerts and idle detection - Graceful fallback when not configured Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d2c30b610b
commit
adb9d5feab
163 changed files with 2756 additions and 2910 deletions
2538
CUserseriknsourcereposdereth-workspacestyle_old.css
Normal file
2538
CUserseriknsourcereposdereth-workspacestyle_old.css
Normal file
File diff suppressed because it is too large
Load diff
16
deploy-frontend.sh
Normal file
16
deploy-frontend.sh
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#!/bin/bash
|
||||
# Build frontend and deploy to static/ — run from MosswartOverlord root
|
||||
set -e
|
||||
|
||||
echo "Building frontend..."
|
||||
cd frontend && npm run build && cd ..
|
||||
|
||||
echo "Syncing build output to static/..."
|
||||
rm -rf static/assets/
|
||||
cp static/_build/index.html static/index.html
|
||||
cp -r static/_build/assets/ static/assets/
|
||||
cp static/_build/sw.js static/sw.js 2>/dev/null || true
|
||||
rm -rf static/_build/
|
||||
|
||||
echo "Done! $(ls static/assets/ | wc -l) asset files deployed."
|
||||
echo "Run 'git add static/ && git commit && git push' to deploy to server."
|
||||
388
frontend/package-lock.json
generated
388
frontend/package-lock.json
generated
|
|
@ -9,8 +9,7 @@
|
|||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"recharts": "^2.15.3"
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.1.2",
|
||||
|
|
@ -255,15 +254,6 @@
|
|||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.29.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
|
||||
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
||||
|
|
@ -1206,69 +1196,6 @@
|
|||
"@babel/types": "^7.28.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-color": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-ease": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-interpolate": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-color": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-path": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-scale": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-time": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-shape": {
|
||||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
|
||||
"integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-path": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-time": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-timer": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
|
|
@ -1387,15 +1314,6 @@
|
|||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
|
|
@ -1407,129 +1325,9 @@
|
|||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"internmap": "1 - 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-ease": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-format": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
|
||||
"integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-interpolate": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-path": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-scale": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2.10.0 - 3",
|
||||
"d3-format": "1 - 3",
|
||||
"d3-interpolate": "1.2.0 - 3",
|
||||
"d3-time": "2.1.1 - 3",
|
||||
"d3-time-format": "2 - 4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-shape": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-path": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time-format": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-time": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-timer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
|
|
@ -1548,22 +1346,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js-light": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
||||
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dom-helpers": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.335",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.335.tgz",
|
||||
|
|
@ -1623,21 +1405,6 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-equals": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
|
||||
"integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
|
|
@ -1681,19 +1448,11 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/internmap": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
|
|
@ -1722,24 +1481,6 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
|
|
@ -1783,15 +1524,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
|
|
@ -1842,23 +1574,6 @@
|
|||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types/node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.5",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
|
||||
|
|
@ -1882,12 +1597,6 @@
|
|||
"react": "^19.2.5"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||
|
|
@ -1898,69 +1607,6 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-smooth": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
|
||||
"integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-equals": "^5.0.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-transition-group": "^4.4.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-transition-group": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dom-helpers": "^5.0.1",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.6.0",
|
||||
"react-dom": ">=16.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/recharts": {
|
||||
"version": "2.15.4",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz",
|
||||
"integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clsx": "^2.0.0",
|
||||
"eventemitter3": "^4.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"react-is": "^18.3.1",
|
||||
"react-smooth": "^4.0.4",
|
||||
"recharts-scale": "^0.4.4",
|
||||
"tiny-invariant": "^1.3.1",
|
||||
"victory-vendor": "^36.6.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/recharts-scale": {
|
||||
"version": "0.4.5",
|
||||
"resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
|
||||
"integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"decimal.js-light": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
|
||||
|
|
@ -2032,12 +1678,6 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-invariant": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.16",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
||||
|
|
@ -2100,28 +1740,6 @@
|
|||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/victory-vendor": {
|
||||
"version": "36.9.2",
|
||||
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
|
||||
"integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
|
||||
"license": "MIT AND ISC",
|
||||
"dependencies": {
|
||||
"@types/d3-array": "^3.0.3",
|
||||
"@types/d3-ease": "^3.0.0",
|
||||
"@types/d3-interpolate": "^3.0.1",
|
||||
"@types/d3-scale": "^4.0.2",
|
||||
"@types/d3-shape": "^3.1.0",
|
||||
"@types/d3-time": "^3.0.0",
|
||||
"@types/d3-timer": "^3.0.0",
|
||||
"d3-array": "^3.1.6",
|
||||
"d3-ease": "^3.0.1",
|
||||
"d3-interpolate": "^3.0.1",
|
||||
"d3-scale": "^4.0.2",
|
||||
"d3-shape": "^3.1.0",
|
||||
"d3-time": "^3.0.0",
|
||||
"d3-timer": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.4.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
|
||||
|
|
|
|||
|
|
@ -10,8 +10,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"recharts": "^2.15.3"
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.1.2",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import { useEffect } from 'react';
|
||||
import { MapLayout } from './components/map/MapLayout';
|
||||
import { useLiveData } from './hooks/useLiveData';
|
||||
import './styles/global.css';
|
||||
import './styles/map-layout.css';
|
||||
|
||||
export default function App() {
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
import { Layout } from './components/Layout';
|
||||
import { GlobalStats } from './components/GlobalStats';
|
||||
import { CharacterGrid } from './components/CharacterGrid';
|
||||
import { TabContainer } from './components/tabs/TabContainer';
|
||||
import { CombatTab } from './components/tabs/CombatTab';
|
||||
import { RaresTab } from './components/tabs/RaresTab';
|
||||
import { MapTab } from './components/tabs/MapTab';
|
||||
import { InventoryTab } from './components/tabs/InventoryTab';
|
||||
import type { DashboardState } from './hooks/useLiveData';
|
||||
|
||||
interface Props {
|
||||
data: DashboardState;
|
||||
onViewToggle: () => void;
|
||||
}
|
||||
|
||||
export default function DashboardView({ data, onViewToggle }: Props) {
|
||||
const tabs = [
|
||||
{ id: 'combat', label: 'Combat', content: <CombatTab characters={data.characters} /> },
|
||||
{ id: 'rares', label: 'Rares', content: <RaresTab characters={data.characters} totalRares={data.totalRares} totalKills={data.totalKills} recentRares={data.recentRares} /> },
|
||||
{ id: 'map', label: 'Map', content: <MapTab characters={data.characters} /> },
|
||||
{ id: 'inventory', label: 'Inventory', content: <InventoryTab /> },
|
||||
];
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-start', marginBottom: 8 }}>
|
||||
<button onClick={onViewToggle} className="tab-btn">Map View</button>
|
||||
</div>
|
||||
<GlobalStats activeChars={data.characters.size} totalKills={data.totalKills} totalRares={data.totalRares} serverHealth={data.serverHealth} />
|
||||
<CharacterGrid characters={data.characters} />
|
||||
<TabContainer tabs={tabs} />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { VitalBar } from './VitalBar';
|
||||
import type { CharacterState } from '../types';
|
||||
|
||||
interface Props {
|
||||
character: CharacterState;
|
||||
}
|
||||
|
||||
const vtankBadge = (state: string) => {
|
||||
const s = (state || 'idle').toLowerCase();
|
||||
if (s === 'combat' || s === 'hunt') return { label: s === 'combat' ? 'Combat' : 'Hunt', cls: 'badge-combat' };
|
||||
if (s === 'nav' || s === 'navigation') return { label: 'Nav', cls: 'badge-other' };
|
||||
if (s === 'default' || s === 'idle' || s === '') return { label: 'Idle', cls: 'badge-idle' };
|
||||
// Show the actual state name for anything else (e.g. turn_in_quests)
|
||||
return { label: state, cls: 'badge-other' };
|
||||
};
|
||||
|
||||
export const CharacterCard: React.FC<Props> = React.memo(({ character }) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const { telemetry: t, vitals: v, combat: c } = character;
|
||||
const badge = vtankBadge(t?.vt_state ?? '');
|
||||
|
||||
return (
|
||||
<div className="char-card" onClick={() => setExpanded(!expanded)}>
|
||||
<div className="char-header">
|
||||
<span className="char-name">{character.name}</span>
|
||||
<span className={`char-badge ${badge.cls}`}>{badge.label}</span>
|
||||
</div>
|
||||
|
||||
{v ? (
|
||||
<div className="char-vitals">
|
||||
<VitalBar label="HP" current={v.health_current} max={v.health_max}
|
||||
color="linear-gradient(90deg, #ff4444, #ff6666)" bgColor="#330000" />
|
||||
<VitalBar label="ST" current={v.stamina_current} max={v.stamina_max}
|
||||
color="linear-gradient(90deg, #ffaa00, #ffcc44)" bgColor="#331a00" />
|
||||
<VitalBar label="MN" current={v.mana_current} max={v.mana_max}
|
||||
color="linear-gradient(90deg, #4488ff, #66aaff)" bgColor="#001433" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="char-vitals-placeholder">Awaiting vitals...</div>
|
||||
)}
|
||||
|
||||
<div className="char-stats-row">
|
||||
<div className="stat">
|
||||
<span className="stat-value">{t?.kills_per_hour ?? '--'}</span>
|
||||
<span className="stat-label">kills/hr</span>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<span className="stat-value">{t?.kills?.toLocaleString() ?? '--'}</span>
|
||||
<span className="stat-label">kills</span>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<span className="stat-value">{t?.deaths ?? '0'}</span>
|
||||
<span className="stat-label">deaths</span>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<span className="stat-value">{t?.onlinetime?.replace(/^00\./, '') ?? '--'}</span>
|
||||
<span className="stat-label">uptime</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{t && (
|
||||
<div className="char-location">
|
||||
{t.ns?.toFixed(1)}N, {t.ew?.toFixed(1)}E
|
||||
</div>
|
||||
)}
|
||||
|
||||
{expanded && (
|
||||
<div className="char-expanded">
|
||||
{v?.vitae ? <div className="vitae-warn">Vitae: {v.vitae}%</div> : null}
|
||||
<div className="expanded-row">
|
||||
<span>Prismatics: {t?.prismatic_taper_count ?? '--'}</span>
|
||||
<span>Total Deaths: {t?.total_deaths ?? '--'}</span>
|
||||
</div>
|
||||
{c?.session && (
|
||||
<div className="expanded-row">
|
||||
<span>Session Dmg: {c.session.total_damage_given?.toLocaleString()}</span>
|
||||
<span>Session Kills: {c.session.total_kills}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="expanded-row">
|
||||
<span>RAM: {t?.mem_mb ? (t.mem_mb / 1048576).toFixed(0) + ' MB' : '--'}</span>
|
||||
<span>CPU: {t?.cpu_pct?.toFixed(1) ?? '--'}%</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
CharacterCard.displayName = 'CharacterCard';
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { CharacterCard } from './CharacterCard';
|
||||
import type { CharacterState } from '../types';
|
||||
|
||||
interface Props {
|
||||
characters: Map<string, CharacterState>;
|
||||
}
|
||||
|
||||
export const CharacterGrid: React.FC<Props> = ({ characters }) => {
|
||||
const sorted = useMemo(() => {
|
||||
return Array.from(characters.values()).sort((a, b) =>
|
||||
a.name.localeCompare(b.name)
|
||||
);
|
||||
}, [characters]);
|
||||
|
||||
if (sorted.length === 0) {
|
||||
return <div className="grid-empty">No active characters</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="char-grid">
|
||||
{sorted.map(ch => (
|
||||
<CharacterCard key={ch.name} character={ch} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
import React from 'react';
|
||||
import type { ServerHealth } from '../types';
|
||||
|
||||
interface Props {
|
||||
activeChars: number;
|
||||
totalKills: number;
|
||||
totalRares: number;
|
||||
serverHealth: ServerHealth | null;
|
||||
}
|
||||
|
||||
export const GlobalStats: React.FC<Props> = ({ activeChars, totalKills, totalRares, serverHealth }) => {
|
||||
const serverStatus = serverHealth?.status?.toLowerCase() ?? 'unknown';
|
||||
const isOnline = serverStatus === 'online' || serverStatus === 'up';
|
||||
|
||||
return (
|
||||
<div className="global-stats">
|
||||
<div className="global-stat">
|
||||
<span className="global-value">{activeChars}</span>
|
||||
<span className="global-label">Active Characters</span>
|
||||
</div>
|
||||
<div className="global-stat">
|
||||
<span className="global-value">{totalKills.toLocaleString()}</span>
|
||||
<span className="global-label">Total Kills</span>
|
||||
</div>
|
||||
<div className="global-stat">
|
||||
<span className="global-value">{totalRares}</span>
|
||||
<span className="global-label">Total Rares</span>
|
||||
</div>
|
||||
<div className="global-stat">
|
||||
<span className={`server-dot ${isOnline ? 'online' : 'offline'}`} />
|
||||
<span className="global-value">{serverHealth?.latency_ms ?? '--'}ms</span>
|
||||
<span className="global-label">Coldeve</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Layout: React.FC<Props> = ({ children }) => {
|
||||
return (
|
||||
<div className="dashboard">
|
||||
<header className="dashboard-header">
|
||||
<h1 className="dashboard-title">Mosswart Overlord</h1>
|
||||
</header>
|
||||
<main className="dashboard-main">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
current: number;
|
||||
max: number;
|
||||
color: string;
|
||||
bgColor: string;
|
||||
}
|
||||
|
||||
export const VitalBar: React.FC<Props> = React.memo(({ label, current, max, color, bgColor }) => {
|
||||
const pct = max > 0 ? Math.min(100, Math.max(0, (current / max) * 100)) : 0;
|
||||
return (
|
||||
<div className="vital-bar">
|
||||
<span className="vital-label">{label}</span>
|
||||
<div className="vital-track" style={{ backgroundColor: bgColor }}>
|
||||
<div className="vital-fill" style={{ width: `${pct}%`, background: color }} />
|
||||
</div>
|
||||
<span className="vital-text">{current}/{max}</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
VitalBar.displayName = 'VitalBar';
|
||||
71
frontend/src/components/effects/DeathNotification.tsx
Normal file
71
frontend/src/components/effects/DeathNotification.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import React, { useEffect, useState, useRef } from 'react';
|
||||
|
||||
interface DeathAlert {
|
||||
character_name: string;
|
||||
vitae: number;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
deathAlerts: DeathAlert[];
|
||||
}
|
||||
|
||||
interface ActiveNotification {
|
||||
key: number;
|
||||
alert: DeathAlert;
|
||||
exiting: boolean;
|
||||
}
|
||||
|
||||
let deathKey = 0;
|
||||
|
||||
export const DeathNotification: React.FC<Props> = ({ deathAlerts }) => {
|
||||
const [active, setActive] = useState<ActiveNotification[]>([]);
|
||||
const lastCount = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (deathAlerts.length > lastCount.current && lastCount.current > 0) {
|
||||
const newAlerts = deathAlerts.slice(lastCount.current);
|
||||
for (const alert of newAlerts) {
|
||||
const key = ++deathKey;
|
||||
setActive(prev => [...prev, { key, alert, exiting: false }]);
|
||||
// Sound
|
||||
try {
|
||||
const ctx = new AudioContext();
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
osc.connect(gain); gain.connect(ctx.destination);
|
||||
osc.frequency.value = 440; osc.type = 'sawtooth'; gain.gain.value = 0.2;
|
||||
osc.start();
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.8);
|
||||
osc.stop(ctx.currentTime + 0.8);
|
||||
} catch {}
|
||||
// Auto-dismiss after 8s
|
||||
setTimeout(() => {
|
||||
setActive(prev => prev.map(n => n.key === key ? { ...n, exiting: true } : n));
|
||||
setTimeout(() => setActive(prev => prev.filter(n => n.key !== key)), 500);
|
||||
}, 8000);
|
||||
}
|
||||
}
|
||||
lastCount.current = deathAlerts.length;
|
||||
}, [deathAlerts.length]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
if (active.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div style={{ position: 'fixed', top: 70, left: '50%', transform: 'translateX(-50%)', zIndex: 99999, display: 'flex', flexDirection: 'column', gap: 6, pointerEvents: 'none' }}>
|
||||
{active.map(n => (
|
||||
<div key={n.key} style={{
|
||||
background: 'linear-gradient(135deg, #2a0a0a, #1a0000)',
|
||||
border: '2px solid #cc4444',
|
||||
borderRadius: 8, padding: '12px 24px', textAlign: 'center',
|
||||
boxShadow: '0 0 30px rgba(204, 68, 68, 0.3)',
|
||||
animation: n.exiting ? 'ml-notif-out 0.5s ease-in forwards' : 'ml-notif-in 0.5s ease-out',
|
||||
}}>
|
||||
<div style={{ fontSize: '1.2rem', fontWeight: 800, color: '#ff4444' }}>☠️ CHARACTER DIED ☠️</div>
|
||||
<div style={{ fontSize: '1rem', fontWeight: 600, color: '#fff', marginTop: 2 }}>{n.alert.character_name}</div>
|
||||
<div style={{ fontSize: '0.8rem', color: '#c88', marginTop: 2 }}>Vitae: {n.alert.vitae}%</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -5,6 +5,7 @@ import { MapView } from './MapView';
|
|||
import { Sidebar } from './Sidebar';
|
||||
import { WindowRenderer } from '../windows/WindowRenderer';
|
||||
import { RareNotification } from '../effects/RareNotification';
|
||||
import { DeathNotification } from '../effects/DeathNotification';
|
||||
import { usePlayerColors } from '../../hooks/usePlayerColors';
|
||||
import type { DashboardState } from '../../hooks/useLiveData';
|
||||
|
||||
|
|
@ -69,6 +70,7 @@ export const MapLayout: React.FC<Props> = ({ data }) => {
|
|||
equipmentCantrips={data.equipmentCantrips} characterStats={data.characterStats}
|
||||
socket={data.socketRef.current} />
|
||||
<RareNotification recentRares={data.recentRares} />
|
||||
<DeathNotification deathAlerts={data.deathAlerts} />
|
||||
</div>
|
||||
</WindowManagerProvider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,142 +0,0 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import {
|
||||
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
|
||||
PieChart, Pie, Cell, Legend,
|
||||
} from 'recharts';
|
||||
import type { CharacterState } from '../../types';
|
||||
|
||||
interface Props {
|
||||
characters: Map<string, CharacterState>;
|
||||
}
|
||||
|
||||
const ELEMENT_COLORS: Record<string, string> = {
|
||||
Slash: '#cc4444',
|
||||
Pierce: '#44cc44',
|
||||
Bludgeon: '#888888',
|
||||
Fire: '#ff6622',
|
||||
Cold: '#4488ff',
|
||||
Acid: '#44cc44',
|
||||
Electric: '#ffcc00',
|
||||
Typeless: '#aa66cc',
|
||||
};
|
||||
|
||||
export const CombatTab: React.FC<Props> = ({ characters }) => {
|
||||
// Kill rate per character
|
||||
const killData = useMemo(() => {
|
||||
return Array.from(characters.values())
|
||||
.filter(c => c.telemetry)
|
||||
.map(c => ({
|
||||
name: c.name.length > 18 ? c.name.slice(0, 16) + '..' : c.name,
|
||||
fullName: c.name,
|
||||
killsPerHour: parseInt(c.telemetry!.kills_per_hour) || 0,
|
||||
totalKills: c.telemetry!.kills || 0,
|
||||
}))
|
||||
.sort((a, b) => b.killsPerHour - a.killsPerHour)
|
||||
.slice(0, 30);
|
||||
}, [characters]);
|
||||
|
||||
// Damage given per character (from combat stats)
|
||||
const damageData = useMemo(() => {
|
||||
return Array.from(characters.values())
|
||||
.filter(c => c.combat?.session)
|
||||
.map(c => ({
|
||||
name: c.name.length > 18 ? c.name.slice(0, 16) + '..' : c.name,
|
||||
fullName: c.name,
|
||||
damage: c.combat!.session!.total_damage_given,
|
||||
}))
|
||||
.sort((a, b) => b.damage - a.damage)
|
||||
.slice(0, 30);
|
||||
}, [characters]);
|
||||
|
||||
// Aggregate element breakdown across all characters
|
||||
const elementData = useMemo(() => {
|
||||
const totals: Record<string, number> = {};
|
||||
for (const ch of characters.values()) {
|
||||
const session = ch.combat?.session;
|
||||
if (!session?.monsters) continue;
|
||||
for (const mon of Object.values(session.monsters)) {
|
||||
if (!mon.offense) continue;
|
||||
for (const byEl of Object.values(mon.offense)) {
|
||||
for (const [el, stats] of Object.entries(byEl)) {
|
||||
if (el === 'None' || el === 'Unknown') continue;
|
||||
totals[el] = (totals[el] || 0) + (stats.damage || 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Object.entries(totals)
|
||||
.map(([name, value]) => ({ name, value }))
|
||||
.filter(d => d.value > 0)
|
||||
.sort((a, b) => b.value - a.value);
|
||||
}, [characters]);
|
||||
|
||||
return (
|
||||
<div className="combat-tab">
|
||||
<div className="chart-section">
|
||||
<h3 className="chart-title">Kills per Hour</h3>
|
||||
<ResponsiveContainer width="100%" height={Math.max(200, killData.length * 28)}>
|
||||
<BarChart data={killData} layout="vertical" margin={{ left: 10, right: 20, top: 5, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#333" />
|
||||
<XAxis type="number" stroke="#888" fontSize={11} />
|
||||
<YAxis type="category" dataKey="name" width={130} stroke="#888" fontSize={11} />
|
||||
<Tooltip
|
||||
contentStyle={{ background: '#1a1a1a', border: '1px solid #444', fontSize: 12 }}
|
||||
formatter={(v: number) => [v.toLocaleString(), 'Kills/hr']}
|
||||
labelFormatter={(l, payload) => payload?.[0]?.payload?.fullName || l}
|
||||
/>
|
||||
<Bar dataKey="killsPerHour" fill="#44cc44" radius={[0, 3, 3, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{damageData.length > 0 && (
|
||||
<div className="chart-section">
|
||||
<h3 className="chart-title">Total Damage (Session)</h3>
|
||||
<ResponsiveContainer width="100%" height={Math.max(200, damageData.length * 28)}>
|
||||
<BarChart data={damageData} layout="vertical" margin={{ left: 10, right: 20, top: 5, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#333" />
|
||||
<XAxis type="number" stroke="#888" fontSize={11} />
|
||||
<YAxis type="category" dataKey="name" width={130} stroke="#888" fontSize={11} />
|
||||
<Tooltip
|
||||
contentStyle={{ background: '#1a1a1a', border: '1px solid #444', fontSize: 12 }}
|
||||
formatter={(v: number) => [v.toLocaleString(), 'Damage']}
|
||||
labelFormatter={(l, payload) => payload?.[0]?.payload?.fullName || l}
|
||||
/>
|
||||
<Bar dataKey="damage" fill="#ff6644" radius={[0, 3, 3, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{elementData.length > 0 && (
|
||||
<div className="chart-section">
|
||||
<h3 className="chart-title">Damage by Element (All Characters)</h3>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={elementData}
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
outerRadius={100}
|
||||
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
|
||||
labelLine={true}
|
||||
fontSize={12}
|
||||
>
|
||||
{elementData.map((d) => (
|
||||
<Cell key={d.name} fill={ELEMENT_COLORS[d.name] || '#888'} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
contentStyle={{ background: '#1a1a1a', border: '1px solid #444', fontSize: 12 }}
|
||||
formatter={(v: number) => v.toLocaleString()}
|
||||
/>
|
||||
<Legend wrapperStyle={{ fontSize: 12, color: '#aaa' }} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import { apiFetch } from '../../api/client';
|
||||
|
||||
interface SearchResult {
|
||||
character_name: string;
|
||||
item_name: string;
|
||||
type?: string;
|
||||
arcanelore?: string;
|
||||
material?: string;
|
||||
set_name?: string;
|
||||
workmanship?: number;
|
||||
value?: number;
|
||||
}
|
||||
|
||||
interface SearchResponse {
|
||||
results: SearchResult[];
|
||||
total: number;
|
||||
query: string;
|
||||
}
|
||||
|
||||
export const InventoryTab: React.FC = () => {
|
||||
const [query, setQuery] = useState('');
|
||||
const [results, setResults] = useState<SearchResult[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const debounceRef = useRef<number>(0);
|
||||
|
||||
const doSearch = useCallback(async (q: string) => {
|
||||
if (q.length < 2) { setResults([]); setTotal(0); return; }
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await apiFetch<SearchResponse>(`/search/items?q=${encodeURIComponent(q)}&limit=100`);
|
||||
setResults(data.results ?? []);
|
||||
setTotal(data.total ?? 0);
|
||||
} catch {
|
||||
setResults([]);
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
const handleInput = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const val = e.target.value;
|
||||
setQuery(val);
|
||||
clearTimeout(debounceRef.current);
|
||||
debounceRef.current = window.setTimeout(() => doSearch(val), 400);
|
||||
}, [doSearch]);
|
||||
|
||||
return (
|
||||
<div className="inventory-tab">
|
||||
<div className="search-bar">
|
||||
<input
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={handleInput}
|
||||
placeholder="Search items across all characters..."
|
||||
className="search-input"
|
||||
/>
|
||||
{loading && <span className="search-spinner">Searching...</span>}
|
||||
</div>
|
||||
|
||||
{total > 0 && (
|
||||
<div className="search-count">{total.toLocaleString()} results</div>
|
||||
)}
|
||||
|
||||
<div className="search-results">
|
||||
{results.length === 0 && query.length >= 2 && !loading && (
|
||||
<div className="search-empty">No items found</div>
|
||||
)}
|
||||
<table className="results-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Character</th>
|
||||
<th>Item</th>
|
||||
<th>Type</th>
|
||||
<th>Material</th>
|
||||
<th>Set</th>
|
||||
<th>Work</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{results.map((r, i) => (
|
||||
<tr key={i}>
|
||||
<td>{r.character_name}</td>
|
||||
<td className="item-name">{r.item_name}</td>
|
||||
<td>{r.type || ''}</td>
|
||||
<td>{r.material || ''}</td>
|
||||
<td>{r.set_name || ''}</td>
|
||||
<td>{r.workmanship || ''}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
import React, { useMemo, useRef, useState, useCallback } from 'react';
|
||||
import type { CharacterState } from '../../types';
|
||||
|
||||
interface Props {
|
||||
characters: Map<string, CharacterState>;
|
||||
}
|
||||
|
||||
// UtilityBelt's coordinate bounds (matches v1 script.js)
|
||||
const MAP_BOUNDS = { west: -102.1, east: 102.1, north: 102.1, south: -102.1 };
|
||||
const MAP_SIZE = 800; // render size in CSS px
|
||||
|
||||
function coordToPixel(ew: number, ns: number): { x: number; y: number } {
|
||||
const x = ((ew - MAP_BOUNDS.west) / (MAP_BOUNDS.east - MAP_BOUNDS.west)) * MAP_SIZE;
|
||||
const y = ((MAP_BOUNDS.north - ns) / (MAP_BOUNDS.north - MAP_BOUNDS.south)) * MAP_SIZE;
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
export const MapTab: React.FC<Props> = ({ characters }) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [hovered, setHovered] = useState<string | null>(null);
|
||||
|
||||
const dots = useMemo(() => {
|
||||
return Array.from(characters.values())
|
||||
.filter(c => c.telemetry && c.telemetry.ew !== undefined)
|
||||
.map(c => {
|
||||
const t = c.telemetry!;
|
||||
const { x, y } = coordToPixel(t.ew, t.ns);
|
||||
const isHunting = (t.vt_state || '').toLowerCase() === 'combat' ||
|
||||
(t.vt_state || '').toLowerCase() === 'hunt';
|
||||
return { name: c.name, x, y, isHunting, ns: t.ns, ew: t.ew };
|
||||
});
|
||||
}, [characters]);
|
||||
|
||||
const handleDotHover = useCallback((name: string | null) => setHovered(name), []);
|
||||
|
||||
return (
|
||||
<div className="map-tab">
|
||||
<div className="map-container" ref={containerRef}>
|
||||
<img
|
||||
src="/dereth_highres.png"
|
||||
alt="Dereth Map"
|
||||
className="map-image"
|
||||
draggable={false}
|
||||
/>
|
||||
<svg className="map-overlay" viewBox={`0 0 ${MAP_SIZE} ${MAP_SIZE}`}>
|
||||
{dots.map(d => (
|
||||
<g key={d.name}>
|
||||
<circle
|
||||
cx={d.x}
|
||||
cy={d.y}
|
||||
r={hovered === d.name ? 6 : 4}
|
||||
fill={d.isHunting ? '#44cc44' : '#ffaa00'}
|
||||
stroke="#000"
|
||||
strokeWidth={1}
|
||||
opacity={0.9}
|
||||
onMouseEnter={() => handleDotHover(d.name)}
|
||||
onMouseLeave={() => handleDotHover(null)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
{hovered === d.name && (
|
||||
<text
|
||||
x={d.x + 8}
|
||||
y={d.y + 4}
|
||||
fill="#fff"
|
||||
fontSize={11}
|
||||
stroke="#000"
|
||||
strokeWidth={0.3}
|
||||
paintOrder="stroke"
|
||||
>
|
||||
{d.name} ({d.ns?.toFixed(1)}N, {d.ew?.toFixed(1)}E)
|
||||
</text>
|
||||
)}
|
||||
</g>
|
||||
))}
|
||||
</svg>
|
||||
</div>
|
||||
<div className="map-legend">
|
||||
<span><span className="legend-dot hunting" /> Hunting/Combat</span>
|
||||
<span><span className="legend-dot other" /> Other state</span>
|
||||
<span className="map-count">{dots.length} characters on map</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import {
|
||||
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
|
||||
} from 'recharts';
|
||||
import type { CharacterState, RareMessage } from '../../types';
|
||||
|
||||
interface Props {
|
||||
characters: Map<string, CharacterState>;
|
||||
totalRares: number;
|
||||
totalKills: number;
|
||||
recentRares: RareMessage[];
|
||||
}
|
||||
|
||||
export const RaresTab: React.FC<Props> = ({ characters, totalRares, totalKills, recentRares }) => {
|
||||
// Rares per character from telemetry
|
||||
const raresData = useMemo(() => {
|
||||
return Array.from(characters.values())
|
||||
.filter(c => c.telemetry && (c.telemetry.total_rares ?? 0) > 0)
|
||||
.map(c => ({
|
||||
name: c.name.length > 18 ? c.name.slice(0, 16) + '..' : c.name,
|
||||
fullName: c.name,
|
||||
rares: c.telemetry!.total_rares ?? 0,
|
||||
}))
|
||||
.sort((a, b) => b.rares - a.rares);
|
||||
}, [characters]);
|
||||
|
||||
const killsPerRare = totalRares > 0 ? Math.round(totalKills / totalRares) : 0;
|
||||
|
||||
return (
|
||||
<div className="rares-tab">
|
||||
{/* Summary stats */}
|
||||
<div className="rares-summary">
|
||||
<div className="rare-stat-card">
|
||||
<span className="rare-stat-value">{totalRares}</span>
|
||||
<span className="rare-stat-label">Total Rares Found</span>
|
||||
</div>
|
||||
<div className="rare-stat-card">
|
||||
<span className="rare-stat-value">{totalKills.toLocaleString()}</span>
|
||||
<span className="rare-stat-label">Total Kills</span>
|
||||
</div>
|
||||
<div className="rare-stat-card">
|
||||
<span className="rare-stat-value">{killsPerRare > 0 ? `1 in ${killsPerRare.toLocaleString()}` : '--'}</span>
|
||||
<span className="rare-stat-label">Drop Rate</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent rare drops */}
|
||||
{recentRares.length > 0 && (
|
||||
<div className="chart-section">
|
||||
<h3 className="chart-title">Recent Rare Drops (This Session)</h3>
|
||||
<div className="rare-timeline">
|
||||
{recentRares.map((r, i) => (
|
||||
<div key={i} className="rare-event">
|
||||
<span className="rare-time">{new Date(r.timestamp).toLocaleTimeString()}</span>
|
||||
<span className="rare-char">{r.character_name}</span>
|
||||
<span className="rare-name">{r.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Rares per character */}
|
||||
{raresData.length > 0 && (
|
||||
<div className="chart-section">
|
||||
<h3 className="chart-title">Rares per Character (Lifetime)</h3>
|
||||
<ResponsiveContainer width="100%" height={Math.max(200, raresData.length * 28)}>
|
||||
<BarChart data={raresData} layout="vertical" margin={{ left: 10, right: 20, top: 5, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#333" />
|
||||
<XAxis type="number" stroke="#888" fontSize={11} />
|
||||
<YAxis type="category" dataKey="name" width={130} stroke="#888" fontSize={11} />
|
||||
<Tooltip
|
||||
contentStyle={{ background: '#1a1a1a', border: '1px solid #444', fontSize: 12 }}
|
||||
formatter={(v: number) => [v, 'Rares']}
|
||||
labelFormatter={(l, payload) => payload?.[0]?.payload?.fullName || l}
|
||||
/>
|
||||
<Bar dataKey="rares" fill="#ffcc00" radius={[0, 3, 3, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
interface Tab {
|
||||
id: string;
|
||||
label: string;
|
||||
content: React.ReactNode;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
tabs: Tab[];
|
||||
}
|
||||
|
||||
export const TabContainer: React.FC<Props> = ({ tabs }) => {
|
||||
const [activeTab, setActiveTab] = useState(tabs[0]?.id ?? '');
|
||||
|
||||
return (
|
||||
<div className="tab-container">
|
||||
<div className="tab-bar">
|
||||
{tabs.map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
className={`tab-btn ${activeTab === tab.id ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="tab-content">
|
||||
{tabs.find(t => t.id === activeTab)?.content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
import React, { createContext, useContext, useReducer, type Dispatch } from 'react';
|
||||
|
||||
interface MapTransform {
|
||||
scale: number;
|
||||
offX: number;
|
||||
offY: number;
|
||||
}
|
||||
|
||||
type Action =
|
||||
| { type: 'SET'; scale: number; offX: number; offY: number }
|
||||
| { type: 'ZOOM'; factor: number; cx: number; cy: number }
|
||||
| { type: 'PAN'; dx: number; dy: number };
|
||||
|
||||
const MAX_ZOOM = 20;
|
||||
const MIN_ZOOM = 0.3;
|
||||
|
||||
function reducer(state: MapTransform, action: Action): MapTransform {
|
||||
switch (action.type) {
|
||||
case 'SET':
|
||||
return { scale: action.scale, offX: action.offX, offY: action.offY };
|
||||
case 'ZOOM': {
|
||||
const newScale = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, state.scale * action.factor));
|
||||
const ratio = newScale / state.scale;
|
||||
return {
|
||||
scale: newScale,
|
||||
offX: action.cx - (action.cx - state.offX) * ratio,
|
||||
offY: action.cy - (action.cy - state.offY) * ratio,
|
||||
};
|
||||
}
|
||||
case 'PAN':
|
||||
return { ...state, offX: state.offX + action.dx, offY: state.offY + action.dy };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
const Ctx = createContext<{ transform: MapTransform; dispatch: Dispatch<Action> }>({
|
||||
transform: { scale: 1, offX: 0, offY: 0 },
|
||||
dispatch: () => {},
|
||||
});
|
||||
|
||||
export const MapTransformProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [transform, dispatch] = useReducer(reducer, { scale: 1, offX: 0, offY: 0 });
|
||||
return <Ctx.Provider value={{ transform, dispatch }}>{children}</Ctx.Provider>;
|
||||
};
|
||||
|
||||
export const useMapTransform = () => useContext(Ctx);
|
||||
|
|
@ -17,6 +17,7 @@ export interface DashboardState {
|
|||
inventoryVersion: number;
|
||||
equipmentCantrips: Map<string, any>;
|
||||
characterStats: Map<string, any>;
|
||||
deathAlerts: Array<{ character_name: string; vitae: number; timestamp: string }>;
|
||||
socketRef: React.RefObject<WebSocket | null>;
|
||||
}
|
||||
|
||||
|
|
@ -33,6 +34,7 @@ export function useLiveData(): DashboardState {
|
|||
const [equipCantripVersion, setEquipCantripVersion] = useState(0);
|
||||
const characterStatsRef = useRef(new Map<string, any>());
|
||||
const [charStatsVersion, setCharStatsVersion] = useState(0);
|
||||
const [deathAlerts, setDeathAlerts] = useState<Array<{ character_name: string; vitae: number; timestamp: string }>>([]);
|
||||
const [nearbyObjects, setNearbyObjects] = useState<Map<string, any>>(new Map());
|
||||
const charsRef = useRef(characters);
|
||||
charsRef.current = characters;
|
||||
|
|
@ -56,6 +58,11 @@ export function useLiveData(): DashboardState {
|
|||
updateChar(t.character_name, s => ({ ...s, telemetry: t, lastUpdate: Date.now() }));
|
||||
} else if (msg.type === 'vitals') {
|
||||
const v = msg as VitalsMessage;
|
||||
// Detect death: vitae went from 0 to > 0
|
||||
const prev = charsRef.current.get(v.character_name)?.vitals;
|
||||
if (prev && (prev.vitae ?? 0) === 0 && (v.vitae ?? 0) > 0) {
|
||||
setDeathAlerts(a => [...a, { character_name: v.character_name, vitae: v.vitae, timestamp: new Date().toISOString() }].slice(-50));
|
||||
}
|
||||
updateChar(v.character_name, s => ({ ...s, vitals: v, lastUpdate: Date.now() }));
|
||||
} else if (msg.type === 'combat_stats') {
|
||||
const c = msg as CombatStatsMessage;
|
||||
|
|
@ -193,5 +200,5 @@ export function useLiveData(): DashboardState {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const characterStats = useMemo(() => characterStatsRef.current, [charStatsVersion]);
|
||||
|
||||
return { characters, serverHealth, totalRares, totalKills, recentRares, chatMessages, nearbyObjects, inventoryVersion, equipmentCantrips, characterStats, socketRef };
|
||||
return { characters, serverHealth, totalRares, totalKills, recentRares, chatMessages, nearbyObjects, inventoryVersion, equipmentCantrips, characterStats, deathAlerts, socketRef };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,507 +0,0 @@
|
|||
/* ── Reset & Variables ─────────────────────────────────── */
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--bg-body: #0d0d0d;
|
||||
--bg-card: #1a1a1a;
|
||||
--bg-card-hover: #222;
|
||||
--bg-header: #111;
|
||||
--border: #333;
|
||||
--text: #ddd;
|
||||
--text-muted: #888;
|
||||
--text-dim: #555;
|
||||
--accent: #4488ff;
|
||||
--hp: linear-gradient(90deg, #ff4444, #ff6666);
|
||||
--hp-bg: #330000;
|
||||
--sta: linear-gradient(90deg, #ffaa00, #ffcc44);
|
||||
--sta-bg: #331a00;
|
||||
--mana: linear-gradient(90deg, #4488ff, #66aaff);
|
||||
--mana-bg: #001433;
|
||||
--badge-combat: #44cc44;
|
||||
--badge-nav: #ffaa00;
|
||||
--badge-idle: #666;
|
||||
--radius: 6px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: var(--bg-body);
|
||||
color: var(--text);
|
||||
line-height: 1.4;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ── Dashboard Layout ─────────────────────────────────── */
|
||||
.dashboard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
background: var(--bg-header);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 12px 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.dashboard-nav {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
font-size: 0.85rem;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.nav-link:hover { color: var(--text); }
|
||||
|
||||
.dashboard-main {
|
||||
flex: 1;
|
||||
padding: 16px 24px;
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
.dashboard-main::-webkit-scrollbar { display: none; }
|
||||
|
||||
/* ── Global Stats Bar ─────────────────────────────────── */
|
||||
.global-stats {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
padding: 12px 0;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.global-stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.global-value {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.global-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.server-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
.server-dot.online { background: #44cc44; }
|
||||
.server-dot.offline { background: #cc4444; }
|
||||
|
||||
/* ── Character Grid ───────────────────────────────────── */
|
||||
.char-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.grid-empty {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
padding: 48px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* ── Character Card ───────────────────────────────────── */
|
||||
.char-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 12px 14px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
.char-card:hover {
|
||||
background: var(--bg-card-hover);
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
.char-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.char-name {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.char-badge {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.badge-combat { background: rgba(68,204,68,0.2); color: var(--badge-combat); }
|
||||
.badge-nav { background: rgba(255,170,0,0.2); color: var(--badge-nav); }
|
||||
.badge-other { background: rgba(204,68,68,0.2); color: #c44; }
|
||||
.badge-idle { background: rgba(100,100,100,0.2); color: var(--badge-idle); }
|
||||
|
||||
/* ── Vital Bars ───────────────────────────────────────── */
|
||||
.char-vitals {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.char-vitals-placeholder {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 8px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.vital-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.vital-label {
|
||||
font-size: 0.65rem;
|
||||
color: var(--text-muted);
|
||||
width: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.vital-track {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vital-fill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease-out;
|
||||
}
|
||||
|
||||
.vital-text {
|
||||
font-size: 0.65rem;
|
||||
color: var(--text-muted);
|
||||
width: 65px;
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* ── Stats Row ────────────────────────────────────────── */
|
||||
.char-stats-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.6rem;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
/* ── Location ─────────────────────────────────────────── */
|
||||
.char-location {
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-dim);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* ── Expanded Details ─────────────────────────────────── */
|
||||
.char-expanded {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.expanded-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.vitae-warn {
|
||||
color: #ff6666;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* ── Tab Container ────────────────────────────────────── */
|
||||
.tab-container {
|
||||
margin-top: 24px;
|
||||
border-top: 1px solid var(--border);
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 16px;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
padding: 8px 20px;
|
||||
font-size: 0.85rem;
|
||||
background: var(--bg-card);
|
||||
color: var(--text-muted);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius) var(--radius) 0 0;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tab-btn:hover { background: var(--bg-card-hover); color: var(--text); }
|
||||
.tab-btn.active {
|
||||
background: var(--bg-card-hover);
|
||||
color: var(--accent);
|
||||
border-bottom-color: var(--bg-body);
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
/* ── Chart Sections ───────────────────────────────────── */
|
||||
.chart-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* ── Rares Tab ────────────────────────────────────────── */
|
||||
.rares-summary {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.rare-stat-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 12px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.rare-stat-value {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
color: #ffcc00;
|
||||
}
|
||||
|
||||
.rare-stat-label {
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.rare-timeline {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.rare-event {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px solid #222;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.rare-time { color: var(--text-dim); width: 80px; }
|
||||
.rare-char { color: var(--text-muted); width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.rare-name { color: #ffcc00; font-weight: 600; }
|
||||
|
||||
/* ── Map Tab ──────────────────────────────────────────── */
|
||||
.map-tab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.map-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
.map-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.map-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.map-legend {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.legend-dot.hunting { background: #44cc44; }
|
||||
.legend-dot.other { background: #ffaa00; }
|
||||
.map-count { margin-left: auto; }
|
||||
|
||||
/* ── Inventory Tab ────────────────────────────────────── */
|
||||
.inventory-tab { width: 100%; }
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
font-size: 0.9rem;
|
||||
background: var(--bg-card);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
outline: none;
|
||||
}
|
||||
.search-input:focus { border-color: var(--accent); }
|
||||
|
||||
.search-spinner { font-size: 0.75rem; color: var(--text-muted); }
|
||||
.search-count { font-size: 0.75rem; color: var(--text-muted); margin-bottom: 8px; }
|
||||
.search-empty { text-align: center; color: var(--text-dim); padding: 24px; }
|
||||
|
||||
.results-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.results-table th {
|
||||
text-align: left;
|
||||
padding: 6px 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.results-table td {
|
||||
padding: 4px 8px;
|
||||
border-bottom: 1px solid #1a1a1a;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.results-table tr:hover td { background: var(--bg-card-hover); }
|
||||
.item-name { font-weight: 500; }
|
||||
|
||||
/* ── Mobile Responsive ────────────────────────────────── */
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-header {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
}
|
||||
.dashboard-main {
|
||||
padding: 12px 12px;
|
||||
}
|
||||
.global-stats {
|
||||
gap: 16px;
|
||||
}
|
||||
.char-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.char-stats-row {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.dashboard-nav {
|
||||
gap: 10px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.char-card {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
{"root":["./src/app.tsx","./src/dashboardview.tsx","./src/main.tsx","./src/api/client.ts","./src/api/endpoints.ts","./src/components/charactercard.tsx","./src/components/charactergrid.tsx","./src/components/globalstats.tsx","./src/components/layout.tsx","./src/components/vitalbar.tsx","./src/components/effects/rarenotification.tsx","./src/components/map/heatmapcanvas.tsx","./src/components/map/maplayout.tsx","./src/components/map/mapview.tsx","./src/components/map/playerdots.tsx","./src/components/map/portalmarkers.tsx","./src/components/map/sidebar.tsx","./src/components/map/trailssvg.tsx","./src/components/sidebar/playerlist.tsx","./src/components/sidebar/playerrow.tsx","./src/components/sidebar/sidebarwindowbuttons.tsx","./src/components/sidebar/sortbuttons.tsx","./src/components/tabs/combattab.tsx","./src/components/tabs/inventorytab.tsx","./src/components/tabs/maptab.tsx","./src/components/tabs/rarestab.tsx","./src/components/tabs/tabcontainer.tsx","./src/components/windows/characterwindow.tsx","./src/components/windows/chatwindow.tsx","./src/components/windows/combatpickerwindow.tsx","./src/components/windows/combatstatswindow.tsx","./src/components/windows/draggablewindow.tsx","./src/components/windows/inventorywindow.tsx","./src/components/windows/issueswindow.tsx","./src/components/windows/radarwindow.tsx","./src/components/windows/statswindow.tsx","./src/components/windows/vitalsharingwindow.tsx","./src/components/windows/windowrenderer.tsx","./src/contexts/maptransformcontext.tsx","./src/contexts/windowmanagercontext.tsx","./src/hooks/uselivedata.ts","./src/hooks/useplayercolors.ts","./src/hooks/usewebsocket.ts","./src/types/index.ts","./src/utils/coordinates.ts"],"version":"5.8.3"}
|
||||
{"root":["./src/app.tsx","./src/main.tsx","./src/api/client.ts","./src/api/endpoints.ts","./src/components/effects/deathnotification.tsx","./src/components/effects/rarenotification.tsx","./src/components/map/heatmapcanvas.tsx","./src/components/map/maplayout.tsx","./src/components/map/mapview.tsx","./src/components/map/playerdots.tsx","./src/components/map/portalmarkers.tsx","./src/components/map/sidebar.tsx","./src/components/map/trailssvg.tsx","./src/components/sidebar/playerlist.tsx","./src/components/sidebar/playerrow.tsx","./src/components/sidebar/sidebarwindowbuttons.tsx","./src/components/sidebar/sortbuttons.tsx","./src/components/windows/characterwindow.tsx","./src/components/windows/chatwindow.tsx","./src/components/windows/combatpickerwindow.tsx","./src/components/windows/combatstatswindow.tsx","./src/components/windows/draggablewindow.tsx","./src/components/windows/inventorywindow.tsx","./src/components/windows/issueswindow.tsx","./src/components/windows/playerdashboardwindow.tsx","./src/components/windows/queststatuswindow.tsx","./src/components/windows/radarwindow.tsx","./src/components/windows/statswindow.tsx","./src/components/windows/vitalsharingwindow.tsx","./src/components/windows/windowrenderer.tsx","./src/contexts/windowmanagercontext.tsx","./src/hooks/uselivedata.ts","./src/hooks/useplayercolors.ts","./src/hooks/usewebsocket.ts","./src/types/index.ts","./src/utils/coordinates.ts"],"version":"5.8.3"}
|
||||
|
|
@ -5,7 +5,7 @@ export default defineConfig({
|
|||
plugins: [react()],
|
||||
base: '/',
|
||||
build: {
|
||||
outDir: '../static/v2', // still build to v2/ first, then we copy
|
||||
outDir: '../static/_build', // temp dir, deploy script copies to static/
|
||||
emptyOutDir: true,
|
||||
chunkSizeWarningLimit: 300,
|
||||
rollupOptions: {
|
||||
|
|
|
|||
69
main.py
69
main.py
|
|
@ -1236,8 +1236,9 @@ async def on_startup():
|
|||
_rares_cache_task = asyncio.create_task(_refresh_total_rares_cache())
|
||||
_server_health_task = asyncio.create_task(monitor_server_health())
|
||||
_cleanup_task = asyncio.create_task(cleanup_connections_loop())
|
||||
_idle_detection_task = asyncio.create_task(_idle_detection_loop())
|
||||
logger.info(
|
||||
"Background cache refresh, server monitoring, and connection cleanup tasks started"
|
||||
"Background cache refresh, server monitoring, connection cleanup, and idle detection tasks started"
|
||||
)
|
||||
# Seed default users on first run
|
||||
await seed_users()
|
||||
|
|
@ -2491,6 +2492,60 @@ _vital_sharing_peer_state: Dict[str, dict] = {}
|
|||
# --- Combat stats (Mag-Tools style per-character combat tracking) ----------
|
||||
# Latest combat_stats payload per character for real-time display.
|
||||
live_combat_stats: Dict[str, dict] = {}
|
||||
|
||||
# --- Idle detection + Discord alerts ----------
|
||||
DISCORD_ACLOG_WEBHOOK = os.getenv("DISCORD_ACLOG_WEBHOOK", "")
|
||||
_idle_alerted: set[str] = set() # chars we've already alerted for this idle period
|
||||
_death_alerted: Dict[str, float] = {} # char → last death alert timestamp
|
||||
|
||||
|
||||
async def _send_discord_aclog(message: str):
|
||||
"""Send a message to the #aclog Discord channel via webhook."""
|
||||
if not DISCORD_ACLOG_WEBHOOK:
|
||||
return
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
await client.post(DISCORD_ACLOG_WEBHOOK, json={"content": message})
|
||||
except Exception as e:
|
||||
logger.debug(f"Discord webhook failed: {e}")
|
||||
|
||||
|
||||
async def _idle_detection_loop():
|
||||
"""Check for idle/dead characters every 60 seconds and alert via Discord."""
|
||||
await asyncio.sleep(30) # initial delay to let telemetry arrive
|
||||
while True:
|
||||
try:
|
||||
# Check all characters in the live telemetry cache
|
||||
if hasattr(_cached_live, "get"):
|
||||
players = _cached_live.get("players", [])
|
||||
else:
|
||||
players = []
|
||||
|
||||
for p in players:
|
||||
name = p.get("character_name", "")
|
||||
vt_state = (p.get("vt_state") or "idle").lower()
|
||||
kph = int(p.get("kills_per_hour", 0) or 0)
|
||||
|
||||
# Idle = state is "default" or "idle" or KPH is 0 for an active character
|
||||
is_idle = vt_state in ("default", "idle", "") or (
|
||||
vt_state in ("combat", "hunt") and kph == 0
|
||||
)
|
||||
|
||||
if is_idle and name not in _idle_alerted:
|
||||
_idle_alerted.add(name)
|
||||
state_text = p.get("vt_state") or "idle"
|
||||
await _send_discord_aclog(
|
||||
f"⚠️ **{name}** appears idle (state: {state_text}, KPH: {kph})"
|
||||
)
|
||||
logger.info(f"IDLE_ALERT: {name} state={state_text} kph={kph}")
|
||||
elif not is_idle and name in _idle_alerted:
|
||||
# Character recovered — clear alert
|
||||
_idle_alerted.discard(name)
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Idle detection error: {e}")
|
||||
|
||||
await asyncio.sleep(60)
|
||||
_combat_last_session: Dict[str, dict] = {} # key = "char:session_id" → last session snapshot
|
||||
_combat_lifetime_cache: Dict[str, dict] = {} # char → accumulated lifetime stats
|
||||
|
||||
|
|
@ -3197,6 +3252,18 @@ async def ws_receive_snapshots(
|
|||
payload.pop("type", None)
|
||||
try:
|
||||
vitals_msg = VitalsMessage.parse_obj(payload)
|
||||
# Detect death: vitae went from 0 to > 0
|
||||
prev = live_vitals.get(vitals_msg.character_name, {})
|
||||
prev_vitae = prev.get("vitae", 0) or 0
|
||||
new_vitae = vitals_msg.vitae or 0
|
||||
if prev_vitae == 0 and new_vitae > 0:
|
||||
now = asyncio.get_event_loop().time()
|
||||
last_alert = _death_alerted.get(vitals_msg.character_name, 0)
|
||||
if now - last_alert > 300: # max 1 death alert per 5 min per char
|
||||
_death_alerted[vitals_msg.character_name] = now
|
||||
asyncio.create_task(_send_discord_aclog(
|
||||
f"☠️ **{vitals_msg.character_name}** died! (vitae: {new_vitae}%)"
|
||||
))
|
||||
live_vitals[vitals_msg.character_name] = vitals_msg.dict()
|
||||
await _broadcast_to_browser_clients(data)
|
||||
logger.debug(
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
import{u as c,j as r,D as d}from"./index-CLqPZs7-.js";import"./react-DlyoauG8.js";const p=({id:n,zIndex:i,characters:a})=>{const{openWindow:s}=c(),e=Array.from(a.keys()).sort();return r.jsx(d,{id:n,title:"Combat Stats — Select Character",zIndex:i,width:300,height:400,children:r.jsx("div",{style:{flex:1,overflowY:"auto",padding:6},children:e.length===0?r.jsx("div",{style:{padding:12,color:"#666",textAlign:"center",fontSize:"0.8rem"},children:"No characters online"}):e.map(o=>r.jsx("div",{style:{padding:"5px 8px",cursor:"pointer",borderBottom:"1px solid #222",color:"#ccc",fontSize:"0.82rem"},onMouseEnter:t=>t.currentTarget.style.background="#2a2a2a",onMouseLeave:t=>t.currentTarget.style.background="",onClick:()=>s(`combat-${o}`,`Combat: ${o}`,o),children:o},o))})})};export{p as CombatPickerWindow};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{u as c,j as r,D as d}from"./index-DzGubmvT.js";import"./react-DlyoauG8.js";const p=({id:n,zIndex:i,characters:a})=>{const{openWindow:s}=c(),e=Array.from(a.keys()).sort();return r.jsx(d,{id:n,title:"Combat Stats — Select Character",zIndex:i,width:300,height:400,children:r.jsx("div",{style:{flex:1,overflowY:"auto",padding:6},children:e.length===0?r.jsx("div",{style:{padding:12,color:"#666",textAlign:"center",fontSize:"0.8rem"},children:"No characters online"}):e.map(o=>r.jsx("div",{style:{padding:"5px 8px",cursor:"pointer",borderBottom:"1px solid #222",color:"#ccc",fontSize:"0.82rem"},onMouseEnter:t=>t.currentTarget.style.background="#2a2a2a",onMouseLeave:t=>t.currentTarget.style.background="",onClick:()=>s(`combat-${o}`,`Combat: ${o}`,o),children:o},o))})})};export{p as CombatPickerWindow};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{u as c,j as r,D as d}from"./index-BNR09N5o.js";import"./react-DlyoauG8.js";const p=({id:n,zIndex:i,characters:a})=>{const{openWindow:s}=c(),e=Array.from(a.keys()).sort();return r.jsx(d,{id:n,title:"Combat Stats — Select Character",zIndex:i,width:300,height:400,children:r.jsx("div",{style:{flex:1,overflowY:"auto",padding:6},children:e.length===0?r.jsx("div",{style:{padding:12,color:"#666",textAlign:"center",fontSize:"0.8rem"},children:"No characters online"}):e.map(o=>r.jsx("div",{style:{padding:"5px 8px",cursor:"pointer",borderBottom:"1px solid #222",color:"#ccc",fontSize:"0.82rem"},onMouseEnter:t=>t.currentTarget.style.background="#2a2a2a",onMouseLeave:t=>t.currentTarget.style.background="",onClick:()=>s(`combat-${o}`,`Combat: ${o}`,o),children:o},o))})})};export{p as CombatPickerWindow};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{u as c,j as r,D as d}from"./index-BZJ3WwmC.js";import"./react-DlyoauG8.js";const p=({id:n,zIndex:i,characters:a})=>{const{openWindow:s}=c(),e=Array.from(a.keys()).sort();return r.jsx(d,{id:n,title:"Combat Stats — Select Character",zIndex:i,width:300,height:400,children:r.jsx("div",{style:{flex:1,overflowY:"auto",padding:6},children:e.length===0?r.jsx("div",{style:{padding:12,color:"#666",textAlign:"center",fontSize:"0.8rem"},children:"No characters online"}):e.map(o=>r.jsx("div",{style:{padding:"5px 8px",cursor:"pointer",borderBottom:"1px solid #222",color:"#ccc",fontSize:"0.82rem"},onMouseEnter:t=>t.currentTarget.style.background="#2a2a2a",onMouseLeave:t=>t.currentTarget.style.background="",onClick:()=>s(`combat-${o}`,`Combat: ${o}`,o),children:o},o))})})};export{p as CombatPickerWindow};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{u as c,j as r,D as d}from"./index-DnlpBAAU.js";import"./react-DlyoauG8.js";const p=({id:n,zIndex:i,characters:a})=>{const{openWindow:s}=c(),e=Array.from(a.keys()).sort();return r.jsx(d,{id:n,title:"Combat Stats — Select Character",zIndex:i,width:300,height:400,children:r.jsx("div",{style:{flex:1,overflowY:"auto",padding:6},children:e.length===0?r.jsx("div",{style:{padding:12,color:"#666",textAlign:"center",fontSize:"0.8rem"},children:"No characters online"}):e.map(o=>r.jsx("div",{style:{padding:"5px 8px",cursor:"pointer",borderBottom:"1px solid #222",color:"#ccc",fontSize:"0.82rem"},onMouseEnter:t=>t.currentTarget.style.background="#2a2a2a",onMouseLeave:t=>t.currentTarget.style.background="",onClick:()=>s(`combat-${o}`,`Combat: ${o}`,o),children:o},o))})})};export{p as CombatPickerWindow};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{u as c,j as r,D as d}from"./index-D34zgfM7.js";import"./react-DlyoauG8.js";const p=({id:n,zIndex:i,characters:a})=>{const{openWindow:s}=c(),e=Array.from(a.keys()).sort();return r.jsx(d,{id:n,title:"Combat Stats — Select Character",zIndex:i,width:300,height:400,children:r.jsx("div",{style:{flex:1,overflowY:"auto",padding:6},children:e.length===0?r.jsx("div",{style:{padding:12,color:"#666",textAlign:"center",fontSize:"0.8rem"},children:"No characters online"}):e.map(o=>r.jsx("div",{style:{padding:"5px 8px",cursor:"pointer",borderBottom:"1px solid #222",color:"#ccc",fontSize:"0.82rem"},onMouseEnter:t=>t.currentTarget.style.background="#2a2a2a",onMouseLeave:t=>t.currentTarget.style.background="",onClick:()=>s(`combat-${o}`,`Combat: ${o}`,o),children:o},o))})})};export{p as CombatPickerWindow};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{u as c,j as r,D as d}from"./index-ahPE6Fpe.js";import"./react-DlyoauG8.js";const p=({id:n,zIndex:i,characters:a})=>{const{openWindow:s}=c(),e=Array.from(a.keys()).sort();return r.jsx(d,{id:n,title:"Combat Stats — Select Character",zIndex:i,width:300,height:400,children:r.jsx("div",{style:{flex:1,overflowY:"auto",padding:6},children:e.length===0?r.jsx("div",{style:{padding:12,color:"#666",textAlign:"center",fontSize:"0.8rem"},children:"No characters online"}):e.map(o=>r.jsx("div",{style:{padding:"5px 8px",cursor:"pointer",borderBottom:"1px solid #222",color:"#ccc",fontSize:"0.82rem"},onMouseEnter:t=>t.currentTarget.style.background="#2a2a2a",onMouseLeave:t=>t.currentTarget.style.background="",onClick:()=>s(`combat-${o}`,`Combat: ${o}`,o),children:o},o))})})};export{p as CombatPickerWindow};
|
||||
|
|
@ -1 +1 @@
|
|||
import{u as c,j as r,D as d}from"./index-BiGwMY76.js";import"./react-yfL0ty4i.js";const p=({id:n,zIndex:i,characters:a})=>{const{openWindow:s}=c(),e=Array.from(a.keys()).sort();return r.jsx(d,{id:n,title:"Combat Stats — Select Character",zIndex:i,width:300,height:400,children:r.jsx("div",{style:{flex:1,overflowY:"auto",padding:6},children:e.length===0?r.jsx("div",{style:{padding:12,color:"#666",textAlign:"center",fontSize:"0.8rem"},children:"No characters online"}):e.map(o=>r.jsx("div",{style:{padding:"5px 8px",cursor:"pointer",borderBottom:"1px solid #222",color:"#ccc",fontSize:"0.82rem"},onMouseEnter:t=>t.currentTarget.style.background="#2a2a2a",onMouseLeave:t=>t.currentTarget.style.background="",onClick:()=>s(`combat-${o}`,`Combat: ${o}`,o),children:o},o))})})};export{p as CombatPickerWindow};
|
||||
import{u as c,j as r,D as d}from"./index-CLr4QmRO.js";import"./react-yfL0ty4i.js";const p=({id:n,zIndex:i,characters:a})=>{const{openWindow:s}=c(),e=Array.from(a.keys()).sort();return r.jsx(d,{id:n,title:"Combat Stats — Select Character",zIndex:i,width:300,height:400,children:r.jsx("div",{style:{flex:1,overflowY:"auto",padding:6},children:e.length===0?r.jsx("div",{style:{padding:12,color:"#666",textAlign:"center",fontSize:"0.8rem"},children:"No characters online"}):e.map(o=>r.jsx("div",{style:{padding:"5px 8px",cursor:"pointer",borderBottom:"1px solid #222",color:"#ccc",fontSize:"0.82rem"},onMouseEnter:t=>t.currentTarget.style.background="#2a2a2a",onMouseLeave:t=>t.currentTarget.style.background="",onClick:()=>s(`combat-${o}`,`Combat: ${o}`,o),children:o},o))})})};export{p as CombatPickerWindow};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{u as c,j as r,D as d}from"./index-D3oSGfL8.js";import"./react-DlyoauG8.js";const p=({id:n,zIndex:i,characters:a})=>{const{openWindow:s}=c(),e=Array.from(a.keys()).sort();return r.jsx(d,{id:n,title:"Combat Stats — Select Character",zIndex:i,width:300,height:400,children:r.jsx("div",{style:{flex:1,overflowY:"auto",padding:6},children:e.length===0?r.jsx("div",{style:{padding:12,color:"#666",textAlign:"center",fontSize:"0.8rem"},children:"No characters online"}):e.map(o=>r.jsx("div",{style:{padding:"5px 8px",cursor:"pointer",borderBottom:"1px solid #222",color:"#ccc",fontSize:"0.82rem"},onMouseEnter:t=>t.currentTarget.style.background="#2a2a2a",onMouseLeave:t=>t.currentTarget.style.background="",onClick:()=>s(`combat-${o}`,`Combat: ${o}`,o),children:o},o))})})};export{p as CombatPickerWindow};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{u as c,j as r,D as d}from"./index-CHvBDIXj.js";import"./react-DlyoauG8.js";const p=({id:n,zIndex:i,characters:a})=>{const{openWindow:s}=c(),e=Array.from(a.keys()).sort();return r.jsx(d,{id:n,title:"Combat Stats — Select Character",zIndex:i,width:300,height:400,children:r.jsx("div",{style:{flex:1,overflowY:"auto",padding:6},children:e.length===0?r.jsx("div",{style:{padding:12,color:"#666",textAlign:"center",fontSize:"0.8rem"},children:"No characters online"}):e.map(o=>r.jsx("div",{style:{padding:"5px 8px",cursor:"pointer",borderBottom:"1px solid #222",color:"#ccc",fontSize:"0.82rem"},onMouseEnter:t=>t.currentTarget.style.background="#2a2a2a",onMouseLeave:t=>t.currentTarget.style.background="",onClick:()=>s(`combat-${o}`,`Combat: ${o}`,o),children:o},o))})})};export{p as CombatPickerWindow};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{u as c,j as r,D as d}from"./index-fhoLMRWN.js";import"./react-yfL0ty4i.js";const p=({id:n,zIndex:i,characters:a})=>{const{openWindow:s}=c(),e=Array.from(a.keys()).sort();return r.jsx(d,{id:n,title:"Combat Stats — Select Character",zIndex:i,width:300,height:400,children:r.jsx("div",{style:{flex:1,overflowY:"auto",padding:6},children:e.length===0?r.jsx("div",{style:{padding:12,color:"#666",textAlign:"center",fontSize:"0.8rem"},children:"No characters online"}):e.map(o=>r.jsx("div",{style:{padding:"5px 8px",cursor:"pointer",borderBottom:"1px solid #222",color:"#ccc",fontSize:"0.82rem"},onMouseEnter:t=>t.currentTarget.style.background="#2a2a2a",onMouseLeave:t=>t.currentTarget.style.background="",onClick:()=>s(`combat-${o}`,`Combat: ${o}`,o),children:o},o))})})};export{p as CombatPickerWindow};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
import{r as d,j as t,D as y}from"./index-D3oSGfL8.js";import"./react-DlyoauG8.js";const b=({id:m,zIndex:u,characters:h})=>{const[o,k]=d.useState("kph"),[c,x]=d.useState(!1),p=d.useMemo(()=>{const e=Array.from(h.values()).filter(s=>s.telemetry).map(s=>{var l,g;const r=s.telemetry;return{name:s.name,kills:r.kills??0,kph:parseInt(r.kills_per_hour)||0,totalKills:r.total_kills??0,rares:r.total_rares??0,sessionRares:r.session_rares??0,deaths:parseInt(r.deaths)||0,totalDeaths:parseInt(r.total_deaths)||0,uptime:((l=r.onlinetime)==null?void 0:l.replace(/^00\./,""))??"",state:r.vt_state??"idle",tapers:parseInt(r.prismatic_taper_count)||0,hp:((g=s.vitals)==null?void 0:g.health_percentage)??0}});return e.sort((s,r)=>{let l=0;switch(o){case"name":l=s.name.localeCompare(r.name);break;case"kills":l=s.kills-r.kills;break;case"kph":l=s.kph-r.kph;break;case"rares":l=s.rares-r.rares;break;case"deaths":l=s.totalDeaths-r.totalDeaths;break;case"uptime":l=s.uptime.localeCompare(r.uptime);break;case"state":l=s.state.localeCompare(r.state);break}return c?l:-l}),e},[h,o,c]),a=e=>{o===e?x(!c):(k(e),x(!1))},i=e=>({padding:"4px 6px",cursor:"pointer",userSelect:"none",color:o===e?"#6af":"#888",fontSize:"0.65rem",fontWeight:600,whiteSpace:"nowrap",borderBottom:"1px solid #444"}),n=e=>o===e?c?" ▲":" ▼":"";return t.jsx(y,{id:m,title:"Player Dashboard",zIndex:u,width:850,height:500,children:t.jsxs("div",{style:{flex:1,overflow:"auto",fontSize:"0.73rem"},children:[t.jsxs("table",{style:{width:"100%",borderCollapse:"collapse"},children:[t.jsx("thead",{children:t.jsxs("tr",{style:{position:"sticky",top:0,background:"#1a1a1a",zIndex:1},children:[t.jsxs("th",{style:{...i("name"),textAlign:"left"},onClick:()=>a("name"),children:["Character",n("name")]}),t.jsxs("th",{style:{...i("state"),textAlign:"center"},onClick:()=>a("state"),children:["State",n("state")]}),t.jsxs("th",{style:{...i("kph"),textAlign:"right"},onClick:()=>a("kph"),children:["KPH",n("kph")]}),t.jsxs("th",{style:{...i("kills"),textAlign:"right"},onClick:()=>a("kills"),children:["Session",n("kills")]}),t.jsx("th",{style:{textAlign:"right",padding:"4px 6px",color:"#888",fontSize:"0.65rem",fontWeight:600,borderBottom:"1px solid #444"},children:"Total"}),t.jsxs("th",{style:{...i("rares"),textAlign:"right"},onClick:()=>a("rares"),children:["Rares",n("rares")]}),t.jsxs("th",{style:{...i("deaths"),textAlign:"right"},onClick:()=>a("deaths"),children:["Deaths",n("deaths")]}),t.jsxs("th",{style:{...i("uptime"),textAlign:"right"},onClick:()=>a("uptime"),children:["Uptime",n("uptime")]}),t.jsx("th",{style:{textAlign:"right",padding:"4px 6px",color:"#888",fontSize:"0.65rem",fontWeight:600,borderBottom:"1px solid #444"},children:"HP%"}),t.jsx("th",{style:{textAlign:"right",padding:"4px 6px",color:"#888",fontSize:"0.65rem",fontWeight:600,borderBottom:"1px solid #444"},children:"Tapers"})]})}),t.jsx("tbody",{children:p.map(e=>{const s=e.state.toLowerCase(),r=s==="combat"||s==="hunt";return t.jsxs("tr",{style:{borderBottom:"1px solid #1a1a1a"},children:[t.jsx("td",{style:{padding:"3px 6px",color:"#ccc",fontWeight:500,maxWidth:180,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:e.name}),t.jsx("td",{style:{textAlign:"center",padding:"3px 6px"},children:t.jsx("span",{style:{fontSize:"0.6rem",padding:"1px 6px",borderRadius:3,background:r?"rgba(68,204,68,0.15)":s==="idle"||s==="default"?"rgba(100,100,100,0.2)":"rgba(204,68,68,0.15)",color:r?"#4c4":s==="idle"||s==="default"?"#888":"#c44"},children:e.state})}),t.jsx("td",{style:{textAlign:"right",padding:"3px 6px",color:"#4c4",fontVariantNumeric:"tabular-nums"},children:e.kph.toLocaleString()}),t.jsx("td",{style:{textAlign:"right",padding:"3px 6px",color:"#ccc",fontVariantNumeric:"tabular-nums"},children:e.kills.toLocaleString()}),t.jsx("td",{style:{textAlign:"right",padding:"3px 6px",color:"#888",fontVariantNumeric:"tabular-nums"},children:e.totalKills.toLocaleString()}),t.jsxs("td",{style:{textAlign:"right",padding:"3px 6px",color:"#fc0",fontVariantNumeric:"tabular-nums"},children:[e.rares,e.sessionRares>0?` (${e.sessionRares})`:""]}),t.jsx("td",{style:{textAlign:"right",padding:"3px 6px",color:e.totalDeaths>0?"#c66":"#555",fontVariantNumeric:"tabular-nums"},children:e.totalDeaths}),t.jsx("td",{style:{textAlign:"right",padding:"3px 6px",color:"#888",fontVariantNumeric:"tabular-nums"},children:e.uptime}),t.jsxs("td",{style:{textAlign:"right",padding:"3px 6px",fontVariantNumeric:"tabular-nums",color:e.hp>80?"#4c4":e.hp>40?"#ca0":"#c44"},children:[e.hp.toFixed(0),"%"]}),t.jsx("td",{style:{textAlign:"right",padding:"3px 6px",color:"#888",fontVariantNumeric:"tabular-nums"},children:e.tapers.toLocaleString()})]},e.name)})})]}),p.length===0&&t.jsx("div",{style:{padding:20,color:"#666",textAlign:"center"},children:"No characters online"})]})})};export{b as PlayerDashboardWindow};
|
||||
|
|
@ -1 +1 @@
|
|||
import{r as c,j as t,D as u,a as f}from"./index-fhoLMRWN.js";import"./react-yfL0ty4i.js";const j=({id:p,zIndex:x})=>{const[s,h]=c.useState(null);c.useEffect(()=>{const e=async()=>{try{h(await f("/quest-status"))}catch{}};e();const o=setInterval(e,3e4);return()=>clearInterval(o)},[]);const i=s?Object.keys(s.quest_data).sort():[],l=new Set;if(s)for(const e of Object.values(s.quest_data))for(const o of Object.keys(e))l.add(o);const n=Array.from(l).sort();return t.jsx(u,{id:p,title:"Quest Status",zIndex:x,width:780,height:500,children:t.jsx("div",{style:{flex:1,overflow:"auto",fontSize:"0.72rem"},children:s?i.length===0?t.jsx("div",{style:{padding:20,color:"#666",textAlign:"center"},children:"No quest data available"}):t.jsxs("table",{style:{width:"100%",borderCollapse:"collapse"},children:[t.jsx("thead",{children:t.jsxs("tr",{style:{position:"sticky",top:0,background:"#1a1a1a",zIndex:1},children:[t.jsx("th",{style:{textAlign:"left",padding:"4px 8px",borderBottom:"1px solid #444",color:"#888",fontSize:"0.65rem",fontWeight:600,minWidth:140},children:"Character"}),n.map(e=>t.jsx("th",{style:{textAlign:"center",padding:"4px 6px",borderBottom:"1px solid #444",color:"#888",fontSize:"0.6rem",fontWeight:600,maxWidth:120,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},title:e,children:e.replace(" Timer","").replace(" Pickup","")},e))]})}),t.jsx("tbody",{children:i.map(e=>{const o=s.quest_data[e]||{};return t.jsxs("tr",{style:{borderBottom:"1px solid #222"},children:[t.jsx("td",{style:{padding:"3px 8px",color:"#ccc",fontWeight:500,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis",maxWidth:160},children:e}),n.map(d=>{const r=o[d],a=r==="READY";return t.jsx("td",{style:{textAlign:"center",padding:"3px 6px",color:a?"#4c4":r?"#ca0":"#333",fontWeight:a?600:400,fontSize:a?"0.7rem":"0.68rem"},children:r||"—"},d)})]},e)})})]}):t.jsx("div",{style:{padding:20,color:"#666",textAlign:"center"},children:"Loading quest data..."})})})};export{j as QuestStatusWindow};
|
||||
import{r as c,j as t,D as u,a as f}from"./index-CLr4QmRO.js";import"./react-yfL0ty4i.js";const j=({id:p,zIndex:x})=>{const[s,h]=c.useState(null);c.useEffect(()=>{const e=async()=>{try{h(await f("/quest-status"))}catch{}};e();const o=setInterval(e,3e4);return()=>clearInterval(o)},[]);const i=s?Object.keys(s.quest_data).sort():[],l=new Set;if(s)for(const e of Object.values(s.quest_data))for(const o of Object.keys(e))l.add(o);const n=Array.from(l).sort();return t.jsx(u,{id:p,title:"Quest Status",zIndex:x,width:780,height:500,children:t.jsx("div",{style:{flex:1,overflow:"auto",fontSize:"0.72rem"},children:s?i.length===0?t.jsx("div",{style:{padding:20,color:"#666",textAlign:"center"},children:"No quest data available"}):t.jsxs("table",{style:{width:"100%",borderCollapse:"collapse"},children:[t.jsx("thead",{children:t.jsxs("tr",{style:{position:"sticky",top:0,background:"#1a1a1a",zIndex:1},children:[t.jsx("th",{style:{textAlign:"left",padding:"4px 8px",borderBottom:"1px solid #444",color:"#888",fontSize:"0.65rem",fontWeight:600,minWidth:140},children:"Character"}),n.map(e=>t.jsx("th",{style:{textAlign:"center",padding:"4px 6px",borderBottom:"1px solid #444",color:"#888",fontSize:"0.6rem",fontWeight:600,maxWidth:120,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},title:e,children:e.replace(" Timer","").replace(" Pickup","")},e))]})}),t.jsx("tbody",{children:i.map(e=>{const o=s.quest_data[e]||{};return t.jsxs("tr",{style:{borderBottom:"1px solid #222"},children:[t.jsx("td",{style:{padding:"3px 8px",color:"#ccc",fontWeight:500,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis",maxWidth:160},children:e}),n.map(d=>{const r=o[d],a=r==="READY";return t.jsx("td",{style:{textAlign:"center",padding:"3px 6px",color:a?"#4c4":r?"#ca0":"#333",fontWeight:a?600:400,fontSize:a?"0.7rem":"0.68rem"},children:r||"—"},d)})]},e)})})]}):t.jsx("div",{style:{padding:20,color:"#666",textAlign:"center"},children:"Loading quest data..."})})})};export{j as QuestStatusWindow};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{r as c,j as t,D as u,a as f}from"./index-BiGwMY76.js";import"./react-yfL0ty4i.js";const j=({id:p,zIndex:x})=>{const[s,h]=c.useState(null);c.useEffect(()=>{const e=async()=>{try{h(await f("/quest-status"))}catch{}};e();const o=setInterval(e,3e4);return()=>clearInterval(o)},[]);const i=s?Object.keys(s.quest_data).sort():[],l=new Set;if(s)for(const e of Object.values(s.quest_data))for(const o of Object.keys(e))l.add(o);const n=Array.from(l).sort();return t.jsx(u,{id:p,title:"Quest Status",zIndex:x,width:780,height:500,children:t.jsx("div",{style:{flex:1,overflow:"auto",fontSize:"0.72rem"},children:s?i.length===0?t.jsx("div",{style:{padding:20,color:"#666",textAlign:"center"},children:"No quest data available"}):t.jsxs("table",{style:{width:"100%",borderCollapse:"collapse"},children:[t.jsx("thead",{children:t.jsxs("tr",{style:{position:"sticky",top:0,background:"#1a1a1a",zIndex:1},children:[t.jsx("th",{style:{textAlign:"left",padding:"4px 8px",borderBottom:"1px solid #444",color:"#888",fontSize:"0.65rem",fontWeight:600,minWidth:140},children:"Character"}),n.map(e=>t.jsx("th",{style:{textAlign:"center",padding:"4px 6px",borderBottom:"1px solid #444",color:"#888",fontSize:"0.6rem",fontWeight:600,maxWidth:120,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},title:e,children:e.replace(" Timer","").replace(" Pickup","")},e))]})}),t.jsx("tbody",{children:i.map(e=>{const o=s.quest_data[e]||{};return t.jsxs("tr",{style:{borderBottom:"1px solid #222"},children:[t.jsx("td",{style:{padding:"3px 8px",color:"#ccc",fontWeight:500,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis",maxWidth:160},children:e}),n.map(d=>{const r=o[d],a=r==="READY";return t.jsx("td",{style:{textAlign:"center",padding:"3px 6px",color:a?"#4c4":r?"#ca0":"#333",fontWeight:a?600:400,fontSize:a?"0.7rem":"0.68rem"},children:r||"—"},d)})]},e)})})]}):t.jsx("div",{style:{padding:20,color:"#666",textAlign:"center"},children:"Loading quest data..."})})})};export{j as QuestStatusWindow};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{r as c,j as t,D as u,a as f}from"./index-D3oSGfL8.js";import"./react-DlyoauG8.js";const j=({id:p,zIndex:x})=>{const[s,h]=c.useState(null);c.useEffect(()=>{const e=async()=>{try{h(await f("/quest-status"))}catch{}};e();const o=setInterval(e,3e4);return()=>clearInterval(o)},[]);const i=s?Object.keys(s.quest_data).sort():[],l=new Set;if(s)for(const e of Object.values(s.quest_data))for(const o of Object.keys(e))l.add(o);const n=Array.from(l).sort();return t.jsx(u,{id:p,title:"Quest Status",zIndex:x,width:780,height:500,children:t.jsx("div",{style:{flex:1,overflow:"auto",fontSize:"0.72rem"},children:s?i.length===0?t.jsx("div",{style:{padding:20,color:"#666",textAlign:"center"},children:"No quest data available"}):t.jsxs("table",{style:{width:"100%",borderCollapse:"collapse"},children:[t.jsx("thead",{children:t.jsxs("tr",{style:{position:"sticky",top:0,background:"#1a1a1a",zIndex:1},children:[t.jsx("th",{style:{textAlign:"left",padding:"4px 8px",borderBottom:"1px solid #444",color:"#888",fontSize:"0.65rem",fontWeight:600,minWidth:140},children:"Character"}),n.map(e=>t.jsx("th",{style:{textAlign:"center",padding:"4px 6px",borderBottom:"1px solid #444",color:"#888",fontSize:"0.6rem",fontWeight:600,maxWidth:120,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},title:e,children:e.replace(" Timer","").replace(" Pickup","")},e))]})}),t.jsx("tbody",{children:i.map(e=>{const o=s.quest_data[e]||{};return t.jsxs("tr",{style:{borderBottom:"1px solid #222"},children:[t.jsx("td",{style:{padding:"3px 8px",color:"#ccc",fontWeight:500,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis",maxWidth:160},children:e}),n.map(d=>{const r=o[d],a=r==="READY";return t.jsx("td",{style:{textAlign:"center",padding:"3px 6px",color:a?"#4c4":r?"#ca0":"#333",fontWeight:a?600:400,fontSize:a?"0.7rem":"0.68rem"},children:r||"—"},d)})]},e)})})]}):t.jsx("div",{style:{padding:20,color:"#666",textAlign:"center"},children:"Loading quest data..."})})})};export{j as QuestStatusWindow};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue