I’m developing a D&D-inspired web application where characters can have multiple vocations (classes). I need to correctly determine if a character is “arcane” (can use magic) based on their vocations. A character is considered arcane if at least one of their vocations has arcaneWielding=true in the database.
I recently updated my database schema to support multiple vocations per character:
// In schema.prisma
model Character {
// ...other fields
vocations String[] @default([])
// ...more fields
}
I’m having issues with the mana calculation in my BodyAndBurden component, which needs to know if the character is arcane to calculate the correct mana value.
Here’s my current implementation:
// In BodyAndBurden.tsx
// Use the direct vocation arcane status hook
const { isArcane, loading: arcaneStatusLoading } = useVocationArcaneStatus(character.vocations);
// Memoize character mana calculation
const characterMana = useMemo(() => {
// Determine if the character is arcane based solely on the hook value
const isCharacterArcane = isArcane === true;
// Calculate mana based on whether the character is arcane
const calculatedMana = calculateCharacterMana(
character.abilityCON,
character.abilityINT,
isCharacterArcane
);
return calculatedMana;
}, [
character.abilityCON,
character.abilityINT,
isArcane,
arcaneStatusLoading,
vocation
]);
The hook that checks arcane status:
// In useVocationArcaneStatus.ts
export function useVocationArcaneStatus(vocations: string | string[] | null) {
const [isArcane, setIsArcane] = useState<boolean | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!vocations) {
setIsArcane(false);
setLoading(false);
return;
}
// Handle both string and array inputs
let vocationNames: string[] = [];
let vocationNameForApi: string;
if (Array.isArray(vocations)) {
// If vocations is already an array, use it directly
vocationNames = vocations.filter(Boolean).map(v => v.toLowerCase());
vocationNameForApi = vocations.join(',');
} else {
// If vocations is a string, split it (for backward compatibility)
vocationNames = vocations.split(/[,;]/).map(v => v.trim().toLowerCase());
vocationNameForApi = vocations;
}
const fetchArcaneStatus = async () => {
try {
// Fetch from API
const response = await dataFetcher.fetch<{ isArcane: boolean }>(
`/api/player-sheet/vocation-arcane-status?name=${encodeURIComponent(vocationNameForApi)}`
);
setIsArcane(response.isArcane);
setError(null);
} catch (err) {
// Error handling...
} finally {
setLoading(false);
}
};
fetchArcaneStatus();
}, [vocations]);
return { isArcane, loading, error };
}
The API endpoint:
// In vocation-arcane-status/route.ts
export async function GET(request: NextRequest) {
// ...
// Handle multiple vocations (comma or semicolon separated)
const vocationNames = vocationName.split(/[,;]/).map(v => v.trim()).filter(Boolean);
// If there are multiple vocations, check each one
if (vocationNames.length > 1) {
const vocations = await db.vocation.findMany({
where: {
name: {
in: vocationNames
}
},
select: {
name: true,
arcaneWielding: true
}
});
// If any vocation is arcane, return true
const isArcane = vocations.some(v => v.arcaneWielding);
return { isArcane };
}
// ...
}
The mana calculation function:
export const calculateCharacterMana = (
constitutionScore: number,
intelligenceScore: number,
isArcane: boolean
): number => {
const constitutionModifier = calculateModifier(constitutionScore);
const intelligenceModifier = calculateModifier(intelligenceScore);
if (isArcane) {
return 10 + constitutionModifier + intelligenceModifier;
} else {
return 5 + constitutionModifier;
}
};
My Question
I’ve fixed a previous issue where we were hardcoding “arcanist” as an arcane vocation instead of properly querying the database. Now I want to ensure my implementation correctly handles multiple vocations.
What’s the most efficient and reliable way to determine if a character with multiple vocations is arcane (has at least one vocation with arcaneWielding=true)? I’m concerned about:
- Race conditions in the hook
- Proper handling of the loading state
- Ensuring the mana calculation is correct when vocations change
- Optimizing database queries for this common operation
Any suggestions for improving this implementation? I’m particularly interested in best practices for handling derived state that depends on multiple asynchronous data sources.