From ebbfed10ce87da8f3eafd447fd8f4cd71910758b Mon Sep 17 00:00:00 2001 From: mumra Date: Tue, 14 Oct 2025 15:06:21 +0100 Subject: [PATCH 1/6] Create Vite projects for lobby and game clients This adds two Vite projects (a JavaScript build tool) for the lobby and game clients, which need to be built as separate bundles. These are just skeletons for now, files will be moved and altered in following commits in an attempt to minimise git conflicts for anyone who might have .js changes in a branch or fork. --- crawl-ref/source/webserver/.gitignore | 1 + .../source/webserver/client/game/.gitignore | 24 + .../source/webserver/client/game/index.html | 13 + .../webserver/client/game/package-lock.json | 1074 +++++++++++++++++ .../source/webserver/client/game/package.json | 15 + .../webserver/client/game/public/vite.svg | 1 + .../webserver/client/game/src/counter.ts | 9 + .../source/webserver/client/game/src/main.ts | 24 + .../webserver/client/game/src/style.css | 96 ++ .../webserver/client/game/src/typescript.svg | 1 + .../webserver/client/game/tsconfig.json | 26 + .../source/webserver/client/lobby/.gitignore | 24 + .../source/webserver/client/lobby/index.html | 13 + .../webserver/client/lobby/package-lock.json | 1074 +++++++++++++++++ .../webserver/client/lobby/package.json | 15 + .../webserver/client/lobby/public/vite.svg | 1 + .../webserver/client/lobby/src/counter.ts | 9 + .../source/webserver/client/lobby/src/main.ts | 24 + .../webserver/client/lobby/src/style.css | 96 ++ .../webserver/client/lobby/src/typescript.svg | 1 + .../webserver/client/lobby/tsconfig.json | 26 + 21 files changed, 2567 insertions(+) create mode 100644 crawl-ref/source/webserver/client/game/.gitignore create mode 100644 crawl-ref/source/webserver/client/game/index.html create mode 100644 crawl-ref/source/webserver/client/game/package-lock.json create mode 100644 crawl-ref/source/webserver/client/game/package.json create mode 100644 crawl-ref/source/webserver/client/game/public/vite.svg create mode 100644 crawl-ref/source/webserver/client/game/src/counter.ts create mode 100644 crawl-ref/source/webserver/client/game/src/main.ts create mode 100644 crawl-ref/source/webserver/client/game/src/style.css create mode 100644 crawl-ref/source/webserver/client/game/src/typescript.svg create mode 100644 crawl-ref/source/webserver/client/game/tsconfig.json create mode 100644 crawl-ref/source/webserver/client/lobby/.gitignore create mode 100644 crawl-ref/source/webserver/client/lobby/index.html create mode 100644 crawl-ref/source/webserver/client/lobby/package-lock.json create mode 100644 crawl-ref/source/webserver/client/lobby/package.json create mode 100644 crawl-ref/source/webserver/client/lobby/public/vite.svg create mode 100644 crawl-ref/source/webserver/client/lobby/src/counter.ts create mode 100644 crawl-ref/source/webserver/client/lobby/src/main.ts create mode 100644 crawl-ref/source/webserver/client/lobby/src/style.css create mode 100644 crawl-ref/source/webserver/client/lobby/src/typescript.svg create mode 100644 crawl-ref/source/webserver/client/lobby/tsconfig.json diff --git a/crawl-ref/source/webserver/.gitignore b/crawl-ref/source/webserver/.gitignore index 4c86c436c01..d3e0c2dc16c 100644 --- a/crawl-ref/source/webserver/.gitignore +++ b/crawl-ref/source/webserver/.gitignore @@ -8,3 +8,4 @@ banned_players.txt webtiles/version.txt Pipfile Pipfile.lock +node_modules/ \ No newline at end of file diff --git a/crawl-ref/source/webserver/client/game/.gitignore b/crawl-ref/source/webserver/client/game/.gitignore new file mode 100644 index 00000000000..a547bf36d8d --- /dev/null +++ b/crawl-ref/source/webserver/client/game/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/crawl-ref/source/webserver/client/game/index.html b/crawl-ref/source/webserver/client/game/index.html new file mode 100644 index 00000000000..749474f06af --- /dev/null +++ b/crawl-ref/source/webserver/client/game/index.html @@ -0,0 +1,13 @@ + + + + + + + Crawl Game + + +
+ + + diff --git a/crawl-ref/source/webserver/client/game/package-lock.json b/crawl-ref/source/webserver/client/game/package-lock.json new file mode 100644 index 00000000000..7df11ffcd9c --- /dev/null +++ b/crawl-ref/source/webserver/client/game/package-lock.json @@ -0,0 +1,1074 @@ +{ + "name": "lobby", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lobby", + "version": "0.0.0", + "devDependencies": { + "typescript": "~5.9.3", + "vite": "^7.1.7" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", + "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz", + "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz", + "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz", + "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz", + "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz", + "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz", + "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz", + "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz", + "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz", + "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz", + "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz", + "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz", + "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz", + "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz", + "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz", + "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz", + "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz", + "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz", + "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz", + "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz", + "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz", + "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", + "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.4", + "@rollup/rollup-android-arm64": "4.52.4", + "@rollup/rollup-darwin-arm64": "4.52.4", + "@rollup/rollup-darwin-x64": "4.52.4", + "@rollup/rollup-freebsd-arm64": "4.52.4", + "@rollup/rollup-freebsd-x64": "4.52.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", + "@rollup/rollup-linux-arm-musleabihf": "4.52.4", + "@rollup/rollup-linux-arm64-gnu": "4.52.4", + "@rollup/rollup-linux-arm64-musl": "4.52.4", + "@rollup/rollup-linux-loong64-gnu": "4.52.4", + "@rollup/rollup-linux-ppc64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-musl": "4.52.4", + "@rollup/rollup-linux-s390x-gnu": "4.52.4", + "@rollup/rollup-linux-x64-gnu": "4.52.4", + "@rollup/rollup-linux-x64-musl": "4.52.4", + "@rollup/rollup-openharmony-arm64": "4.52.4", + "@rollup/rollup-win32-arm64-msvc": "4.52.4", + "@rollup/rollup-win32-ia32-msvc": "4.52.4", + "@rollup/rollup-win32-x64-gnu": "4.52.4", + "@rollup/rollup-win32-x64-msvc": "4.52.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "7.1.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.10.tgz", + "integrity": "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/crawl-ref/source/webserver/client/game/package.json b/crawl-ref/source/webserver/client/game/package.json new file mode 100644 index 00000000000..4cf4f4d1d3b --- /dev/null +++ b/crawl-ref/source/webserver/client/game/package.json @@ -0,0 +1,15 @@ +{ + "name": "crawl-game", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "devDependencies": { + "typescript": "~5.9.3", + "vite": "^7.1.7" + } +} diff --git a/crawl-ref/source/webserver/client/game/public/vite.svg b/crawl-ref/source/webserver/client/game/public/vite.svg new file mode 100644 index 00000000000..e7b8dfb1b2a --- /dev/null +++ b/crawl-ref/source/webserver/client/game/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/crawl-ref/source/webserver/client/game/src/counter.ts b/crawl-ref/source/webserver/client/game/src/counter.ts new file mode 100644 index 00000000000..09e5afd2d8a --- /dev/null +++ b/crawl-ref/source/webserver/client/game/src/counter.ts @@ -0,0 +1,9 @@ +export function setupCounter(element: HTMLButtonElement) { + let counter = 0 + const setCounter = (count: number) => { + counter = count + element.innerHTML = `count is ${counter}` + } + element.addEventListener('click', () => setCounter(counter + 1)) + setCounter(0) +} diff --git a/crawl-ref/source/webserver/client/game/src/main.ts b/crawl-ref/source/webserver/client/game/src/main.ts new file mode 100644 index 00000000000..6396b50a232 --- /dev/null +++ b/crawl-ref/source/webserver/client/game/src/main.ts @@ -0,0 +1,24 @@ +import './style.css' +import typescriptLogo from './typescript.svg' +import viteLogo from '/vite.svg' +import { setupCounter } from './counter.ts' + +document.querySelector('#app')!.innerHTML = ` +
+ + + + + + +

Vite + TypeScript

+
+ +
+

+ Click on the Vite and TypeScript logos to learn more +

+
+` + +setupCounter(document.querySelector('#counter')!) diff --git a/crawl-ref/source/webserver/client/game/src/style.css b/crawl-ref/source/webserver/client/game/src/style.css new file mode 100644 index 00000000000..3bcdbd00d55 --- /dev/null +++ b/crawl-ref/source/webserver/client/game/src/style.css @@ -0,0 +1,96 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.vanilla:hover { + filter: drop-shadow(0 0 2em #3178c6aa); +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/crawl-ref/source/webserver/client/game/src/typescript.svg b/crawl-ref/source/webserver/client/game/src/typescript.svg new file mode 100644 index 00000000000..d91c910cc30 --- /dev/null +++ b/crawl-ref/source/webserver/client/game/src/typescript.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/crawl-ref/source/webserver/client/game/tsconfig.json b/crawl-ref/source/webserver/client/game/tsconfig.json new file mode 100644 index 00000000000..4ba8dd95cfe --- /dev/null +++ b/crawl-ref/source/webserver/client/game/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/crawl-ref/source/webserver/client/lobby/.gitignore b/crawl-ref/source/webserver/client/lobby/.gitignore new file mode 100644 index 00000000000..a547bf36d8d --- /dev/null +++ b/crawl-ref/source/webserver/client/lobby/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/crawl-ref/source/webserver/client/lobby/index.html b/crawl-ref/source/webserver/client/lobby/index.html new file mode 100644 index 00000000000..7eb31ec53c0 --- /dev/null +++ b/crawl-ref/source/webserver/client/lobby/index.html @@ -0,0 +1,13 @@ + + + + + + + Crawl Lobby + + +
+ + + diff --git a/crawl-ref/source/webserver/client/lobby/package-lock.json b/crawl-ref/source/webserver/client/lobby/package-lock.json new file mode 100644 index 00000000000..7df11ffcd9c --- /dev/null +++ b/crawl-ref/source/webserver/client/lobby/package-lock.json @@ -0,0 +1,1074 @@ +{ + "name": "lobby", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lobby", + "version": "0.0.0", + "devDependencies": { + "typescript": "~5.9.3", + "vite": "^7.1.7" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", + "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz", + "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz", + "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz", + "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz", + "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz", + "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz", + "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz", + "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz", + "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz", + "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz", + "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz", + "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz", + "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz", + "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz", + "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz", + "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz", + "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz", + "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz", + "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz", + "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz", + "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz", + "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", + "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.4", + "@rollup/rollup-android-arm64": "4.52.4", + "@rollup/rollup-darwin-arm64": "4.52.4", + "@rollup/rollup-darwin-x64": "4.52.4", + "@rollup/rollup-freebsd-arm64": "4.52.4", + "@rollup/rollup-freebsd-x64": "4.52.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", + "@rollup/rollup-linux-arm-musleabihf": "4.52.4", + "@rollup/rollup-linux-arm64-gnu": "4.52.4", + "@rollup/rollup-linux-arm64-musl": "4.52.4", + "@rollup/rollup-linux-loong64-gnu": "4.52.4", + "@rollup/rollup-linux-ppc64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-musl": "4.52.4", + "@rollup/rollup-linux-s390x-gnu": "4.52.4", + "@rollup/rollup-linux-x64-gnu": "4.52.4", + "@rollup/rollup-linux-x64-musl": "4.52.4", + "@rollup/rollup-openharmony-arm64": "4.52.4", + "@rollup/rollup-win32-arm64-msvc": "4.52.4", + "@rollup/rollup-win32-ia32-msvc": "4.52.4", + "@rollup/rollup-win32-x64-gnu": "4.52.4", + "@rollup/rollup-win32-x64-msvc": "4.52.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "7.1.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.10.tgz", + "integrity": "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/crawl-ref/source/webserver/client/lobby/package.json b/crawl-ref/source/webserver/client/lobby/package.json new file mode 100644 index 00000000000..4e3625aaeee --- /dev/null +++ b/crawl-ref/source/webserver/client/lobby/package.json @@ -0,0 +1,15 @@ +{ + "name": "crawl-lobby", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "devDependencies": { + "typescript": "~5.9.3", + "vite": "^7.1.7" + } +} diff --git a/crawl-ref/source/webserver/client/lobby/public/vite.svg b/crawl-ref/source/webserver/client/lobby/public/vite.svg new file mode 100644 index 00000000000..e7b8dfb1b2a --- /dev/null +++ b/crawl-ref/source/webserver/client/lobby/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/crawl-ref/source/webserver/client/lobby/src/counter.ts b/crawl-ref/source/webserver/client/lobby/src/counter.ts new file mode 100644 index 00000000000..09e5afd2d8a --- /dev/null +++ b/crawl-ref/source/webserver/client/lobby/src/counter.ts @@ -0,0 +1,9 @@ +export function setupCounter(element: HTMLButtonElement) { + let counter = 0 + const setCounter = (count: number) => { + counter = count + element.innerHTML = `count is ${counter}` + } + element.addEventListener('click', () => setCounter(counter + 1)) + setCounter(0) +} diff --git a/crawl-ref/source/webserver/client/lobby/src/main.ts b/crawl-ref/source/webserver/client/lobby/src/main.ts new file mode 100644 index 00000000000..6396b50a232 --- /dev/null +++ b/crawl-ref/source/webserver/client/lobby/src/main.ts @@ -0,0 +1,24 @@ +import './style.css' +import typescriptLogo from './typescript.svg' +import viteLogo from '/vite.svg' +import { setupCounter } from './counter.ts' + +document.querySelector('#app')!.innerHTML = ` +
+ + + + + + +

Vite + TypeScript

+
+ +
+

+ Click on the Vite and TypeScript logos to learn more +

+
+` + +setupCounter(document.querySelector('#counter')!) diff --git a/crawl-ref/source/webserver/client/lobby/src/style.css b/crawl-ref/source/webserver/client/lobby/src/style.css new file mode 100644 index 00000000000..3bcdbd00d55 --- /dev/null +++ b/crawl-ref/source/webserver/client/lobby/src/style.css @@ -0,0 +1,96 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.vanilla:hover { + filter: drop-shadow(0 0 2em #3178c6aa); +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/crawl-ref/source/webserver/client/lobby/src/typescript.svg b/crawl-ref/source/webserver/client/lobby/src/typescript.svg new file mode 100644 index 00000000000..d91c910cc30 --- /dev/null +++ b/crawl-ref/source/webserver/client/lobby/src/typescript.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/crawl-ref/source/webserver/client/lobby/tsconfig.json b/crawl-ref/source/webserver/client/lobby/tsconfig.json new file mode 100644 index 00000000000..4ba8dd95cfe --- /dev/null +++ b/crawl-ref/source/webserver/client/lobby/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} From e2bb4dc2b526a4a913937b61777037a14c194341 Mon Sep 17 00:00:00 2001 From: mumra Date: Sun, 19 Oct 2025 09:06:12 +0100 Subject: [PATCH 2/6] Move static JavaScript to /src folders for bundling This moves all the static .js from webtiles into the appropriate project src file. This commit literally just moves the files so that git can track the history of the files and (hopefully) help resolve conflicts if any other developers have touched these files. --- .../game/src}/action_panel.js | 0 .../game/src}/cell_renderer.js | 0 .../webserver/client/game/src/counter.ts | 9 -- .../static => client/game/src}/debug.js | 0 .../static => client/game/src}/display.js | 0 .../game/src}/dungeon_renderer.js | 0 .../static => client/game/src}/enums.js | 0 .../static => client/game/src}/focus-trap.js | 0 .../static => client/game/src}/game.js | 0 .../source/webserver/client/game/src/main.js | 26 +++++ .../source/webserver/client/game/src/main.ts | 24 ----- .../game/src}/map_knowledge.js | 0 .../static => client/game/src}/menu.js | 0 .../static => client/game/src}/messages.js | 0 .../static => client/game/src}/minimap.js | 0 .../game/src}/monster_list.js | 0 .../game/src}/mouse_control.js | 0 .../static => client/game/src}/options.js | 0 .../static => client/game/src}/player.js | 0 .../static => client/game/src}/scroller.js | 0 .../static => client/game/src}/simplebar.js | 0 .../webserver/client/game/src/style.css | 96 ------------------- .../static => client/game/src}/text.js | 0 .../static => client/game/src}/textinput.js | 0 .../static => client/game/src}/tileinfos.js | 0 .../webserver/client/game/src/typescript.svg | 1 - .../static => client/game/src}/ui-layouts.js | 0 .../static => client/game/src}/ui.js | 0 .../static => client/game/src}/util.js | 0 .../static => client/game/src}/view_data.js | 0 .../scripts => client/lobby/src}/app.js | 0 .../scripts => client/lobby/src}/chat.js | 0 .../scripts => client/lobby/src}/client.js | 0 .../scripts => client/lobby/src}/comm.js | 0 .../webserver/client/lobby/src/counter.ts | 9 -- .../webserver/client/lobby/src/globals.js | 0 .../lobby/src}/key_conversion.js | 0 .../scripts => client/lobby/src}/linkify.js | 0 .../source/webserver/client/lobby/src/main.js | 21 ++++ .../source/webserver/client/lobby/src/main.ts | 24 ----- .../webserver/client/lobby/src/style.css | 96 ------------------- .../webserver/client/lobby/src/typescript.svg | 1 - 42 files changed, 47 insertions(+), 260 deletions(-) rename crawl-ref/source/webserver/{game_data/static => client/game/src}/action_panel.js (100%) rename crawl-ref/source/webserver/{game_data/static => client/game/src}/cell_renderer.js (100%) delete mode 100644 crawl-ref/source/webserver/client/game/src/counter.ts rename crawl-ref/source/webserver/{game_data/static => client/game/src}/debug.js (100%) rename crawl-ref/source/webserver/{game_data/static => client/game/src}/display.js (100%) rename crawl-ref/source/webserver/{game_data/static => client/game/src}/dungeon_renderer.js (100%) rename crawl-ref/source/webserver/{game_data/static => client/game/src}/enums.js (100%) rename crawl-ref/source/webserver/{game_data/static => client/game/src}/focus-trap.js (100%) rename crawl-ref/source/webserver/{game_data/static => client/game/src}/game.js (100%) create mode 100644 crawl-ref/source/webserver/client/game/src/main.js delete mode 100644 crawl-ref/source/webserver/client/game/src/main.ts rename crawl-ref/source/webserver/{game_data/static => client/game/src}/map_knowledge.js (100%) rename crawl-ref/source/webserver/{game_data/static => client/game/src}/menu.js (100%) rename crawl-ref/source/webserver/{game_data/static => client/game/src}/messages.js (100%) rename crawl-ref/source/webserver/{game_data/static => client/game/src}/minimap.js (100%) rename crawl-ref/source/webserver/{game_data/static => client/game/src}/monster_list.js (100%) rename crawl-ref/source/webserver/{game_data/static => client/game/src}/mouse_control.js (100%) rename crawl-ref/source/webserver/{game_data/static => client/game/src}/options.js (100%) rename crawl-ref/source/webserver/{game_data/static => client/game/src}/player.js (100%) rename crawl-ref/source/webserver/{game_data/static => client/game/src}/scroller.js (100%) rename crawl-ref/source/webserver/{game_data/static => client/game/src}/simplebar.js (100%) delete mode 100644 crawl-ref/source/webserver/client/game/src/style.css rename crawl-ref/source/webserver/{game_data/static => client/game/src}/text.js (100%) rename crawl-ref/source/webserver/{game_data/static => client/game/src}/textinput.js (100%) rename crawl-ref/source/webserver/{game_data/static => client/game/src}/tileinfos.js (100%) delete mode 100644 crawl-ref/source/webserver/client/game/src/typescript.svg rename crawl-ref/source/webserver/{game_data/static => client/game/src}/ui-layouts.js (100%) rename crawl-ref/source/webserver/{game_data/static => client/game/src}/ui.js (100%) rename crawl-ref/source/webserver/{game_data/static => client/game/src}/util.js (100%) rename crawl-ref/source/webserver/{game_data/static => client/game/src}/view_data.js (100%) rename crawl-ref/source/webserver/{static/scripts => client/lobby/src}/app.js (100%) rename crawl-ref/source/webserver/{static/scripts => client/lobby/src}/chat.js (100%) rename crawl-ref/source/webserver/{static/scripts => client/lobby/src}/client.js (100%) rename crawl-ref/source/webserver/{static/scripts => client/lobby/src}/comm.js (100%) delete mode 100644 crawl-ref/source/webserver/client/lobby/src/counter.ts create mode 100644 crawl-ref/source/webserver/client/lobby/src/globals.js rename crawl-ref/source/webserver/{static/scripts => client/lobby/src}/key_conversion.js (100%) rename crawl-ref/source/webserver/{static/scripts => client/lobby/src}/linkify.js (100%) create mode 100644 crawl-ref/source/webserver/client/lobby/src/main.js delete mode 100644 crawl-ref/source/webserver/client/lobby/src/main.ts delete mode 100644 crawl-ref/source/webserver/client/lobby/src/style.css delete mode 100644 crawl-ref/source/webserver/client/lobby/src/typescript.svg diff --git a/crawl-ref/source/webserver/game_data/static/action_panel.js b/crawl-ref/source/webserver/client/game/src/action_panel.js similarity index 100% rename from crawl-ref/source/webserver/game_data/static/action_panel.js rename to crawl-ref/source/webserver/client/game/src/action_panel.js diff --git a/crawl-ref/source/webserver/game_data/static/cell_renderer.js b/crawl-ref/source/webserver/client/game/src/cell_renderer.js similarity index 100% rename from crawl-ref/source/webserver/game_data/static/cell_renderer.js rename to crawl-ref/source/webserver/client/game/src/cell_renderer.js diff --git a/crawl-ref/source/webserver/client/game/src/counter.ts b/crawl-ref/source/webserver/client/game/src/counter.ts deleted file mode 100644 index 09e5afd2d8a..00000000000 --- a/crawl-ref/source/webserver/client/game/src/counter.ts +++ /dev/null @@ -1,9 +0,0 @@ -export function setupCounter(element: HTMLButtonElement) { - let counter = 0 - const setCounter = (count: number) => { - counter = count - element.innerHTML = `count is ${counter}` - } - element.addEventListener('click', () => setCounter(counter + 1)) - setCounter(0) -} diff --git a/crawl-ref/source/webserver/game_data/static/debug.js b/crawl-ref/source/webserver/client/game/src/debug.js similarity index 100% rename from crawl-ref/source/webserver/game_data/static/debug.js rename to crawl-ref/source/webserver/client/game/src/debug.js diff --git a/crawl-ref/source/webserver/game_data/static/display.js b/crawl-ref/source/webserver/client/game/src/display.js similarity index 100% rename from crawl-ref/source/webserver/game_data/static/display.js rename to crawl-ref/source/webserver/client/game/src/display.js diff --git a/crawl-ref/source/webserver/game_data/static/dungeon_renderer.js b/crawl-ref/source/webserver/client/game/src/dungeon_renderer.js similarity index 100% rename from crawl-ref/source/webserver/game_data/static/dungeon_renderer.js rename to crawl-ref/source/webserver/client/game/src/dungeon_renderer.js diff --git a/crawl-ref/source/webserver/game_data/static/enums.js b/crawl-ref/source/webserver/client/game/src/enums.js similarity index 100% rename from crawl-ref/source/webserver/game_data/static/enums.js rename to crawl-ref/source/webserver/client/game/src/enums.js diff --git a/crawl-ref/source/webserver/game_data/static/focus-trap.js b/crawl-ref/source/webserver/client/game/src/focus-trap.js similarity index 100% rename from crawl-ref/source/webserver/game_data/static/focus-trap.js rename to crawl-ref/source/webserver/client/game/src/focus-trap.js diff --git a/crawl-ref/source/webserver/game_data/static/game.js b/crawl-ref/source/webserver/client/game/src/game.js similarity index 100% rename from crawl-ref/source/webserver/game_data/static/game.js rename to crawl-ref/source/webserver/client/game/src/game.js diff --git a/crawl-ref/source/webserver/client/game/src/main.js b/crawl-ref/source/webserver/client/game/src/main.js new file mode 100644 index 00000000000..197a6e84c93 --- /dev/null +++ b/crawl-ref/source/webserver/client/game/src/main.js @@ -0,0 +1,26 @@ +// Bundling styles didn't work, because there's no good way to *unload* them +// when we switch game client. This could probably be handled properly but for +// now including them with plain old html is the path of least resistance. +// import "./style.css"; +// import "./simplebar.css"; +import "./game"; + +// document.querySelector('#app')!.innerHTML = ` +//
+// +// +// +// +// +// +//

Vite + TypeScript

+//
+// +//
+//

+// Click on the Vite and TypeScript logos to learn more +//

+//
+// ` + +// setupCounter(document.querySelector('#counter')!) diff --git a/crawl-ref/source/webserver/client/game/src/main.ts b/crawl-ref/source/webserver/client/game/src/main.ts deleted file mode 100644 index 6396b50a232..00000000000 --- a/crawl-ref/source/webserver/client/game/src/main.ts +++ /dev/null @@ -1,24 +0,0 @@ -import './style.css' -import typescriptLogo from './typescript.svg' -import viteLogo from '/vite.svg' -import { setupCounter } from './counter.ts' - -document.querySelector('#app')!.innerHTML = ` -
- - - - - - -

Vite + TypeScript

-
- -
-

- Click on the Vite and TypeScript logos to learn more -

-
-` - -setupCounter(document.querySelector('#counter')!) diff --git a/crawl-ref/source/webserver/game_data/static/map_knowledge.js b/crawl-ref/source/webserver/client/game/src/map_knowledge.js similarity index 100% rename from crawl-ref/source/webserver/game_data/static/map_knowledge.js rename to crawl-ref/source/webserver/client/game/src/map_knowledge.js diff --git a/crawl-ref/source/webserver/game_data/static/menu.js b/crawl-ref/source/webserver/client/game/src/menu.js similarity index 100% rename from crawl-ref/source/webserver/game_data/static/menu.js rename to crawl-ref/source/webserver/client/game/src/menu.js diff --git a/crawl-ref/source/webserver/game_data/static/messages.js b/crawl-ref/source/webserver/client/game/src/messages.js similarity index 100% rename from crawl-ref/source/webserver/game_data/static/messages.js rename to crawl-ref/source/webserver/client/game/src/messages.js diff --git a/crawl-ref/source/webserver/game_data/static/minimap.js b/crawl-ref/source/webserver/client/game/src/minimap.js similarity index 100% rename from crawl-ref/source/webserver/game_data/static/minimap.js rename to crawl-ref/source/webserver/client/game/src/minimap.js diff --git a/crawl-ref/source/webserver/game_data/static/monster_list.js b/crawl-ref/source/webserver/client/game/src/monster_list.js similarity index 100% rename from crawl-ref/source/webserver/game_data/static/monster_list.js rename to crawl-ref/source/webserver/client/game/src/monster_list.js diff --git a/crawl-ref/source/webserver/game_data/static/mouse_control.js b/crawl-ref/source/webserver/client/game/src/mouse_control.js similarity index 100% rename from crawl-ref/source/webserver/game_data/static/mouse_control.js rename to crawl-ref/source/webserver/client/game/src/mouse_control.js diff --git a/crawl-ref/source/webserver/game_data/static/options.js b/crawl-ref/source/webserver/client/game/src/options.js similarity index 100% rename from crawl-ref/source/webserver/game_data/static/options.js rename to crawl-ref/source/webserver/client/game/src/options.js diff --git a/crawl-ref/source/webserver/game_data/static/player.js b/crawl-ref/source/webserver/client/game/src/player.js similarity index 100% rename from crawl-ref/source/webserver/game_data/static/player.js rename to crawl-ref/source/webserver/client/game/src/player.js diff --git a/crawl-ref/source/webserver/game_data/static/scroller.js b/crawl-ref/source/webserver/client/game/src/scroller.js similarity index 100% rename from crawl-ref/source/webserver/game_data/static/scroller.js rename to crawl-ref/source/webserver/client/game/src/scroller.js diff --git a/crawl-ref/source/webserver/game_data/static/simplebar.js b/crawl-ref/source/webserver/client/game/src/simplebar.js similarity index 100% rename from crawl-ref/source/webserver/game_data/static/simplebar.js rename to crawl-ref/source/webserver/client/game/src/simplebar.js diff --git a/crawl-ref/source/webserver/client/game/src/style.css b/crawl-ref/source/webserver/client/game/src/style.css deleted file mode 100644 index 3bcdbd00d55..00000000000 --- a/crawl-ref/source/webserver/client/game/src/style.css +++ /dev/null @@ -1,96 +0,0 @@ -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -#app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.vanilla:hover { - filter: drop-shadow(0 0 2em #3178c6aa); -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} diff --git a/crawl-ref/source/webserver/game_data/static/text.js b/crawl-ref/source/webserver/client/game/src/text.js similarity index 100% rename from crawl-ref/source/webserver/game_data/static/text.js rename to crawl-ref/source/webserver/client/game/src/text.js diff --git a/crawl-ref/source/webserver/game_data/static/textinput.js b/crawl-ref/source/webserver/client/game/src/textinput.js similarity index 100% rename from crawl-ref/source/webserver/game_data/static/textinput.js rename to crawl-ref/source/webserver/client/game/src/textinput.js diff --git a/crawl-ref/source/webserver/game_data/static/tileinfos.js b/crawl-ref/source/webserver/client/game/src/tileinfos.js similarity index 100% rename from crawl-ref/source/webserver/game_data/static/tileinfos.js rename to crawl-ref/source/webserver/client/game/src/tileinfos.js diff --git a/crawl-ref/source/webserver/client/game/src/typescript.svg b/crawl-ref/source/webserver/client/game/src/typescript.svg deleted file mode 100644 index d91c910cc30..00000000000 --- a/crawl-ref/source/webserver/client/game/src/typescript.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/crawl-ref/source/webserver/game_data/static/ui-layouts.js b/crawl-ref/source/webserver/client/game/src/ui-layouts.js similarity index 100% rename from crawl-ref/source/webserver/game_data/static/ui-layouts.js rename to crawl-ref/source/webserver/client/game/src/ui-layouts.js diff --git a/crawl-ref/source/webserver/game_data/static/ui.js b/crawl-ref/source/webserver/client/game/src/ui.js similarity index 100% rename from crawl-ref/source/webserver/game_data/static/ui.js rename to crawl-ref/source/webserver/client/game/src/ui.js diff --git a/crawl-ref/source/webserver/game_data/static/util.js b/crawl-ref/source/webserver/client/game/src/util.js similarity index 100% rename from crawl-ref/source/webserver/game_data/static/util.js rename to crawl-ref/source/webserver/client/game/src/util.js diff --git a/crawl-ref/source/webserver/game_data/static/view_data.js b/crawl-ref/source/webserver/client/game/src/view_data.js similarity index 100% rename from crawl-ref/source/webserver/game_data/static/view_data.js rename to crawl-ref/source/webserver/client/game/src/view_data.js diff --git a/crawl-ref/source/webserver/static/scripts/app.js b/crawl-ref/source/webserver/client/lobby/src/app.js similarity index 100% rename from crawl-ref/source/webserver/static/scripts/app.js rename to crawl-ref/source/webserver/client/lobby/src/app.js diff --git a/crawl-ref/source/webserver/static/scripts/chat.js b/crawl-ref/source/webserver/client/lobby/src/chat.js similarity index 100% rename from crawl-ref/source/webserver/static/scripts/chat.js rename to crawl-ref/source/webserver/client/lobby/src/chat.js diff --git a/crawl-ref/source/webserver/static/scripts/client.js b/crawl-ref/source/webserver/client/lobby/src/client.js similarity index 100% rename from crawl-ref/source/webserver/static/scripts/client.js rename to crawl-ref/source/webserver/client/lobby/src/client.js diff --git a/crawl-ref/source/webserver/static/scripts/comm.js b/crawl-ref/source/webserver/client/lobby/src/comm.js similarity index 100% rename from crawl-ref/source/webserver/static/scripts/comm.js rename to crawl-ref/source/webserver/client/lobby/src/comm.js diff --git a/crawl-ref/source/webserver/client/lobby/src/counter.ts b/crawl-ref/source/webserver/client/lobby/src/counter.ts deleted file mode 100644 index 09e5afd2d8a..00000000000 --- a/crawl-ref/source/webserver/client/lobby/src/counter.ts +++ /dev/null @@ -1,9 +0,0 @@ -export function setupCounter(element: HTMLButtonElement) { - let counter = 0 - const setCounter = (count: number) => { - counter = count - element.innerHTML = `count is ${counter}` - } - element.addEventListener('click', () => setCounter(counter + 1)) - setCounter(0) -} diff --git a/crawl-ref/source/webserver/client/lobby/src/globals.js b/crawl-ref/source/webserver/client/lobby/src/globals.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/crawl-ref/source/webserver/static/scripts/key_conversion.js b/crawl-ref/source/webserver/client/lobby/src/key_conversion.js similarity index 100% rename from crawl-ref/source/webserver/static/scripts/key_conversion.js rename to crawl-ref/source/webserver/client/lobby/src/key_conversion.js diff --git a/crawl-ref/source/webserver/static/scripts/linkify.js b/crawl-ref/source/webserver/client/lobby/src/linkify.js similarity index 100% rename from crawl-ref/source/webserver/static/scripts/linkify.js rename to crawl-ref/source/webserver/client/lobby/src/linkify.js diff --git a/crawl-ref/source/webserver/client/lobby/src/main.js b/crawl-ref/source/webserver/client/lobby/src/main.js new file mode 100644 index 00000000000..f098b267cf3 --- /dev/null +++ b/crawl-ref/source/webserver/client/lobby/src/main.js @@ -0,0 +1,21 @@ +import './app.ts' + +// document.querySelector('#app')!.innerHTML = ` +//
+// +// +// +// +// +// +//

Vite + TypeScript

+//
+// +//
+//

+// Click on the Vite and TypeScript logos to learn more +//

+//
+// ` + +// setupCounter(document.querySelector('#counter')!) \ No newline at end of file diff --git a/crawl-ref/source/webserver/client/lobby/src/main.ts b/crawl-ref/source/webserver/client/lobby/src/main.ts deleted file mode 100644 index 6396b50a232..00000000000 --- a/crawl-ref/source/webserver/client/lobby/src/main.ts +++ /dev/null @@ -1,24 +0,0 @@ -import './style.css' -import typescriptLogo from './typescript.svg' -import viteLogo from '/vite.svg' -import { setupCounter } from './counter.ts' - -document.querySelector('#app')!.innerHTML = ` -
- - - - - - -

Vite + TypeScript

-
- -
-

- Click on the Vite and TypeScript logos to learn more -

-
-` - -setupCounter(document.querySelector('#counter')!) diff --git a/crawl-ref/source/webserver/client/lobby/src/style.css b/crawl-ref/source/webserver/client/lobby/src/style.css deleted file mode 100644 index 3bcdbd00d55..00000000000 --- a/crawl-ref/source/webserver/client/lobby/src/style.css +++ /dev/null @@ -1,96 +0,0 @@ -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -#app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.vanilla:hover { - filter: drop-shadow(0 0 2em #3178c6aa); -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} diff --git a/crawl-ref/source/webserver/client/lobby/src/typescript.svg b/crawl-ref/source/webserver/client/lobby/src/typescript.svg deleted file mode 100644 index d91c910cc30..00000000000 --- a/crawl-ref/source/webserver/client/lobby/src/typescript.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file From 114aafc41ab2e811d22e30c4f6c81215b94a0b84 Mon Sep 17 00:00:00 2001 From: mumra Date: Sun, 19 Oct 2025 09:09:25 +0100 Subject: [PATCH 3/6] Build JavaScript bundles and run a linting and formatting pass Get the new bundling system working, and use the biome tool to apply linting fixes and auto formatting to .js files across the board. --- .gitignore | 1 + crawl-ref/.gitignore | 3 + .../rltiles/tool/tile_list_processor.cc | 33 +- crawl-ref/source/webserver/.gitignore | 2 +- .../source/webserver/client/game/biome.json | 40 + .../source/webserver/client/game/index.html | 3 +- .../webserver/client/game/package-lock.json | 14 +- .../source/webserver/client/game/package.json | 13 +- .../webserver/client/game/public/vite.svg | 1 - .../webserver/client/game/src/action_panel.js | 1099 +++--- .../client/game/src/cell_renderer.js | 2650 +++++++------ .../webserver/client/game/src/client.js | 1 + .../source/webserver/client/game/src/comm.js | 1 + .../client/game/src/contrib/simplebar.js | 3353 ++++++++++++++++ .../source/webserver/client/game/src/debug.js | 108 +- .../webserver/client/game/src/display.js | 196 +- .../client/game/src/dungeon_renderer.js | 876 +++-- .../source/webserver/client/game/src/enums.js | 617 ++- .../webserver/client/game/src/focus-trap.js | 571 --- .../source/webserver/client/game/src/game.js | 962 +++-- .../client/game/src/key_conversion.js | 1 + .../source/webserver/client/game/src/main.js | 24 +- .../client/game/src/map_knowledge.js | 404 +- .../source/webserver/client/game/src/menu.js | 2113 +++++------ .../webserver/client/game/src/messages.js | 291 +- .../webserver/client/game/src/minimap.js | 487 ++- .../webserver/client/game/src/monster_list.js | 516 ++- .../client/game/src/mouse_control.js | 149 +- .../webserver/client/game/src/options.js | 115 +- .../webserver/client/game/src/player.js | 1208 +++--- .../webserver/client/game/src/scroller.js | 59 +- .../webserver/client/game/src/simplebar.js | 3072 --------------- .../source/webserver/client/game/src/text.js | 98 +- .../webserver/client/game/src/textinput.js | 598 ++- .../webserver/client/game/src/tileinfos.js | 35 +- .../webserver/client/game/src/ui-layouts.js | 2240 +++++------ .../source/webserver/client/game/src/ui.js | 978 +++-- .../source/webserver/client/game/src/util.js | 300 +- .../webserver/client/game/src/view_data.js | 167 +- .../webserver/client/game/vite.config.js | 16 + .../source/webserver/client/lobby/biome.json | 39 + .../source/webserver/client/lobby/index.html | 365 +- .../webserver/client/lobby/package-lock.json | 204 +- .../webserver/client/lobby/package.json | 16 +- .../webserver/client/lobby/public/vite.svg | 1 - .../source/webserver/client/lobby/src/app.js | 14 +- .../source/webserver/client/lobby/src/chat.js | 422 +-- .../webserver/client/lobby/src/client.js | 3355 ++++++++--------- .../source/webserver/client/lobby/src/comm.js | 97 +- .../lobby/src/contrib/ba-linkify.min.js | Bin 0 -> 2837 bytes .../client/lobby/src/contrib/inflate.js | 2138 +++++++++++ .../lobby/src/contrib/jquery.waitforimages.js | 135 + .../client/lobby/src/contrib/require.js | 39 + .../client/lobby/src/key_conversion.js | 723 ++-- .../webserver/client/lobby/src/linkify.js | 51 +- .../source/webserver/client/lobby/src/main.js | 5 +- .../webserver/client/lobby/src/style.css | 507 +++ .../webserver/client/lobby/tsconfig.json | 1 + .../webserver/client/lobby/vite.config.js | 3 + .../webserver/game_data/templates/game.html | 15 +- .../source/webserver/templates/client.html | 363 -- package-lock.json | 1358 +++++++ package.json | 18 + 63 files changed, 18352 insertions(+), 14932 deletions(-) create mode 100644 crawl-ref/source/webserver/client/game/biome.json delete mode 100644 crawl-ref/source/webserver/client/game/public/vite.svg create mode 100644 crawl-ref/source/webserver/client/game/src/client.js create mode 100644 crawl-ref/source/webserver/client/game/src/comm.js create mode 100644 crawl-ref/source/webserver/client/game/src/contrib/simplebar.js delete mode 100644 crawl-ref/source/webserver/client/game/src/focus-trap.js create mode 100644 crawl-ref/source/webserver/client/game/src/key_conversion.js delete mode 100644 crawl-ref/source/webserver/client/game/src/simplebar.js create mode 100644 crawl-ref/source/webserver/client/game/vite.config.js create mode 100644 crawl-ref/source/webserver/client/lobby/biome.json delete mode 100644 crawl-ref/source/webserver/client/lobby/public/vite.svg create mode 100644 crawl-ref/source/webserver/client/lobby/src/contrib/ba-linkify.min.js create mode 100644 crawl-ref/source/webserver/client/lobby/src/contrib/inflate.js create mode 100644 crawl-ref/source/webserver/client/lobby/src/contrib/jquery.waitforimages.js create mode 100644 crawl-ref/source/webserver/client/lobby/src/contrib/require.js create mode 100644 crawl-ref/source/webserver/client/lobby/src/style.css create mode 100644 crawl-ref/source/webserver/client/lobby/vite.config.js delete mode 100644 crawl-ref/source/webserver/templates/client.html create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore index fad58842c9f..a91c75adbb3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .cquery_cache/ .DS_Store .vscode/ +node_modules/ .vs *.sublime-workspace diff --git a/crawl-ref/.gitignore b/crawl-ref/.gitignore index 2a73c0d290b..47b85770c3c 100644 --- a/crawl-ref/.gitignore +++ b/crawl-ref/.gitignore @@ -115,8 +115,11 @@ chunk /source/webserver/game_data/static/gui.png /source/webserver/game_data/static/icons.png /source/webserver/game_data/static/tileinfo-*.js +/source/webserver/game_data/static/game.js /source/webserver/static/stone_soup_icon-32x32.png /source/webserver/static/title_*.png +/source/webserver/static/index-*.js +/source/webserver/static/index-*.css # Makefile-generated junk makefile.dep diff --git a/crawl-ref/source/rltiles/tool/tile_list_processor.cc b/crawl-ref/source/rltiles/tool/tile_list_processor.cc index 275956007d0..9e3603daeaa 100644 --- a/crawl-ref/source/rltiles/tool/tile_list_processor.cc +++ b/crawl-ref/source/rltiles/tool/tile_list_processor.cc @@ -1673,39 +1673,30 @@ bool tile_list_processor::write_data(bool image, bool code) if (m_abstract.size() == 0) { - fprintf(fp, "define(["); if (m_start_value_module.size() > 0) - fprintf(fp, "\"./tileinfo-%s\"", m_start_value_module.c_str()); - fprintf(fp, "], function(m) {\n"); + fprintf(fp, "import m from \"./tileinfo-%s\"\n", m_start_value_module.c_str()); } else { - fprintf(fp, "define([\"jquery\","); - for (const auto& abstract : m_abstract) - fprintf(fp, "\"./tileinfo-%s\", ", abstract.first.c_str()); - fprintf(fp, "],\n function ($, "); - for (size_t i = 0; i < m_abstract.size(); ++i) - { - if (i < m_abstract.size() - 1) - fprintf(fp, "%s, ", m_abstract[i].first.c_str()); - else - fprintf(fp, "%s", m_abstract[i].first.c_str()); - } - fprintf(fp, ") {\n"); + for (const auto &abstract : m_abstract) + fprintf(fp, "import %s from \"./tileinfo-%s\"\n", + abstract.first.c_str(), abstract.first.c_str()); } fprintf(fp, "// This file has been automatically generated.\n\n"); - fprintf(fp, "var exports = {};\n"); + fprintf(fp, "const exports = { "); if (m_abstract.size() > 0) { - for (const auto& abstract : m_abstract) - fprintf(fp, "$.extend(exports, %s);\n", abstract.first.c_str()); + for (const auto &abstract : m_abstract) + fprintf(fp, "...%s, ", abstract.first.c_str()); } + fprintf(fp, "};\n"); + if (m_start_value_module.size() > 0) - fprintf(fp, "\nvar val = m.%s;\n", m_start_value.c_str()); + fprintf(fp, "\nlet val = m.%s;\n", m_start_value.c_str()); else - fprintf(fp, "\nvar val = %s;\n", m_start_value.c_str()); + fprintf(fp, "\nlet val = %s;\n", m_start_value.c_str()); string old_enum_name = ""; int count = 0; @@ -1862,7 +1853,7 @@ bool tile_list_processor::write_data(bool image, bool code) fprintf(fp, "};\n\n"); } - fprintf(fp, "return exports;\n});\n"); + fprintf(fp, "export default exports\n"); fclose(fp); } diff --git a/crawl-ref/source/webserver/.gitignore b/crawl-ref/source/webserver/.gitignore index d3e0c2dc16c..5624774d36d 100644 --- a/crawl-ref/source/webserver/.gitignore +++ b/crawl-ref/source/webserver/.gitignore @@ -8,4 +8,4 @@ banned_players.txt webtiles/version.txt Pipfile Pipfile.lock -node_modules/ \ No newline at end of file +templates/client.html \ No newline at end of file diff --git a/crawl-ref/source/webserver/client/game/biome.json b/crawl-ref/source/webserver/client/game/biome.json new file mode 100644 index 00000000000..7b33387a879 --- /dev/null +++ b/crawl-ref/source/webserver/client/game/biome.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.2.6/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false, + "includes": ["*", "src/*", "!src/contrib"] + + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "suspicious": { + "noVar": "error" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/crawl-ref/source/webserver/client/game/index.html b/crawl-ref/source/webserver/client/game/index.html index 749474f06af..50c6326fe24 100644 --- a/crawl-ref/source/webserver/client/game/index.html +++ b/crawl-ref/source/webserver/client/game/index.html @@ -2,12 +2,11 @@ - Crawl Game
- + diff --git a/crawl-ref/source/webserver/client/game/package-lock.json b/crawl-ref/source/webserver/client/game/package-lock.json index 7df11ffcd9c..84b5abeb8da 100644 --- a/crawl-ref/source/webserver/client/game/package-lock.json +++ b/crawl-ref/source/webserver/client/game/package-lock.json @@ -1,12 +1,15 @@ { - "name": "lobby", + "name": "crawl-game", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "lobby", + "name": "crawl-game", "version": "0.0.0", + "dependencies": { + "jquery": "^1.12.4" + }, "devDependencies": { "typescript": "~5.9.3", "vite": "^7.1.7" @@ -844,6 +847,13 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/jquery": { + "version": "1.12.4", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-1.12.4.tgz", + "integrity": "sha512-UEVp7PPK9xXYSk8xqXCJrkXnKZtlgWkd2GsAQbMRFK6S/ePU2JN5G2Zum8hIVjzR3CpdfSqdqAzId/xd4TJHeg==", + "deprecated": "This version is deprecated. Please upgrade to the latest version or find support at https://www.herodevs.com/support/jquery-nes.", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", diff --git a/crawl-ref/source/webserver/client/game/package.json b/crawl-ref/source/webserver/client/game/package.json index 4cf4f4d1d3b..4c62da85b17 100644 --- a/crawl-ref/source/webserver/client/game/package.json +++ b/crawl-ref/source/webserver/client/game/package.json @@ -5,11 +5,20 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc && vite build", - "preview": "vite preview" + "build": "vite build && npm run copy", + "lint": "biome lint ./src/*", + "lint:fix": "biome lint ./src/* --write --unsafe", + "format": "biome format ./src/*", + "preview": "vite preview", + "copy": "cp dist/static/* ../../game_data/static" }, "devDependencies": { + "@biomejs/biome": "^2.2.6", "typescript": "~5.9.3", "vite": "^7.1.7" + }, + "dependencies": { + "focus-trap": "^7.6.5", + "jquery": "^1.12.4" } } diff --git a/crawl-ref/source/webserver/client/game/public/vite.svg b/crawl-ref/source/webserver/client/game/public/vite.svg deleted file mode 100644 index e7b8dfb1b2a..00000000000 --- a/crawl-ref/source/webserver/client/game/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/crawl-ref/source/webserver/client/game/src/action_panel.js b/crawl-ref/source/webserver/client/game/src/action_panel.js index 8a89bd747b9..f62842bd96e 100644 --- a/crawl-ref/source/webserver/client/game/src/action_panel.js +++ b/crawl-ref/source/webserver/client/game/src/action_panel.js @@ -1,561 +1,556 @@ -define(["jquery", "comm", "client", - "./cell_renderer", "./enums", "./options", "./player", - "./tileinfo-icons", "./tileinfo-gui", "./tileinfo-main", - "./util", "./focus-trap", "./ui"], -function ($, comm, client, cr, enums, options, player, icons, gui, main, - util, focus_trap, ui) { - "use strict"; - - var filtered_inv; - var renderer, $canvas, $settings, $tooltip; - var borders_width; - var minimized; - var settings_visible; - var tooltip_timeout = null; - // Options - var panel_disabled; - var scale, orientation, font_family, font_size; - var font; // cached font name for the canvas: size (in px) + family - var draw_glyphs; - var selected = -1; - const NUM_RESERVED_BUTTONS = 2; - - function send_options() - { - if (client.is_watching()) - return; - options.set("action_panel_orientation", orientation, false); - options.set("action_panel_show", !minimized, false); - options.set("action_panel_scale", scale, false); - options.set("action_panel_font_size", font_size, false); - // TODO: some cleaner approach to this than multiple msgs - options.send("action_panel_orientation"); - options.send("action_panel_show"); - options.send("action_panel_scale"); - options.send("action_panel_font_size"); +import $ from "jquery"; +import { createFocusTrap } from "focus-trap"; + +import comm from "./comm"; +import client from "./client"; + +import cr from "./cell_renderer"; +import enums from "./enums"; +import options from "./options"; +import player from "./player"; +import icons from "../../../game_data/static/tileinfo-icons"; +import gui from "../../../game_data/static/tileinfo-gui"; +import main from "../../../game_data/static/tileinfo-main"; +import util from "./util"; +import ui from "./ui"; + +let filtered_inv; +let renderer, $canvas, $settings, $tooltip; +let borders_width; +let minimized; +let settings_visible; +let tooltip_timeout = null; +// Options +let panel_disabled; +let scale, orientation, font_family, font_size; +let font; // cached font name for the canvas: size (in px) + family +let draw_glyphs; +let selected = -1; +const NUM_RESERVED_BUTTONS = 2; + +function send_options() { + if (client.is_watching()) return; + options.set("action_panel_orientation", orientation, false); + options.set("action_panel_show", !minimized, false); + options.set("action_panel_scale", scale, false); + options.set("action_panel_font_size", font_size, false); + // TODO: some cleaner approach to this than multiple msgs + options.send("action_panel_orientation"); + options.send("action_panel_show"); + options.send("action_panel_scale"); + options.send("action_panel_font_size"); +} + +function hide_panel(send_opts = true) { + $("#action-panel-settings").hide(); + $("#action-panel").addClass("hidden"); + // if the player configured the panel to not show any items, + // don't even show the placeholder button, don't update settings, etc. + if (!panel_disabled) { + $("#action-panel-placeholder").removeClass("hidden").show(); + // order of these two matters + minimized = true; + hide_settings(); + if (send_opts) send_options(); + } +} + +function show_panel(send_opts = true) { + if (settings_visible) return; + $("#action-panel-settings").hide(); // sanitize + $("#action-panel").removeClass("hidden"); + $("#action-panel-placeholder").addClass("hidden"); + minimized = false; + if (send_opts) send_options(); +} + +function show_settings(e) { + if (selected > 0) return false; + hide_tooltip(); + const o_button = $(`#action-orient-${orientation}`); + // Initialize the form with the current values + o_button.prop("checked", true); + + // TODO: should these just reset to 100/16, rather than this somewhat + // complicate context-sensitive behavior? + // use parseInt to cut out any decimals + const scale_percent = parseInt(scale, 10); + $("#scale-val").val(scale_percent); + if (!$("#scale-val").data("default")) + $("#scale-val").data("default", scale_percent); + + $("#font-size-val").val(font_size); + if (!$("#font-size-val").data("default")) + $("#font-size-val").data("default", font_size); + + // Show the context menu near the cursor + $settings = $("#action-panel-settings"); + $settings.css({ top: `${e.pageY + 10}px`, left: `${e.pageX + 10}px` }); + $settings.show(); + settings_visible = true; + + // TODO: I have had to set the buttons with tabindex -1, as I cannot + // for the life of me get focus-trap to intercept their key input + // correctly for esc and tab/shift-tab. This is non-ideal for + // accessibility reasons. + $settings[0].focus_trap = createFocusTrap($settings[0], { + escapeDeactivates: true, + onActivate: () => { + $settings.addClass("focus-trap"); + }, + onDeactivate: () => { + $settings.removeClass("focus-trap"); + // ugly to do this as a timeout, but it is the best way I've + // found to get the key handling sequence right. This ensures + // that if a mousedown event triggers deactivate, it is handled + // while the settings panel is still open. + setTimeout(hide_settings_final, 50); + }, + returnFocusOnDeactivate: false, + clickOutsideDeactivates: true, + }).activate(); + + return false; +} + +function hide_settings() { + if (!settings_visible) return; + $settings[0].focus_trap.deactivate(); +} + +// triggered via focus-trap onDeactivate +function hide_settings_final() { + $("#action-panel-settings").hide(); + settings_visible = false; + + // TODO: I can't quite figure out why the conditional is necessary, but + // maybe something about the timing. Without it, sync_focus_state + // steals focus from the chat window. + if (!$("#chat").hasClass("focus-trap")) ui.sync_focus_state(); + + // somewhat hacky: if hide_settings is triggered by hide_panel, + // don't send options twice. Assumes that this flag is set first... + if (!minimized) send_options(); +} + +function hide_tooltip() { + if (tooltip_timeout) clearTimeout(tooltip_timeout); + $tooltip.hide(); +} + +function show_tooltip(x, y, slot) { + if (slot >= filtered_inv.length) { + hide_tooltip(); + return; + } + $tooltip.css({ top: `${y + 10}px`, left: `${x + 10}px` }); + if (slot === -2) { + $tooltip.html( + "Left click: minimize
" + + "Right click: open settings" + ); + } else if (slot === -1 && game.get_input_mode() === enums.mouse_mode.COMMAND) + $tooltip.html("Left click: show main menu"); + else { + const item = filtered_inv[slot]; + $tooltip.empty().text(`${String.fromCharCode(item.letter)} - `); + $tooltip.append(player.inventory_item_desc(item.slot)); + if (game.get_input_mode() === enums.mouse_mode.COMMAND) { + if (item.action_verb) + $tooltip.append( + "
Left click: " + + item.action_verb.toLowerCase() + + "" + ); + $tooltip.append("
Right click: describe"); } - - function hide_panel(send_opts=true) - { - $("#action-panel-settings").hide(); - $("#action-panel").addClass("hidden"); - // if the player configured the panel to not show any items, - // don't even show the placeholder button, don't update settings, etc. - if (!panel_disabled) - { - $("#action-panel-placeholder").removeClass("hidden").show(); - // order of these two matters - minimized = true; - hide_settings(); - if (send_opts) - send_options(); - } - } - - function show_panel(send_opts=true) - { - if (settings_visible) - return; - $("#action-panel-settings").hide(); // sanitize - $("#action-panel").removeClass("hidden"); - $("#action-panel-placeholder").addClass("hidden"); - minimized = false; - if (send_opts) - send_options(); + } + $tooltip.show(); +} + +// Initial setup for the panel and its settings menu. +// Note that "game_init" happens before the client receives +// the options and inventory data from the server. +$(document).bind("game_init", () => { + $canvas = $("#action-panel"); + $settings = $("#action-panel-settings"); + $tooltip = $("#action-panel-tooltip"); + if (client.is_watching()) { + $canvas.addClass("hidden"); + return; + } + + renderer = new cr.DungeonCellRenderer(); + borders_width = (parseInt($canvas.css("border-left-width"), 10) || 0) * 2; + minimized = false; + settings_visible = false; + tooltip_timeout = null; + filtered_inv = []; + + $canvas.on("update", update); + + $canvas.on("mousemove mouseleave mousedown mouseenter", (ev) => { + handle_mouse(ev); + }); + + $canvas.contextmenu(() => false); + + // We don't need a context menu for the context menu + $settings.contextmenu(() => false); + + // Clicking on the panel/Close button closes the settings menu + $("#action-panel, #close-settings").click(() => { + hide_settings(); + }); + + // Triggering this function on keyup might be too aggressive, + // but at least the player doesn't have to press Enter to confirm changes + $("#action-panel-settings input[type=radio],input[type=number]").on( + "change keyup", + (e) => { + if (e.which === 27) { + hide_settings(); + e.preventDefault(); + return; + } + const input = e.target; + if (input.type === "number" && !input.checkValidity()) return; + options.set(input.name, input.value); } + ); + + $("#action-panel-settings button.reset").click(function () { + const input = $(this).siblings("input"); + const default_value = input.data("default"); + input.val(default_value); + options.set(input.prop("name"), default_value); + }); + + $("#minimize-panel").click(hide_panel); + + $("#action-panel-placeholder").click(() => { + show_panel(); + update(); + }); + + // To prevent the game from showing an empty panel before + // any inventory data arrives, we hide it via inline CSS + // and the "hidden" class. The next line deactivates + // the inline rule, and the first call to update() will + // remove "hidden" if the (filtered) inventory is not empty. + $canvas.show(); +}); - function show_settings(e) - { - if (selected > 0) - return false; +function _horizontal() { + return orientation === "horizontal"; +} + +function _update_font_props() { + font = `${font_size || "16"}px ${font_family || "monospace"}`; +} + +function handle_mouse(ev) { + if (ev.type === "mouseleave") { + selected = -1; + hide_tooltip(); + update(); + } else { + // focus-trap handles this case: the settings panel is about to + // be closed and we should ignore the click + if (ev.type === "mousedown" && settings_visible) return; + + // for finding the mouse position, we need to *not* use the device + // pixel ratio to adjust the scale + const cell_width = (renderer.cell_width * scale) / 100; + const cell_height = (renderer.cell_height * scale) / 100; + const _cell_length = _horizontal() ? cell_width : cell_height; + const loc = { + x: Math.round(ev.clientX / cell_width - 0.5), + y: Math.round(ev.clientY / cell_height - 0.5), + }; + + if (ev.type === "mousemove" || ev.type === "mouseenter") { + const oldselected = selected; + selected = _horizontal() ? loc.x : loc.y; + update(); + if (oldselected !== selected && !settings_visible) { hide_tooltip(); - var o_button = $("#action-orient-" + orientation); - // Initialize the form with the current values - o_button.prop("checked", true); - - // TODO: should these just reset to 100/16, rather than this somewhat - // complicate context-sensitive behavior? - // use parseInt to cut out any decimals - var scale_percent = parseInt(scale); - $("#scale-val").val(scale_percent); - if (!$("#scale-val").data("default")) - $("#scale-val").data("default", scale_percent); - - $("#font-size-val").val(font_size); - if (!$("#font-size-val").data("default")) - $("#font-size-val").data("default", font_size); - - // Show the context menu near the cursor - $settings = $("#action-panel-settings"); - $settings.css({top: e.pageY + 10 + "px", - left: e.pageX + 10 + "px"}); - $settings.show(); - settings_visible = true; - - // TODO: I have had to set the buttons with tabindex -1, as I cannot - // for the life of me get focus-trap to intercept their key input - // correctly for esc and tab/shift-tab. This is non-ideal for - // accessibility reasons. - $settings[0].focus_trap = focus_trap($settings[0], { - escapeDeactivates: true, - onActivate: function () { - $settings.addClass("focus-trap"); - }, - onDeactivate: function() { - $settings.removeClass("focus-trap"); - // ugly to do this as a timeout, but it is the best way I've - // found to get the key handling sequence right. This ensures - // that if a mousedown event triggers deactivate, it is handled - // while the settings panel is still open. - setTimeout(hide_settings_final, 50); - }, - returnFocusOnDeactivate: false, - clickOutsideDeactivates: true, - }).activate(); - - return false; - } - - function hide_settings() - { - if (!settings_visible) - return; - $settings[0].focus_trap.deactivate(); - } - - // triggered via focus-trap onDeactivate - function hide_settings_final() - { - $("#action-panel-settings").hide(); - settings_visible = false; - - // TODO: I can't quite figure out why the conditional is necessary, but - // maybe something about the timing. Without it, sync_focus_state - // steals focus from the chat window. - if (!$("#chat").hasClass("focus-trap")) - ui.sync_focus_state(); - - // somewhat hacky: if hide_settings is triggered by hide_panel, - // don't send options twice. Assumes that this flag is set first... - if (!minimized) - send_options(); - } - - function hide_tooltip() - { - if (tooltip_timeout) - clearTimeout(tooltip_timeout); - $tooltip.hide(); - } - - function show_tooltip(x, y, slot) - { - if (slot >= filtered_inv.length) - { - hide_tooltip(); - return; - } - $tooltip.css({top: y + 10 + "px", - left: x + 10 + "px"}); - if (slot == -2) - { - $tooltip.html("Left click: minimize
" - + "Right click: open settings"); - } - else if (slot == -1 && game.get_input_mode() == enums.mouse_mode.COMMAND) - $tooltip.html("Left click: show main menu"); - else - { - var item = filtered_inv[slot]; - $tooltip.empty().text(String.fromCharCode(item.letter) + " - "); - $tooltip.append(player.inventory_item_desc(item.slot)); - if (game.get_input_mode() == enums.mouse_mode.COMMAND) - { - if (item.action_verb) - $tooltip.append("
Left click: " - + item.action_verb.toLowerCase() - + ""); - $tooltip.append("
Right click: describe"); - } - } - $tooltip.show(); - } - - // Initial setup for the panel and its settings menu. - // Note that "game_init" happens before the client receives - // the options and inventory data from the server. - $(document).bind("game_init", function () { - $canvas = $("#action-panel"); - $settings = $("#action-panel-settings"); - $tooltip = $("#action-panel-tooltip"); - if (client.is_watching()) - { - $canvas.addClass("hidden"); - return; - } - - renderer = new cr.DungeonCellRenderer(); - borders_width = (parseInt($canvas.css("border-left-width"), 10) || 0) * 2; - minimized = false; - settings_visible = false; - tooltip_timeout = null; - filtered_inv = []; - - $canvas.on("update", update); - - $canvas.on("mousemove mouseleave mousedown mouseenter", function (ev) { - handle_mouse(ev); - }); - - $canvas.contextmenu(function() { return false; }); - - // We don't need a context menu for the context menu - $settings.contextmenu(function () { - return false; + tooltip_timeout = setTimeout(() => { + show_tooltip(ev.pageX, ev.pageY, selected - NUM_RESERVED_BUTTONS); + }, 500); + } + } else if (ev.type === "mousedown" && ev.which === 1) { + if (selected === 0) + // It should be available even in targeting mode + hide_panel(); + else if ( + game.get_input_mode() === enums.mouse_mode.COMMAND && + selected === 1 + ) { + comm.send_message("main_menu_action"); + } else if ( + game.get_input_mode() === enums.mouse_mode.COMMAND && + selected >= NUM_RESERVED_BUTTONS && + selected < filtered_inv.length + NUM_RESERVED_BUTTONS + ) { + comm.send_message("inv_item_action", { + slot: filtered_inv[selected - NUM_RESERVED_BUTTONS].slot, }); - - // Clicking on the panel/Close button closes the settings menu - $("#action-panel, #close-settings").click(function () { - hide_settings(); - }); - - // Triggering this function on keyup might be too aggressive, - // but at least the player doesn't have to press Enter to confirm changes - $("#action-panel-settings input[type=radio],input[type=number]") - .on("change keyup", function (e) { - if (e.which == 27) - { - hide_settings(); - e.preventDefault(); - return; - } - var input = e.target; - if (input.type === "number" && !input.checkValidity()) - return; - options.set(input.name, input.value); - }); - - $("#action-panel-settings button.reset").click(function () { - var input = $(this).siblings("input"); - var default_value = input.data("default"); - input.val(default_value); - options.set(input.prop("name"), default_value); - }); - - $("#minimize-panel").click(hide_panel); - - $("#action-panel-placeholder").click(function () { - show_panel(); - update(); + } + } else if (ev.type === "mousedown" && ev.which === 3) { + if (selected === 0) + // right click on the x shows settings + show_settings(ev); + else if ( + game.get_input_mode() === enums.mouse_mode.COMMAND && + selected >= NUM_RESERVED_BUTTONS && + selected < filtered_inv.length + NUM_RESERVED_BUTTONS + ) { + comm.send_message("inv_item_describe", { + slot: filtered_inv[selected - NUM_RESERVED_BUTTONS].slot, }); - - // To prevent the game from showing an empty panel before - // any inventory data arrives, we hide it via inline CSS - // and the "hidden" class. The next line deactivates - // the inline rule, and the first call to update() will - // remove "hidden" if the (filtered) inventory is not empty. - $canvas.show(); - }); - - function _horizontal() - { - return orientation === "horizontal" ? true : false; + } } - - function _update_font_props() - { - font = (font_size || "16") + "px " + (font_family || "monospace"); - } - - function handle_mouse(ev) - { - if (ev.type === "mouseleave") - { - selected = -1; - hide_tooltip(); - update(); - } - else - { - // focus-trap handles this case: the settings panel is about to - // be closed and we should ignore the click - if (ev.type == "mousedown" && settings_visible) - return; - - // for finding the mouse position, we need to *not* use the device - // pixel ratio to adjust the scale - var cell_width = renderer.cell_width * scale / 100; - var cell_height = renderer.cell_height * scale / 100; - var cell_length = _horizontal() ? cell_width : cell_height; - var loc = { - x: Math.round(ev.clientX / cell_width - 0.5), - y: Math.round(ev.clientY / cell_height - 0.5) - }; - - if (ev.type === "mousemove" || ev.type === "mouseenter") - { - var oldselected = selected; - selected = _horizontal() ? loc.x : loc.y; - update(); - if (oldselected != selected && !settings_visible) - { - hide_tooltip(); - tooltip_timeout = setTimeout(function() - { - show_tooltip(ev.pageX, ev.pageY, - selected - NUM_RESERVED_BUTTONS); - }, 500); - } - } - else if (ev.type === "mousedown" && ev.which == 1) - { - if (selected == 0) // It should be available even in targeting mode - hide_panel(); - else if (game.get_input_mode() == enums.mouse_mode.COMMAND - && selected == 1) - { - comm.send_message("main_menu_action"); - } - else if (game.get_input_mode() == enums.mouse_mode.COMMAND - && selected >= NUM_RESERVED_BUTTONS - && selected < filtered_inv.length + NUM_RESERVED_BUTTONS) - { - comm.send_message("inv_item_action", - {slot: filtered_inv[selected - NUM_RESERVED_BUTTONS].slot}); - } - } - else if (ev.type === "mousedown" && ev.which == 3) - { - if (selected == 0) // right click on the x shows settings - show_settings(ev); - else if (game.get_input_mode() == enums.mouse_mode.COMMAND - && selected >= NUM_RESERVED_BUTTONS - && selected < filtered_inv.length + NUM_RESERVED_BUTTONS) - { - comm.send_message("inv_item_describe", - {slot: filtered_inv[selected - NUM_RESERVED_BUTTONS].slot}); - } - } - } - } - - function draw_action(texture, tiles, item, offset, scale, needs_cursor, - text, useless) - { - if (item && draw_glyphs) - { - // XX just the glyph is not very informative. One idea might - // be to tack on the subtype icon, but those are currently - // baked into the item tile so this would be a lot of work. - renderer.render_glyph(_horizontal() ? offset : 0, - _horizontal() ? 0 : offset, - item, true, true, scale); - } - else - { - tiles = Array.isArray(tiles) ? tiles : [tiles]; - tiles.forEach(function (tile) { - renderer.draw_tile(tile, - _horizontal() ? offset : 0, - _horizontal() ? 0 : offset, - texture, - undefined, undefined, undefined, undefined, - scale); - }); - } - - if (text) - { - // TODO: at some scalings, this don't dodge the green highlight - // square very well - renderer.draw_quantity(text, - _horizontal() ? offset : 0, - _horizontal() ? 0 : offset, - font); - } - - if (needs_cursor) - { - renderer.draw_icon(icons.CURSOR3, - _horizontal() ? offset : 0, - _horizontal() ? 0 : offset, - undefined, undefined, - scale); - } - - if (useless) - { - renderer.draw_icon(icons.OOR_MESH, - _horizontal() ? offset : 0, - _horizontal() ? 0 : offset, - undefined, undefined, - scale); - } - } - - function update() - { - if (client.is_watching()) - return; - - // Have we received the inventory yet? - // Note: an empty inventory will still have 52 empty slots. - var inventory_initialized = Object.values(player.inv).length; - if (!inventory_initialized) - { - $("#action-panel").addClass("hidden"); - return; - } - - if (panel_disabled || minimized) - { - hide_panel(false); - return; - } - - // Filter - filtered_inv = Object.values(player.inv).filter(function (item) { - return item.quantity && item.action_panel_order >= 0; - }); - - // primary sort: determined by the `action_panel` option - // secondary sort: determined by subtype - filtered_inv.sort(function (a, b) { - if (a.action_panel_order === b.action_panel_order) - return a.sub_type - b.sub_type; - - return a.action_panel_order - b.action_panel_order; - }); - - // Render - const ratio = window.devicePixelRatio; - - // first we readjust the dimensions according to whether the panel - // should be horizontal or vertical, and how much space is available. - // These calculations are in logical pixels. - var adjusted_scale = scale / 100; - - var cell_width = renderer.cell_width * adjusted_scale; - var cell_height = renderer.cell_height * adjusted_scale; - var cell_length = _horizontal() ? cell_width - : cell_height; - var required_length = cell_length * (filtered_inv.length + NUM_RESERVED_BUTTONS); - var available_length = _horizontal() - ? $("#dungeon").width() - : $("#dungeon").height(); - available_length -= borders_width; - var max_cells = Math.floor(available_length / cell_length); - var panel_length = Math.min(required_length, available_length); - - util.init_canvas($canvas[0], - _horizontal() ? panel_length : cell_width, - _horizontal() ? cell_height : panel_length); - renderer.init($canvas[0]); - renderer.clear(); - - // now draw the thing. From this point forward, use device pixels. - const cell = renderer.scaled_size(); - const inc = (_horizontal() ? cell.width : cell.height) * adjusted_scale; - - // XX The "X" should definitely be a different/custom icon - // TODO: select tile via something like c++ `tileidx_command` - draw_action(gui, gui.PROMPT_NO, null, 0, adjusted_scale, selected == 0); - draw_action(gui, gui.CMD_GAME_MENU, null, inc, adjusted_scale, - selected == 1); - - draw_glyphs = options.get("action_panel_glyphs"); - - if (draw_glyphs) - { - // need to manually initialize for this type of renderer - renderer.glyph_mode_font_size = options.get("glyph_mode_font_size"); - renderer.glyph_mode_font = options.get("glyph_mode_font"); - renderer.glyph_mode_update_font_metrics(); - } - - // Inventory items - filtered_inv.slice(0, max_cells).forEach(function (item, idx) { - let offset = inc * (idx + NUM_RESERVED_BUTTONS); - let qty_field_name = item.qty_field; - let qty = ""; - if (item.hasOwnProperty(qty_field_name)) - qty = item[qty_field_name]; - let cursor_required = selected == idx + NUM_RESERVED_BUTTONS; - - draw_action(main, item.tile, item, offset, adjusted_scale, - cursor_required, qty, item.useless); - }); - - if (available_length < required_length) - { - const ellipsis = icons.ELLIPSIS; - var x_pos = 0, y_pos = 0; - - if (_horizontal()) - x_pos = available_length - icons.get_tile_info(ellipsis).w * adjusted_scale; - else - y_pos = available_length - icons.get_tile_info(ellipsis).h * adjusted_scale; - - renderer.draw_icon(ellipsis, x_pos * ratio, y_pos * ratio, -2, -2, adjusted_scale); - } - $canvas.removeClass("hidden"); - } - - options.add_listener(function () { - if (client.is_watching()) - return; - // synchronize visible state with new options. Because of messy timing - // issues with the crawl binary, this will run at least twice on - // startup. - var update_required = false; - - var new_scale = options.get("action_panel_scale"); - if (scale !== new_scale) - { - scale = new_scale; - update_required = true; - } - - // is one of: horizontal, vertical - var new_orientation = options.get("action_panel_orientation"); - if (orientation !== new_orientation) - { - orientation = new_orientation; - update_required = true; - } - - var new_min = !options.get("action_panel_show"); - if (new_min != minimized) - { - minimized = new_min; - update_required = true; - } - - var new_font_family = options.get("action_panel_font_family"); - if (font_family !== new_font_family) - { - font_family = new_font_family; - _update_font_props(); - update_required = true; - } - - var new_font_size = options.get("action_panel_font_size"); - if (font_size !== new_font_size) - { - font_size = new_font_size; - _update_font_props(); - update_required = true; - } - - // The panel should be disabled completely only if the player - // set the action_panel option to an empty string in the .rc - panel_disabled = options.get("action_panel_disabled"); - - if (update_required) - { - if (!minimized) - show_panel(false); - update(); - } + } +} + +function draw_action( + texture, + tiles, + item, + offset, + scale, + needs_cursor, + text, + useless +) { + if (item && draw_glyphs) { + // XX just the glyph is not very informative. One idea might + // be to tack on the subtype icon, but those are currently + // baked into the item tile so this would be a lot of work. + renderer.render_glyph( + _horizontal() ? offset : 0, + _horizontal() ? 0 : offset, + item, + true, + true, + scale + ); + } else { + tiles = Array.isArray(tiles) ? tiles : [tiles]; + tiles.forEach((tile) => { + renderer.draw_tile( + tile, + _horizontal() ? offset : 0, + _horizontal() ? 0 : offset, + texture, + undefined, + undefined, + undefined, + undefined, + scale + ); }); + } + + if (text) { + // TODO: at some scalings, this don't dodge the green highlight + // square very well + renderer.draw_quantity( + text, + _horizontal() ? offset : 0, + _horizontal() ? 0 : offset, + font + ); + } + + if (needs_cursor) { + renderer.draw_icon( + icons.CURSOR3, + _horizontal() ? offset : 0, + _horizontal() ? 0 : offset, + undefined, + undefined, + scale + ); + } + + if (useless) { + renderer.draw_icon( + icons.OOR_MESH, + _horizontal() ? offset : 0, + _horizontal() ? 0 : offset, + undefined, + undefined, + scale + ); + } +} + +function update() { + if (client.is_watching()) return; + + // Have we received the inventory yet? + // Note: an empty inventory will still have 52 empty slots. + const inventory_initialized = Object.values(player.inv).length; + if (!inventory_initialized) { + $("#action-panel").addClass("hidden"); + return; + } + + if (panel_disabled || minimized) { + hide_panel(false); + return; + } + + // Filter + filtered_inv = Object.values(player.inv).filter((item) => item.quantity && item.action_panel_order >= 0); + + // primary sort: determined by the `action_panel` option + // secondary sort: determined by subtype + filtered_inv.sort((a, b) => { + if (a.action_panel_order === b.action_panel_order) + return a.sub_type - b.sub_type; + + return a.action_panel_order - b.action_panel_order; + }); + + // Render + const ratio = window.devicePixelRatio; + + // first we readjust the dimensions according to whether the panel + // should be horizontal or vertical, and how much space is available. + // These calculations are in logical pixels. + const adjusted_scale = scale / 100; + + const cell_width = renderer.cell_width * adjusted_scale; + const cell_height = renderer.cell_height * adjusted_scale; + const cell_length = _horizontal() ? cell_width : cell_height; + const required_length = + cell_length * (filtered_inv.length + NUM_RESERVED_BUTTONS); + let available_length = _horizontal() + ? $("#dungeon").width() + : $("#dungeon").height(); + available_length -= borders_width; + const max_cells = Math.floor(available_length / cell_length); + const panel_length = Math.min(required_length, available_length); + + util.init_canvas( + $canvas[0], + _horizontal() ? panel_length : cell_width, + _horizontal() ? cell_height : panel_length + ); + renderer.init($canvas[0]); + renderer.clear(); + + // now draw the thing. From this point forward, use device pixels. + const cell = renderer.scaled_size(); + const inc = (_horizontal() ? cell.width : cell.height) * adjusted_scale; + + // XX The "X" should definitely be a different/custom icon + // TODO: select tile via something like c++ `tileidx_command` + draw_action(gui, gui.PROMPT_NO, null, 0, adjusted_scale, selected === 0); + draw_action(gui, gui.CMD_GAME_MENU, null, inc, adjusted_scale, selected === 1); + + draw_glyphs = options.get("action_panel_glyphs"); + + if (draw_glyphs) { + // need to manually initialize for this type of renderer + renderer.glyph_mode_font_size = options.get("glyph_mode_font_size"); + renderer.glyph_mode_font = options.get("glyph_mode_font"); + renderer.glyph_mode_update_font_metrics(); + } + + // Inventory items + filtered_inv.slice(0, max_cells).forEach((item, idx) => { + const offset = inc * (idx + NUM_RESERVED_BUTTONS); + const qty_field_name = item.qty_field; + let qty = ""; + if (Object.hasOwn(item, qty_field_name)) qty = item[qty_field_name]; + const cursor_required = selected === idx + NUM_RESERVED_BUTTONS; + + draw_action( + main, + item.tile, + item, + offset, + adjusted_scale, + cursor_required, + qty, + item.useless + ); + }); + + if (available_length < required_length) { + const ellipsis = icons.ELLIPSIS; + let x_pos = 0, + y_pos = 0; + + if (_horizontal()) + x_pos = + available_length - icons.get_tile_info(ellipsis).w * adjusted_scale; + else + y_pos = + available_length - icons.get_tile_info(ellipsis).h * adjusted_scale; + + renderer.draw_icon( + ellipsis, + x_pos * ratio, + y_pos * ratio, + -2, + -2, + adjusted_scale + ); + } + $canvas.removeClass("hidden"); +} + +options.add_listener(() => { + if (client.is_watching()) return; + // synchronize visible state with new options. Because of messy timing + // issues with the crawl binary, this will run at least twice on + // startup. + let update_required = false; + + const new_scale = options.get("action_panel_scale"); + if (scale !== new_scale) { + scale = new_scale; + update_required = true; + } + + // is one of: horizontal, vertical + const new_orientation = options.get("action_panel_orientation"); + if (orientation !== new_orientation) { + orientation = new_orientation; + update_required = true; + } + + const new_min = !options.get("action_panel_show"); + if (new_min !== minimized) { + minimized = new_min; + update_required = true; + } + + const new_font_family = options.get("action_panel_font_family"); + if (font_family !== new_font_family) { + font_family = new_font_family; + _update_font_props(); + update_required = true; + } + + const new_font_size = options.get("action_panel_font_size"); + if (font_size !== new_font_size) { + font_size = new_font_size; + _update_font_props(); + update_required = true; + } + + // The panel should be disabled completely only if the player + // set the action_panel option to an empty string in the .rc + panel_disabled = options.get("action_panel_disabled"); + + if (update_required) { + if (!minimized) show_panel(false); + update(); + } }); diff --git a/crawl-ref/source/webserver/client/game/src/cell_renderer.js b/crawl-ref/source/webserver/client/game/src/cell_renderer.js index 6ceaed9a551..3ee131af33c 100644 --- a/crawl-ref/source/webserver/client/game/src/cell_renderer.js +++ b/crawl-ref/source/webserver/client/game/src/cell_renderer.js @@ -1,1357 +1,1335 @@ -define(["jquery", "./view_data", "./tileinfo-gui", "./tileinfo-main", - "./tileinfo-player", "./tileinfo-icons", "./tileinfo-dngn", "./enums", - "./map_knowledge", "./tileinfos", "./player", "./options", - "contrib/jquery.json"], -function ($, view_data, gui, main, tileinfo_player, icons, dngn, enums, - map_knowledge, tileinfos, player, options) { - "use strict"; - - function DungeonCellRenderer() - { - this.set_cell_size(32, 32); +import $ from "jquery"; + +import view_data from "./view_data"; +import gui from "../../../game_data/static/tileinfo-gui"; +import main from "../../../game_data/static/tileinfo-main"; +import tileinfo_player from "../../../game_data/static/tileinfo-player"; +import icons from "../../../game_data/static/tileinfo-icons"; +import dngn from "../../../game_data/static/tileinfo-dngn"; +import enums from "./enums"; +import map_knowledge from "./map_knowledge"; +import tileinfos from "./tileinfos"; +import player from "./player"; +import options from "./options"; + +function DungeonCellRenderer() { + this.set_cell_size(32, 32); +} + +let fg_term_colours, bg_term_colours; +let healthy, hp_spend, magic, magic_spend; + +function determine_colours() { + fg_term_colours = []; + bg_term_colours = []; + const $game = $("#game"); + for (let i = 0; i < 16; ++i) { + const elem = $(``); + $game.append(elem); + fg_term_colours.push(elem.css("color")); + bg_term_colours.push(elem.css("background-color")); + elem.detach(); + } + + // FIXME: CSS lookup doesn't work on Chrome after saving and loading a + // game, style information is missing for some reason. + // Use hard coded values instead. + healthy = "#8ae234"; + hp_spend = "#b30009"; + magic = "#5e78ff"; + // healthy = $("#stats_hp_bar_full").css("background-color"); + // hp_spend = $("#stats_hp_bar_decrease").css("background-color"); + // magic = $("#stats_mp_bar_full").css("background-color"); + magic_spend = "black"; +} + +function in_water(cell) { + return cell.bg.WATER && !cell.fg.FLYING; +} + +function split_term_colour(col) { + const fg = col & 0xf; + const bg = 0; + const attr = (col & 0xf0) >> 4; + const param = (col & 0xf000) >> 12; + return { fg: fg, bg: bg, attr: attr, param: param }; +} + +function term_colour_apply_attributes(col) { + if (col.attr === enums.CHATTR.HILITE) { + col.bg = col.param; + if (col.bg === col.fg) col.fg = 0; + } + if (col.attr === enums.CHATTR.REVERSE) { + const z = col.bg; + col.bg = col.fg; + col.fg = z; + } +} + +function get_img(id) { + return $(`#${id}`)[0]; +} + +function obj_to_str(o) { + return $.toJSON(o); +} + +$.extend(DungeonCellRenderer.prototype, { + init: function (element) { + this.element = element; + this.ctx = this.element.getContext("2d"); + }, + + set_cell_size: function (w, h) { + this.cell_width = Math.floor(w); + this.cell_height = Math.floor(h); + this.x_scale = this.cell_width / 32; + this.y_scale = this.cell_height / 32; + }, + + glyph_mode_font_name: function (scale, device) { + let glyph_scale; + if (scale) glyph_scale = scale * 100; + else { + if (this.ui_state === enums.ui.VIEW_MAP) + glyph_scale = options.get("tile_map_scale"); + else glyph_scale = options.get("tile_viewport_scale"); + glyph_scale = (glyph_scale - 100) / 2 + 100; } - - var fg_term_colours, bg_term_colours; - var healthy, hp_spend, magic, magic_spend; - - function determine_colours() - { - fg_term_colours = []; - bg_term_colours = []; - var $game = $("#game"); - for (var i = 0; i < 16; ++i) - { - var elem = $(""); - $game.append(elem); - fg_term_colours.push(elem.css("color")); - bg_term_colours.push(elem.css("background-color")); - elem.detach(); + if (device) glyph_scale = glyph_scale * window.devicePixelRatio; + + return ( + Math.floor((this.glyph_mode_font_size * glyph_scale) / 100) + + "px " + + this.glyph_mode_font + ); + }, + + glyph_mode_update_font_metrics: function () { + this.ctx.font = this.glyph_mode_font_name(); + + // Glyph used here does not matter because fontBoundingBoxAscent + // and fontBoundingBoxDescent are specific to the font whereas all + // glyphs in a monospaced font will have the same width + const metrics = this.ctx.measureText("@"); + this.glyph_mode_font_width = Math.ceil(metrics.width); + + // 2022: this feature appears to still be unavailable by default + // in firefox + if (metrics.fontBoundingBoxAscent) { + this.glyph_mode_baseline = metrics.fontBoundingBoxAscent; + this.glyph_mode_line_height = + metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent; + } else { + // Inspired by https://stackoverflow.com/q/1134586/ + const body = document.body; + const ref_glyph = document.createElement("span"); + const ref_block = document.createElement("div"); + const div = document.createElement("div"); + + ref_glyph.innerHTML = "@"; + ref_glyph.style.font = this.ctx.font; + + ref_block.style.display = "inline-block"; + ref_block.style.width = "1px"; + ref_block.style.height = "0px"; + + div.style.visibility = "hidden"; + div.appendChild(ref_glyph); + div.appendChild(ref_block); + body.appendChild(div); + + try { + ref_block.style["vertical-align"] = "baseline"; + this.glyph_mode_baseline = ref_block.offsetTop - ref_glyph.offsetTop; + ref_block.style["vertical-align"] = "bottom"; + this.glyph_mode_line_height = ref_block.offsetTop - ref_glyph.offsetTop; + } finally { + document.body.removeChild(div); + } + } + }, + + render_cursors: function (cx, cy, x, y) { + $.each(view_data.cursor_locs, (type, loc) => { + if (loc && loc.x === cx && loc.y === cy) { + let idx; + + switch (type) { + case enums.CURSOR_TUTORIAL: + idx = icons.TUTORIAL_CURSOR; + break; + case enums.CURSOR_MOUSE: + idx = icons.CURSOR; + // TODO: tilei.CURSOR2 if not visible + break; + case enums.CURSOR_MAP: + idx = icons.CURSOR; + break; } - // FIXME: CSS lookup doesn't work on Chrome after saving and loading a - // game, style information is missing for some reason. - // Use hard coded values instead. - healthy = "#8ae234"; - hp_spend = "#b30009"; - magic = "#5e78ff"; - // healthy = $("#stats_hp_bar_full").css("background-color"); - // hp_spend = $("#stats_hp_bar_decrease").css("background-color"); - // magic = $("#stats_mp_bar_full").css("background-color"); - magic_spend = "black"; + this.draw_icon(idx, x, y); + } + }); + }, + + scaled_size: function (w, h) { + w = w === undefined ? this.cell_width : w; + h = h === undefined ? this.cell_height : h; + const ratio = window.devicePixelRatio; + const width = Math.floor(w * ratio); + const height = Math.floor(h * ratio); + return { width: width, height: height }; + }, + + clear: function () { + this.ctx.fillStyle = "black"; + // element dimensions are already scaled + this.ctx.fillRect(0, 0, this.element.width, this.element.height); + }, + + do_render_cell: function (cx, cy, x, y, map_cell, cell) { + const scaled = this.scaled_size(); + + this.ctx.fillStyle = "black"; + this.ctx.fillRect(x, y, scaled.width, scaled.height); + + map_cell = map_cell || map_knowledge.get(cx, cy); + cell = cell || map_cell.t; + + if (!cell) { + this.render_cursors(cx, cy, x, y); + return; } - function in_water(cell) - { - return ((cell.bg.WATER) && !(cell.fg.FLYING)); + // track whether this cell overlaps to the top or left + this.current_sy = 0; + this.current_left_overlap = 0; + + cell.fg = enums.prepare_fg_flags(cell.fg || 0); + cell.bg = enums.prepare_bg_flags(cell.bg || 0); + cell.cloud = enums.prepare_fg_flags(cell.cloud || 0); + cell.icons = cell.icons || []; + cell.flv = cell.flv || {}; + cell.flv.f = cell.flv.f || 0; + cell.flv.s = cell.flv.s || 0; + map_cell.g = map_cell.g || " "; + if (map_cell.col === undefined) map_cell.col = 7; + + if (options.get("tile_display_mode") === "glyphs") { + this.render_glyph(x, y, map_cell, false); + + this.render_cursors(cx, cy, x, y); + this.draw_ray(x, y, cell); + return; } - function split_term_colour(col) - { - var fg = col & 0xF; - var bg = 0; - var attr = (col & 0xF0) >> 4; - var param = (col & 0xF000) >> 12; - return { fg: fg, bg: bg, attr: attr, param: param }; + // cell is basically a packed_cell + doll + mcache entries + if (options.get("tile_display_mode") === "tiles") + this.draw_background(x, y, cell); + + const fg_idx = cell.fg.value; + const is_in_water = in_water(cell); + + // draw clouds + if (cell.cloud.value) { + this.ctx.save(); + // If there will be a front/back cloud pair, draw + // the underlying one with correct alpha + if (fg_idx) { + try { + this.ctx.globalAlpha = 0.6; + this.set_nonsubmerged_clip(x, y, 20); + this.draw_main(cell.cloud.value, x, y); + } finally { + this.ctx.restore(); + } + + this.ctx.save(); + try { + this.ctx.globalAlpha = 0.2; + this.set_submerged_clip(x, y, 20); + this.draw_main(cell.cloud.value, x, y); + } finally { + this.ctx.restore(); + } + } else { + try { + this.ctx.globalAlpha = 1.0; + this.draw_main(cell.cloud.value, x, y); + } finally { + this.ctx.restore(); + } + } } - function term_colour_apply_attributes(col) - { - if (col.attr == enums.CHATTR.HILITE) - { - col.bg = col.param; - if (col.bg == col.fg) - col.fg = 0; + // Canvas doesn't support applying an alpha gradient + // to an image while drawing; so to achieve the same + // effect as in local tiles, it would probably be best + // to pregenerate water tiles with the (inverse) alpha + // gradient built in. This simply draws the lower + // half with increased transparency; for now, it looks + // good enough. + + const renderer = this; + function draw_dolls() { + if (fg_idx >= main.MAIN_MAX && cell.doll) { + const mcache_map = {}; + if (cell.mcache) { + for (let i = 0; i < cell.mcache.length; ++i) + mcache_map[cell.mcache[i][0]] = i; } - if (col.attr == enums.CHATTR.REVERSE) - { - var z = col.bg; - col.bg = col.fg; - col.fg = z; + $.each(cell.doll, (_i, doll_part) => { + let xofs = 0; + let yofs = 0; + if (mcache_map[doll_part[0]] !== undefined) { + const mind = mcache_map[doll_part[0]]; + xofs = cell.mcache[mind][1]; + yofs = cell.mcache[mind][2]; + } + renderer.draw_player(doll_part[0], x, y, xofs, yofs, doll_part[1]); + }); + } + + if (fg_idx >= tileinfo_player.MCACHE_START && cell.mcache) { + $.each(cell.mcache, (_i, mcache_part) => { + if (mcache_part) { + renderer.draw_player( + mcache_part[0], + x, + y, + mcache_part[1], + mcache_part[2] + ); + } + }); + } + } + + if (is_in_water && options.get("tile_display_mode") === "tiles") { + this.ctx.save(); + try { + this.ctx.globalAlpha = cell.trans ? 0.5 : 1.0; + + this.set_nonsubmerged_clip(x, y, 20); + + draw_dolls(); + } finally { + this.ctx.restore(); + } + + this.ctx.save(); + try { + this.ctx.globalAlpha = cell.trans ? 0.1 : 0.3; + this.set_submerged_clip(x, y, 20); + + draw_dolls(); + } finally { + this.ctx.restore(); + } + } else if (options.get("tile_display_mode") === "tiles") { + this.ctx.save(); + try { + this.ctx.globalAlpha = cell.trans ? 0.55 : 1.0; + + draw_dolls(); + } finally { + this.ctx.restore(); + } + } + + this.draw_foreground(x, y, map_cell); + + // draw clouds over stuff + if (fg_idx && cell.cloud.value) { + this.ctx.save(); + try { + this.ctx.globalAlpha = 0.4; + this.set_nonsubmerged_clip(x, y, 20); + this.draw_main(cell.cloud.value, x, y); + } finally { + this.ctx.restore(); + } + + this.ctx.save(); + try { + this.ctx.globalAlpha = 0.8; + this.set_submerged_clip(x, y, 20); + this.draw_main(cell.cloud.value, x, y); + } finally { + this.ctx.restore(); + } + } + + // Draw main-tile overlays (i.e. zaps), on top of clouds. + if (cell.ov) { + $.each(cell.ov, (_i, overlay) => { + if (dngn.FEAT_MAX <= overlay && overlay < main.MAIN_MAX) { + renderer.draw_main(overlay, x, y); } + }); } - function get_img(id) - { - return $("#" + id)[0]; + this.render_flash(x, y, map_cell); + + this.render_cursors(cx, cy, x, y); + + if ( + cx === player.pos.x && + cy === player.pos.y && + map_knowledge.player_on_level() + ) { + this.draw_minibars(x, y); } - function obj_to_str(o) - { - return $.toJSON(o); + // Debug helper + if (cell.mark) { + this.ctx.fillStyle = "red"; + this.ctx.font = "12px monospace"; + this.ctx.textAlign = "center"; + this.ctx.textBaseline = "middle"; + const scaled = this.scaled_size(); + this.ctx.fillText( + cell.mark, + x + 0.5 * scaled.width, + y + 0.5 * scaled.height + ); } - $.extend(DungeonCellRenderer.prototype, { - init: function(element) - { - this.element = element; - this.ctx = this.element.getContext("2d"); - }, - - set_cell_size: function(w, h) - { - this.cell_width = Math.floor(w); - this.cell_height = Math.floor(h); - this.x_scale = this.cell_width / 32; - this.y_scale = this.cell_height / 32; - }, - - glyph_mode_font_name: function (scale, device) - { - var glyph_scale; - if (scale) - glyph_scale = scale * 100; - else - { - if (this.ui_state == enums.ui.VIEW_MAP) - glyph_scale = options.get("tile_map_scale"); - else - glyph_scale = options.get("tile_viewport_scale"); - glyph_scale = ((glyph_scale - 100) / 2 + 100); - } - if (device) - glyph_scale = glyph_scale * window.devicePixelRatio; - - return (Math.floor(this.glyph_mode_font_size * glyph_scale / 100) - + "px " + this.glyph_mode_font); - }, - - glyph_mode_update_font_metrics: function () - { - this.ctx.font = this.glyph_mode_font_name(); - - // Glyph used here does not matter because fontBoundingBoxAscent - // and fontBoundingBoxDescent are specific to the font whereas all - // glyphs in a monospaced font will have the same width - var metrics = this.ctx.measureText('@'); - this.glyph_mode_font_width = Math.ceil(metrics.width); - - // 2022: this feature appears to still be unavailable by default - // in firefox - if (metrics.fontBoundingBoxAscent) - { - this.glyph_mode_baseline = metrics.fontBoundingBoxAscent; - this.glyph_mode_line_height = (metrics.fontBoundingBoxAscent - + metrics.fontBoundingBoxDescent); - } - else - { // Inspired by https://stackoverflow.com/q/1134586/ - var body = document.body; - var ref_glyph = document.createElement("span"); - var ref_block = document.createElement("div"); - var div = document.createElement("div"); - - ref_glyph.innerHTML = '@'; - ref_glyph.style.font = this.ctx.font; - - ref_block.style.display = "inline-block"; - ref_block.style.width = "1px"; - ref_block.style.height = "0px"; - - div.style.visibility = "hidden"; - div.appendChild(ref_glyph); - div.appendChild(ref_block); - body.appendChild(div); - - try - { - ref_block.style["vertical-align"] = "baseline"; - this.glyph_mode_baseline = (ref_block.offsetTop - - ref_glyph.offsetTop); - ref_block.style["vertical-align"] = "bottom"; - this.glyph_mode_line_height = (ref_block.offsetTop - - ref_glyph.offsetTop); - } - finally - { - document.body.removeChild(div); - } - } - }, - - render_cursors: function(cx, cy, x, y) - { - var renderer = this; - $.each(view_data.cursor_locs, function (type, loc) { - if (loc && (loc.x == cx) && (loc.y == cy)) - { - var idx; - - switch (type) - { - case enums.CURSOR_TUTORIAL: - idx = icons.TUTORIAL_CURSOR; - break; - case enums.CURSOR_MOUSE: - idx = icons.CURSOR; - // TODO: tilei.CURSOR2 if not visible - break; - case enums.CURSOR_MAP: - idx = icons.CURSOR; - break; - } - - renderer.draw_icon(idx, x, y); - } - }); - }, - - scaled_size: function(w, h) - { - w = w === undefined ? this.cell_width : w; - h = h === undefined ? this.cell_height : h; - const ratio = window.devicePixelRatio; - const width = Math.floor(w * ratio); - const height = Math.floor(h * ratio); - return {width: width, height: height}; - }, - - clear: function() - { - this.ctx.fillStyle = "black"; - // element dimensions are already scaled - this.ctx.fillRect(0, 0, this.element.width, this.element.height); - }, - - do_render_cell: function(cx, cy, x, y, map_cell, cell) - { - let scaled = this.scaled_size(); - - this.ctx.fillStyle = "black"; - this.ctx.fillRect(x, y, scaled.width, scaled.height); - - map_cell = map_cell || map_knowledge.get(cx, cy); - cell = cell || map_cell.t; - - if (!cell) - { - this.render_cursors(cx, cy, x, y); - return; - } - - // track whether this cell overlaps to the top or left - this.current_sy = 0; - this.current_left_overlap = 0; - - cell.fg = enums.prepare_fg_flags(cell.fg || 0); - cell.bg = enums.prepare_bg_flags(cell.bg || 0); - cell.cloud = enums.prepare_fg_flags(cell.cloud || 0); - cell.icons = cell.icons || []; - cell.flv = cell.flv || {}; - cell.flv.f = cell.flv.f || 0; - cell.flv.s = cell.flv.s || 0; - map_cell.g = map_cell.g || ' '; - if (map_cell.col == undefined) map_cell.col = 7; - - if (options.get("tile_display_mode") == "glyphs") - { - this.render_glyph(x, y, map_cell, false); - - this.render_cursors(cx, cy, x, y); - this.draw_ray(x, y, cell); - return; - } - - // cell is basically a packed_cell + doll + mcache entries - if (options.get("tile_display_mode") == "tiles") - this.draw_background(x, y, cell); - - var fg_idx = cell.fg.value; - var is_in_water = in_water(cell); - - // draw clouds - if (cell.cloud.value) - { - this.ctx.save(); - // If there will be a front/back cloud pair, draw - // the underlying one with correct alpha - if (fg_idx) - { - try - { - this.ctx.globalAlpha = 0.6; - this.set_nonsubmerged_clip(x, y, 20); - this.draw_main(cell.cloud.value, x, y); - } - finally - { - this.ctx.restore(); - } - - this.ctx.save(); - try - { - this.ctx.globalAlpha = 0.2; - this.set_submerged_clip(x, y, 20); - this.draw_main(cell.cloud.value, x, y); - } - finally - { - this.ctx.restore(); - } - } - else - { - try - { - this.ctx.globalAlpha = 1.0; - this.draw_main(cell.cloud.value, x, y); - } - finally - { - this.ctx.restore(); - } - } - } - - // Canvas doesn't support applying an alpha gradient - // to an image while drawing; so to achieve the same - // effect as in local tiles, it would probably be best - // to pregenerate water tiles with the (inverse) alpha - // gradient built in. This simply draws the lower - // half with increased transparency; for now, it looks - // good enough. - - var renderer = this; - function draw_dolls() - { - if ((fg_idx >= main.MAIN_MAX) && cell.doll) - { - var mcache_map = {}; - if (cell.mcache) - { - for (var i = 0; i < cell.mcache.length; ++i) - mcache_map[cell.mcache[i][0]] = i; - } - $.each(cell.doll, function (i, doll_part) { - var xofs = 0; - var yofs = 0; - if (mcache_map[doll_part[0]] !== undefined) - { - var mind = mcache_map[doll_part[0]]; - xofs = cell.mcache[mind][1]; - yofs = cell.mcache[mind][2]; - } - renderer.draw_player(doll_part[0], - x, y, xofs, yofs, doll_part[1]); - }); - } - - if ((fg_idx >= tileinfo_player.MCACHE_START) && cell.mcache) - { - $.each(cell.mcache, function (i, mcache_part) { - if (mcache_part) { - renderer.draw_player(mcache_part[0], - x, y, mcache_part[1], mcache_part[2]); - } - }); - } - } - - if (is_in_water && options.get("tile_display_mode") == "tiles") - { - this.ctx.save(); - try - { - this.ctx.globalAlpha = cell.trans ? 0.5 : 1.0; - - this.set_nonsubmerged_clip(x, y, 20); - - draw_dolls(); - } - finally - { - this.ctx.restore(); - } - - this.ctx.save(); - try - { - this.ctx.globalAlpha = cell.trans ? 0.1 : 0.3; - this.set_submerged_clip(x, y, 20); - - draw_dolls(); - } - finally - { - this.ctx.restore(); - } - } - else if (options.get("tile_display_mode") == "tiles") - { - this.ctx.save(); - try - { - this.ctx.globalAlpha = cell.trans ? 0.55 : 1.0; - - draw_dolls(); - } - finally - { - this.ctx.restore(); - } - } - - this.draw_foreground(x, y, map_cell); - - // draw clouds over stuff - if (fg_idx && cell.cloud.value) - { - this.ctx.save(); - try - { - this.ctx.globalAlpha = 0.4; - this.set_nonsubmerged_clip(x, y, 20); - this.draw_main(cell.cloud.value, x, y); - } - finally - { - this.ctx.restore(); - } - - this.ctx.save(); - try - { - this.ctx.globalAlpha = 0.8; - this.set_submerged_clip(x, y, 20); - this.draw_main(cell.cloud.value, x, y); - } - finally - { - this.ctx.restore(); - } - } - - // Draw main-tile overlays (i.e. zaps), on top of clouds. - if (cell.ov) - { - $.each(cell.ov, function (i, overlay) - { - if (dngn.FEAT_MAX <= overlay && overlay < main.MAIN_MAX) - { - renderer.draw_main(overlay, x, y); - } - }); - } - - this.render_flash(x, y, map_cell); - - this.render_cursors(cx, cy, x, y); - - if (cx == player.pos.x && cy == player.pos.y - && map_knowledge.player_on_level()) - { - this.draw_minibars(x, y); - } - - // Debug helper - if (cell.mark) - { - this.ctx.fillStyle = "red"; - this.ctx.font = "12px monospace"; - this.ctx.textAlign = "center"; - this.ctx.textBaseline = "middle"; - let scaled = this.scaled_size(); - this.ctx.fillText(cell.mark, - x + 0.5 * scaled.width, - y + 0.5 * scaled.height); - } - - cell.sy = this.current_sy; - cell.left_overlap = this.current_left_overlap; - }, - - // adapted from DungeonRegion::draw_minibars in tilereg_dgn.cc - draw_minibars: function(x, y) - { - var show_health = options.get("tile_show_minihealthbar"); - var show_magic = options.get("tile_show_minimagicbar"); - - // don't draw if hp and mp is full - if ((player.hp == player.hp_max || !show_health) && - (player.mp == player.mp_max || !show_magic)) - return; - - let cell = this.scaled_size(); - var bar_height = Math.floor(cell.height / 16); - var hp_bar_offset = bar_height; - - // TODO: use different colors if heavily wounded, like in the tiles version - if (player.mp_max > 0 && show_magic) - { - var mp_percent = player.mp / player.mp_max; - if (mp_percent < 0) mp_percent = 0; - - this.ctx.fillStyle = magic_spend; - this.ctx.fillRect(x, y + cell.height - bar_height, - cell.width, bar_height); - - this.ctx.fillStyle = magic; - this.ctx.fillRect(x, y + cell.height - bar_height, - cell.width * mp_percent, bar_height); - - hp_bar_offset += bar_height; - } - - if (show_health) - { - var hp_percent = player.hp / player.hp_max; - if (hp_percent < 0) hp_percent = 0; - - this.ctx.fillStyle = hp_spend; - this.ctx.fillRect(x, y + cell.height - hp_bar_offset, - cell.width, bar_height); - - this.ctx.fillStyle = healthy; - this.ctx.fillRect(x, y + cell.height - hp_bar_offset, - cell.width * hp_percent, bar_height); - } - }, - - render_cell: function() - { - if (window.debug_mode) - this.do_render_cell.apply(this, arguments); - else - { - try - { - this.do_render_cell.apply(this, arguments); - } - catch (err) - { - var cx = arguments[0]; - var cy = arguments[1]; - var cell = arguments[5]; - console.error("Error while drawing cell " + obj_to_str(cell) - + " at " + cx + "/" + cy + ": " + err); - } - } - }, - - render_glyph: function (x, y, map_cell, omit_bg, square, scale) - { - // `map_cell` can be anything as long as it provides `col` and `g` - var col = split_term_colour(map_cell.col); - if (omit_bg && col.attr == enums.CHATTR.REVERSE) - col.attr = 0; - term_colour_apply_attributes(col); - let cell = this.scaled_size(); - var w = cell.width; - var h = cell.height; - if (scale) - { - // assume x and y are already scaled... - w = w * scale; - h = h * scale; - } - - var prefix = ""; - if (col.attr == enums.CHATTR.BOLD) - prefix = "bold "; - - if (!omit_bg) - { - this.ctx.fillStyle = bg_term_colours[col.bg]; - this.ctx.fillRect(x, y, w, h); - } - this.ctx.fillStyle = fg_term_colours[col.fg]; - this.ctx.font = prefix + this.glyph_mode_font_name(scale, true); - - this.ctx.save(); - - try - { - this.ctx.beginPath(); - this.ctx.rect(x, y, w, h); - this.ctx.clip(); - - if (square) - { - this.ctx.textAlign = "center"; - this.ctx.textBaseline = "middle"; - this.ctx.fillText(map_cell.g, - Math.floor(x + w/2), - Math.floor(y + h/2)); - } - else - { - this.ctx.fillText(map_cell.g, x, - y + this.glyph_mode_baseline * window.devicePixelRatio); - } - } - finally - { - this.ctx.restore(); - } - }, - - render_flash: function(x, y, map_cell) - { - if (map_cell.flc) - { - var col = view_data.get_flash_colour(map_cell.flc, map_cell.fla); - this.ctx.save(); - try - { - this.ctx.fillStyle = "rgb(" + col.r + "," + - col.g + "," + col.b + ")"; - this.ctx.globalAlpha = col.a / 255; - const cell = this.scaled_size(); - this.ctx.fillRect(x, y, cell.width, cell.height); - } - finally - { - this.ctx.restore(); - } - } - }, - - set_submerged_clip: function(x, y, water_level) - { - this.ctx.beginPath(); - const scaled = this.scaled_size(null, water_level * this.y_scale); - // this clip is across the entire row - this.ctx.rect(0, y + scaled.height, - this.element.width, - this.element.height - y - scaled.height); - this.ctx.clip(); - }, - - set_nonsubmerged_clip: function(x, y, water_level) - { - this.ctx.beginPath(); - const scaled = this.scaled_size(null, water_level * this.y_scale); - this.ctx.rect(0, 0, this.element.width, y + scaled.height); - this.ctx.clip(); - }, - - // Much of the following is more or less directly copied from tiledgnbuf.cc - draw_blood_overlay: function(x, y, cell, is_wall) - { - var offset; - - if (cell.liquefied && !is_wall) - { - offset = cell.flv.s % dngn.tile_count(dngn.LIQUEFACTION); - this.draw_dngn(dngn.LIQUEFACTION + offset, x, y); - } - else if (cell.bloody) - { - cell.blood_rotation = cell.blood_rotation || 0; - var basetile; - if (is_wall) - { - basetile = cell.old_blood ? dngn.WALL_OLD_BLOOD : dngn.WALL_BLOOD_S; - basetile += dngn.tile_count(basetile) * cell.blood_rotation; - basetile = dngn.WALL_BLOOD_S + dngn.tile_count(dngn.WALL_BLOOD_S) - * cell.blood_rotation; - } - else - basetile = dngn.BLOOD; - offset = cell.flv.s % dngn.tile_count(basetile); - this.draw_dngn(basetile + offset, x, y); - } - else if (cell.moldy) - { - offset = cell.flv.s % dngn.tile_count(dngn.MOLD); - this.draw_dngn(dngn.MOLD + offset, x, y); - } - else if (cell.glowing_mold) - { - offset = cell.flv.s % dngn.tile_count(dngn.GLOWING_MOLD); - this.draw_dngn(dngn.GLOWING_MOLD + offset, x, y); - } - }, - - draw_ray: function(x, y, cell) - { - var bg = cell.bg; - var bg_idx = cell.bg.value; - var renderer = this; - - if (bg.RAY) - this.draw_dngn(dngn.RAY, x, y); - else if (bg.RAY_OOR) - this.draw_dngn(dngn.RAY_OUT_OF_RANGE, x, y); - else if (bg.LANDING) - this.draw_dngn(dngn.LANDING, x, y); - else if (bg.RAY_MULTI) - this.draw_dngn(dngn.RAY_MULTI, x, y); - }, - - draw_background: function(x, y, cell) - { - var bg = cell.bg; - var bg_idx = cell.bg.value; - var renderer = this; - - if (cell.mangrove_water && bg_idx > dngn.DNGN_UNSEEN) - this.draw_dngn(dngn.DNGN_SHALLOW_WATER, x, y); - else if (bg_idx >= dngn.DNGN_FIRST_TRANSPARENT) - { - this.draw_dngn(cell.flv.f, x, y); // f = floor - - // Draw floor overlays beneath the feature - if (cell.ov) - { - $.each(cell.ov, function (i, overlay) - { - if (overlay && overlay <= dngn.FLOOR_MAX) - renderer.draw_dngn(overlay, x, y); - }); - } - } - - // Draw blood beneath feature tiles. - if (bg_idx > dngn.WALL_MAX) - this.draw_blood_overlay(x, y, cell); - - if (cell.mangrove_water) // Draw the tree submerged - { - this.ctx.save(); - try - { - this.ctx.globalAlpha = 1.0; - - this.set_nonsubmerged_clip(x, y, 20); - - this.draw_dngn(bg_idx, x, y); - } - finally - { - this.ctx.restore(); - } - - this.ctx.save(); - try - { - this.ctx.globalAlpha = 0.3; - this.set_submerged_clip(x, y, 20); - - this.draw_dngn(bg_idx, x, y); - } - finally - { - this.ctx.restore(); - } - } - else - this.draw_dngn(bg_idx, x, y); - - if (bg_idx > dngn.DNGN_UNSEEN) - { - // Draw blood on top of wall tiles. - if (bg_idx <= dngn.WALL_MAX) - this.draw_blood_overlay(x, y, cell, bg_idx > dngn.FLOOR_MAX); - - // Draw overlays - var ray_tile = 0; - if (cell.ov) - { - $.each(cell.ov, function (i, overlay) - { - if (overlay > dngn.DNGN_MAX) - return; - else if (overlay == dngn.RAY - || overlay == dngn.RAY_MULTI - || overlay == dngn.RAY_OUT_OF_RANGE) - { - // these need to be drawn last because of the - // way alpha blending happens here, but for - // hard-to-change reasons they are assembled on - // the server side in the wrong order. (In local - // tiles it's ok to blend them in any order.) - // assumption: only one can appear on any tile. - // TODO: a more general fix for this issue? - ray_tile = overlay; - } - else if (overlay && - (bg_idx < dngn.DNGN_FIRST_TRANSPARENT || - overlay > dngn.FLOOR_MAX)) - { - renderer.draw_dngn(overlay, x, y); - } - }); - } - if (ray_tile) - renderer.draw_dngn(ray_tile, x, y); - - if (!bg.UNSEEN) - { - if (bg.KRAKEN_NW) - this.draw_dngn(dngn.KRAKEN_OVERLAY_NW, x, y); - else if (bg.ELDRITCH_NW) - this.draw_dngn(dngn.ELDRITCH_OVERLAY_NW, x, y); - if (bg.KRAKEN_NE) - this.draw_dngn(dngn.KRAKEN_OVERLAY_NE, x, y); - else if (bg.ELDRITCH_NE) - this.draw_dngn(dngn.ELDRITCH_OVERLAY_NE, x, y); - if (bg.KRAKEN_SE) - this.draw_dngn(dngn.KRAKEN_OVERLAY_SE, x, y); - else if (bg.ELDRITCH_SE) - this.draw_dngn(dngn.ELDRITCH_OVERLAY_SE, x, y); - if (bg.KRAKEN_SW) - this.draw_dngn(dngn.KRAKEN_OVERLAY_SW, x, y); - else if (bg.ELDRITCH_SW) - this.draw_dngn(dngn.ELDRITCH_OVERLAY_SW, x, y); - } - - if (!bg.UNSEEN) - { - if (cell.sanctuary) - this.draw_dngn(dngn.SANCTUARY, x, y); - if (cell.blasphemy) - this.draw_dngn(dngn.BLASPHEMY, x, y); - if (cell.has_bfb_corpse) - this.draw_dngn(dngn.BLOOD_FOR_BLOOD, x, y); - if (cell.silenced) - this.draw_dngn(dngn.SILENCED, x, y); - if (cell.halo == enums.HALO_RANGE) - this.draw_dngn(dngn.HALO_RANGE, x, y); - if (cell.halo >= enums.HALO_UMBRA_FIRST - && cell.halo <= enums.HALO_UMBRA_LAST) - { - var variety = cell.halo - enums.HALO_UMBRA_FIRST; - this.draw_dngn(dngn.UMBRA + variety, x, y); - } - if (cell.orb_glow) - this.draw_dngn(dngn.ORB_GLOW + cell.orb_glow - 1, x, y); - if (cell.quad_glow) - this.draw_dngn(dngn.QUAD_GLOW, x, y); - if (cell.disjunct) - this.draw_dngn(dngn.DISJUNCT + cell.disjunct - 1, x, y); - if (cell.awakened_forest) - this.draw_icon(icons.BERSERK, x, y); - - if (cell.fg) - { - var fg = cell.fg; - if (fg.PET) - this.draw_dngn(dngn.HALO_FRIENDLY, x, y); - else if (fg.GD_NEUTRAL) - this.draw_dngn(dngn.HALO_GD_NEUTRAL, x, y); - else if (fg.NEUTRAL) - this.draw_dngn(dngn.HALO_NEUTRAL, x, y); - - // Monster difficulty. Ghosts get a special highlight. - if (fg.GHOST) - { - if (fg.TRIVIAL) - this.draw_dngn(dngn.THREAT_GHOST_TRIVIAL, x, y); - else if (fg.EASY) - this.draw_dngn(dngn.THREAT_GHOST_EASY, x, y); - else if (fg.TOUGH) - this.draw_dngn(dngn.THREAT_GHOST_TOUGH, x, y); - else if (fg.NASTY) - this.draw_dngn(dngn.THREAT_GHOST_NASTY, x, y); - else if (fg.UNUSUAL) - this.draw_dngn(dngn.THREAT_UNUSUAL, x, y); - } - else - { - if (fg.TRIVIAL) - this.draw_dngn(dngn.THREAT_TRIVIAL, x, y); - else if (fg.EASY) - this.draw_dngn(dngn.THREAT_EASY, x, y); - else if (fg.TOUGH) - this.draw_dngn(dngn.THREAT_TOUGH, x, y); - else if (fg.NASTY) - this.draw_dngn(dngn.THREAT_NASTY, x, y); - else if (fg.UNUSUAL) - this.draw_dngn(dngn.THREAT_UNUSUAL, x, y); - } - - if (cell.highlighted_summoner) - this.draw_dngn(dngn.HALO_SUMMONER, x, y); - } - - - // Apply the travel exclusion under the foreground if the cell is - // visible. It will be applied later if the cell is unseen. - if (bg.EXCL_CTR) - this.draw_dngn(dngn.TRAVEL_EXCLUSION_CENTRE_BG, x, y); - else if (bg.TRAV_EXCL) - this.draw_dngn(dngn.TRAVEL_EXCLUSION_BG, x, y); - } - } - - this.draw_ray(x, y, cell); - }, - - draw_submerged_tile: function(base_idx, idx, x, y, trans, img_scale) - { - this.ctx.save(); - try - { - this.ctx.globalAlpha = trans ? 0.5 : 1.0; - - this.set_nonsubmerged_clip(x, y, 20); - - if (base_idx) - this.draw_main(base_idx, x, y, img_scale); - - this.draw_main(idx, x, y, img_scale); - } - finally - { - this.ctx.restore(); - } - - this.ctx.save(); - try - { - this.ctx.globalAlpha = trans ? 0.1 : 0.3; - this.set_submerged_clip(x, y, 20); - - if (base_idx) - this.draw_main(base_idx, x, y, img_scale); - - this.draw_main(idx, x, y, img_scale); - } - finally - { - this.ctx.restore(); - } - }, - - draw_foreground: function(x, y, map_cell, img_scale) - { - var cell = map_cell.t; - var fg = cell.fg; - var bg = cell.bg; - var fg_idx = cell.fg.value; - var is_in_water = in_water(cell); - - if (fg_idx && fg_idx <= main.MAIN_MAX && options.get("tile_display_mode") == "tiles") - { - var base_idx = cell.base; - if (is_in_water) - { - this.draw_submerged_tile(base_idx, fg_idx, x, y, - cell.trans, img_scale); - } - else - { - if (base_idx) - this.draw_main(base_idx, x, y, img_scale); - - this.draw_main(fg_idx, x, y, img_scale); - } - - if (fg_idx >= main.PARCHMENT_LOW && fg_idx <= main.PARCHMENT_HIGH) - { - if (cell.overlay1) - { - if (is_in_water) - { - this.draw_submerged_tile(null, cell.overlay1, x, y, - cell.trans, img_scale) - } - else - this.draw_main(cell.overlay1, x, y, img_scale); - } - if (cell.overlay2) - { - if (is_in_water) - { - this.draw_submerged_tile(null, cell.overlay2, x, y, - cell.trans, img_scale) - } - else - this.draw_main(cell.overlay2, x, y, img_scale); - } - } - } - else if (options.get("tile_display_mode") == "hybrid") - { - this.render_glyph(x, y, map_cell, true, true); - this.draw_ray(x, y, cell); - img_scale = undefined; // TODO: make this work? - } - - if (fg.NET) - this.draw_icon(icons.TRAP_NET, x, y, undefined, undefined, img_scale); - - if (fg.WEB) - this.draw_icon(icons.TRAP_WEB, x, y, undefined, undefined, img_scale); - - if (fg.S_UNDER) - this.draw_icon(icons.SOMETHING_UNDER, x, y, undefined, undefined, img_scale); - - // Pet mark - if (fg.PET) - this.draw_icon(icons.FRIENDLY, x, y, undefined, undefined, img_scale); - else if (fg.GD_NEUTRAL) - this.draw_icon(icons.GOOD_NEUTRAL, x, y, undefined, undefined, img_scale); - else if (fg.NEUTRAL) - this.draw_icon(icons.NEUTRAL, x, y, undefined, undefined, img_scale); - - var status_shift = 0; - if (fg.PARALYSED) - { - this.draw_icon(icons.PARALYSED, x, y, undefined, undefined, img_scale); - status_shift += 12; - } - else if (fg.STAB) - { - this.draw_icon(icons.STAB_BRAND, x, y, undefined, undefined, img_scale); - status_shift += 12; - } - else if (fg.MAY_STAB) - { - this.draw_icon(icons.UNAWARE, x, y, undefined, undefined, img_scale); - status_shift += 7; - } - else if (fg.FLEEING) - { - this.draw_icon(icons.FLEEING, x, y, undefined, undefined, img_scale); - status_shift += 3; - } - - if (fg.POISON) - { - this.draw_icon(icons.POISON, x, y, -status_shift, 0, img_scale); - status_shift += 5; - } - else if (fg.MORE_POISON) - { - this.draw_icon(icons.MORE_POISON, x, y, -status_shift, 0, img_scale); - status_shift += 5; - } - else if (fg.MAX_POISON) - { - this.draw_icon(icons.MAX_POISON, x, y, -status_shift, 0, img_scale); - status_shift += 5; - } - - for (var i = 0; i < cell.icons.length; ++i) - { - status_shift += this.draw_icon_type(cell.icons[i], x, y, -status_shift, 0, img_scale); - } - - if (bg.UNSEEN && (bg.value || fg.value)) - this.draw_icon(icons.MESH, x, y, undefined, undefined, img_scale); - - if (bg.OOR && (bg.value || fg.value)) - this.draw_icon(icons.OOR_MESH, x, y, undefined, undefined, img_scale); - - if (bg.MM_UNSEEN && (bg.value || fg.value)) - this.draw_icon(icons.MAGIC_MAP_MESH, x, y, undefined, undefined, img_scale); - - if (bg.RAMPAGE) - this.draw_icon(icons.RAMPAGE, x, y, undefined, undefined, img_scale); - - // Don't let the "new stair" icon cover up any existing icons, but - // draw it otherwise. - if (bg.NEW_STAIR && status_shift == 0) - this.draw_icon(icons.NEW_STAIR, x, y, undefined, undefined, img_scale); - - if (bg.NEW_TRANSPORTER && status_shift == 0) - this.draw_icon(icons.NEW_TRANSPORTER, x, y, undefined, undefined, img_scale); - - if (bg.EXCL_CTR && bg.UNSEEN) - this.draw_icon(icons.TRAVEL_EXCLUSION_CENTRE_FG, x, y, undefined, undefined, img_scale); - else if (bg.TRAV_EXCL && bg.UNSEEN) - this.draw_icon(icons.TRAVEL_EXCLUSION_FG, x, y, undefined, undefined, img_scale); - - // Tutorial cursor takes precedence over other cursors. - if (bg.TUT_CURSOR) - { - this.draw_icon(icons.TUTORIAL_CURSOR, x, y, undefined, undefined, img_scale); - } - else if (bg.CURSOR1) - { - this.draw_icon(icons.CURSOR, x, y, undefined, undefined, img_scale); - } - else if (bg.CURSOR2) - { - this.draw_icon(icons.CURSOR2, x, y, undefined, undefined, img_scale); - } - else if (bg.CURSOR3) - { - this.draw_icon(icons.CURSOR3, x, y, undefined, undefined, img_scale); - } - - if (cell.travel_trail & 0xF) - { - this.draw_icon(icons.TRAVEL_PATH_FROM + - (cell.travel_trail & 0xF) - 1, x, y, undefined, undefined, img_scale); - } - if (cell.travel_trail & 0xF0) - { - this.draw_icon(icons.TRAVEL_PATH_TO + - ((cell.travel_trail & 0xF0) >> 4) - 1, x, y, undefined, undefined, img_scale); - } - - if (fg.MDAM_LIGHT) - this.draw_icon(icons.MDAM_LIGHTLY_DAMAGED, x, y, undefined, undefined, img_scale); - else if (fg.MDAM_MOD) - this.draw_icon(icons.MDAM_MODERATELY_DAMAGED, x, y, undefined, undefined, img_scale); - else if (fg.MDAM_HEAVY) - this.draw_icon(icons.MDAM_HEAVILY_DAMAGED, x, y, undefined, undefined, img_scale); - else if (fg.MDAM_SEV) - this.draw_icon(icons.MDAM_SEVERELY_DAMAGED, x, y, undefined, undefined, img_scale); - else if (fg.MDAM_ADEAD) - this.draw_icon(icons.MDAM_ALMOST_DEAD, x, y, undefined, undefined, img_scale); - - if (options.get("tile_show_demon_tier") === true) - { - if (fg.DEMON_1) - this.draw_icon(icons.DEMON_NUM1, x, y, undefined, undefined, img_scale); - else if (fg.DEMON_2) - this.draw_icon(icons.DEMON_NUM2, x, y, undefined, undefined, img_scale); - else if (fg.DEMON_3) - this.draw_icon(icons.DEMON_NUM3, x, y, undefined, undefined, img_scale); - else if (fg.DEMON_4) - this.draw_icon(icons.DEMON_NUM4, x, y, undefined, undefined, img_scale); - else if (fg.DEMON_5) - this.draw_icon(icons.DEMON_NUM5, x, y, undefined, undefined, img_scale); - } - }, - - draw_icon_type: function(idx, x, y, ofsx, ofsy, img_scale) - { - switch (idx) - { - //These icons are in the lower right, so status_shift doesn't need changing. - case icons.BERSERK: - case icons.IDEALISED: - case icons.TOUCH_OF_BEOGH: - case icons.SHADOWLESS: - // Anim. weap. and summoned might overlap, but that's okay - case icons.SUMMONED: - case icons.MINION: - case icons.UNREWARDING: - case icons.ANIMATED_WEAPON: - case icons.VENGEANCE_TARGET: - case icons.VAMPIRE_THRALL: - case icons.ENKINDLED_1: - case icons.ENKINDLED_2: - case icons.NOBODY_MEMORY_1: - case icons.NOBODY_MEMORY_2: - case icons.NOBODY_MEMORY_3: - case icons.PYRRHIC: - case icons.FRENZIED: - this.draw_icon(idx, x, y, undefined, undefined, img_scale); - return 0; - case icons.DRAIN: - case icons.MIGHT: - case icons.SWIFT: - case icons.DAZED: - case icons.HASTED: - case icons.SLOWED: - case icons.CORRODED: - case icons.INFESTED: - case icons.WEAKENED: - case icons.PETRIFIED: - case icons.PETRIFYING: - case icons.BOUND_SOUL: - case icons.POSSESSABLE: - case icons.PARTIALLY_CHARGED: - case icons.FULLY_CHARGED: - case icons.VITRIFIED: - case icons.CONFUSED: - case icons.LACED_WITH_CHAOS: - case icons.SENTINEL_MARK: - case icons.DIMMED: - this.draw_icon(idx, x, y, ofsx, ofsy, img_scale); - return 6; - case icons.CONC_VENOM: - case icons.FIRE_CHAMP: - case icons.INNER_FLAME: - case icons.PAIN_MIRROR: - case icons.STICKY_FLAME: - this.draw_icon(idx, x, y, ofsx, ofsy, img_scale); - return 7; - case icons.ANGUISH: - case icons.FIRE_VULN: - case icons.RESISTANCE: - case icons.GHOSTLY: - case icons.MALMUTATED: - this.draw_icon(idx, x, y, ofsx, ofsy, img_scale); - return 8; - case icons.RECALL: - case icons.TELEPORTING: - this.draw_icon(idx, x, y, ofsx, ofsy, img_scale); - return 9; - case icons.BLIND: - case icons.BRILLIANCE: - case icons.SLOWLY_DYING: - case icons.WATERLOGGED: - case icons.STILL_WINDS: - case icons.ANTIMAGIC: - case icons.REPEL_MISSILES: - case icons.INJURY_BOND: - case icons.GLOW_LIGHT: - case icons.GLOW_HEAVY: - case icons.BULLSEYE: - case icons.CURSE_OF_AGONY: - case icons.REGENERATION: - case icons.RETREAT: - case icons.RIMEBLIGHT: - case icons.UNDYING_ARMS: - case icons.BIND: - case icons.SIGN_OF_RUIN: - case icons.WEAK_WILLED: - case icons.DOUBLED_VIGOUR: - case icons.KINETIC_GRAPNEL: - case icons.TEMPERED: - case icons.HEART: - case icons.UNSTABLE: - case icons.VEXED: - case icons.PARADOX: - case icons.WARDING: - case icons.FIGMENT: - this.draw_icon(idx, x, y, ofsx, ofsy, img_scale); - return 10; - case icons.CONSTRICTED: - case icons.VILE_CLUTCH: - case icons.PAIN_BOND: - this.draw_icon(idx, x, y, ofsx, ofsy, img_scale); - return 11; - } - }, - - // Helper functions for drawing from specific textures - draw_tile: function(idx, x, y, mod, ofsx, ofsy, y_max, centre, img_scale) - { - // assumption: x and y are already appropriately scaled for the - // canvas. Now we just need to figure out where in the scaled - // cell size the tile belongs. - var info = mod.get_tile_info(idx); - var img = get_img(mod.get_img(idx)); - if (!info) - { - throw new Error("Tile not found: " + idx); - } - - // this somewhat convoluted approach is to avoid fp scaling - // artifacts at scale 1.0 - var img_xscale = this.x_scale; - var img_yscale = this.y_scale; - if (img_scale != undefined) - { - img_xscale = img_xscale * img_scale; - img_yscale = img_yscale * img_scale; - } - else - img_scale = 1.0; - - centre = centre === undefined ? true : centre; - var size_ox = !centre ? 0 : 32 / 2 - info.w / 2; - var size_oy = !centre ? 0 : 32 - info.h; - var pos_sy_adjust = (ofsy || 0) + info.oy + size_oy; - var pos_ey_adjust = pos_sy_adjust + info.ey - info.sy; - var sy = pos_sy_adjust; - var ey = pos_ey_adjust; - if (y_max && y_max < ey) - ey = y_max; - - if (sy >= ey) - return; - - var total_x_offset = ((ofsx || 0) + info.ox + size_ox); - - // Offsets can be negative, in which case we are drawing overlapped - // with a cell either to the right or above. If so, store the - // overlap in cell state so that it can later be checked to trigger - // a redraw. - // These store logical pixels, but that currently doesn't matter - // because they are only used for a comparison vs 0 - // See dungeon_renderer.js, render_loc. - if (total_x_offset < this.current_left_overlap) - this.current_left_overlap = total_x_offset; - - if (sy < this.current_sy) - this.current_sy = sy; - - // dimensions in the source (the tilesheet) - const w = info.ex - info.sx; - const h = ey - sy; - const ratio = window.devicePixelRatio; - // dimensions in the target cell. To get this right at the pixel - // level, we need to calculate the height/width as if it is offset - // relative to the cell origin. Because of differences in how x/y - // are treated above, these may look like they're doing something - // different, but they shouldn't be. - const scaled_w = Math.floor((total_x_offset + w) * img_xscale * ratio) - - Math.floor(total_x_offset * img_xscale * ratio); - const scaled_h = Math.floor(ey * img_yscale * ratio) - - Math.floor(sy * img_yscale * ratio); - - this.ctx.imageSmoothingEnabled = options.get("tile_filter_scaling"); - this.ctx.drawImage(img, - info.sx, info.sy + sy - pos_sy_adjust, w, h, - x + Math.floor(total_x_offset * img_xscale * ratio), - y + Math.floor(sy * img_yscale * ratio), - scaled_w, - scaled_h); - }, - - draw_dngn: function(idx, x, y, img_scale) - { - this.draw_tile(idx, x, y, dngn, - undefined, undefined, undefined, undefined, - img_scale); - }, - - draw_gui: function(idx, x, y, img_scale) - { - this.draw_tile(idx, x, y, gui, - undefined, undefined, undefined, undefined, - img_scale); - }, - - draw_main: function(idx, x, y, img_scale) - { - this.draw_tile(idx, x, y, main, - undefined, undefined, undefined, undefined, - img_scale); - }, - - draw_player: function(idx, x, y, ofsx, ofsy, y_max, img_scale) - { - this.draw_tile(idx, x, y, tileinfo_player, ofsx, ofsy, y_max, - undefined, - img_scale); - }, - - draw_icon: function(idx, x, y, ofsx, ofsy, img_scale) - { - this.draw_tile(idx, x, y, icons, ofsx, ofsy, - undefined, undefined, - img_scale); - }, - - draw_quantity: function(qty, x, y, font) - { - qty = Math.max(0, Math.min(999, qty)); - - this.ctx.fillStyle = "white"; - this.ctx.font = font; - - this.ctx.shadowColor = "black"; - this.ctx.shadowBlur = 2; - this.ctx.shadowOffsetX = 1; - this.ctx.shadowOffsetY = 1; - this.ctx.textAlign = "left"; - this.ctx.textBaseline = "top"; - this.ctx.fillText(qty, (x + 2), (y + 2)); - }, - - draw_from_texture: function (idx, x, y, tex, ofsx, ofsy, y_max, centre, img_scale) - { - var mod = tileinfos(tex); - this.draw_tile(idx, x, y, mod, ofsx, ofsy, y_max, centre, img_scale); - }, - }); + cell.sy = this.current_sy; + cell.left_overlap = this.current_left_overlap; + }, + + // adapted from DungeonRegion::draw_minibars in tilereg_dgn.cc + draw_minibars: function (x, y) { + const show_health = options.get("tile_show_minihealthbar"); + const show_magic = options.get("tile_show_minimagicbar"); + + // don't draw if hp and mp is full + if ( + (player.hp === player.hp_max || !show_health) && + (player.mp === player.mp_max || !show_magic) + ) + return; + + const cell = this.scaled_size(); + const bar_height = Math.floor(cell.height / 16); + let hp_bar_offset = bar_height; + + // TODO: use different colors if heavily wounded, like in the tiles version + if (player.mp_max > 0 && show_magic) { + let mp_percent = player.mp / player.mp_max; + if (mp_percent < 0) mp_percent = 0; + + this.ctx.fillStyle = magic_spend; + this.ctx.fillRect( + x, + y + cell.height - bar_height, + cell.width, + bar_height + ); + + this.ctx.fillStyle = magic; + this.ctx.fillRect( + x, + y + cell.height - bar_height, + cell.width * mp_percent, + bar_height + ); + + hp_bar_offset += bar_height; + } + + if (show_health) { + let hp_percent = player.hp / player.hp_max; + if (hp_percent < 0) hp_percent = 0; + + this.ctx.fillStyle = hp_spend; + this.ctx.fillRect( + x, + y + cell.height - hp_bar_offset, + cell.width, + bar_height + ); + + this.ctx.fillStyle = healthy; + this.ctx.fillRect( + x, + y + cell.height - hp_bar_offset, + cell.width * hp_percent, + bar_height + ); + } + }, + + render_cell: function () { + if (window.debug_mode) this.do_render_cell.apply(this, arguments); + else { + try { + this.do_render_cell.apply(this, arguments); + } catch (err) { + const cx = arguments[0]; + const cy = arguments[1]; + const cell = arguments[5]; + console.error( + "Error while drawing cell " + + obj_to_str(cell) + + " at " + + cx + + "/" + + cy + + ": " + + err + ); + } + } + }, + + render_glyph: function (x, y, map_cell, omit_bg, square, scale) { + // `map_cell` can be anything as long as it provides `col` and `g` + const col = split_term_colour(map_cell.col); + if (omit_bg && col.attr === enums.CHATTR.REVERSE) col.attr = 0; + term_colour_apply_attributes(col); + const cell = this.scaled_size(); + let w = cell.width; + let h = cell.height; + if (scale) { + // assume x and y are already scaled... + w = w * scale; + h = h * scale; + } + + let prefix = ""; + if (col.attr === enums.CHATTR.BOLD) prefix = "bold "; + + if (!omit_bg) { + this.ctx.fillStyle = bg_term_colours[col.bg]; + this.ctx.fillRect(x, y, w, h); + } + this.ctx.fillStyle = fg_term_colours[col.fg]; + this.ctx.font = prefix + this.glyph_mode_font_name(scale, true); + + this.ctx.save(); + + try { + this.ctx.beginPath(); + this.ctx.rect(x, y, w, h); + this.ctx.clip(); + + if (square) { + this.ctx.textAlign = "center"; + this.ctx.textBaseline = "middle"; + this.ctx.fillText( + map_cell.g, + Math.floor(x + w / 2), + Math.floor(y + h / 2) + ); + } else { + this.ctx.fillText( + map_cell.g, + x, + y + this.glyph_mode_baseline * window.devicePixelRatio + ); + } + } finally { + this.ctx.restore(); + } + }, + + render_flash: function (x, y, map_cell) { + if (map_cell.flc) { + const col = view_data.get_flash_colour(map_cell.flc, map_cell.fla); + this.ctx.save(); + try { + this.ctx.fillStyle = `rgb(${col.r},${col.g},${col.b})`; + this.ctx.globalAlpha = col.a / 255; + const cell = this.scaled_size(); + this.ctx.fillRect(x, y, cell.width, cell.height); + } finally { + this.ctx.restore(); + } + } + }, + + set_submerged_clip: function (_x, y, water_level) { + this.ctx.beginPath(); + const scaled = this.scaled_size(null, water_level * this.y_scale); + // this clip is across the entire row + this.ctx.rect( + 0, + y + scaled.height, + this.element.width, + this.element.height - y - scaled.height + ); + this.ctx.clip(); + }, + + set_nonsubmerged_clip: function (_x, y, water_level) { + this.ctx.beginPath(); + const scaled = this.scaled_size(null, water_level * this.y_scale); + this.ctx.rect(0, 0, this.element.width, y + scaled.height); + this.ctx.clip(); + }, + + // Much of the following is more or less directly copied from tiledgnbuf.cc + draw_blood_overlay: function (x, y, cell, is_wall) { + let offset; + + if (cell.liquefied && !is_wall) { + offset = cell.flv.s % dngn.tile_count(dngn.LIQUEFACTION); + this.draw_dngn(dngn.LIQUEFACTION + offset, x, y); + } else if (cell.bloody) { + cell.blood_rotation = cell.blood_rotation || 0; + let basetile; + if (is_wall) { + basetile = cell.old_blood ? dngn.WALL_OLD_BLOOD : dngn.WALL_BLOOD_S; + basetile += dngn.tile_count(basetile) * cell.blood_rotation; + basetile = + dngn.WALL_BLOOD_S + + dngn.tile_count(dngn.WALL_BLOOD_S) * cell.blood_rotation; + } else basetile = dngn.BLOOD; + offset = cell.flv.s % dngn.tile_count(basetile); + this.draw_dngn(basetile + offset, x, y); + } else if (cell.moldy) { + offset = cell.flv.s % dngn.tile_count(dngn.MOLD); + this.draw_dngn(dngn.MOLD + offset, x, y); + } else if (cell.glowing_mold) { + offset = cell.flv.s % dngn.tile_count(dngn.GLOWING_MOLD); + this.draw_dngn(dngn.GLOWING_MOLD + offset, x, y); + } + }, + + draw_ray: function (x, y, cell) { + const bg = cell.bg; + const _bg_idx = cell.bg.value; + + if (bg.RAY) this.draw_dngn(dngn.RAY, x, y); + else if (bg.RAY_OOR) this.draw_dngn(dngn.RAY_OUT_OF_RANGE, x, y); + else if (bg.LANDING) this.draw_dngn(dngn.LANDING, x, y); + else if (bg.RAY_MULTI) this.draw_dngn(dngn.RAY_MULTI, x, y); + }, + + draw_background: function (x, y, cell) { + const bg = cell.bg; + const bg_idx = cell.bg.value; + + if (cell.mangrove_water && bg_idx > dngn.DNGN_UNSEEN) + this.draw_dngn(dngn.DNGN_SHALLOW_WATER, x, y); + else if (bg_idx >= dngn.DNGN_FIRST_TRANSPARENT) { + this.draw_dngn(cell.flv.f, x, y); // f = floor + + // Draw floor overlays beneath the feature + if (cell.ov) { + $.each(cell.ov, (_i, overlay) => { + if (overlay && overlay <= dngn.FLOOR_MAX) + this.draw_dngn(overlay, x, y); + }); + } + } - $(document).off("game_init.cell_renderer") - .on("game_init.cell_renderer", function () { - determine_colours(); + // Draw blood beneath feature tiles. + if (bg_idx > dngn.WALL_MAX) this.draw_blood_overlay(x, y, cell); + + if (cell.mangrove_water) { + // Draw the tree submerged + this.ctx.save(); + try { + this.ctx.globalAlpha = 1.0; + + this.set_nonsubmerged_clip(x, y, 20); + + this.draw_dngn(bg_idx, x, y); + } finally { + this.ctx.restore(); + } + + this.ctx.save(); + try { + this.ctx.globalAlpha = 0.3; + this.set_submerged_clip(x, y, 20); + + this.draw_dngn(bg_idx, x, y); + } finally { + this.ctx.restore(); + } + } else this.draw_dngn(bg_idx, x, y); + + if (bg_idx > dngn.DNGN_UNSEEN) { + // Draw blood on top of wall tiles. + if (bg_idx <= dngn.WALL_MAX) + this.draw_blood_overlay(x, y, cell, bg_idx > dngn.FLOOR_MAX); + + // Draw overlays + let ray_tile = 0; + if (cell.ov) { + $.each(cell.ov, (_i, overlay) => { + if (overlay > dngn.DNGN_MAX) return; + else if ( + overlay === dngn.RAY || + overlay === dngn.RAY_MULTI || + overlay === dngn.RAY_OUT_OF_RANGE + ) { + // these need to be drawn last because of the + // way alpha blending happens here, but for + // hard-to-change reasons they are assembled on + // the server side in the wrong order. (In local + // tiles it's ok to blend them in any order.) + // assumption: only one can appear on any tile. + // TODO: a more general fix for this issue? + ray_tile = overlay; + } else if ( + overlay && + (bg_idx < dngn.DNGN_FIRST_TRANSPARENT || overlay > dngn.FLOOR_MAX) + ) { + this.draw_dngn(overlay, x, y); + } }); + } + if (ray_tile) this.draw_dngn(ray_tile, x, y); + + if (!bg.UNSEEN) { + if (bg.KRAKEN_NW) this.draw_dngn(dngn.KRAKEN_OVERLAY_NW, x, y); + else if (bg.ELDRITCH_NW) this.draw_dngn(dngn.ELDRITCH_OVERLAY_NW, x, y); + if (bg.KRAKEN_NE) this.draw_dngn(dngn.KRAKEN_OVERLAY_NE, x, y); + else if (bg.ELDRITCH_NE) this.draw_dngn(dngn.ELDRITCH_OVERLAY_NE, x, y); + if (bg.KRAKEN_SE) this.draw_dngn(dngn.KRAKEN_OVERLAY_SE, x, y); + else if (bg.ELDRITCH_SE) this.draw_dngn(dngn.ELDRITCH_OVERLAY_SE, x, y); + if (bg.KRAKEN_SW) this.draw_dngn(dngn.KRAKEN_OVERLAY_SW, x, y); + else if (bg.ELDRITCH_SW) this.draw_dngn(dngn.ELDRITCH_OVERLAY_SW, x, y); + } + + if (!bg.UNSEEN) { + if (cell.sanctuary) this.draw_dngn(dngn.SANCTUARY, x, y); + if (cell.blasphemy) this.draw_dngn(dngn.BLASPHEMY, x, y); + if (cell.has_bfb_corpse) this.draw_dngn(dngn.BLOOD_FOR_BLOOD, x, y); + if (cell.silenced) this.draw_dngn(dngn.SILENCED, x, y); + if (cell.halo === enums.HALO_RANGE) + this.draw_dngn(dngn.HALO_RANGE, x, y); + if ( + cell.halo >= enums.HALO_UMBRA_FIRST && + cell.halo <= enums.HALO_UMBRA_LAST + ) { + const variety = cell.halo - enums.HALO_UMBRA_FIRST; + this.draw_dngn(dngn.UMBRA + variety, x, y); + } + if (cell.orb_glow) + this.draw_dngn(dngn.ORB_GLOW + cell.orb_glow - 1, x, y); + if (cell.quad_glow) this.draw_dngn(dngn.QUAD_GLOW, x, y); + if (cell.disjunct) + this.draw_dngn(dngn.DISJUNCT + cell.disjunct - 1, x, y); + if (cell.awakened_forest) this.draw_icon(icons.BERSERK, x, y); + + if (cell.fg) { + const fg = cell.fg; + if (fg.PET) this.draw_dngn(dngn.HALO_FRIENDLY, x, y); + else if (fg.GD_NEUTRAL) this.draw_dngn(dngn.HALO_GD_NEUTRAL, x, y); + else if (fg.NEUTRAL) this.draw_dngn(dngn.HALO_NEUTRAL, x, y); + + // Monster difficulty. Ghosts get a special highlight. + if (fg.GHOST) { + if (fg.TRIVIAL) this.draw_dngn(dngn.THREAT_GHOST_TRIVIAL, x, y); + else if (fg.EASY) this.draw_dngn(dngn.THREAT_GHOST_EASY, x, y); + else if (fg.TOUGH) this.draw_dngn(dngn.THREAT_GHOST_TOUGH, x, y); + else if (fg.NASTY) this.draw_dngn(dngn.THREAT_GHOST_NASTY, x, y); + else if (fg.UNUSUAL) this.draw_dngn(dngn.THREAT_UNUSUAL, x, y); + } else { + if (fg.TRIVIAL) this.draw_dngn(dngn.THREAT_TRIVIAL, x, y); + else if (fg.EASY) this.draw_dngn(dngn.THREAT_EASY, x, y); + else if (fg.TOUGH) this.draw_dngn(dngn.THREAT_TOUGH, x, y); + else if (fg.NASTY) this.draw_dngn(dngn.THREAT_NASTY, x, y); + else if (fg.UNUSUAL) this.draw_dngn(dngn.THREAT_UNUSUAL, x, y); + } + + if (cell.highlighted_summoner) + this.draw_dngn(dngn.HALO_SUMMONER, x, y); + } - return { - DungeonCellRenderer: DungeonCellRenderer, - }; + // Apply the travel exclusion under the foreground if the cell is + // visible. It will be applied later if the cell is unseen. + if (bg.EXCL_CTR) this.draw_dngn(dngn.TRAVEL_EXCLUSION_CENTRE_BG, x, y); + else if (bg.TRAV_EXCL) this.draw_dngn(dngn.TRAVEL_EXCLUSION_BG, x, y); + } + } + + this.draw_ray(x, y, cell); + }, + + draw_submerged_tile: function (base_idx, idx, x, y, trans, img_scale) { + this.ctx.save(); + try { + this.ctx.globalAlpha = trans ? 0.5 : 1.0; + + this.set_nonsubmerged_clip(x, y, 20); + + if (base_idx) this.draw_main(base_idx, x, y, img_scale); + + this.draw_main(idx, x, y, img_scale); + } finally { + this.ctx.restore(); + } + + this.ctx.save(); + try { + this.ctx.globalAlpha = trans ? 0.1 : 0.3; + this.set_submerged_clip(x, y, 20); + + if (base_idx) this.draw_main(base_idx, x, y, img_scale); + + this.draw_main(idx, x, y, img_scale); + } finally { + this.ctx.restore(); + } + }, + + draw_foreground: function (x, y, map_cell, img_scale) { + const cell = map_cell.t; + const fg = cell.fg; + const bg = cell.bg; + const fg_idx = cell.fg.value; + const is_in_water = in_water(cell); + + if ( + fg_idx && + fg_idx <= main.MAIN_MAX && + options.get("tile_display_mode") === "tiles" + ) { + const base_idx = cell.base; + if (is_in_water) { + this.draw_submerged_tile(base_idx, fg_idx, x, y, cell.trans, img_scale); + } else { + if (base_idx) this.draw_main(base_idx, x, y, img_scale); + + this.draw_main(fg_idx, x, y, img_scale); + } + + if (fg_idx >= main.PARCHMENT_LOW && fg_idx <= main.PARCHMENT_HIGH) { + if (cell.overlay1) { + if (is_in_water) { + this.draw_submerged_tile( + null, + cell.overlay1, + x, + y, + cell.trans, + img_scale + ); + } else this.draw_main(cell.overlay1, x, y, img_scale); + } + if (cell.overlay2) { + if (is_in_water) { + this.draw_submerged_tile( + null, + cell.overlay2, + x, + y, + cell.trans, + img_scale + ); + } else this.draw_main(cell.overlay2, x, y, img_scale); + } + } + } else if (options.get("tile_display_mode") === "hybrid") { + this.render_glyph(x, y, map_cell, true, true); + this.draw_ray(x, y, cell); + img_scale = undefined; // TODO: make this work? + } + + if (fg.NET) + this.draw_icon(icons.TRAP_NET, x, y, undefined, undefined, img_scale); + + if (fg.WEB) + this.draw_icon(icons.TRAP_WEB, x, y, undefined, undefined, img_scale); + + if (fg.S_UNDER) + this.draw_icon( + icons.SOMETHING_UNDER, + x, + y, + undefined, + undefined, + img_scale + ); + + // Pet mark + if (fg.PET) + this.draw_icon(icons.FRIENDLY, x, y, undefined, undefined, img_scale); + else if (fg.GD_NEUTRAL) + this.draw_icon(icons.GOOD_NEUTRAL, x, y, undefined, undefined, img_scale); + else if (fg.NEUTRAL) + this.draw_icon(icons.NEUTRAL, x, y, undefined, undefined, img_scale); + + let status_shift = 0; + if (fg.PARALYSED) { + this.draw_icon(icons.PARALYSED, x, y, undefined, undefined, img_scale); + status_shift += 12; + } else if (fg.STAB) { + this.draw_icon(icons.STAB_BRAND, x, y, undefined, undefined, img_scale); + status_shift += 12; + } else if (fg.MAY_STAB) { + this.draw_icon(icons.UNAWARE, x, y, undefined, undefined, img_scale); + status_shift += 7; + } else if (fg.FLEEING) { + this.draw_icon(icons.FLEEING, x, y, undefined, undefined, img_scale); + status_shift += 3; + } + + if (fg.POISON) { + this.draw_icon(icons.POISON, x, y, -status_shift, 0, img_scale); + status_shift += 5; + } else if (fg.MORE_POISON) { + this.draw_icon(icons.MORE_POISON, x, y, -status_shift, 0, img_scale); + status_shift += 5; + } else if (fg.MAX_POISON) { + this.draw_icon(icons.MAX_POISON, x, y, -status_shift, 0, img_scale); + status_shift += 5; + } + + for (let i = 0; i < cell.icons.length; ++i) { + status_shift += this.draw_icon_type( + cell.icons[i], + x, + y, + -status_shift, + 0, + img_scale + ); + } + + if (bg.UNSEEN && (bg.value || fg.value)) + this.draw_icon(icons.MESH, x, y, undefined, undefined, img_scale); + + if (bg.OOR && (bg.value || fg.value)) + this.draw_icon(icons.OOR_MESH, x, y, undefined, undefined, img_scale); + + if (bg.MM_UNSEEN && (bg.value || fg.value)) + this.draw_icon( + icons.MAGIC_MAP_MESH, + x, + y, + undefined, + undefined, + img_scale + ); + + if (bg.RAMPAGE) + this.draw_icon(icons.RAMPAGE, x, y, undefined, undefined, img_scale); + + // Don't let the "new stair" icon cover up any existing icons, but + // draw it otherwise. + if (bg.NEW_STAIR && status_shift === 0) + this.draw_icon(icons.NEW_STAIR, x, y, undefined, undefined, img_scale); + + if (bg.NEW_TRANSPORTER && status_shift === 0) + this.draw_icon( + icons.NEW_TRANSPORTER, + x, + y, + undefined, + undefined, + img_scale + ); + + if (bg.EXCL_CTR && bg.UNSEEN) + this.draw_icon( + icons.TRAVEL_EXCLUSION_CENTRE_FG, + x, + y, + undefined, + undefined, + img_scale + ); + else if (bg.TRAV_EXCL && bg.UNSEEN) + this.draw_icon( + icons.TRAVEL_EXCLUSION_FG, + x, + y, + undefined, + undefined, + img_scale + ); + + // Tutorial cursor takes precedence over other cursors. + if (bg.TUT_CURSOR) { + this.draw_icon( + icons.TUTORIAL_CURSOR, + x, + y, + undefined, + undefined, + img_scale + ); + } else if (bg.CURSOR1) { + this.draw_icon(icons.CURSOR, x, y, undefined, undefined, img_scale); + } else if (bg.CURSOR2) { + this.draw_icon(icons.CURSOR2, x, y, undefined, undefined, img_scale); + } else if (bg.CURSOR3) { + this.draw_icon(icons.CURSOR3, x, y, undefined, undefined, img_scale); + } + + if (cell.travel_trail & 0xf) { + this.draw_icon( + icons.TRAVEL_PATH_FROM + (cell.travel_trail & 0xf) - 1, + x, + y, + undefined, + undefined, + img_scale + ); + } + if (cell.travel_trail & 0xf0) { + this.draw_icon( + icons.TRAVEL_PATH_TO + ((cell.travel_trail & 0xf0) >> 4) - 1, + x, + y, + undefined, + undefined, + img_scale + ); + } + + if (fg.MDAM_LIGHT) + this.draw_icon( + icons.MDAM_LIGHTLY_DAMAGED, + x, + y, + undefined, + undefined, + img_scale + ); + else if (fg.MDAM_MOD) + this.draw_icon( + icons.MDAM_MODERATELY_DAMAGED, + x, + y, + undefined, + undefined, + img_scale + ); + else if (fg.MDAM_HEAVY) + this.draw_icon( + icons.MDAM_HEAVILY_DAMAGED, + x, + y, + undefined, + undefined, + img_scale + ); + else if (fg.MDAM_SEV) + this.draw_icon( + icons.MDAM_SEVERELY_DAMAGED, + x, + y, + undefined, + undefined, + img_scale + ); + else if (fg.MDAM_ADEAD) + this.draw_icon( + icons.MDAM_ALMOST_DEAD, + x, + y, + undefined, + undefined, + img_scale + ); + + if (options.get("tile_show_demon_tier") === true) { + if (fg.DEMON_1) + this.draw_icon(icons.DEMON_NUM1, x, y, undefined, undefined, img_scale); + else if (fg.DEMON_2) + this.draw_icon(icons.DEMON_NUM2, x, y, undefined, undefined, img_scale); + else if (fg.DEMON_3) + this.draw_icon(icons.DEMON_NUM3, x, y, undefined, undefined, img_scale); + else if (fg.DEMON_4) + this.draw_icon(icons.DEMON_NUM4, x, y, undefined, undefined, img_scale); + else if (fg.DEMON_5) + this.draw_icon(icons.DEMON_NUM5, x, y, undefined, undefined, img_scale); + } + }, + + draw_icon_type: function (idx, x, y, ofsx, ofsy, img_scale) { + switch (idx) { + //These icons are in the lower right, so status_shift doesn't need changing. + case icons.BERSERK: + case icons.IDEALISED: + case icons.TOUCH_OF_BEOGH: + case icons.SHADOWLESS: + // Anim. weap. and summoned might overlap, but that's okay + case icons.SUMMONED: + case icons.MINION: + case icons.UNREWARDING: + case icons.ANIMATED_WEAPON: + case icons.VENGEANCE_TARGET: + case icons.VAMPIRE_THRALL: + case icons.ENKINDLED_1: + case icons.ENKINDLED_2: + case icons.NOBODY_MEMORY_1: + case icons.NOBODY_MEMORY_2: + case icons.NOBODY_MEMORY_3: + case icons.PYRRHIC: + case icons.FRENZIED: + this.draw_icon(idx, x, y, undefined, undefined, img_scale); + return 0; + case icons.DRAIN: + case icons.MIGHT: + case icons.SWIFT: + case icons.DAZED: + case icons.HASTED: + case icons.SLOWED: + case icons.CORRODED: + case icons.INFESTED: + case icons.WEAKENED: + case icons.PETRIFIED: + case icons.PETRIFYING: + case icons.BOUND_SOUL: + case icons.POSSESSABLE: + case icons.PARTIALLY_CHARGED: + case icons.FULLY_CHARGED: + case icons.VITRIFIED: + case icons.CONFUSED: + case icons.LACED_WITH_CHAOS: + case icons.SENTINEL_MARK: + case icons.DIMMED: + this.draw_icon(idx, x, y, ofsx, ofsy, img_scale); + return 6; + case icons.CONC_VENOM: + case icons.FIRE_CHAMP: + case icons.INNER_FLAME: + case icons.PAIN_MIRROR: + case icons.STICKY_FLAME: + this.draw_icon(idx, x, y, ofsx, ofsy, img_scale); + return 7; + case icons.ANGUISH: + case icons.FIRE_VULN: + case icons.RESISTANCE: + case icons.GHOSTLY: + case icons.MALMUTATED: + this.draw_icon(idx, x, y, ofsx, ofsy, img_scale); + return 8; + case icons.RECALL: + case icons.TELEPORTING: + this.draw_icon(idx, x, y, ofsx, ofsy, img_scale); + return 9; + case icons.BLIND: + case icons.BRILLIANCE: + case icons.SLOWLY_DYING: + case icons.WATERLOGGED: + case icons.STILL_WINDS: + case icons.ANTIMAGIC: + case icons.REPEL_MISSILES: + case icons.INJURY_BOND: + case icons.GLOW_LIGHT: + case icons.GLOW_HEAVY: + case icons.BULLSEYE: + case icons.CURSE_OF_AGONY: + case icons.REGENERATION: + case icons.RETREAT: + case icons.RIMEBLIGHT: + case icons.UNDYING_ARMS: + case icons.BIND: + case icons.SIGN_OF_RUIN: + case icons.WEAK_WILLED: + case icons.DOUBLED_VIGOUR: + case icons.KINETIC_GRAPNEL: + case icons.TEMPERED: + case icons.HEART: + case icons.UNSTABLE: + case icons.VEXED: + case icons.PARADOX: + case icons.WARDING: + case icons.FIGMENT: + this.draw_icon(idx, x, y, ofsx, ofsy, img_scale); + return 10; + case icons.CONSTRICTED: + case icons.VILE_CLUTCH: + case icons.PAIN_BOND: + this.draw_icon(idx, x, y, ofsx, ofsy, img_scale); + return 11; + } + }, + + // Helper functions for drawing from specific textures + draw_tile: function (idx, x, y, mod, ofsx, ofsy, y_max, centre, img_scale) { + // assumption: x and y are already appropriately scaled for the + // canvas. Now we just need to figure out where in the scaled + // cell size the tile belongs. + const info = mod.get_tile_info(idx); + const img = get_img(mod.get_img(idx)); + if (!info) { + throw new Error(`Tile not found: ${idx}`); + } + + // this somewhat convoluted approach is to avoid fp scaling + // artifacts at scale 1.0 + let img_xscale = this.x_scale; + let img_yscale = this.y_scale; + if (img_scale !== undefined) { + img_xscale = img_xscale * img_scale; + img_yscale = img_yscale * img_scale; + } else img_scale = 1.0; + + centre = centre === undefined ? true : centre; + const size_ox = !centre ? 0 : 32 / 2 - info.w / 2; + const size_oy = !centre ? 0 : 32 - info.h; + const pos_sy_adjust = (ofsy || 0) + info.oy + size_oy; + const pos_ey_adjust = pos_sy_adjust + info.ey - info.sy; + const sy = pos_sy_adjust; + let ey = pos_ey_adjust; + if (y_max && y_max < ey) ey = y_max; + + if (sy >= ey) return; + + const total_x_offset = (ofsx || 0) + info.ox + size_ox; + + // Offsets can be negative, in which case we are drawing overlapped + // with a cell either to the right or above. If so, store the + // overlap in cell state so that it can later be checked to trigger + // a redraw. + // These store logical pixels, but that currently doesn't matter + // because they are only used for a comparison vs 0 + // See dungeon_renderer.js, render_loc. + if (total_x_offset < this.current_left_overlap) + this.current_left_overlap = total_x_offset; + + if (sy < this.current_sy) this.current_sy = sy; + + // dimensions in the source (the tilesheet) + const w = info.ex - info.sx; + const h = ey - sy; + const ratio = window.devicePixelRatio; + // dimensions in the target cell. To get this right at the pixel + // level, we need to calculate the height/width as if it is offset + // relative to the cell origin. Because of differences in how x/y + // are treated above, these may look like they're doing something + // different, but they shouldn't be. + const scaled_w = + Math.floor((total_x_offset + w) * img_xscale * ratio) - + Math.floor(total_x_offset * img_xscale * ratio); + const scaled_h = + Math.floor(ey * img_yscale * ratio) - Math.floor(sy * img_yscale * ratio); + + this.ctx.imageSmoothingEnabled = options.get("tile_filter_scaling"); + this.ctx.drawImage( + img, + info.sx, + info.sy + sy - pos_sy_adjust, + w, + h, + x + Math.floor(total_x_offset * img_xscale * ratio), + y + Math.floor(sy * img_yscale * ratio), + scaled_w, + scaled_h + ); + }, + + draw_dngn: function (idx, x, y, img_scale) { + this.draw_tile( + idx, + x, + y, + dngn, + undefined, + undefined, + undefined, + undefined, + img_scale + ); + }, + + draw_gui: function (idx, x, y, img_scale) { + this.draw_tile( + idx, + x, + y, + gui, + undefined, + undefined, + undefined, + undefined, + img_scale + ); + }, + + draw_main: function (idx, x, y, img_scale) { + this.draw_tile( + idx, + x, + y, + main, + undefined, + undefined, + undefined, + undefined, + img_scale + ); + }, + + draw_player: function (idx, x, y, ofsx, ofsy, y_max, img_scale) { + this.draw_tile( + idx, + x, + y, + tileinfo_player, + ofsx, + ofsy, + y_max, + undefined, + img_scale + ); + }, + + draw_icon: function (idx, x, y, ofsx, ofsy, img_scale) { + this.draw_tile( + idx, + x, + y, + icons, + ofsx, + ofsy, + undefined, + undefined, + img_scale + ); + }, + + draw_quantity: function (qty, x, y, font) { + qty = Math.max(0, Math.min(999, qty)); + + this.ctx.fillStyle = "white"; + this.ctx.font = font; + + this.ctx.shadowColor = "black"; + this.ctx.shadowBlur = 2; + this.ctx.shadowOffsetX = 1; + this.ctx.shadowOffsetY = 1; + this.ctx.textAlign = "left"; + this.ctx.textBaseline = "top"; + this.ctx.fillText(qty, x + 2, y + 2); + }, + + draw_from_texture: function ( + idx, + x, + y, + tex, + ofsx, + ofsy, + y_max, + centre, + img_scale + ) { + const mod = tileinfos(tex); + this.draw_tile(idx, x, y, mod, ofsx, ofsy, y_max, centre, img_scale); + }, }); + +$(document) + .off("game_init.cell_renderer") + .on("game_init.cell_renderer", () => { + determine_colours(); + }); + +export default { + DungeonCellRenderer: DungeonCellRenderer, +}; diff --git a/crawl-ref/source/webserver/client/game/src/client.js b/crawl-ref/source/webserver/client/game/src/client.js new file mode 100644 index 00000000000..2a56a455c07 --- /dev/null +++ b/crawl-ref/source/webserver/client/game/src/client.js @@ -0,0 +1 @@ +export default window.DCSS.client; diff --git a/crawl-ref/source/webserver/client/game/src/comm.js b/crawl-ref/source/webserver/client/game/src/comm.js new file mode 100644 index 00000000000..9f7538e8b88 --- /dev/null +++ b/crawl-ref/source/webserver/client/game/src/comm.js @@ -0,0 +1 @@ +export default window.DCSS.comm; diff --git a/crawl-ref/source/webserver/client/game/src/contrib/simplebar.js b/crawl-ref/source/webserver/client/game/src/contrib/simplebar.js new file mode 100644 index 00000000000..1687d1723bf --- /dev/null +++ b/crawl-ref/source/webserver/client/game/src/contrib/simplebar.js @@ -0,0 +1,3353 @@ +/** + * SimpleBar.js - v3.0.0-beta.4 + * Scrollbars, simpler. + * https://grsmto.github.io/simplebar/ + * + * Includes the following embedded node package dependencies: + * - can-use-dom + * - lodash-throttle + * - resize-observer-polyfill + * - scrollbarwidth + * + * Made by Adrien Denat from a fork by Jonathan Nicol + * Under MIT License + */ + +const _isObject = (it) => + typeof it === "object" ? it !== null : typeof it === "function"; + +const _anObject = (it) => { + if (!_isObject(it)) throw TypeError(`${it} is not an object!`); + return it; +}; + +const _fails = (exec) => { + try { + return !!exec(); + } catch (_e) { + return true; + } +}; + +// Thank's IE8 for his funny defineProperty +const _descriptors = !_fails( + () => + Object.defineProperty({}, "a", { + get: () => 7, + }).a !== 7 +); + +const commonjsGlobal = + typeof window !== "undefined" + ? window + : typeof global !== "undefined" + ? global + : typeof self !== "undefined" + ? self + : {}; + +function createCommonjsModule(fn, module) { + return (module = { exports: {} }), fn(module, module.exports), module.exports; +} + +const _global = createCommonjsModule((module) => { + // https://github.com/zloirock/core-js/issues/86#issuecomment-115759028 + const global = (module.exports = + typeof window !== "undefined" && window.Math === Math + ? window + : typeof self !== "undefined" && self.Math === Math + ? self + : // eslint-disable-next-line no-new-func + Function("return this")()); + if (typeof __g === "number") __g = global; // eslint-disable-line no-undef +}); + +const document$1 = _global.document; +// typeof document.createElement is 'object' in old IE +const is = _isObject(document$1) && _isObject(document$1.createElement); +const _domCreate = (it) => (is ? document$1.createElement(it) : {}); + +const _ie8DomDefine = + !_descriptors && + !_fails( + () => + Object.defineProperty(_domCreate("div"), "a", { + get: () => 7, + }).a !== 7 + ); + +// 7.1.1 ToPrimitive(input [, PreferredType]) + +// instead of the ES6 spec version, we didn't implement @@toPrimitive case +// and the second argument - flag - preferred type is a string +const _toPrimitive = (it, S) => { + if (!_isObject(it)) return it; + let fn, val; + if ( + S && + typeof (fn = it.toString) === "function" && + !_isObject((val = fn.call(it))) + ) + return val; + if ( + typeof (fn = it.valueOf) === "function" && + !_isObject((val = fn.call(it))) + ) + return val; + if ( + !S && + typeof (fn = it.toString) === "function" && + !_isObject((val = fn.call(it))) + ) + return val; + throw TypeError("Can't convert object to primitive value"); +}; + +const dP = Object.defineProperty; + +const f = _descriptors + ? Object.defineProperty + : function defineProperty(O, P, Attributes) { + _anObject(O); + P = _toPrimitive(P, true); + _anObject(Attributes); + if (_ie8DomDefine) + try { + return dP(O, P, Attributes); + } catch (_e) { + /* empty */ + } + if ("get" in Attributes || "set" in Attributes) + throw TypeError("Accessors not supported!"); + if ("value" in Attributes) O[P] = Attributes.value; + return O; + }; + +const _objectDp = { + f: f, +}; + +const _propertyDesc = (bitmap, value) => ({ + enumerable: !(bitmap & 1), + configurable: !(bitmap & 2), + writable: !(bitmap & 4), + value: value, +}); + +const _hide = _descriptors + ? (object, key, value) => _objectDp.f(object, key, _propertyDesc(1, value)) + : (object, key, value) => { + object[key] = value; + return object; + }; + +const hasOwnProperty = {}.hasOwnProperty; +const _has = (it, key) => hasOwnProperty.call(it, key); + +let id = 0; +const px = Math.random(); +const _uid = (key) => + "Symbol(".concat( + key === undefined ? "" : key, + ")_", + (++id + px).toString(36) + ); + +const _core = createCommonjsModule((module) => { + const core = (module.exports = { version: "2.5.7" }); + if (typeof __e === "number") __e = core; // eslint-disable-line no-undef +}); +const _core_1 = _core.version; + +const _redefine = createCommonjsModule((module) => { + const SRC = _uid("src"); + const TO_STRING = "toString"; + const $toString = Function[TO_STRING]; + const TPL = `${$toString}`.split(TO_STRING); + + _core.inspectSource = (it) => $toString.call(it); + + (module.exports = (O, key, val, safe) => { + const isFunction = typeof val === "function"; + if (isFunction) _has(val, "name") || _hide(val, "name", key); + if (O[key] === val) return; + if (isFunction) + _has(val, SRC) || + _hide(val, SRC, O[key] ? `${O[key]}` : TPL.join(String(key))); + if (O === _global) { + O[key] = val; + } else if (!safe) { + delete O[key]; + _hide(O, key, val); + } else if (O[key]) { + O[key] = val; + } else { + _hide(O, key, val); + } + // add fake Function#toString for correct work wrapped methods / constructors with methods like LoDash isNative + })(Function.prototype, TO_STRING, function toString() { + return (typeof this === "function" && this[SRC]) || $toString.call(this); + }); +}); + +// 7.2.1 RequireObjectCoercible(argument) +const _defined = (it) => { + if (it === undefined) throw TypeError(`Can't call method on ${it}`); + return it; +}; + +const _library = false; + +const _shared = createCommonjsModule((module) => { + const SHARED = "__core-js_shared__"; + const store = _global[SHARED] || (_global[SHARED] = {}); + + (module.exports = (key, value) => + store[key] || (store[key] = value !== undefined ? value : {}))( + "versions", + [] + ).push({ + version: _core.version, + mode: _library ? "pure" : "global", + copyright: "© 2018 Denis Pushkarev (zloirock.ru)", + }); +}); + +const _wks = createCommonjsModule((module) => { + const store = _shared("wks"); + + const Symbol = _global.Symbol; + const USE_SYMBOL = typeof Symbol === "function"; + + const $exports = (module.exports = (name) => + store[name] || + (store[name] = + (USE_SYMBOL && Symbol[name]) || + (USE_SYMBOL ? Symbol : _uid)(`Symbol.${name}`))); + + $exports.store = store; +}); + +const _fixReWks = (KEY, length, exec) => { + const SYMBOL = _wks(KEY); + const fns = exec(_defined, SYMBOL, ""[KEY]); + const strfn = fns[0]; + const rxfn = fns[1]; + if ( + _fails(() => { + const O = {}; + O[SYMBOL] = () => 7; + return ""[KEY](O) !== 7; + }) + ) { + _redefine(String.prototype, KEY, strfn); + _hide( + RegExp.prototype, + SYMBOL, + length === 2 + ? // 21.2.5.8 RegExp.prototype[@@replace](string, replaceValue) + // 21.2.5.11 RegExp.prototype[@@split](string, limit) + function (string, arg) { + return rxfn.call(string, this, arg); + } + : // 21.2.5.6 RegExp.prototype[@@match](string) + // 21.2.5.9 RegExp.prototype[@@search](string) + function (string) { + return rxfn.call(string, this); + } + ); + } +}; + +// @@replace logic +_fixReWks("replace", 2, (defined, REPLACE, $replace) => { + // 21.1.3.14 String.prototype.replace(searchValue, replaceValue) + return [ + function replace(searchValue, replaceValue) { + const O = defined(this); + const fn = searchValue === undefined ? undefined : searchValue[REPLACE]; + return fn !== undefined + ? fn.call(searchValue, O, replaceValue) + : $replace.call(String(O), searchValue, replaceValue); + }, + $replace, + ]; +}); + +const dP$1 = _objectDp.f; +const FProto = Function.prototype; +const nameRE = /^\s*function ([^ (]*)/; +const NAME = "name"; + +// 19.2.4.2 name +NAME in FProto || + (_descriptors && + dP$1(FProto, NAME, { + configurable: true, + get: function () { + try { + return `${this}`.match(nameRE)[1]; + } catch (_e) { + return ""; + } + }, + })); + +// @@match logic +_fixReWks("match", 1, (defined, MATCH, $match) => { + // 21.1.3.11 String.prototype.match(regexp) + return [ + function match(regexp) { + const O = defined(this); + const fn = regexp === undefined ? undefined : regexp[MATCH]; + return fn !== undefined + ? fn.call(regexp, O) + : new RegExp(regexp)[MATCH](String(O)); + }, + $match, + ]; +}); + +// 22.1.3.31 Array.prototype[@@unscopables] +const UNSCOPABLES = _wks("unscopables"); +const ArrayProto = Array.prototype; +if (ArrayProto[UNSCOPABLES] === undefined) _hide(ArrayProto, UNSCOPABLES, {}); +const _addToUnscopables = (key) => { + ArrayProto[UNSCOPABLES][key] = true; +}; + +const _iterStep = (done, value) => ({ value: value, done: !!done }); + +const _iterators = {}; + +const toString = {}.toString; + +const _cof = (it) => toString.call(it).slice(8, -1); + +// fallback for non-array-like ES3 and non-enumerable old V8 strings + +// eslint-disable-next-line no-prototype-builtins +const _iobject = Object("z").propertyIsEnumerable(0) + ? Object + : (it) => (_cof(it) === "String" ? it.split("") : Object(it)); + +// to indexed object, toObject with fallback for non-array-like ES3 strings + +const _toIobject = (it) => _iobject(_defined(it)); + +const _aFunction = (it) => { + if (typeof it !== "function") throw TypeError(`${it} is not a function!`); + return it; +}; + +// optional / simple context binding + +const _ctx = (fn, that, length) => { + _aFunction(fn); + if (that === undefined) return fn; + switch (length) { + case 1: + return (a) => fn.call(that, a); + case 2: + return (a, b) => fn.call(that, a, b); + case 3: + return (a, b, c) => fn.call(that, a, b, c); + } + return (/* ...args */) => fn.apply(that, arguments); +}; + +const PROTOTYPE = "prototype"; + +const $export = (type, name, source) => { + const IS_FORCED = type & $export.F; + const IS_GLOBAL = type & $export.G; + const IS_STATIC = type & $export.S; + const IS_PROTO = type & $export.P; + const IS_BIND = type & $export.B; + const target = IS_GLOBAL + ? _global + : IS_STATIC + ? _global[name] || (_global[name] = {}) + : _global[name]?.[PROTOTYPE]; + const exports = IS_GLOBAL ? _core : _core[name] || (_core[name] = {}); + const expProto = exports[PROTOTYPE] || (exports[PROTOTYPE] = {}); + let key, own, out, exp; + if (IS_GLOBAL) source = name; + for (key in source) { + // contains in native + own = !IS_FORCED && target && target[key] !== undefined; + // export native or passed + out = (own ? target : source)[key]; + // bind timers to global for call from export context + exp = + IS_BIND && own + ? _ctx(out, _global) + : IS_PROTO && typeof out === "function" + ? _ctx(Function.call, out) + : out; + // extend global + if (target) _redefine(target, key, out, type & $export.U); + // export + if (exports[key] !== out) _hide(exports, key, exp); + if (IS_PROTO && expProto[key] !== out) expProto[key] = out; + } +}; +_global.core = _core; +// type bitmap +$export.F = 1; // forced +$export.G = 2; // global +$export.S = 4; // static +$export.P = 8; // proto +$export.B = 16; // bind +$export.W = 32; // wrap +$export.U = 64; // safe +$export.R = 128; // real proto method for `library` +const _export = $export; + +// 7.1.4 ToInteger +const ceil = Math.ceil; +const floor = Math.floor; +const _toInteger = (it) => + Number.isNaN((it = +it)) ? 0 : (it > 0 ? floor : ceil)(it); + +// 7.1.15 ToLength + +const min = Math.min; +const _toLength = (it) => { + return it > 0 ? min(_toInteger(it), 0x1fffffffffffff) : 0; // pow(2, 53) - 1 == 9007199254740991 +}; + +const max = Math.max; +const min$1 = Math.min; +const _toAbsoluteIndex = (index, length) => { + index = _toInteger(index); + return index < 0 ? max(index + length, 0) : min$1(index, length); +}; + +// false -> Array#indexOf +// true -> Array#includes + +const _arrayIncludes = (IS_INCLUDES) => ($this, el, fromIndex) => { + const O = _toIobject($this); + const length = _toLength(O.length); + let index = _toAbsoluteIndex(fromIndex, length); + let value; + // Array#includes uses SameValueZero equality algorithm + // eslint-disable-next-line no-self-compare + if (IS_INCLUDES && el !== el) + while (length > index) { + value = O[index++]; + // eslint-disable-next-line no-self-compare + if (value !== value) return true; + // Array#indexOf ignores holes, Array#includes - not + } + else + for (; length > index; index++) + if (IS_INCLUDES || index in O) { + if (O[index] === el) return IS_INCLUDES || index || 0; + } + return !IS_INCLUDES && -1; +}; + +const shared = _shared("keys"); + +const _sharedKey = (key) => shared[key] || (shared[key] = _uid(key)); + +const arrayIndexOf = _arrayIncludes(false); +const IE_PROTO = _sharedKey("IE_PROTO"); + +const _objectKeysInternal = (object, names) => { + const O = _toIobject(object); + let i = 0; + const result = []; + let key; + for (key in O) if (key !== IE_PROTO) _has(O, key) && result.push(key); + // Don't enum bug & hidden keys + while (names.length > i) + if (_has(O, (key = names[i++]))) { + ~arrayIndexOf(result, key) || result.push(key); + } + return result; +}; + +// IE 8- don't enum bug keys +const _enumBugKeys = + "constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split( + "," + ); + +// 19.1.2.14 / 15.2.3.14 Object.keys(O) + +const _objectKeys = + Object.keys || + function keys(O) { + return _objectKeysInternal(O, _enumBugKeys); + }; + +const _objectDps = _descriptors + ? Object.defineProperties + : function defineProperties(O, Properties) { + _anObject(O); + const keys = _objectKeys(Properties); + const length = keys.length; + let i = 0; + let P; + while (length > i) _objectDp.f(O, (P = keys[i++]), Properties[P]); + return O; + }; + +const document$2 = _global.document; +const _html = document$2?.documentElement; + +// 19.1.2.2 / 15.2.3.5 Object.create(O [, Properties]) + +const IE_PROTO$1 = _sharedKey("IE_PROTO"); +const Empty = () => { + /* empty */ +}; +const PROTOTYPE$1 = "prototype"; + +// Create object with fake `null` prototype: use iframe Object with cleared prototype +let createDict = () => { + // Thrash, waste and sodomy: IE GC bug + const iframe = _domCreate("iframe"); + let i = _enumBugKeys.length; + const lt = "<"; + const gt = ">"; + let iframeDocument; + iframe.style.display = "none"; + _html.appendChild(iframe); + iframe.src = "javascript:"; // eslint-disable-line no-script-url + // createDict = iframe.contentWindow.Object; + // html.removeChild(iframe); + iframeDocument = iframe.contentWindow.document; + iframeDocument.open(); + iframeDocument.write(`${lt}script${gt}document.F=Object${lt}/script${gt}`); + iframeDocument.close(); + createDict = iframeDocument.F; + while (i--) delete createDict[PROTOTYPE$1][_enumBugKeys[i]]; + return createDict(); +}; + +const _objectCreate = + Object.create || + function create(O, Properties) { + let result; + if (O !== null) { + Empty[PROTOTYPE$1] = _anObject(O); + result = new Empty(); + Empty[PROTOTYPE$1] = null; + // add "__proto__" for Object.getPrototypeOf polyfill + result[IE_PROTO$1] = O; + } else result = createDict(); + return Properties === undefined ? result : _objectDps(result, Properties); + }; + +const def = _objectDp.f; + +const TAG = _wks("toStringTag"); + +const _setToStringTag = (it, tag, stat) => { + if (it && !_has((it = stat ? it : it.prototype), TAG)) + def(it, TAG, { configurable: true, value: tag }); +}; + +const IteratorPrototype = {}; + +// 25.1.2.1.1 %IteratorPrototype%[@@iterator]() +_hide(IteratorPrototype, _wks("iterator"), function () { + return this; +}); + +const _iterCreate = (Constructor, NAME, next) => { + Constructor.prototype = _objectCreate(IteratorPrototype, { + next: _propertyDesc(1, next), + }); + _setToStringTag(Constructor, `${NAME} Iterator`); +}; + +// 7.1.13 ToObject(argument) + +const _toObject = (it) => Object(_defined(it)); + +// 19.1.2.9 / 15.2.3.2 Object.getPrototypeOf(O) + +const IE_PROTO$2 = _sharedKey("IE_PROTO"); +const ObjectProto = Object.prototype; + +const _objectGpo = + Object.getPrototypeOf || + ((O) => { + O = _toObject(O); + if (_has(O, IE_PROTO$2)) return O[IE_PROTO$2]; + if (typeof O.constructor === "function" && O instanceof O.constructor) { + return O.constructor.prototype; + } + return O instanceof Object ? ObjectProto : null; + }); + +const ITERATOR = _wks("iterator"); +const BUGGY = !([].keys && "next" in [].keys()); // Safari has buggy iterators w/o `next` +const FF_ITERATOR = "@@iterator"; +const KEYS = "keys"; +const VALUES = "values"; + +const returnThis = function () { + return this; +}; + +const _iterDefine = ( + Base, + NAME, + Constructor, + next, + DEFAULT, + IS_SET, + FORCED +) => { + _iterCreate(Constructor, NAME, next); + const getMethod = (kind) => { + if (!BUGGY && kind in proto) return proto[kind]; + switch (kind) { + case KEYS: + return function keys() { + return new Constructor(this, kind); + }; + case VALUES: + return function values() { + return new Constructor(this, kind); + }; + } + return function entries() { + return new Constructor(this, kind); + }; + }; + const TAG = `${NAME} Iterator`; + const DEF_VALUES = DEFAULT === VALUES; + let VALUES_BUG = false; + const proto = Base.prototype; + const $native = + proto[ITERATOR] || proto[FF_ITERATOR] || (DEFAULT && proto[DEFAULT]); + let $default = $native || getMethod(DEFAULT); + const $entries = DEFAULT + ? !DEF_VALUES + ? $default + : getMethod("entries") + : undefined; + const $anyNative = NAME === "Array" ? proto.entries || $native : $native; + let methods, key, IteratorPrototype; + // Fix native + if ($anyNative) { + IteratorPrototype = _objectGpo($anyNative.call(new Base())); + if (IteratorPrototype !== Object.prototype && IteratorPrototype.next) { + // Set @@toStringTag to native iterators + _setToStringTag(IteratorPrototype, TAG, true); + // fix for some old engines + if (!_library && typeof IteratorPrototype[ITERATOR] !== "function") + _hide(IteratorPrototype, ITERATOR, returnThis); + } + } + // fix Array#{values, @@iterator}.name in V8 / FF + if (DEF_VALUES && $native && $native.name !== VALUES) { + VALUES_BUG = true; + $default = function values() { + return $native.call(this); + }; + } + // Define iterator + if ((!_library || FORCED) && (BUGGY || VALUES_BUG || !proto[ITERATOR])) { + _hide(proto, ITERATOR, $default); + } + // Plug for library + _iterators[NAME] = $default; + _iterators[TAG] = returnThis; + if (DEFAULT) { + methods = { + values: DEF_VALUES ? $default : getMethod(VALUES), + keys: IS_SET ? $default : getMethod(KEYS), + entries: $entries, + }; + if (FORCED) + for (key in methods) { + if (!(key in proto)) _redefine(proto, key, methods[key]); + } + else _export(_export.P + _export.F * (BUGGY || VALUES_BUG), NAME, methods); + } + return methods; +}; + +// 22.1.3.4 Array.prototype.entries() +// 22.1.3.13 Array.prototype.keys() +// 22.1.3.29 Array.prototype.values() +// 22.1.3.30 Array.prototype[@@iterator]() +const es6_array_iterator = _iterDefine( + Array, + "Array", + function (iterated, kind) { + this._t = _toIobject(iterated); // target + this._i = 0; // next index + this._k = kind; // kind + // 22.1.5.2.1 %ArrayIteratorPrototype%.next() + }, + function () { + const O = this._t; + const kind = this._k; + const index = this._i++; + if (!O || index >= O.length) { + this._t = undefined; + return _iterStep(1); + } + if (kind === "keys") return _iterStep(0, index); + if (kind === "values") return _iterStep(0, O[index]); + return _iterStep(0, [index, O[index]]); + }, + "values" +); + +// argumentsList[@@iterator] is %ArrayProto_values% (9.4.4.6, 9.4.4.7) +_iterators.Arguments = _iterators.Array; + +_addToUnscopables("keys"); +_addToUnscopables("values"); +_addToUnscopables("entries"); + +const ITERATOR$1 = _wks("iterator"); +const TO_STRING_TAG = _wks("toStringTag"); +const ArrayValues = _iterators.Array; + +const DOMIterables = { + CSSRuleList: true, // TODO: Not spec compliant, should be false. + CSSStyleDeclaration: false, + CSSValueList: false, + ClientRectList: false, + DOMRectList: false, + DOMStringList: false, + DOMTokenList: true, + DataTransferItemList: false, + FileList: false, + HTMLAllCollection: false, + HTMLCollection: false, + HTMLFormElement: false, + HTMLSelectElement: false, + MediaList: true, // TODO: Not spec compliant, should be false. + MimeTypeArray: false, + NamedNodeMap: false, + NodeList: true, + PaintRequestList: false, + Plugin: false, + PluginArray: false, + SVGLengthList: false, + SVGNumberList: false, + SVGPathSegList: false, + SVGPointList: false, + SVGStringList: false, + SVGTransformList: false, + SourceBufferList: false, + StyleSheetList: true, // TODO: Not spec compliant, should be false. + TextTrackCueList: false, + TextTrackList: false, + TouchList: false, +}; + +for ( + let collections = _objectKeys(DOMIterables), i = 0; + i < collections.length; + i++ +) { + const NAME$1 = collections[i]; + const explicit = DOMIterables[NAME$1]; + const Collection = _global[NAME$1]; + const proto = Collection?.prototype; + let key; + if (proto) { + if (!proto[ITERATOR$1]) _hide(proto, ITERATOR$1, ArrayValues); + if (!proto[TO_STRING_TAG]) _hide(proto, TO_STRING_TAG, NAME$1); + _iterators[NAME$1] = ArrayValues; + if (explicit) + for (key in es6_array_iterator) + if (!proto[key]) _redefine(proto, key, es6_array_iterator[key], true); + } +} + +// true -> String#at +// false -> String#codePointAt +const _stringAt = (TO_STRING) => (that, pos) => { + const s = String(_defined(that)); + const i = _toInteger(pos); + const l = s.length; + let a, b; + if (i < 0 || i >= l) return TO_STRING ? "" : undefined; + a = s.charCodeAt(i); + return a < 0xd800 || + a > 0xdbff || + i + 1 === l || + (b = s.charCodeAt(i + 1)) < 0xdc00 || + b > 0xdfff + ? TO_STRING + ? s.charAt(i) + : a + : TO_STRING + ? s.slice(i, i + 2) + : ((a - 0xd800) << 10) + (b - 0xdc00) + 0x10000; +}; + +const $at = _stringAt(true); + +// 21.1.3.27 String.prototype[@@iterator]() +_iterDefine( + String, + "String", + function (iterated) { + this._t = String(iterated); // target + this._i = 0; // next index + // 21.1.5.2.1 %StringIteratorPrototype%.next() + }, + function () { + const O = this._t; + const index = this._i; + let point; + if (index >= O.length) return { value: undefined, done: true }; + point = $at(O, index); + this._i += point.length; + return { value: point, done: false }; + } +); + +// call something on iterator step with safe closing on error + +const _iterCall = (iterator, fn, value, entries) => { + try { + return entries ? fn(_anObject(value)[0], value[1]) : fn(value); + // 7.4.6 IteratorClose(iterator, completion) + } catch (e) { + const ret = iterator.return; + if (ret !== undefined) _anObject(ret.call(iterator)); + throw e; + } +}; + +// check on default Array iterator + +const ITERATOR$2 = _wks("iterator"); +const ArrayProto$1 = Array.prototype; + +const _isArrayIter = (it) => + it !== undefined && + (_iterators.Array === it || ArrayProto$1[ITERATOR$2] === it); + +const _createProperty = (object, index, value) => { + if (index in object) _objectDp.f(object, index, _propertyDesc(0, value)); + else object[index] = value; +}; + +// getting tag from 19.1.3.6 Object.prototype.toString() + +const TAG$1 = _wks("toStringTag"); +// ES3 wrong here +const ARG = _cof((() => arguments)()) === "Arguments"; + +// fallback for IE11 Script Access Denied error +const tryGet = (it, key) => { + try { + return it[key]; + } catch (_e) { + /* empty */ + } +}; + +const _classof = (it) => { + let O, T, B; + return it === undefined + ? "Undefined" + : it === null + ? "Null" + : // @@toStringTag case + typeof (T = tryGet((O = Object(it)), TAG$1)) === "string" + ? T + : // builtinTag case + ARG + ? _cof(O) + : // ES3 arguments fallback + (B = _cof(O)) === "Object" && typeof O.callee === "function" + ? "Arguments" + : B; +}; + +const ITERATOR$3 = _wks("iterator"); + +const core_getIteratorMethod = (_core.getIteratorMethod = (it) => { + if (it !== undefined) + return it[ITERATOR$3] || it["@@iterator"] || _iterators[_classof(it)]; +}); + +const ITERATOR$4 = _wks("iterator"); +let SAFE_CLOSING = false; + +try { + const riter = [7][ITERATOR$4](); + riter.return = () => { + SAFE_CLOSING = true; + }; +} catch (_e) { + /* empty */ +} + +const _iterDetect = (exec, skipClosing) => { + if (!skipClosing && !SAFE_CLOSING) return false; + let safe = false; + try { + const arr = [7]; + const iter = arr[ITERATOR$4](); + iter.next = () => ({ done: (safe = true) }); + arr[ITERATOR$4] = () => iter; + exec(arr); + } catch (_e) { + /* empty */ + } + return safe; +}; + +_export(_export.S + _export.F * !_iterDetect((_iter) => {}), "Array", { + // 22.1.2.1 Array.from(arrayLike, mapfn = undefined, thisArg = undefined) + from: function from( + arrayLike /* , mapfn = undefined, thisArg = undefined */ + ) { + const O = _toObject(arrayLike); + const C = typeof this === "function" ? this : Array; + const aLen = arguments.length; + let mapfn = aLen > 1 ? arguments[1] : undefined; + const mapping = mapfn !== undefined; + let index = 0; + const iterFn = core_getIteratorMethod(O); + let length, result, step, iterator; + if (mapping) mapfn = _ctx(mapfn, aLen > 2 ? arguments[2] : undefined, 2); + // if object isn't iterable or it's array with default iterator - use simple case + if (iterFn !== undefined && !(C === Array && _isArrayIter(iterFn))) { + for ( + iterator = iterFn.call(O), result = new C(); + !(step = iterator.next()).done; + index++ + ) { + _createProperty( + result, + index, + mapping + ? _iterCall(iterator, mapfn, [step.value, index], true) + : step.value + ); + } + } else { + length = _toLength(O.length); + for (result = new C(length); length > index; index++) { + _createProperty( + result, + index, + mapping ? mapfn(O[index], index) : O[index] + ); + } + } + result.length = index; + return result; + }, +}); + +const f$1 = Object.getOwnPropertySymbols; + +const _objectGops = { + f: f$1, +}; + +const f$2 = {}.propertyIsEnumerable; + +const _objectPie = { + f: f$2, +}; + +// 19.1.2.1 Object.assign(target, source, ...) + +const $assign = Object.assign; + +// should work with symbols and should have deterministic property order (V8 bug) +const _objectAssign = + !$assign || + _fails(() => { + const A = {}; + const B = {}; + // eslint-disable-next-line no-undef + const S = Symbol(); + const K = "abcdefghijklmnopqrst"; + A[S] = 7; + K.split("").forEach((k) => { + B[k] = k; + }); + return ( + $assign({}, A)[S] !== 7 || Object.keys($assign({}, B)).join("") !== K + ); + }) + ? function assign(target, _source) { + // eslint-disable-line no-unused-vars + const T = _toObject(target); + const aLen = arguments.length; + let index = 1; + const getSymbols = _objectGops.f; + const isEnum = _objectPie.f; + while (aLen > index) { + const S = _iobject(arguments[index++]); + const keys = getSymbols + ? _objectKeys(S).concat(getSymbols(S)) + : _objectKeys(S); + const length = keys.length; + let j = 0; + let key; + while (length > j) + if (isEnum.call(S, (key = keys[j++]))) T[key] = S[key]; + } + return T; + } + : $assign; + +// 19.1.3.1 Object.assign(target, source) + +_export(_export.S + _export.F, "Object", { assign: _objectAssign }); + +function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } +} + +function _defineProperties(target, props) { + for (let i = 0; i < props.length; i++) { + const descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } +} + +function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + return Constructor; +} + +const scrollbarWidth = createCommonjsModule((module, _exports) => { + /*! scrollbarWidth.js v0.1.3 | felixexter | MIT | https://github.com/felixexter/scrollbarWidth */ + ((_root, factory) => { + module.exports = factory(); + })(commonjsGlobal, () => { + function scrollbarWidth() { + if (typeof document === "undefined") { + return 0; + } + + let body = document.body, + box = document.createElement("div"), + boxStyle = box.style, + width; + + boxStyle.position = "absolute"; + boxStyle.top = boxStyle.left = "-9999px"; + boxStyle.width = boxStyle.height = "100px"; + boxStyle.overflow = "scroll"; + + body.appendChild(box); + + width = box.offsetWidth - box.clientWidth; + + body.removeChild(box); + + return width; + } + + return scrollbarWidth; + }); +}); + +/** + * lodash (Custom Build) + * Build: `lodash modularize exports="npm" -o ./` + * Copyright jQuery Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + */ + +/** Used as the `TypeError` message for "Functions" methods. */ +const FUNC_ERROR_TEXT = "Expected a function"; + +/** Used as references for various `Number` constants. */ +const NAN = 0 / 0; + +/** `Object#toString` result references. */ +const symbolTag = "[object Symbol]"; + +/** Used to match leading and trailing whitespace. */ +const reTrim = /^\s+|\s+$/g; + +/** Used to detect bad signed hexadecimal string values. */ +const reIsBadHex = /^[-+]0x[0-9a-f]+$/i; + +/** Used to detect binary string values. */ +const reIsBinary = /^0b[01]+$/i; + +/** Used to detect octal string values. */ +const reIsOctal = /^0o[0-7]+$/i; + +/** Built-in method references without a dependency on `root`. */ +const freeParseInt = parseInt; + +/** Detect free variable `global` from Node.js. */ +const freeGlobal = + typeof commonjsGlobal === "object" && + commonjsGlobal && + commonjsGlobal.Object === Object && + commonjsGlobal; + +/** Detect free variable `self`. */ +const freeSelf = + typeof self === "object" && self && self.Object === Object && self; + +/** Used as a reference to the global object. */ +const root = freeGlobal || freeSelf || Function("return this")(); + +/** Used for built-in method references. */ +const objectProto = Object.prototype; + +/** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ +const objectToString = objectProto.toString; + +/* Built-in method references for those with the same name as other `lodash` methods. */ +const nativeMax = Math.max, + nativeMin = Math.min; + +/** + * Gets the timestamp of the number of milliseconds that have elapsed since + * the Unix epoch (1 January 1970 00:00:00 UTC). + * + * @static + * @memberOf _ + * @since 2.4.0 + * @category Date + * @returns {number} Returns the timestamp. + * @example + * + * _.defer(function(stamp) { + * console.log(_.now() - stamp); + * }, _.now()); + * // => Logs the number of milliseconds it took for the deferred invocation. + */ +const now = () => root.Date.now(); + +/** + * Creates a debounced function that delays invoking `func` until after `wait` + * milliseconds have elapsed since the last time the debounced function was + * invoked. The debounced function comes with a `cancel` method to cancel + * delayed `func` invocations and a `flush` method to immediately invoke them. + * Provide `options` to indicate whether `func` should be invoked on the + * leading and/or trailing edge of the `wait` timeout. The `func` is invoked + * with the last arguments provided to the debounced function. Subsequent + * calls to the debounced function return the result of the last `func` + * invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is + * invoked on the trailing edge of the timeout only if the debounced function + * is invoked more than once during the `wait` timeout. + * + * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred + * until to the next tick, similar to `setTimeout` with a timeout of `0`. + * + * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) + * for details over the differences between `_.debounce` and `_.throttle`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to debounce. + * @param {number} [wait=0] The number of milliseconds to delay. + * @param {Object} [options={}] The options object. + * @param {boolean} [options.leading=false] + * Specify invoking on the leading edge of the timeout. + * @param {number} [options.maxWait] + * The maximum time `func` is allowed to be delayed before it's invoked. + * @param {boolean} [options.trailing=true] + * Specify invoking on the trailing edge of the timeout. + * @returns {Function} Returns the new debounced function. + * @example + * + * // Avoid costly calculations while the window size is in flux. + * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); + * + * // Invoke `sendMail` when clicked, debouncing subsequent calls. + * jQuery(element).on('click', _.debounce(sendMail, 300, { + * 'leading': true, + * 'trailing': false + * })); + * + * // Ensure `batchLog` is invoked once after 1 second of debounced calls. + * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 }); + * var source = new EventSource('/stream'); + * jQuery(source).on('message', debounced); + * + * // Cancel the trailing debounced invocation. + * jQuery(window).on('popstate', debounced.cancel); + */ +function debounce(func, wait, options) { + let lastArgs, + lastThis, + maxWait, + result, + timerId, + lastCallTime, + lastInvokeTime = 0, + leading = false, + maxing = false, + trailing = true; + + if (typeof func !== "function") { + throw new TypeError(FUNC_ERROR_TEXT); + } + wait = toNumber(wait) || 0; + if (isObject(options)) { + leading = !!options.leading; + maxing = "maxWait" in options; + maxWait = maxing + ? nativeMax(toNumber(options.maxWait) || 0, wait) + : maxWait; + trailing = "trailing" in options ? !!options.trailing : trailing; + } + + function invokeFunc(time) { + const args = lastArgs, + thisArg = lastThis; + + lastArgs = lastThis = undefined; + lastInvokeTime = time; + result = func.apply(thisArg, args); + return result; + } + + function leadingEdge(time) { + // Reset any `maxWait` timer. + lastInvokeTime = time; + // Start the timer for the trailing edge. + timerId = setTimeout(timerExpired, wait); + // Invoke the leading edge. + return leading ? invokeFunc(time) : result; + } + + function remainingWait(time) { + const timeSinceLastCall = time - lastCallTime, + timeSinceLastInvoke = time - lastInvokeTime, + result = wait - timeSinceLastCall; + + return maxing ? nativeMin(result, maxWait - timeSinceLastInvoke) : result; + } + + function shouldInvoke(time) { + const timeSinceLastCall = time - lastCallTime, + timeSinceLastInvoke = time - lastInvokeTime; + + // Either this is the first call, activity has stopped and we're at the + // trailing edge, the system time has gone backwards and we're treating + // it as the trailing edge, or we've hit the `maxWait` limit. + return ( + lastCallTime === undefined || + timeSinceLastCall >= wait || + timeSinceLastCall < 0 || + (maxing && timeSinceLastInvoke >= maxWait) + ); + } + + function timerExpired() { + const time = now(); + if (shouldInvoke(time)) { + return trailingEdge(time); + } + // Restart the timer. + timerId = setTimeout(timerExpired, remainingWait(time)); + } + + function trailingEdge(time) { + timerId = undefined; + + // Only invoke if we have `lastArgs` which means `func` has been + // debounced at least once. + if (trailing && lastArgs) { + return invokeFunc(time); + } + lastArgs = lastThis = undefined; + return result; + } + + function cancel() { + if (timerId !== undefined) { + clearTimeout(timerId); + } + lastInvokeTime = 0; + lastArgs = lastCallTime = lastThis = timerId = undefined; + } + + function flush() { + return timerId === undefined ? result : trailingEdge(now()); + } + + function debounced() { + const time = now(), + isInvoking = shouldInvoke(time); + + lastArgs = arguments; + lastThis = this; + lastCallTime = time; + + if (isInvoking) { + if (timerId === undefined) { + return leadingEdge(lastCallTime); + } + if (maxing) { + // Handle invocations in a tight loop. + timerId = setTimeout(timerExpired, wait); + return invokeFunc(lastCallTime); + } + } + if (timerId === undefined) { + timerId = setTimeout(timerExpired, wait); + } + return result; + } + debounced.cancel = cancel; + debounced.flush = flush; + return debounced; +} + +/** + * Creates a throttled function that only invokes `func` at most once per + * every `wait` milliseconds. The throttled function comes with a `cancel` + * method to cancel delayed `func` invocations and a `flush` method to + * immediately invoke them. Provide `options` to indicate whether `func` + * should be invoked on the leading and/or trailing edge of the `wait` + * timeout. The `func` is invoked with the last arguments provided to the + * throttled function. Subsequent calls to the throttled function return the + * result of the last `func` invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is + * invoked on the trailing edge of the timeout only if the throttled function + * is invoked more than once during the `wait` timeout. + * + * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred + * until to the next tick, similar to `setTimeout` with a timeout of `0`. + * + * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) + * for details over the differences between `_.throttle` and `_.debounce`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to throttle. + * @param {number} [wait=0] The number of milliseconds to throttle invocations to. + * @param {Object} [options={}] The options object. + * @param {boolean} [options.leading=true] + * Specify invoking on the leading edge of the timeout. + * @param {boolean} [options.trailing=true] + * Specify invoking on the trailing edge of the timeout. + * @returns {Function} Returns the new throttled function. + * @example + * + * // Avoid excessively updating the position while scrolling. + * jQuery(window).on('scroll', _.throttle(updatePosition, 100)); + * + * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes. + * var throttled = _.throttle(renewToken, 300000, { 'trailing': false }); + * jQuery(element).on('click', throttled); + * + * // Cancel the trailing throttled invocation. + * jQuery(window).on('popstate', throttled.cancel); + */ +function throttle(func, wait, options) { + let leading = true, + trailing = true; + + if (typeof func !== "function") { + throw new TypeError(FUNC_ERROR_TEXT); + } + if (isObject(options)) { + leading = "leading" in options ? !!options.leading : leading; + trailing = "trailing" in options ? !!options.trailing : trailing; + } + return debounce(func, wait, { + leading: leading, + maxWait: wait, + trailing: trailing, + }); +} + +/** + * Checks if `value` is the + * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) + * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(_.noop); + * // => true + * + * _.isObject(null); + * // => false + */ +function isObject(value) { + const type = typeof value; + return !!value && (type === "object" || type === "function"); +} + +/** + * Checks if `value` is object-like. A value is object-like if it's not `null` + * and has a `typeof` result of "object". + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is object-like, else `false`. + * @example + * + * _.isObjectLike({}); + * // => true + * + * _.isObjectLike([1, 2, 3]); + * // => true + * + * _.isObjectLike(_.noop); + * // => false + * + * _.isObjectLike(null); + * // => false + */ +function isObjectLike(value) { + return !!value && typeof value === "object"; +} + +/** + * Checks if `value` is classified as a `Symbol` primitive or object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. + * @example + * + * _.isSymbol(Symbol.iterator); + * // => true + * + * _.isSymbol('abc'); + * // => false + */ +function isSymbol(value) { + return ( + typeof value === "symbol" || + (isObjectLike(value) && objectToString.call(value) === symbolTag) + ); +} + +/** + * Converts `value` to a number. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to process. + * @returns {number} Returns the number. + * @example + * + * _.toNumber(3.2); + * // => 3.2 + * + * _.toNumber(Number.MIN_VALUE); + * // => 5e-324 + * + * _.toNumber(Infinity); + * // => Infinity + * + * _.toNumber('3.2'); + * // => 3.2 + */ +function toNumber(value) { + if (typeof value === "number") { + return value; + } + if (isSymbol(value)) { + return NAN; + } + if (isObject(value)) { + const other = typeof value.valueOf === "function" ? value.valueOf() : value; + value = isObject(other) ? `${other}` : other; + } + if (typeof value !== "string") { + return value === 0 ? value : +value; + } + value = value.replace(reTrim, ""); + const isBinary = reIsBinary.test(value); + return isBinary || reIsOctal.test(value) + ? freeParseInt(value.slice(2), isBinary ? 2 : 8) + : reIsBadHex.test(value) + ? NAN + : +value; +} + +const lodash_throttle = throttle; + +/** + * A collection of shims that provide minimal functionality of the ES6 collections. + * + * These implementations are not meant to be used outside of the ResizeObserver + * modules as they cover only a limited range of use cases. + */ +/* eslint-disable require-jsdoc, valid-jsdoc */ +const MapShim = (() => { + if (typeof Map !== "undefined") { + return Map; + } + + /** + * Returns index in provided array that matches the specified key. + * + * @param {Array} arr + * @param {*} key + * @returns {number} + */ + function getIndex(arr, key) { + let result = -1; + + arr.some((entry, index) => { + if (entry[0] === key) { + result = index; + + return true; + } + + return false; + }); + + return result; + } + + return (() => { + function anonymous() { + this.__entries__ = []; + } + + const prototypeAccessors = { size: { configurable: true } }; + + /** + * @returns {boolean} + */ + prototypeAccessors.size.get = function () { + return this.__entries__.length; + }; + + /** + * @param {*} key + * @returns {*} + */ + anonymous.prototype.get = function (key) { + const index = getIndex(this.__entries__, key); + const entry = this.__entries__[index]; + + return entry?.[1]; + }; + + /** + * @param {*} key + * @param {*} value + * @returns {void} + */ + anonymous.prototype.set = function (key, value) { + const index = getIndex(this.__entries__, key); + + if (~index) { + this.__entries__[index][1] = value; + } else { + this.__entries__.push([key, value]); + } + }; + + /** + * @param {*} key + * @returns {void} + */ + anonymous.prototype.delete = function (key) { + const entries = this.__entries__; + const index = getIndex(entries, key); + + if (~index) { + entries.splice(index, 1); + } + }; + + /** + * @param {*} key + * @returns {void} + */ + anonymous.prototype.has = function (key) { + return !!~getIndex(this.__entries__, key); + }; + + /** + * @returns {void} + */ + anonymous.prototype.clear = function () { + this.__entries__.splice(0); + }; + + /** + * @param {Function} callback + * @param {*} [ctx=null] + * @returns {void} + */ + anonymous.prototype.forEach = function (callback, ctx) { + if (ctx === void 0) ctx = null; + + for (let i = 0, list = this.__entries__; i < list.length; i += 1) { + const entry = list[i]; + + callback.call(ctx, entry[1], entry[0]); + } + }; + + Object.defineProperties(anonymous.prototype, prototypeAccessors); + + return anonymous; + })(); +})(); + +/** + * Detects whether window and document objects are available in current environment. + */ +const isBrowser = + typeof window !== "undefined" && + typeof document !== "undefined" && + window.document === document; + +// Returns global object of a current environment. +const global$1 = (() => { + if (typeof global !== "undefined" && global.Math === Math) { + return global; + } + + if (typeof self !== "undefined" && self.Math === Math) { + return self; + } + + if (typeof window !== "undefined" && window.Math === Math) { + return window; + } + + // eslint-disable-next-line no-new-func + return Function("return this")(); +})(); + +/** + * A shim for the requestAnimationFrame which falls back to the setTimeout if + * first one is not supported. + * + * @returns {number} Requests' identifier. + */ +const requestAnimationFrame$1 = (() => { + if (typeof requestAnimationFrame === "function") { + // It's required to use a bounded function because IE sometimes throws + // an "Invalid calling object" error if rAF is invoked without the global + // object on the left hand side. + return requestAnimationFrame.bind(global$1); + } + + return (callback) => setTimeout(() => callback(Date.now()), 1000 / 60); +})(); + +// Defines minimum timeout before adding a trailing call. +const trailingTimeout = 2; + +/** + * Creates a wrapper function which ensures that provided callback will be + * invoked only once during the specified delay period. + * + * @param {Function} callback - Function to be invoked after the delay period. + * @param {number} delay - Delay after which to invoke callback. + * @returns {Function} + */ +const throttle$1 = (callback, delay) => { + let leadingCall = false, + trailingCall = false, + lastCallTime = 0; + + /** + * Invokes the original callback function and schedules new invocation if + * the "proxy" was called during current request. + * + * @returns {void} + */ + function resolvePending() { + if (leadingCall) { + leadingCall = false; + + callback(); + } + + if (trailingCall) { + proxy(); + } + } + + /** + * Callback invoked after the specified delay. It will further postpone + * invocation of the original function delegating it to the + * requestAnimationFrame. + * + * @returns {void} + */ + function timeoutCallback() { + requestAnimationFrame$1(resolvePending); + } + + /** + * Schedules invocation of the original function. + * + * @returns {void} + */ + function proxy() { + const timeStamp = Date.now(); + + if (leadingCall) { + // Reject immediately following calls. + if (timeStamp - lastCallTime < trailingTimeout) { + return; + } + + // Schedule new call to be in invoked when the pending one is resolved. + // This is important for "transitions" which never actually start + // immediately so there is a chance that we might miss one if change + // happens amids the pending invocation. + trailingCall = true; + } else { + leadingCall = true; + trailingCall = false; + + setTimeout(timeoutCallback, delay); + } + + lastCallTime = timeStamp; + } + + return proxy; +}; + +// Minimum delay before invoking the update of observers. +const REFRESH_DELAY = 20; + +// A list of substrings of CSS properties used to find transition events that +// might affect dimensions of observed elements. +const transitionKeys = [ + "top", + "right", + "bottom", + "left", + "width", + "height", + "size", + "weight", +]; + +// Check if MutationObserver is available. +const mutationObserverSupported = typeof MutationObserver !== "undefined"; + +/** + * Singleton controller class which handles updates of ResizeObserver instances. + */ +const ResizeObserverController = function () { + this.connected_ = false; + this.mutationEventsAdded_ = false; + this.mutationsObserver_ = null; + this.observers_ = []; + + this.onTransitionEnd_ = this.onTransitionEnd_.bind(this); + this.refresh = throttle$1(this.refresh.bind(this), REFRESH_DELAY); +}; + +/** + * Adds observer to observers list. + * + * @param {ResizeObserverSPI} observer - Observer to be added. + * @returns {void} + */ + +/** + * Holds reference to the controller's instance. + * + * @private {ResizeObserverController} + */ + +/** + * Keeps reference to the instance of MutationObserver. + * + * @private {MutationObserver} + */ + +/** + * Indicates whether DOM listeners have been added. + * + * @private {boolean} + */ +ResizeObserverController.prototype.addObserver = function (observer) { + if (!~this.observers_.indexOf(observer)) { + this.observers_.push(observer); + } + + // Add listeners if they haven't been added yet. + if (!this.connected_) { + this.connect_(); + } +}; + +/** + * Removes observer from observers list. + * + * @param {ResizeObserverSPI} observer - Observer to be removed. + * @returns {void} + */ +ResizeObserverController.prototype.removeObserver = function (observer) { + const observers = this.observers_; + const index = observers.indexOf(observer); + + // Remove observer if it's present in registry. + if (~index) { + observers.splice(index, 1); + } + + // Remove listeners if controller has no connected observers. + if (!observers.length && this.connected_) { + this.disconnect_(); + } +}; + +/** + * Invokes the update of observers. It will continue running updates insofar + * it detects changes. + * + * @returns {void} + */ +ResizeObserverController.prototype.refresh = function () { + const changesDetected = this.updateObservers_(); + + // Continue running updates if changes have been detected as there might + // be future ones caused by CSS transitions. + if (changesDetected) { + this.refresh(); + } +}; + +/** + * Updates every observer from observers list and notifies them of queued + * entries. + * + * @private + * @returns {boolean} Returns "true" if any observer has detected changes in + * dimensions of it's elements. + */ +ResizeObserverController.prototype.updateObservers_ = function () { + // Collect observers that have active observations. + const activeObservers = this.observers_.filter( + (observer) => (observer.gatherActive(), observer.hasActive()) + ); + + // Deliver notifications in a separate cycle in order to avoid any + // collisions between observers, e.g. when multiple instances of + // ResizeObserver are tracking the same element and the callback of one + // of them changes content dimensions of the observed target. Sometimes + // this may result in notifications being blocked for the rest of observers. + activeObservers.forEach((observer) => observer.broadcastActive()); + + return activeObservers.length > 0; +}; + +/** + * Initializes DOM listeners. + * + * @private + * @returns {void} + */ +ResizeObserverController.prototype.connect_ = function () { + // Do nothing if running in a non-browser environment or if listeners + // have been already added. + if (!isBrowser || this.connected_) { + return; + } + + // Subscription to the "Transitionend" event is used as a workaround for + // delayed transitions. This way it's possible to capture at least the + // final state of an element. + document.addEventListener("transitionend", this.onTransitionEnd_); + + window.addEventListener("resize", this.refresh); + + if (mutationObserverSupported) { + this.mutationsObserver_ = new MutationObserver(this.refresh); + + this.mutationsObserver_.observe(document, { + attributes: true, + childList: true, + characterData: true, + subtree: true, + }); + } else { + document.addEventListener("DOMSubtreeModified", this.refresh); + + this.mutationEventsAdded_ = true; + } + + this.connected_ = true; +}; + +/** + * Removes DOM listeners. + * + * @private + * @returns {void} + */ +ResizeObserverController.prototype.disconnect_ = function () { + // Do nothing if running in a non-browser environment or if listeners + // have been already removed. + if (!isBrowser || !this.connected_) { + return; + } + + document.removeEventListener("transitionend", this.onTransitionEnd_); + window.removeEventListener("resize", this.refresh); + + if (this.mutationsObserver_) { + this.mutationsObserver_.disconnect(); + } + + if (this.mutationEventsAdded_) { + document.removeEventListener("DOMSubtreeModified", this.refresh); + } + + this.mutationsObserver_ = null; + this.mutationEventsAdded_ = false; + this.connected_ = false; +}; + +/** + * "Transitionend" event handler. + * + * @private + * @param {TransitionEvent} event + * @returns {void} + */ +ResizeObserverController.prototype.onTransitionEnd_ = function (ref) { + let propertyName = ref.propertyName; + if (propertyName === void 0) propertyName = ""; + + // Detect whether transition may affect dimensions of an element. + const isReflowProperty = transitionKeys.some( + (key) => !!~propertyName.indexOf(key) + ); + + if (isReflowProperty) { + this.refresh(); + } +}; + +/** + * Returns instance of the ResizeObserverController. + * + * @returns {ResizeObserverController} + */ +ResizeObserverController.getInstance = function () { + if (!this.instance_) { + this.instance_ = new ResizeObserverController(); + } + + return this.instance_; +}; + +ResizeObserverController.instance_ = null; + +/** + * Defines non-writable/enumerable properties of the provided target object. + * + * @param {Object} target - Object for which to define properties. + * @param {Object} props - Properties to be defined. + * @returns {Object} Target object. + */ +const defineConfigurable = (target, props) => { + for (let i = 0, list = Object.keys(props); i < list.length; i += 1) { + const key = list[i]; + + Object.defineProperty(target, key, { + value: props[key], + enumerable: false, + writable: false, + configurable: true, + }); + } + + return target; +}; + +/** + * Returns the global object associated with provided element. + * + * @param {Object} target + * @returns {Object} + */ +const getWindowOf = (target) => { + // Assume that the element is an instance of Node, which means that it + // has the "ownerDocument" property from which we can retrieve a + // corresponding global object. + const ownerGlobal = target?.ownerDocument?.defaultView; + + // Return the local global object if it's not possible extract one from + // provided element. + return ownerGlobal || global$1; +}; + +// Placeholder of an empty content rectangle. +const emptyRect = createRectInit(0, 0, 0, 0); + +/** + * Converts provided string to a number. + * + * @param {number|string} value + * @returns {number} + */ +function toFloat(value) { + return parseFloat(value) || 0; +} + +/** + * Extracts borders size from provided styles. + * + * @param {CSSStyleDeclaration} styles + * @param {...string} positions - Borders positions (top, right, ...) + * @returns {number} + */ +function getBordersSize(styles) { + let positions = [], + len = arguments.length - 1; + while (len-- > 0) positions[len] = arguments[len + 1]; + + return positions.reduce((size, position) => { + const value = styles[`border-${position}-width`]; + + return size + toFloat(value); + }, 0); +} + +/** + * Extracts paddings sizes from provided styles. + * + * @param {CSSStyleDeclaration} styles + * @returns {Object} Paddings box. + */ +function getPaddings(styles) { + const positions = ["top", "right", "bottom", "left"]; + const paddings = {}; + + for (let i = 0, list = positions; i < list.length; i += 1) { + const position = list[i]; + + const value = styles[`padding-${position}`]; + + paddings[position] = toFloat(value); + } + + return paddings; +} + +/** + * Calculates content rectangle of provided SVG element. + * + * @param {SVGGraphicsElement} target - Element content rectangle of which needs + * to be calculated. + * @returns {DOMRectInit} + */ +function getSVGContentRect(target) { + const bbox = target.getBBox(); + + return createRectInit(0, 0, bbox.width, bbox.height); +} + +/** + * Calculates content rectangle of provided HTMLElement. + * + * @param {HTMLElement} target - Element for which to calculate the content rectangle. + * @returns {DOMRectInit} + */ +function getHTMLElementContentRect(target) { + // Client width & height properties can't be + // used exclusively as they provide rounded values. + const clientWidth = target.clientWidth; + const clientHeight = target.clientHeight; + + // By this condition we can catch all non-replaced inline, hidden and + // detached elements. Though elements with width & height properties less + // than 0.5 will be discarded as well. + // + // Without it we would need to implement separate methods for each of + // those cases and it's not possible to perform a precise and performance + // effective test for hidden elements. E.g. even jQuery's ':visible' filter + // gives wrong results for elements with width & height less than 0.5. + if (!clientWidth && !clientHeight) { + return emptyRect; + } + + const styles = getWindowOf(target).getComputedStyle(target); + const paddings = getPaddings(styles); + const horizPad = paddings.left + paddings.right; + const vertPad = paddings.top + paddings.bottom; + + // Computed styles of width & height are being used because they are the + // only dimensions available to JS that contain non-rounded values. It could + // be possible to utilize the getBoundingClientRect if only it's data wasn't + // affected by CSS transformations let alone paddings, borders and scroll bars. + let width = toFloat(styles.width), + height = toFloat(styles.height); + + // Width & height include paddings and borders when the 'border-box' box + // model is applied (except for IE). + if (styles.boxSizing === "border-box") { + // Following conditions are required to handle Internet Explorer which + // doesn't include paddings and borders to computed CSS dimensions. + // + // We can say that if CSS dimensions + paddings are equal to the "client" + // properties then it's either IE, and thus we don't need to subtract + // anything, or an element merely doesn't have paddings/borders styles. + if (Math.round(width + horizPad) !== clientWidth) { + width -= getBordersSize(styles, "left", "right") + horizPad; + } + + if (Math.round(height + vertPad) !== clientHeight) { + height -= getBordersSize(styles, "top", "bottom") + vertPad; + } + } + + // Following steps can't be applied to the document's root element as its + // client[Width/Height] properties represent viewport area of the window. + // Besides, it's as well not necessary as the itself neither has + // rendered scroll bars nor it can be clipped. + if (!isDocumentElement(target)) { + // In some browsers (only in Firefox, actually) CSS width & height + // include scroll bars size which can be removed at this step as scroll + // bars are the only difference between rounded dimensions + paddings + // and "client" properties, though that is not always true in Chrome. + const vertScrollbar = Math.round(width + horizPad) - clientWidth; + const horizScrollbar = Math.round(height + vertPad) - clientHeight; + + // Chrome has a rather weird rounding of "client" properties. + // E.g. for an element with content width of 314.2px it sometimes gives + // the client width of 315px and for the width of 314.7px it may give + // 314px. And it doesn't happen all the time. So just ignore this delta + // as a non-relevant. + if (Math.abs(vertScrollbar) !== 1) { + width -= vertScrollbar; + } + + if (Math.abs(horizScrollbar) !== 1) { + height -= horizScrollbar; + } + } + + return createRectInit(paddings.left, paddings.top, width, height); +} + +/** + * Checks whether provided element is an instance of the SVGGraphicsElement. + * + * @param {Element} target - Element to be checked. + * @returns {boolean} + */ +const isSVGGraphicsElement = (() => { + // Some browsers, namely IE and Edge, don't have the SVGGraphicsElement + // interface. + if (typeof SVGGraphicsElement !== "undefined") { + return (target) => target instanceof getWindowOf(target).SVGGraphicsElement; + } + + // If it's so, then check that element is at least an instance of the + // SVGElement and that it has the "getBBox" method. + // eslint-disable-next-line no-extra-parens + return (target) => + target instanceof getWindowOf(target).SVGElement && + typeof target.getBBox === "function"; +})(); + +/** + * Checks whether provided element is a document element (). + * + * @param {Element} target - Element to be checked. + * @returns {boolean} + */ +function isDocumentElement(target) { + return target === getWindowOf(target).document.documentElement; +} + +/** + * Calculates an appropriate content rectangle for provided html or svg element. + * + * @param {Element} target - Element content rectangle of which needs to be calculated. + * @returns {DOMRectInit} + */ +function getContentRect(target) { + if (!isBrowser) { + return emptyRect; + } + + if (isSVGGraphicsElement(target)) { + return getSVGContentRect(target); + } + + return getHTMLElementContentRect(target); +} + +/** + * Creates rectangle with an interface of the DOMRectReadOnly. + * Spec: https://drafts.fxtf.org/geometry/#domrectreadonly + * + * @param {DOMRectInit} rectInit - Object with rectangle's x/y coordinates and dimensions. + * @returns {DOMRectReadOnly} + */ +function createReadOnlyRect(ref) { + const x = ref.x; + const y = ref.y; + const width = ref.width; + const height = ref.height; + + // If DOMRectReadOnly is available use it as a prototype for the rectangle. + const Constr = + typeof DOMRectReadOnly !== "undefined" ? DOMRectReadOnly : Object; + const rect = Object.create(Constr.prototype); + + // Rectangle's properties are not writable and non-enumerable. + defineConfigurable(rect, { + x: x, + y: y, + width: width, + height: height, + top: y, + right: x + width, + bottom: height + y, + left: x, + }); + + return rect; +} + +/** + * Creates DOMRectInit object based on the provided dimensions and the x/y coordinates. + * Spec: https://drafts.fxtf.org/geometry/#dictdef-domrectinit + * + * @param {number} x - X coordinate. + * @param {number} y - Y coordinate. + * @param {number} width - Rectangle's width. + * @param {number} height - Rectangle's height. + * @returns {DOMRectInit} + */ +function createRectInit(x, y, width, height) { + return { x: x, y: y, width: width, height: height }; +} + +/** + * Class that is responsible for computations of the content rectangle of + * provided DOM element and for keeping track of it's changes. + */ +const ResizeObservation = function (target) { + this.broadcastWidth = 0; + this.broadcastHeight = 0; + this.contentRect_ = createRectInit(0, 0, 0, 0); + + this.target = target; +}; + +/** + * Updates content rectangle and tells whether it's width or height properties + * have changed since the last broadcast. + * + * @returns {boolean} + */ + +/** + * Reference to the last observed content rectangle. + * + * @private {DOMRectInit} + */ + +/** + * Broadcasted width of content rectangle. + * + * @type {number} + */ +ResizeObservation.prototype.isActive = function () { + const rect = getContentRect(this.target); + + this.contentRect_ = rect; + + return ( + rect.width !== this.broadcastWidth || rect.height !== this.broadcastHeight + ); +}; + +/** + * Updates 'broadcastWidth' and 'broadcastHeight' properties with a data + * from the corresponding properties of the last observed content rectangle. + * + * @returns {DOMRectInit} Last observed content rectangle. + */ +ResizeObservation.prototype.broadcastRect = function () { + const rect = this.contentRect_; + + this.broadcastWidth = rect.width; + this.broadcastHeight = rect.height; + + return rect; +}; + +const ResizeObserverEntry = function (target, rectInit) { + const contentRect = createReadOnlyRect(rectInit); + + // According to the specification following properties are not writable + // and are also not enumerable in the native implementation. + // + // Property accessors are not being used as they'd require to define a + // private WeakMap storage which may cause memory leaks in browsers that + // don't support this type of collections. + defineConfigurable(this, { target: target, contentRect: contentRect }); +}; + +const ResizeObserverSPI = function (callback, controller, callbackCtx) { + this.activeObservations_ = []; + this.observations_ = new MapShim(); + + if (typeof callback !== "function") { + throw new TypeError( + "The callback provided as parameter 1 is not a function." + ); + } + + this.callback_ = callback; + this.controller_ = controller; + this.callbackCtx_ = callbackCtx; +}; + +/** + * Starts observing provided element. + * + * @param {Element} target - Element to be observed. + * @returns {void} + */ + +/** + * Registry of the ResizeObservation instances. + * + * @private {Map} + */ + +/** + * Public ResizeObserver instance which will be passed to the callback + * function and used as a value of it's "this" binding. + * + * @private {ResizeObserver} + */ + +/** + * Collection of resize observations that have detected changes in dimensions + * of elements. + * + * @private {Array} + */ +ResizeObserverSPI.prototype.observe = function (target) { + if (!arguments.length) { + throw new TypeError("1 argument required, but only 0 present."); + } + + // Do nothing if current environment doesn't have the Element interface. + if (typeof Element === "undefined" || !(Element instanceof Object)) { + return; + } + + if (!(target instanceof getWindowOf(target).Element)) { + throw new TypeError('parameter 1 is not of type "Element".'); + } + + const observations = this.observations_; + + // Do nothing if element is already being observed. + if (observations.has(target)) { + return; + } + + observations.set(target, new ResizeObservation(target)); + + this.controller_.addObserver(this); + + // Force the update of observations. + this.controller_.refresh(); +}; + +/** + * Stops observing provided element. + * + * @param {Element} target - Element to stop observing. + * @returns {void} + */ +ResizeObserverSPI.prototype.unobserve = function (target) { + if (!arguments.length) { + throw new TypeError("1 argument required, but only 0 present."); + } + + // Do nothing if current environment doesn't have the Element interface. + if (typeof Element === "undefined" || !(Element instanceof Object)) { + return; + } + + if (!(target instanceof getWindowOf(target).Element)) { + throw new TypeError('parameter 1 is not of type "Element".'); + } + + const observations = this.observations_; + + // Do nothing if element is not being observed. + if (!observations.has(target)) { + return; + } + + observations.delete(target); + + if (!observations.size) { + this.controller_.removeObserver(this); + } +}; + +/** + * Stops observing all elements. + * + * @returns {void} + */ +ResizeObserverSPI.prototype.disconnect = function () { + this.clearActive(); + this.observations_.clear(); + this.controller_.removeObserver(this); +}; + +/** + * Collects observation instances the associated element of which has changed + * it's content rectangle. + * + * @returns {void} + */ +ResizeObserverSPI.prototype.gatherActive = function () { + this.clearActive(); + + this.observations_.forEach((observation) => { + if (observation.isActive()) { + this.activeObservations_.push(observation); + } + }); +}; + +/** + * Invokes initial callback function with a list of ResizeObserverEntry + * instances collected from active resize observations. + * + * @returns {void} + */ +ResizeObserverSPI.prototype.broadcastActive = function () { + // Do nothing if observer doesn't have active observations. + if (!this.hasActive()) { + return; + } + + const ctx = this.callbackCtx_; + + // Create ResizeObserverEntry instance for every active observation. + const entries = this.activeObservations_.map( + (observation) => + new ResizeObserverEntry(observation.target, observation.broadcastRect()) + ); + + this.callback_.call(ctx, entries, ctx); + this.clearActive(); +}; + +/** + * Clears the collection of active observations. + * + * @returns {void} + */ +ResizeObserverSPI.prototype.clearActive = function () { + this.activeObservations_.splice(0); +}; + +/** + * Tells whether observer has active observations. + * + * @returns {boolean} + */ +ResizeObserverSPI.prototype.hasActive = function () { + return this.activeObservations_.length > 0; +}; + +// Registry of internal observers. If WeakMap is not available use current shim +// for the Map collection as it has all required methods and because WeakMap +// can't be fully polyfilled anyway. +const observers = + typeof WeakMap !== "undefined" ? new WeakMap() : new MapShim(); + +/** + * ResizeObserver API. Encapsulates the ResizeObserver SPI implementation + * exposing only those methods and properties that are defined in the spec. + */ +const ResizeObserver = function (callback) { + if (!(this instanceof ResizeObserver)) { + throw new TypeError("Cannot call a class as a function."); + } + if (!arguments.length) { + throw new TypeError("1 argument required, but only 0 present."); + } + + const controller = ResizeObserverController.getInstance(); + const observer = new ResizeObserverSPI(callback, controller, this); + + observers.set(this, observer); +}; + +// Expose public methods of ResizeObserver. +["observe", "unobserve", "disconnect"].forEach((method) => { + ResizeObserver.prototype[method] = function () { + return (ref = observers.get(this))[method].apply(ref, arguments); + let ref; + }; +}); + +const index = (() => { + // Export existing implementation if available. + if (typeof global$1.ResizeObserver !== "undefined") { + return global$1.ResizeObserver; + } + + return ResizeObserver; +})(); + +const canUseDOM = !!( + typeof window !== "undefined" && + window.document && + window.document.createElement +); + +const canUseDom = canUseDOM; + +const SimpleBar = + /*#__PURE__*/ + (() => { + function SimpleBar(element, options) { + _classCallCheck(this, SimpleBar); + + this.onScrollX = () => { + if (!this.scrollXTicking) { + window.requestAnimationFrame(this.scrollX); + this.scrollXTicking = true; + } + }; + + this.onScrollY = () => { + if (!this.scrollYTicking) { + window.requestAnimationFrame(this.scrollY); + this.scrollYTicking = true; + } + }; + + this.scrollX = () => { + this.showScrollbar("x"); + + this.positionScrollbar("x"); + + this.scrollXTicking = false; + }; + + this.scrollY = () => { + this.showScrollbar("y"); + + this.positionScrollbar("y"); + + this.scrollYTicking = false; + }; + + this.onMouseEnter = () => { + this.showScrollbar("x"); + + this.showScrollbar("y"); + }; + + this.onMouseMove = (e) => { + const bboxY = this.trackY.getBoundingClientRect(); + + const bboxX = this.trackX.getBoundingClientRect(); + + this.mouseX = e.clientX; + this.mouseY = e.clientY; + + if (this.isWithinBounds(bboxY)) { + this.showScrollbar("y"); + } + + if (this.isWithinBounds(bboxX)) { + this.showScrollbar("x"); + } + }; + + this.onWindowResize = () => { + this.hideNativeScrollbar(); + }; + + this.hideScrollbars = () => { + const bboxY = this.trackY.getBoundingClientRect(); + + const bboxX = this.trackX.getBoundingClientRect(); + + if (!this.isWithinBounds(bboxY)) { + this.scrollbarY.classList.remove("visible"); + + this.isVisible.y = false; + } + + if (!this.isWithinBounds(bboxX)) { + this.scrollbarX.classList.remove("visible"); + + this.isVisible.x = false; + } + }; + + this.onMouseDown = (e) => { + const bboxY = this.scrollbarY.getBoundingClientRect(); + + const bboxX = this.scrollbarX.getBoundingClientRect(); + + if (this.isWithinBounds(bboxY)) { + e.preventDefault(); + + this.onDrag(e, "y"); + } + + if (this.isWithinBounds(bboxX)) { + e.preventDefault(); + + this.onDrag(e, "x"); + } + }; + + this.drag = (e) => { + let eventOffset, track, scrollEl; + e.preventDefault(); + + if (this.currentAxis === "y") { + eventOffset = e.pageY; + track = this.trackY; + scrollEl = this.scrollContentEl; + } else { + eventOffset = e.pageX; + track = this.trackX; + scrollEl = this.contentEl; + } // Calculate how far the user's mouse is from the top/left of the scrollbar (minus the dragOffset). + + const dragPos = + eventOffset - + track.getBoundingClientRect()[this.offsetAttr[this.currentAxis]] - + this.dragOffset[this.currentAxis]; // Convert the mouse position into a percentage of the scrollbar height/width. + + const dragPerc = dragPos / track[this.sizeAttr[this.currentAxis]]; // Scroll the content by the same percentage. + + const scrollPos = + dragPerc * this.contentEl[this.scrollSizeAttr[this.currentAxis]]; + scrollEl[this.scrollOffsetAttr[this.currentAxis]] = scrollPos; + }; + + this.onEndDrag = () => { + document.removeEventListener("mousemove", this.drag); + document.removeEventListener("mouseup", this.onEndDrag); + }; + + this.el = element; + this.flashTimeout; + this.contentEl; + this.scrollContentEl; + this.dragOffset = { + x: 0, + y: 0, + }; + this.isEnabled = { + x: true, + y: true, + }; + this.isVisible = { + x: false, + y: false, + }; + this.scrollOffsetAttr = { + x: "scrollLeft", + y: "scrollTop", + }; + this.sizeAttr = { + x: "offsetWidth", + y: "offsetHeight", + }; + this.scrollSizeAttr = { + x: "scrollWidth", + y: "scrollHeight", + }; + this.offsetAttr = { + x: "left", + y: "top", + }; + this.handleSize = { + x: 0, + y: 0, + }; + this.globalObserver; + this.mutationObserver; + this.resizeObserver; + this.currentAxis; + this.scrollbarWidth; + this.options = Object.assign({}, SimpleBar.defaultOptions, options); + this.isRtl = this.options.direction === "rtl"; + this.classNames = this.options.classNames; + this.offsetSize = 20; + this.recalculateImmediate = this.recalculate.bind(this); + this.recalculate = lodash_throttle(this.recalculate.bind(this), 1000); + this.onMouseMove = lodash_throttle(this.onMouseMove.bind(this), 100); + this.init(); + } + + _createClass( + SimpleBar, + [ + { + key: "init", + value: function init() { + // Save a reference to the instance, so we know this DOM node has already been instancied + this.el.SimpleBar = this; + this.initDOM(); // We stop here on server-side + + if (canUseDom) { + // Calculate content size + this.hideNativeScrollbar(); + this.render(); + this.initListeners(); + } + }, + }, + { + key: "initDOM", + value: function initDOM() { + // make sure this element doesn't have the elements yet + if ( + Array.from(this.el.children).filter((child) => + child.classList.contains(this.classNames.scrollContent) + ).length + ) { + // assume that element has his DOM already initiated + this.trackX = this.el.querySelector( + ".".concat(this.classNames.track, ".horizontal") + ); + this.trackY = this.el.querySelector( + ".".concat(this.classNames.track, ".vertical") + ); + this.scrollContentEl = this.el.querySelector( + ".".concat(this.classNames.scrollContent) + ); + this.contentEl = this.el.querySelector( + ".".concat(this.classNames.content) + ); + } else { + // Prepare DOM + this.scrollContentEl = document.createElement("div"); + this.contentEl = document.createElement("div"); + this.scrollContentEl.classList.add(this.classNames.scrollContent); + this.contentEl.classList.add(this.classNames.content); + + while (this.el.firstChild) { + this.contentEl.appendChild(this.el.firstChild); + } + + this.scrollContentEl.appendChild(this.contentEl); + this.el.appendChild(this.scrollContentEl); + } + + if (!this.trackX || !this.trackY) { + const track = document.createElement("div"); + const scrollbar = document.createElement("div"); + track.classList.add(this.classNames.track); + scrollbar.classList.add(this.classNames.scrollbar); + + if (!this.options.autoHide) { + scrollbar.classList.add("visible"); + } + + track.appendChild(scrollbar); + this.trackX = track.cloneNode(true); + this.trackX.classList.add("horizontal"); + this.trackY = track.cloneNode(true); + this.trackY.classList.add("vertical"); + this.el.insertBefore(this.trackX, this.el.firstChild); + this.el.insertBefore(this.trackY, this.el.firstChild); + } + + this.scrollbarX = this.trackX.querySelector( + ".".concat(this.classNames.scrollbar) + ); + this.scrollbarY = this.trackY.querySelector( + ".".concat(this.classNames.scrollbar) + ); + this.el.setAttribute("data-simplebar", "init"); + }, + }, + { + key: "initListeners", + value: function initListeners() { + // Event listeners + if (this.options.autoHide) { + this.el.addEventListener("mouseenter", this.onMouseEnter); + } + + this.el.addEventListener("mousedown", this.onMouseDown); + this.el.addEventListener("mousemove", this.onMouseMove); + this.contentEl.addEventListener("scroll", this.onScrollX); + this.scrollContentEl.addEventListener("scroll", this.onScrollY); // Browser zoom triggers a window resize + + window.addEventListener("resize", this.onWindowResize); // MutationObserver is IE11+ + + if (typeof MutationObserver !== "undefined") { + // create an observer instance + this.mutationObserver = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if ( + this.isChildNode(mutation.target) || + mutation.addedNodes.length + ) { + this.recalculate(); + } + }); + }); // pass in the target node, as well as the observer options + + this.mutationObserver.observe(this.el, { + attributes: true, + childList: true, + characterData: true, + subtree: true, + }); + } + + this.resizeObserver = new index(this.recalculate); + this.resizeObserver.observe(this.el); + }, + /** + * Recalculate scrollbar + */ + }, + { + key: "recalculate", + value: function recalculate() { + this.render(); + }, + }, + { + key: "render", + value: function render() { + this.contentSizeX = this.contentEl[this.scrollSizeAttr.x]; + this.contentSizeY = + this.contentEl[this.scrollSizeAttr.y] - + (this.scrollbarWidth || this.offsetSize); + this.trackXSize = this.trackX[this.sizeAttr.x]; + this.trackYSize = this.trackY[this.sizeAttr.y]; // Set isEnabled to false if scrollbar is not necessary (content is shorter than wrapper) + + this.isEnabled.x = this.trackXSize < this.contentSizeX; + this.isEnabled.y = this.trackYSize < this.contentSizeY; + this.resizeScrollbar("x"); + this.resizeScrollbar("y"); + this.positionScrollbar("x"); + this.positionScrollbar("y"); + this.toggleTrackVisibility("x"); + this.toggleTrackVisibility("y"); + }, + /** + * Resize scrollbar + */ + }, + { + key: "resizeScrollbar", + value: function resizeScrollbar() { + const axis = + arguments.length > 0 && arguments[0] !== undefined + ? arguments[0] + : "y"; + let scrollbar; + let contentSize; + let trackSize; + + if (!this.isEnabled[axis] && !this.options.forceVisible) { + return; + } + + if (axis === "x") { + scrollbar = this.scrollbarX; + contentSize = this.contentSizeX; + trackSize = this.trackXSize; + } else { + // 'y' + scrollbar = this.scrollbarY; + contentSize = this.contentSizeY; + trackSize = this.trackYSize; + } + + const scrollbarRatio = trackSize / contentSize; // Calculate new height/position of drag handle. + + this.handleSize[axis] = Math.max( + ~~(scrollbarRatio * trackSize), + this.options.scrollbarMinSize + ); + + if (this.options.scrollbarMaxSize) { + this.handleSize[axis] = Math.min( + this.handleSize[axis], + this.options.scrollbarMaxSize + ); + } + + if (axis === "x") { + scrollbar.style.width = "".concat(this.handleSize[axis], "px"); + } else { + scrollbar.style.height = "".concat(this.handleSize[axis], "px"); + } + }, + }, + { + key: "positionScrollbar", + value: function positionScrollbar() { + const axis = + arguments.length > 0 && arguments[0] !== undefined + ? arguments[0] + : "y"; + let scrollbar; + let scrollOffset; + let contentSize; + let trackSize; + + if (axis === "x") { + scrollbar = this.scrollbarX; + scrollOffset = this.contentEl[this.scrollOffsetAttr[axis]]; // Either scrollTop() or scrollLeft(). + + contentSize = this.contentSizeX; + trackSize = this.trackXSize; + } else { + // 'y' + scrollbar = this.scrollbarY; + scrollOffset = this.scrollContentEl[this.scrollOffsetAttr[axis]]; // Either scrollTop() or scrollLeft(). + + contentSize = this.contentSizeY; + trackSize = this.trackYSize; + } + + const scrollPourcent = scrollOffset / (contentSize - trackSize); + const handleOffset = ~~( + (trackSize - this.handleSize[axis]) * + scrollPourcent + ); + + if (this.isEnabled[axis] || this.options.forceVisible) { + if (axis === "x") { + scrollbar.style.transform = "translate3d(".concat( + handleOffset, + "px, 0, 0)" + ); + } else { + scrollbar.style.transform = "translate3d(0, ".concat( + handleOffset, + "px, 0)" + ); + } + } + }, + }, + { + key: "toggleTrackVisibility", + value: function toggleTrackVisibility() { + const axis = + arguments.length > 0 && arguments[0] !== undefined + ? arguments[0] + : "y"; + const track = axis === "y" ? this.trackY : this.trackX; + const scrollbar = axis === "y" ? this.scrollbarY : this.scrollbarX; + + if (this.isEnabled[axis] || this.options.forceVisible) { + track.style.visibility = "visible"; + } else { + track.style.visibility = "hidden"; + } // Even if forceVisible is enabled, scrollbar itself should be hidden + + if (this.options.forceVisible) { + if (this.isEnabled[axis]) { + scrollbar.style.visibility = "visible"; + } else { + scrollbar.style.visibility = "hidden"; + } + } + }, + }, + { + key: "hideNativeScrollbar", + value: function hideNativeScrollbar() { + // Recalculate scrollbarWidth in case it's a zoom + const offset = 20; // matched in style.css: [data-simplebar="init"] {...} + this.scrollbarWidth = scrollbarWidth(); + this.scrollContentEl.style[ + this.isRtl ? "paddingLeft" : "paddingRight" + ] = "".concat( + (this.scrollbarWidth || this.offsetSize) + offset, + "px" + ); + this.scrollContentEl.style.marginBottom = "-".concat( + this.scrollbarWidth * 2 || this.offsetSize, + "px" + ); + this.contentEl.style.paddingBottom = "".concat( + this.scrollbarWidth || this.offsetSize, + "px" + ); + + if (this.scrollbarWidth !== 0) { + this.contentEl.style[this.isRtl ? "marginLeft" : "marginRight"] = + "-".concat(offset, "px"); + } + }, + /** + * On scroll event handling + */ + }, + { + key: "showScrollbar", + + /** + * Show scrollbar + */ + value: function showScrollbar() { + const axis = + arguments.length > 0 && arguments[0] !== undefined + ? arguments[0] + : "y"; + let scrollbar; // Scrollbar already visible + + if (this.isVisible[axis]) { + return; + } + + if (axis === "x") { + scrollbar = this.scrollbarX; + } else { + // 'y' + scrollbar = this.scrollbarY; + } + + if (this.isEnabled[axis]) { + scrollbar.classList.add("visible"); + this.isVisible[axis] = true; + } + + if (!this.options.autoHide) { + return; + } + + window.clearInterval(this.flashTimeout); + this.flashTimeout = window.setInterval( + this.hideScrollbars, + this.options.timeout + ); + }, + /** + * Hide Scrollbar + */ + }, + { + key: "onDrag", + + /** + * on scrollbar handle drag + */ + value: function onDrag(e) { + const axis = + arguments.length > 1 && arguments[1] !== undefined + ? arguments[1] + : "y"; + // Preventing the event's default action stops text being + // selectable during the drag. + e.preventDefault(); + const scrollbar = axis === "y" ? this.scrollbarY : this.scrollbarX; // Measure how far the user's mouse is from the top of the scrollbar drag handle. + + const eventOffset = axis === "y" ? e.pageY : e.pageX; + this.dragOffset[axis] = + eventOffset - + scrollbar.getBoundingClientRect()[this.offsetAttr[axis]]; + this.currentAxis = axis; + document.addEventListener("mousemove", this.drag); + document.addEventListener("mouseup", this.onEndDrag); + }, + /** + * Drag scrollbar handle + */ + }, + { + key: "getScrollElement", + + /** + * Getter for original scrolling element + */ + value: function getScrollElement() { + const axis = + arguments.length > 0 && arguments[0] !== undefined + ? arguments[0] + : "y"; + return axis === "y" ? this.scrollContentEl : this.contentEl; + }, + /** + * Getter for content element + */ + }, + { + key: "getContentElement", + value: function getContentElement() { + return this.contentEl; + }, + }, + { + key: "removeListeners", + value: function removeListeners() { + // Event listeners + if (this.options.autoHide) { + this.el.removeEventListener("mouseenter", this.onMouseEnter); + } + + this.scrollContentEl.removeEventListener("scroll", this.onScrollY); + this.contentEl.removeEventListener("scroll", this.onScrollX); + this.mutationObserver.disconnect(); + this.resizeObserver.disconnect(); + }, + /** + * UnMount mutation observer and delete SimpleBar instance from DOM element + */ + }, + { + key: "unMount", + value: function unMount() { + this.removeListeners(); + this.el.SimpleBar = null; + }, + /** + * Recursively walks up the parent nodes looking for this.el + */ + }, + { + key: "isChildNode", + value: function isChildNode(el) { + if (el === null) return false; + if (el === this.el) return true; + return this.isChildNode(el.parentNode); + }, + /** + * Check if mouse is within bounds + */ + }, + { + key: "isWithinBounds", + value: function isWithinBounds(bbox) { + return ( + this.mouseX >= bbox.left && + this.mouseX <= bbox.left + bbox.width && + this.mouseY >= bbox.top && + this.mouseY <= bbox.top + bbox.height + ); + }, + }, + ], + [ + { + key: "initHtmlApi", + value: function initHtmlApi() { + this.initDOMLoadedElements = this.initDOMLoadedElements.bind(this); // MutationObserver is IE11+ + + if (typeof MutationObserver !== "undefined") { + // Mutation observer to observe dynamically added elements + this.globalObserver = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + Array.from(mutation.addedNodes).forEach((addedNode) => { + if (addedNode.nodeType === 1) { + if (addedNode.hasAttribute("data-simplebar")) { + !addedNode.SimpleBar && + new SimpleBar( + addedNode, + SimpleBar.getElOptions(addedNode) + ); + } else { + Array.from( + addedNode.querySelectorAll("[data-simplebar]") + ).forEach((el) => { + !el.SimpleBar && + new SimpleBar(el, SimpleBar.getElOptions(el)); + }); + } + } + }); + Array.from(mutation.removedNodes).forEach((removedNode) => { + if (removedNode.nodeType === 1) { + if (removedNode.hasAttribute("data-simplebar")) { + removedNode.SimpleBar?.unMount(); + } else { + Array.from( + removedNode.querySelectorAll("[data-simplebar]") + ).forEach((el) => { + el.SimpleBar?.unMount(); + }); + } + } + }); + }); + }); + this.globalObserver.observe(document, { + childList: true, + subtree: true, + }); + } // Taken from jQuery `ready` function + // Instantiate elements already present on the page + + if ( + document.readyState === "complete" || + (document.readyState !== "loading" && + !document.documentElement.doScroll) + ) { + // Handle it asynchronously to allow scripts the opportunity to delay init + window.setTimeout(this.initDOMLoadedElements); + } else { + document.addEventListener( + "DOMContentLoaded", + this.initDOMLoadedElements + ); + window.addEventListener("load", this.initDOMLoadedElements); + } + }, // Helper function to retrieve options from element attributes + }, + { + key: "getElOptions", + value: function getElOptions(el) { + const options = Array.from(el.attributes).reduce( + (acc, attribute) => { + const option = attribute.name.match(/data-simplebar-(.+)/); + + if (option) { + const key = option[1].replace(/\W+(.)/g, (_x, chr) => + chr.toUpperCase() + ); + + switch (attribute.value) { + case "true": + acc[key] = true; + break; + + case "false": + acc[key] = false; + break; + + case undefined: + acc[key] = true; + break; + + default: + acc[key] = attribute.value; + } + } + + return acc; + }, + {} + ); + return options; + }, + }, + { + key: "removeObserver", + value: function removeObserver() { + this.globalObserver.disconnect(); + }, + }, + { + key: "initDOMLoadedElements", + value: function initDOMLoadedElements() { + document.removeEventListener( + "DOMContentLoaded", + this.initDOMLoadedElements + ); + window.removeEventListener("load", this.initDOMLoadedElements); + Array.from(document.querySelectorAll("[data-simplebar]")).forEach( + (el) => { + if (!el.SimpleBar) + new SimpleBar(el, SimpleBar.getElOptions(el)); + } + ); + }, + }, + { + key: "defaultOptions", + get: function get() { + return { + autoHide: true, + forceVisible: false, + classNames: { + content: "simplebar-content", + scrollContent: "simplebar-scroll-content", + scrollbar: "simplebar-scrollbar", + track: "simplebar-track", + }, + scrollbarMinSize: 25, + scrollbarMaxSize: 0, + direction: "ltr", + timeout: 1000, + }; + }, + }, + ] + ); + + return SimpleBar; + })(); + +if (canUseDom) { + SimpleBar.initHtmlApi(); +} + +export default SimpleBar; diff --git a/crawl-ref/source/webserver/client/game/src/debug.js b/crawl-ref/source/webserver/client/game/src/debug.js index 51ff304ffdb..58182bf471b 100644 --- a/crawl-ref/source/webserver/client/game/src/debug.js +++ b/crawl-ref/source/webserver/client/game/src/debug.js @@ -1,56 +1,52 @@ -define(["jquery", "client", "./dungeon_renderer", "./minimap", - "./monster_list", "./map_knowledge", "./enums", "./display", "exports", - "contrib/jquery.json"], -function ($, c, r, mm, ml, mk, enums, display, exports) { - "use strict"; - - exports.client = c; - exports.renderer = r; - exports.$ = $; - exports.minimap = mm; - exports.monster_list = ml; - exports.map_knowledge = mk; - exports.display = display; - - window.debug = exports; - - // Debug helper - exports.mark_cell = function (x, y, mark) - { - mark = mark || "m"; - - if (mk.get(x, y).t) - mk.get(x, y).t.mark = mark; - - r.render_loc(x, y); - } - exports.unmark_cell = function (x, y) - { - var cell = mk.get(x, y); - if (cell) - { - delete cell.t.mark; - } - - r.render_loc(x, y); - } - exports.mark_all = function () - { - var view = r.view; - for (var x = 0; x < r.cols; x++) - for (var y = 0; y < r.rows; y++) - mark_cell(view.x + x, view.y + y, (view.x + x) + "/" + (view.y + y)); - } - exports.unmark_all = function () - { - var view = r.view; - for (var x = 0; x < r.cols; x++) - for (var y = 0; y < r.rows; y++) - unmark_cell(view.x + x, view.y + y); - } - - exports.obj_to_str = function (o) - { - return $.toJSON(o); - } -}); +import $ from "jquery"; + +import c from "./client"; +import r from "./dungeon_renderer"; +import mm from "./minimap"; +import ml from "./monster_list"; +import mk from "./map_knowledge"; +import display from "./display"; + +const exports = {}; + +exports.client = c; +exports.renderer = r; +exports.$ = $; +exports.minimap = mm; +exports.monster_list = ml; +exports.map_knowledge = mk; +exports.display = display; + +window.debug = exports; + +// Debug helper +exports.mark_cell = (x, y, mark) => { + mark = mark || "m"; + + if (mk.get(x, y).t) mk.get(x, y).t.mark = mark; + + r.render_loc(x, y); +}; +exports.unmark_cell = (x, y) => { + const cell = mk.get(x, y); + if (cell) { + delete cell.t.mark; + } + + r.render_loc(x, y); +}; +exports.mark_all = () => { + const view = r.view; + for (let x = 0; x < r.cols; x++) + for (let y = 0; y < r.rows; y++) + mark_cell(view.x + x, view.y + y, `${view.x + x}/${view.y + y}`); +}; +exports.unmark_all = () => { + const view = r.view; + for (let x = 0; x < r.cols; x++) + for (let y = 0; y < r.rows; y++) unmark_cell(view.x + x, view.y + y); +}; + +exports.obj_to_str = (o) => $.toJSON(o); + +export default exports; diff --git a/crawl-ref/source/webserver/client/game/src/display.js b/crawl-ref/source/webserver/client/game/src/display.js index a834ab7451c..247cbc76f97 100644 --- a/crawl-ref/source/webserver/client/game/src/display.js +++ b/crawl-ref/source/webserver/client/game/src/display.js @@ -1,118 +1,102 @@ -define(["jquery", "comm", "./map_knowledge", "./view_data", "./monster_list", - "./minimap", "./dungeon_renderer"], -function ($, comm, map_knowledge, view_data, monster_list, minimap, - dungeon_renderer) { - "use strict"; - - function invalidate(minimap_too) - { - var b = map_knowledge.bounds(); - if (!b) return; - - var view = dungeon_renderer.view; - - var xs = minimap_too ? b.left : view.x; - var ys = minimap_too ? b.top : view.y; - var xe = minimap_too ? b.right : view.x + dungeon_renderer.cols - 1; - var ye = minimap_too ? b.bottom : view.y + dungeon_renderer.rows - 1; - for (var x = xs; x <= xe; x++) - for (var y = ys; y <= ye; y++) - { - map_knowledge.touch(x, y); - } - } +import $ from "jquery"; - function display() - { - // Update the display. - if (!map_knowledge.bounds()) - return; - - var t1 = new Date(); - - if (map_knowledge.reset_bounds_changed()) - minimap.center(); - - var dirty_locs = map_knowledge.dirty(); - for (var i = 0; i < dirty_locs.length; i++) - { - var loc = dirty_locs[i]; - var cell = map_knowledge.get(loc.x, loc.y); - cell.dirty = false; - monster_list.update_loc(loc); - dungeon_renderer.render_loc(loc.x, loc.y, cell); - minimap.update(loc.x, loc.y, cell); - } - map_knowledge.reset_dirty(); - - dungeon_renderer.animate(); - - monster_list.update(); - - var render_time = (new Date() - t1); - if (!window.render_times) - window.render_times = []; - if (window.render_times.length >= 20) - window.render_times.shift(); - window.render_times.push(render_time); - } +import comm from "./comm"; - function clear_map() - { - map_knowledge.clear(); +import map_knowledge from "./map_knowledge"; +import monster_list from "./monster_list"; +import minimap from "./minimap"; +import dungeon_renderer from "./dungeon_renderer"; - dungeon_renderer.clear(); +function invalidate(minimap_too) { + const b = map_knowledge.bounds(); + if (!b) return; - minimap.clear(); + const view = dungeon_renderer.view; - monster_list.clear(); + const xs = minimap_too ? b.left : view.x; + const ys = minimap_too ? b.top : view.y; + const xe = minimap_too ? b.right : view.x + dungeon_renderer.cols - 1; + const ye = minimap_too ? b.bottom : view.y + dungeon_renderer.rows - 1; + for (let x = xs; x <= xe; x++) + for (let y = ys; y <= ye; y++) { + map_knowledge.touch(x, y); } +} - // Message handlers - function handle_map_message(data) - { - if (data.clear) - clear_map(); - - if (data.player_on_level != null) - map_knowledge.set_player_on_level(data.player_on_level); - - if (data.vgrdc) - minimap.do_view_center_update(data.vgrdc.x, data.vgrdc.y); - - if (data.cells) - map_knowledge.merge(data.cells); - - // Mark cells overlapped by dirty cells as dirty - $.each(map_knowledge.dirty().slice(), function (i, loc) { - var cell = map_knowledge.get(loc.x, loc.y); - // high cell - if (cell.t && cell.t.sy && cell.t.sy < 0) - map_knowledge.touch(loc.x, loc.y - 1); - // left overlap - if (cell.t && cell.t.left_overlap && cell.t.left_overlap < 0) - { - map_knowledge.touch(loc.x - 1, loc.y); - // If we overlap at both top *and* left, we may additionally - // overlap diagonally. - if (cell.t.sy && cell.t.sy < 0) - map_knowledge.touch(loc.x - 1, loc.y - 1); - } - }); - - display(); - } +function display() { + // Update the display. + if (!map_knowledge.bounds()) return; + + const t1 = new Date(); + + if (map_knowledge.reset_bounds_changed()) minimap.center(); + + const dirty_locs = map_knowledge.dirty(); + for (let i = 0; i < dirty_locs.length; i++) { + const loc = dirty_locs[i]; + const cell = map_knowledge.get(loc.x, loc.y); + cell.dirty = false; + monster_list.update_loc(loc); + dungeon_renderer.render_loc(loc.x, loc.y, cell); + minimap.update(loc.x, loc.y, cell); + } + map_knowledge.reset_dirty(); + + dungeon_renderer.animate(); + + monster_list.update(); + + const render_time = Date.now() - t1; + if (!window.render_times) window.render_times = []; + if (window.render_times.length >= 20) window.render_times.shift(); + window.render_times.push(render_time); +} + +function clear_map() { + map_knowledge.clear(); - function handle_vgrdc(data) - { + dungeon_renderer.clear(); + + minimap.clear(); + + monster_list.clear(); +} + +// Message handlers +function handle_map_message(data) { + if (data.clear) clear_map(); + + if (data.player_on_level != null) + map_knowledge.set_player_on_level(data.player_on_level); + + if (data.vgrdc) minimap.do_view_center_update(data.vgrdc.x, data.vgrdc.y); + + if (data.cells) map_knowledge.merge(data.cells); + + // Mark cells overlapped by dirty cells as dirty + $.each(map_knowledge.dirty().slice(), (_i, loc) => { + const cell = map_knowledge.get(loc.x, loc.y); + // high cell + if (cell.t?.sy && cell.t.sy < 0) map_knowledge.touch(loc.x, loc.y - 1); + // left overlap + if (cell.t?.left_overlap && cell.t.left_overlap < 0) { + map_knowledge.touch(loc.x - 1, loc.y); + // If we overlap at both top *and* left, we may additionally + // overlap diagonally. + if (cell.t.sy && cell.t.sy < 0) map_knowledge.touch(loc.x - 1, loc.y - 1); } + }); - comm.register_handlers({ - "map": handle_map_message, - }); + display(); +} - return { - invalidate: invalidate, - display: display, - }; +function _handle_vgrdc(_data) {} + +comm.register_handlers({ + map: handle_map_message, }); + +export default { + invalidate: invalidate, + display: display, +}; diff --git a/crawl-ref/source/webserver/client/game/src/dungeon_renderer.js b/crawl-ref/source/webserver/client/game/src/dungeon_renderer.js index e926162fc82..d86d759a6b0 100644 --- a/crawl-ref/source/webserver/client/game/src/dungeon_renderer.js +++ b/crawl-ref/source/webserver/client/game/src/dungeon_renderer.js @@ -1,461 +1,441 @@ -define(["jquery", "comm", "./cell_renderer", "./map_knowledge", "./options", - "./tileinfo-dngn", "./util", "./view_data", "./enums", "./mouse_control"], -function ($, comm, cr, map_knowledge, options, dngn, util, view_data, enums, - mouse_control) { - "use strict"; - var global_anim_counter = 0; - - function is_torch(basetile) - { - return (basetile == dngn.WALL_BRICK_DARK_2_TORCH - || basetile == dngn.WALL_BRICK_DARK_4_TORCH - || basetile == dngn.WALL_BRICK_DARK_6_TORCH); - } - - function cell_is_animated(cell) - { - if (cell == null || cell.bg == null) return false; - var base_bg = dngn.basetile(cell.bg.value); - if (base_bg >= dngn.DNGN_LAVA && base_bg < dngn.FLOOR_MAX) - return options.get("tile_water_anim"); - else if ((base_bg >= dngn.DNGN_ENTER_ZOT_CLOSED && base_bg < dngn.DNGN_CACHE_OF_FRUIT) - || (base_bg >= dngn.DNGN_SILVER_STATUE && base_bg < dngn.ARCANE_CONDUIT) - || (base_bg >= dngn.ARCANE_CONDUIT && base_bg < dngn.STORM_CONDUIT) - || (base_bg >= dngn.WALL_STONE_CRACKLE_1 && base_bg <= dngn.WALL_STONE_CRACKLE_4) - || is_torch(base_bg) || base_bg == dngn.DNGN_TRAP_HARLEQUIN) - { - return options.get("tile_misc_anim"); +import $ from "jquery"; + +import comm from "./comm"; + +import cr from "./cell_renderer"; +import map_knowledge from "./map_knowledge"; +import options from "./options"; +import dngn from "../../../game_data/static/tileinfo-dngn"; +import util from "./util"; +import view_data from "./view_data"; +import enums from "./enums"; + +let global_anim_counter = 0; + +function is_torch(basetile) { + return ( + basetile === dngn.WALL_BRICK_DARK_2_TORCH || + basetile === dngn.WALL_BRICK_DARK_4_TORCH || + basetile === dngn.WALL_BRICK_DARK_6_TORCH + ); +} + +function cell_is_animated(cell) { + if (cell == null || cell.bg == null) return false; + const base_bg = dngn.basetile(cell.bg.value); + if (base_bg >= dngn.DNGN_LAVA && base_bg < dngn.FLOOR_MAX) + return options.get("tile_water_anim"); + else if ( + (base_bg >= dngn.DNGN_ENTER_ZOT_CLOSED && + base_bg < dngn.DNGN_CACHE_OF_FRUIT) || + (base_bg >= dngn.DNGN_SILVER_STATUE && base_bg < dngn.ARCANE_CONDUIT) || + (base_bg >= dngn.ARCANE_CONDUIT && base_bg < dngn.STORM_CONDUIT) || + (base_bg >= dngn.WALL_STONE_CRACKLE_1 && + base_bg <= dngn.WALL_STONE_CRACKLE_4) || + is_torch(base_bg) || + base_bg === dngn.DNGN_TRAP_HARLEQUIN + ) { + return options.get("tile_misc_anim"); + } else return false; +} + +function animate_cell(cell) { + const base_bg = dngn.basetile(cell.bg.value); + if ( + base_bg === dngn.DNGN_PORTAL_WIZARD_LAB || + base_bg === dngn.DNGN_EXIT_NECROPOLIS || + base_bg === dngn.DNGN_ALTAR_JIYVA || + base_bg === dngn.DNGN_TRAP_HARLEQUIN || + (base_bg >= dngn.ARCANE_CONDUIT && base_bg < dngn.STORM_CONDUIT) || + is_torch(base_bg) + ) { + cell.bg.value = + base_bg + ((cell.bg.value - base_bg + 1) % dngn.tile_count(base_bg)); + } else if ( + (base_bg > dngn.DNGN_LAVA && base_bg < dngn.BLOOD) || + (base_bg >= dngn.WALL_STONE_CRACKLE_1 && + base_bg <= dngn.WALL_STONE_CRACKLE_4) || + (base_bg >= dngn.DNGN_SILVER_STATUE && base_bg < dngn.ARCANE_CONDUIT) + ) { + cell.bg.value = + base_bg + Math.floor(Math.random() * dngn.tile_count(base_bg)); + } else if (base_bg === dngn.DNGN_LAVA) { + const tile = (cell.bg.value - base_bg) % 4; + cell.bg.value = + base_bg + ((tile + 4 * global_anim_counter) % dngn.tile_count(base_bg)); + } +} + +function DungeonViewRenderer() { + cr.DungeonCellRenderer.call(this); + + this.cols = 0; + this.rows = 0; + + this.view = { x: 0, y: 0 }; + this.view_center = { x: 0, y: 0 }; + this.ui_state = -1; + this.last_sent_cursor = { x: 0, y: 0 }; +} + +DungeonViewRenderer.prototype = new cr.DungeonCellRenderer(); + +$.extend(DungeonViewRenderer.prototype, { + init: function (element) { + $(element) + .off("update_cells mousemove mouseleave mousedown") + .on("update_cells", (_ev, cells) => { + $.each(cells, (_i, loc) => { + this.render_loc(loc.x, loc.y); + }); + }) + .on("mousemove mouseleave mousedown", (ev) => { + this.handle_mouse(ev); + }); + + cr.DungeonCellRenderer.prototype.init.call(this, element); + }, + + handle_mouse: function (ev) { + if (!options.get("tile_web_mouse_control")) return; + if (ev.type === "mouseleave") { + if (this.tooltip_timeout) { + clearTimeout(this.tooltip_timeout); + this.tooltip_timeout = null; + } + + view_data.remove_cursor(enums.CURSOR_MOUSE); + } else { + // convert logical mouse coordinates to device coordinates + const scaled = this.scaled_size(ev.clientX, ev.clientY); + const cell_size = this.scaled_size(); + const loc = { + x: Math.floor(scaled.width / cell_size.width) + this.view.x, + y: Math.floor(scaled.height / cell_size.height) + this.view.y, + }; + + view_data.place_cursor(enums.CURSOR_MOUSE, loc); + + if (game.can_target()) { + // XX refactor into mouse_control.js? + if ( + loc.x !== this.last_sent_cursor.x || + loc.y !== this.last_sent_cursor.y + ) { + this.last_sent_cursor = { x: loc.x, y: loc.y }; + comm.send_message("target_cursor", this.last_sent_cursor); } - else - return false; + } + + if (ev.type === "mousemove") { + if (this.tooltip_timeout) clearTimeout(this.tooltip_timeout); + + const element = this.element; + this.tooltip_timeout = setTimeout(() => { + const new_ev = $.extend({}, ev, { + type: "cell_tooltip", + cell: loc, + }); + $(element).trigger(new_ev); + }, 500); + } else if (ev.type === "mousedown") { + const new_ev = $.extend({}, ev, { + type: "cell_click", + cell: loc, + }); + $(this.element).trigger(new_ev); + } } - - function animate_cell(cell) - { - var base_bg = dngn.basetile(cell.bg.value); - if (base_bg == dngn.DNGN_PORTAL_WIZARD_LAB - || base_bg == dngn.DNGN_EXIT_NECROPOLIS - || base_bg == dngn.DNGN_ALTAR_JIYVA - || base_bg == dngn.DNGN_TRAP_HARLEQUIN - || base_bg >= dngn.ARCANE_CONDUIT && base_bg < dngn.STORM_CONDUIT - || is_torch(base_bg)) - { - cell.bg.value = base_bg + (cell.bg.value - base_bg + 1) % dngn.tile_count(base_bg); - } - else if ((base_bg > dngn.DNGN_LAVA && base_bg < dngn.BLOOD) || - (base_bg >= dngn.WALL_STONE_CRACKLE_1 && base_bg <= dngn.WALL_STONE_CRACKLE_4) || - (base_bg >= dngn.DNGN_SILVER_STATUE && base_bg < dngn.ARCANE_CONDUIT)) - { - cell.bg.value = base_bg + Math.floor(Math.random() * dngn.tile_count(base_bg)) - } - else if (base_bg == dngn.DNGN_LAVA) - { - var tile = (cell.bg.value - base_bg) % 4; - cell.bg.value = base_bg + (tile + 4 * global_anim_counter) % dngn.tile_count(base_bg); - } + }, + + set_size: function (c, r) { + if ( + this.cols === c && + this.rows === r && + this.element.width === c * this.cell_width && + this.element.height === r * this.cell_height + ) + return; + + this.cols = c; + this.rows = r; + this.view.x = this.view_center.x - Math.floor(c / 2); + this.view.y = this.view_center.y - Math.floor(r / 2); + util.init_canvas(this.element, c * this.cell_width, r * this.cell_height); + this.init(this.element); + this.clear(); // clear the canvas and override default alpha fill + }, + + set_view_center: function (x, y) { + this.view_center.x = x; + this.view_center.y = y; + const old_view = this.view; + this.view = { + x: x - Math.floor(this.cols / 2), + y: y - Math.floor(this.rows / 2), + }; + this.shift(this.view.x - old_view.x, this.view.y - old_view.y); + }, + + // convert grid coordinates/dimensions to scaled canvas coordinates + // by default, use map grid coordinates; set screen_grid to instead + // use the visible grid (with 0,0 in the upper left of the screen) + canvas_coords: function (cx, cy, cw, ch, screen_grid) { + const scaled_cell = this.scaled_size(); + cw = cw || 1; + ch = ch || 1; + cx = cx || 0; + cy = cy || 0; + if (!screen_grid) { + cx -= this.view.x; + cy -= this.view.y; + } + return { + x: cx * scaled_cell.width, + y: cy * scaled_cell.height, + width: cw * scaled_cell.width, + height: ch * scaled_cell.height, + }; + }, + + // Shifts the dungeon view by cx/cy cells. + shift: function (x, y) { + if (x === 0 && y === 0) return; + + if (x > this.cols) x = this.cols; + if (x < -this.cols) x = -this.cols; + if (y > this.rows) y = this.rows; + if (y < -this.rows) y = -this.rows; + + let sx, sy, dx, dy; + + if (x > 0) { + sx = x; + dx = 0; + } else { + sx = 0; + dx = -x; + } + if (y > 0) { + sy = y; + dy = 0; + } else { + sy = 0; + dy = -y; } - function DungeonViewRenderer() - { - cr.DungeonCellRenderer.call(this); + const cw = this.cols - Math.abs(x); + const ch = this.rows - Math.abs(y); + + const source = this.canvas_coords(sx, sy, cw, ch, true); + + if (source.width > 0 && source.height > 0) { + // copy from source to dest + // written a bit over-generally: don't really need to + // recalculate destination size, unless this function is + // ever used for rescaling + const dest = this.canvas_coords(dx, dy, cw, ch, true); + this.ctx.drawImage( + this.element, + source.x, + source.y, + source.width, + source.height, + dest.x, + dest.y, + dest.width, + dest.height + ); + } - this.cols = 0; - this.rows = 0; + // Render cells that came into view + for (let cy = 0; cy < dy; cy++) + for (let cx = 0; cx < this.cols; cx++) + this.render_grid_cell(cx + this.view.x, cy + this.view.y); - this.view = { x: 0, y: 0 }; - this.view_center = { x: 0, y: 0 }; - this.ui_state = -1; - this.last_sent_cursor = { x: 0, y: 0 }; + for (let cy = dy; cy < this.rows - sy; cy++) { + for (let cx = 0; cx < dx; cx++) + this.render_grid_cell(cx + this.view.x, cy + this.view.y); + for (let cx = this.cols - sx; cx < this.cols; cx++) + this.render_grid_cell(cx + this.view.x, cy + this.view.y); } - DungeonViewRenderer.prototype = new cr.DungeonCellRenderer(); - - $.extend(DungeonViewRenderer.prototype, { - init: function (element) - { - var renderer = this; - $(element) - .off("update_cells mousemove mouseleave mousedown") - .on("update_cells", function (ev, cells) { - $.each(cells, function (i, loc) { - renderer.render_loc(loc.x, loc.y); - }); - }) - .on("mousemove mouseleave mousedown", function (ev) { - renderer.handle_mouse(ev); - }) - - cr.DungeonCellRenderer.prototype.init.call(this, element); - }, - - handle_mouse: function (ev) - { - if (!options.get("tile_web_mouse_control")) - return; - if (ev.type === "mouseleave") - { - if (this.tooltip_timeout) - { - clearTimeout(this.tooltip_timeout); - this.tooltip_timeout = null; - } - - view_data.remove_cursor(enums.CURSOR_MOUSE); - } - else - { - // convert logical mouse coordinates to device coordinates - let scaled = this.scaled_size(ev.clientX, ev.clientY); - let cell_size = this.scaled_size(); - var loc = { - x: Math.floor(scaled.width / cell_size.width) + this.view.x, - y: Math.floor(scaled.height / cell_size.height) + this.view.y - }; - - view_data.place_cursor(enums.CURSOR_MOUSE, loc); - - if (game.can_target()) - { - // XX refactor into mouse_control.js? - if (loc.x != this.last_sent_cursor.x - || loc.y != this.last_sent_cursor.y) - { - this.last_sent_cursor = {x: loc.x, y: loc.y}; - comm.send_message("target_cursor", - this.last_sent_cursor); - } - } - - if (ev.type === "mousemove") - { - - if (this.tooltip_timeout) - clearTimeout(this.tooltip_timeout); - - var element = this.element; - this.tooltip_timeout = setTimeout(function () { - var new_ev = $.extend({}, ev, { - type: "cell_tooltip", - cell: loc - }); - $(element).trigger(new_ev); - }, 500); - } - else if (ev.type === "mousedown") - { - var new_ev = $.extend({}, ev, { - type: "cell_click", - cell: loc - }); - $(this.element).trigger(new_ev); - } - } - }, - - set_size: function (c, r) - { - if ((this.cols == c) && (this.rows == r) && - (this.element.width == c * this.cell_width) && - (this.element.height == r * this.cell_height)) - return; - - this.cols = c; - this.rows = r; - this.view.x = this.view_center.x - Math.floor(c / 2); - this.view.y = this.view_center.y - Math.floor(r / 2); - util.init_canvas(this.element, - c * this.cell_width, - r * this.cell_height); - this.init(this.element); - this.clear(); // clear the canvas and override default alpha fill - }, - - set_view_center: function(x, y) - { - this.view_center.x = x; - this.view_center.y = y; - var old_view = this.view; - this.view = { - x: x - Math.floor(this.cols / 2), - y: y - Math.floor(this.rows / 2) - }; - this.shift(this.view.x - old_view.x, this.view.y - old_view.y); - }, - - // convert grid coordinates/dimensions to scaled canvas coordinates - // by default, use map grid coordinates; set screen_grid to instead - // use the visible grid (with 0,0 in the upper left of the screen) - canvas_coords: function(cx, cy, cw, ch, screen_grid) - { - let scaled_cell = this.scaled_size(); - cw = cw || 1; - ch = ch || 1; - cx = cx || 0; - cy = cy || 0; - if (!screen_grid) - { - cx -= this.view.x; - cy -= this.view.y; - } - return { - x: cx * scaled_cell.width, - y: cy * scaled_cell.height, - width: cw * scaled_cell.width, - height: ch * scaled_cell.height - }; - }, - - // Shifts the dungeon view by cx/cy cells. - shift: function(x, y) - { - if ((x == 0) && (y == 0)) - return; - - if (x > this.cols) x = this.cols; - if (x < -this.cols) x = -this.cols; - if (y > this.rows) y = this.rows; - if (y < -this.rows) y = -this.rows; - - var sx, sy, dx, dy; - - if (x > 0) - { - sx = x; - dx = 0; - } - else - { - sx = 0; - dx = -x; - } - if (y > 0) - { - sy = y; - dy = 0; - } - else - { - sy = 0; - dy = -y; - } - - const cw = this.cols - Math.abs(x); - const ch = this.rows - Math.abs(y); - - let source = this.canvas_coords(sx, sy, cw, ch, true); - - if (source.width > 0 && source.height > 0) - { - // copy from source to dest - // written a bit over-generally: don't really need to - // recalculate destination size, unless this function is - // ever used for rescaling - let dest = this.canvas_coords(dx, dy, cw, ch, true); - this.ctx.drawImage(this.element, - source.x, source.y, source.width, source.height, - dest.x, dest.y, dest.width, dest.height); - } - - // Render cells that came into view - for (var cy = 0; cy < dy; cy++) - for (var cx = 0; cx < this.cols; cx++) - this.render_grid_cell(cx + this.view.x, cy + this.view.y); - - for (var cy = dy; cy < this.rows - sy; cy++) - { - for (var cx = 0; cx < dx; cx++) - this.render_grid_cell(cx + this.view.x, cy + this.view.y); - for (var cx = this.cols - sx; cx < this.cols; cx++) - this.render_grid_cell(cx + this.view.x, cy + this.view.y); - } - - for (var cy = this.rows - sy; cy < this.rows; cy++) - for (var cx = 0; cx < this.cols; cx++) - this.render_grid_cell(cx + this.view.x, cy + this.view.y); - }, - - fit_to: function(width, height, min_diameter) - { - var scale; - if (this.ui_state == enums.ui.VIEW_MAP) - scale = options.get("tile_map_scale"); - else - scale = options.get("tile_viewport_scale"); - var tile_size = Math.floor(options.get("tile_cell_pixels") - * scale / 100); - var cell_size = { - w: Math.floor(tile_size), - h: Math.floor(tile_size) - }; - - if (options.get("tile_display_mode") == "glyphs") - { - // TODO: this should rescale to ensure los, similar to tiles - this.glyph_mode_update_font_metrics(); - this.set_cell_size(this.glyph_mode_font_width, - this.glyph_mode_line_height); - } - else if ((min_diameter * cell_size.w > width) - || (min_diameter * cell_size.h > height)) - { - // TODO: this produces bad results in hybrid mode, because the - // font size isn't appropriately updated - // scale down if necessary, so that los is in view - var rescale = Math.min(width / (min_diameter * cell_size.w), - height / (min_diameter * cell_size.h)); - this.set_cell_size(Math.floor(cell_size.w * rescale), - Math.floor(cell_size.h * rescale)); - } - else - this.set_cell_size(cell_size.w, cell_size.h); - - var view_width = Math.floor(width / this.cell_width); - var view_height = Math.floor(height / this.cell_height); - this.set_size(view_width, view_height); - }, - - in_view: function(cx, cy) - { - return (cx >= this.view.x) && (this.view.x + this.cols > cx) && - (cy >= this.view.y) && (this.view.y + this.rows > cy); - }, - - // render a single cell on the map grid - render_grid_cell: function(cx, cy, map_cell, cell) - { - if (!this.in_view(cx, cy)) - return; - map_cell = map_cell || map_knowledge.get(cx, cy); - cell = cell || map_cell.t; - let scaled = this.canvas_coords(cx, cy); - this.render_cell(cx, cy, scaled.x, scaled.y, map_cell, cell); - }, - - // render a cell on the map grid, recursing as necessary based on - // overlaps with adjacent cells below/right - render_loc: function(cx, cy, map_cell) - { - this.render_grid_cell(cx, cy, map_cell); - - // Redraw the cell below if it overlapped - if (this.in_view(cx, cy + 1)) - { - let cell_below = map_knowledge.get(cx, cy + 1); - if (cell_below.t && cell_below.t.sy && (cell_below.t.sy < 0)) - this.render_loc(cx, cy + 1, cell_below); - } - // Redraw the cell to the right if it overlapped - if (this.in_view(cx + 1, cy)) - { - let cell_right = map_knowledge.get(cx + 1, cy); - if (cell_right.t && cell_right.t.left_overlap - && (cell_right.t.left_overlap < 0)) - { - this.render_loc(cx + 1, cy, cell_right); - } - } - // And the cell to the bottom-right if both overlapped - if (this.in_view(cx + 1, cy + 1)) - { - let cell_diag = map_knowledge.get(cx + 1, cy + 1); - if (cell_diag.t && cell_diag.t.sy && (cell_diag.t.sy < 0) - && cell_diag.t.left_overlap && (cell_diag.t.left_overlap < 0)) - { - this.render_loc(cx + 1, cy + 1, cell_diag); - } - } - }, - - animate: function () - { - global_anim_counter++; - if (global_anim_counter >= 65536) global_anim_counter = 0; - - for (var cy = this.view.y; cy < this.view.y + this.rows; ++cy) - for (var cx = this.view.x; cx < this.view.x + this.cols; ++cx) - { - var map_cell = map_knowledge.get(cx, cy); - var cell = map_cell.t; - if (map_knowledge.visible(map_cell) && cell_is_animated(cell)) - { - animate_cell(cell); - this.render_grid_cell(cx, cy, map_cell, cell); - } - } - }, - - // This is mostly here so that it can inherit cell size - new_renderer: function(tiles) - { - tiles = tiles || []; - var renderer = new cr.DungeonCellRenderer(); - var canvas = $(""); - renderer.set_cell_size(this.cell_width, - this.cell_height); - util.init_canvas(canvas[0], this.cell_width, this.cell_height); - renderer.init(canvas[0]); - renderer.draw_tiles(tiles); - - return renderer; - }, - - set_ui_state: function(s) - { - this.ui_state = s; - }, - - update_mouse_mode: function (m) - { - if (!game.can_target()) - this.last_sent_cursor = { x: 0, y: 0 }; - }, - }); - - var renderer = new DungeonViewRenderer(); - var anim_interval = null; - - function update_animation_interval() - { - if (anim_interval) - { - clearTimeout(anim_interval); - anim_interval = null; - } - if (options.get("tile_realtime_anim")) - { - anim_interval = setInterval(function () { - renderer.animate(); - }, 1000 / 4); - } + for (let cy = this.rows - sy; cy < this.rows; cy++) + for (let cx = 0; cx < this.cols; cx++) + this.render_grid_cell(cx + this.view.x, cy + this.view.y); + }, + + fit_to: function (width, height, min_diameter) { + let scale; + if (this.ui_state === enums.ui.VIEW_MAP) + scale = options.get("tile_map_scale"); + else scale = options.get("tile_viewport_scale"); + const tile_size = Math.floor( + (options.get("tile_cell_pixels") * scale) / 100 + ); + const cell_size = { + w: Math.floor(tile_size), + h: Math.floor(tile_size), + }; + + if (options.get("tile_display_mode") === "glyphs") { + // TODO: this should rescale to ensure los, similar to tiles + this.glyph_mode_update_font_metrics(); + this.set_cell_size( + this.glyph_mode_font_width, + this.glyph_mode_line_height + ); + } else if ( + min_diameter * cell_size.w > width || + min_diameter * cell_size.h > height + ) { + // TODO: this produces bad results in hybrid mode, because the + // font size isn't appropriately updated + // scale down if necessary, so that los is in view + const rescale = Math.min( + width / (min_diameter * cell_size.w), + height / (min_diameter * cell_size.h) + ); + this.set_cell_size( + Math.floor(cell_size.w * rescale), + Math.floor(cell_size.h * rescale) + ); + } else this.set_cell_size(cell_size.w, cell_size.h); + + const view_width = Math.floor(width / this.cell_width); + const view_height = Math.floor(height / this.cell_height); + this.set_size(view_width, view_height); + }, + + in_view: function (cx, cy) { + return ( + cx >= this.view.x && + this.view.x + this.cols > cx && + cy >= this.view.y && + this.view.y + this.rows > cy + ); + }, + + // render a single cell on the map grid + render_grid_cell: function (cx, cy, map_cell, cell) { + if (!this.in_view(cx, cy)) return; + map_cell = map_cell || map_knowledge.get(cx, cy); + cell = cell || map_cell.t; + const scaled = this.canvas_coords(cx, cy); + this.render_cell(cx, cy, scaled.x, scaled.y, map_cell, cell); + }, + + // render a cell on the map grid, recursing as necessary based on + // overlaps with adjacent cells below/right + render_loc: function (cx, cy, map_cell) { + this.render_grid_cell(cx, cy, map_cell); + + // Redraw the cell below if it overlapped + if (this.in_view(cx, cy + 1)) { + const cell_below = map_knowledge.get(cx, cy + 1); + if (cell_below.t?.sy && cell_below.t.sy < 0) + this.render_loc(cx, cy + 1, cell_below); + } + // Redraw the cell to the right if it overlapped + if (this.in_view(cx + 1, cy)) { + const cell_right = map_knowledge.get(cx + 1, cy); + if (cell_right.t?.left_overlap && cell_right.t.left_overlap < 0) { + this.render_loc(cx + 1, cy, cell_right); + } } + // And the cell to the bottom-right if both overlapped + if (this.in_view(cx + 1, cy + 1)) { + const cell_diag = map_knowledge.get(cx + 1, cy + 1); + if ( + cell_diag.t?.sy && + cell_diag.t.sy < 0 && + cell_diag.t.left_overlap && + cell_diag.t.left_overlap < 0 + ) { + this.render_loc(cx + 1, cy + 1, cell_diag); + } + } + }, + + animate: function () { + global_anim_counter++; + if (global_anim_counter >= 65536) global_anim_counter = 0; + + for (let cy = this.view.y; cy < this.view.y + this.rows; ++cy) + for (let cx = this.view.x; cx < this.view.x + this.cols; ++cx) { + const map_cell = map_knowledge.get(cx, cy); + const cell = map_cell.t; + if (map_knowledge.visible(map_cell) && cell_is_animated(cell)) { + animate_cell(cell); + this.render_grid_cell(cx, cy, map_cell, cell); + } + } + }, + + // This is mostly here so that it can inherit cell size + new_renderer: function (tiles) { + tiles = tiles || []; + const renderer = new cr.DungeonCellRenderer(); + const canvas = $(""); + renderer.set_cell_size(this.cell_width, this.cell_height); + util.init_canvas(canvas[0], this.cell_width, this.cell_height); + renderer.init(canvas[0]); + renderer.draw_tiles(tiles); - options.add_listener(update_animation_interval); - - $(document).off("game_init.dungeon_renderer"); - $(document).on("game_init.dungeon_renderer", function () { - renderer.cols = 0; - renderer.rows = 0; - renderer.view = { x: 0, y: 0 }; - renderer.view_center = { x: 0, y: 0 }; - - renderer.init($("#dungeon")[0]); - }); - - $(document).off("game_cleanup.dungeon_renderer") - .on("game_cleanup.dungeon_renderer", function () { - if (anim_interval) - { - clearTimeout(anim_interval); - anim_interval = null; - } - }); + return renderer; + }, - /* Hack to show animations (if real-time ones are not enabled) - even in turns where nothing else happens */ - $(document).off("text_update.dungeon_renderer") - .on("text_update.dungeon_renderer", function () { - renderer.animate(); - }); + set_ui_state: function (s) { + this.ui_state = s; + }, - return renderer; + update_mouse_mode: function (_m) { + if (!game.can_target()) this.last_sent_cursor = { x: 0, y: 0 }; + }, }); + +const renderer = new DungeonViewRenderer(); +let anim_interval = null; + +function update_animation_interval() { + if (anim_interval) { + clearTimeout(anim_interval); + anim_interval = null; + } + if (options.get("tile_realtime_anim")) { + anim_interval = setInterval(() => { + renderer.animate(); + }, 1000 / 4); + } +} + +options.add_listener(update_animation_interval); + +$(document).off("game_init.dungeon_renderer"); +$(document).on("game_init.dungeon_renderer", () => { + renderer.cols = 0; + renderer.rows = 0; + renderer.view = { x: 0, y: 0 }; + renderer.view_center = { x: 0, y: 0 }; + + renderer.init($("#dungeon")[0]); +}); + +$(document) + .off("game_cleanup.dungeon_renderer") + .on("game_cleanup.dungeon_renderer", () => { + if (anim_interval) { + clearTimeout(anim_interval); + anim_interval = null; + } + }); + +/* Hack to show animations (if real-time ones are not enabled) + even in turns where nothing else happens */ +$(document) + .off("text_update.dungeon_renderer") + .on("text_update.dungeon_renderer", () => { + renderer.animate(); + }); + +export default renderer; diff --git a/crawl-ref/source/webserver/client/game/src/enums.js b/crawl-ref/source/webserver/client/game/src/enums.js index 35c8a176f11..3fe9bc87aee 100644 --- a/crawl-ref/source/webserver/client/game/src/enums.js +++ b/crawl-ref/source/webserver/client/game/src/enums.js @@ -1,330 +1,305 @@ // TODO: Generate this automatically from enum.h? -define(function () { - "use strict"; - - var exports = {}, val; - // Various constants - exports.gxm = 80; - exports.gym = 70; - exports.stat_width = 42; - - // UI States (tileweb.h) - exports.ui = {}; - exports.ui.NORMAL = 0; - exports.ui.CRT = 1; - exports.ui.VIEW_MAP = 2; - - // Mouse modes - val = 0; - exports.mouse_mode = {}; - exports.mouse_mode.NORMAL = val++; - exports.mouse_mode.COMMAND = val++; - exports.mouse_mode.TARGET = val++; - exports.mouse_mode.TARGET_DIR = val++; - exports.mouse_mode.TARGET_PATH = val++; - exports.mouse_mode.MORE = val++; - exports.mouse_mode.MACRO = val++; - exports.mouse_mode.PROMPT = val++; - exports.mouse_mode.YESNO = val++; - exports.mouse_mode.MAX = val++; - - // Textures - val = 0; - exports.texture = {}; - exports.texture.FLOOR = val++; // floor.png - exports.texture.WALL = val++; // wall.png - exports.texture.FEAT = val++; // feat.png - exports.texture.PLAYER = val++; // player.png - exports.texture.DEFAULT = val++; // main.png - exports.texture.GUI = val++; // gui.png - exports.texture.ICONS = val++; // icons.png - - // Cursors - exports.CURSOR_MOUSE = 0; - exports.CURSOR_TUTORIAL = 1; - exports.CURSOR_MAP = 2; - exports.CURSOR_MAX = 3; - - // Halo flags - exports.HALO_NONE = 0; - exports.HALO_RANGE = 1; - exports.HALO_UMBRA_FIRST = 2; - exports.HALO_UMBRA_1 = exports.HALO_UMBRA_FIRST; - exports.HALO_UMBRA_2 = 3; - exports.HALO_UMBRA_3 = 4; - exports.HALO_UMBRA_4 = 5; - exports.HALO_UMBRA_LAST = exports.HALO_UMBRA_4; - - // Tile flags. - // Mostly this complicated because they need more than 32 bits. - - function array_and(arr1, arr2) - { - var result = []; - for (var i = 0; i < arr1.length && i < arr2.length; ++i) - { - result.push(arr1[i] & arr2[i]); - } - return result; - } - - function array_equal(arr1, arr2) - { - // return (arr1 <= arr2) && (arr1 >= arr2); - for (var i = 0; i < arr1.length || i < arr2.length; ++i) - { - if ((arr1[i] || 0) != (arr2[i] || 0)) - return false; - } - return true; - } - function array_nonzero(arr) - { - for (var i = 0; i < arr.length; ++i) - { - if (arr[i] != 0) return true; - } - return false; +let exports = {}, + val; +// Various constants +exports.gxm = 80; +exports.gym = 70; +exports.stat_width = 42; + +// UI States (tileweb.h) +exports.ui = {}; +exports.ui.NORMAL = 0; +exports.ui.CRT = 1; +exports.ui.VIEW_MAP = 2; + +// Mouse modes +val = 0; +exports.mouse_mode = {}; +exports.mouse_mode.NORMAL = val++; +exports.mouse_mode.COMMAND = val++; +exports.mouse_mode.TARGET = val++; +exports.mouse_mode.TARGET_DIR = val++; +exports.mouse_mode.TARGET_PATH = val++; +exports.mouse_mode.MORE = val++; +exports.mouse_mode.MACRO = val++; +exports.mouse_mode.PROMPT = val++; +exports.mouse_mode.YESNO = val++; +exports.mouse_mode.MAX = val++; + +// Textures +val = 0; +exports.texture = {}; +exports.texture.FLOOR = val++; // floor.png +exports.texture.WALL = val++; // wall.png +exports.texture.FEAT = val++; // feat.png +exports.texture.PLAYER = val++; // player.png +exports.texture.DEFAULT = val++; // main.png +exports.texture.GUI = val++; // gui.png +exports.texture.ICONS = val++; // icons.png + +// Cursors +exports.CURSOR_MOUSE = 0; +exports.CURSOR_TUTORIAL = 1; +exports.CURSOR_MAP = 2; +exports.CURSOR_MAX = 3; + +// Halo flags +exports.HALO_NONE = 0; +exports.HALO_RANGE = 1; +exports.HALO_UMBRA_FIRST = 2; +exports.HALO_UMBRA_1 = exports.HALO_UMBRA_FIRST; +exports.HALO_UMBRA_2 = 3; +exports.HALO_UMBRA_3 = 4; +exports.HALO_UMBRA_4 = 5; +exports.HALO_UMBRA_LAST = exports.HALO_UMBRA_4; + +// Tile flags. +// Mostly this complicated because they need more than 32 bits. + +function array_and(arr1, arr2) { + const result = []; + for (let i = 0; i < arr1.length && i < arr2.length; ++i) { + result.push(arr1[i] & arr2[i]); + } + return result; +} + +function array_equal(arr1, arr2) { + // return (arr1 <= arr2) && (arr1 >= arr2); + for (let i = 0; i < arr1.length || i < arr2.length; ++i) { + if ((arr1[i] || 0) !== (arr2[i] || 0)) return false; + } + return true; +} + +function array_nonzero(arr) { + for (let i = 0; i < arr.length; ++i) { + if (arr[i] !== 0) return true; + } + return false; +} + +function prepare_flags(tileidx, flagdata, cache) { + if (typeof tileidx === "number") tileidx = [tileidx]; + else if (tileidx.value !== undefined) return tileidx; + while (tileidx.length < 2) tileidx.push(0); + + if (cache[[tileidx[0], tileidx[1]]] !== undefined) + return cache[[tileidx[0], tileidx[1]]]; + + for (const flagname in flagdata.flags) { + const flagmask = flagdata.flags[flagname]; + if (Array.isArray(flagmask)) + tileidx[flagname] = array_nonzero(array_and(tileidx, flagmask)); + else tileidx[flagname] = (tileidx[0] & flagmask) !== 0; + } + + for (let i = 0; i < flagdata.exclusive_flags.length; ++i) { + const excl = flagdata.exclusive_flags[i]; + let val; + if (Array.isArray(excl.mask)) val = array_and(tileidx, excl.mask); + else val = [tileidx[0] & excl.mask]; + + for (const flagname in excl) { + if (flagname === "mask") continue; + if (Array.isArray(excl[flagname])) + tileidx[flagname] = array_equal(val, excl[flagname]); + else tileidx[flagname] = val[0] === excl[flagname]; } + } - function prepare_flags(tileidx, flagdata, cache) - { - if (!isNaN(tileidx)) - tileidx = [tileidx]; - else if (tileidx.value !== undefined) - return tileidx; - while (tileidx.length < 2) tileidx.push(0); - - if (cache[[tileidx[0],tileidx[1]]] !== undefined) - return cache[[tileidx[0],tileidx[1]]]; - - for (var flagname in flagdata.flags) - { - var flagmask = flagdata.flags[flagname]; - if (isNaN(flagmask)) - tileidx[flagname] = array_nonzero(array_and(tileidx, flagmask)); - else - tileidx[flagname] = (tileidx[0] & flagmask) != 0; - } - - for (var i = 0; i < flagdata.exclusive_flags.length; ++i) - { - var excl = flagdata.exclusive_flags[i]; - var val; - if (isNaN(excl.mask)) - val = array_and(tileidx, excl.mask); - else - val = [tileidx[0] & excl.mask]; - - for (var flagname in excl) - { - if (flagname === "mask") continue; - if (isNaN(excl[flagname])) - tileidx[flagname] = array_equal(val, excl[flagname]); - else - tileidx[flagname] = (val[0] == excl[flagname]); - } - } - - tileidx.value = tileidx[0] & flagdata.mask; - cache[[tileidx[0],tileidx[1]]] = tileidx; - cache.size++; - return tileidx; - } + tileidx.value = tileidx[0] & flagdata.mask; + cache[[tileidx[0], tileidx[1]]] = tileidx; + cache.size++; + return tileidx; +} - /* Hex literals are signed, so values with the highest bit set +/* Hex literals are signed, so values with the highest bit set would have to be written in 2-complement; this way is easier to read */ - var highbit = 1 << 31; - - // Foreground flags - - // 3 mutually exclusive flags for attitude. - var fg_flags = { flags: {}, exclusive_flags: [] }; - fg_flags.exclusive_flags.push({ - mask : 0x00030000, - PET : 0x00010000, - GD_NEUTRAL : 0x00020000, - NEUTRAL : 0x00030000, - }); - - fg_flags.flags.S_UNDER = 0x00040000; - fg_flags.flags.FLYING = 0x00080000; - - // 4 mutually exclusive flags for behaviour. - fg_flags.exclusive_flags.push({ - mask : 0x00700000, - STAB : 0x00100000, - MAY_STAB : 0x00200000, - FLEEING : 0x00300000, - PARALYSED : 0x00400000, - }); - - fg_flags.flags.NET = 0x00800000; - fg_flags.flags.WEB = 0x01000000; - - // Three levels of poison in 2 bits. - fg_flags.exclusive_flags.push({ - mask : [0, 0x18000000], - POISON : [0, 0x08000000], - MORE_POISON : [0, 0x10000000], - MAX_POISON : [0, 0x18000000] - }); - - // 5 mutually exclusive flags for threat level. - fg_flags.exclusive_flags.push({ - mask : [0, 0x60000000 | highbit], - TRIVIAL : [0, 0x20000000], - EASY : [0, 0x40000000], - TOUGH : [0, 0x60000000], - NASTY : [0, highbit], - UNUSUAL : [0, 0x60000000 | highbit], - }); - - fg_flags.flags.GHOST = [0, 0x00100000]; - - // MDAM has 5 possibilities, so uses 3 bits. - fg_flags.exclusive_flags.push({ - mask : [0x40000000 | highbit, 0x01], - MDAM_LIGHT : [0x40000000, 0x00], - MDAM_MOD : [highbit, 0x00], - MDAM_HEAVY : [0x40000000 | highbit, 0x00], - MDAM_SEV : [0x00000000, 0x01], - MDAM_ADEAD : [0x40000000 | highbit, 0x01], - }); - - // Demon difficulty has 5 possibilities, so uses 3 bits. - fg_flags.exclusive_flags.push({ - mask : [0, 0x0E], - DEMON_5 : [0, 0x02], - DEMON_4 : [0, 0x04], - DEMON_3 : [0, 0x06], - DEMON_2 : [0, 0x08], - DEMON_1 : [0, 0x0E], - }); - - fg_flags.mask = 0x0000FFFF; - - // Background flags - var bg_flags = { flags: {}, exclusive_flags: [] }; - bg_flags.flags.RAY = 0x00010000; - bg_flags.flags.MM_UNSEEN = 0x00020000; - bg_flags.flags.UNSEEN = 0x00040000; - bg_flags.exclusive_flags.push({ - mask : 0x00180000, - CURSOR1 : 0x00180000, - CURSOR2 : 0x00080000, - CURSOR3 : 0x00100000, - }); - bg_flags.flags.TUT_CURSOR = 0x00200000; - bg_flags.flags.TRAV_EXCL = 0x00400000; - bg_flags.flags.EXCL_CTR = 0x00800000; - bg_flags.flags.RAY_OOR = 0x01000000; - bg_flags.flags.OOR = 0x02000000; - bg_flags.flags.WATER = 0x04000000; - bg_flags.flags.NEW_STAIR = 0x08000000; - bg_flags.flags.NEW_TRANSPORTER = 0x10000000; - - // Kraken tentacle overlays. - bg_flags.flags.KRAKEN_NW = 0x20000000; - bg_flags.flags.KRAKEN_NE = 0x40000000; - bg_flags.flags.KRAKEN_SE = highbit; - bg_flags.flags.KRAKEN_SW = [0, 0x01]; - - bg_flags.flags.RAMPAGE = [0, 0x020]; - - bg_flags.flags.LANDING = [0, 0x200]; - bg_flags.flags.RAY_MULTI = [0, 0x400]; - bg_flags.mask = 0x0000FFFF; - - // Since the current flag implementation is really slow we use a trivial - // cache system for now. - var fg_cache = { size : 0 }; - exports.prepare_fg_flags = function (tileidx) - { - if (fg_cache.size >= 100) - fg_cache = { size : 0 }; - return prepare_flags(tileidx, fg_flags, fg_cache); - } - var bg_cache = { size : 0 }; - exports.prepare_bg_flags = function (tileidx) - { - if (bg_cache.size >= 250) - bg_cache = { size : 0 }; - return prepare_flags(tileidx, bg_flags, bg_cache); - } +const highbit = 1 << 31; + +// Foreground flags + +// 3 mutually exclusive flags for attitude. +const fg_flags = { flags: {}, exclusive_flags: [] }; +fg_flags.exclusive_flags.push({ + mask: 0x00030000, + PET: 0x00010000, + GD_NEUTRAL: 0x00020000, + NEUTRAL: 0x00030000, +}); + +fg_flags.flags.S_UNDER = 0x00040000; +fg_flags.flags.FLYING = 0x00080000; + +// 4 mutually exclusive flags for behaviour. +fg_flags.exclusive_flags.push({ + mask: 0x00700000, + STAB: 0x00100000, + MAY_STAB: 0x00200000, + FLEEING: 0x00300000, + PARALYSED: 0x00400000, +}); + +fg_flags.flags.NET = 0x00800000; +fg_flags.flags.WEB = 0x01000000; + +// Three levels of poison in 2 bits. +fg_flags.exclusive_flags.push({ + mask: [0, 0x18000000], + POISON: [0, 0x08000000], + MORE_POISON: [0, 0x10000000], + MAX_POISON: [0, 0x18000000], +}); + +// 5 mutually exclusive flags for threat level. +fg_flags.exclusive_flags.push({ + mask: [0, 0x60000000 | highbit], + TRIVIAL: [0, 0x20000000], + EASY: [0, 0x40000000], + TOUGH: [0, 0x60000000], + NASTY: [0, highbit], + UNUSUAL: [0, 0x60000000 | highbit], +}); + +fg_flags.flags.GHOST = [0, 0x00100000]; + +// MDAM has 5 possibilities, so uses 3 bits. +fg_flags.exclusive_flags.push({ + mask: [0x40000000 | highbit, 0x01], + MDAM_LIGHT: [0x40000000, 0x00], + MDAM_MOD: [highbit, 0x00], + MDAM_HEAVY: [0x40000000 | highbit, 0x00], + MDAM_SEV: [0x00000000, 0x01], + MDAM_ADEAD: [0x40000000 | highbit, 0x01], +}); + +// Demon difficulty has 5 possibilities, so uses 3 bits. +fg_flags.exclusive_flags.push({ + mask: [0, 0x0e], + DEMON_5: [0, 0x02], + DEMON_4: [0, 0x04], + DEMON_3: [0, 0x06], + DEMON_2: [0, 0x08], + DEMON_1: [0, 0x0e], +}); - // Menu flags -- see menu.h - // many things here are unimplemented - var mf = {}; - mf.NOSELECT = 0x0001; - mf.SINGLESELECT = 0x0002; - mf.MULTISELECT = 0x0004; - mf.SELECT_QTY = 0x0008; - mf.ANYPRINTABLE = 0x0010; - mf.SELECT_BY_PAGE = 0x0020; - mf.INIT_HOVER = 0x0040; - mf.WRAP = 0x0080; - mf.ALLOW_FILTER = 0x0100; - mf.ALLOW_FORMATTING = 0x0200; - mf.SHOW_PAGENUMBERS = 0x0400; - // ... - mf.START_AT_END = 0x1000; - mf.PRESELECTED = 0x2000; - // ... - mf.ARROWS_SELECT = 0x40000; - exports.menu_flag = mf; - - val = 0; - exports.CHATTR = {}; - exports.CHATTR.NORMAL = val++; - exports.CHATTR.STANDOUT = val++; - exports.CHATTR.BOLD = val++; - exports.CHATTR.BLINK = val++; - exports.CHATTR.UNDERLINE = val++; - exports.CHATTR.REVERSE = val++; - exports.CHATTR.DIM = val++; - exports.CHATTR.HILITE = val++; - exports.CHATTR.ATTRMASK = 0xF; - - // Minimap features - val = 0; - exports.MF_UNSEEN = val++; - exports.MF_FLOOR = val++; - exports.MF_WALL = val++; - exports.MF_MAP_FLOOR = val++; - exports.MF_MAP_WALL = val++; - exports.MF_DOOR = val++; - exports.MF_ITEM = val++; - exports.MF_MONS_FRIENDLY = val++; - exports.MF_MONS_PEACEFUL = val++; - exports.MF_MONS_NEUTRAL = val++; - exports.MF_MONS_HOSTILE = val++; - exports.MF_MONS_NO_EXP = val++; - exports.MF_STAIR_UP = val++; - exports.MF_STAIR_DOWN = val++; - exports.MF_STAIR_BRANCH = val++; - exports.MF_FEATURE = val++; - exports.MF_WATER = val++; - exports.MF_LAVA = val++; - exports.MF_TRAP = val++; - exports.MF_EXCL_ROOT = val++; - exports.MF_EXCL = val++; - exports.MF_PLAYER = val++; - exports.MF_DEEP_WATER = val++; - exports.MF_PORTAL = val++; - exports.MF_MAX = val++; - - exports.MF_SKIP = val++; - - exports.reverse_lookup = function (e, value) { - for (var prop in e) - { - if (e[prop] == value) - return prop; - } - }; - - return exports; +fg_flags.mask = 0x0000ffff; + +// Background flags +const bg_flags = { flags: {}, exclusive_flags: [] }; +bg_flags.flags.RAY = 0x00010000; +bg_flags.flags.MM_UNSEEN = 0x00020000; +bg_flags.flags.UNSEEN = 0x00040000; +bg_flags.exclusive_flags.push({ + mask: 0x00180000, + CURSOR1: 0x00180000, + CURSOR2: 0x00080000, + CURSOR3: 0x00100000, }); +bg_flags.flags.TUT_CURSOR = 0x00200000; +bg_flags.flags.TRAV_EXCL = 0x00400000; +bg_flags.flags.EXCL_CTR = 0x00800000; +bg_flags.flags.RAY_OOR = 0x01000000; +bg_flags.flags.OOR = 0x02000000; +bg_flags.flags.WATER = 0x04000000; +bg_flags.flags.NEW_STAIR = 0x08000000; +bg_flags.flags.NEW_TRANSPORTER = 0x10000000; + +// Kraken tentacle overlays. +bg_flags.flags.KRAKEN_NW = 0x20000000; +bg_flags.flags.KRAKEN_NE = 0x40000000; +bg_flags.flags.KRAKEN_SE = highbit; +bg_flags.flags.KRAKEN_SW = [0, 0x01]; + +bg_flags.flags.RAMPAGE = [0, 0x020]; + +bg_flags.flags.LANDING = [0, 0x200]; +bg_flags.flags.RAY_MULTI = [0, 0x400]; +bg_flags.mask = 0x0000ffff; + +// Since the current flag implementation is really slow we use a trivial +// cache system for now. +let fg_cache = { size: 0 }; +exports.prepare_fg_flags = (tileidx) => { + if (fg_cache.size >= 100) fg_cache = { size: 0 }; + return prepare_flags(tileidx, fg_flags, fg_cache); +}; +let bg_cache = { size: 0 }; +exports.prepare_bg_flags = (tileidx) => { + if (bg_cache.size >= 250) bg_cache = { size: 0 }; + return prepare_flags(tileidx, bg_flags, bg_cache); +}; + +// Menu flags -- see menu.h +// many things here are unimplemented +const mf = {}; +mf.NOSELECT = 0x0001; +mf.SINGLESELECT = 0x0002; +mf.MULTISELECT = 0x0004; +mf.SELECT_QTY = 0x0008; +mf.ANYPRINTABLE = 0x0010; +mf.SELECT_BY_PAGE = 0x0020; +mf.INIT_HOVER = 0x0040; +mf.WRAP = 0x0080; +mf.ALLOW_FILTER = 0x0100; +mf.ALLOW_FORMATTING = 0x0200; +mf.SHOW_PAGENUMBERS = 0x0400; +// ... +mf.START_AT_END = 0x1000; +mf.PRESELECTED = 0x2000; +// ... +mf.ARROWS_SELECT = 0x40000; +exports.menu_flag = mf; + +val = 0; +exports.CHATTR = {}; +exports.CHATTR.NORMAL = val++; +exports.CHATTR.STANDOUT = val++; +exports.CHATTR.BOLD = val++; +exports.CHATTR.BLINK = val++; +exports.CHATTR.UNDERLINE = val++; +exports.CHATTR.REVERSE = val++; +exports.CHATTR.DIM = val++; +exports.CHATTR.HILITE = val++; +exports.CHATTR.ATTRMASK = 0xf; + +// Minimap features +val = 0; +exports.MF_UNSEEN = val++; +exports.MF_FLOOR = val++; +exports.MF_WALL = val++; +exports.MF_MAP_FLOOR = val++; +exports.MF_MAP_WALL = val++; +exports.MF_DOOR = val++; +exports.MF_ITEM = val++; +exports.MF_MONS_FRIENDLY = val++; +exports.MF_MONS_PEACEFUL = val++; +exports.MF_MONS_NEUTRAL = val++; +exports.MF_MONS_HOSTILE = val++; +exports.MF_MONS_NO_EXP = val++; +exports.MF_STAIR_UP = val++; +exports.MF_STAIR_DOWN = val++; +exports.MF_STAIR_BRANCH = val++; +exports.MF_FEATURE = val++; +exports.MF_WATER = val++; +exports.MF_LAVA = val++; +exports.MF_TRAP = val++; +exports.MF_EXCL_ROOT = val++; +exports.MF_EXCL = val++; +exports.MF_PLAYER = val++; +exports.MF_DEEP_WATER = val++; +exports.MF_PORTAL = val++; +exports.MF_MAX = val++; + +exports.MF_SKIP = val++; + +exports.reverse_lookup = (e, value) => { + for (const prop in e) { + if (e[prop] === value) return prop; + } +}; + +export default exports; diff --git a/crawl-ref/source/webserver/client/game/src/focus-trap.js b/crawl-ref/source/webserver/client/game/src/focus-trap.js deleted file mode 100644 index bfe1c10b367..00000000000 --- a/crawl-ref/source/webserver/client/game/src/focus-trap.js +++ /dev/null @@ -1,571 +0,0 @@ -/** - * focus-trap.js - v5.0.2 - * - * Includes the following embedded node package dependencies: - * - tabbable - * - xtend - * - * Under MIT License - */ - -(function(f){if (typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if (typeof define==="function"&&define.amd){define([],f)}else{var g;if (typeof window!=="undefined"){g=window}else if (typeof global!=="undefined"){g=global}else if (typeof self!=="undefined"){g=self}else{g=this}g.focusTrap = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if (!n[i]){if (!e[i]){var c="function"==typeof require&&require;if (!f&&c)return c(i,!0);if (u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for (var u="function"==typeof require&&require,i=0;i 0) { - var activeTrap = trapQueue[trapQueue.length - 1]; - if (activeTrap !== trap) { - activeTrap.pause(); - } - } - - var trapIndex = trapQueue.indexOf(trap); - if (trapIndex === -1) { - trapQueue.push(trap); - } else { - // move this existing trap to the front of the queue - trapQueue.splice(trapIndex, 1); - trapQueue.push(trap); - } - }, - - deactivateTrap: function(trap) { - var trapIndex = trapQueue.indexOf(trap); - if (trapIndex !== -1) { - trapQueue.splice(trapIndex, 1); - } - - if (trapQueue.length > 0) { - trapQueue[trapQueue.length - 1].unpause(); - } - } - }; -})(); - -function focusTrap(element, userOptions) { - var doc = document; - var container = - typeof element === 'string' ? doc.querySelector(element) : element; - - var config = xtend( - { - returnFocusOnDeactivate: true, - escapeDeactivates: true - }, - userOptions - ); - - var state = { - firstTabbableNode: null, - lastTabbableNode: null, - nodeFocusedBeforeActivation: null, - mostRecentlyFocusedNode: null, - active: false, - paused: false - }; - - var trap = { - activate: activate, - deactivate: deactivate, - pause: pause, - unpause: unpause - }; - - return trap; - - function activate(activateOptions) { - if (state.active) return; - - updateTabbableNodes(); - - state.active = true; - state.paused = false; - state.nodeFocusedBeforeActivation = doc.activeElement; - - var onActivate = - activateOptions && activateOptions.onActivate - ? activateOptions.onActivate - : config.onActivate; - if (onActivate) { - onActivate(); - } - - addListeners(); - return trap; - } - - function deactivate(deactivateOptions) { - if (!state.active) return; - - clearTimeout(activeFocusDelay); - - removeListeners(); - state.active = false; - state.paused = false; - - activeFocusTraps.deactivateTrap(trap); - - var onDeactivate = - deactivateOptions && deactivateOptions.onDeactivate !== undefined - ? deactivateOptions.onDeactivate - : config.onDeactivate; - if (onDeactivate) { - onDeactivate(); - } - - var returnFocus = - deactivateOptions && deactivateOptions.returnFocus !== undefined - ? deactivateOptions.returnFocus - : config.returnFocusOnDeactivate; - if (returnFocus) { - delay(function() { - tryFocus(state.nodeFocusedBeforeActivation); - }); - } - - return trap; - } - - function pause() { - if (state.paused || !state.active) return; - state.paused = true; - removeListeners(); - } - - function unpause() { - if (!state.paused || !state.active) return; - state.paused = false; - updateTabbableNodes(); - addListeners(); - } - - function addListeners() { - if (!state.active) return; - - // There can be only one listening focus trap at a time - activeFocusTraps.activateTrap(trap); - - // Delay ensures that the focused element doesn't capture the event - // that caused the focus trap activation. - activeFocusDelay = delay(function() { - tryFocus(getInitialFocusNode()); - }); - - doc.addEventListener('focusin', checkFocusIn, true); - doc.addEventListener('mousedown', checkPointerDown, { - capture: true, - passive: false - }); - doc.addEventListener('touchstart', checkPointerDown, { - capture: true, - passive: false - }); - doc.addEventListener('click', checkClick, { - capture: true, - passive: false - }); - doc.addEventListener('keydown', checkKey, { - capture: true, - passive: false - }); - - return trap; - } - - function removeListeners() { - if (!state.active) return; - - doc.removeEventListener('focusin', checkFocusIn, true); - doc.removeEventListener('mousedown', checkPointerDown, true); - doc.removeEventListener('touchstart', checkPointerDown, true); - doc.removeEventListener('click', checkClick, true); - doc.removeEventListener('keydown', checkKey, true); - - return trap; - } - - function getNodeForOption(optionName) { - var optionValue = config[optionName]; - var node = optionValue; - if (!optionValue) { - return null; - } - if (typeof optionValue === 'string') { - node = doc.querySelector(optionValue); - if (!node) { - throw new Error('`' + optionName + '` refers to no known node'); - } - } - if (typeof optionValue === 'function') { - node = optionValue(); - if (!node) { - throw new Error('`' + optionName + '` did not return a node'); - } - } - return node; - } - - function getInitialFocusNode() { - var node; - if (getNodeForOption('initialFocus') !== null) { - node = getNodeForOption('initialFocus'); - } else if (container.contains(doc.activeElement)) { - node = doc.activeElement; - } else { - node = state.firstTabbableNode || getNodeForOption('fallbackFocus'); - } - - if (!node) { - throw new Error( - "You can't have a focus-trap without at least one focusable element" - ); - } - - return node; - } - - // This needs to be done on mousedown and touchstart instead of click - // so that it precedes the focus event. - function checkPointerDown(e) { - if (container.contains(e.target)) return; - if (config.clickOutsideDeactivates) { - deactivate({ - returnFocus: config.returnFocusOnDeactivate - && !tabbable.isFocusable(e.target), - }); - return; - } - // This is needed for mobile devices. - // (If we'll only let `click` events through, - // then on mobile they will be blocked anyways if `touchstart` is blocked.) - if (config.allowOutsideClick && config.allowOutsideClick(e)) { - return; - } - e.preventDefault(); - } - - // In case focus escapes the trap for some strange reason, pull it back in. - function checkFocusIn(e) { - // In Firefox when you Tab out of an iframe the Document is briefly focused. - if (container.contains(e.target) || e.target instanceof Document) { - return; - } - e.stopImmediatePropagation(); - tryFocus(state.mostRecentlyFocusedNode || getInitialFocusNode()); - } - - function checkKey(e) { - if (config.escapeDeactivates !== false && isEscapeEvent(e)) { - e.preventDefault(); - deactivate(); - return; - } - if (isTabEvent(e)) { - checkTab(e); - return; - } - } - - // Hijack Tab events on the first and last focusable nodes of the trap, - // in order to prevent focus from escaping. If it escapes for even a - // moment it can end up scrolling the page and causing confusion so we - // kind of need to capture the action at the keydown phase. - function checkTab(e) { - updateTabbableNodes(); - if (e.shiftKey && e.target === state.firstTabbableNode) { - e.preventDefault(); - tryFocus(state.lastTabbableNode); - return; - } - if (!e.shiftKey && e.target === state.lastTabbableNode) { - e.preventDefault(); - tryFocus(state.firstTabbableNode); - return; - } - } - - function checkClick(e) { - if (config.clickOutsideDeactivates) return; - if (container.contains(e.target)) return; - if (config.allowOutsideClick && config.allowOutsideClick(e)) { - return; - } - e.preventDefault(); - e.stopImmediatePropagation(); - } - - function updateTabbableNodes() { - var tabbableNodes = tabbable(container); - state.firstTabbableNode = tabbableNodes[0] || getInitialFocusNode(); - state.lastTabbableNode = - tabbableNodes[tabbableNodes.length - 1] || getInitialFocusNode(); - } - - function tryFocus(node) { - if (node === doc.activeElement) return; - if (!node || !node.focus) { - tryFocus(getInitialFocusNode()); - return; - } - - node.focus(); - state.mostRecentlyFocusedNode = node; - if (isSelectableInput(node)) { - node.select(); - } - } -} - -function isSelectableInput(node) { - return ( - node.tagName && - node.tagName.toLowerCase() === 'input' && - typeof node.select === 'function' - ); -} - -function isEscapeEvent(e) { - return e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27; -} - -function isTabEvent(e) { - return e.key === 'Tab' || e.keyCode === 9; -} - -function delay(fn) { - return setTimeout(fn, 0); -} - -module.exports = focusTrap; - -},{"tabbable":2,"xtend":3}],2:[function(require,module,exports){ -var candidateSelectors = [ - 'input', - 'select', - 'textarea', - 'a[href]', - 'button', - '[tabindex]', - 'audio[controls]', - 'video[controls]', - '[contenteditable]:not([contenteditable="false"])', -]; -var candidateSelector = candidateSelectors.join(','); - -var matches = typeof Element === 'undefined' - ? function () {} - : Element.prototype.matches || Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; - -function tabbable(el, options) { - options = options || {}; - - var elementDocument = el.ownerDocument || el; - var regularTabbables = []; - var orderedTabbables = []; - - var untouchabilityChecker = new UntouchabilityChecker(elementDocument); - var candidates = el.querySelectorAll(candidateSelector); - - if (options.includeContainer) { - if (matches.call(el, candidateSelector)) { - candidates = Array.prototype.slice.apply(candidates); - candidates.unshift(el); - } - } - - var i, candidate, candidateTabindex; - for (i = 0; i < candidates.length; i++) { - candidate = candidates[i]; - - if (!isNodeMatchingSelectorTabbable(candidate, untouchabilityChecker)) continue; - - candidateTabindex = getTabindex(candidate); - if (candidateTabindex === 0) { - regularTabbables.push(candidate); - } else { - orderedTabbables.push({ - documentOrder: i, - tabIndex: candidateTabindex, - node: candidate, - }); - } - } - - var tabbableNodes = orderedTabbables - .sort(sortOrderedTabbables) - .map(function(a) { return a.node }) - .concat(regularTabbables); - - return tabbableNodes; -} - -tabbable.isTabbable = isTabbable; -tabbable.isFocusable = isFocusable; - -function isNodeMatchingSelectorTabbable(node, untouchabilityChecker) { - if ( - !isNodeMatchingSelectorFocusable(node, untouchabilityChecker) - || isNonTabbableRadio(node) - || getTabindex(node) < 0 - ) { - return false; - } - return true; -} - -function isTabbable(node, untouchabilityChecker) { - if (!node) throw new Error('No node provided'); - if (matches.call(node, candidateSelector) === false) return false; - return isNodeMatchingSelectorTabbable(node, untouchabilityChecker); -} - -function isNodeMatchingSelectorFocusable(node, untouchabilityChecker) { - untouchabilityChecker = untouchabilityChecker || new UntouchabilityChecker(node.ownerDocument || node); - if ( - node.disabled - || isHiddenInput(node) - || untouchabilityChecker.isUntouchable(node) - ) { - return false; - } - return true; -} - -var focusableCandidateSelector = candidateSelectors.concat('iframe').join(','); -function isFocusable(node, untouchabilityChecker) { - if (!node) throw new Error('No node provided'); - if (matches.call(node, focusableCandidateSelector) === false) return false; - return isNodeMatchingSelectorFocusable(node, untouchabilityChecker); -} - -function getTabindex(node) { - var tabindexAttr = parseInt(node.getAttribute('tabindex'), 10); - if (!isNaN(tabindexAttr)) return tabindexAttr; - // Browsers do not return `tabIndex` correctly for contentEditable nodes; - // so if they don't have a tabindex attribute specifically set, assume it's 0. - if (isContentEditable(node)) return 0; - return node.tabIndex; -} - -function sortOrderedTabbables(a, b) { - return a.tabIndex === b.tabIndex ? a.documentOrder - b.documentOrder : a.tabIndex - b.tabIndex; -} - -// Array.prototype.find not available in IE. -function find(list, predicate) { - for (var i = 0, length = list.length; i < length; i++) { - if (predicate(list[i])) return list[i]; - } -} - -function isContentEditable(node) { - return node.contentEditable === 'true'; -} - -function isInput(node) { - return node.tagName === 'INPUT'; -} - -function isHiddenInput(node) { - return isInput(node) && node.type === 'hidden'; -} - -function isRadio(node) { - return isInput(node) && node.type === 'radio'; -} - -function isNonTabbableRadio(node) { - return isRadio(node) && !isTabbableRadio(node); -} - -function getCheckedRadio(nodes) { - for (var i = 0; i < nodes.length; i++) { - if (nodes[i].checked) { - return nodes[i]; - } - } -} - -function isTabbableRadio(node) { - if (!node.name) return true; - // This won't account for the edge case where you have radio groups with the same - // in separate forms on the same page. - var radioSet = node.ownerDocument.querySelectorAll('input[type="radio"][name="' + node.name + '"]'); - var checked = getCheckedRadio(radioSet); - return !checked || checked === node; -} - -// An element is "untouchable" if *it or one of its ancestors* has -// `visibility: hidden` or `display: none`. -function UntouchabilityChecker(elementDocument) { - this.doc = elementDocument; - // Node cache must be refreshed on every check, in case - // the content of the element has changed. The cache contains tuples - // mapping nodes to their boolean result. - this.cache = []; -} - -// getComputedStyle accurately reflects `visibility: hidden` of ancestors -// but not `display: none`, so we need to recursively check parents. -UntouchabilityChecker.prototype.hasDisplayNone = function hasDisplayNone(node, nodeComputedStyle) { - if (node.nodeType !== Node.ELEMENT_NODE) return false; - - // Search for a cached result. - var cached = find(this.cache, function(item) { - return item === node; - }); - if (cached) return cached[1]; - - nodeComputedStyle = nodeComputedStyle || this.doc.defaultView.getComputedStyle(node); - - var result = false; - - if (nodeComputedStyle.display === 'none') { - result = true; - } else if (node.parentNode) { - result = this.hasDisplayNone(node.parentNode); - } - - this.cache.push([node, result]); - - return result; -} - -UntouchabilityChecker.prototype.isUntouchable = function isUntouchable(node) { - if (node === this.doc.documentElement) return false; - var computedStyle = this.doc.defaultView.getComputedStyle(node); - if (this.hasDisplayNone(node, computedStyle)) return true; - return computedStyle.visibility === 'hidden'; -} - -module.exports = tabbable; - -},{}],3:[function(require,module,exports){ -module.exports = extend - -var hasOwnProperty = Object.prototype.hasOwnProperty; - -function extend() { - var target = {} - - for (var i = 0; i < arguments.length; i++) { - var source = arguments[i] - - for (var key in source) { - if (hasOwnProperty.call(source, key)) { - target[key] = source[key] - } - } - } - - return target -} - -},{}]},{},[1])(1) -}); diff --git a/crawl-ref/source/webserver/client/game/src/game.js b/crawl-ref/source/webserver/client/game/src/game.js index e8296803d02..2bc3750c65e 100644 --- a/crawl-ref/source/webserver/client/game/src/game.js +++ b/crawl-ref/source/webserver/client/game/src/game.js @@ -1,509 +1,469 @@ -define(["jquery", "exports", "comm", "client", "key_conversion", "./dungeon_renderer", - "./display", "./minimap", "./enums", "./messages", "./options", - "./mouse_control", "./text", "./menu", "./action_panel", "./player", - "./ui","./ui-layouts"], -function ($, exports, comm, client, key_conversion, dungeon_renderer, display, - minimap, enums, messages, options, mouse_control) { - "use strict"; - - var layout_parameters = null, ui_state, input_mode; - var stat_width = 42; - var msg_height; - var show_diameter = 17; - - function setup_keycodes() - { - key_conversion.reset_keycodes(); - // the `codes` check and the else clause are here to prevent crashes - // while players' caches get updated client.js and key_conversion.js - // TODO: remove someday - if (key_conversion.codes) - key_conversion.enable_code_conversion(); - else - { - key_conversion.simple[96] = -1000; // numpad 0 - key_conversion.simple[97] = -1001; // numpad 1 - key_conversion.simple[98] = -1002; // numpad 2 - key_conversion.simple[99] = -1003; // numpad 3 - key_conversion.simple[100] = -1004; // numpad 4 - key_conversion.simple[101] = -1005; // numpad 5 - key_conversion.simple[102] = -1006; // numpad 6 - key_conversion.simple[103] = -1007; // numpad 7 - key_conversion.simple[104] = -1008; // numpad 8 - key_conversion.simple[105] = -1009; // numpad 9 - key_conversion.simple[106] = -1015; // numpad * - key_conversion.simple[107] = -1016; // numpad + - key_conversion.simple[108] = -1019; // numpad . (some firefox versions? TODO: confirm) - key_conversion.simple[109] = -1018; // numpad - - key_conversion.simple[110] = -1019; // numpad . - key_conversion.simple[111] = -1012; // numpad / - - // fix some function key issues (see note in key_conversions.js). - key_conversion.simple[112] = -265; // F1 - key_conversion.simple[113] = -266; // F2 - key_conversion.simple[114] = -267; // F3 - key_conversion.simple[115] = -268; // F4 - key_conversion.simple[116] = -269; // F5 - key_conversion.simple[117] = -270; // F6 - key_conversion.simple[118] = -271; // F7 - key_conversion.simple[119] = -272; // F8 - key_conversion.simple[120] = -273; // F9 - key_conversion.simple[121] = -274; // F10 - // reserve F11 for the browser (full screen) - // reserve F12 for toggling chat - key_conversion.simple[124] = -277; // F13, may not work - // F13 may also show up as printscr, but we don't want to bind that - key_conversion.simple[125] = -278; // F14, may not work? - key_conversion.simple[126] = -279; // F15, may not work? - key_conversion.simple[127] = -280; - key_conversion.simple[128] = -281; - key_conversion.simple[129] = -282; - key_conversion.simple[130] = -283; - } - - // any version-specific keycode overrides can be added here. (Though - // hopefully this will be rarely needed in the future...) - } - - function init() - { - layout_parameters = null; - ui_state = -1; - options.clear(); - setup_keycodes(); - } - - $(document).on("game_preinit game_cleanup", init); - - function layout_params_differ(old_params, new_params) - { - if (!old_params) return true; - for (var param in new_params) - { - if (old_params.hasOwnProperty(param) && - old_params[param] != new_params[param]) - return true; - } - return false; - } - - function layout(params, force) - { - var window_width = params.window_width = $(window).width(); - var window_height = params.window_height = $(window).height(); - - if (!force && !layout_params_differ(layout_parameters, params)) - return false; - - layout_parameters = params; - - var state = ui_state; - set_ui_state(enums.ui.NORMAL); - - // Determine width of stats area - var old_html = $("#stats").html(); - var s = ""; - for (var i = 0; i < enums.stat_width; i++) - s = s + " "; - $("#stats").html(s); - var stat_width_px = $("#stats").outerWidth(); - $("#stats").html(old_html); - - // Determine height of messages area - - // need to clone this, not copy html, since there can be event handlers - // embedded in here on an input box. - var old_messages = $("#messages").clone(true); - var old_scroll_top = $("#messages_container").scrollTop(); - s = ""; - for (var i = 0; i < msg_height+1; i++) - s = s + "
"; - $("#messages").html(s); - var msg_height_px = $("#messages").outerHeight(); - $("#messages").replaceWith(old_messages); - $("#messages_container").scrollTop(old_scroll_top); - - var remaining_width = window_width - stat_width_px; - var remaining_height = window_height - msg_height_px; - - layout_parameters.remaining_width = remaining_width; - layout_parameters.remaining_height = remaining_height; - - // Position controls - client.set_layer("normal"); - $("#messages_container").css({ - "max-height": msg_height_px*msg_height/(msg_height+1), - "width": remaining_width - }); - dungeon_renderer.fit_to(remaining_width, remaining_height, - show_diameter); - - minimap.fit_to(stat_width_px, layout_parameters); - - $("#stats").width(stat_width_px); - $("#monster_list").width(stat_width_px); - $("#mobile_input input").width(remaining_width-12); // 2*padding+2*border - - // Go back to the old layer - set_ui_state(state); - - // Update the view - display.invalidate(true); - display.display(); - minimap.update_overlay(); - - var possible_input = $("#messages .game_message input"); - if (possible_input) - possible_input.focus(); - - // Input helper for mobile browsers - // XX should this really happen in `layout`? - if (!client.is_watching()) - { - var mobile_input = options.get("tile_web_mobile_input_helper"); - if ((mobile_input === 'true') || (mobile_input === 'auto' && is_mobile())) - { - $("#mobile_input").show(); - $("#mobile_input input") - .off("keydown") - .on("keydown", handle_mobile_keydown) - .off("input") - .on("input", handle_mobile_input) - .off("mousedown focusout") - .on("mousedown", mobile_input_click); - // the following requires a fairly modern browser - $(document).on("visibilitychange", - function(ev) - { - // try to regularize the behavior, on iOS it's flaky - // otherwise - // bug: on iOS zooming out to view tabs doesn't seem - // to trip this event handler. Presumably because - // "prerendered" isn't yet implemented? - if (document.visibilityState !== "visible") - mobile_input_force_defocus(); - }); - } - } - } - - options.add_listener(function () { - minimap.init_options(); - if (layout_parameters) - layout(layout_parameters, true); - display.invalidate(true); - display.display(); - glyph_mode_font_init(); - init_custom_text_colours(); - }); - - function toggle_full_window_dungeon_view(full) - { - // Toggles the dungeon view for X map mode - if (layout_parameters == null) return; - if (full) - { - var width = layout_parameters.remaining_width; - var height = layout_parameters.remaining_height; - - if (options.get("tile_level_map_hide_sidebar") === true) { - width = layout_parameters.window_width - 5; - $("#right_column").hide(); - } - if (options.get("tile_level_map_hide_messages") === true) { - height = layout_parameters.window_height - 5; - messages.hide(); - } - $(".action-panel").hide(); - - dungeon_renderer.fit_to(width, height, show_diameter); - } - else - { - dungeon_renderer.fit_to(layout_parameters.remaining_width, - layout_parameters.remaining_height, - show_diameter); - $("#right_column").show(); - $(".action-panel").show(); - messages.show(); - } - minimap.stop_minimap_farview(); - minimap.update_overlay(); - display.invalidate(true); - display.display(); - } - - function set_ui_state(state) - { - dungeon_renderer.set_ui_state(state); - if (state == ui_state) return; - - var old_state = ui_state; - ui_state = state; - switch (ui_state) - { - case enums.ui.NORMAL: - client.set_layer("normal"); - if (old_state == enums.ui.VIEW_MAP) - toggle_full_window_dungeon_view(false); - break; - - case enums.ui.CRT: - client.set_layer("crt"); - break; - - case enums.ui.VIEW_MAP: - toggle_full_window_dungeon_view(true); - break; - } - } - - function handle_set_layout(data) - { - if (data.message_pane.height) - { - msg_height = data.message_pane.height - + (data.message_pane.small_more ? 0 : -1); - messages.message_pane_height = msg_height; - } - else - msg_height = messages.message_pane_height; - - if (layout_parameters == null) - layout({}); - else - { - var params = $.extend({}, layout_parameters); - layout(params, true); - } - } - - function handle_set_ui_state(data) - { - set_ui_state(data.state); - } - - function handle_set_ui_cutoff(data) - { - var popups = document.querySelectorAll("#ui-stack > .ui-popup"); - Array.from(popups).forEach(function (p, i) { - p.classList.toggle("hidden", i <= data.cutoff); - }); - } - - function set_input_mode(mode) - { - if (mode == input_mode) return; - input_mode = mode; - dungeon_renderer.update_mouse_mode(input_mode); - if (mode == enums.mouse_mode.COMMAND) - messages.new_command(); - } - - game.get_input_mode = function() { return input_mode; } - game.get_ui_state = function() { return ui_state; } - - game.can_target = function() - { - return (input_mode == enums.mouse_mode.TARGET - || input_mode == enums.mouse_mode.TARGET_DIR - || input_mode == enums.mouse_mode.TARGET_PATH); - } - game.can_move = function() - { - // XX just looking - return (input_mode == enums.mouse_mode.COMMAND - || ui_state == enums.ui.VIEW_MAP); - } - game.can_describe = function() - { - return (input_mode == enums.mouse_mode.TARGET - || input_mode == enums.mouse_mode.TARGET_DIR - || input_mode == enums.mouse_mode.TARGET_PATH - || input_mode == enums.mouse_mode.COMMAND - || ui_state == enums.ui.VIEW_MAP); - } - - - function handle_set_input_mode(data) - { - set_input_mode(data.mode); - } - - function handle_delay(data) - { - client.delay(data.t); - } - - var game_version; - function handle_version(data) - { - game_version = data; - document.title = data.text; - } - - function glyph_mode_font_init() - { - if (options.get("tile_display_mode") == "tiles") return; - - var glyph_font, glyph_size; - glyph_size = options.get("glyph_mode_font_size"); - glyph_font = options.get("glyph_mode_font"); - - if (!document.fonts.check(glyph_size + "px " + glyph_font)) - glyph_font = "monospace"; - - var renderer_settings = { - glyph_mode_font_size: glyph_size, - glyph_mode_font: glyph_font - }; - $.extend(dungeon_renderer, renderer_settings); - } - - function init_custom_text_colours() - { - const root = document.querySelector(':root'); - - // Reset colours first - for (let i = 0; i < 16; i++) - root.style.removeProperty('--color-' + i); - - // Load custom replacements - var colours = options.get("custom_text_colours"); - for (var i in colours) - { - root.style.setProperty('--color-' + colours[i].index, - "rgba(" + colours[i].r + ", " - + colours[i].g + ", " - + colours[i].b + ", 255)"); - } - } - - function is_mobile() - { - return ('ontouchstart' in document.documentElement); - } - - function handle_mobile_input(e) - { - e.target.value = e.target.defaultValue; - comm.send_message("input", { text: e.originalEvent.data }); - } - - function handle_mobile_keydown(e) - { - // translate backspace/delete to esc -- the backspace key is almost - // entirely unused outside of text input, and the lack of esc on a - // mobile keyboard is really painful. Text input doesn't go through - // this input, so that case is fine. (One minor case is the macro - // edit menu, but there's at least an alternate way to clear a - // binding.) - if (e.which == 8 || e.which == 46) - { - comm.send_message("key", { keycode: 27 }); - e.preventDefault(); - return false; - } - } - - function mobile_input_focus_style(focused) - { - // manually manage the style so that it doesn't blink when the - // focusout handler is doing its thing - var mi = $("#mobile_input input"); - if (focused) - { - mi.attr("placeholder", "Tap here to close keyboard"); - mi.css("background", "rgba(100, 100, 100, 0.5)"); - } - else - { - mi.attr("placeholder", "Tap here for keyboard input"); - mi.css("background", "rgba(0, 0, 0, 0.5)"); - } - } - - function mobile_input_focused() - { - return $("#mobile_input input").is(":focus"); +import $ from "jquery"; + +import comm from "./comm"; +import client from "./client"; +import key_conversion from "./key_conversion"; +import dungeon_renderer from "./dungeon_renderer"; +import display from "./display"; +import minimap from "./minimap"; +import enums from "./enums"; +import messages from "./messages"; +import options from "./options"; +import mouse_control from "./mouse_control"; + +import "./text"; +import "./menu"; +import "./action_panel"; +import "./player"; +import "./ui"; +import "./ui-layouts"; + +let layout_parameters = null, + ui_state, + input_mode; +const _stat_width = 42; +let msg_height; +const show_diameter = 17; + +function setup_keycodes() { + key_conversion.reset_keycodes(); + // the `codes` check and the else clause are here to prevent crashes + // while players' caches get updated client.js and key_conversion.js + // TODO: remove someday + if (key_conversion.codes) key_conversion.enable_code_conversion(); + else { + key_conversion.simple[96] = -1000; // numpad 0 + key_conversion.simple[97] = -1001; // numpad 1 + key_conversion.simple[98] = -1002; // numpad 2 + key_conversion.simple[99] = -1003; // numpad 3 + key_conversion.simple[100] = -1004; // numpad 4 + key_conversion.simple[101] = -1005; // numpad 5 + key_conversion.simple[102] = -1006; // numpad 6 + key_conversion.simple[103] = -1007; // numpad 7 + key_conversion.simple[104] = -1008; // numpad 8 + key_conversion.simple[105] = -1009; // numpad 9 + key_conversion.simple[106] = -1015; // numpad * + key_conversion.simple[107] = -1016; // numpad + + key_conversion.simple[108] = -1019; // numpad . (some firefox versions? TODO: confirm) + key_conversion.simple[109] = -1018; // numpad - + key_conversion.simple[110] = -1019; // numpad . + key_conversion.simple[111] = -1012; // numpad / + + // fix some function key issues (see note in key_conversions.js). + key_conversion.simple[112] = -265; // F1 + key_conversion.simple[113] = -266; // F2 + key_conversion.simple[114] = -267; // F3 + key_conversion.simple[115] = -268; // F4 + key_conversion.simple[116] = -269; // F5 + key_conversion.simple[117] = -270; // F6 + key_conversion.simple[118] = -271; // F7 + key_conversion.simple[119] = -272; // F8 + key_conversion.simple[120] = -273; // F9 + key_conversion.simple[121] = -274; // F10 + // reserve F11 for the browser (full screen) + // reserve F12 for toggling chat + key_conversion.simple[124] = -277; // F13, may not work + // F13 may also show up as printscr, but we don't want to bind that + key_conversion.simple[125] = -278; // F14, may not work? + key_conversion.simple[126] = -279; // F15, may not work? + key_conversion.simple[127] = -280; + key_conversion.simple[128] = -281; + key_conversion.simple[129] = -282; + key_conversion.simple[130] = -283; + } + + // any version-specific keycode overrides can be added here. (Though + // hopefully this will be rarely needed in the future...) +} + +function init() { + layout_parameters = null; + ui_state = -1; + options.clear(); + setup_keycodes(); +} + +$(document).on("game_preinit game_cleanup", init); + +function layout_params_differ(old_params, new_params) { + if (!old_params) return true; + for (const param in new_params) { + if ( + Object.hasOwn(old_params, param) && + old_params[param] !== new_params[param] + ) + return true; + } + return false; +} + +function layout(params, force) { + const window_width = (params.window_width = $(window).width()); + const window_height = (params.window_height = $(window).height()); + + if (!force && !layout_params_differ(layout_parameters, params)) return false; + + layout_parameters = params; + + const state = ui_state; + set_ui_state(enums.ui.NORMAL); + + // Determine width of stats area + const old_html = $("#stats").html(); + let s = ""; + for (let i = 0; i < enums.stat_width; i++) s = `${s} `; + $("#stats").html(s); + const stat_width_px = $("#stats").outerWidth(); + $("#stats").html(old_html); + + // Determine height of messages area + + // need to clone this, not copy html, since there can be event handlers + // embedded in here on an input box. + const old_messages = $("#messages").clone(true); + const old_scroll_top = $("#messages_container").scrollTop(); + s = ""; + for (let i = 0; i < msg_height + 1; i++) s = `${s}
`; + $("#messages").html(s); + const msg_height_px = $("#messages").outerHeight(); + $("#messages").replaceWith(old_messages); + $("#messages_container").scrollTop(old_scroll_top); + + const remaining_width = window_width - stat_width_px; + const remaining_height = window_height - msg_height_px; + + layout_parameters.remaining_width = remaining_width; + layout_parameters.remaining_height = remaining_height; + + // Position controls + client.set_layer("normal"); + $("#messages_container").css({ + "max-height": (msg_height_px * msg_height) / (msg_height + 1), + width: remaining_width, + }); + dungeon_renderer.fit_to(remaining_width, remaining_height, show_diameter); + + minimap.fit_to(stat_width_px, layout_parameters); + + $("#stats").width(stat_width_px); + $("#monster_list").width(stat_width_px); + $("#mobile_input input").width(remaining_width - 12); // 2*padding+2*border + + // Go back to the old layer + set_ui_state(state); + + // Update the view + display.invalidate(true); + display.display(); + minimap.update_overlay(); + + const possible_input = $("#messages .game_message input"); + if (possible_input) possible_input.focus(); + + // Input helper for mobile browsers + // XX should this really happen in `layout`? + if (!client.is_watching()) { + const mobile_input = options.get("tile_web_mobile_input_helper"); + if (mobile_input === "true" || (mobile_input === "auto" && is_mobile())) { + $("#mobile_input").show(); + $("#mobile_input input") + .off("keydown") + .on("keydown", handle_mobile_keydown) + .off("input") + .on("input", handle_mobile_input) + .off("mousedown focusout") + .on("mousedown", mobile_input_click); + // the following requires a fairly modern browser + $(document).on("visibilitychange", (_ev) => { + // try to regularize the behavior, on iOS it's flaky + // otherwise + // bug: on iOS zooming out to view tabs doesn't seem + // to trip this event handler. Presumably because + // "prerendered" isn't yet implemented? + if (document.visibilityState !== "visible") + mobile_input_force_defocus(); + }); } + } +} + +options.add_listener(() => { + minimap.init_options(); + if (layout_parameters) layout(layout_parameters, true); + display.invalidate(true); + display.display(); + glyph_mode_font_init(); + init_custom_text_colours(); +}); - function mobile_input_focusout(ev) - { - var $mi = $("#mobile_input input"); - if (ev.relatedTarget && $(ev.relatedTarget).is("input")) - { - // ok, we'll allow it, but we want it back later. As long as focus - // goes from input to input, the keyboard seems to stay open. - $(ev.relatedTarget).off("focusout").on("focusout", function (ev) - { - $mi[0].focus(); - $mi.off("focusout").on("focusout", mobile_input_focusout); - mobile_input_focus_style(true); - $(ev.relatedTarget).off("focusout"); - }); - $mi.off("focusout"); - mobile_input_focus_style(false); - } - else - { - // force focus to stay on the mobile input. For iOS purposes, this - // is the only thing I've tried that works. This *can't* happen in - // a timeout, or the temporary loss of focus is enough to close - // the keyboard. - // Bug: tapping chat close hides the keyboard - $mi[0].focus(); - } - } +function toggle_full_window_dungeon_view(full) { + // Toggles the dungeon view for X map mode + if (layout_parameters == null) return; + if (full) { + let width = layout_parameters.remaining_width; + let height = layout_parameters.remaining_height; - function mobile_input_force_defocus() - { - var $mi = $("#mobile_input input"); - // remove any event handlers first - $mi.off("focusout"); - mobile_input_focus_style(false); - $mi.blur(); + if (options.get("tile_level_map_hide_sidebar") === true) { + width = layout_parameters.window_width - 5; + $("#right_column").hide(); } - - function mobile_input_click(ev) - { - var $mi = $("#mobile_input input"); - if (mobile_input_focused()) - { - mobile_input_force_defocus(); - ev.preventDefault(); - } - else - { - $mi.off("focusout").on("focusout", mobile_input_focusout); - mobile_input_focus_style(true); - $mi.focus(); - ev.preventDefault(); - } + if (options.get("tile_level_map_hide_messages") === true) { + height = layout_parameters.window_height - 5; + messages.hide(); } - - $(document).ready(function () { - $(window) - .off("resize.game") - .on("resize.game", function () { - if (layout_parameters) - { - var params = $.extend({}, layout_parameters); - layout(params); - } - $("#action-panel").triggerHandler("update"); - }); - }); - - comm.register_handlers({ - "delay": handle_delay, - "version": handle_version, - "layout": handle_set_layout, - "ui_state": handle_set_ui_state, - "ui_cutoff": handle_set_ui_cutoff, - "input_mode": handle_set_input_mode, + $(".action-panel").hide(); + + dungeon_renderer.fit_to(width, height, show_diameter); + } else { + dungeon_renderer.fit_to( + layout_parameters.remaining_width, + layout_parameters.remaining_height, + show_diameter + ); + $("#right_column").show(); + $(".action-panel").show(); + messages.show(); + } + minimap.stop_minimap_farview(); + minimap.update_overlay(); + display.invalidate(true); + display.display(); +} + +function set_ui_state(state) { + dungeon_renderer.set_ui_state(state); + if (state === ui_state) return; + + const old_state = ui_state; + ui_state = state; + switch (ui_state) { + case enums.ui.NORMAL: + client.set_layer("normal"); + if (old_state === enums.ui.VIEW_MAP) + toggle_full_window_dungeon_view(false); + break; + + case enums.ui.CRT: + client.set_layer("crt"); + break; + + case enums.ui.VIEW_MAP: + toggle_full_window_dungeon_view(true); + break; + } +} + +function handle_set_layout(data) { + if (data.message_pane.height) { + msg_height = + data.message_pane.height + (data.message_pane.small_more ? 0 : -1); + messages.message_pane_height = msg_height; + } else msg_height = messages.message_pane_height; + + if (layout_parameters == null) layout({}); + else { + const params = $.extend({}, layout_parameters); + layout(params, true); + } +} + +function handle_set_ui_state(data) { + set_ui_state(data.state); +} + +function handle_set_ui_cutoff(data) { + const popups = document.querySelectorAll("#ui-stack > .ui-popup"); + Array.from(popups).forEach((p, i) => { + p.classList.toggle("hidden", i <= data.cutoff); + }); +} + +function set_input_mode(mode) { + if (mode === input_mode) return; + input_mode = mode; + dungeon_renderer.update_mouse_mode(input_mode); + if (mode === enums.mouse_mode.COMMAND) messages.new_command(); +} + +game.get_input_mode = () => input_mode; +game.get_ui_state = () => ui_state; + +game.can_target = () => + input_mode === enums.mouse_mode.TARGET || + input_mode === enums.mouse_mode.TARGET_DIR || + input_mode === enums.mouse_mode.TARGET_PATH; +game.can_move = () => { + // XX just looking + return ( + input_mode === enums.mouse_mode.COMMAND || ui_state === enums.ui.VIEW_MAP + ); +}; +game.can_describe = () => + input_mode === enums.mouse_mode.TARGET || + input_mode === enums.mouse_mode.TARGET_DIR || + input_mode === enums.mouse_mode.TARGET_PATH || + input_mode === enums.mouse_mode.COMMAND || + ui_state === enums.ui.VIEW_MAP; + +function handle_set_input_mode(data) { + set_input_mode(data.mode); +} + +function handle_delay(data) { + client.delay(data.t); +} + +let _game_version; +function handle_version(data) { + _game_version = data; + document.title = data.text; +} + +function glyph_mode_font_init() { + if (options.get("tile_display_mode") === "tiles") return; + + let glyph_font, glyph_size; + glyph_size = options.get("glyph_mode_font_size"); + glyph_font = options.get("glyph_mode_font"); + + if (!document.fonts.check(`${glyph_size}px ${glyph_font}`)) + glyph_font = "monospace"; + + const renderer_settings = { + glyph_mode_font_size: glyph_size, + glyph_mode_font: glyph_font, + }; + $.extend(dungeon_renderer, renderer_settings); +} + +function init_custom_text_colours() { + const root = document.querySelector(":root"); + + // Reset colours first + for (let i = 0; i < 16; i++) root.style.removeProperty(`--color-${i}`); + + // Load custom replacements + const colours = options.get("custom_text_colours"); + for (const i in colours) { + root.style.setProperty( + `--color-${colours[i].index}`, + "rgba(" + + colours[i].r + + ", " + + colours[i].g + + ", " + + colours[i].b + + ", 255)" + ); + } +} + +function is_mobile() { + return "ontouchstart" in document.documentElement; +} + +function handle_mobile_input(e) { + e.target.value = e.target.defaultValue; + comm.send_message("input", { text: e.originalEvent.data }); +} + +function handle_mobile_keydown(e) { + // translate backspace/delete to esc -- the backspace key is almost + // entirely unused outside of text input, and the lack of esc on a + // mobile keyboard is really painful. Text input doesn't go through + // this input, so that case is fine. (One minor case is the macro + // edit menu, but there's at least an alternate way to clear a + // binding.) + if (e.which === 8 || e.which === 46) { + comm.send_message("key", { keycode: 27 }); + e.preventDefault(); + return false; + } +} + +function mobile_input_focus_style(focused) { + // manually manage the style so that it doesn't blink when the + // focusout handler is doing its thing + const mi = $("#mobile_input input"); + if (focused) { + mi.attr("placeholder", "Tap here to close keyboard"); + mi.css("background", "rgba(100, 100, 100, 0.5)"); + } else { + mi.attr("placeholder", "Tap here for keyboard input"); + mi.css("background", "rgba(0, 0, 0, 0.5)"); + } +} + +function mobile_input_focused() { + return $("#mobile_input input").is(":focus"); +} + +function mobile_input_focusout(ev) { + const $mi = $("#mobile_input input"); + if (ev.relatedTarget && $(ev.relatedTarget).is("input")) { + // ok, we'll allow it, but we want it back later. As long as focus + // goes from input to input, the keyboard seems to stay open. + $(ev.relatedTarget) + .off("focusout") + .on("focusout", (ev) => { + $mi[0].focus(); + $mi.off("focusout").on("focusout", mobile_input_focusout); + mobile_input_focus_style(true); + $(ev.relatedTarget).off("focusout"); + }); + $mi.off("focusout"); + mobile_input_focus_style(false); + } else { + // force focus to stay on the mobile input. For iOS purposes, this + // is the only thing I've tried that works. This *can't* happen in + // a timeout, or the temporary loss of focus is enough to close + // the keyboard. + // Bug: tapping chat close hides the keyboard + $mi[0].focus(); + } +} + +function mobile_input_force_defocus() { + const $mi = $("#mobile_input input"); + // remove any event handlers first + $mi.off("focusout"); + mobile_input_focus_style(false); + $mi.blur(); +} + +function mobile_input_click(ev) { + const $mi = $("#mobile_input input"); + if (mobile_input_focused()) { + mobile_input_force_defocus(); + ev.preventDefault(); + } else { + $mi.off("focusout").on("focusout", mobile_input_focusout); + mobile_input_focus_style(true); + $mi.focus(); + ev.preventDefault(); + } +} + +$(document).ready(() => { + $(window) + .off("resize.game") + .on("resize.game", () => { + if (layout_parameters) { + const params = $.extend({}, layout_parameters); + layout(params); + } + $("#action-panel").triggerHandler("update"); }); +}); - // ugly circular reference breaking - mouse_control.game = game; +comm.register_handlers({ + delay: handle_delay, + version: handle_version, + layout: handle_set_layout, + ui_state: handle_set_ui_state, + ui_cutoff: handle_set_ui_cutoff, + input_mode: handle_set_input_mode, }); + +// ugly circular reference breaking +mouse_control.game = game; diff --git a/crawl-ref/source/webserver/client/game/src/key_conversion.js b/crawl-ref/source/webserver/client/game/src/key_conversion.js new file mode 100644 index 00000000000..000b187ef46 --- /dev/null +++ b/crawl-ref/source/webserver/client/game/src/key_conversion.js @@ -0,0 +1 @@ +export default window.DCSS.key_conversion; diff --git a/crawl-ref/source/webserver/client/game/src/main.js b/crawl-ref/source/webserver/client/game/src/main.js index 197a6e84c93..e629006152d 100644 --- a/crawl-ref/source/webserver/client/game/src/main.js +++ b/crawl-ref/source/webserver/client/game/src/main.js @@ -5,22 +5,10 @@ // import "./simplebar.css"; import "./game"; -// document.querySelector('#app')!.innerHTML = ` -//
-// -// -// -// -// -// -//

Vite + TypeScript

-//
-// -//
-//

-// Click on the Vite and TypeScript logos to learn more -//

-//
-// ` +import $ from "jquery"; +import client from "./client"; -// setupCounter(document.querySelector('#counter')!) +// Game initialisation (moved from inline script in html template) +$(document).trigger("game_preinit"); +$(document).trigger("game_init"); +client.uninhibit_messages(); diff --git a/crawl-ref/source/webserver/client/game/src/map_knowledge.js b/crawl-ref/source/webserver/client/game/src/map_knowledge.js index f32f5d30722..d3a8185a6ab 100644 --- a/crawl-ref/source/webserver/client/game/src/map_knowledge.js +++ b/crawl-ref/source/webserver/client/game/src/map_knowledge.js @@ -1,227 +1,189 @@ -define(["jquery", "./enums", "./util"], function ($, enums, util) { - "use strict"; - - var k, player_on_level, monster_table, dirty_locs, bounds, bounds_changed; - - function init() - { - k = new Array(65536); - monster_table = {}; - dirty_locs = []; - bounds = null; - bounds_changed = false; +import $ from "jquery"; +import enums from "./enums"; +import util from "./util"; + +let k, player_on_level, monster_table, dirty_locs, bounds, bounds_changed; + +function init() { + k = new Array(65536); + monster_table = {}; + dirty_locs = []; + bounds = null; + bounds_changed = false; +} + +$(document).bind("game_init", init); + +function get(x, y) { + const key = util.make_key(x, y); + + while (key >= k.length) { + k = k.concat(new Array(k.length)); + } + + let val = k[key]; + if (val === undefined) { + val = { x: x, y: y }; + k[key] = val; + } + return val; +} + +function clear() { + k = new Array(65536); + monster_table = {}; + bounds = null; +} + +function visible(cell) { + if (cell.t) { + cell.t.bg = enums.prepare_bg_flags(cell.t.bg || 0); + return !cell.t.bg.UNSEEN && !cell.t.bg.MM_UNSEEN; + } + return false; +} + +function touch(x, y) { + const pos = y === undefined ? x : { x, y }; + const cell = get(pos.x, pos.y); + if (!cell.dirty) { + dirty_locs.push(pos); + cell.dirty = true; + } +} + +function merge_objects(current, diff) { + if (!current) return diff; + + for (const prop in diff) current[prop] = diff[prop]; + + return current; +} + +function set_monster_defaults(mon) { + mon.att = mon.att || 0; +} + +function merge_monster(old_mon, mon) { + if (old_mon?.refs) { + old_mon.refs--; + } + + if (!mon) { + return null; + } + + const id = mon.id; + + let last = monster_table[id]; + if (!last) { + if (old_mon) last = merge_objects(merge_objects({}, old_mon), mon); + else { + last = mon; + set_monster_defaults(last); } - - $(document).bind("game_init", init); - - function get(x, y) - { - var key = util.make_key(x, y); - - while (key >= k.length) { - k = k.concat(new Array(k.length)); - } - - var val = k[key]; - if (val === undefined) { - val = {x: x, y: y}; - k[key] = val; - } - return val; - } - - function clear() - { - k = new Array(65536); - monster_table = {}; - bounds = null; - } - - function visible(cell) - { - if (cell.t) - { - cell.t.bg = enums.prepare_bg_flags(cell.t.bg || 0); - return !cell.t.bg.UNSEEN && !cell.t.bg.MM_UNSEEN; - } - return false; - } - - function touch(x, y) - { - var pos = (y === undefined) ? x : {x: x, y: y}; - var cell = get(pos.x, pos.y); - if (!cell.dirty) - dirty_locs.push(pos); - cell.dirty = true; - } - - function merge_objects(current, diff) - { - if (!current) - return diff; - - for (var prop in diff) - current[prop] = diff[prop]; - - return current; - } - - function set_monster_defaults(mon) - { - mon.att = mon.att || 0; + } else { + merge_objects(last, mon); + } + + if (id) { + last.refs = last.refs || 0; + last.refs++; + monster_table[id] = last; + } + + return last; +} + +function clean_monster_table() { + for (const id in monster_table) { + if (!monster_table[id].refs) delete monster_table[id]; + } +} + +let merge_last_x, merge_last_y; + +function merge(val) { + if (val === undefined) return; + + let x, y; + if (val.x === undefined) x = merge_last_x + 1; + else x = val.x; + if (val.y === undefined) y = merge_last_y; + else y = val.y; + merge_last_x = x; + merge_last_y = y; + + const entry = get(x, y); + + for (const prop in val) { + if (prop === "mon") { + entry[prop] = merge_monster(entry[prop], val[prop]); + } else if (prop === "t") { + entry[prop] = merge_objects(entry[prop], val[prop]); + + // The transparency flag is linked to the doll; + // if the doll changes, it is reset + if (val[prop].doll && val[prop].trans === undefined) + entry[prop].trans = false; + } else entry[prop] = val[prop]; + } + + touch(x, y); + + if (bounds) { + if (bounds.left > x) { + bounds.left = x; + bounds_changed = true; } - - function merge_monster(old_mon, mon) - { - if (old_mon && old_mon.refs) - { - old_mon.refs--; - } - - if (!mon) - { - return null; - } - - var id = mon.id; - - var last = monster_table[id]; - if (!last) - { - if (old_mon) - last = merge_objects(merge_objects({}, old_mon), mon); - else - { - last = mon; - set_monster_defaults(last); - } - } - else - { - merge_objects(last, mon); - } - - if (id) - { - last.refs = last.refs || 0; - last.refs++; - monster_table[id] = last; - } - - return last; + if (bounds.right < x) { + bounds.right = x; + bounds_changed = true; } - - function clean_monster_table() - { - for (var id in monster_table) - { - if (!monster_table[id].refs) - delete monster_table[id]; - } + if (bounds.top > y) { + bounds.top = y; + bounds_changed = true; } - - var merge_last_x, merge_last_y; - - function merge(val) - { - if (val === undefined) return; - - var x, y; - if (val.x === undefined) - x = merge_last_x + 1; - else - x = val.x; - if (val.y === undefined) - y = merge_last_y; - else - y = val.y; - merge_last_x = x; - merge_last_y = y; - - var entry = get(x, y); - - for (var prop in val) - { - if (prop == "mon") - { - entry[prop] = merge_monster(entry[prop], val[prop]); - } - else if (prop == "t") - { - entry[prop] = merge_objects(entry[prop], val[prop]); - - // The transparency flag is linked to the doll; - // if the doll changes, it is reset - if (val[prop].doll && val[prop].trans === undefined) - entry[prop].trans = false; - } - else - entry[prop] = val[prop]; - } - - touch(x, y); - - if (bounds) - { - if (bounds.left > x) - { - bounds.left = x; - bounds_changed = true; - } - if (bounds.right < x) - { - bounds.right = x; - bounds_changed = true; - } - if (bounds.top > y) - { - bounds.top = y; - bounds_changed = true; - } - if (bounds.bottom < y) - { - bounds.bottom = y; - bounds_changed = true; - } - } - else - { - bounds = { - left: x, - top: y, - right: x, - bottom: y - }; - } - + if (bounds.bottom < y) { + bounds.bottom = y; + bounds_changed = true; } - - function merge_diff(vals) - { - $.each(vals, function (i, val) - { - merge(val); - }); - - clean_monster_table(); - }; - - return { - get: get, - merge: merge_diff, - clear: clear, - touch: touch, - visible: visible, - player_on_level: function () { return player_on_level; }, - set_player_on_level: function (v) { player_on_level = v; }, - dirty: function () { return dirty_locs; }, - reset_dirty: function () { dirty_locs = []; }, - bounds: function () { return bounds; }, - reset_bounds_changed: function () { - var bc = bounds_changed; - bounds_changed = false; - return bc; - }, + } else { + bounds = { + left: x, + top: y, + right: x, + bottom: y, }; -}); + } +} + +function merge_diff(vals) { + $.each(vals, (_i, val) => { + merge(val); + }); + + clean_monster_table(); +} + +export default { + get: get, + merge: merge_diff, + clear: clear, + touch: touch, + visible: visible, + player_on_level: () => player_on_level, + set_player_on_level: (v) => { + player_on_level = v; + }, + dirty: () => dirty_locs, + reset_dirty: () => { + dirty_locs = []; + }, + bounds: () => bounds, + reset_bounds_changed: () => { + const bc = bounds_changed; + bounds_changed = false; + return bc; + }, +}; diff --git a/crawl-ref/source/webserver/client/game/src/menu.js b/crawl-ref/source/webserver/client/game/src/menu.js index fa892ffc057..4bf4f405060 100644 --- a/crawl-ref/source/webserver/client/game/src/menu.js +++ b/crawl-ref/source/webserver/client/game/src/menu.js @@ -1,1134 +1,989 @@ -define(["jquery", "comm", "client", "./ui", "./enums", "./cell_renderer", - "./util", "./options", "./scroller"], -function ($, comm, client, ui, enums, cr, util, options, scroller) { - "use strict"; - - // Helpers - - function item_selectable(item) - { - // TODO: the logic on the c++ side is somewhat different here - return item.level == 2 - // in the use item menu, selecting a non-hotkeyed item triggers - // relettering on the server - && (menu.tag == "use_item" || item.hotkeys && item.hotkeys.length); - } - - function item_text(item) - { - return item.text; - } - - function item_colour(item) - { - if (item.colour === undefined) - return 7; - return item.colour; - } - - function menu_title_indent() - { - if (!options.get("tile_menu_icons") - || options.get("tile_display_mode") !== "tiles" - || !(menu.tag === "ability" || menu.tag === "spell")) - return 0; - return 32 + 2; // menu
    has a 2px margin - } - - function set_item_contents(item, elem) - { - elem.html(util.formatted_string_to_html(item_text(item))); - elem.css('min-height', '0.5em'); - var col = item_colour(item); - elem.removeClass(); - elem.addClass("level" + item.level); - elem.addClass("fg" + col); - - if (item.level < 2) - elem.css("padding-left", menu_title_indent()+"px"); - - if (item_selectable(item)) - { - elem.addClass("selectable"); - elem.off("click.menu_item").off("contextmenu.menu_item"); - elem.on("click.menu_item", item_click_handler) - .on("contextmenu.menu_item", item_click_handler); - } - - if (item.tiles && item.tiles.length > 0 - && options.get("tile_display_mode") == "tiles") - { - var renderer = new cr.DungeonCellRenderer(); - var canvas = $(""); - util.init_canvas(canvas[0], renderer.cell_width, - renderer.cell_height); - canvas.css("vertical-align", "middle"); - renderer.init(canvas[0]); - - $.each(item.tiles, function () { - renderer.draw_from_texture(this.t, 0, 0, this.tex, 0, 0, this.ymax, false); - }); - - elem.prepend(canvas); - } - } - - var menu_stack = []; - var menu = null; - var update_server_scroll_timeout = null; - var menu_close_timeout = null; - var mouse_hover_suppressed = null; - - function add_hover_class(item) - { - if (item < 0 || item >= menu.items.length) - return; - menu.items[item].elem.addClass("hovered"); - } - - function remove_hover_class(item) - { - if (item < 0 || item >= menu.items.length) - return; - menu.items[item].elem.removeClass("hovered"); - } - - function mouse_set_hovered(index) - { - if (mouse_hover_suppressed) - return; - if (index >= 0 && - (index < menu.first_part_visible || index > menu.last_part_visible)) - { - return; - } - set_hovered(index, false, true); - } - - function clear_suppress() - { - mouse_hover_suppressed = null; - } - - function suppress_mouse_hover() - { - // ugh -- keep mouseenter from triggering, is there a better way? - if (mouse_hover_suppressed) - clearTimeout(mouse_hover_suppressed); - mouse_hover_suppressed = setTimeout(clear_suppress, 200); - } - - function set_hovered(index, snap=true, from_mouse=false) - { - if (index >= menu.items.length) - index = Math.max(0, menu.items.length - 1); - if (index == menu.last_hovered - && (index < 0 || item_selectable(menu.items[index]))) - { - // just make sure the hover class is set correctly - add_hover_class(menu.last_hovered); - return; - } - remove_hover_class(menu.last_hovered); - if (index < 0 || item_selectable(menu.items[index])) - { - menu.last_hovered = index; - add_hover_class(menu.last_hovered); - if (menu.last_hovered >= 0 && snap == true) - snap_in_page(menu.last_hovered); - comm.send_message("menu_hover", - { - hover: menu.last_hovered, - mouse: from_mouse - }); - } - } - - function menu_cleanup() - { - menu_stack = []; - menu = null; - if (update_server_scroll_timeout) - { - clearTimeout(update_server_scroll_timeout); - update_server_scroll_timeout = null; - } - if (menu_close_timeout) - { - clearTimeout(menu_close_timeout); - menu_close_timeout = null; - } - mouse_hover_suppressed = null; - } - - function display_menu() - { - var menu_div = $(".templates > .menu").clone(); - menu_div.addClass("menu_" + menu.tag); - menu.elem = menu_div; - - if (menu.type === "crt") - { - // Custom-drawn CRT menu - menu_div.removeClass("menu").addClass("menu_txt"); - ui.show_popup(menu_div, menu["ui-centred"]); - return; - } - - // Normal menu - menu_div.prepend("