├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.css ├── App.tsx ├── api │ ├── RawAPI.js │ ├── ScreepsAPI.js │ ├── Socket.js │ ├── index.ts │ ├── websocket.prototype.d.ts │ └── websocket.ts ├── components │ ├── Canvas.tsx │ ├── ErrorBoundary.tsx │ ├── Game.tsx │ └── loading │ │ ├── Loading.css │ │ ├── Loading.tsx │ │ └── logo.svg ├── config │ ├── resourceMap.ts │ └── worldConfigs.ts ├── hooks │ ├── useAuth.ts │ ├── useWorldStartRoom.ts │ └── useZoom.ts ├── index.tsx ├── react-app-env.d.ts ├── reportWebVitals.ts ├── setupTests.ts └── utils.ts ├── tsconfig.json └── types ├── api.d.ts ├── global.d.ts └── screeps_renderer.d.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set end-of-line to LF 2 | * text eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .eslintcache 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) justjavac 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Screeps Web UI(WIP) 2 | 3 | 使用 React 实现的自定义 Screeps Web UI。在线演示地址 [https://screeps.devtips.cn/a](https://screeps.devtips.cn/a) 4 | 5 | ## 背景 6 | 7 | Screeps 是一款面向编程爱好者的开源 MMO RTS 沙盒游戏,其核心机制是为您的单位编写AI。您可以通过编写 JavaScript 来控制自己的殖民地。 8 | 9 | - [给前端程序员推荐一款游戏 Screeps:使用 JS/TS 代码控制自己的殖民地](https://zhuanlan.zhihu.com/p/330082031) 10 | 11 | Screeps 的后端代码是开源的,可以在自己的服务器上搭建一个游戏私服,但是前端 UI 并没有开源。只能通过 Sream 客户端来连接游戏服务器。于是我开发了这个 Web UI。 12 | 13 | ## 进度 14 | 15 | ⚠️ 目前只是一个 demo 版本,刚刚完成了房间地图的绘制和单位的显示。 16 | 17 | ## 本地开发 18 | 19 | clone 本仓库,运行 `npm start`。 20 | 21 | ```bash 22 | git clone git@github.com:justjavac/screeps-web-ui.git 23 | cd screeps-web-ui 24 | npm install 25 | npm start 26 | ``` 27 | 28 | 如果没有报错,则说明本地服务已经正常启动。 29 | 在浏览器中打开 [http://localhost:3000](http://localhost:3000) 可以看到页面。 30 | 31 | ## 说明 32 | 33 | Api 部分的代码在 [node-screeps-api](https://github.com/screepers/node-screeps-api) 的代码库基础上进行了修改,并适配了浏览器。将来此部分代码会基于 [swr](https://github.com/vercel/swr) 使用 React Hooks 重写。 34 | 35 | ### 许可证 36 | 37 | [screeps-web-ui](https://github.com/justjavac/screeps-web-ui) 的源码使用 MIT License 发布。具体内容请查看 [LICENSE](./LICENSE) 文件。 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "screeps-web-ui", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "react-scripts start", 7 | "build": "react-scripts build", 8 | "test": "react-scripts test", 9 | "eject": "react-scripts eject" 10 | }, 11 | "dependencies": { 12 | "@screeps/renderer": "^1.5.9", 13 | "@screeps/renderer-metadata": "^1.5.5", 14 | "@testing-library/jest-dom": "^5.11.6", 15 | "@testing-library/react": "^11.2.2", 16 | "@testing-library/user-event": "^12.6.0", 17 | "@types/jest": "^26.0.19", 18 | "@types/lodash": "^4.14.165", 19 | "@types/react": "^17.0.0", 20 | "@types/react-dom": "^17.0.0", 21 | "axios": "^0.21.0", 22 | "debug": "^4.3.1", 23 | "lodash": "^4.17.20", 24 | "promisify": "0.0.3", 25 | "react": "^17.0.1", 26 | "react-dom": "^17.0.1", 27 | "react-scripts": "4.0.1", 28 | "web-vitals": "^1.0.1" 29 | }, 30 | "homepage": "https://screeps.devtips.cn/a", 31 | "eslintConfig": { 32 | "extends": [ 33 | "react-app", 34 | "react-app/jest" 35 | ] 36 | }, 37 | "browserslist": { 38 | "production": [ 39 | "last 4 chrome version" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | }, 47 | "devDependencies": { 48 | "@types/debug": "^4.1.5", 49 | "typescript": "^4.1.3" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justjavac/screeps-web-ui/181eecd7df01da9d78be7a3d776b7b6a082bdc29/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Screeps Web UI 11 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justjavac/screeps-web-ui/181eecd7df01da9d78be7a3d776b7b6a082bdc29/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justjavac/screeps-web-ui/181eecd7df01da9d78be7a3d776b7b6a082bdc29/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Screeps UI", 3 | "name": "Screeps Web UI", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import "./App.css"; 4 | 5 | import ErrorBoundary from "./components/ErrorBoundary"; 6 | import Loading from "./components/loading/Loading"; 7 | import Game from "./components/Game"; 8 | 9 | import useAuth from "./hooks/useAuth"; 10 | 11 | const username = "demo"; 12 | const password = "123456"; 13 | 14 | function App() { 15 | const [token, loading, error] = useAuth(username, password); 16 | const room = localStorage.getItem("room") ?? "W12N12"; 17 | 18 | if (loading) { 19 | return ; 20 | } 21 | 22 | if (error != null || token == null) { 23 | console.error(error, token); 24 | return

Something went wrong.

; 25 | } 26 | 27 | return ( 28 | Something went wrong.}> 29 | 30 | 31 | ); 32 | } 33 | 34 | export default App; 35 | -------------------------------------------------------------------------------- /src/api/RawAPI.js: -------------------------------------------------------------------------------- 1 | import URL from "url"; 2 | import { EventEmitter } from "eventemitter3"; 3 | import zlib from "browserify-zlib"; 4 | import axios from "axios"; 5 | import Debug from "debug"; 6 | import promisify from "promisify"; 7 | 8 | const debugHttp = Debug("screepsapi:http"); 9 | const debugRateLimit = Debug("screepsapi:ratelimit"); 10 | 11 | const { format } = URL; 12 | 13 | const gunzipAsync = promisify.cb_func(zlib.gunzip); 14 | const inflateAsync = promisify.cb_func(zlib.inflate); 15 | 16 | const DEFAULT_SHARD = "shard0"; 17 | const OFFICIAL_HISTORY_INTERVAL = 100; 18 | const PRIVATE_HISTORY_INTERVAL = 20; 19 | 20 | const sleep = (ms) => new Promise((resolve) => setInterval(resolve, ms)); 21 | 22 | export class RawAPI extends EventEmitter { 23 | constructor(opts = {}) { 24 | super(); 25 | this.setServer(opts); 26 | const self = this; 27 | this.raw = { 28 | version() { 29 | return self.req("GET", "/api/version"); 30 | }, 31 | authmod() { 32 | if (self.isOfficialServer()) { 33 | return Promise.resolve({ name: "official" }); 34 | } 35 | return self.req("GET", "/api/authmod"); 36 | }, 37 | history(room, tick, shard = DEFAULT_SHARD) { 38 | if (self.isOfficialServer()) { 39 | tick -= tick % OFFICIAL_HISTORY_INTERVAL; 40 | return self.req("GET", `/room-history/${shard}/${room}/${tick}.json`); 41 | } else { 42 | tick -= tick % PRIVATE_HISTORY_INTERVAL; 43 | return self.req("GET", "/room-history", { room, time: tick }); 44 | } 45 | }, 46 | servers: { 47 | list() { 48 | return self.req("POST", "/api/servers.list", {}); 49 | }, 50 | }, 51 | auth: { 52 | signin(email, password) { 53 | return self.req("POST", "/api/auth/signin", { email, password }); 54 | }, 55 | steamTicket(ticket, useNativeAuth = false) { 56 | return self.req( 57 | "POST", 58 | "/api/auth/steam-ticket", 59 | { ticket, useNativeAuth }, 60 | ); 61 | }, 62 | me() { 63 | return self.req("GET", "/api/auth/me"); 64 | }, 65 | queryToken(token) { 66 | return self.req("GET", "/api/auth/query-token", { token }); 67 | }, 68 | }, 69 | register: { 70 | checkEmail(email) { 71 | return self.req("GET", "/api/register/check-email", { email }); 72 | }, 73 | checkUsername(username) { 74 | return self.req("GET", "/api/register/check-username", { username }); 75 | }, 76 | setUsername(username) { 77 | return self.req("POST", "/api/register/set-username", { username }); 78 | }, 79 | submit(username, email, password, modules) { 80 | return self.req( 81 | "POST", 82 | "/api/register/submit", 83 | { username, email, password, modules }, 84 | ); 85 | }, 86 | }, 87 | userMessages: { 88 | list(respondent) { 89 | return self.req("GET", "/api/user/messages/list", { respondent }); 90 | }, 91 | index() { 92 | return self.req("GET", "/api/user/messages/index"); 93 | }, 94 | unreadCount() { 95 | return self.req("GET", "/api/user/messages/unread-count"); 96 | }, 97 | send(respondent, text) { 98 | return self.req( 99 | "POST", 100 | "/api/user/messages/send", 101 | { respondent, text }, 102 | ); 103 | }, 104 | markRead(id) { 105 | return self.req("POST", "/api/user/messages/mark-read", { id }); 106 | }, 107 | }, 108 | game: { 109 | mapStats(rooms, statName, shard = DEFAULT_SHARD) { 110 | return self.req( 111 | "POST", 112 | "/api/game/map-stats", 113 | { rooms, statName, shard }, 114 | ); 115 | }, 116 | genUniqueObjectName(type, shard = DEFAULT_SHARD) { 117 | return self.req( 118 | "POST", 119 | "/api/game/gen-unique-object-name", 120 | { type, shard }, 121 | ); 122 | }, 123 | checkUniqueObjectName(type, name, shard = DEFAULT_SHARD) { 124 | return self.req( 125 | "POST", 126 | "/api/game/check-unique-object-name", 127 | { type, name, shard }, 128 | ); 129 | }, 130 | placeSpawn(room, x, y, name, shard = DEFAULT_SHARD) { 131 | return self.req( 132 | "POST", 133 | "/api/game/place-spawn", 134 | { name, room, x, y, shard }, 135 | ); 136 | }, 137 | createFlag( 138 | room, 139 | x, 140 | y, 141 | name, 142 | color = 1, 143 | secondaryColor = 1, 144 | shard = DEFAULT_SHARD, 145 | ) { 146 | return self.req( 147 | "POST", 148 | "/api/game/create-flag", 149 | { name, room, x, y, color, secondaryColor, shard }, 150 | ); 151 | }, 152 | genUniqueFlagName(shard = DEFAULT_SHARD) { 153 | return self.req("POST", "/api/game/gen-unique-flag-name", { shard }); 154 | }, 155 | checkUniqueFlagName(name, shard = DEFAULT_SHARD) { 156 | return self.req( 157 | "POST", 158 | "/api/game/check-unique-flag-name", 159 | { name, shard }, 160 | ); 161 | }, 162 | changeFlagColor(color = 1, secondaryColor = 1, shard = DEFAULT_SHARD) { 163 | return self.req( 164 | "POST", 165 | "/api/game/change-flag-color", 166 | { color, secondaryColor, shard }, 167 | ); 168 | }, 169 | removeFlag(room, name, shard = DEFAULT_SHARD) { 170 | return self.req( 171 | "POST", 172 | "/api/game/remove-flag", 173 | { name, room, shard }, 174 | ); 175 | }, 176 | addObjectIntent(room, name, intent, shard = DEFAULT_SHARD) { 177 | return self.req( 178 | "POST", 179 | "/api/game/add-object-intent", 180 | { room, name, intent, shard }, 181 | ); 182 | }, 183 | createConstruction( 184 | room, 185 | x, 186 | y, 187 | structureType, 188 | name, 189 | shard = DEFAULT_SHARD, 190 | ) { 191 | return self.req( 192 | "POST", 193 | "/api/game/create-construction", 194 | { room, x, y, structureType, name, shard }, 195 | ); 196 | }, 197 | setNotifyWhenAttacked(_id, enabled = true, shard = DEFAULT_SHARD) { 198 | return self.req( 199 | "POST", 200 | "/api/game/set-notify-when-attacked", 201 | { _id, enabled, shard }, 202 | ); 203 | }, 204 | createInvader( 205 | room, 206 | x, 207 | y, 208 | size, 209 | type, 210 | boosted = false, 211 | shard = DEFAULT_SHARD, 212 | ) { 213 | return self.req( 214 | "POST", 215 | "/api/game/create-invader", 216 | { room, x, y, size, type, boosted, shard }, 217 | ); 218 | }, 219 | removeInvader(_id, shard = DEFAULT_SHARD) { 220 | return self.req("POST", "/api/game/remove-invader", { _id, shard }); 221 | }, 222 | time(shard = DEFAULT_SHARD) { 223 | return self.req("GET", "/api/game/time", { shard }); 224 | }, 225 | worldSize(shard = DEFAULT_SHARD) { 226 | return self.req("GET", "/api/game/world-size", { shard }); 227 | }, 228 | roomDecorations(room, shard = DEFAULT_SHARD) { 229 | return self.req("GET", "/api/game/room-decorations", { room, shard }); 230 | }, 231 | roomObjects(room, shard = DEFAULT_SHARD) { 232 | return self.req("GET", "/api/game/room-objects", { room, shard }); 233 | }, 234 | 235 | /** 236 | * @param {string} room 237 | * @param {number} encoded 238 | * @param {string} shard 239 | * @returns {Promise<{ok: number, terrain: API.TerrainEncoded}>} 240 | */ 241 | roomTerrain(room, encoded = 1, shard = DEFAULT_SHARD) { 242 | return self.req( 243 | "GET", 244 | "/api/game/room-terrain", 245 | { room, encoded, shard }, 246 | ); 247 | }, 248 | roomStatus(room, shard = DEFAULT_SHARD) { 249 | return self.req("GET", "/api/game/room-status", { room, shard }); 250 | }, 251 | roomOverview(room, interval = 8, shard = DEFAULT_SHARD) { 252 | return self.req( 253 | "GET", 254 | "/api/game/room-overview", 255 | { room, interval, shard }, 256 | ); 257 | }, 258 | market: { 259 | ordersIndex(shard = DEFAULT_SHARD) { 260 | return self.req("GET", "/api/game/market/orders-index", { shard }); 261 | }, 262 | myOrders() { 263 | return self.req("GET", "/api/game/market/my-orders").then( 264 | self.mapToShard, 265 | ); 266 | }, 267 | orders(resourceType, shard = DEFAULT_SHARD) { 268 | return self.req( 269 | "GET", 270 | "/api/game/market/orders", 271 | { resourceType, shard }, 272 | ); 273 | }, 274 | stats(resourceType, shard = DEFAULT_SHARD) { 275 | return self.req( 276 | "GET", 277 | "/api/game/market/stats", 278 | { resourceType, shard }, 279 | ); 280 | }, 281 | }, 282 | shards: { 283 | info() { 284 | return self.req("GET", "/api/game/shards/info"); 285 | }, 286 | }, 287 | }, 288 | leaderboard: { 289 | list(limit = 10, mode = "world", offset = 0, season) { 290 | if (mode !== "world" && mode !== "power") { 291 | throw new Error("incorrect mode parameter"); 292 | } 293 | if (!season) season = self.currentSeason(); 294 | return self.req( 295 | "GET", 296 | "/api/leaderboard/list", 297 | { limit, mode, offset, season }, 298 | ); 299 | }, 300 | find(username, mode = "world", season = "") { 301 | return self.req( 302 | "GET", 303 | "/api/leaderboard/find", 304 | { season, mode, username }, 305 | ); 306 | }, 307 | seasons() { 308 | return self.req("GET", "/api/leaderboard/seasons"); 309 | }, 310 | }, 311 | user: { 312 | badge(badge) { 313 | return self.req("POST", "/api/user/badge", { badge }); 314 | }, 315 | respawn() { 316 | return self.req("POST", "/api/user/respawn"); 317 | }, 318 | setActiveBranch(branch, activeName) { 319 | return self.req( 320 | "POST", 321 | "/api/user/set-active-branch", 322 | { branch, activeName }, 323 | ); 324 | }, 325 | cloneBranch(branch, newName, defaultModules) { 326 | return self.req( 327 | "POST", 328 | "/api/user/clone-branch", 329 | { branch, newName, defaultModules }, 330 | ); 331 | }, 332 | deleteBranch(branch) { 333 | return self.req("POST", "/api/user/delete-branch", { branch }); 334 | }, 335 | notifyPrefs(prefs) { 336 | // disabled,disabledOnMessages,sendOnline,interval,errorsInterval 337 | return self.req("POST", "/api/user/notify-prefs", prefs); 338 | }, 339 | tutorialDone() { 340 | return self.req("POST", "/api/user/tutorial-done"); 341 | }, 342 | email(email) { 343 | return self.req("POST", "/api/user/email", { email }); 344 | }, 345 | worldStartRoom(shard) { 346 | return self.req("GET", "/api/user/world-start-room", { shard }); 347 | }, 348 | worldStatus() { 349 | return self.req("GET", "/api/user/world-status"); 350 | }, 351 | branches() { 352 | return self.req("GET", "/api/user/branches"); 353 | }, 354 | code: { 355 | get(branch) { 356 | return self.req("GET", "/api/user/code", { branch }); 357 | }, 358 | set(branch, modules, _hash) { 359 | if (!_hash) _hash = Date.now(); 360 | return self.req( 361 | "POST", 362 | "/api/user/code", 363 | { branch, modules, _hash }, 364 | ); 365 | }, 366 | }, 367 | decorations: { 368 | inventory() { 369 | return self.req("GET", "/api/user/decorations/inventory"); 370 | }, 371 | themes() { 372 | return self.req("GET", "/api/user/decorations/inventory"); 373 | }, 374 | convert(decorations) { 375 | return self.req( 376 | "POST", 377 | "/api/user/decorations/convert", 378 | { decorations }, 379 | ); // decorations is a string array of ids 380 | }, 381 | pixelize(count, theme = "") { 382 | return self.req( 383 | "POST", 384 | "/api/user/decorations/pixelize", 385 | { count, theme }, 386 | ); 387 | }, 388 | activate(_id, active) { 389 | return self.req( 390 | "POST", 391 | "/api/user/decorations/activate", 392 | { _id, active }, 393 | ); 394 | }, 395 | deactivate(decorations) { 396 | return self.req( 397 | "POST", 398 | "/api/user/decorations/deactivate", 399 | { decorations }, 400 | ); // decorations is a string array of ids 401 | }, 402 | }, 403 | respawnProhibitedRooms() { 404 | return self.req("GET", "/api/user/respawn-prohibited-rooms"); 405 | }, 406 | memory: { 407 | get(path, shard = DEFAULT_SHARD) { 408 | return self.req("GET", "/api/user/memory", { path, shard }); 409 | }, 410 | set(path, value, shard = DEFAULT_SHARD) { 411 | return self.req("POST", "/api/user/memory", { path, value, shard }); 412 | }, 413 | segment: { 414 | get(segment, shard = DEFAULT_SHARD) { 415 | return self.req( 416 | "GET", 417 | "/api/user/memory-segment", 418 | { segment, shard }, 419 | ); 420 | }, 421 | set(segment, data, shard = DEFAULT_SHARD) { 422 | return self.req( 423 | "POST", 424 | "/api/user/memory-segment", 425 | { segment, data, shard }, 426 | ); 427 | }, 428 | }, 429 | }, 430 | find(username) { 431 | return self.req("GET", "/api/user/find", { username }); 432 | }, 433 | findById(id) { 434 | return self.req("GET", "/api/user/find", { id }); 435 | }, 436 | stats(interval) { 437 | return self.req("GET", "/api/user/stats", { interval }); 438 | }, 439 | rooms(id) { 440 | return self.req("GET", "/api/user/rooms", { id }).then( 441 | self.mapToShard, 442 | ); 443 | }, 444 | overview(interval, statName) { 445 | return self.req("GET", "/api/user/overview", { interval, statName }); 446 | }, 447 | moneyHistory(page = 0) { 448 | return self.req("GET", "/api/user/money-history", { page }); 449 | }, 450 | console(expression, shard = DEFAULT_SHARD) { 451 | return self.req("POST", "/api/user/console", { expression, shard }); 452 | }, 453 | name() { 454 | return self.req("GET", "/api/user/name"); 455 | }, 456 | }, 457 | experimental: { 458 | pvp(interval = 100) { 459 | return self.req("GET", "/api/experimental/pvp", { interval }).then( 460 | self.mapToShard, 461 | ); 462 | }, 463 | nukes() { 464 | return self.req("GET", "/api/experimental/nukes").then( 465 | self.mapToShard, 466 | ); 467 | }, 468 | }, 469 | warpath: { 470 | battles(interval = 100) { 471 | return self.req("GET", "/api/warpath/battles", { interval }); 472 | }, 473 | }, 474 | }; 475 | } 476 | 477 | currentSeason() { 478 | const now = new Date(); 479 | const year = now.getFullYear(); 480 | let month = (now.getUTCMonth() + 1).toString(); 481 | if (month.length === 1) month = `0${month}`; 482 | return `${year}-${month}`; 483 | } 484 | 485 | isOfficialServer() { 486 | return this.opts.url.match(/screeps\.com/) !== null; 487 | } 488 | 489 | mapToShard(res) { 490 | if (!res.shards) { 491 | res.shards = { 492 | privSrv: res.list || res.rooms, 493 | }; 494 | } 495 | return res; 496 | } 497 | 498 | setServer(opts) { 499 | if (!this.opts) { 500 | this.opts = {}; 501 | } 502 | Object.assign(this.opts, opts); 503 | if (opts.path && !opts.pathname) { 504 | this.opts.pathname = this.opts.path; 505 | } 506 | if (!opts.url) { 507 | this.opts.url = format(this.opts); 508 | if (!this.opts.url.endsWith("/")) this.opts.url += "/"; 509 | } 510 | if (opts.token) { 511 | this.token = opts.token; 512 | } 513 | this.http = axios.create({ 514 | baseURL: this.opts.url, 515 | }); 516 | } 517 | 518 | /** 519 | * @param {string} email 520 | * @param {string} password 521 | * @param {object} opts 522 | * @returns {Promise<{ ok: number, token: string }>} 523 | */ 524 | async auth(email, password, opts = {}) { 525 | this.setServer(opts); 526 | if (email && password) { 527 | Object.assign(this.opts, { email, password }); 528 | } 529 | const res = await this.raw.auth.signin(this.opts.email, this.opts.password); 530 | this.emit("token", res.token); 531 | this.emit("auth"); 532 | this.__authed = true; 533 | return res; 534 | } 535 | 536 | async req(method, path, body = {}) { 537 | const opts = { 538 | method, 539 | url: path, 540 | headers: {}, 541 | }; 542 | debugHttp(`${method} ${path} ${JSON.stringify(body)}`); 543 | if (this.token) { 544 | Object.assign(opts.headers, { 545 | "X-Token": this.token, 546 | "X-Username": this.token, 547 | }); 548 | } 549 | if (method === "GET") { 550 | opts.params = body; 551 | } else { 552 | opts.data = body; 553 | } 554 | try { 555 | const res = await this.http(opts); 556 | const token = res.headers["x-token"]; 557 | if (token) { 558 | this.emit("token", token); 559 | } 560 | const rateLimit = this.buildRateLimit(method, path, res); 561 | this.emit("rateLimit", rateLimit); 562 | debugRateLimit( 563 | `${method} ${path} ${rateLimit.remaining}/${rateLimit.limit} ${rateLimit.toReset}s`, 564 | ); 565 | if ( 566 | typeof res.data.data === "string" && res.data.data.slice(0, 3) === "gz:" 567 | ) { 568 | res.data.data = await this.gz(res.data.data); 569 | } 570 | this.emit("response", res); 571 | return res.data; 572 | } catch (err) { 573 | const res = err.response || {}; 574 | const rateLimit = this.buildRateLimit(method, path, res); 575 | this.emit("rateLimit", rateLimit); 576 | debugRateLimit( 577 | `${method} ${path} ${rateLimit.remaining}/${rateLimit.limit} ${rateLimit.toReset}s`, 578 | ); 579 | if (res.status === 401) { 580 | if (this.__authed && this.opts.email && this.opts.password) { 581 | this.__authed = false; 582 | await this.auth(this.opts.email, this.opts.password); 583 | return this.req(method, path, body); 584 | } else { 585 | throw new Error("Not Authorized"); 586 | } 587 | } 588 | if ( 589 | res.status === 429 && !res.headers["x-ratelimit-limit"] && 590 | this.opts.experimentalRetry429 591 | ) { 592 | await sleep(Math.floor(Math.random() * 500) + 200); 593 | return this.req(method, path, body); 594 | } 595 | throw new Error(res.data); 596 | } 597 | } 598 | 599 | async gz(data) { 600 | const buf = Buffer.from(data.slice(3), "base64"); 601 | const ret = await gunzipAsync(buf); 602 | return JSON.parse(ret.toString()); 603 | } 604 | 605 | async inflate(data) { // es 606 | const buf = Buffer.from(data.slice(3), "base64"); 607 | const ret = await inflateAsync(buf); 608 | return JSON.parse(ret.toString()); 609 | } 610 | 611 | buildRateLimit(method, path, res) { 612 | const { 613 | headers: { 614 | "x-ratelimit-limit": limit, 615 | "x-ratelimit-remaining": remaining, 616 | "x-ratelimit-reset": reset, 617 | } = {}, 618 | } = res; 619 | return { 620 | method, 621 | path, 622 | limit: +limit, 623 | remaining: +remaining, 624 | reset: +reset, 625 | toReset: reset - Math.floor(Date.now() / 1000), 626 | }; 627 | } 628 | } 629 | -------------------------------------------------------------------------------- /src/api/ScreepsAPI.js: -------------------------------------------------------------------------------- 1 | import { Socket } from "./Socket"; 2 | import { RawAPI } from "./RawAPI"; 3 | 4 | const DEFAULTS = { 5 | protocol: "https", 6 | hostname: "screeps.devtips.cn", 7 | port: 443, 8 | path: "/", 9 | }; 10 | 11 | export class ScreepsAPI extends RawAPI { 12 | constructor(opts) { 13 | opts = Object.assign({}, DEFAULTS, opts); 14 | super(opts); 15 | this.on("token", (token) => { 16 | this.token = token; 17 | this.raw.token = token; 18 | }); 19 | const defaultLimit = (limit, period) => ({ 20 | limit, 21 | period, 22 | remaining: limit, 23 | reset: 0, 24 | toReset: 0, 25 | }); 26 | this.rateLimits = { 27 | global: defaultLimit(120, "minute"), 28 | GET: { 29 | "/api/game/room-terrain": defaultLimit(360, "hour"), 30 | "/api/user/code": defaultLimit(60, "hour"), 31 | "/api/user/memory": defaultLimit(1440, "day"), 32 | "/api/user/memory-segment": defaultLimit(360, "hour"), 33 | "/api/game/market/orders-index": defaultLimit(60, "hour"), 34 | "/api/game/market/orders": defaultLimit(60, "hour"), 35 | "/api/game/market/my-orders": defaultLimit(60, "hour"), 36 | "/api/game/market/stats": defaultLimit(60, "hour"), 37 | "/api/game/user/money-history": defaultLimit(60, "hour"), 38 | }, 39 | POST: { 40 | "/api/user/console": defaultLimit(360, "hour"), 41 | "/api/game/map-stats": defaultLimit(60, "hour"), 42 | "/api/user/code": defaultLimit(240, "day"), 43 | "/api/user/set-active-branch": defaultLimit(240, "day"), 44 | "/api/user/memory": defaultLimit(240, "day"), 45 | "/api/user/memory-segment": defaultLimit(60, "hour"), 46 | }, 47 | }; 48 | this.on("rateLimit", (limits) => { 49 | const rate = this.rateLimits[limits.method][limits.path] || 50 | this.rateLimits.global; 51 | const copy = Object.assign({}, limits); 52 | delete copy.path; 53 | delete copy.method; 54 | Object.assign(rate, copy); 55 | }); 56 | this.socket = new Socket(this); 57 | } 58 | 59 | getRateLimit(method, path) { 60 | return this.rateLimits[method][path] || this.rateLimits.global; 61 | } 62 | 63 | get rateLimitResetUrl() { 64 | return `https://screeps.com/a/#!/account/auth-tokens/noratelimit?token=${ 65 | this.token.slice( 66 | 0, 67 | 8, 68 | ) 69 | }`; 70 | } 71 | 72 | async me() { 73 | if (this._user) return this._user; 74 | const tokenInfo = await this.tokenInfo(); 75 | if (tokenInfo.full) { 76 | this._user = await this.raw.auth.me(); 77 | } else { 78 | const { username } = await this.raw.user.name(); 79 | const { user } = await this.raw.user.find(username); 80 | this._user = user; 81 | } 82 | return this._user; 83 | } 84 | 85 | async tokenInfo() { 86 | if (this._tokenInfo) { 87 | return this._tokenInfo; 88 | } 89 | if (this.opts.token) { 90 | const { token } = await this.raw.auth.queryToken(this.token); 91 | this._tokenInfo = token; 92 | } else { 93 | this._tokenInfo = { full: true }; 94 | } 95 | return this._tokenInfo; 96 | } 97 | 98 | async userID() { 99 | const user = await this.me(); 100 | return user._id; 101 | } 102 | 103 | get history() { 104 | return this.raw.history; 105 | } 106 | 107 | get authmod() { 108 | return this.raw.authmod; 109 | } 110 | 111 | get version() { 112 | return this.raw.version; 113 | } 114 | 115 | get time() { 116 | return this.raw.game.time; 117 | } 118 | 119 | get leaderboard() { 120 | return this.raw.leaderboard; 121 | } 122 | 123 | get market() { 124 | return this.raw.game.market; 125 | } 126 | 127 | get registerUser() { 128 | return this.raw.register.submit; 129 | } 130 | 131 | get code() { 132 | return this.raw.user.code; 133 | } 134 | 135 | get memory() { 136 | return this.raw.user.memory; 137 | } 138 | 139 | get segment() { 140 | return this.raw.user.memory.segment; 141 | } 142 | 143 | get console() { 144 | return this.raw.user.console; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/api/Socket.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "eventemitter3"; 2 | import Debug from "debug"; 3 | 4 | import "./websocket"; 5 | 6 | const debug = Debug("screepsapi:socket"); 7 | 8 | const DEFAULTS = { 9 | reconnect: true, 10 | resubscribe: true, 11 | keepAlive: true, 12 | maxRetries: 10, 13 | maxRetryDelay: 60 * 1000, // in milli-seconds 14 | }; 15 | 16 | export class Socket extends EventEmitter { 17 | constructor(ScreepsAPI) { 18 | super(); 19 | this.api = ScreepsAPI; 20 | this.opts = Object.assign({}, DEFAULTS); 21 | this.on("error", () => {}); // catch to prevent unhandled-exception errors 22 | this.reset(); 23 | this.on("auth", (ev) => { 24 | if (ev.data.status === "ok") { 25 | while (this.__queue.length) { 26 | this.emit(this.__queue.shift()); 27 | } 28 | clearInterval(this.keepAliveInter); 29 | if (this.opts.keepAlive) { 30 | this.keepAliveInter = setInterval( 31 | () => this.ws && this.ws.send("ping 1"), 32 | 10000, 33 | ); 34 | } 35 | } 36 | }); 37 | } 38 | 39 | reset() { 40 | this.authed = false; 41 | this.connected = false; 42 | this.reconnecting = false; 43 | clearInterval(this.keepAliveInter); 44 | this.keepAliveInter = 0; 45 | this.__queue = []; // pending messages (to send once authenticated) 46 | this.__subQueue = []; // pending subscriptions (to request once authenticated) 47 | this.__subs = {}; // number of callbacks for each subscription 48 | } 49 | 50 | async connect(opts = {}) { 51 | Object.assign(this.opts, opts); 52 | if (!this.api.token) { 53 | throw new Error( 54 | "No token! Call api.auth() before connecting the socket!", 55 | ); 56 | } 57 | return new Promise((resolve, reject) => { 58 | const baseURL = this.api.opts.url.replace("http", "ws"); 59 | const wsurl = new URL("socket/websocket", baseURL); 60 | this.ws = new WebSocket(wsurl); 61 | this.ws.on("open", () => { 62 | this.connected = true; 63 | this.reconnecting = false; 64 | if (this.opts.resubscribe) { 65 | this.__subQueue.push(...Object.keys(this.__subs)); 66 | } 67 | debug("connected"); 68 | this.emit("connected"); 69 | resolve(this.auth(this.api.token)); 70 | }); 71 | this.ws.on("close", () => { 72 | clearInterval(this.keepAliveInter); 73 | this.authed = false; 74 | this.connected = false; 75 | debug("disconnected"); 76 | this.emit("disconnected"); 77 | if (this.opts.reconnect) { 78 | this.reconnect().catch(() => {/* error emitted in reconnect() */}); 79 | } 80 | }); 81 | this.ws.on("error", (err) => { 82 | this.ws.terminate(); 83 | this.emit("error", err); 84 | debug(`error ${err}`); 85 | if (!this.connected) { 86 | reject(err); 87 | } 88 | }); 89 | this.ws.on("unexpected-response", (req, res) => { 90 | const err = new Error( 91 | `WS Unexpected Response: ${res.statusCode} ${res.statusMessage}`, 92 | ); 93 | this.emit("error", err); 94 | reject(err); 95 | }); 96 | this.ws.on("message", (data) => this.handleMessage(data)); 97 | }); 98 | } 99 | 100 | async reconnect() { 101 | Object.keys(this.__subs).forEach((sub) => this.subscribe(sub)); 102 | this.reconnecting = true; 103 | let retries = 0; 104 | let retry; 105 | do { 106 | let time = Math.pow(2, retries) * 100; 107 | if (time > this.opts.maxRetryDelay) time = this.opts.maxRetryDelay; 108 | await this.sleep(time); 109 | if (!this.reconnecting) return; // reset() called in-between 110 | try { 111 | await this.connect(); 112 | retry = false; 113 | } catch (err) { 114 | retry = true; 115 | } 116 | retries++; 117 | debug(`reconnect ${retries}/${this.opts.maxRetries}`); 118 | } while (retry && retries < this.opts.maxRetries); 119 | if (retry) { 120 | const err = new Error( 121 | `Reconnection failed after ${this.opts.maxRetries} retries`, 122 | ); 123 | this.reconnecting = false; 124 | debug("reconnect failed"); 125 | this.emit("error", err); 126 | throw err; 127 | } 128 | } 129 | 130 | disconnect() { 131 | debug("disconnect"); 132 | clearInterval(this.keepAliveInter); 133 | this.ws.removeAllListeners(); // remove listeners first or we may trigger reconnection & Co. 134 | this.ws.terminate(); 135 | this.reset(); 136 | this.emit("disconnected"); 137 | } 138 | 139 | sleep(time) { 140 | return new Promise((resolve, reject) => { 141 | setTimeout(resolve, time); 142 | }); 143 | } 144 | 145 | handleMessage(msg) { 146 | msg = msg.data || msg; // Handle ws/browser difference 147 | if (msg.slice(0, 3) === "gz:") msg = this.api.inflate(msg); 148 | debug(`message ${msg}`); 149 | if (msg[0] === "[") { 150 | msg = JSON.parse(msg); 151 | let [, type, id, channel] = msg[0].match(/^(.+):(.+?)(?:\/(.+))?$/); 152 | channel = channel || type; 153 | const event = { channel, id, type, data: msg[1] }; 154 | this.emit(msg[0], event); 155 | this.emit(event.channel, event); 156 | this.emit("message", event); 157 | } else { 158 | const [channel, ...data] = msg.split(" "); 159 | const event = { type: "server", channel, data }; 160 | if (channel === "auth") event.data = { status: data[0], token: data[1] }; 161 | if (["protocol", "time", "package"].includes(channel)) { 162 | event.data = { [channel]: data[0] }; 163 | } 164 | this.emit(channel, event); 165 | this.emit("message", event); 166 | } 167 | } 168 | 169 | async gzip(bool) { 170 | this.send(`gzip ${bool ? "on" : "off"}`); 171 | } 172 | 173 | async send(data) { 174 | if (!this.connected) { 175 | this.__queue.push(data); 176 | } else { 177 | this.ws.send(data); 178 | } 179 | } 180 | 181 | auth(token) { 182 | return new Promise((resolve, reject) => { 183 | this.send(`auth ${token}`); 184 | this.once("auth", (ev) => { 185 | const { data } = ev; 186 | if (data.status === "ok") { 187 | this.authed = true; 188 | this.emit("token", data.token); 189 | this.emit("authed"); 190 | while (this.__subQueue.length) { 191 | this.send(this.__subQueue.shift()); 192 | } 193 | resolve(); 194 | } else { 195 | reject(new Error("socket auth failed")); 196 | } 197 | }); 198 | }); 199 | } 200 | 201 | async subscribe(path, cb) { 202 | if (!path) return; 203 | const userID = await this.api.userID(); 204 | if (!path.match(/^(\w+):(.+?)$/)) path = `user:${userID}/${path}`; 205 | if (this.authed) { 206 | this.send(`subscribe ${path}`); 207 | } else { 208 | this.__subQueue.push(`subscribe ${path}`); 209 | } 210 | this.emit("subscribe", path); 211 | this.__subs[path] = this.__subs[path] || 0; 212 | this.__subs[path]++; 213 | if (cb) this.on(path, cb); 214 | } 215 | 216 | async unsubscribe(path) { 217 | if (!path) return; 218 | const userID = await this.api.userID(); 219 | if (!path.match(/^(\w+):(.+?)$/)) path = `user:${userID}/${path}`; 220 | this.send(`unsubscribe ${path}`); 221 | this.emit("unsubscribe", path); 222 | if (this.__subs[path]) this.__subs[path]--; 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import { ScreepsAPI } from "./ScreepsAPI"; 2 | 3 | export default new ScreepsAPI(); 4 | -------------------------------------------------------------------------------- /src/api/websocket.prototype.d.ts: -------------------------------------------------------------------------------- 1 | interface WebSocket { 2 | on(type: string, listener: EventListener): void; 3 | off(type: string, listener: EventListener): void; 4 | once(type: string, listener: EventListener): void; 5 | } 6 | -------------------------------------------------------------------------------- /src/api/websocket.ts: -------------------------------------------------------------------------------- 1 | WebSocket.prototype.on = function ( 2 | event: string, 3 | callback: EventListener, 4 | ): void { 5 | this.addEventListener(event, callback); 6 | }; 7 | 8 | WebSocket.prototype.off = function ( 9 | event: string, 10 | callback: EventListener, 11 | ): void { 12 | this.removeEventListener(event, callback); 13 | }; 14 | 15 | WebSocket.prototype.once = function ( 16 | event: string, 17 | callback: EventListener, 18 | ): void { 19 | var self = this; 20 | this.addEventListener(event, function handler(...args) { 21 | callback.apply(callback, args); 22 | self.removeEventListener(event, handler); 23 | }); 24 | }; 25 | 26 | export {}; 27 | -------------------------------------------------------------------------------- /src/components/Canvas.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, MouseEvent, RefObject } from "react"; 2 | import _ from "lodash"; 3 | 4 | import GameRenderer, { ObjectState } from "@screeps/renderer"; 5 | 6 | import { rescaleResources, resourceMap } from "../config/resourceMap"; 7 | import worldConfigs from "../config/worldConfigs"; 8 | 9 | import api from "../api"; 10 | import { applyDiff, decodeTerrain } from "../utils"; 11 | 12 | const TICK_DURATION = 0.3; 13 | 14 | type Position = { 15 | x: number; 16 | y: number; 17 | }; 18 | 19 | type CanvasProps = { 20 | zoomLevel: number; 21 | room: string; 22 | }; 23 | 24 | export default class Canvas extends Component { 25 | private gameApp!: GameRenderer; 26 | private pan?: Position | null; 27 | private gameCanvas: RefObject; 28 | 29 | constructor(props: CanvasProps) { 30 | super(props); 31 | 32 | this.gameCanvas = React.createRef(); 33 | 34 | this.onMouseDown = this.onMouseDown.bind(this); 35 | this.onMouseMove = this.onMouseMove.bind(this); 36 | this.onMouseUp = this.onMouseUp.bind(this); 37 | this.subscribeRoom = this.subscribeRoom.bind(this); 38 | } 39 | 40 | /** 41 | * In this case, componentDidMount is used to grab the canvas container ref, and 42 | * and hook up the PixiJS renderer 43 | */ 44 | async componentDidMount() { 45 | GameRenderer.compileMetadata(worldConfigs.metadata); 46 | 47 | this.gameApp = new GameRenderer({ 48 | size: { 49 | width: this.gameCanvas.current!.clientWidth, 50 | height: this.gameCanvas.current!.clientHeight, 51 | }, 52 | resourceMap, 53 | worldConfigs, 54 | rescaleResources, 55 | countMetrics: false, 56 | useDefaultLogger: false, 57 | backgroundColor: 0x050505, 58 | }); 59 | 60 | await this.gameApp.init(this.gameCanvas.current!); 61 | this.gameApp.resize(); 62 | this.gameApp.zoomLevel = 0.2; 63 | 64 | Promise.resolve() 65 | .then(() => api.raw.game.roomTerrain(this.props.room)) 66 | .then((terrain) => { 67 | const _terrain = decodeTerrain(terrain.terrain); 68 | this.gameApp.setTerrain(_terrain as ObjectState[]); 69 | this.subscribeRoom(); 70 | }) 71 | .catch((err) => { 72 | console.error("err", err); 73 | }); 74 | } 75 | 76 | subscribeRoom() { 77 | const objects: ObjectState[] = []; 78 | const users: Record = {}; 79 | 80 | api.socket.subscribe(`room:${this.props.room}`, (event: API.RoomEvent) => { 81 | applyDiff(objects, event.data.objects); 82 | if (event.data.users) { 83 | _.merge(users, event.data.users); 84 | } 85 | const sample = { 86 | gameTime: event.data.gameTime, 87 | info: event.data.info, 88 | flags: [], 89 | visual: event.data.visual, 90 | users, 91 | objects: _.cloneDeep(objects), 92 | }; 93 | 94 | this.gameApp.applyState(sample, TICK_DURATION); 95 | }); 96 | } 97 | 98 | render() { 99 | return ( 100 | // eslint-disable-next-line react/no-string-refs 101 |
115 | ); 116 | } 117 | 118 | onMouseDown(e: MouseEvent) { 119 | this.pan = { x: e.pageX, y: e.pageY }; 120 | } 121 | 122 | onMouseMove(e: MouseEvent) { 123 | if (this.pan) { 124 | this.gameApp.pan(e.pageX - this.pan.x, e.pageY - this.pan.y); 125 | this.pan = { x: e.pageX, y: e.pageY }; 126 | } 127 | } 128 | 129 | onMouseUp(e: MouseEvent) { 130 | this.pan = null; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import React, { ErrorInfo, ReactNode } from "react"; 2 | 3 | type Props = { 4 | fallback: ReactNode; 5 | }; 6 | 7 | type State = { 8 | hasError: boolean; 9 | }; 10 | 11 | export default class ErrorBoundary extends React.Component { 12 | state = { hasError: false }; 13 | 14 | static getDerivedStateFromError(error: Error) { 15 | return { 16 | hasError: true, 17 | }; 18 | } 19 | 20 | componentDidCatch(error: Error, errorInfo: ErrorInfo) { 21 | console.error(error, errorInfo); 22 | } 23 | 24 | render() { 25 | if (this.state.hasError) { 26 | return this.props.fallback; 27 | } 28 | return this.props.children; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Game.tsx: -------------------------------------------------------------------------------- 1 | import Canvas from "./Canvas"; 2 | 3 | import useZoom from "../hooks/useZoom"; 4 | 5 | type Props = { 6 | room: string; 7 | }; 8 | 9 | export default function Game({ room }: Props) { 10 | const [zoom, zoomIn, zoomOut] = useZoom(0.2); 11 | 12 | return
13 | 17 |
26 | 27 | 28 |
29 |
; 30 | } 31 | -------------------------------------------------------------------------------- /src/components/loading/Loading.css: -------------------------------------------------------------------------------- 1 | 2 | .Loading { 3 | background-color: #282c34; 4 | min-height: 100vh; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: center; 9 | font-size: calc(10px + 2vmin); 10 | color: white; 11 | } 12 | 13 | .Loading-logo { 14 | height: 40vmin; 15 | pointer-events: none; 16 | } 17 | 18 | @media (prefers-reduced-motion: no-preference) { 19 | .Loading-logo { 20 | animation: Loading-logo-spin infinite 20s linear; 21 | } 22 | } 23 | 24 | @keyframes Loading-logo-spin { 25 | from { 26 | transform: rotate(0deg); 27 | } 28 | to { 29 | transform: rotate(360deg); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/loading/Loading.tsx: -------------------------------------------------------------------------------- 1 | import logo from "./logo.svg"; 2 | import "./Loading.css"; 3 | 4 | function Loading() { 5 | return ( 6 |
7 | logo 8 |

Loading......

9 |
10 | ); 11 | } 12 | 13 | export default Loading; 14 | -------------------------------------------------------------------------------- /src/components/loading/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/config/resourceMap.ts: -------------------------------------------------------------------------------- 1 | const resourceMap: Record = { 2 | berserk: "berserk.svg", 3 | bodyPartBar: "bodyPartBar.svg", 4 | // circle: "circle.svg", 5 | "commander-lvl0": "commander-lvl0.svg", 6 | "commander-lvl1": "commander-lvl1.svg", 7 | "commander-lvl2": "commander-lvl2.svg", 8 | "commander-lvl3": "commander-lvl3.svg", 9 | "commander-lvl4": "commander-lvl4.svg", 10 | constructedWall: "constructedWall.svg", 11 | controller: "controller.svg", 12 | "controller-level": "controller-level.svg", 13 | "disrupt-source": "disrupt-source.svg", 14 | cover: "cover.svg", 15 | "creep-npc": "creep-npc.svg", 16 | "creep-mask": "creep-mask.png", 17 | defend: "defend.svg", 18 | demolish: "demolish.svg", 19 | "deposit-biomass": "deposit-biomass.svg", 20 | "deposit-biomass-fill": "deposit-biomass-fill.svg", 21 | "deposit-metal": "deposit-metal.svg", 22 | "deposit-metal-fill": "deposit-metal-fill.svg", 23 | "deposit-mist": "deposit-mist.svg", 24 | "deposit-mist-fill": "deposit-mist-fill.svg", 25 | "deposit-silicon": "deposit-silicon.svg", 26 | "deposit-silicon-fill": "deposit-silicon-fill.svg", 27 | disable: "disable.svg", 28 | "disrupt-spawn": "disrupt-spawn.svg", 29 | "disrupt-tower": "disrupt-tower.svg", 30 | "disrupt-terminal": "disrupt-terminal.svg", 31 | "drain-extension": "drain-extension.svg", 32 | encourage: "encourage.svg", 33 | exhaust: "exhaust.svg", 34 | "regenerate-mineral": "regen-mineral.svg", 35 | "regenerate-source": "regen-source.svg", 36 | factory: "factory.svg", 37 | "factory-lvl0": "factory-lvl0.svg", 38 | "factory-lvl1": "factory-lvl1.svg", 39 | "factory-lvl2": "factory-lvl2.svg", 40 | "factory-lvl3": "factory-lvl3.svg", 41 | "factory-lvl4": "factory-lvl4.svg", 42 | "factory-lvl5": "factory-lvl5.svg", 43 | "factory-border": "factory-border.svg", 44 | "factory-highlight": "factory-highlight.svg", 45 | flag: "flag.svg", 46 | "flag-secondary": "flag-secondary.svg", 47 | "executor-lvl0": "executor-lvl0.svg", 48 | "executor-lvl1": "executor-lvl1.svg", 49 | "executor-lvl2": "executor-lvl2.svg", 50 | "executor-lvl3": "executor-lvl3.svg", 51 | "executor-lvl4": "executor-lvl4.svg", 52 | extension: "extension.svg", 53 | "extension-border50": "extension-border50.svg", 54 | "extension-border100": "extension-border100.svg", 55 | "extension-border200": "extension-border200.svg", 56 | extractor: "extractor.svg", 57 | "exit-left": "exit-left.svg", 58 | "exit-top": "exit-top.svg", 59 | "exit-bottom": "exit-bottom.svg", 60 | "exit-right": "exit-right.svg", 61 | flare1: "flare1.png", 62 | flare2: "flare2.png", 63 | flare3: "flare3.png", 64 | "harvest-energy": "harvest-energy.svg", 65 | "harvest-mineral": "harvest-mineral.svg", 66 | "generate-ops": "generate-ops.svg", 67 | glow: "glow.png", 68 | ground: "ground.png", 69 | "ground-mask": "ground-mask.png", 70 | // "ground-mask2": "ground-mask2.png", 71 | kill: "kill.svg", 72 | lab: "lab.svg", 73 | "lab-highlight": "lab-highlight.svg", 74 | "lab-mineral": "lab-mineral.svg", 75 | link: "link.svg", 76 | "link-energy": "link-energy.svg", 77 | "link-border": "link-border.svg", 78 | "mass-repair": "mass-repair.svg", 79 | noise1: "noise1.png", 80 | noise2: "noise2.png", 81 | nuke: "nuke.svg", 82 | nuker: "nuker.svg", 83 | "nuker-border": "nuker-border.svg", 84 | "operate-extension": "operate-extension.svg", 85 | "operate-lab": "operate-lab.svg", 86 | "operate-observer": "operate-observer.svg", 87 | "operate-spawn": "operate-spawn.svg", 88 | "operate-storage": "operate-storage.svg", 89 | "operate-terminal": "operate-terminal.svg", 90 | "operate-tower": "operate-tower.svg", 91 | "operator-lvl0": "operator-lvl0.svg", 92 | "operator-lvl1": "operator-lvl1.svg", 93 | "operator-lvl2": "operator-lvl2.svg", 94 | "operator-lvl3": "operator-lvl3.svg", 95 | "operator-lvl4": "operator-lvl4.svg", 96 | powerBank: "powerBank.svg", 97 | punch: "punch.svg", 98 | rampart: "rampart.svg", 99 | rectangle: "rectangle.svg", 100 | reflect: "reflect.svg", 101 | reinforce: "reinforce.svg", 102 | "remote-transfer": "remote-transfer.svg", 103 | renew: "renew.svg", 104 | shield: "shield.svg", 105 | sight: "sight.svg", 106 | snipe: "snipe.svg", 107 | storage: "storage.svg", 108 | "storage-border": "storage-border.svg", 109 | summon: "summon.svg", 110 | tbd: "tbd.svg", 111 | terminal: "terminal.svg", 112 | "terminal-border": "terminal-border.svg", 113 | "terminal-highlight": "terminal-highlight.svg", 114 | "terminal-arrows": "terminal-arrows.svg", 115 | "tombstone-border": "tombstone-border.svg", 116 | "tombstone-resource": "tombstone-resource.svg", 117 | tough: "tough.svg", 118 | "tower-base": "tower-base.svg", 119 | "tower-rotatable": "tower-rotatable.svg", 120 | "tower-rotatable-npc": "tower-rotatable-npc.svg", 121 | "invaderCore": "invaderCore.svg", 122 | "ruin": "ruin.svg", 123 | }; 124 | 125 | for (const k in resourceMap) { 126 | resourceMap[k] = `https://screeps.devtips.cn/metadata/${resourceMap[k]}`; 127 | } 128 | 129 | const rescaleResources: string[] = [ 130 | "bodyPartBar", 131 | "controller", 132 | "controller-level", 133 | "constructedWall", 134 | "creep-npc", 135 | "extension-border50", 136 | "extension-border100", 137 | "extension-border200", 138 | "extractor", 139 | "flag", 140 | "flag-secondary", 141 | "lab", 142 | "link", 143 | "link-border", 144 | "nuker", 145 | "nuker-border", 146 | "powerBank", 147 | "storage", 148 | "storage-border", 149 | "terminal-border", 150 | "terminal-arrows", 151 | "tombstone-border", 152 | "tombstone-resource", 153 | "tower-base", 154 | "factory", 155 | "factory-lvl0", 156 | "factory-lvl1", 157 | "factory-lvl2", 158 | "factory-lvl3", 159 | "factory-lvl4", 160 | "factory-lvl5", 161 | "factory-border", 162 | "factory-highlight", 163 | "invaderCore", 164 | "ruin", 165 | ]; 166 | 167 | export { rescaleResources, resourceMap }; 168 | -------------------------------------------------------------------------------- /src/config/worldConfigs.ts: -------------------------------------------------------------------------------- 1 | import "@screeps/renderer-metadata/dist/renderer-metadata"; 2 | 3 | const woldConfigs = { 4 | ATTACK_PENETRATION: 10, 5 | CELL_SIZE: 100, 6 | RENDER_SIZE: { 7 | width: 2048, 8 | height: 2048, 9 | }, 10 | VIEW_BOX: 5000, 11 | BADGE_URL: "https://screeps.devtips.cn/api/user/badge-svg?username=%1", 12 | metadata: window.RENDERER_METADATA, 13 | gameData: { 14 | player: "5fd8550610a11805f1ab1bf5", 15 | showMyNames: { 16 | spawns: true, 17 | creeps: true, 18 | }, 19 | showEnemyNames: { 20 | spawns: true, 21 | creeps: true, 22 | }, 23 | showFlagsNames: true, 24 | showCreepSpeech: false, 25 | swampTexture: "animated", 26 | }, 27 | lighting: "normal", 28 | forceCanvas: false, 29 | }; 30 | 31 | export default woldConfigs; 32 | -------------------------------------------------------------------------------- /src/hooks/useAuth.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | import api from "../api"; 4 | 5 | /** Authentication */ 6 | function useAuth( 7 | username: string, 8 | password: string, 9 | ): [string | null, boolean, Error | null] { 10 | const [token, setToken] = useState(null); 11 | const [loading, setLoading] = useState(true); 12 | const [error, setError] = useState(null); 13 | 14 | useEffect(() => { 15 | setLoading(true); 16 | Promise.resolve() 17 | .then(() => api.auth(username, password)) 18 | .then((res) => { 19 | setToken(res.token); 20 | setLoading(false); 21 | }) 22 | .then(() => api.socket.connect()) 23 | .catch((ex) => { 24 | setError(ex); 25 | setLoading(false); 26 | }); 27 | }, [username, password]); 28 | 29 | return [token, loading, error]; 30 | } 31 | 32 | export default useAuth; 33 | -------------------------------------------------------------------------------- /src/hooks/useWorldStartRoom.ts: -------------------------------------------------------------------------------- 1 | import api from "../api"; 2 | 3 | type Response = { 4 | ok: number; 5 | room: [string]; 6 | }; 7 | 8 | function useWorldStartRoom(): Promise { 9 | return api.raw.user.worldStartRoom(); 10 | } 11 | 12 | export default useWorldStartRoom; 13 | -------------------------------------------------------------------------------- /src/hooks/useZoom.ts: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export default function useZoom(initialZoom = 1.0) { 4 | const [zoom, setZoom] = useState(initialZoom); 5 | 6 | const zoomIn = () => setZoom(zoom + 0.1); 7 | const zoomOut = () => setZoom(zoom - 0.1); 8 | 9 | return [zoom, zoomIn, zoomOut] as const; 10 | } 11 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import reportWebVitals from "./reportWebVitals"; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById("root"), 11 | ); 12 | 13 | // If you want to start measuring performance in your app, pass a function 14 | // to log results (for example: reportWebVitals(console.log)) 15 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 16 | reportWebVitals(); 17 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from "web-vitals"; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom"; 6 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | 3 | function decode( 4 | room: string, 5 | terrain: string, 6 | ): Array<{ room: string; x: number; y: number; type: string }> { 7 | const decoded = []; 8 | for (let i = 0; i < terrain.length; i++) { 9 | decoded.push({ 10 | room, 11 | x: i % 50, 12 | y: Math.floor(i / 50), 13 | type: ["plain", "wall", "swamp", "wall"][terrain[i] as unknown as number], 14 | }); 15 | } 16 | return decoded; 17 | } 18 | 19 | export function decodeTerrain(terrain: API.TerrainEncoded): API.Terrain { 20 | return terrain.flatMap((t) => decode(t.room, t.terrain)); 21 | } 22 | 23 | export function arraysToObject(obj: Record) { 24 | for (var key in obj) { 25 | if (_.isArray(obj[key])) { 26 | var result: Record = {}; 27 | for (var i = 0; i < obj[key].length; i++) { 28 | result[i] = obj[key][i]; 29 | } 30 | obj[key] = result; 31 | } 32 | } 33 | } 34 | 35 | export function applyDiff(objects: object[], diff: object[]) { 36 | for (var id in diff) { 37 | var objDiff = diff[id]; 38 | var obj = _.find(objects, { _id: id }); 39 | if (obj) { 40 | if (objDiff !== null) { 41 | arraysToObject(obj); 42 | arraysToObject(objDiff); 43 | obj = _.merge(obj, objDiff, (a: unknown, b: unknown) => { 44 | if (_.isArray(a) && _.isArray(b)) { 45 | return b; 46 | } 47 | }); 48 | } else { 49 | _.remove(objects, { _id: id }); 50 | } 51 | } else if (objDiff) { 52 | obj = _.cloneDeep(objDiff); 53 | objects.push(obj); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src", 25 | "types" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /types/api.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace API { 2 | type Terrain = Array<{ 3 | room: string; 4 | x: number; 5 | y: number; 6 | type: string; 7 | }>; 8 | 9 | type TerrainEncoded = Array<{ 10 | _id: string; 11 | room: string; 12 | terrain: string; 13 | }>; 14 | 15 | interface RoomEvent { 16 | data: { 17 | gameTime: number; 18 | info: Record; 19 | visual: string; 20 | objects: Record[]; 21 | users?: { 22 | _id: string; 23 | username: string; 24 | badge?: { 25 | color1: string; 26 | color2: string; 27 | color3: string; 28 | flip: boolean; 29 | param: number; 30 | type: number; 31 | }; 32 | }; 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare var RENDERER_METADATA: any; 2 | -------------------------------------------------------------------------------- /types/screeps_renderer.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module "@screeps/renderer" { 4 | import Application = PIXI.Application; 5 | import Container = PIXI.Container; 6 | import Resource = PIXI.loaders.Resource; 7 | import ResourceDictionary = PIXI.loaders.ResourceDictionary; 8 | 9 | export interface WorldOptions extends WorldConfigs { 10 | actionManager: ActionManager; 11 | app: Application; 12 | logger: object; 13 | objectFilter: ObjectFilterFunc; 14 | resourceMap: { [key: string]: string }; 15 | rescaleResources: Array; 16 | size: Size; 17 | } 18 | 19 | export default GameRenderer; 20 | 21 | export class GameRenderer { 22 | static isWebGLSupported: boolean; 23 | static compileMetadata(metadata: any): void; 24 | 25 | metrics: GameMetrics; 26 | 27 | constructor(options: { 28 | autoFocus?: boolean; 29 | autoStart?: boolean; 30 | useDefaultLogger?: boolean; 31 | logger?: object; 32 | size?: Size; 33 | worldConfigs: WorldConfigs; 34 | resourceMap: { [key: string]: string }; 35 | rescaleResources: Array; 36 | objectFilter?: ObjectFilterFunc; 37 | onGameLoop?: () => void; 38 | countMetrics?: boolean; 39 | backgroundColor?: number; 40 | }); 41 | 42 | init(container: object): Promise; 43 | 44 | release(): void; 45 | 46 | start(): void; 47 | 48 | animateChecker(): void; 49 | 50 | applyState(state: Partial, tickDuration: number): void; 51 | 52 | set zoomLevel(value: number); 53 | 54 | set cameraPosition(position: Point); 55 | 56 | setTerrain(terrain: Array): void; 57 | 58 | resize(newSize?: Size): void; 59 | 60 | animate(): void; 61 | 62 | zoomTo(value: number, x: number, y: number): void; 63 | pan(x: number, y: number): void; 64 | } 65 | 66 | export class World { 67 | constructor(options: WorldOptions); 68 | 69 | init(): Promise; 70 | 71 | applyState( 72 | state: Partial, 73 | tickDuration: number, 74 | globalOnly: boolean, 75 | ): void; 76 | 77 | runStatePreprocessor( 78 | preprocessors: Array, 79 | preprocessorParams: PreprocessorParams, 80 | ); 81 | 82 | release(): void; 83 | 84 | get metrics(): Metrics; 85 | 86 | getWorldPosition(): Point; 87 | 88 | createData(options: { layer: string }): Container; 89 | 90 | destroyData(container: Container): void; 91 | 92 | runProcessor( 93 | processorMetadata: ProcessorMetadata, 94 | processorParams: ProcessorParams, 95 | ): Container | void; 96 | 97 | destructProcessor( 98 | processorMetadata: ProcessorMetadata, 99 | processorParams: ProcessorParams, 100 | container: Container, 101 | ): void; 102 | 103 | runActions( 104 | actionsMeta: Array, 105 | processorParams: ProcessorParams, 106 | container: Container, 107 | ): Array; 108 | 109 | cancelActions(actions: Array): void; 110 | 111 | cancelActionsForObj(container: Container): void; 112 | 113 | finishActions(actions: Array): void; 114 | 115 | countObjects(container: Container): number; 116 | } 117 | 118 | /** 119 | * It's ts doc 120 | */ 121 | export class GameObject { 122 | rootContainer: Container; 123 | constructor(id: string, objectMetadata: ObjectMetadata, world: World); 124 | 125 | remove(tickDuration: number): void; 126 | 127 | applyState( 128 | objectState: ObjectState, 129 | tickDuration: number, 130 | state: State, 131 | ): void; 132 | 133 | propsChanged( 134 | runnableMetadata: RunnableMetadata, 135 | stateParams: StateParams, 136 | ): boolean; 137 | 138 | shouldRun( 139 | runnableMetadata: RunnableMetadata, 140 | stateParams: StateParams, 141 | context: { propsChanged: boolean; firstRun: boolean }, 142 | ): boolean; 143 | 144 | onceAllow( 145 | runnableMetadata: RunnableMetadata, 146 | context: { propsChanged: boolean; firstRun: boolean }, 147 | ): boolean; 148 | 149 | shouldDestruct( 150 | runnableMetadata: RunnableMetadata, 151 | stateParams: StateParams, 152 | context: { propsChanged: boolean; firstRun: boolean }, 153 | ): boolean; 154 | 155 | destructProcessor( 156 | scope: Scope, 157 | processorMetadata: ProcessorMetadata, 158 | processorParams: ProcessorParams, 159 | ): void; 160 | 161 | get rendererCounter(): number; 162 | } 163 | 164 | export class ResourceManager { 165 | constructor(options: { logger: object; app: Application }); 166 | load(): Promise; 167 | getResource(name: string, ...params: any[]): Promise; 168 | getCachedResource(name: string): void | Resource; 169 | release(): void; 170 | } 171 | 172 | export type Metadata = { 173 | preprocessors: Array; 174 | layers: Array; 175 | objects: { [key: string]: ObjectMetadata }; 176 | }; 177 | 178 | export type Expression = 179 | | undefined 180 | | null 181 | | Array> 182 | | PredefinedExpression 183 | | ActionMetadata 184 | | { [key: string]: Expression } 185 | | T; 186 | 187 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 188 | export class PredefinedExpression { 189 | } 190 | 191 | export class AddExpression extends PredefinedExpression { 192 | $add: [Array>]; 193 | } 194 | 195 | export class AndExpression extends PredefinedExpression { 196 | $and: [Array>]; 197 | } 198 | 199 | export class CalcExpression extends PredefinedExpression { 200 | $calc: string; 201 | default?: number; 202 | koef?: number; 203 | } 204 | 205 | export class DivExpression extends PredefinedExpression { 206 | $div: [Expression, Expression]; 207 | } 208 | 209 | export class GtExpression extends PredefinedExpression { 210 | $gt: [Expression, Expression]; 211 | } 212 | 213 | export class GteExpression extends PredefinedExpression { 214 | $gte: [Expression, Expression]; 215 | } 216 | 217 | export class IfExpression extends PredefinedExpression { 218 | $if: Expression; 219 | then?: Expression; 220 | else?: Expression; 221 | } 222 | 223 | export class LtExpression extends PredefinedExpression { 224 | $lt: [Expression, Expression]; 225 | } 226 | 227 | export class LteExpression extends PredefinedExpression { 228 | $lte: [Expression, Expression]; 229 | } 230 | 231 | export class MinExpression extends PredefinedExpression { 232 | $min: [Array]; 233 | } 234 | 235 | export class MaxExpression extends PredefinedExpression { 236 | $max: [Array]; 237 | } 238 | 239 | export class MulExpression extends PredefinedExpression { 240 | $mul: [Array]; 241 | } 242 | 243 | export class NotExpression extends PredefinedExpression { 244 | $not: Expression; 245 | } 246 | 247 | export class OrExpression extends PredefinedExpression { 248 | $or: [Array]; 249 | } 250 | 251 | export class ProcessorParamExpression extends PredefinedExpression { 252 | $processorParam: string; 253 | default?: number; 254 | koef?: number; 255 | } 256 | 257 | export class RandomExpression extends PredefinedExpression { 258 | $random: number; 259 | } 260 | 261 | export class RelExpression extends PredefinedExpression { 262 | $rel: string; 263 | default?: number; 264 | koef?: number; 265 | } 266 | 267 | export class StateExpression extends PredefinedExpression { 268 | $state: string; 269 | default?: number; 270 | koef?: number; 271 | } 272 | 273 | export class SubExpression extends PredefinedExpression { 274 | $sub: [Array]; 275 | } 276 | 277 | export type Preprocessor = (params: PreprocessorParams) => void; 278 | export type Calculation = (params: CalculationParams) => any; 279 | 280 | export interface RunnableMetadata { 281 | props?: string | Array; 282 | once?: boolean; 283 | /** 284 | * @deprecated Use when instead. 285 | * @param {"render-engine".StateParams} params 286 | * @return {boolean} 287 | */ 288 | shouldRun?: (params: StateParams) => boolean | Expression; 289 | until?: (params: StateParams) => boolean | Expression; 290 | when?: (params: StateParams) => boolean | Expression; 291 | } 292 | 293 | export interface ActionMetadata extends RunnableMetadata { 294 | action: string; 295 | params: Array; 296 | } 297 | 298 | export interface CalculationMetadata extends RunnableMetadata { 299 | id: string; 300 | func: Calculation | Expression; 301 | } 302 | 303 | export interface ProcessorMetadata 304 | extends RunnableMetadata { 305 | type: string; 306 | id: string; 307 | payload?: T; 308 | actions?: Array; 309 | layer?: string; 310 | } 311 | 312 | export interface ProcessorActionMetadata extends RunnableMetadata { 313 | id?: string; 314 | targetId?: string; 315 | actions?: Array; 316 | } 317 | 318 | export interface ObjectMetadata extends RunnableMetadata { 319 | data?: { [key: string]: any }; 320 | texture?: string; 321 | calculations?: Array; 322 | processors?: Array; 323 | disappearProcessor?: ProcessorMetadata; 324 | actions?: Array; 325 | zIndex?: number; 326 | } 327 | 328 | export type LayerMetadata = { 329 | id: string; 330 | isDefault?: boolean; 331 | afterCreate: (params: LayerParams) => Promise; 332 | }; 333 | 334 | export class ProcessorPayload { 335 | } 336 | 337 | // eslint-disable-next-line @typescript-eslint/no-use-before-define 338 | export class CircleProcessorPayload extends DrawProcessorPayload { 339 | color?: number; 340 | radius?: number; 341 | stroke?: number; 342 | strokeWidth?: number; 343 | } 344 | 345 | // eslint-disable-next-line @typescript-eslint/no-use-before-define 346 | export class ContainerProcessorPayload extends ObjectProcessorPayload { 347 | } 348 | 349 | export class CreepActionsProcessorPayload extends ProcessorPayload { 350 | parentId?: string; 351 | } 352 | 353 | export class CreepBuildBodyProcessorPayload extends ProcessorPayload { 354 | parentId?: string; 355 | } 356 | 357 | // eslint-disable-next-line @typescript-eslint/no-use-before-define 358 | export class DrawProcessorPayload extends ObjectProcessorPayload { 359 | drawings?: Array<{ 360 | method: string; 361 | params: Array; 362 | }>; 363 | } 364 | 365 | export class MoveToProcessorPayload extends ProcessorPayload { 366 | targetKey?: string; 367 | shouldRotate?: boolean; 368 | } 369 | 370 | export class ObjectProcessorPayload extends ProcessorPayload { 371 | addToParent?: boolean; 372 | anchor?: { x: Expression; y: Expression } | Expression; 373 | blur?: Expression; 374 | Class?: Container; 375 | constructorParams?: Array; 376 | id?: string; 377 | parentId?: string; 378 | pivot?: { x: Expression; y: Expression } | Expression; 379 | scale?: { x: Expression; y: Expression } | Expression; 380 | shouldCreate?: boolean; 381 | height?: Expression; 382 | width?: Expression; 383 | [key: string]: Expression 384 | } 385 | 386 | export class ResourceCircleProcessorPayload extends CircleProcessorPayload { 387 | } 388 | 389 | export class RunActionProcessorPayload extends ProcessorPayload { 390 | id?: string; 391 | } 392 | 393 | export class SayProcessorPayload extends ContainerProcessorPayload { 394 | say: Expression; 395 | } 396 | 397 | export class SiteProgressProcessorPayload extends ProcessorPayload { 398 | color: Expression; 399 | lineWidth: Expression; 400 | progressTotal: Expression; 401 | radius: Expression; 402 | } 403 | 404 | export class SpriteProcessorPayload extends ObjectProcessorPayload { 405 | } 406 | 407 | export class TextProcessorPayload extends ProcessorPayload { 408 | style?: Expression; 409 | text?: Expression; 410 | } 411 | 412 | export class UserBadgeProcessorPayload extends ProcessorPayload { 413 | parentId?: string; 414 | radius?: number; 415 | color?: number; 416 | } 417 | 418 | export interface WorldConfigs { 419 | ATTACK_PENETRATION: number; 420 | CELL_SIZE: number; 421 | RENDER_SIZE: { 422 | width: number; 423 | height: number; 424 | }; 425 | VIEW_BOX: number; 426 | BADGE_URL: string; 427 | metadata: Metadata; 428 | gameData: GameData; 429 | lighting: string; 430 | forceCanvas: boolean; 431 | } 432 | 433 | export type GameData = { 434 | player: string; 435 | showMyNames: { 436 | spawns: boolean; 437 | creeps: boolean; 438 | }; 439 | showEnemyNames: { 440 | spawns: boolean; 441 | creeps: boolean; 442 | }; 443 | showFlagsNames: boolean; 444 | showCreepSpeech: boolean; 445 | swampTexture: string; 446 | }; 447 | 448 | export type ObjectFilterFunc = ( 449 | objects: Array, 450 | ) => Array; 451 | 452 | export type Point = { 453 | width: number; 454 | height: number; 455 | }; 456 | 457 | export type Size = { 458 | width: number; 459 | height: number; 460 | }; 461 | 462 | export interface PreprocessorParams { 463 | state: State; 464 | world: World; 465 | } 466 | 467 | export interface StateParams { 468 | calcs: { [key: string]: any }; 469 | firstRun: boolean; 470 | objectMetadata: ObjectMetadata; 471 | prevCalcs: { [key: string]: any }; 472 | prevState: ObjectState; 473 | world: World; 474 | rootContainer: Container; 475 | scope: Scope; 476 | state: ObjectState; 477 | stateExtra: State; 478 | tickDuration: number; 479 | } 480 | 481 | export interface ProcessorParams extends StateParams, ProcessorMetadata { 482 | } 483 | 484 | export interface CalculationParams extends StateParams { 485 | payload?: object; 486 | } 487 | 488 | export interface LayerParams { 489 | app: Application; 490 | resourceManager: ResourceManager; 491 | world: World; 492 | } 493 | 494 | export type State = { 495 | objects: Array; 496 | gameData: GameData; 497 | }; 498 | 499 | export type ObjectState = { 500 | type: string; 501 | _id: string; 502 | room: string; 503 | x: number; 504 | y: number; 505 | [key: string]: any; 506 | }; 507 | 508 | export type Scope = { 509 | processors: { [key: string]: any }; 510 | }; 511 | 512 | export type Metrics = { 513 | gameObjectCounter: number; 514 | rendererCounter: number; 515 | devicePixelRatio: number; 516 | renderer: { 517 | size: number; 518 | maxSvgSize: number; 519 | }; 520 | }; 521 | export type GameMetrics = Metrics & { fps: number }; 522 | 523 | export class ActionManager { 524 | constructor(); 525 | update(delta: number): void; 526 | runAction(container: Container, action: Action): ActionHandle; 527 | cancelAction(actionHandle: ActionHandle); 528 | void; 529 | finishAction(actionHandle: ActionHandle); 530 | void; 531 | cancelActionForContainer(container: Container); 532 | void; 533 | } 534 | 535 | export class Action { 536 | reset(): void; 537 | update(): boolean; 538 | finish(): void; 539 | } 540 | 541 | export class ActionHandle { 542 | container: Container; 543 | action: Action; 544 | constructor(container: Container, action: Action); 545 | update(delta: number): void; 546 | isEnded(): boolean; 547 | } 548 | } 549 | --------------------------------------------------------------------------------