New feature system for training and homebrew

This commit is contained in:
Clément Pons 2025-06-03 16:42:41 +02:00
parent df3577f673
commit 42915d699f
18 changed files with 677 additions and 207 deletions

View File

@ -4,10 +4,12 @@
<div class="grid grid-cols-8 px-3 pt-2 pb-2">
<ToastTitle v-if="toast.title" class="font-semibold text-xl col-span-7 text-light-70 dark:text-dark-70" asChild><h4>{{ toast.title }}</h4></ToastTitle>
<ToastClose v-if="toast.closeable" aria-label="Close" class="text-xl -translate-y-2 translate-x-4 cursor-pointer"><span aria-hidden>×</span></ToastClose>
<ToastDescription v-if="toast.content" class="text-sm col-span-8 text-light-70 dark:text-dark-70" asChild><span>{{ toast.content }}</span></ToastDescription>
<ToastDescription v-if="toast.content" class="text-sm col-span-8 text-light-100 dark:text-dark-100" asChild><span>{{ toast.content }}</span></ToastDescription>
</div>
<TimerProgress v-if="toast.timer" shape="thin" :delay="toast.duration" class="mb-0 mt-0 w-full group-data-[type=error]:bg-light-redBack dark:group-data-[type=error]:bg-dark-redBack group-data-[type=error]:*:bg-light-red dark:group-data-[type=error]:*:bg-dark-red
group-data-[type=success]:bg-light-greenBack dark:group-data-[type=success]:bg-dark-greenBack group-data-[type=success]:*:bg-light-green dark:group-data-[type=success]:*:bg-dark-green" @finish="() => tryClose(toast, false)" />
<TimerProgress v-if="toast.timer" shape="thin" :delay="toast.duration" class="mb-0 mt-0 w-full
group-data-[type=error]:*:bg-light-red dark:group-data-[type=error]:*:bg-dark-red group-data-[type=success]:*:bg-light-green dark:group-data-[type=success]:*:bg-dark-green
group-data-[type=error]:bg-light-red dark:group-data-[type=error]:bg-dark-red group-data-[type=success]:bg-light-green dark:group-data-[type=success]:bg-dark-green !bg-opacity-50"
@finish="() => tryClose(toast, false)" />
</ToastRoot>
<ToastViewport class="fixed bottom-0 right-0 flex flex-col p-6 gap-2 max-w-[512px] z-50 outline-none min-w-72" />
@ -37,14 +39,16 @@ function tryClose(config: ExtraToastConfig, state: boolean)
.ToastRoot[data-type='error'] {
@apply border-light-red;
@apply dark:border-dark-red;
@apply bg-light-redBack;
@apply dark:bg-dark-redBack;
@apply bg-light-red;
@apply dark:bg-dark-red;
@apply !bg-opacity-50;
}
.ToastRoot[data-type='success'] {
@apply border-light-green;
@apply dark:border-dark-green;
@apply bg-light-greenBack;
@apply dark:bg-dark-greenBack;
@apply bg-light-green;
@apply dark:bg-dark-green;
@apply !bg-opacity-50;
}
.ToastRoot[data-state='open'] {
animation: slideIn .15s cubic-bezier(0.16, 1, 0.3, 1);

BIN
db.sqlite

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -14,12 +14,12 @@
</template>
<script setup lang="ts">
import type { NuxtError } from '#app'
import type { NuxtError } from '#app';
import { Icon } from '@iconify/vue/dist/iconify.js';
const props = defineProps({
error: Object as () => NuxtError
})
});
const handleError = () => clearError({ redirect: '/' })
const handleError = () => clearError({ redirect: '/' });
</script>

View File

@ -56,14 +56,14 @@ export default defineNuxtConfig({
current: 'currentColor',
light: {
red: '#e93147',
redBack: '#F9C7CD',
orange: '#ec7500',
yellow: '#e0ac00',
green: '#08b94e',
greenBack: '#BCECCF',
orange: '#FF9800',
yellow: '#FFEB3B',
green: '#388E3C',
indigo: '#7986CB',
cyan: '#00bfbc',
lime: '#8BC34A',
blue: '#086ddd',
purple: '#7852ee',
purple: '#AB47BC',
pink: '#d53984',
0: "#ffffff",
5: "#fcfcfc",

View File

@ -352,7 +352,7 @@ useShortcuts({
<span class="text-lg font-bold">{{ spell.name }}</span>
<div class="flex flex-row items-center gap-6">
<div class="flex flex-row text-sm gap-2">
<span v-for="element of spell.elements" :class="elementTexts[element].class">{{ elementTexts[element].text }}</span>
<span v-for="element of spell.elements" :class="elementTexts[element].class" class="border !border-opacity-50 rounded-full !bg-opacity-20 px-2 py-px">{{ elementTexts[element].text }}</span>
</div>
<div class="flex flex-row text-sm gap-1">
<span class="">Rang {{ spell.rank }}</span><span>/</span>

View File

@ -12,6 +12,18 @@ const { user } = useUserSession();
const { add } = useToast();
const { data: character, status, error } = await useFetch(`/api/character/${id}/compiled`);
/*
text-light-red dark:text-dark-red border-light-red dark:border-dark-red bg-light-red dark:bg-dark-red
text-light-blue dark:text-dark-blue border-light-blue dark:border-dark-blue bg-light-blue dark:bg-dark-blue
text-light-yellow dark:text-dark-yellow border-light-yellow dark:border-dark-yellow bg-light-yellow dark:bg-dark-yellow
text-light-orange dark:text-dark-orange border-light-orange dark:border-dark-orange bg-light-orange dark:bg-dark-orange
text-light-indigo dark:text-dark-indigo border-light-indigo dark:border-dark-indigo bg-light-indigo dark:bg-dark-indigo
text-light-lime dark:text-dark-lime border-light-lime dark:border-dark-lime bg-light-lime dark:bg-dark-lime
text-light-green dark:text-dark-green border-light-green dark:border-dark-green bg-light-green dark:bg-dark-green
text-light-yellow dark:text-dark-yellow border-light-yellow dark:border-dark-yellow bg-light-yellow dark:bg-dark-yellow
text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-purple bg-light-purple dark:bg-dark-purple
*/
</script>
<template>
@ -39,7 +51,7 @@ const { data: character, status, error } = await useFetch(`/api/character/${id}/
</div>
</div>
<div class="flex gap-6 lg:border-l border-light-30 dark:border-dark-30 py-4 ps-4">
<span class="flex flex-row items-center gap-2">PV: {{ character.health - character.values.hp }}/{{ character.health }}</span>
<span class="flex flex-row items-center gap-2">PV: {{ character.health - character.values.healht }}/{{ character.health }}</span>
<span class="flex flex-row items-center gap-2">Mana: {{ character.mana - character.values.mana }}/{{ character.mana }}</span>
</div>
</div>
@ -158,7 +170,7 @@ const { data: character, status, error } = await useFetch(`/api/character/${id}/
<span class="text-lg font-bold">{{ spell.name }}</span>
<div class="flex flex-row items-center gap-6">
<div class="flex flex-row text-sm gap-2">
<span v-for="element of spell.elements" :class="elementTexts[element].class">{{ elementTexts[element].text }}</span>
<span v-for="element of spell.elements" :class="elementTexts[element].class" class="border !border-opacity-50 rounded-full !bg-opacity-20 px-2 py-px">{{ elementTexts[element].text }}</span>
</div>
<div class="flex flex-row text-sm gap-1">
<span class="">Rang {{ spell.rank }}</span><span>/</span>

View File

@ -0,0 +1,57 @@
<script setup lang="ts">
import { unifySlug } from '~/shared/general.util';
import type { Feature } from '~/types/character';
definePageMeta({
guestsGoesTo: '/user/login',
rights: ['admin', 'editor'],
});
const id = unifySlug(useRouter().currentRoute.value.params.id);
const { add } = useToast();
const homebrew = ref<Feature>({
id: id === 'new' ? -1 : parseInt(id, 10),
list: [],
});
if(id !== 'new')
{
const _homebrew = await useRequestFetch()(`/api/homebrew/${id}`);
if(!_homebrew) throw new Error('Donnée de personnalisation introuvable');
homebrew.value = Object.assign(homebrew.value, _homebrew);
}
function save(force: boolean)
{
}
</script>
<template>
<Head>
<Title>d[any] - Edition de {{ homebrew.name || 'nouvelle personnalisation' }}</Title>
</Head>
<div class="flex flex-col gap-8 align-center">
<div class="flex flex-row gap-4 align-center justify-center">
<div class="flex flex-row gap-4 align-center justify-center">
<Label class="flex items-center justify-between flex-row gap-2">
<span class="pb-1 mx-2 md:p-0">Titre</span>
<input class="caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50
bg-light-20 dark:bg-dark-20 outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20"
type="text" v-model="homebrew.name">
</Label>
</div>
<div class="self-center">
<Tooltip side="right" message="Ctrl+S"><Button @click="() => save(true)">Enregistrer</Button></Tooltip>
</div>
</div>
<div class="flex flex-1 flex-col min-w-[800px] w-[75vw] max-w-[1200px]">
<div class="m-2 overflow-auto">
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,3 @@
<template>
</template>

View File

@ -1,9 +1,9 @@
import useDatabase from '~/composables/useDatabase';
import { defaultCharacter, type Ability, type Character, type CharacterConfig, type CompiledCharacter, type DoubleIndex, type Feature, type Level, type MainStat, type TrainingLevel, type TrainingOption } from '~/types/character';
import { defaultCharacter, type Ability, type Character, type CharacterConfig, type CompiledCharacter, type DoubleIndex, type Feature, type FeatureItem, type Level, type MainStat, type TrainingLevel, type TrainingOption } from '~/types/character';
import characterData from '#shared/character-config.json';
import { group } from '~/shared/general.util';
export default defineEventHandler(async (e) => {
export default defineCachedEventHandler(async (e) => {
const id = getRouterParam(e, "id");
if(!id)
{
@ -53,7 +53,7 @@ export default defineEventHandler(async (e) => {
setResponseStatus(e, 404);
return;
}/* , { name: "character", getKey: (e) => getRouterParam(e, "id") || 'error' } */);
}, { name: "character", getKey: (e) => getRouterParam(e, "id") || 'error' });
function compileCharacter(character: Character & { username?: string }): CompiledCharacter
{
@ -112,6 +112,7 @@ function compileCharacter(character: Character & { username?: string }): Compile
spells: character.spells ?? [],
speed: false,
defense: {
hardcap: Infinity,
static: 6,
activeparry: 0,
activedodge: 0,
@ -126,7 +127,8 @@ function compileCharacter(character: Character & { username?: string }): Compile
multiattack: 1,
magicpower: 0,
magicspeed: 0,
magicelement: 0
magicelement: 0,
magicinstinct: 0,
},
resistance: {
stun: [0, 0],
@ -145,74 +147,73 @@ function compileCharacter(character: Character & { username?: string }): Compile
notes: character.notes ?? "",
};
features.forEach(e => e[1].forEach((_e, i) => applyTrainingOption(e[0], _e, compiled, i === e[1].length - 1)));
specialFeatures(compiled, character.training);
Object.entries(character.abilities).forEach(e => compiled.abilities[e[0] as Ability]! += e[1][0]);
features.forEach(e => e[1].forEach(_e => _e.features?.forEach(f => applyFeature(compiled, f))));
return compiled;
}
function applyTrainingOption(stat: MainStat, option: TrainingOption, character: CompiledCharacter, last: boolean)
function applyFeature(character: CompiledCharacter, f: FeatureItem)
{
if(option.health) character.health += option.health;
if(option.mana) character.mana += option.mana;
if(option.mastery) character.mastery[option.mastery]++;
if(option.speed) character.speed = option.speed;
if(option.initiative) character.initiative += option.initiative;
if(option.spellrank) character.spellranks[option.spellrank]++;
if(option.defense) option.defense.forEach(e => character.defense[e]++);
if(option.resistance) option.resistance.forEach(e => character.resistance[e[0]][e[1] === "attack" ? 0 : 1]++);
if(option.spellslot) character.spellslots += option.spellslot in character.modifier ? character.modifier[option.spellslot as MainStat] : option.spellslot as number;
if(option.arts) character.artslots += option.arts in character.modifier ? character.modifier[option.arts as MainStat] : option.arts as number;
if(option.spell) character.spells.push(option.spell);
switch(f.category)
{
case "action":
character.features.action.push(f.text);
option.description.forEach(line => !line.disposable && (last || !line.replaced) && character.features[line.category ?? "misc"].push(line.text));
return;
case "reaction":
character.features.reaction.push(f.text);
//if(option.features) option.features.forEach(e => applyFeature(e, character));
return;
case "freeaction":
character.features.freeaction.push(f.text);
return;
case "misc":
character.features.misc.push(f.text);
return;
case "asset":
if(f.type === 'add')
character[f.kind].push(f.asset);
else
character[f.kind] = character[f.kind].filter(e => e !== f.asset);
return;
case "value":
const path = f.property.split(".");
const object = path.slice(0, -1).reduce((p, v) => p[v], character as any);
switch(f.type)
{
case "add":
if(!['number'].includes(typeof object[path[path.length - 1]]))
break;
object[path[path.length - 1]] += f.value;
break;
case "remove":
if(!['number'].includes(typeof object[path[path.length - 1]]))
break;
object[path[path.length - 1]] -= f.value as number;
break;
case "set":
if(!['number', 'boolean'].includes(typeof object[path[path.length - 1]]))
break;
object[path[path.length - 1]] = f.value;
break;
default:
break;
}
return;
default:
return;
}
}
function specialFeatures(character: CompiledCharacter, levels: Record<MainStat, DoubleIndex<TrainingLevel>[]>)
{
//Cap la défense
const strengthCap3 = levels.strength.some(e => e[0] === 0);
const strengthCap6 = levels.strength.some(e => e[0] === 1);
const strengthUncapped = levels.strength.some(e => e[0] === 2);
const dexterityCap3 = levels.dexterity.some(e => e[0] === 0);
const dexterityCap3Stat = levels.dexterity.some(e => e[0] === 1);
const dexterityUncapped = levels.dexterity.some(e => e[0] === 2);
if(!strengthUncapped || !dexterityUncapped)
{
if(strengthCap6)
{
character.defense = {
static: 6,
activeparry: 0,
activedodge: 0,
passiveparry: 0,
passivedodge: 0,
};
}
else if(strengthCap3 || dexterityCap3)
{
character.defense = {
static: 3,
activeparry: 0,
activedodge: 0,
passiveparry: 0,
passivedodge: 0,
};
}
else if(dexterityCap3Stat)
{
character.defense.static = 3;
}
}
}/*
function applyFeature(feature: Feature, character: CompiledCharacter)
{
} */
export function getFeaturesOf(stat: MainStat, progression: DoubleIndex<TrainingLevel>[]): TrainingOption[]
{
const config = characterData as CharacterConfig;

View File

@ -1,6 +1,6 @@
import { eq } from 'drizzle-orm';
import useDatabase from '~/composables/useDatabase';
import { characterTable } from '~/db/schema';
import { characterAbilitiesTable, characterLevelingTable, characterModifiersTable, characterSpellsTable, characterTable, characterTrainingTable } from '~/db/schema';
export default defineEventHandler(async (e) => {
const id = getRouterParam(e, "id");
@ -26,12 +26,46 @@ export default defineEventHandler(async (e) => {
return;
}
const returned = await db.insert(characterTable).values({
name: `Copie de ${old.name}`,
progress: old.progress,
owner: session.user.id,
}).returning({ id: characterTable.id });
try
{
const _id = db.transaction((tx) => {
const _id = tx.insert(characterTable).values({
name: old.name,
owner: session.user!.id,
people: old.people!,
level: old.level,
aspect: old.aspect,
notes: old.notes,
health: old.health,
mana: old.mana,
visibility: old.visibility,
thumbnail: old.thumbnail,
}).returning({ id: characterTable.id }).get().id;
const leveling = tx.select().from(characterLevelingTable).where(eq(characterLevelingTable.character, parseInt(id, 10))).all();
if(leveling.length > 0) tx.insert(characterLevelingTable).values(leveling.map(e => ({ character: _id, level: e.level, choice: e.choice }))).run();
const training = tx.select().from(characterTrainingTable).where(eq(characterTrainingTable.character, parseInt(id, 10))).all();
if(training.length > 0) tx.insert(characterTrainingTable).values(training.map(e => ({ character: _id, stat: e.stat, level: e.level, choice: e.choice }))).run();
const modifiers = tx.select().from(characterModifiersTable).where(eq(characterModifiersTable.character, parseInt(id, 10))).all();
if(modifiers.length > 0) tx.insert(characterModifiersTable).values(modifiers.map(e => ({ character: _id, modifier: e.modifier, value: e.value }))).run();
const spells = tx.select().from(characterSpellsTable).where(eq(characterSpellsTable.character, parseInt(id, 10))).all();
if(spells.length > 0) tx.insert(characterSpellsTable).values(spells.map(e => ({ character: _id, value: e.value }))).run();
const abilities = tx.select().from(characterAbilitiesTable).where(eq(characterAbilitiesTable.character, parseInt(id, 10))).all();
if(abilities.length > 0) tx.insert(characterAbilitiesTable).values(abilities.map(e => ({ character: _id, ability: e.ability, value: e.value, max: e.max }))).run();
return _id;
});
setResponseStatus(e, 201);
return returned[0].id;
return _id;
}
catch(_e)
{
setResponseStatus(e, 201);
throw _e;
}
});

View File

@ -21,12 +21,13 @@ export default defineEventHandler(async (e) => {
const db = useDatabase();
const character = db.select({
values: characterTable.values
health: characterTable.health,
mana: characterTable.mana,
}).from(characterTable).where(and(eq(characterTable.id, parseInt(id, 10)), eq(characterTable.owner, session.user.id))).get();
if(character !== undefined)
{
return character.values as CharacterValues;
return character as CharacterValues;
}
setResponseStatus(e, 404);

View File

@ -1,6 +1,7 @@
import { eq } from 'drizzle-orm';
import useDatabase from '~/composables/useDatabase';
import { characterTable } from '~/db/schema';
import type { CharacterValues } from '~/types/character';
export default defineEventHandler(async (e) => {
const id = getRouterParam(e, "id");
@ -10,7 +11,7 @@ export default defineEventHandler(async (e) => {
return;
}
const body = await readBody(e);
const body = await readBody(e) as CharacterValues;
if(!body)
{
setResponseStatus(e, 400);
@ -34,7 +35,8 @@ export default defineEventHandler(async (e) => {
}
db.update(characterTable).set({
values: body,
health: body.health,
mana: body.mana,
}).where(eq(characterTable.id, parseInt(id, 10))).run();
setResponseStatus(e, 200);

View File

@ -0,0 +1,22 @@
export default defineEventHandler(async (e) => {
const id = getRouterParam(e, "id");
if(!id)
{
setResponseStatus(e, 400);
return;
}
const session = await getUserSession(e);
if(!session.user)
{
setResponseStatus(e, 401);
return;
}
console.log(id);
setResponseStatus(e, 200);
return {};
});

View File

@ -0,0 +1,23 @@
export default defineEventHandler(async (e) => {
const id = getRouterParam(e, "id");
if(!id)
{
setResponseStatus(e, 400);
return;
}
const session = await getUserSession(e);
if(!session.user)
{
setResponseStatus(e, 401);
return;
}
console.log(id);
console.log(await readBody(e));
setResponseStatus(e, 200);
return;
});

View File

@ -81,6 +81,14 @@
"disposable": false,
"replaced": true
}
],
"features": [
{
"category": "value",
"type": "set",
"property": "defense.hardcap",
"value": 3
}
]
}
],
@ -92,7 +100,7 @@
"disposable": true
},
{
"text": "Def max à 5.",
"text": "Def max à 6.",
"disposable": false,
"replaced": true
},
@ -102,7 +110,20 @@
"replaced": true
}
],
"speed": false
"features": [
{
"category": "value",
"type": "set",
"property": "defense.hardcap",
"value": 6
},
{
"category": "value",
"type": "set",
"property": "speed",
"value": false
}
]
}
],
"2": [
@ -122,8 +143,26 @@
"replaced": true
}
],
"speed": 0,
"mastery": "strength"
"features": [
{
"category": "value",
"type": "set",
"property": "defense.hardcap",
"value": 9999
},
{
"category": "value",
"type": "set",
"property": "speed",
"value": 0
},
{
"category": "value",
"type": "add",
"property": "mastery.strength",
"value": 1
}
]
}
],
"3": [
@ -142,9 +181,26 @@
"disposable": true
}
],
"speed": 3,
"mastery": "armor",
"defense": ["activeparry"]
"features": [
{
"category": "value",
"type": "set",
"property": "speed",
"value": 3
},
{
"category": "value",
"type": "add",
"property": "mastery.armor",
"value": 1
},
{
"category": "value",
"type": "add",
"property": "defense.activeparry",
"value": 1
}
]
}
],
"4": [
@ -163,9 +219,32 @@
"disposable": true
}
],
"speed": 6,
"mastery": "strength",
"defense": ["passiveparry", "activeparry"]
"features": [
{
"category": "value",
"type": "set",
"property": "speed",
"value": 6
},
{
"category": "value",
"type": "add",
"property": "mastery.strength",
"value": 1
},
{
"category": "value",
"type": "add",
"property": "defense.activeparry",
"value": 1
},
{
"category": "value",
"type": "add",
"property": "defense.passiveparry",
"value": 1
}
]
}
],
"5": [
@ -176,7 +255,14 @@
"disposable": true
}
],
"mastery": "strength"
"features": [
{
"category": "value",
"type": "add",
"property": "mastery.strength",
"value": 1
}
]
},
{
"description": [
@ -184,6 +270,12 @@
"text": "Lorsque vous [[1. Règles/3. Le combat/2. Actions en combat#Intercepter|interceptez]] un adversaire, vous pouvez faire une attaque plutôt que de le contraindre.",
"disposable": false
}
],
"features": [
{
"category": "misc",
"text": "Lorsque vous [[1. Règles/3. Le combat/2. Actions en combat#Intercepter|interceptez]] un adversaire, vous pouvez faire une attaque plutôt que de le contraindre."
}
]
},
{
@ -193,7 +285,14 @@
"disposable": true
}
],
"mastery": "shield"
"features": [
{
"category": "value",
"type": "add",
"property": "mastery.shield",
"value": 1
}
]
}
],
"6": [
@ -204,7 +303,14 @@
"disposable": true
}
],
"mastery": "strength"
"features": [
{
"category": "value",
"type": "add",
"property": "mastery.strength",
"value": 1
}
]
},
{
"description": [
@ -212,6 +318,12 @@
"text": "En infligeant des dégâts critique, vous pouvez choisir d'ignorer l'armure adverse.",
"disposable": false
}
],
"features": [
{
"category": "misc",
"text": "En infligeant des dégâts critique, vous pouvez choisir d'ignorer l'armure adverse."
}
]
},
{
@ -221,7 +333,14 @@
"disposable": true
}
],
"mastery": "armor"
"features": [
{
"category": "value",
"type": "add",
"property": "mastery.armor",
"value": 1
}
]
}
],
"7": [
@ -231,6 +350,12 @@
"text": "Utiliser la [[1. Règles/2. L'entrainement/1. Entrainement#La force|force]] pour frapper avec une arme augmente les dégâts infligés de 2.",
"disposable": false
}
],
"features": [
{
"category": "misc",
"text": "Utiliser la [[1. Règles/2. L'entrainement/1. Entrainement#La force|force]] pour frapper avec une arme augmente les dégâts infligés de 2."
}
]
},
{
@ -240,7 +365,14 @@
"disposable": true
}
],
"mastery": "strength"
"features": [
{
"category": "value",
"type": "add",
"property": "mastery.strength",
"value": 1
}
]
},
{
"description": [
@ -249,7 +381,14 @@
"disposable": true
}
],
"mastery": "armor"
"features": [
{
"category": "value",
"type": "add",
"property": "mastery.armor",
"value": 1
}
]
}
],
"8": [
@ -259,6 +398,14 @@
"text": "Frapper avec une [[1. Règles/99. Annexes/4. Équipement#Les armes|arme standard]], [[1. Règles/99. Annexes/4. Équipement#Les armes lourdes|lourdes]] ou [[1. Règles/99. Annexes/4. Équipement#Les armes à deux mains|à deux mains]] augmente les dégâts infligés de 2, mais réduit le [[1. Règles/99. Annexes/1. Les évolutions de valeur.canvas#Les niveaux de dé de dégâts|dé de dégâts]] au niveau inférieur.",
"disposable": false
}
],
"features": [
{
"category": "value",
"type": "add",
"property": "mastery.armor",
"value": 1
}
]
},
{
@ -268,6 +415,14 @@
"disposable": false,
"category": "action"
}
],
"features": [
{
"category": "value",
"type": "add",
"property": "mastery.armor",
"value": 1
}
]
},
{
@ -276,6 +431,12 @@
"text": "En vous [[1. Règles/3. Le combat/2. Actions en combat#S'interposer|interposant]], vous gagnez un bonus de +2 pour contraindre.",
"disposable": false
}
],
"features": [
{
"category": "misc",
"text": "En vous [[1. Règles/3. Le combat/2. Actions en combat#S'interposer|interposant]], vous gagnez un bonus de +2 pour contraindre."
}
]
}
],
@ -286,6 +447,12 @@
"text": "Au prix d'un point de [[1. Règles/99. Annexes/3. Fatigue et repos#Fatigue temporaire|fatigue temporaire]], durant votre tour, les dégâts que vous infligerez avec une [[1. Règles/99. Annexes/4. Équipement#Les armes|arme standard]], [[1. Règles/99. Annexes/4. Équipement#Les armes lourdes|lourdes]] ou [[1. Règles/99. Annexes/4. Équipement#Les armes à deux mains|à deux mains]] vous permet de lancer un second dé de dégâts de votre arme. *Ce dé peut être doublé en cas de dégâts critique.*",
"disposable": false
}
],
"features": [
{
"category": "misc",
"text": "Au prix d'un point de [[1. Règles/99. Annexes/3. Fatigue et repos#Fatigue temporaire|fatigue temporaire]], durant votre tour, les dégâts que vous infligerez avec une [[1. Règles/99. Annexes/4. Équipement#Les armes|arme standard]], [[1. Règles/99. Annexes/4. Équipement#Les armes lourdes|lourdes]] ou [[1. Règles/99. Annexes/4. Équipement#Les armes à deux mains|à deux mains]] vous permet de lancer un second dé de dégâts de votre arme. *Ce dé peut être doublé en cas de dégâts critique.*"
}
]
},
{
@ -294,6 +461,12 @@
"text": "Après avoir pris un adversaire en tenaille, si un allié parvient à le toucher, vous obtenez également un [[1. Règles/1. Introduction/2. Glossaire#Avantage et désavantage|avantage]] sur votre **première** attaque contre cet adversaire.",
"disposable": false
}
],
"features": [
{
"category": "misc",
"text": "Après avoir pris un adversaire en tenaille, si un allié parvient à le toucher, vous obtenez également un [[1. Règles/1. Introduction/2. Glossaire#Avantage et désavantage|avantage]] sur votre **première** attaque contre cet adversaire."
}
]
},
{
@ -303,7 +476,14 @@
"disposable": true
}
],
"mastery": "shield"
"features": [
{
"category": "value",
"type": "add",
"property": "mastery.shield",
"value": 1
}
]
}
],
"10": [
@ -313,6 +493,12 @@
"text": "Au prix d'un point de [[1. Règles/99. Annexes/3. Fatigue et repos#Fatigue persistante|fatigue persistante]], durant tout votre tour, vous obtenez un bonus de +4 pour frapper avec une [[1. Règles/99. Annexes/4. Équipement#Les armes|arme standard]], [[1. Règles/99. Annexes/4. Équipement#Les armes lourdes|lourdes]] ou [[1. Règles/99. Annexes/4. Équipement#Les armes à deux mains|à deux mains]].",
"disposable": false
}
],
"features": [
{
"category": "misc",
"text": "Au prix d'un point de [[1. Règles/99. Annexes/3. Fatigue et repos#Fatigue persistante|fatigue persistante]], durant tout votre tour, vous obtenez un bonus de +4 pour frapper avec une [[1. Règles/99. Annexes/4. Équipement#Les armes|arme standard]], [[1. Règles/99. Annexes/4. Équipement#Les armes lourdes|lourdes]] ou [[1. Règles/99. Annexes/4. Équipement#Les armes à deux mains|à deux mains]]."
}
]
},
{
@ -321,6 +507,12 @@
"text": "Lorsque vous frappez en utilisant la [[1. Règles/2. L'entrainement/1. Entrainement#La force|force]], faire un 11 sur le lancer de d12 est considéré comme un coup critique. Cependant, vous subissez un malus de 1 point pour frapper.",
"disposable": false
}
],
"features": [
{
"category": "misc",
"text": "Lorsque vous frappez en utilisant la [[1. Règles/2. L'entrainement/1. Entrainement#La force|force]], faire un 11 sur le lancer de d12 est considéré comme un coup critique. Cependant, vous subissez un malus de 1 point pour frapper."
}
]
},
{
@ -329,6 +521,12 @@
"text": "Parer une attaque au corps à corps permet à **un seul** allié de saisir l'opportunité pour l'attaquer (au corps à corps).",
"disposable": false
}
],
"features": [
{
"category": "misc",
"text": "Parer une attaque au corps à corps permet à **un seul** allié de saisir l'opportunité pour l'attaquer (au corps à corps)."
}
]
}
],
@ -339,6 +537,12 @@
"text": "En frappant avec une [[1. Règles/99. Annexes/4. Équipement#Les armes|arme standard]], [[1. Règles/99. Annexes/4. Équipement#Les armes lourdes|lourde]] ou [[1. Règles/99. Annexes/4. Équipement#Les armes à deux mains|à deux mains]], vous pouvez choisir de subir un malus de -4 pour infliger 8 points de dégâts supplémentaires. *A annoncer avant le lancer de dé.*",
"disposable": false
}
],
"features": [
{
"category": "misc",
"text": "En frappant avec une [[1. Règles/99. Annexes/4. Équipement#Les armes|arme standard]], [[1. Règles/99. Annexes/4. Équipement#Les armes lourdes|lourde]] ou [[1. Règles/99. Annexes/4. Équipement#Les armes à deux mains|à deux mains]], vous pouvez choisir de subir un malus de -4 pour infliger 8 points de dégâts supplémentaires. *A annoncer avant le lancer de dé.*"
}
]
},
{
@ -347,6 +551,12 @@
"text": "Lorsque ce n'est pas votre tour, vous pouvez [[1. Règles/3. Le combat/2. Actions en combat#Saisir une opportunité|saisir l'opportunité]] pour frapper un adversaire lorsqu'il se déplace pour esquiver.",
"disposable": false
}
],
"features": [
{
"category": "misc",
"text": "Lorsque ce n'est pas votre tour, vous pouvez [[1. Règles/3. Le combat/2. Actions en combat#Saisir une opportunité|saisir l'opportunité]] pour frapper un adversaire lorsqu'il se déplace pour esquiver."
}
]
},
{
@ -355,6 +565,12 @@
"text": "En prenant en tenaille un adversaire, vous offrez l'[[1. Règles/1. Introduction/2. Glossaire#Avantage et désavantage|avantage]] aux attaques à distance et aux [[1. Règles/4. La magie/1. Magie#Les sorts de précision|sorts de précision]].",
"disposable": false
}
],
"features": [
{
"category": "misc",
"text": "En prenant en tenaille un adversaire, vous offrez l'[[1. Règles/1. Introduction/2. Glossaire#Avantage et désavantage|avantage]] aux attaques à distance et aux [[1. Règles/4. La magie/1. Magie#Les sorts de précision|sorts de précision]]."
}
]
}
],
@ -365,6 +581,12 @@
"text": "Vous êtes capable de tenir une [[1. Règles/99. Annexes/4. Équipement#Les armes à deux mains|arme à deux mains]] dans une seule main. Vous ne pouvez cependant pas tenir d'arme dans votre autre main, *même en ayant progressé dans l'[[1. Règles/99. Annexes/1. Les évolutions de valeur.canvas#Les armes multiples|arbre des armes multiples]]*.",
"disposable": true
}
],
"features": [
{
"category": "misc",
"text": "Vous êtes capable de tenir une [[1. Règles/99. Annexes/4. Équipement#Les armes à deux mains|arme à deux mains]] dans une seule main. Vous ne pouvez cependant pas tenir d'arme dans votre autre main, *même en ayant progressé dans l'[[1. Règles/99. Annexes/1. Les évolutions de valeur.canvas#Les armes multiples|arbre des armes multiples]]*."
}
]
},
{
@ -373,14 +595,26 @@
"text": "Au prix d'un point de [[1. Règles/99. Annexes/3. Fatigue et repos#Fatigue temporaire|fatigue temporaire]], durant tout un tour, faire une attaque ne demande que 1 point d'action.",
"disposable": false
}
],
"features": [
{
"category": "misc",
"text": "Au prix d'un point de [[1. Règles/99. Annexes/3. Fatigue et repos#Fatigue temporaire|fatigue temporaire]], durant tout un tour, faire une attaque ne demande que 1 point d'action."
}
]
},
{
"description": [
{
"text": "Vous pouvez frapper, puis vous [[1. Règles/3. Le combat/2. Actions en combat#S'interposer|interposer]] en 3 points d'action.",
"disposable": false,
"category": "action"
"disposable": false
}
],
"features": [
{
"category": "action",
"text": "Vous pouvez frapper, puis vous [[1. Règles/3. Le combat/2. Actions en combat#S'interposer|interposer]] en 3 points d'action.",
"cost": 3
}
]
}
@ -392,7 +626,8 @@
"text": "En tenant une [[1. Règles/99. Annexes/4. Équipement#Les armes|arme standard]], [[1. Règles/99. Annexes/4. Équipement#Les armes lourdes|lourdes]] ou [[1. Règles/99. Annexes/4. Équipement#Les armes à deux mains|à deux mains]], vous gagnez un bonus de +3 pour résister aux désarmement. De plus, lorsque l'on rate à vous contraindre au corps à corps, vous pouvez gratuitement contre attaquer avec votre poing *même si vous tenez une arme*.",
"disposable": false
}
]
],
"features": []
},
{
"description": [
@ -400,7 +635,8 @@
"text": "En infligeant des [[1. Règles/3. Le combat/1. Combat#Réussite critique|dégâts critique]], vous pouvez choisir de doubler les dégâts fixes.",
"disposable": false
}
]
],
"features": []
},
{
"description": [
@ -408,7 +644,8 @@
"text": "Vous êtes capable de tenir un [[1. Règles/99. Annexes/4. Équipement#Les boucliers à deux mains|bouclier à deux mains]] dans une seule main. Vous pouvez **au mieux** tenir une [[1. Règles/99. Annexes/4. Équipement#Les armes légères|arme légère]] ou [[1. Règles/99. Annexes/4. Équipement#Les armes de jet|de jet]] dans l'autre main.",
"disposable": false
}
]
],
"features": []
}
],
"14": [
@ -418,7 +655,8 @@
"text": "",
"disposable": false
}
]
],
"features": []
},
{
"description": [
@ -426,7 +664,8 @@
"text": "Vous pouvez, jusqu'au début de votre prochain tour, réduire votre défense à 5. Cependant, si vous êtes frappé par une attaque au corps à corps, vous pouvez immédiatement contre attaquer **gratuitement** avec un [[1. Règles/1. Introduction/2. Glossaire#Avantage et désavantage absolu|avantage absolu]].",
"disposable": false
}
]
],
"features": []
},
{
"description": [
@ -435,7 +674,8 @@
"disposable": false,
"category": "reaction"
}
]
],
"features": []
}
],
"15": [
@ -445,7 +685,8 @@
"text": "",
"disposable": false
}
]
],
"features": []
},
{
"description": [
@ -453,7 +694,8 @@
"text": "Votre érudition du combat est légendaire. Vous êtes capable en [[1. Règles/3. Le combat/2. Actions en combat#Communiquer|communiquant]] d'offrir un bonus de +3 à un allié que vous voyez attaquer.",
"disposable": false
}
]
],
"features": []
},
{
"description": [
@ -461,7 +703,8 @@
"text": "Lorsque vous parez passivement, vous réduisez les dégâts d'un montant égal à votre bonus de parade passive.",
"disposable": false
}
]
],
"features": []
}
]
},
@ -479,7 +722,8 @@
"disposable": false,
"replaced": true
}
]
],
"features": []
}
],
"1": [
@ -500,7 +744,8 @@
"disposable": false,
"replaced": true
}
]
],
"features": []
}
],
"2": [
@ -516,7 +761,8 @@
"disposable": false,
"replaced": true
}
]
],
"features": []
}
],
"3": [
@ -532,7 +778,8 @@
"replaced": true
}
],
"defense": ["activedodge"]
"defense": ["activedodge"],
"features": []
}
],
"4": [
@ -551,7 +798,8 @@
"disposable": true
}
],
"defense": ["passivedodge", "activedodge"]
"defense": ["passivedodge", "activedodge"],
"features": []
}
],
"5": [
@ -561,7 +809,8 @@
"text": "Vous pouvez utiliser la dextérité pour frapper avec une [[1. Règles/99. Annexes/4. Équipement#Les armes naturelles|arme naturelle]]. Une arme naturelle est considéré comme une [[1. Règles/99. Annexes/4. Équipement#Les armes|arme]] et bénéficie des mêmes bonus.",
"disposable": false
}
]
],
"features": []
},
{
"description": [
@ -570,7 +819,8 @@
"disposable": true
}
],
"mastery": "dexterity"
"mastery": "dexterity",
"features": []
},
{
"description": [
@ -578,7 +828,8 @@
"text": "Vous progressez dans l'arbre des [[1. Règles/99. Annexes/1. Les évolutions de valeur.canvas#Les armes multiples|armes multiples]].",
"disposable": true
}
]
],
"features": []
}
],
"6": [
@ -593,7 +844,8 @@
"disposable": false
}
],
"initiative": 1
"initiative": 1,
"features": []
},
{
"description": [
@ -602,7 +854,8 @@
"disposable": true
}
],
"mastery": "dexterity"
"mastery": "dexterity",
"features": []
},
{
"description": [
@ -611,7 +864,8 @@
"disposable": true
}
],
"resistance": [["precision", "defense"]]
"resistance": [["precision", "defense"]],
"features": []
}
],
"7": [
@ -626,7 +880,8 @@
"disposable": true
}
],
"defense": ["passivedodge", "activedodge"]
"defense": ["passivedodge", "activedodge"],
"features": []
},
{
"description": [
@ -638,7 +893,8 @@
"text": "",
"disposable": false
}
]
],
"features": []
},
{
"description": [
@ -646,7 +902,8 @@
"text": "Frapper un ennemi au corps à corps dans le dos applique les même bonus que la [[1. Règles/3. Le combat/2. Actions en combat#Prendre en tenaille|prise en tenaille]]. Si l'ennemi a déjà été pris en tenaille ce tour ci, vous pouvez ignorer l'[[1. Règles/99. Annexes/4. Équipement#Les armures lourdes|armure lourde]].",
"disposable": false
}
]
],
"features": []
}
],
"8": [
@ -656,7 +913,8 @@
"text": "Vous pouvez utiliser 1 point d'action pour vous concentrer et viser, vous permettant de gagner un bonus de +2 pour votre prochaine attaque avec une [[1. Règles/99. Annexes/4. Équipement#Les armes à projectiles|arme à projectiles]], une [[1. Règles/99. Annexes/4. Équipement#Les armes de jet|arme de jet]] ou avec un [[1. Règles/4. La magie/1. Magie#Les sorts de précision|sort de précision]].",
"disposable": false
}
]
],
"features": []
},
{
"description": [
@ -664,7 +922,8 @@
"text": "Utiliser une [[1. Règles/99. Annexes/4. Équipement#Les armes à projectiles|arme à projectiles]] ou un [[1. Règles/4. La magie/1. Magie#Les sorts de précision|sort de précision]] au corps à corps ne provoque plus de [[1. Règles/1. Introduction/2. Glossaire#Avantage et désavantage|désavantage]].",
"disposable": false
}
]
],
"features": []
},
{
"description": [
@ -672,7 +931,8 @@
"text": "En frappant un adversaire en étant [[1. Règles/99. Annexes/6. Visibilité et lumière#Caché|caché]], vous pouvez subir un malus de -5 et déclencher un [[1. Règles/3. Le combat/1. Combat#Réussite critique|dégât critique]] si vous touchez.",
"disposable": false
}
]
],
"features": []
}
],
"9": [
@ -682,7 +942,8 @@
"text": "",
"disposable": false
}
]
],
"features": []
},
{
"description": [
@ -691,7 +952,8 @@
"disposable": true
}
],
"resistance": [["precision", "attack"]]
"resistance": [["precision", "attack"]],
"features": []
},
{
"description": [
@ -699,7 +961,8 @@
"text": "Vous progressez dans l'arbre des [[1. Règles/99. Annexes/1. Les évolutions de valeur.canvas#Les armes multiples|armes multiples]].",
"disposable": true
}
]
],
"features": []
}
],
"10": [
@ -715,7 +978,8 @@
}
],
"defense": ["activedodge"],
"initiative": 1
"initiative": 1,
"features": []
},
{
"description": [
@ -723,7 +987,8 @@
"text": "En attaquant avec une [[1. Règles/99. Annexes/4. Équipement#Les armes à projectiles|arme à projectiles]], vous pouvez choisir de subir un malus de -4 pour infliger 8 points de dégâts supplémentaires. *A annoncer avant le lancer de dé.*",
"disposable": false
}
]
],
"features": []
},
{
"description": [
@ -732,7 +997,8 @@
"disposable": true
}
],
"resistance": [["bleed", "attack"]]
"resistance": [["bleed", "attack"]],
"features": []
}
],
"11": [
@ -748,7 +1014,8 @@
}
],
"defense": ["passivedodge"],
"resistance": [["precision", "defense"]]
"resistance": [["precision", "defense"]],
"features": []
},
{
"description": [
@ -760,7 +1027,8 @@
"text": "vous pouvez tirer 2 projectiles en une attaque. Applique les règles de l'.",
"disposable": false
}
]
],
"features": []
},
{
"description": [
@ -768,7 +1036,8 @@
"text": "Votre malus d'attaque avec des armes multiples est réduit de 1.",
"disposable": false
}
]
],
"features": []
}
],
"12": [
@ -778,7 +1047,8 @@
"text": "Vous êtes capable d'esquiver passivement même lorsque vous êtes [[1. Règles/99. Annexes/2. Liste des effets#Agrippé|agrippé]]. Vous êtes capable d'esquiver activement sans vous déplacer.",
"disposable": false
}
]
],
"features": []
},
{
"description": [
@ -787,7 +1057,8 @@
"disposable": true
}
],
"resistance": [["precision", "attack"]]
"resistance": [["precision", "attack"]],
"features": []
},
{
"description": [
@ -795,7 +1066,8 @@
"text": "",
"disposable": false
}
]
],
"features": []
}
],
"13": [
@ -806,7 +1078,8 @@
"disposable": false,
"category": "reaction"
}
]
],
"features": []
},
{
"description": [
@ -814,7 +1087,8 @@
"text": "",
"disposable": false
}
]
],
"features": []
},
{
"description": [
@ -822,7 +1096,8 @@
"text": "Lorsque vous parvenez à parer activement avec une [[1. Règles/99. Annexes/4. Équipement#Les armes légères|arme légère]], vous pouvez gratuitement attaquer **une seule fois** avec cette même arme.",
"disposable": false
}
]
],
"features": []
}
],
"14": [
@ -833,7 +1108,8 @@
"disposable": true
}
],
"defense": ["activedodge"]
"defense": ["activedodge"],
"features": []
},
{
"description": [
@ -841,7 +1117,8 @@
"text": "",
"disposable": false
}
]
],
"features": []
},
{
"description": [
@ -849,7 +1126,8 @@
"text": "Vous progressez dans l'arbre des [[1. Règles/99. Annexes/1. Les évolutions de valeur.canvas#Les armes multiples|armes multiples]].",
"disposable": true
}
]
],
"features": []
}
],
"15": [
@ -859,7 +1137,8 @@
"text": "Vous ne pouvez plus esquiver activement. Tant que vous portez au mieux une [[1. Règles/99. Annexes/4. Équipement#Les armures légères|armure légère]], votre esquive passive est égale à votre esquive active.",
"disposable": false
}
]
],
"features": []
},
{
"description": [
@ -868,7 +1147,8 @@
"disposable": false,
"category": "action"
}
]
],
"features": []
},
{
"description": [
@ -876,7 +1156,8 @@
"text": "Au prix d'un point de [[1. Règles/99. Annexes/3. Fatigue et repos#Fatigue temporaire|fatigue temporaire]], votre malus d'attaque avec des armes multiples est réduit de 1 jusqu'à la fin de votre tour.",
"disposable": false
}
]
],
"features": []
}
]
},
@ -895,7 +1176,8 @@
"disposable": false,
"replaced": true
}
]
],
"features": []
}
],
"1": [
@ -907,7 +1189,8 @@
"disposable": false,
"replaced": true
}
]
],
"features": []
}
],
"2": [
@ -923,7 +1206,8 @@
"text": "+3 PV max.",
"disposable": true
}
]
],
"features": []
}
],
"3": [
@ -939,7 +1223,8 @@
"text": "+2 PV max.",
"disposable": true
}
]
],
"features": []
}
],
"4": [
@ -950,7 +1235,8 @@
"text": "+6 PV max.",
"disposable": true
}
]
],
"features": []
}
],
"5": [
@ -1594,6 +1880,14 @@
"text": "Vous apprenez le sort unique [[1. Règles/4. La magie/2. Liste des sorts#^484fc3|Dévastation]].",
"disposable": false
}
],
"features": [
{
"category": "asset",
"type": "add",
"kind": "spells",
"asset": "special-1"
}
]
}
],
@ -1652,7 +1946,7 @@
{
"description": [
{
"text": "Vous apprenez le sort unique [[1. Règles/4. La magie/2. Liste des sorts#^73b8bd|Focalisation destructrice]].",
"text": "Vous apprenez le sort unique [[1. Règles/4. La magie/2. Liste des sorts#^73b8bd|Soin]].",
"disposable": false
}
]
@ -3596,6 +3890,23 @@
"Dégats"
],
"id": "48"
},
{
"name": "Dévastation elementaire",
"rank": 4,
"type": "precision",
"cost": 8,
"speed": "action",
"elements": [
"fire",
"ice",
"thunder"
],
"effect": "Faites un jet d'attaque soit la [[1. Entrainement#Dextérité|dextérité]], soit l'[[1. Entrainement#L'intelligence|intelligence]], soit la [[1. Entrainement#La psyché|psyché]]. Inflige 10+3d10 dégâts. Si vous attaquez avec la dextérité, vous infligez des dégâts de feu. Si vous attaquez avec l'intelligence, vous infligez des dégâts de glace et si vous attaquez avec la psyché, vous faites des dégâts de foudre.",
"tags": [
"Dégats"
],
"id": "special-1"
}
]
}

View File

@ -38,18 +38,18 @@ export const mainStatTexts: Record<MainStat, string> = {
"curiosity": "Curiosité",
"charisma": "Charisme",
"psyche": "Psyché",
}
};
export const elementTexts: Record<SpellElement, { class: string, text: string }> = {
fire: { class: 'text-light-red dark:text-dark-red', text: 'Feu' },
ice: { class: 'text-light-blue dark:text-dark-blue', text: 'Glace' },
thunder: { class: 'text-light-yellow dark:text-dark-yellow', text: 'Foudre' },
earth: { class: 'text-light-orange dark:text-dark-orange', text: 'Terre' },
arcana: { class: 'text-light-purple dark:text-dark-purple', text: 'Arcane' },
air: { class: 'text-light-green dark:text-dark-green', text: 'Air' },
nature: { class: 'text-light-green dark:text-dark-green', text: 'Nature' },
light: { class: 'text-light-yellow dark:text-dark-yellow', text: 'Lumière' },
psyche: { class: 'text-light-purple dark:text-dark-purple', text: 'Psy' },
}
fire: { class: 'text-light-red dark:text-dark-red border-light-red dark:border-dark-red bg-light-red dark:bg-dark-red', text: 'Feu' },
ice: { class: 'text-light-blue dark:text-dark-blue border-light-blue dark:border-dark-blue bg-light-blue dark:bg-dark-blue', text: 'Glace' },
thunder: { class: 'text-light-yellow dark:text-dark-yellow border-light-yellow dark:border-dark-yellow bg-light-yellow dark:bg-dark-yellow', text: 'Foudre' },
earth: { class: 'text-light-orange dark:text-dark-orange border-light-orange dark:border-dark-orange bg-light-orange dark:bg-dark-orange', text: 'Terre' },
arcana: { class: 'text-light-indigo dark:text-dark-indigo border-light-indigo dark:border-dark-indigo bg-light-indigo dark:bg-dark-indigo', text: 'Arcane' },
air: { class: 'text-light-lime dark:text-dark-lime border-light-lime dark:border-dark-lime bg-light-lime dark:bg-dark-lime', text: 'Air' },
nature: { class: 'text-light-green dark:text-dark-green border-light-green dark:border-dark-green bg-light-green dark:bg-dark-green', text: 'Nature' },
light: { class: 'text-light-yellow dark:text-dark-yellow border-light-yellow dark:border-dark-yellow bg-light-yellow dark:bg-dark-yellow', text: 'Lumière' },
psyche: { class: 'text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-purple bg-light-purple dark:bg-dark-purple', text: 'Psy' },
};
export const spellTypeTexts: Record<SpellType, string> = { "instinct": "Instinct", "knowledge": "Savoir", "precision": "Précision", "arts": "Oeuvres" };
export const CharacterValidation = z.object({
@ -79,7 +79,7 @@ export const CharacterValidation = z.object({
username: z.string().optional(),
visibility: z.enum(["public", "private"]),
thumbnail: z.any(),
})
});
export type Character = {
id: number;
@ -115,7 +115,7 @@ export type CharacterConfig = {
export type SpellConfig = {
id: string;
name: string;
rank: 1 | 2 | 3;
rank: 1 | 2 | 3 | 4;
type: SpellType;
cost: number;
speed: "action" | "reaction" | number;
@ -149,31 +149,29 @@ export type RaceOption = {
abilities?: number;
spellslots?: number;
};
export type Feature = {
text?: string;
} & (ActionFeature | ReactionFeature | FreeActionFeature | BonusFeature | MiscFeature);
type ActionFeature = {
type: "action";
export type FeatureItem = {
category: "freeaction" | "misc";
text: string;
} | {
category: "action" | "reaction";
cost: 1 | 2 | 3;
text: string;
};
type ReactionFeature = {
type: "reaction";
text: string;
};
type FreeActionFeature = {
type: "freeaction";
text: string;
};
type BonusFeature = {
type: "bonus";
action: "add" | "remove" | "set" | "cap";
value: number;
} | {
category: "value";
type: "add" | "remove" | "set";
value: number | false;
property: string;
};
type MiscFeature = {
type: "misc";
text: string;
} | {
category: "asset";
type: "add" | "remove";
kind: "spells";
asset: string;
}
export type Feature = {
id: number;
name?: string
text?: string;
list?: FeatureItem[];
};
export type TrainingOption = {
description: Array<{
@ -201,7 +199,7 @@ export type TrainingOption = {
spellslot?: number | MainStat;
arts?: number | MainStat;
features?: Feature[]; //TODO
features?: FeatureItem[]; //TODO
};
export type CompiledCharacter = {
id: number;
@ -222,6 +220,7 @@ export type CompiledCharacter = {
values: CharacterValues,
defense: {
hardcap: number;
static: number;
activeparry: number;
activedodge: number;
@ -238,6 +237,7 @@ export type CompiledCharacter = {
magicpower: number;
magicspeed: number;
magicelement: number;
magicinstinct: number;
};
//First is attack, second is defense