diff --git a/demo/.swcrc b/demo/.swcrc index 3281a95..3b33e21 100644 --- a/demo/.swcrc +++ b/demo/.swcrc @@ -1,8 +1,14 @@ { "jsc": { "parser": { - "syntax": "typescript" + "syntax": "typescript", + "tsx": true }, - "target": "es2020" + "target": "es2020", + "transform": { + "react": { + "runtime": "automatic" + } + } } } diff --git a/demo/css/global.css b/demo/css/global.css deleted file mode 100644 index 1e8e22c..0000000 --- a/demo/css/global.css +++ /dev/null @@ -1,57 +0,0 @@ -* { - margin: 0; - padding: 0; - border: 0; - outline: 0; - box-sizing: border-box; - font: inherit; -} - -*::before, -*::after { - box-sizing: inherit; -} - -html, -body { - width: 100%; - height: 100%; - font-family: sans-serif; - font-size: 16px; - line-height: 1.2; - background-color: #fff; - color: #323232; -} - -.main-container { - padding: 1rem; - width: 100%; - min-width: 480px; - max-width: 640px; - margin: auto; -} - -.main-section + .main-section { - margin-top: 2rem; -} - -.canvas-wrapper { - position: relative; - border-radius: 0.25rem; - overflow: hidden; -} - -.canvas-wrapper .reload-button { - position: absolute; - top: 0.5rem; - right: 0.5rem; - padding: 0.5rem 1rem; - background-color: #d4dae0; - border-radius: 0.1rem; - opacity: 0.5; - z-index: 1; -} - -.canvas-wrapper .reload-button:hover { - opacity: 1; -} diff --git a/demo/css/index.css b/demo/css/index.css deleted file mode 100644 index 55070ff..0000000 --- a/demo/css/index.css +++ /dev/null @@ -1,2 +0,0 @@ -@import url('./global.css'); -@import url('./typography.css'); diff --git a/demo/css/typography.css b/demo/css/typography.css deleted file mode 100644 index 314396c..0000000 --- a/demo/css/typography.css +++ /dev/null @@ -1,61 +0,0 @@ -h1 { - font-size: 2em; -} - -h2 { - font-size: 1.5em; -} - -h3 { - font-size: 1.17em; -} - -h4 { - font-size: 1em; -} - -h5 { - font-size: 0.83em; -} - -h6 { - font-size: 0.67em; -} - -h1, -h2, -h3, -h4, -h5, -h6 { - font-weight: bold; - margin-bottom: 1em; -} - -a { - color: #54a0ff; - text-decoration: none; -} - -a:hover { - cursor: pointer; - text-decoration: underline; -} - -hr { - display: block; - width: auto; - height: 1px; - background-color: #eee; - margin: 1rem 0; -} - -p { - margin: 0.5em 0; -} - -canvas { - display: block; - width: 100%; - height: auto; -} diff --git a/demo/index.html b/demo/index.html deleted file mode 100644 index 8d9986f..0000000 --- a/demo/index.html +++ /dev/null @@ -1,58 +0,0 @@ - - - - - rglk.js demo - - -
-
-

rglk.js

-

Simple library for development roguelike games.

-

- API on - GitHub. -

-
-
-
-

Features

-

Dungeon

-

You can generate a random 2d dungeons consisting of walls and floors.

-

Simple dungeon (rooms with corridors)

-
- - -
-
-
-

Labyrinth (rooms with size 1x1)

-
- - -
-
-
-

Explorer

-

Explorer allows to define FOV based on raycasting algorytm.

-

Use mouse to see work...

-
- - -
-
-
-

Pathfinder

-

The work of pathfinder is based on the A* algorithm.

-
- - -
-
- -
- - diff --git a/demo/js/index.js b/demo/js/index.js deleted file mode 100644 index dc0bce6..0000000 --- a/demo/js/index.js +++ /dev/null @@ -1,219 +0,0 @@ -import { Dungeon, createExplorer, createPathfinder } from '../../src'; - -let mousePosition = { x: 0, y: 0 }; - -initSection('.js-section-dungeon', canvas => { - const dungeon = new Dungeon({ - roomsAmount: 12, - roomMinSize: 3, - roomMaxSize: 8, - corridorMinLength: 1, - corridorMaxLength: 4, - corridorComplexity: 3, - }); - - draw(canvas, { - dungeon, - roomColor: '#3c6382', - corridorColor: '#60a3bc', - }); -}); - -initSection('.js-section-labyrinth', canvas => { - const dungeon = new Dungeon({ - roomsAmount: 64, - roomMinSize: 1, - roomMaxSize: 1, - corridorMinLength: 1, - corridorMaxLength: 1, - corridorComplexity: 1, - }); - - draw(canvas, { - dungeon, - roomColor: '#4a69bd', - corridorColor: '#4a69bd', - }); -}); - -initSection('.js-section-explorer', canvas => { - const dungeon = new Dungeon({ - roomsAmount: 3, - roomMinSize: 5, - roomMaxSize: 7, - corridorMinLength: 1, - corridorMaxLength: 1, - corridorComplexity: 1, - }); - - const explore = createExplorer((x, y) => dungeon.isFloor(x, y)); - - const tileSize = calculateTileSize(dungeon, canvas); - - const fov = explore(parseInt(dungeon.rooms[0].center.x), parseInt(dungeon.rooms[0].center.y), 12); - - canvas.onmousemove = event => { - mousePosition = getMousePosition(event); - const dungeonPosition = { - x: Math.floor(mousePosition.x / tileSize), - y: Math.floor(mousePosition.y / tileSize), - }; - const newFov = explore(dungeonPosition.x, dungeonPosition.y, 12); - if (dungeon.isFloor(dungeonPosition.x, dungeonPosition.y)) { - draw(canvas, { - fov: newFov, - dungeon, - radius: 12, - roomColor: '#1e272e', - corridorColor: '#1e272e', - center: dungeonPosition, - }); - } - }; - - draw(canvas, { - fov, - dungeon, - radius: 12, - roomColor: '#1e272e', - corridorColor: '#1e272e', - center: dungeon.rooms[0].center, - }); -}); - -initSection('.js-section-pathfinder', canvas => { - const dungeon = new Dungeon({ - roomsAmount: 16, - roomMinSize: 4, - roomMaxSize: 12, - corridorMinLength: 1, - corridorMaxLength: 4, - corridorComplexity: 2, - }); - - const findPath = createPathfinder((x, y) => dungeon.isFloor(x, y)); - - const path = findPath( - parseInt(dungeon.rooms[0].center.x), - parseInt(dungeon.rooms[0].center.y), - parseInt(dungeon.rooms[dungeon.rooms.length - 1].center.x), - parseInt(dungeon.rooms[dungeon.rooms.length - 1].center.y), - ); - - draw(canvas, { - path, - dungeon, - roomColor: '#d4dae0', - corridorColor: '#d4dae0', - }); -}); - -function initSection(sectionSelector, render) { - const section = document.querySelector(sectionSelector); - - if (section) { - const canvas = section.querySelector('canvas'); - const button = section.querySelector('.js-reload-button'); - - if (canvas) { - canvas.width = canvas.clientWidth; - canvas.height = (canvas.width / 16) * 9; - render(canvas); - } - - if (button) { - button.addEventListener('click', () => render(canvas)); - } - } -} - -function draw(canvas, data) { - const context = canvas.getContext('2d'); - const { fov, path, dungeon } = data; - - data.tileSize = calculateTileSize(dungeon, canvas); - context.fillStyle = '#f4f5f7'; - context.fillRect(0, 0, canvas.width, canvas.height); - - dungeon && drawMap(context, data); - fov && drawFOV(context, data); - path && drawPath(context, data); -} - -function drawMap(context, data) { - const { dungeon, roomColor = '#82ccdd', corridorColor = '#82ccdd', tileSize } = data; - - // rooms - dungeon.rooms.forEach(room => { - context.fillStyle = roomColor; - context.fillRect( - room.x * tileSize, - room.y * tileSize, - room.width * tileSize, - room.height * tileSize, - ); - }); - - // corridors - dungeon.corridors.concat(dungeon.connectors).forEach(corridor => { - context.fillStyle = corridorColor; - context.fillRect( - corridor.x * tileSize, - corridor.y * tileSize, - corridor.width * tileSize, - corridor.height * tileSize, - ); - }); -} - -function drawFOV(context, data) { - const { fov, center, radius, tileSize } = data; - - fov.forEach(tile => { - const distance = Math.sqrt((center.x - tile.x) ** 2 + (center.y - tile.y) ** 2); - const proportion = 1 - distance / radius; - context.fillStyle = `rgba(255,210,150,${proportion})`; - context.globalAlpha = 0.7; - context.fillRect(tile.x * tileSize, tile.y * tileSize, tileSize, tileSize); - context.globalAlpha = 1; - }); -} - -function drawPath(context, { path, tileSize }) { - context.strokeStyle = '#e74c3c'; - context.fillStyle = '#e74c3c'; - context.lineWidth = Math.ceil(tileSize / 3); - - if (path.length) { - context.beginPath(); - context.moveTo(path[0].x * tileSize + tileSize / 2, path[0].y * tileSize + tileSize / 2); - path.forEach((point, index) => { - if (index === 0 || index === path.length - 1) { - context.fillRect(point.x * tileSize, point.y * tileSize, tileSize, tileSize); - } - context.lineTo(point.x * tileSize + tileSize / 2, point.y * tileSize + tileSize / 2); - }); - context.stroke(); - context.closePath(); - } -} - -function calculateTileSize(dungeon, canvas) { - const canvasMinSide = Math.min(canvas.width, canvas.height); - const dungeonMaxSide = Math.max(dungeon.width, dungeon.height); - - return Math.floor(canvasMinSide / dungeonMaxSide) || 8; -} - -function getMousePosition(event) { - const mousePosition = { x: 0, y: 0 }; - - if (event instanceof Event) { - const rect = event.target.getBoundingClientRect(); - - mousePosition.x = event.clientX - rect.left; - mousePosition.y = event.clientY - rect.top; - } - - return mousePosition; -} diff --git a/demo/package-lock.json b/demo/package-lock.json index 6eff2a3..171a638 100644 --- a/demo/package-lock.json +++ b/demo/package-lock.json @@ -8,6 +8,12 @@ "name": "rglk-demo-page", "version": "0.0.0", "license": "Apache-2.0", + "dependencies": { + "@types/react": "^18.0.8", + "@types/react-dom": "^18.0.3", + "react": "^18.1.0", + "react-dom": "^18.1.0" + }, "devDependencies": { "@swc/core": "^1.2.171", "css-loader": "^6.7.1", @@ -15,6 +21,7 @@ "html-webpack-plugin": "^5.5.0", "mini-css-extract-plugin": "^2.6.0", "swc-loader": "^0.2.0", + "typescript": "^4.6.4", "webpack": "^5.72.0", "webpack-cli": "^4.9.2", "webpack-dev-server": "^4.8.1" @@ -403,6 +410,11 @@ "integrity": "sha512-oNBIZjIqyHYP8VCNAV9uEytXVeXG2oR0w9lgAXro20eugRQfY002qr3CUl6BAe+Yf/z3CRjPdz27Pu6WWtuSRw==", "dev": true }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + }, "node_modules/@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", @@ -415,12 +427,35 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", "dev": true }, + "node_modules/@types/react": { + "version": "18.0.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.8.tgz", + "integrity": "sha512-+j2hk9BzCOrrOSJASi5XiOyBbERk9jG5O73Ya4M0env5Ixi6vUNli4qy994AINcEF+1IEHISYFfIT4zwr++LKw==", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.3.tgz", + "integrity": "sha512-1RRW9kst+67gveJRYPxGmVy8eVJ05O43hg77G2j5m76/RFJtMbcfAs2viQ2UNsvvDg8F7OfQZx8qQcl6ymygaQ==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "dev": true }, + "node_modules/@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" + }, "node_modules/@types/serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", @@ -1376,6 +1411,11 @@ "node": ">=8.0.0" } }, + "node_modules/csstype": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz", + "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==" + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -2413,6 +2453,11 @@ "node": ">= 10.13.0" } }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, "node_modules/json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -2488,6 +2533,17 @@ "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", "dev": true }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -3646,6 +3702,29 @@ "node": ">= 0.8" } }, + "node_modules/react": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.1.0.tgz", + "integrity": "sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.1.0.tgz", + "integrity": "sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.22.0" + }, + "peerDependencies": { + "react": "^18.1.0" + } + }, "node_modules/readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -3809,6 +3888,14 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "node_modules/scheduler": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.22.0.tgz", + "integrity": "sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/schema-utils": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", @@ -4463,6 +4550,19 @@ "node": ">= 0.6" } }, + "node_modules/typescript": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz", + "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -5141,6 +5241,11 @@ "integrity": "sha512-oNBIZjIqyHYP8VCNAV9uEytXVeXG2oR0w9lgAXro20eugRQfY002qr3CUl6BAe+Yf/z3CRjPdz27Pu6WWtuSRw==", "dev": true }, + "@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + }, "@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", @@ -5153,12 +5258,35 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", "dev": true }, + "@types/react": { + "version": "18.0.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.8.tgz", + "integrity": "sha512-+j2hk9BzCOrrOSJASi5XiOyBbERk9jG5O73Ya4M0env5Ixi6vUNli4qy994AINcEF+1IEHISYFfIT4zwr++LKw==", + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.3.tgz", + "integrity": "sha512-1RRW9kst+67gveJRYPxGmVy8eVJ05O43hg77G2j5m76/RFJtMbcfAs2viQ2UNsvvDg8F7OfQZx8qQcl6ymygaQ==", + "requires": { + "@types/react": "*" + } + }, "@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "dev": true }, + "@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" + }, "@types/serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", @@ -5897,6 +6025,11 @@ "css-tree": "^1.1.2" } }, + "csstype": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz", + "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==" + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -6675,6 +6808,11 @@ "supports-color": "^8.0.0" } }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -6738,6 +6876,14 @@ "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", "dev": true }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, "lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -7549,6 +7695,23 @@ } } }, + "react": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.1.0.tgz", + "integrity": "sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-dom": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.1.0.tgz", + "integrity": "sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==", + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.22.0" + } + }, "readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -7662,6 +7825,14 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "scheduler": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.22.0.tgz", + "integrity": "sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ==", + "requires": { + "loose-envify": "^1.1.0" + } + }, "schema-utils": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", @@ -8165,6 +8336,12 @@ "mime-types": "~2.1.24" } }, + "typescript": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz", + "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==", + "dev": true + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/demo/package.json b/demo/package.json index 2e34c59..fd8275e 100644 --- a/demo/package.json +++ b/demo/package.json @@ -5,7 +5,8 @@ "description": "Demo page of rglk library", "scripts": { "start": "webpack serve --mode=development", - "build": "webpack --mode=production" + "build": "webpack --mode=production", + "type-check": "tsc -p . --noEmit" }, "author": "Dmitry Petrov", "license": "Apache-2.0", @@ -16,8 +17,15 @@ "html-webpack-plugin": "^5.5.0", "mini-css-extract-plugin": "^2.6.0", "swc-loader": "^0.2.0", + "typescript": "^4.6.4", "webpack": "^5.72.0", "webpack-cli": "^4.9.2", "webpack-dev-server": "^4.8.1" + }, + "dependencies": { + "@types/react": "^18.0.8", + "@types/react-dom": "^18.0.3", + "react": "^18.1.0", + "react-dom": "^18.1.0" } } diff --git a/demo/src/components/app/app.module.css b/demo/src/components/app/app.module.css new file mode 100644 index 0000000..d92c5d5 --- /dev/null +++ b/demo/src/components/app/app.module.css @@ -0,0 +1,25 @@ +.wrapper { + padding: 0 32px; +} + +.inner { + margin: 0 auto; + padding: 20px 0; + max-width: 800px; +} + +.header { + padding: 40px 0; + text-align: center; + margin-bottom: 48px; +} + +.title { + font-size: 48px; + margin-bottom: 16px; +} + +.footer { + margin-top: 20px; + text-align: right; +} diff --git a/demo/src/components/app/index.tsx b/demo/src/components/app/index.tsx new file mode 100644 index 0000000..ca512e3 --- /dev/null +++ b/demo/src/components/app/index.tsx @@ -0,0 +1,28 @@ +import { SectionDungeon } from '../dungeon'; +import { SectionLabyrinth } from '../labyrinth'; +import { SectionFOV } from '../fov'; +import styles from './app.module.css'; +import { SectionPathfinder } from '../pathfinder'; + +export function App() { + return ( +
+
+
+

rglk.js

+

+ Simple library for development roguelike games.
Check API on{' '} + GitHub. +

+
+ + + + + + + +
+
+ ); +} diff --git a/demo/src/components/canvas/canvas.module.css b/demo/src/components/canvas/canvas.module.css new file mode 100644 index 0000000..506e865 --- /dev/null +++ b/demo/src/components/canvas/canvas.module.css @@ -0,0 +1,22 @@ +.root { + position: relative; + overflow: hidden; + border-radius: 4px; + background: #eee; +} + +.canvas { + display: block; + width: 100%; + height: auto; +} + +.button { + position: absolute; + bottom: 12px; + right: 12px; + padding: 8px 20px; + border-radius: 4px; + background: #d4dae0; + cursor: pointer; +} diff --git a/demo/src/components/canvas/index.tsx b/demo/src/components/canvas/index.tsx new file mode 100644 index 0000000..a56c7ab --- /dev/null +++ b/demo/src/components/canvas/index.tsx @@ -0,0 +1,50 @@ +import { forwardRef, useEffect, useImperativeHandle, useRef, CanvasHTMLAttributes } from 'react'; +import styles from './canvas.module.css'; + +export interface CanvasData { + canvas: HTMLCanvasElement; + context: CanvasRenderingContext2D; +} + +export interface CanvasProps extends CanvasHTMLAttributes { + onReload?: () => void; +} + +export const Canvas = forwardRef(function Canvas( + { onReload, ...props }, + ref, +) { + const canvasRef = useRef(null); + const contextRef = useRef(null); + + useEffect(() => { + if (canvasRef.current) { + canvasRef.current.width = canvasRef.current.clientWidth; + canvasRef.current.height = (canvasRef.current.width / 16) * 9; + contextRef.current = canvasRef.current.getContext('2d'); + } + }); + + useImperativeHandle(ref, () => { + if (canvasRef.current) { + contextRef.current = canvasRef.current.getContext('2d'); + } + + if (canvasRef.current && contextRef.current) { + return { canvas: canvasRef.current, context: contextRef.current }; + } else { + return null; + } + }); + + return ( +
+ {onReload && ( + + )} + +
+ ); +}); diff --git a/demo/src/components/dungeon.tsx b/demo/src/components/dungeon.tsx new file mode 100644 index 0000000..b07ec49 --- /dev/null +++ b/demo/src/components/dungeon.tsx @@ -0,0 +1,40 @@ +import { useEffect, useRef, useState } from 'react'; +import { Dungeon } from '../../../src'; +import { Canvas, CanvasData } from './canvas'; +import { Section } from './section'; +import { calculateFitFactor } from './utils'; + +export function SectionDungeon() { + const [version, setVersion] = useState(0); + const viewRef = useRef(null); + + useEffect(() => { + if (viewRef.current) { + const { canvas, context } = viewRef.current; + + const dungeon = new Dungeon({ + roomsAmount: 12, + roomMinSize: 3, + roomMaxSize: 8, + corridorMinLength: 1, + corridorMaxLength: 4, + corridorComplexity: 3, + }); + + const tileSize = calculateFitFactor(dungeon, canvas); + + dungeon.forEachTile((x, y, isFloor) => { + if (isFloor) { + context.fillStyle = '#423a59'; + context.fillRect(x * tileSize, y * tileSize, tileSize, tileSize); + } + }); + } + }, [version]); + + return ( +
+ setVersion(v => v + 1)} /> +
+ ); +} diff --git a/demo/src/components/fov.tsx b/demo/src/components/fov.tsx new file mode 100644 index 0000000..39d55cd --- /dev/null +++ b/demo/src/components/fov.tsx @@ -0,0 +1,97 @@ +import { MouseEvent, useEffect, useRef, useState } from 'react'; +import { createExplorer } from '../../../src'; +import { Canvas, CanvasData } from './canvas'; +import { Section } from './section'; +import { calculateFitFactor } from './utils'; + +export function SectionFOV() { + const [mousePosition, setState] = useState<{ x: number; y: number }>({ x: 0, y: 0 }); + const viewRef = useRef(null); + + useEffect(() => { + if (viewRef.current) { + const { canvas, context } = viewRef.current; + + const fovRadius = 24; + const map = createMap(); + const explore = createExplorer((x, y) => map.tiles[x]?.[y] === 0); + + // draw + const tileSize = calculateFitFactor(map, canvas); + const center = { + x: Math.floor(mousePosition.x / tileSize), + y: Math.floor(mousePosition.y / tileSize), + }; + + context.fillStyle = '#252033'; + context.fillRect(0, 0, canvas.width, canvas.height); + + for (const visibleTile of explore(center.x, center.y, fovRadius)) { + const distance = Math.sqrt( + (center.x - visibleTile.x) ** 2 + (center.y - visibleTile.y) ** 2, + ); + + context.fillStyle = `rgba(255, 210, 150, ${1 - distance / fovRadius})`; + context.globalAlpha = 0.7; + context.fillRect(visibleTile.x * tileSize, visibleTile.y * tileSize, tileSize, tileSize); + context.globalAlpha = 1; + } + } + }, [mousePosition]); + + return ( +
+ setState(getMousePosition(e))} /> +
+ ); +} + +function getMousePosition(event: MouseEvent) { + const mousePosition = { x: 0, y: 0 }; + + if (event.target instanceof HTMLCanvasElement) { + const rect = event.target.getBoundingClientRect(); + + mousePosition.x = event.clientX - rect.left; + mousePosition.y = event.clientY - rect.top; + } + + return mousePosition; +} + +function createMap() { + const tiles: number[][] = []; + const width = 60; + const height = 33; + + // init map area + for (let x = 0; x < width; x++) { + const column: number[] = []; + + for (let y = 0; y < height; y++) { + column[y] = 0; + } + + tiles[x] = column; + } + + // prepare some squares + const rectangles: Array<{ x: number; y: number; w: number; h: number }> = [ + { x: 10, y: 10, w: 10, h: 1 }, + { x: 50, y: 10, w: 1, h: 10 }, + { x: 25, y: 16, w: 5, h: 5 }, + ]; + + // add squares on map + for (const rect of rectangles) { + for (let x = rect.x; x < rect.x + rect.w; x++) { + for (let y = rect.y; y < rect.y + rect.h; y++) { + if (tiles[x]) { + tiles[x][y] = 1; + } + } + } + } + + return { tiles, width, height }; +} diff --git a/demo/src/components/labyrinth.tsx b/demo/src/components/labyrinth.tsx new file mode 100644 index 0000000..d9f1f2c --- /dev/null +++ b/demo/src/components/labyrinth.tsx @@ -0,0 +1,40 @@ +import { useEffect, useRef, useState } from 'react'; +import { Dungeon } from '../../../src'; +import { Canvas, CanvasData } from './canvas'; +import { Section } from './section'; +import { calculateFitFactor } from './utils'; + +export function SectionLabyrinth() { + const [version, setVersion] = useState(0); + const viewRef = useRef(null); + + useEffect(() => { + if (viewRef.current) { + const { canvas, context } = viewRef.current; + + const labyrinth = new Dungeon({ + roomsAmount: 64, + roomMinSize: 1, + roomMaxSize: 1, + corridorMinLength: 1, + corridorMaxLength: 1, + corridorComplexity: 3, + }); + + const tileSize = calculateFitFactor(labyrinth, canvas); + + labyrinth.forEachTile((x, y, isFloor) => { + if (isFloor) { + context.fillStyle = '#423a59'; + context.fillRect(x * tileSize, y * tileSize, tileSize, tileSize); + } + }); + } + }, [version]); + + return ( +
+ setVersion(v => v + 1)} /> +
+ ); +} diff --git a/demo/src/components/pathfinder.tsx b/demo/src/components/pathfinder.tsx new file mode 100644 index 0000000..e0db031 --- /dev/null +++ b/demo/src/components/pathfinder.tsx @@ -0,0 +1,69 @@ +import { useEffect, useRef, useState } from 'react'; +import { createPathfinder, Dungeon } from '../../../src'; +import { calculateFitFactor } from './utils'; +import { Canvas, CanvasData } from './canvas'; +import { Section } from './section'; + +export function SectionPathfinder() { + const [version, setVersion] = useState(0); + const viewRef = useRef(null); + + useEffect(() => { + if (viewRef.current) { + const { canvas, context } = viewRef.current; + + const dungeon = new Dungeon({ + roomsAmount: 16, + roomMinSize: 4, + roomMaxSize: 12, + corridorMinLength: 1, + corridorMaxLength: 4, + corridorComplexity: 2, + }); + + const findPath = createPathfinder((x, y) => dungeon.isFloor(x, y)); + + const tileSize = calculateFitFactor(dungeon, canvas); + + dungeon.forEachTile((x, y, isFloor) => { + if (isFloor) { + context.fillStyle = '#423a59'; + context.fillRect(x * tileSize, y * tileSize, tileSize, tileSize); + } + }); + + context.strokeStyle = '#f00'; + context.fillStyle = '#f00'; + context.lineWidth = Math.ceil(tileSize / 3); + + // draw path + const path = findPath( + Math.floor(dungeon.rooms[0].center.x), + Math.floor(dungeon.rooms[0].center.y), + Math.floor(dungeon.rooms[dungeon.rooms.length - 1].center.x), + Math.floor(dungeon.rooms[dungeon.rooms.length - 1].center.y), + ); + + console.log(path); + + if (path.length) { + context.beginPath(); + context.moveTo(path[0].x * tileSize + tileSize / 2, path[0].y * tileSize + tileSize / 2); + path.forEach((point, index) => { + if (index === 0 || index === path.length - 1) { + context.fillRect(point.x * tileSize, point.y * tileSize, tileSize, tileSize); + } + context.lineTo(point.x * tileSize + tileSize / 2, point.y * tileSize + tileSize / 2); + }); + context.stroke(); + context.closePath(); + } + } + }, [version]); + + return ( +
+ setVersion(v => v + 1)} /> +
+ ); +} diff --git a/demo/src/components/section/index.tsx b/demo/src/components/section/index.tsx new file mode 100644 index 0000000..5f355da --- /dev/null +++ b/demo/src/components/section/index.tsx @@ -0,0 +1,11 @@ +import { ReactNode } from 'react'; +import styles from './section.module.css'; + +export function Section({ title, children }: { title: string; children?: ReactNode }) { + return ( +
+

{title}

+
{children}
+
+ ); +} diff --git a/demo/src/components/section/section.module.css b/demo/src/components/section/section.module.css new file mode 100644 index 0000000..023fc96 --- /dev/null +++ b/demo/src/components/section/section.module.css @@ -0,0 +1,7 @@ +.root + .root { + margin-top: 48px; +} + +.title { + margin-bottom: 12px; +} diff --git a/demo/src/components/utils.ts b/demo/src/components/utils.ts new file mode 100644 index 0000000..533692b --- /dev/null +++ b/demo/src/components/utils.ts @@ -0,0 +1,6 @@ +export function calculateFitFactor( + map: { width: number; height: number }, + canvas: HTMLCanvasElement, +): number { + return Math.floor(Math.min(canvas.width / map.width, canvas.height / map.height)); +} diff --git a/demo/src/custom.d.ts b/demo/src/custom.d.ts new file mode 100644 index 0000000..3d673e2 --- /dev/null +++ b/demo/src/custom.d.ts @@ -0,0 +1,4 @@ +declare module '*.module.css' { + const classes: { [key: string]: string }; + export default classes; +} diff --git a/demo/src/index.css b/demo/src/index.css new file mode 100644 index 0000000..137bc3d --- /dev/null +++ b/demo/src/index.css @@ -0,0 +1,62 @@ +* { + margin: 0; + padding: 0; + border: 0; + outline: 0; + box-sizing: border-box; + font: inherit; + line-height: inherit; +} + +html, +body { + width: 100%; + height: 100%; +} + +body { + color: #323232; + background-color: #fff; + font-size: 16px; + line-height: 1.5; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, + 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; +} + +h1 { + font-size: 2em; +} + +h2 { + font-size: 1.5em; +} + +h3 { + font-size: 1.17em; +} + +h4 { + font-size: 1em; +} + +h5 { + font-size: 0.83em; +} + +h6 { + font-size: 0.67em; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-weight: bold; +} + +a { + color: #54a0ff; + text-decoration: none; +} diff --git a/demo/src/index.html b/demo/src/index.html new file mode 100644 index 0000000..ae89a5b --- /dev/null +++ b/demo/src/index.html @@ -0,0 +1,10 @@ + + + + + rglk.js demo + + +
+ + diff --git a/demo/src/index.tsx b/demo/src/index.tsx new file mode 100644 index 0000000..1281ecc --- /dev/null +++ b/demo/src/index.tsx @@ -0,0 +1,15 @@ +import { createRoot } from 'react-dom/client'; +import { App } from './components/app'; +import './index.css'; + +window.addEventListener('DOMContentLoaded', () => { + const container = document.querySelector('#main'); + + if (container) { + const root = createRoot(container); + + root.render(); + } else { + throw Error('Container element not found'); + } +}); diff --git a/demo/tsconfig.json b/demo/tsconfig.json new file mode 100644 index 0000000..cc27883 --- /dev/null +++ b/demo/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "target": "es2020", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "skipLibCheck": true + }, + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/demo/webpack.config.js b/demo/webpack.config.js index 44bb089..5a161f8 100644 --- a/demo/webpack.config.js +++ b/demo/webpack.config.js @@ -4,27 +4,44 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); const config = { - entry: ['./js/index.js', './css/index.css'], + entry: './src/index.tsx', output: { path: path.resolve(__dirname, 'dist'), filename: 'index.js', }, resolve: { - extensions: ['.ts', '.js'], + extensions: ['.js', '.jsx', '.ts', '.tsx'], }, module: { rules: [ { - test: /\.(js|ts)$/, + test: /\.(js|jsx|ts|tsx)$/, exclude: /node_modules/, use: { loader: 'swc-loader', }, }, + + // css { test: /\.css$/i, + exclude: /\.module\.css$/i, use: [MiniCssExtractPlugin.loader, 'css-loader'], }, + + // css modules + { + test: /\.module\.css$/i, + use: [ + MiniCssExtractPlugin.loader, + { + loader: 'css-loader', + options: { + modules: true, + }, + }, + ], + }, ], }, plugins: [ @@ -33,7 +50,7 @@ const config = { }), new HtmlPlugin({ publicPath: './', - template: path.resolve(__dirname, 'index.html'), + template: path.resolve(__dirname, 'src/index.html'), }), ], optimization: {