Compare commits

..

12 Commits

Author SHA1 Message Date
d2e4345d20 Enhance SheetHeader component: add total attribute calculation methods (LP, AP, KP, SP), update progress bar logic, and streamline data handling. 2025-07-14 00:07:09 +02:00
ca72b7119f Add Shaddar est Mudor.json to public/data: introduce new JSON file for data management. 2025-07-13 23:48:23 +02:00
b5acb2b76d Add CharacterSheet type definition: introduce foundational TypeScript interfaces and types for character management. 2025-07-13 02:31:29 +02:00
81b365d8a2 Refactor Skills component: improve data loading logic with useCallback, add error handling, update UI styles, and enhance state management. 2025-07-13 02:28:15 +02:00
fbd5b2cfb6 Refactor SheetHeader styles and layout: adjust attribute card design, improve responsiveness, and streamline conditional rendering logic. 2025-07-13 02:28:04 +02:00
2a1d0a470f Enhance SheetHeader component: add error handling, implement loading states, support conditional rendering for attributes, and improve state management with useCallback. 2025-07-12 20:13:48 +02:00
44f60270eb Refactor imports and paths: implement base URL aliasing with @, adjust TypeScript configuration, and update all relevant imports. 2025-07-06 23:21:27 +02:00
87a533858a Enhance SheetHeader tooltips: add hoverable tooltips for attribute names. 2025-07-06 22:36:18 +02:00
fd11a2efe0 Refactor SheetHeader component: improve formatting, adjust styles, and update data handling logic. 2025-07-06 22:33:41 +02:00
b5d85bc0fc Refactor SheetHeader component: improve formatting, adjust styles, and update data handling logic. 2025-07-06 22:32:52 +02:00
44d74396e5 added racevariant parsing 2025-07-06 22:19:41 +02:00
1a4c5d96e7 Refactor data loading logic: replace dynamic imports with fetch, update skills type, and integrate vite-plugin-static-copy for JSON assets. 2025-07-06 21:37:07 +02:00
19 changed files with 1085 additions and 320 deletions

191
package-lock.json generated
View File

@@ -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",

View File

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

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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/>
</>
);

View File

@@ -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>,
);

View File

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

View File

@@ -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>
);
}

View File

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

View File

@@ -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");

View File

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

View File

@@ -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>
);
}

View File

@@ -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
View 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;
}

View File

@@ -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);

View File

@@ -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"
]
}

View File

@@ -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: {