Compare commits
12 Commits
2323e9f277
...
main
Author | SHA1 | Date | |
---|---|---|---|
d2e4345d20 | |||
ca72b7119f | |||
b5acb2b76d | |||
81b365d8a2 | |||
fbd5b2cfb6 | |||
2a1d0a470f | |||
44f60270eb | |||
87a533858a | |||
fd11a2efe0 | |||
b5d85bc0fc | |||
44d74396e5 | |||
1a4c5d96e7 |
191
package-lock.json
generated
191
package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@types/node": "^24.0.10",
|
||||
"js-yaml": "^4.1.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
@@ -26,7 +27,8 @@
|
||||
"globals": "^16.2.0",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.34.1",
|
||||
"vite": "^7.0.0"
|
||||
"vite": "^7.0.0",
|
||||
"vite-plugin-static-copy": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
@@ -1636,6 +1638,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz",
|
||||
"integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
|
||||
@@ -2003,6 +2014,20 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
@@ -2016,6 +2041,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
@@ -2121,6 +2159,44 @@
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar/node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
||||
@@ -2606,6 +2682,21 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "11.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz",
|
||||
"integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@@ -2716,6 +2807,19 @@
|
||||
"node": ">=0.8.19"
|
||||
}
|
||||
},
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
@@ -2831,6 +2935,19 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
|
||||
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -3237,6 +3354,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||
@@ -3287,6 +3414,19 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-map": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz",
|
||||
"integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
@@ -3439,6 +3579,19 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
@@ -3756,6 +3909,22 @@
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
|
||||
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
||||
@@ -3871,6 +4040,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-static-copy": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.1.0.tgz",
|
||||
"integrity": "sha512-ONFBaYoN1qIiCxMCfeHI96lqLza7ujx/QClIXp4kEULUbyH2qLgYoaL8JHhk3FWjSB4TpzoaN3iMCyCFldyXzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^3.5.3",
|
||||
"fs-extra": "^11.3.0",
|
||||
"p-map": "^7.0.3",
|
||||
"picocolors": "^1.1.1",
|
||||
"tinyglobby": "^0.2.14"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/fdir": {
|
||||
"version": "6.4.6",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
||||
|
@@ -11,6 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@types/node": "^24.0.10",
|
||||
"js-yaml": "^4.1.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
@@ -28,6 +29,7 @@
|
||||
"globals": "^16.2.0",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.34.1",
|
||||
"vite": "^7.0.0"
|
||||
"vite": "^7.0.0",
|
||||
"vite-plugin-static-copy": "^3.1.0"
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
BIN
public/data/Faelyn Eichenhauch.pdf
Normal file
BIN
public/data/Faelyn Eichenhauch.pdf
Normal file
Binary file not shown.
1
public/data/Iboron Stich.json
Normal file
1
public/data/Iboron Stich.json
Normal file
File diff suppressed because one or more lines are too long
1
public/data/Shaddar est Mudor.json
Normal file
1
public/data/Shaddar est Mudor.json
Normal file
File diff suppressed because one or more lines are too long
64
src/App.tsx
64
src/App.tsx
@@ -1,46 +1,46 @@
|
||||
import './App.css'
|
||||
import Main from "./modules/main/components/Main.tsx";
|
||||
import Footer from "./modules/footer/components/Footer.tsx";
|
||||
import {useEffect, useState} from "react";
|
||||
import type {CharacterData} from "./types/CharacterJson.ts";
|
||||
import Header from "./modules/header/components/Header.tsx";
|
||||
import '@/App.css'
|
||||
import {useState} from "react";
|
||||
import type {CharacterData} from "@/types/CharacterJson.ts";
|
||||
import Header from "@/modules/header/components/Header.tsx";
|
||||
import Footer from "@/modules/footer/components/Footer.tsx";
|
||||
import Main from "@/modules/main/components/Main.tsx";
|
||||
|
||||
function App() {
|
||||
const [jsonData, setJsonData] = useState<CharacterData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
// const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const response = await fetch('/data/Faelyn Eichenahauch.json');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
setJsonData(data);
|
||||
} catch (error) {
|
||||
console.error('Error loading JSON file:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, []);
|
||||
// useEffect(() => {
|
||||
// const loadData = async () => {
|
||||
// try {
|
||||
// const response = await fetch('/data/Faelyn Eichenahauch.json');
|
||||
// if (!response.ok) {
|
||||
// throw new Error(`HTTP error! status: ${response.status}`);
|
||||
// }
|
||||
// const data = await response.json();
|
||||
// setJsonData(data);
|
||||
// } catch (error) {
|
||||
// console.error('Error loading JSON file:', error);
|
||||
// } finally {
|
||||
// setLoading(false);
|
||||
// }
|
||||
// };
|
||||
//
|
||||
// loadData();
|
||||
// }, []);
|
||||
|
||||
const handleFileLoad = (data: CharacterData) => {
|
||||
setJsonData(data);
|
||||
setLoading(false);
|
||||
// setLoading(false);
|
||||
};
|
||||
|
||||
if (loading) return <div>Loading...</div>;
|
||||
// if (loading) return <div>Loading...</div>;
|
||||
|
||||
if (!jsonData) return (
|
||||
<>
|
||||
<Header onFileLoad={handleFileLoad}/>
|
||||
|
||||
<Footer/>
|
||||
</>
|
||||
<>
|
||||
<Header onFileLoad={handleFileLoad}/>
|
||||
|
||||
<Footer/>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
|
10
src/main.tsx
10
src/main.tsx
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
import App from '@/App'
|
||||
import '@/index.css'
|
||||
|
||||
// Ensure root element exists
|
||||
const rootElement = document.getElementById('root');
|
||||
@@ -11,7 +11,7 @@ if (!rootElement) {
|
||||
|
||||
// Create root and render app
|
||||
ReactDOM.createRoot(rootElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
<React.StrictMode>
|
||||
<App/>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import ImportButton from "./ImportButton";
|
||||
import type { CharacterData } from '../../../types/CharacterJson.ts';
|
||||
import type {CharacterData} from '@/types/CharacterJson.ts';
|
||||
import ImportButton from "@/modules/header/components/ImportButton";
|
||||
|
||||
interface HeaderProps {
|
||||
onFileLoad: (data: CharacterData) => void;
|
||||
}
|
||||
|
||||
export default function Header({ onFileLoad }: HeaderProps) {
|
||||
export default function Header({onFileLoad}: HeaderProps) {
|
||||
return (
|
||||
<header className="bg-gray-800">
|
||||
<nav className="container mx-auto flex justify-between items-center py-6 px-6">
|
||||
|
@@ -1,13 +1,13 @@
|
||||
import { useRef } from 'react';
|
||||
import type { CharacterData } from '../../../../types/CharacterJson.ts';
|
||||
import {useRef} from 'react';
|
||||
import type {CharacterData} from '@/types/CharacterJson.ts';
|
||||
|
||||
interface ImportButtonProps {
|
||||
onFileLoad: (data: CharacterData) => void;
|
||||
}
|
||||
|
||||
export default function ImportButton({ onFileLoad }: ImportButtonProps) {
|
||||
export default function ImportButton({onFileLoad}: ImportButtonProps) {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
|
||||
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
|
||||
@@ -38,23 +38,23 @@ export default function ImportButton({ onFileLoad }: ImportButtonProps) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".json"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
id="character-import"
|
||||
/>
|
||||
<label
|
||||
htmlFor="character-import"
|
||||
className="cursor-pointer bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg transition-colors duration-200"
|
||||
>
|
||||
Charakter laden
|
||||
</label>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".json"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
id="character-import"
|
||||
/>
|
||||
<label
|
||||
htmlFor="character-import"
|
||||
className="cursor-pointer bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg transition-colors duration-200"
|
||||
>
|
||||
Charakter laden
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import Tabs from "./Tabs";
|
||||
import SheetHeader from "../modules/sheetHeader/components/SheetHeader.tsx";
|
||||
import type {CharacterData} from "../../../types/CharacterJson.ts";
|
||||
import type {CharacterData} from "@/types/CharacterJson.ts";
|
||||
import SheetHeader from "@/modules/main/modules/sheetHeader/components/SheetHeader.tsx";
|
||||
import Tabs from "@/modules/main/components/Tabs.tsx";
|
||||
|
||||
export default function Main({jsonData}: { jsonData: CharacterData }) {
|
||||
return (
|
||||
|
@@ -1,13 +1,13 @@
|
||||
import {useState} from "react";
|
||||
import Skills from "../modules/skills/components/Skills.tsx";
|
||||
import Combat from "../modules/combat/components/Combat.tsx";
|
||||
import Equipment from "../modules/equipment/components/Equipment.tsx";
|
||||
import Attributes from "../modules/attributes/components/Attributes.tsx";
|
||||
import State from "../modules/state/components/State.tsx";
|
||||
import Magic from "../modules/magic/components/Magic.tsx";
|
||||
import Religion from "../modules/religion/components/Religion.tsx";
|
||||
import Notes from "../modules/notes/components/Notes.tsx";
|
||||
import type {CharacterData} from "../../../types/CharacterJson.ts";
|
||||
import Skills from "@/modules/main/modules/skills/components/Skills.tsx";
|
||||
import Combat from "@/modules/main/modules/combat/components/Combat.tsx";
|
||||
import Attributes from "@/modules/main/modules/attributes/components/Attributes.tsx";
|
||||
import Equipment from "@/modules/main/modules/equipment/components/Equipment.tsx";
|
||||
import State from "@/modules/main/modules/state/components/State.tsx";
|
||||
import Magic from "@/modules/main/modules/magic/components/Magic.tsx";
|
||||
import Religion from "@/modules/main/modules/religion/components/Religion.tsx";
|
||||
import Notes from "@/modules/main/modules/notes/components/Notes.tsx";
|
||||
import type {CharacterData} from "@/types/CharacterJson.ts";
|
||||
|
||||
export default function Tabs({jsonData}: { jsonData: CharacterData }) {
|
||||
const [activeTab, setActiveTab] = useState("skills");
|
||||
|
@@ -1,131 +1,253 @@
|
||||
import type {CharacterData} from "../../../../../types/CharacterJson.ts";
|
||||
import type {AttributeWithValue} from "../../../../../utils/loaders";
|
||||
import {useState, useEffect} from 'react';
|
||||
import {loadRace, loadProfession, loadAttributesWithValues} from "../../../../../utils/loaders";
|
||||
import type {CharacterData} from "@/types/CharacterJson.ts";
|
||||
import {
|
||||
type AttributeWithValue,
|
||||
loadAttributesWithValues,
|
||||
loadProfession,
|
||||
loadRace,
|
||||
loadRaceVariant
|
||||
} from "@/utils/loaders";
|
||||
import {useCallback, useEffect, useState} from 'react';
|
||||
|
||||
export default function SheetHeader({jsonData}: { jsonData: CharacterData }) {
|
||||
const [raceName, setRaceName] = useState<string>(jsonData.r);
|
||||
const [raceVariantName, setRaceVariantName] = useState<string>(jsonData.rv || '');
|
||||
const [professionName, setProfessionName] = useState<string>(jsonData.p);
|
||||
const [attributes, setAttributes] = useState<AttributeWithValue[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
// Load race and profession using the new loader functions
|
||||
const [race, profession] = await Promise.all([
|
||||
loadRace(jsonData.r),
|
||||
loadProfession(jsonData.p)
|
||||
]);
|
||||
|
||||
// Process attributes using the new loadAttributesWithValues function
|
||||
const loadedAttributes = await loadAttributesWithValues(jsonData.attr.values);
|
||||
|
||||
if (isMounted) {
|
||||
setRaceName(race?.name || jsonData.r);
|
||||
// For profession, handle gendered name
|
||||
if (profession?.name) {
|
||||
setProfessionName(profession.name.m || profession.name.f || jsonData.p);
|
||||
} else {
|
||||
setProfessionName(jsonData.p);
|
||||
}
|
||||
// Set the attributes with their values, names, and short forms
|
||||
setAttributes(loadedAttributes);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error);
|
||||
if (isMounted) {
|
||||
setRaceName(jsonData.r);
|
||||
setProfessionName(jsonData.p);
|
||||
}
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const resetToDefaults = useCallback(() => {
|
||||
setRaceName(jsonData.r);
|
||||
setRaceVariantName(jsonData.rv || '');
|
||||
setProfessionName(jsonData.p);
|
||||
setAttributes([]);
|
||||
}, [jsonData.r, jsonData.rv, jsonData.p]);
|
||||
|
||||
const loadData = useCallback(async (signal: AbortSignal) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Load race and profession using the new loader functions
|
||||
const [race, raceVariant, profession, loadedAttributes] = await Promise.all([
|
||||
loadRace(jsonData.r),
|
||||
loadRaceVariant(jsonData.rv || ''),
|
||||
loadProfession(jsonData.p),
|
||||
loadAttributesWithValues(jsonData.attr.values),
|
||||
]);
|
||||
|
||||
// Check if component is still mounted and request wasn't cancelled
|
||||
if (signal.aborted) return;
|
||||
|
||||
// Update state with loaded data
|
||||
setRaceName(race?.name || jsonData.r);
|
||||
setRaceVariantName(raceVariant?.name || '');
|
||||
|
||||
// Handle profession name with gender preference
|
||||
if (profession?.name) {
|
||||
setProfessionName(profession.name.m || profession.name.f || jsonData.p);
|
||||
} else {
|
||||
setProfessionName(jsonData.p);
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
|
||||
|
||||
setAttributes(loadedAttributes);
|
||||
} catch (err) {
|
||||
if (signal.aborted) return;
|
||||
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
||||
console.error('Error loading character data:', errorMessage);
|
||||
setError(errorMessage);
|
||||
|
||||
// Reset to default values on error
|
||||
resetToDefaults();
|
||||
} finally {
|
||||
if (!signal.aborted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, [jsonData.r, jsonData.rv, jsonData.p, jsonData.attr, resetToDefaults]);
|
||||
|
||||
useEffect(() => {
|
||||
const abortController = new AbortController();
|
||||
|
||||
loadData(abortController.signal);
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
abortController.abort();
|
||||
};
|
||||
}, [jsonData.r, jsonData.p, jsonData.attr]);
|
||||
|
||||
console.log(jsonData)
|
||||
|
||||
}, [loadData]);
|
||||
|
||||
// Optional: Add loading state handling
|
||||
if (loading) {
|
||||
return <div className="animate-pulse">Loading character data...</div>;
|
||||
}
|
||||
|
||||
// Optional: Add error state handling
|
||||
if (error) {
|
||||
return (
|
||||
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
||||
Error loading character data: {error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const calculateTotalLp = () => {
|
||||
// GW der Spezies + KO + KO
|
||||
const tempRaceLp = 5 // TODO: Needs to be pulled from universal stats
|
||||
const constVal = attributes.find(attr => attr.id === 'ATTR_7')?.value || 0; // ATTR_7 = Constitution
|
||||
const bonusMalus = 0 // Negative when malus
|
||||
const zukauf = 0
|
||||
return (jsonData.attr.lp + constVal * 2 + tempRaceLp + zukauf + bonusMalus);
|
||||
}
|
||||
|
||||
const calculateTotalAP = () => {
|
||||
// TODO: Only show if Zauberer
|
||||
// 20 durch Zauberer + Leiteigenschaft
|
||||
const initial = 20
|
||||
const leiteigenschaft = 0 // TODO: needs to be pulled
|
||||
const bonusMalus = 0 // Negative when malus
|
||||
const zukauf = 0
|
||||
return (jsonData.attr.ae + initial + leiteigenschaft + bonusMalus + zukauf);
|
||||
}
|
||||
|
||||
const calculateTotalKP = () => {
|
||||
// TODO: Only show if Geweihter
|
||||
// 20 durch Geweiht + Leiteigenschaft
|
||||
const initial = 20
|
||||
const leiteigenschaft = 0 // TODO: needs to be pulled
|
||||
const bonusMalus = 0 // Negative when malus
|
||||
const zukauf = 0
|
||||
return (jsonData.attr.kp + initial + leiteigenschaft + bonusMalus + zukauf);
|
||||
}
|
||||
|
||||
const calculateTotalSp = () => {
|
||||
// TODO: needs to be pulled
|
||||
const initial = 3
|
||||
const bonus = 0
|
||||
return (jsonData.attr.sp || 0 + initial + bonus);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-between items-center py-5 px-5">
|
||||
<div className="flex justify-between items-end py-5 px-5">
|
||||
<div className="w-[30%]">
|
||||
<p className="border-b border-gray-500 font-semibold">
|
||||
{jsonData.name}
|
||||
</p>
|
||||
<small>Name</small>
|
||||
<small className="text-gray-400">Name</small>
|
||||
</div>
|
||||
<div className="w-[30%]">
|
||||
<p className="border-b border-gray-500 font-semibold">
|
||||
{raceName}
|
||||
{raceName} {raceVariantName ? <>({raceVariantName})</> : ''}
|
||||
</p>
|
||||
<small>Spezies</small>
|
||||
<small className="text-gray-400">Spezies</small>
|
||||
</div>
|
||||
<div className="w-[30%]">
|
||||
<p className="border-b border-gray-500 font-semibold">
|
||||
{professionName}
|
||||
</p>
|
||||
<small>Profession</small>
|
||||
<small className="text-gray-400">Profession</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center py-3 px-10">
|
||||
<div className="w-[20%] h-[20%] rounded-full overflow-hidden">
|
||||
<img src="#" alt="char bild"/>
|
||||
|
||||
<div className="flex justify-between items-center py-5 px-5 gap-6">
|
||||
<div className="w-[30%] h-full rounded-md bg-gray-600 overflow-hidden"
|
||||
style={{height: '12.5em', width: '9.375em'}}>
|
||||
<img src="#" alt="char bild" className="object-cover w-full h-full"/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 w-[80%]">
|
||||
<div className="flex justify-between">
|
||||
|
||||
<div className="w-[70%] flex flex-col gap-2 w-[80%]">
|
||||
<div className="w-[100%] flex justify-between">
|
||||
{attributes.map(attr => (
|
||||
<div key={attr.id} className="w-[10%] text-center">
|
||||
<div className="font-bold">{attr.short}</div>
|
||||
<div>{attr.value}</div>
|
||||
<div key={attr.id} className="">
|
||||
<div className="relative group cursor-help flex items-center justify-center">
|
||||
<div>{attr.short}</div>
|
||||
<span
|
||||
className="absolute left-1/2 transform -translate-x-1/2 bottom-full bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap">
|
||||
{attr.name}
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-bold" style={{fontSize: '1.5em'}}>{attr.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="w-[100%] flex pt-5">
|
||||
<span className="relative group cursor-help">
|
||||
LP
|
||||
<span
|
||||
className="absolute left-0 bottom-full mb-1 px-2 py-1 bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap">
|
||||
Lebenspunkte
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<div className="w-[100%] flex pt-2">
|
||||
<span className="relative group cursor-help">
|
||||
LP
|
||||
<span
|
||||
className="absolute left-0 bottom-full mb-1 px-2 bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap">
|
||||
Lebenspunkte
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
className="ms-5 w-[50%] bg-red-800 rounded-s-full h-6 flex items-center justify-center text-white text-sm font-medium">
|
||||
15 / 15
|
||||
className="ms-5 w-[100%] bg-gray-600 rounded-full h-6 flex items-center overflow-hidden relative">
|
||||
<div
|
||||
className="bg-gradient-to-r from-red-900 to-red-600 h-full transition-all duration-300"
|
||||
style={{width: `${Math.min(((calculateTotalLp() - jsonData.attr.permanentLP.lost) / calculateTotalLp()) * 100, 100)}%`}}
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center text-white text-sm font-medium">
|
||||
{calculateTotalLp()} / {calculateTotalLp() - jsonData.attr.permanentLP.lost}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="me-5 w-[50%] bg-green-800 rounded-e-full h-6 flex items-center justify-center text-white text-sm font-medium">
|
||||
16 / 16
|
||||
</div>
|
||||
<span className="relative group cursor-help whitespace-nowrap">
|
||||
AP/KP
|
||||
<span
|
||||
className="absolute right-0 bottom-full mb-1 px-2 py-1 bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap">
|
||||
Astralpunkte / Karmapunkte
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="w-[100%] flex pt-5">
|
||||
<span className="w-[10%] relative group cursor-help">
|
||||
SP
|
||||
<span
|
||||
className="absolute left-0 bottom-full mb-1 px-2 py-1 bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap">
|
||||
Schicksalspunkte
|
||||
</span>
|
||||
</span>
|
||||
<div className="w-[90%] flex items-center gap-2">
|
||||
<div className="rounded-full w-5 h-5 bg-violet-400"></div>
|
||||
<div className="rounded-full w-5 h-5 bg-violet-400"></div>
|
||||
<div className="rounded-full w-5 h-5 bg-violet-400"></div>
|
||||
|
||||
<div className="w-[100%] flex pt-2">
|
||||
<span className="relative group cursor-help">
|
||||
AP
|
||||
<span
|
||||
className="absolute left-0 bottom-full mb-1 px-2 bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap">
|
||||
Astralpunkte
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
className="ms-5 w-[100%] bg-gray-600 rounded-full h-6 flex items-center overflow-hidden relative">
|
||||
<div
|
||||
className="bg-gradient-to-r from-blue-900 to-blue-500 h-full transition-all duration-300"
|
||||
style={{width: `${Math.min(((calculateTotalAP() - jsonData.attr.permanentAE.lost) / calculateTotalAP()) * 100, 100)}%`}}
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center text-white text-sm font-medium">
|
||||
{calculateTotalAP()} / {calculateTotalAP() - jsonData.attr.permanentAE.lost}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-[100%] flex pt-2">
|
||||
<span className="relative group cursor-help">
|
||||
KP
|
||||
<span
|
||||
className="absolute left-0 bottom-full mb-1 px-2 bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap">
|
||||
Karmapunkte
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
className="ms-5 w-[100%] bg-gray-600 rounded-full h-6 flex items-center overflow-hidden relative">
|
||||
<div
|
||||
className="bg-gradient-to-r from-emerald-900 to-emerald-500 h-full transition-all duration-300"
|
||||
style={{width: `${Math.min(((calculateTotalKP() - jsonData.attr.permanentKP.lost) / calculateTotalKP()) * 100, 100)}%`}}
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center text-white text-sm font-medium">
|
||||
{calculateTotalKP()} / {calculateTotalKP() - jsonData.attr.permanentKP.lost}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-[100%] flex pt-2">
|
||||
<span className="relative group cursor-help">
|
||||
SP
|
||||
<span
|
||||
className="absolute left-0 bottom-full mb-1 px-2 py-1 bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap">
|
||||
Schicksalspunkte
|
||||
</span>
|
||||
</span>
|
||||
<div className="ms-5 w-[100%] flex items-center gap-2">
|
||||
{Array.from({length: calculateTotalSp()}, (_, index) => (
|
||||
<div
|
||||
key={`sp-available-${index}`}
|
||||
className="rounded-full w-5 h-5 bg-gradient-to-br from-violet-700 to-violet-500"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,84 +1,105 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import type { CharacterData } from '../../../../../types/CharacterJson';
|
||||
import { loadSkillsWithValues, type SkillWithValue } from '../../../../../utils/loaders';
|
||||
import {useCallback, useEffect, useState} from 'react';
|
||||
import type {CharacterData} from '@/types/CharacterJson.ts';
|
||||
import {loadSkillsWithValues, type SkillWithValue} from '@/utils/loaders.ts';
|
||||
|
||||
export default function Skills({ jsonData }: { jsonData: CharacterData }) {
|
||||
export default function Skills({jsonData}: { jsonData: CharacterData }) {
|
||||
const [skills, setSkills] = useState<SkillWithValue[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// Load skills with their values using the new loader function
|
||||
const loadedSkills = await loadSkillsWithValues(jsonData.talents);
|
||||
|
||||
if (isMounted) {
|
||||
setSkills(loadedSkills);
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading skills:', error);
|
||||
if (isMounted) {
|
||||
setError('Failed to load skills. Please try again.');
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
const resetToDefaults = useCallback(() => {
|
||||
setSkills([]);
|
||||
}, []);
|
||||
|
||||
const loadData = useCallback(async (signal: AbortSignal) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Load skills with their values using the new loader function
|
||||
const loadedSkills = await loadSkillsWithValues(jsonData.talents);
|
||||
|
||||
// Check if component is still mounted and request wasn't cancelled
|
||||
if (signal.aborted) return;
|
||||
|
||||
setSkills(loadedSkills);
|
||||
} catch (err) {
|
||||
if (signal.aborted) return;
|
||||
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
||||
console.error('Error loading skills:', errorMessage);
|
||||
setError('Failed to load skills. Please try again.');
|
||||
|
||||
// Reset to default values on error
|
||||
resetToDefaults();
|
||||
} finally {
|
||||
if (!signal.aborted) {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
|
||||
}
|
||||
}, [jsonData.talents, resetToDefaults]);
|
||||
|
||||
useEffect(() => {
|
||||
const abortController = new AbortController();
|
||||
|
||||
loadData(abortController.signal);
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
abortController.abort();
|
||||
};
|
||||
}, [jsonData.talents]);
|
||||
|
||||
}, [loadData]);
|
||||
|
||||
if (loading) {
|
||||
return <div>Loading skills...</div>;
|
||||
return <div className="animate-pulse">Loading skills...</div>;
|
||||
}
|
||||
|
||||
|
||||
if (error) {
|
||||
return <div className="text-red-500">{error}</div>;
|
||||
return (
|
||||
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
||||
Error loading skills: {error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (skills.length === 0) {
|
||||
return <div>No skills found.</div>;
|
||||
}
|
||||
|
||||
|
||||
console.log(jsonData);
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<h1 className="text-2xl font-bold mb-4">Skills</h1>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{skills.map(skill => (
|
||||
<div key={skill.id} className="border p-3 rounded shadow">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-lg font-semibold">{skill.name}</h2>
|
||||
<span className="bg-blue-100 text-blue-800 font-bold px-2 py-1 rounded">
|
||||
<div className="p-4">
|
||||
<h1 className="text-2xl font-bold mb-4">Skills</h1>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{skills.map(skill => (
|
||||
<div key={skill.id} className="border p-3 rounded shadow">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-lg font-semibold">{skill.name}</h2>
|
||||
<span
|
||||
className="bg-blue-100 text-blue-800 font-bold px-2 py-1 rounded flex items-center justify-center"
|
||||
style={{width: '2.5em'}}>
|
||||
{skill.value}
|
||||
</span>
|
||||
</div>
|
||||
{/*{skill.applications && skill.applications.length > 0 && (*/}
|
||||
{/* <div className="mt-2">*/}
|
||||
{/* <h3 className="font-medium">Applications:</h3>*/}
|
||||
{/* <ul className="list-disc list-inside">*/}
|
||||
{/* {skill.applications.map((app: any, index: number) => (*/}
|
||||
{/* <li key={index}>{app.name}</li>*/}
|
||||
{/* ))}*/}
|
||||
{/* </ul>*/}
|
||||
{/* </div>*/}
|
||||
{/*)}*/}
|
||||
{/*{skill.tools && (*/}
|
||||
{/* <div className="mt-2">*/}
|
||||
{/* <h3 className="font-medium">Tools:</h3>*/}
|
||||
{/* <p className="text-sm">{skill.tools}</p>*/}
|
||||
{/* </div>*/}
|
||||
{/*)}*/}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/*{skill.applications && skill.applications.length > 0 && (*/}
|
||||
{/* <div className="mt-2">*/}
|
||||
{/* <h3 className="font-medium">Applications:</h3>*/}
|
||||
{/* <ul className="list-disc list-inside">*/}
|
||||
{/* {skill.applications.map((app: any, index: number) => (*/}
|
||||
{/* <li key={index}>{app.name}</li>*/}
|
||||
{/* ))}*/}
|
||||
{/* </ul>*/}
|
||||
{/* </div>*/}
|
||||
{/*)}*/}
|
||||
{skill.tools && (
|
||||
<div className="mt-2">
|
||||
<h3 className="font-medium">Tools:</h3>
|
||||
<p className="text-sm">{skill.tools}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -1,5 +1,3 @@
|
||||
import type {Skills} from "./Skill.ts";
|
||||
|
||||
export interface AttributeValue {
|
||||
id: string;
|
||||
value: number;
|
||||
@@ -16,11 +14,10 @@ export interface Attributes {
|
||||
ae: number;
|
||||
kp: number;
|
||||
lp: number;
|
||||
sp: number;
|
||||
permanentAE: PermanentAttribute;
|
||||
permanentKP: PermanentAttribute;
|
||||
permanentLP: {
|
||||
lost: number;
|
||||
};
|
||||
permanentLP: PermanentAttribute;
|
||||
}
|
||||
|
||||
export interface PersonalDetails {
|
||||
@@ -89,13 +86,14 @@ export interface CharacterData {
|
||||
ap: AdventurePoints;
|
||||
el: string; // Experience Level
|
||||
r: string; // Race
|
||||
rv?: string;// Race_Variant
|
||||
c: string; // Culture
|
||||
p: string; // Profession
|
||||
sex: string;
|
||||
pers: PersonalDetails;
|
||||
attr: Attributes;
|
||||
activatable: Activatables;
|
||||
talents: Skills;
|
||||
talents: Record<string, number>;
|
||||
ct: CombatTechniques;
|
||||
spells: Spells;
|
||||
cantrips: string[];
|
||||
|
142
src/types/CharacterSheet.ts
Normal file
142
src/types/CharacterSheet.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
// Basic ID pattern types
|
||||
type HeroId = string; // Pattern: "^H_[1-9]\\d*$"
|
||||
type RaceId = string; // Pattern: "^R_[1-9]\\d*$"
|
||||
type RaceVariantId = string; // Pattern: "^RV_[1-9]\\d*$"
|
||||
type CultureId = string; // Pattern: "^C_[1-9]\\d*$"
|
||||
type ProfessionId = string; // Pattern: "^P_[1-9]\\d*$"
|
||||
type ProfessionVariantId = string; // Pattern: "^PV_[1-9]\\d*$"
|
||||
type ExperienceLevelId = string; // Pattern: "^EL_[1-9]\\d*$"
|
||||
type AttributeId = string; // Pattern: "^ATTR_[1-9]\\d*$"
|
||||
type TalentId = string; // Pattern: "^TAL_[1-9]\\d*$"
|
||||
type CombatTechniqueId = string; // Pattern: "^CT_[1-9]\\d*$"
|
||||
type SpellId = string; // Pattern: "^SPELL_[1-9]\\d*$"
|
||||
type CantripId = string; // Pattern: "^CANTRIP_[1-9]\\d*$"
|
||||
type LiturgyId = string; // Pattern: "^LITURGY_[1-9]\\d*$"
|
||||
type BlessingId = string; // Pattern: "^BLESSING_[1-9]\\d*$"
|
||||
type ItemId = string; // Pattern: "^ITEM_[1-9]\\d*$"
|
||||
type ActivatableId = string; // Pattern: "^(ADV|DISADV|SA)_[1-9]\\d*$"
|
||||
|
||||
// Adventure Points
|
||||
interface AdventurePoints {
|
||||
total: number; // minimum: 1
|
||||
}
|
||||
|
||||
// Rules configuration
|
||||
interface Rules {
|
||||
higherParadeValues: 0 | 2 | 4;
|
||||
attributeValueLimit: boolean;
|
||||
enableAllRuleBooks: boolean;
|
||||
enabledRuleBooks: string[];
|
||||
enableLanguageSpecializations: boolean;
|
||||
}
|
||||
|
||||
// Personal data
|
||||
interface PersonalData {
|
||||
family?: string;
|
||||
placeofbirth?: string;
|
||||
dateofbirth?: string;
|
||||
age?: string;
|
||||
haircolor?: string;
|
||||
eyecolor?: string;
|
||||
size?: string;
|
||||
weight?: string;
|
||||
title?: string;
|
||||
socialstatus?: number; // 1-5
|
||||
characteristics?: string;
|
||||
otherinfo?: string;
|
||||
cultureAreaKnowledge?: string;
|
||||
}
|
||||
|
||||
// Activatable items (advantages, disadvantages, special abilities)
|
||||
interface ActivatableOption {
|
||||
sid?: string | number;
|
||||
sid2?: string | number;
|
||||
sid3?: string | number;
|
||||
tier?: number; // minimum: 1
|
||||
cost?: number; // minimum: 0
|
||||
}
|
||||
|
||||
// Permanent energy attributes
|
||||
interface PermanentEnergy {
|
||||
lost: number; // minimum: 0
|
||||
redeemed?: number; // minimum: 0
|
||||
}
|
||||
|
||||
interface PermanentLP {
|
||||
lost: number; // minimum: 0
|
||||
}
|
||||
|
||||
// Attribute value (older version - tuple format)
|
||||
type AttributeTuple = [AttributeId, number, number]; // [id, value, adjustment]
|
||||
|
||||
// Attribute value (newer version - object format)
|
||||
interface AttributeValue {
|
||||
id: AttributeId;
|
||||
value: number; // minimum: 8
|
||||
}
|
||||
|
||||
// Attributes - supports both old and new formats
|
||||
interface AttributesOld {
|
||||
values: AttributeTuple[];
|
||||
lp: number; // minimum: 0
|
||||
ae: number; // minimum: 0
|
||||
kp: number; // minimum: 0
|
||||
permanentLP?: PermanentLP;
|
||||
permanentAE: PermanentEnergy;
|
||||
permanentKP: PermanentEnergy;
|
||||
}
|
||||
|
||||
interface AttributesNew {
|
||||
values: AttributeValue[];
|
||||
attributeAdjustmentSelected: AttributeId;
|
||||
lp: number; // minimum: 0
|
||||
ae: number; // minimum: 0
|
||||
kp: number; // minimum: 0
|
||||
permanentLP?: PermanentLP;
|
||||
permanentAE: PermanentEnergy;
|
||||
permanentKP: PermanentEnergy;
|
||||
}
|
||||
|
||||
// Belongings
|
||||
interface Item {
|
||||
id: ItemId;
|
||||
price?: number; // minimum: 0
|
||||
weight?: number; // minimum: 0
|
||||
}
|
||||
|
||||
interface Belongings {
|
||||
items: Record<ItemId, Item>;
|
||||
}
|
||||
|
||||
// Main CharacterSheet type
|
||||
export interface CharacterSheet {
|
||||
id: HeroId;
|
||||
name: string;
|
||||
clientVersion: string; // Version pattern: (0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(-(\\w+)\\.(0|[1-9]\\d*))?
|
||||
dateCreated: string; // ISO date-time format
|
||||
dateModified: string; // ISO date-time format
|
||||
locale?: string; // Pattern: "^[a-z]\\-[A-Z]$"
|
||||
avatar?: string;
|
||||
ap: AdventurePoints;
|
||||
r?: RaceId;
|
||||
rv?: RaceVariantId;
|
||||
c?: CultureId;
|
||||
isCulturalPackageActive?: boolean;
|
||||
p?: ProfessionId;
|
||||
professionName?: string;
|
||||
pv?: ProfessionVariantId;
|
||||
sex: "m" | "f";
|
||||
rules: Rules;
|
||||
phase: 1 | 2 | 3; // 1: RCP selection, 2: standard creation, 3: after creation
|
||||
el: ExperienceLevelId;
|
||||
pers: PersonalData;
|
||||
activatable: Record<ActivatableId, ActivatableOption[]>;
|
||||
attr: AttributesOld | AttributesNew;
|
||||
talents: Record<TalentId, number>; // minimum: 0
|
||||
ct: Record<CombatTechniqueId, number>; // minimum: 0
|
||||
spells: Record<SpellId, number>; // minimum: 0
|
||||
cantrips: CantripId[];
|
||||
liturgies: Record<LiturgyId, number>; // minimum: 0
|
||||
blessings: BlessingId[];
|
||||
belongings?: Belongings;
|
||||
}
|
@@ -1,64 +1,65 @@
|
||||
import type {Race} from '../types/Race';
|
||||
import type {Profession} from '../types/Profession';
|
||||
import type {Attribute} from '../types/Attribute';
|
||||
import type {AttributeValue} from '../types/CharacterJson';
|
||||
import type {Advantage} from '../types/Advantage';
|
||||
import type {AnimistForce} from '../types/AnimistForce';
|
||||
// import type { ArcaneBardTradition } from '../types/ArcaneBardTradition';
|
||||
// import type { ArcaneDancerTradition } from '../types/ArcaneDancerTradition';
|
||||
// import type { ArmorType } from '../types/ArmorType';
|
||||
// import type { Aspect } from '../types/Aspect';
|
||||
// import type { BlessedTradition } from '../types/BlessedTradition';
|
||||
// import type { Blessing } from '../types/Blessing';
|
||||
// import type { Book } from '../types/Book';
|
||||
// import type { Brew } from '../types/Brew';
|
||||
// import type { Cantrip } from '../types/Cantrip';
|
||||
// import type { CombatSpecialAbilityGroup } from '../types/CombatSpecialAbilityGroup';
|
||||
// import type { CombatTechniqueGroup } from '../types/CombatTechniqueGroup';
|
||||
// import type { CombatTechnique } from '../types/CombatTechnique';
|
||||
// import type { Condition } from '../types/Condition';
|
||||
// import type { Culture } from '../types/Culture';
|
||||
// import type { Curse } from '../types/Curse';
|
||||
// import type { DerivedCharacteristic } from '../types/DerivedCharacteristic';
|
||||
// import type { Disadvantage } from '../types/Disadvantage';
|
||||
// import type { DominationRitual } from '../types/DominationRitual';
|
||||
// import type { Element } from '../types/Element';
|
||||
// import type { ElvenMagicalSong } from '../types/ElvenMagicalSong';
|
||||
// import type { EquipmentGroup } from '../types/EquipmentGroup';
|
||||
// import type { EquipmentPackage } from '../types/EquipmentPackage';
|
||||
// import type { Equipment } from '../types/Equipment';
|
||||
// import type { ExperienceLevel } from '../types/ExperienceLevel';
|
||||
// import type { EyeColor } from '../types/EyeColor';
|
||||
// import type { FocusRule } from '../types/FocusRule';
|
||||
// import type { GeodeRitual } from '../types/GeodeRitual';
|
||||
// import type { HairColor } from '../types/HairColor';
|
||||
// import type { LiturgicalChantEnhancement } from '../types/LiturgicalChantEnhancement';
|
||||
// import type { LiturgicalChantGroup } from '../types/LiturgicalChantGroup';
|
||||
// import type { LiturgicalChant } from '../types/LiturgicalChant';
|
||||
// import type { MagicalDance } from '../types/MagicalDance';
|
||||
// import type { MagicalMelodie } from '../types/MagicalMelodie';
|
||||
// import type { MagicalTradition } from '../types/MagicalTradition';
|
||||
// import type { OptionalRule } from '../types/OptionalRule';
|
||||
// import type { Pact } from '../types/Pact';
|
||||
// import type { PatronCategorie } from '../types/PatronCategorie';
|
||||
// import type { Patron } from '../types/Patron';
|
||||
// import type { ProfessionVariant } from '../types/ProfessionVariant';
|
||||
// import type { Propertie } from '../types/Propertie';
|
||||
// import type { RaceVariant } from '../types/RaceVariant';
|
||||
// import type { Reache } from '../types/Reache';
|
||||
// import type { RogueSpell } from '../types/RogueSpell';
|
||||
// import type { SkillGroup } from '../types/SkillGroup';
|
||||
import type {Skill, Skills} from '../types/Skill';
|
||||
// import type { SocialStatuse } from '../types/SocialStatuse';
|
||||
// import type { SpecialAbilitie } from '../types/SpecialAbilitie';
|
||||
// import type { SpecialAbilityGroup } from '../types/SpecialAbilityGroup';
|
||||
// import type { SpellEnhancement } from '../types/SpellEnhancement';
|
||||
// import type { SpellGroup } from '../types/SpellGroup';
|
||||
// import type { Spell } from '../types/Spell';
|
||||
// import type { State } from '../types/State';
|
||||
// import type { Subject } from '../types/Subject';
|
||||
// import type { Tribe } from '../types/Tribe';
|
||||
// import type { ZibiljaRitual } from '../types/ZibiljaRitual';
|
||||
import type {Race} from '@/types/Race';
|
||||
import type {Profession} from '@/types/Profession';
|
||||
import type {Attribute} from '@/types/Attribute';
|
||||
import type {AttributeValue} from '@/types/CharacterJson';
|
||||
import type {Advantage} from '@/types/Advantage';
|
||||
import type {AnimistForce} from '@/types/AnimistForce';
|
||||
// import type { ArcaneBardTradition } from '@/types/ArcaneBardTradition';
|
||||
// import type { ArcaneDancerTradition } from '@/types/ArcaneDancerTradition';
|
||||
// import type { ArmorType } from '@/types/ArmorType';
|
||||
// import type { Aspect } from '@/types/Aspect';
|
||||
// import type { BlessedTradition } from '@/types/BlessedTradition';
|
||||
// import type { Blessing } from '@/types/Blessing';
|
||||
// import type { Book } from '@/types/Book';
|
||||
// import type { Brew } from '@/types/Brew';
|
||||
// import type { Cantrip } from '@/types/Cantrip';
|
||||
// import type { CombatSpecialAbilityGroup } from '@/types/CombatSpecialAbilityGroup';
|
||||
// import type { CombatTechniqueGroup } from '@/types/CombatTechniqueGroup';
|
||||
// import type { CombatTechnique } from '@/types/CombatTechnique';
|
||||
// import type { Condition } from '@/types/Condition';
|
||||
// import type { Culture } from '@/types/Culture';
|
||||
// import type { Curse } from '@/types/Curse';
|
||||
// import type { DerivedCharacteristic } from '@/types/DerivedCharacteristic';
|
||||
// import type { Disadvantage } from '@/types/Disadvantage';
|
||||
// import type { DominationRitual } from '@/types/DominationRitual';
|
||||
// import type { Element } from '@/types/Element';
|
||||
// import type { ElvenMagicalSong } from '@/types/ElvenMagicalSong';
|
||||
// import type { EquipmentGroup } from '@/types/EquipmentGroup';
|
||||
// import type { EquipmentPackage } from '@/types/EquipmentPackage';
|
||||
// import type { Equipment } from '@/types/Equipment';
|
||||
// import type { ExperienceLevel } from '@/types/ExperienceLevel';
|
||||
// import type { EyeColor } from '@/types/EyeColor';
|
||||
// import type { FocusRule } from '@/types/FocusRule';
|
||||
// import type { GeodeRitual } from '@/types/GeodeRitual';
|
||||
// import type { HairColor } from '@/types/HairColor';
|
||||
// import type { LiturgicalChantEnhancement } from '@/types/LiturgicalChantEnhancement';
|
||||
// import type { LiturgicalChantGroup } from '@/types/LiturgicalChantGroup';
|
||||
// import type { LiturgicalChant } from '@/types/LiturgicalChant';
|
||||
// import type { MagicalDance } from '@/types/MagicalDance';
|
||||
// import type { MagicalMelodie } from '@/types/MagicalMelodie';
|
||||
// import type { MagicalTradition } from '@/types/MagicalTradition';
|
||||
// import type { OptionalRule } from '@/types/OptionalRule';
|
||||
// import type { Pact } from '@/types/Pact';
|
||||
// import type { PatronCategorie } from '@/types/PatronCategorie';
|
||||
// import type { Patron } from '@/types/Patron';
|
||||
// import type { ProfessionVariant } from '@/types/ProfessionVariant';
|
||||
// import type { Propertie } from '@/types/Propertie';
|
||||
// import type { RaceVariant } from '@/types/RaceVariant';
|
||||
// import type { Reache } from '@/types/Reache';
|
||||
// import type { RogueSpell } from '@/types/RogueSpell';
|
||||
// import type { SkillGroup } from '@/types/SkillGroup';
|
||||
import type {Skill} from '@/types/Skill';
|
||||
import type {RaceVariant} from "@/types/RaceVariant.ts";
|
||||
// import type { SocialStatuse } from '@/types/SocialStatuse';
|
||||
// import type { SpecialAbilitie } from '@/types/SpecialAbilitie';
|
||||
// import type { SpecialAbilityGroup } from '@/types/SpecialAbilityGroup';
|
||||
// import type { SpellEnhancement } from '@/types/SpellEnhancement';
|
||||
// import type { SpellGroup } from '@/types/SpellGroup';
|
||||
// import type { Spell } from '@/types/Spell';
|
||||
// import type { State } from '@/types/State';
|
||||
// import type { Subject } from '@/types/Subject';
|
||||
// import type { Tribe } from '@/types/Tribe';
|
||||
// import type { ZibiljaRitual } from '@/types/ZibiljaRitual';
|
||||
|
||||
/**
|
||||
* Generic function to load data from a JSON file
|
||||
@@ -67,12 +68,11 @@ import type {Skill, Skills} from '../types/Skill';
|
||||
*/
|
||||
async function loadData<T>(fileName: string): Promise<T[]> {
|
||||
try {
|
||||
// Ensure proper file extension
|
||||
const fileNameWithExt = fileName.endsWith('.json') ? fileName : `${fileName}.json`;
|
||||
const module = await import(`../assets/database/${fileNameWithExt}`);
|
||||
|
||||
// Access the default export or named export
|
||||
const data = module.default || module;
|
||||
const response = await fetch(`./database/${fileName}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load ${fileName}: ${response.statusText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
// Validate it's an array
|
||||
if (!Array.isArray(data)) {
|
||||
@@ -137,6 +137,11 @@ export async function loadRace(id: string): Promise<Race | undefined> {
|
||||
return getItemById<Race>('Races.json', id);
|
||||
}
|
||||
|
||||
// Race_Variant loader
|
||||
export async function loadRaceVariant(id: string): Promise<RaceVariant | undefined> {
|
||||
return getItemById<RaceVariant>('RaceVariants.json', id);
|
||||
}
|
||||
|
||||
// Profession loader
|
||||
export async function loadProfession(id: string): Promise<Profession | undefined> {
|
||||
return getItemById<Profession>('Professions.json', id);
|
||||
@@ -432,9 +437,8 @@ export async function loadSkillWithValue(id: string, value: number): Promise<Ski
|
||||
}
|
||||
|
||||
// Load multiple skills with values
|
||||
export async function loadSkillsWithValues(talents: Skills): Promise<SkillWithValue[]> {
|
||||
export async function loadSkillsWithValues(talents: Record<string, number>): Promise<SkillWithValue[]> {
|
||||
const skillIds = Object.keys(talents);
|
||||
// @ts-ignore
|
||||
const promises = skillIds.map((id) => loadSkillWithValue(id, talents[id]));
|
||||
const skills = await Promise.all(promises);
|
||||
|
||||
|
@@ -3,10 +3,13 @@
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
@@ -14,14 +17,21 @@
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
|
@@ -1,14 +1,26 @@
|
||||
import {defineConfig} from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import {viteStaticCopy} from "vite-plugin-static-copy";
|
||||
import path from 'path'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
tailwindcss(),
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{src: 'src/assets/database/*.json', dest: 'database'},
|
||||
],
|
||||
}),
|
||||
],
|
||||
base: './',
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
}
|
||||
},
|
||||
build: {
|
||||
minify: 'esbuild', // ← Statt terser
|
||||
rollupOptions: {
|
||||
|
Reference in New Issue
Block a user