├── .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 |

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 |
--------------------------------------------------------------------------------