Implement complete inventory system with equipment database

Features:
- HP Control component with damage/heal/direct modes (mobile-optimized)
- Conditions system with PF2e condition database
- Equipment database with 5,482 items from PF2e (weapons, armor, equipment)
- AddItemModal with search, category filters, and pagination
- Bulk tracking with encumbered/overburdened status display
- Item management (add, remove, toggle equipped)

Backend:
- Equipment module with search/filter endpoints
- Prisma migration for equipment detail fields
- Equipment seed script importing from JSON data files
- Extended Equipment model (damage, hands, AC, etc.)

Frontend:
- New components: HpControl, AddConditionModal, AddItemModal
- Improved character sheet with tabbed interface
- API methods for equipment search and item management

Documentation:
- CLAUDE.md with project philosophy and architecture decisions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-19 01:55:01 +01:00
parent 94335ecd12
commit e60a8df4f0
26 changed files with 59218 additions and 315 deletions

572
server/package-lock.json generated
View File

@@ -59,6 +59,7 @@
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.21.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
}
@@ -258,6 +259,7 @@
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.28.6",
"@babel/generator": "^7.28.6",
@@ -839,7 +841,8 @@
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.2.tgz",
"integrity": "sha512-zfWWa+V2ViDCY/cmUfRqeWY1yLto+EpxjXnZzenB1TyxsTiXaTWeZFIZw6mac52BsuQm0RjCnisjBtdBaXOI6w==",
"devOptional": true,
"license": "Apache-2.0"
"license": "Apache-2.0",
"peer": true
},
"node_modules/@electric-sql/pglite-socket": {
"version": "0.0.6",
@@ -898,6 +901,448 @@
"tslib": "^2.4.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
"integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
"integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
"integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
"integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
"integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
"integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
"integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
"integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
"integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
"integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
"integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
"integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
"integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
"integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
"integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
"integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
"integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
"integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
"integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
"integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
"integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
"integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
"integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
"integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
"integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
"integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
@@ -2279,6 +2724,7 @@
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.12.tgz",
"integrity": "sha512-v6U3O01YohHO+IE3EIFXuRuu3VJILWzyMmSYZXpyBbnp0hk0mFyHxK2w3dF4I5WnbwiRbWlEXdeXFvPQ7qaZzw==",
"license": "MIT",
"peer": true,
"dependencies": {
"file-type": "21.3.0",
"iterare": "1.2.1",
@@ -2338,6 +2784,7 @@
"integrity": "sha512-97DzTYMf5RtGAVvX1cjwpKRiCUpkeQ9CCzSAenqkAhOmNVVFaApbhuw+xrDt13rsCa2hHVOYPrV4dBgOYMJjsA==",
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@nuxt/opencollective": "0.4.1",
"fast-safe-stringify": "2.1.1",
@@ -2421,6 +2868,7 @@
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.12.tgz",
"integrity": "sha512-GYK/vHI0SGz5m8mxr7v3Urx8b9t78Cf/dj5aJMZlGd9/1D9OI1hAl00BaphjEXINUJ/BQLxIlF2zUjrYsd6enQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"cors": "2.8.5",
"express": "5.2.1",
@@ -2442,6 +2890,7 @@
"resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.12.tgz",
"integrity": "sha512-1itTTYsAZecrq2NbJOkch32y8buLwN7UpcNRdJrhlS+ovJ5GxLx3RyJ3KylwBhbYnO5AeYyL1U/i4W5mg/4qDA==",
"license": "MIT",
"peer": true,
"dependencies": {
"socket.io": "4.8.3",
"tslib": "2.8.1"
@@ -2620,6 +3069,7 @@
"resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.1.12.tgz",
"integrity": "sha512-ulSOYcgosx1TqY425cRC5oXtAu1R10+OSmVfgyR9ueR25k4luekURt8dzAZxhxSCI0OsDj9WKCFLTkEuAwg0wg==",
"license": "MIT",
"peer": true,
"dependencies": {
"iterare": "1.2.1",
"object-hash": "3.0.0",
@@ -3092,6 +3542,7 @@
"integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "*",
"@types/json-schema": "*"
@@ -3230,6 +3681,7 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz",
"integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -3411,6 +3863,7 @@
"integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.53.0",
"@typescript-eslint/types": "8.53.0",
@@ -4092,6 +4545,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4141,6 +4595,7 @@
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -4583,6 +5038,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -4844,6 +5300,7 @@
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"readdirp": "^4.0.1"
},
@@ -4901,13 +5358,15 @@
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/class-validator": {
"version": "0.14.3",
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz",
"integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/validator": "^13.15.3",
"libphonenumber-js": "^1.11.1",
@@ -5261,8 +5720,7 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"devOptional": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.3",
@@ -5691,6 +6149,48 @@
"node": ">= 0.4"
}
},
"node_modules/esbuild": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
"integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.2",
"@esbuild/android-arm": "0.27.2",
"@esbuild/android-arm64": "0.27.2",
"@esbuild/android-x64": "0.27.2",
"@esbuild/darwin-arm64": "0.27.2",
"@esbuild/darwin-x64": "0.27.2",
"@esbuild/freebsd-arm64": "0.27.2",
"@esbuild/freebsd-x64": "0.27.2",
"@esbuild/linux-arm": "0.27.2",
"@esbuild/linux-arm64": "0.27.2",
"@esbuild/linux-ia32": "0.27.2",
"@esbuild/linux-loong64": "0.27.2",
"@esbuild/linux-mips64el": "0.27.2",
"@esbuild/linux-ppc64": "0.27.2",
"@esbuild/linux-riscv64": "0.27.2",
"@esbuild/linux-s390x": "0.27.2",
"@esbuild/linux-x64": "0.27.2",
"@esbuild/netbsd-arm64": "0.27.2",
"@esbuild/netbsd-x64": "0.27.2",
"@esbuild/openbsd-arm64": "0.27.2",
"@esbuild/openbsd-x64": "0.27.2",
"@esbuild/openharmony-arm64": "0.27.2",
"@esbuild/sunos-x64": "0.27.2",
"@esbuild/win32-arm64": "0.27.2",
"@esbuild/win32-ia32": "0.27.2",
"@esbuild/win32-x64": "0.27.2"
}
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -5726,6 +6226,7 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -5786,6 +6287,7 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -6018,6 +6520,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
@@ -6556,6 +7059,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-tsconfig": {
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
"integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
},
"funding": {
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/giget": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
@@ -6755,6 +7271,7 @@
"integrity": "sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g==",
"devOptional": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=16.9.0"
}
@@ -7141,6 +7658,7 @@
"integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/core": "30.2.0",
"@jest/types": "30.2.0",
@@ -8939,6 +9457,7 @@
"resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
"integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"passport-strategy": "1.x.x",
"pause": "0.0.1",
@@ -9071,6 +9590,7 @@
"resolved": "https://registry.npmjs.org/pg/-/pg-8.17.1.tgz",
"integrity": "sha512-EIR+jXdYNSMOrpRp7g6WgQr7SaZNZfS7IzZIO0oTNEeibq956JxeD15t3Jk3zZH0KH8DmOIx38qJfQenoE8bXQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"pg-connection-string": "^2.10.0",
"pg-pool": "^3.11.0",
@@ -9354,6 +9874,7 @@
"integrity": "sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -9412,6 +9933,7 @@
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@prisma/config": "7.2.0",
"@prisma/dev": "0.17.0",
@@ -9622,7 +10144,8 @@
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
"license": "Apache-2.0"
"license": "Apache-2.0",
"peer": true
},
"node_modules/regexp-to-ast": {
"version": "0.5.0",
@@ -9707,6 +10230,16 @@
"node": ">=4"
}
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/restore-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
@@ -9759,6 +10292,7 @@
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"tslib": "^2.1.0"
}
@@ -9794,8 +10328,7 @@
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"devOptional": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/schema-utils": {
"version": "3.3.0",
@@ -10504,6 +11037,7 @@
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -10847,6 +11381,7 @@
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
@@ -10932,6 +11467,26 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/tsx": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5"
},
"bin": {
"tsx": "dist/cli.mjs"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
}
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -10994,6 +11549,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -11275,6 +11831,7 @@
"integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8",
@@ -11344,6 +11901,7 @@
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",

View File

@@ -8,8 +8,13 @@
"scripts": {
"build": "nest build",
"db:generate": "prisma generate",
"db:push": "prisma db push",
"db:migrate:dev": "prisma migrate dev",
"db:migrate:deploy": "prisma migrate deploy",
"db:migrate:reset": "prisma migrate reset",
"db:migrate:status": "prisma migrate status",
"db:studio": "prisma studio",
"db:seed": "tsx prisma/seed.ts",
"db:seed:equipment": "tsx prisma/seed-equipment.ts",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
@@ -73,6 +78,7 @@
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.21.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,16 @@
-- AlterTable
ALTER TABLE "Equipment" ADD COLUMN "ac" INTEGER,
ADD COLUMN "armorCategory" TEXT,
ADD COLUMN "armorGroup" TEXT,
ADD COLUMN "checkPenalty" INTEGER,
ADD COLUMN "damageType" TEXT,
ADD COLUMN "dexCap" INTEGER,
ADD COLUMN "duration" TEXT,
ADD COLUMN "reload" TEXT,
ADD COLUMN "shieldBt" INTEGER,
ADD COLUMN "shieldHardness" INTEGER,
ADD COLUMN "shieldHp" INTEGER,
ADD COLUMN "speedPenalty" INTEGER,
ADD COLUMN "strength" INTEGER,
ADD COLUMN "usage" TEXT,
ADD COLUMN "weaponGroup" TEXT;

View File

@@ -482,18 +482,41 @@ model Equipment {
id String @id @default(uuid())
name String @unique
traits String[]
itemCategory String
itemCategory String // "Weapons", "Armor", "Consumables", "Shields", etc.
itemSubcategory String?
bulk String?
bulk String? // "L" for light, "1", "2", etc.
url String?
summary String?
activation String?
hands String?
damage String?
range String?
weaponCategory String?
price Int? // In CP
level Int?
price Int? // In CP
// Weapon-specific fields
hands String?
damage String? // "1d8 S", "1d6 P", etc.
damageType String? // "S", "P", "B" (Slashing, Piercing, Bludgeoning)
range String?
reload String?
weaponCategory String? // "Simple", "Martial", "Advanced", "Ammunition"
weaponGroup String? // "Sword", "Axe", "Bow", etc.
// Armor-specific fields
ac Int?
dexCap Int?
checkPenalty Int?
speedPenalty Int?
strength Int? // Strength requirement
armorCategory String? // "Unarmored", "Light", "Medium", "Heavy"
armorGroup String? // "Leather", "Chain", "Plate", etc.
// Shield-specific fields
shieldHp Int?
shieldHardness Int?
shieldBt Int? // Broken Threshold
// Consumable/Equipment-specific fields
activation String? // "Cast A Spell", "[one-action]", etc.
duration String?
usage String?
characterItems CharacterItem[]
}

View File

@@ -0,0 +1,254 @@
import 'dotenv/config';
import * as fs from 'fs';
import * as path from 'path';
import { PrismaClient } from '../src/generated/prisma/client.js';
import { PrismaPg } from '@prisma/adapter-pg';
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
const prisma = new PrismaClient({ adapter });
interface WeaponJson {
name: string;
trait: string;
item_category: string;
item_subcategory: string;
bulk: string;
url: string;
summary: string;
hands?: string;
damage?: string;
range?: string;
weapon_category?: string;
}
interface ArmorJson {
name: string;
trait: string;
item_category: string;
item_subcategory: string;
bulk: string;
url: string;
summary: string;
ac?: string;
dex_cap?: string;
}
interface EquipmentJson {
name: string;
trait: string;
item_category: string;
item_subcategory: string;
bulk: string;
url: string;
summary: string;
activation?: string;
}
function parseTraits(traitString: string): string[] {
if (!traitString || traitString.trim() === '') return [];
return traitString.split(',').map(t => t.trim()).filter(t => t.length > 0);
}
function parseDamage(damageStr: string): { damage: string | null; damageType: string | null } {
if (!damageStr || damageStr.trim() === '') return { damage: null, damageType: null };
// Parse strings like "1d8 S", "1d6 P", "2d6 B"
const match = damageStr.match(/^(.+?)\s+([SPB])$/i);
if (match) {
return { damage: match[1].trim(), damageType: match[2].toUpperCase() };
}
return { damage: damageStr, damageType: null };
}
function parseNumber(str: string | undefined): number | null {
if (!str || str.trim() === '') return null;
const num = parseInt(str, 10);
return isNaN(num) ? null : num;
}
async function seedWeapons() {
const dataPath = path.join(__dirname, 'data', 'weapons.json');
const data: WeaponJson[] = JSON.parse(fs.readFileSync(dataPath, 'utf-8'));
console.log(`⚔️ Importing ${data.length} weapons...`);
let created = 0;
let updated = 0;
let errors = 0;
for (const item of data) {
try {
const { damage, damageType } = parseDamage(item.damage || '');
// Check if exists first
const existing = await prisma.equipment.findUnique({ where: { name: item.name } });
if (existing) {
// Update with weapon-specific fields
await prisma.equipment.update({
where: { name: item.name },
data: {
hands: item.hands || existing.hands,
damage: damage || existing.damage,
damageType: damageType || existing.damageType,
range: item.range || existing.range,
weaponCategory: item.weapon_category || existing.weaponCategory,
// Don't overwrite traits/summary if already set
traits: existing.traits.length > 0 ? existing.traits : parseTraits(item.trait),
summary: existing.summary || item.summary || null,
},
});
updated++;
} else {
await prisma.equipment.create({
data: {
name: item.name,
traits: parseTraits(item.trait),
itemCategory: item.item_category || 'Weapons',
itemSubcategory: item.item_subcategory || null,
bulk: item.bulk || null,
url: item.url || null,
summary: item.summary || null,
hands: item.hands || null,
damage,
damageType,
range: item.range || null,
weaponCategory: item.weapon_category || null,
},
});
created++;
}
} catch (error: any) {
if (errors === 0) {
// Print full error for first failure only
console.log(` ⚠️ First error for "${item.name}":`);
console.log(error.message);
}
errors++;
}
}
console.log(` ✅ Created: ${created}, Updated: ${updated}, Errors: ${errors}`);
}
async function seedArmor() {
const dataPath = path.join(__dirname, 'data', 'armor.json');
const data: ArmorJson[] = JSON.parse(fs.readFileSync(dataPath, 'utf-8'));
console.log(`🛡️ Importing ${data.length} armor items...`);
let created = 0;
let updated = 0;
let errors = 0;
for (const item of data) {
try {
const existing = await prisma.equipment.findUnique({ where: { name: item.name } });
if (existing) {
// Update with armor-specific fields
await prisma.equipment.update({
where: { name: item.name },
data: {
ac: parseNumber(item.ac) ?? existing.ac,
dexCap: parseNumber(item.dex_cap) ?? existing.dexCap,
traits: existing.traits.length > 0 ? existing.traits : parseTraits(item.trait),
summary: existing.summary || item.summary || null,
},
});
updated++;
} else {
await prisma.equipment.create({
data: {
name: item.name,
traits: parseTraits(item.trait),
itemCategory: item.item_category || 'Armor',
itemSubcategory: item.item_subcategory || null,
bulk: item.bulk || null,
url: item.url || null,
summary: item.summary || null,
ac: parseNumber(item.ac),
dexCap: parseNumber(item.dex_cap),
},
});
created++;
}
} catch (error: any) {
if (errors < 3) {
console.log(` ⚠️ Error for "${item.name}": ${error.message?.slice(0, 100)}`);
}
errors++;
}
}
console.log(` ✅ Created: ${created}, Updated: ${updated}, Errors: ${errors}`);
}
async function seedEquipment() {
const dataPath = path.join(__dirname, 'data', 'equipment.json');
const data: EquipmentJson[] = JSON.parse(fs.readFileSync(dataPath, 'utf-8'));
console.log(`📦 Importing ${data.length} equipment items...`);
let created = 0;
let updated = 0;
let errors = 0;
for (const item of data) {
try {
await prisma.equipment.upsert({
where: { name: item.name },
update: {
traits: parseTraits(item.trait),
itemCategory: item.item_category || 'Equipment',
itemSubcategory: item.item_subcategory || null,
bulk: item.bulk || null,
url: item.url || null,
summary: item.summary || null,
activation: item.activation || null,
},
create: {
name: item.name,
traits: parseTraits(item.trait),
itemCategory: item.item_category || 'Equipment',
itemSubcategory: item.item_subcategory || null,
bulk: item.bulk || null,
url: item.url || null,
summary: item.summary || null,
activation: item.activation || null,
},
});
created++;
} catch (error) {
// Item with same name already exists - count as update attempt
updated++;
}
}
console.log(` ✅ Created: ${created}, Duplicates: ${updated}, Errors: ${errors}`);
}
async function main() {
console.log('🗃️ Seeding Pathfinder 2e Equipment Database...\n');
const startTime = Date.now();
// WICHTIG: Equipment zuerst, dann Waffen/Rüstung um spezifische Felder zu ergänzen
await seedEquipment();
await seedWeapons(); // Ergänzt damage, hands, weapon_category etc.
await seedArmor(); // Ergänzt ac, dex_cap etc.
const totalCount = await prisma.equipment.count();
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
console.log(`\n✅ Equipment database seeded successfully!`);
console.log(` Total items in database: ${totalCount}`);
console.log(` Duration: ${duration}s`);
}
main()
.catch((e) => {
console.error('Equipment seeding failed:', e);
process.exit(1);
})
.finally(() => prisma.$disconnect());

View File

@@ -7,9 +7,13 @@ const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
const prisma = new PrismaClient({ adapter });
async function main() {
const passwordHash = await bcrypt.hash('admin123', 10);
console.log('Seeding database...\n');
const user = await prisma.user.upsert({
// Create password hash
const passwordHash = await bcrypt.hash('password123', 10);
// Create Admin User
const admin = await prisma.user.upsert({
where: { email: 'admin@dimension47.local' },
update: {},
create: {
@@ -19,10 +23,208 @@ async function main() {
role: 'ADMIN',
},
});
console.log('Created Admin:', admin.username);
console.log('Admin user created:', user.username, user.email);
// Create GM User
const gm = await prisma.user.upsert({
where: { email: 'gm@dimension47.local' },
update: {},
create: {
username: 'gamemaster',
email: 'gm@dimension47.local',
passwordHash,
role: 'GM',
},
});
console.log('Created GM:', gm.username);
// Create Player Users
const player1 = await prisma.user.upsert({
where: { email: 'player1@dimension47.local' },
update: {},
create: {
username: 'spieler1',
email: 'player1@dimension47.local',
passwordHash,
role: 'PLAYER',
},
});
console.log('Created Player:', player1.username);
const player2 = await prisma.user.upsert({
where: { email: 'player2@dimension47.local' },
update: {},
create: {
username: 'spieler2',
email: 'player2@dimension47.local',
passwordHash,
role: 'PLAYER',
},
});
console.log('Created Player:', player2.username);
// Create Test Campaign
const campaign = await prisma.campaign.upsert({
where: { id: '00000000-0000-0000-0000-000000000001' },
update: {},
create: {
id: '00000000-0000-0000-0000-000000000001',
name: 'Abendliche Schatten',
description: 'Eine spannende Kampagne in der Welt von Golarion. Die Helden erkunden uralte Ruinen und stellen sich finsteren Mächten.',
gmId: gm.id,
},
});
console.log('Created Campaign:', campaign.name);
// Add members to campaign
await prisma.campaignMember.upsert({
where: { campaignId_userId: { campaignId: campaign.id, userId: gm.id } },
update: {},
create: { campaignId: campaign.id, userId: gm.id },
});
await prisma.campaignMember.upsert({
where: { campaignId_userId: { campaignId: campaign.id, userId: player1.id } },
update: {},
create: { campaignId: campaign.id, userId: player1.id },
});
await prisma.campaignMember.upsert({
where: { campaignId_userId: { campaignId: campaign.id, userId: player2.id } },
update: {},
create: { campaignId: campaign.id, userId: player2.id },
});
console.log('Added members to campaign');
// Create Test Characters
const character1 = await prisma.character.upsert({
where: { id: '00000000-0000-0000-0000-000000000101' },
update: {},
create: {
id: '00000000-0000-0000-0000-000000000101',
campaignId: campaign.id,
ownerId: player1.id,
name: 'Thorin Eisenschild',
type: 'PC',
level: 3,
hpCurrent: 38,
hpMax: 42,
hpTemp: 0,
ancestryId: 'dwarf',
classId: 'fighter',
backgroundId: 'warrior',
experiencePoints: 1200,
},
});
console.log('Created Character:', character1.name);
// Add abilities for character1
const abilities1 = [
{ ability: 'STR' as const, score: 18 },
{ ability: 'DEX' as const, score: 12 },
{ ability: 'CON' as const, score: 16 },
{ ability: 'INT' as const, score: 10 },
{ ability: 'WIS' as const, score: 14 },
{ ability: 'CHA' as const, score: 8 },
];
for (const ab of abilities1) {
await prisma.characterAbility.upsert({
where: { characterId_ability: { characterId: character1.id, ability: ab.ability } },
update: { score: ab.score },
create: { characterId: character1.id, ability: ab.ability, score: ab.score },
});
}
console.log('Added abilities to', character1.name);
const character2 = await prisma.character.upsert({
where: { id: '00000000-0000-0000-0000-000000000102' },
update: {},
create: {
id: '00000000-0000-0000-0000-000000000102',
campaignId: campaign.id,
ownerId: player2.id,
name: 'Elara Sternenlicht',
type: 'PC',
level: 3,
hpCurrent: 24,
hpMax: 28,
hpTemp: 0,
ancestryId: 'elf',
classId: 'wizard',
backgroundId: 'scholar',
experiencePoints: 1200,
},
});
console.log('Created Character:', character2.name);
// Add abilities for character2
const abilities2 = [
{ ability: 'STR' as const, score: 8 },
{ ability: 'DEX' as const, score: 14 },
{ ability: 'CON' as const, score: 12 },
{ ability: 'INT' as const, score: 18 },
{ ability: 'WIS' as const, score: 14 },
{ ability: 'CHA' as const, score: 12 },
];
for (const ab of abilities2) {
await prisma.characterAbility.upsert({
where: { characterId_ability: { characterId: character2.id, ability: ab.ability } },
update: { score: ab.score },
create: { characterId: character2.id, ability: ab.ability, score: ab.score },
});
}
console.log('Added abilities to', character2.name);
// Create an NPC
const npc = await prisma.character.upsert({
where: { id: '00000000-0000-0000-0000-000000000201' },
update: {},
create: {
id: '00000000-0000-0000-0000-000000000201',
campaignId: campaign.id,
ownerId: null,
name: 'Meister Aldric',
type: 'NPC',
level: 5,
hpCurrent: 55,
hpMax: 55,
hpTemp: 0,
},
});
console.log('Created NPC:', npc.name);
// Create a second campaign
const campaign2 = await prisma.campaign.upsert({
where: { id: '00000000-0000-0000-0000-000000000002' },
update: {},
create: {
id: '00000000-0000-0000-0000-000000000002',
name: 'Die verlorene Stadt',
description: 'Eine Expedition in die legendäre verlorene Stadt Xin-Shalast.',
gmId: gm.id,
},
});
console.log('Created Campaign:', campaign2.name);
await prisma.campaignMember.upsert({
where: { campaignId_userId: { campaignId: campaign2.id, userId: gm.id } },
update: {},
create: { campaignId: campaign2.id, userId: gm.id },
});
console.log('\n✅ Database seeded successfully!');
console.log('\n📋 Test Accounts:');
console.log(' Admin: admin@dimension47.local / password123');
console.log(' GM: gm@dimension47.local / password123');
console.log(' Player 1: player1@dimension47.local / password123');
console.log(' Player 2: player2@dimension47.local / password123');
}
main()
.catch(console.error)
.catch((e) => {
console.error('Seeding failed:', e);
process.exit(1);
})
.finally(() => prisma.$disconnect());

View File

@@ -11,6 +11,7 @@ import { AuthModule } from './modules/auth/auth.module';
import { CampaignsModule } from './modules/campaigns/campaigns.module';
import { CharactersModule } from './modules/characters/characters.module';
import { TranslationsModule } from './modules/translations/translations.module';
import { EquipmentModule } from './modules/equipment/equipment.module';
// Guards
import { JwtAuthGuard } from './modules/auth/guards/jwt-auth.guard';
@@ -33,6 +34,7 @@ import { RolesGuard } from './modules/auth/guards/roles.guard';
CampaignsModule,
CharactersModule,
TranslationsModule,
EquipmentModule,
],
providers: [
// Global JWT Auth Guard

View File

@@ -0,0 +1,128 @@
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiQuery, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard.js';
import { EquipmentService } from './equipment.service.js';
@ApiTags('Equipment')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Controller('equipment')
export class EquipmentController {
constructor(private readonly equipmentService: EquipmentService) {}
@Get()
@ApiOperation({ summary: 'Search and browse equipment' })
@ApiQuery({ name: 'query', required: false, description: 'Search term for name' })
@ApiQuery({ name: 'category', required: false, description: 'Filter by category (Weapons, Armor, Consumables, etc.)' })
@ApiQuery({ name: 'subcategory', required: false, description: 'Filter by subcategory' })
@ApiQuery({ name: 'minLevel', required: false, type: Number })
@ApiQuery({ name: 'maxLevel', required: false, type: Number })
@ApiQuery({ name: 'traits', required: false, description: 'Comma-separated list of traits' })
@ApiQuery({ name: 'page', required: false, type: Number, description: 'Page number (default: 1)' })
@ApiQuery({ name: 'limit', required: false, type: Number, description: 'Items per page (default: 50)' })
async search(
@Query('query') query?: string,
@Query('category') category?: string,
@Query('subcategory') subcategory?: string,
@Query('minLevel') minLevel?: string,
@Query('maxLevel') maxLevel?: string,
@Query('traits') traits?: string,
@Query('page') page?: string,
@Query('limit') limit?: string,
) {
return this.equipmentService.search({
query,
category,
subcategory,
minLevel: minLevel ? parseInt(minLevel, 10) : undefined,
maxLevel: maxLevel ? parseInt(maxLevel, 10) : undefined,
traits: traits ? traits.split(',').map(t => t.trim()) : undefined,
page: page ? parseInt(page, 10) : 1,
limit: limit ? parseInt(limit, 10) : 50,
});
}
@Get('categories')
@ApiOperation({ summary: 'Get all equipment categories' })
async getCategories() {
return this.equipmentService.getCategories();
}
@Get('categories/:category/subcategories')
@ApiOperation({ summary: 'Get subcategories for a category' })
async getSubcategories(@Param('category') category: string) {
return this.equipmentService.getSubcategories(category);
}
@Get('weapons')
@ApiOperation({ summary: 'Browse weapons' })
@ApiQuery({ name: 'query', required: false })
@ApiQuery({ name: 'subcategory', required: false })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'limit', required: false, type: Number })
async getWeapons(
@Query('query') query?: string,
@Query('subcategory') subcategory?: string,
@Query('page') page?: string,
@Query('limit') limit?: string,
) {
return this.equipmentService.getWeapons({
query,
subcategory,
page: page ? parseInt(page, 10) : 1,
limit: limit ? parseInt(limit, 10) : 50,
});
}
@Get('armor')
@ApiOperation({ summary: 'Browse armor' })
@ApiQuery({ name: 'query', required: false })
@ApiQuery({ name: 'subcategory', required: false })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'limit', required: false, type: Number })
async getArmor(
@Query('query') query?: string,
@Query('subcategory') subcategory?: string,
@Query('page') page?: string,
@Query('limit') limit?: string,
) {
return this.equipmentService.getArmor({
query,
subcategory,
page: page ? parseInt(page, 10) : 1,
limit: limit ? parseInt(limit, 10) : 50,
});
}
@Get('consumables')
@ApiOperation({ summary: 'Browse consumables' })
@ApiQuery({ name: 'query', required: false })
@ApiQuery({ name: 'subcategory', required: false })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'limit', required: false, type: Number })
async getConsumables(
@Query('query') query?: string,
@Query('subcategory') subcategory?: string,
@Query('page') page?: string,
@Query('limit') limit?: string,
) {
return this.equipmentService.getConsumables({
query,
subcategory,
page: page ? parseInt(page, 10) : 1,
limit: limit ? parseInt(limit, 10) : 50,
});
}
@Get(':id')
@ApiOperation({ summary: 'Get equipment by ID' })
async getById(@Param('id') id: string) {
return this.equipmentService.getById(id);
}
@Get('by-name/:name')
@ApiOperation({ summary: 'Get equipment by exact name' })
async getByName(@Param('name') name: string) {
return this.equipmentService.getByName(decodeURIComponent(name));
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { EquipmentController } from './equipment.controller.js';
import { EquipmentService } from './equipment.service.js';
import { PrismaModule } from '../../prisma/prisma.module.js';
@Module({
imports: [PrismaModule],
controllers: [EquipmentController],
providers: [EquipmentService],
exports: [EquipmentService],
})
export class EquipmentModule {}

View File

@@ -0,0 +1,153 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service.js';
export interface EquipmentSearchParams {
query?: string;
category?: string;
subcategory?: string;
minLevel?: number;
maxLevel?: number;
traits?: string[];
page?: number;
limit?: number;
}
export interface EquipmentSearchResult {
items: any[];
total: number;
page: number;
limit: number;
totalPages: number;
categories: string[];
}
@Injectable()
export class EquipmentService {
constructor(private readonly prisma: PrismaService) {}
async search(params: EquipmentSearchParams): Promise<EquipmentSearchResult> {
const {
query,
category,
subcategory,
minLevel,
maxLevel,
traits,
page = 1,
limit = 50,
} = params;
// Build where clause
const where: any = {};
// Text search on name
if (query && query.trim()) {
where.name = {
contains: query.trim(),
mode: 'insensitive',
};
}
// Category filter
if (category) {
where.itemCategory = category;
}
// Subcategory filter
if (subcategory) {
where.itemSubcategory = subcategory;
}
// Level range
if (minLevel !== undefined || maxLevel !== undefined) {
where.level = {};
if (minLevel !== undefined) {
where.level.gte = minLevel;
}
if (maxLevel !== undefined) {
where.level.lte = maxLevel;
}
}
// Traits filter (has any of the specified traits)
if (traits && traits.length > 0) {
where.traits = {
hasSome: traits,
};
}
// Get total count
const total = await this.prisma.equipment.count({ where });
// Get paginated results
const items = await this.prisma.equipment.findMany({
where,
orderBy: [
{ level: 'asc' },
{ name: 'asc' },
],
skip: (page - 1) * limit,
take: limit,
});
// Get all unique categories for filter UI
const categoriesResult = await this.prisma.equipment.groupBy({
by: ['itemCategory'],
orderBy: { itemCategory: 'asc' },
});
const categories = categoriesResult.map(c => c.itemCategory);
return {
items,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
categories,
};
}
async getById(id: string) {
return this.prisma.equipment.findUnique({
where: { id },
});
}
async getByName(name: string) {
return this.prisma.equipment.findUnique({
where: { name },
});
}
async getCategories(): Promise<string[]> {
const result = await this.prisma.equipment.groupBy({
by: ['itemCategory'],
orderBy: { itemCategory: 'asc' },
});
return result.map(c => c.itemCategory);
}
async getSubcategories(category: string): Promise<string[]> {
const result = await this.prisma.equipment.groupBy({
by: ['itemSubcategory'],
where: {
itemCategory: category,
itemSubcategory: { not: null },
},
orderBy: { itemSubcategory: 'asc' },
});
return result.map(c => c.itemSubcategory).filter((s): s is string => s !== null);
}
async getWeapons(params: Omit<EquipmentSearchParams, 'category'>) {
return this.search({ ...params, category: 'Weapons' });
}
async getArmor(params: Omit<EquipmentSearchParams, 'category'>) {
return this.search({ ...params, category: 'Armor' });
}
async getConsumables(params: Omit<EquipmentSearchParams, 'category'>) {
return this.search({ ...params, category: 'Consumables' });
}
}