Character creation implementation. People and training ready, still need to work on abilities and spells

This commit is contained in:
Peaceultime 2025-04-21 21:53:15 +02:00
parent 7beeed8a61
commit f599b561af
30 changed files with 7538 additions and 7 deletions

View File

@ -39,4 +39,8 @@ const { list } = useToast();
@apply bg-light-50; @apply bg-light-50;
@apply dark:bg-dark-50; @apply dark:bg-dark-50;
} }
::-webkit-scrollbar-corner {
@apply bg-transparent;
}
</style> </style>

View File

@ -1,7 +1,8 @@
<template> <template>
<CollapsibleRoot v-model:open="model" :disabled="disabled" :defaultOpen="defaultOpen"> <CollapsibleRoot v-model:open="model" :disabled="disabled" :defaultOpen="defaultOpen">
<slot name="alwaysVisible"></slot>
<div class="flex flex-row justify-center items-center"> <div class="flex flex-row justify-center items-center">
<span v-if="!!label">{{ label }}</span> <span>{{ label }}<slot name="label"></slot></span>
<CollapsibleTrigger class="ms-4" asChild> <CollapsibleTrigger class="ms-4" asChild>
<Button icon :disabled="disabled"> <Button icon :disabled="disabled">
<Icon v-if="model" icon="radix-icons:cross-2" class="h-4 w-4" /> <Icon v-if="model" icon="radix-icons:cross-2" class="h-4 w-4" />
@ -9,7 +10,6 @@
</Button> </Button>
</CollapsibleTrigger> </CollapsibleTrigger>
</div> </div>
<slot name="alwaysVisible"></slot>
<CollapsibleContent class="overflow-hidden data-[state=closed]:animate-[collapseClose_0.2s_ease-in-out] data-[state=open]:animate-[collapseOpen_0.2s_ease-in-out]"> <CollapsibleContent class="overflow-hidden data-[state=closed]:animate-[collapseClose_0.2s_ease-in-out] data-[state=open]:animate-[collapseOpen_0.2s_ease-in-out]">
<slot></slot> <slot></slot>
</CollapsibleContent> </CollapsibleContent>

View File

@ -111,6 +111,7 @@ if(overview.value && !overview.value.content)
loading.value = false; loading.value = false;
} }
const canvas = computed(() => overview.value && overview.value.content ? overview.value.content : undefined); const canvas = computed(() => overview.value && overview.value.content ? overview.value.content : undefined);
console.log(canvas.value);
const dispX = ref(0), dispY = ref(0), minZoom = ref(0.1), zoom = ref(0.5); const dispX = ref(0), dispY = ref(0), minZoom = ref(0.1), zoom = ref(0.5);
const canvasRef = useTemplateRef('canvasRef'), transformRef = useTemplateRef('transformRef'); const canvasRef = useTemplateRef('canvasRef'), transformRef = useTemplateRef('transformRef');

View File

@ -0,0 +1,30 @@
<template>
<span class="text-accent-blue inline-flex items-center" :class="class">
<HoverCard nuxt-client class="max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]" :class="{'overflow-hidden !p-0': overview?.type === 'canvas'}" :disabled="!overview">
<template #content>
<Markdown v-if="overview?.type === 'markdown'" class="!px-6" :path="decodeURIComponent(pathname)" :filter="hash.substring(1)" popover />
<template v-else-if="overview?.type === 'canvas'"><div class="w-[600px] h-[600px] relative"><Canvas :path="decodeURIComponent(pathname)" /></div></template>
</template>
<span>
<slot v-bind="$attrs"></slot>
<Icon class="w-4 h-4 inline-block" v-if="overview && overview.type !== 'markdown'" :icon="iconByType[overview.type]" />
</span>
</HoverCard>
</span>
</template>
<script setup lang="ts">
import { parseURL } from 'ufo';
import { Icon } from '@iconify/vue/dist/iconify.js';
import { iconByType } from '#shared/general.util';
const { href } = defineProps<{
href: string
class?: string
}>();
const { hash, pathname } = parseURL(href);
const { content } = useContent();
const overview = computed(() => content.value.find(e => e.path === decodeURIComponent(pathname)));
</script>

View File

@ -1,5 +1,5 @@
<template> <template>
<NuxtLink class="text-accent-blue inline-flex items-center" :to="overview ? { name: 'explore-path', params: { path: overview.path }, hash: hash } : href" :class="class"> <NuxtLink class="text-accent-blue inline-flex items-center" :to="overview ? { name: 'explore-path', params: { path: overview.path }, hash: decodeURIComponent(hash) } : href" :class="class">
<HoverCard nuxt-client class="max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]" :class="{'overflow-hidden !p-0': overview?.type === 'canvas'}" :disabled="!overview"> <HoverCard nuxt-client class="max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]" :class="{'overflow-hidden !p-0': overview?.type === 'canvas'}" :disabled="!overview">
<template #content> <template #content>
<Markdown v-if="overview?.type === 'markdown'" class="!px-6" :path="decodeURIComponent(pathname)" :filter="hash.substring(1)" popover /> <Markdown v-if="overview?.type === 'markdown'" class="!px-6" :path="decodeURIComponent(pathname)" :filter="hash.substring(1)" popover />

BIN
db.sqlite

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -53,6 +53,14 @@ export const emailValidationTable = sqliteTable("email_validation", {
timestamp: int({ mode: 'timestamp' }).notNull(), timestamp: int({ mode: 'timestamp' }).notNull(),
}) })
export const characterTable = sqliteTable("character", {
id: int().primaryKey({ autoIncrement: true }),
name: text().notNull(),
owner: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
progress: text({ mode: 'json' }).notNull(),
thumbnail: blob(),
})
export const usersRelation = relations(usersTable, ({ one, many }) => ({ export const usersRelation = relations(usersTable, ({ one, many }) => ({
data: one(usersDataTable, { fields: [usersTable.id], references: [usersDataTable.id], }), data: one(usersDataTable, { fields: [usersTable.id], references: [usersDataTable.id], }),
session: many(userSessionsTable), session: many(userSessionsTable),
@ -71,3 +79,6 @@ export const userPermissionsRelation = relations(userPermissionsTable, ({ one })
export const explorerContentRelation = relations(explorerContentTable, ({ one }) => ({ export const explorerContentRelation = relations(explorerContentTable, ({ one }) => ({
users: one(usersTable, { fields: [explorerContentTable.owner], references: [usersTable.id], }), users: one(usersTable, { fields: [explorerContentTable.owner], references: [usersTable.id], }),
})); }));
export const characterRelation = relations(characterTable, ({ one }) => ({
users: one(usersTable, { fields: [characterTable.owner], references: [usersTable.id], }),
}));

View File

@ -0,0 +1,7 @@
CREATE TABLE `character` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`owner` integer NOT NULL,
`options` text NOT NULL,
FOREIGN KEY (`owner`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
);

View File

@ -0,0 +1,14 @@
PRAGMA foreign_keys=OFF;--> statement-breakpoint
CREATE TABLE `__new_character` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL,
`owner` integer NOT NULL,
`progress` text NOT NULL,
`thumbnail` blob,
FOREIGN KEY (`owner`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
);
--> statement-breakpoint
INSERT INTO `__new_character`("id", "name", "owner", "progress", "thumbnail") SELECT "id", "name", "owner", "progress", "thumbnail" FROM `character`;--> statement-breakpoint
DROP TABLE `character`;--> statement-breakpoint
ALTER TABLE `__new_character` RENAME TO `character`;--> statement-breakpoint
PRAGMA foreign_keys=ON;

View File

@ -0,0 +1,411 @@
{
"version": "6",
"dialect": "sqlite",
"id": "4e31a794-f0ae-4c44-a846-6e1bafa4b247",
"prevId": "a2731c1f-4150-4423-946e-670d794f8961",
"tables": {
"character": {
"name": "character",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"options": {
"name": "options",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_owner_users_id_fk": {
"name": "character_owner_users_id_fk",
"tableFrom": "character",
"tableTo": "users",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"email_validation": {
"name": "email_validation",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"explorer_content": {
"name": "explorer_content",
"columns": {
"path": {
"name": "path",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"content": {
"name": "content",
"type": "blob",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"navigable": {
"name": "navigable",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"private": {
"name": "private",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"order": {
"name": "order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"visit": {
"name": "visit",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"explorer_content_owner_users_id_fk": {
"name": "explorer_content_owner_users_id_fk",
"tableFrom": "explorer_content",
"tableTo": "users",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_permissions": {
"name": "user_permissions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"permission": {
"name": "permission",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_permissions_id_users_id_fk": {
"name": "user_permissions_id_users_id_fk",
"tableFrom": "user_permissions",
"tableTo": "users",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"user_permissions_id_permission_pk": {
"columns": [
"id",
"permission"
],
"name": "user_permissions_id_permission_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_sessions": {
"name": "user_sessions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_sessions_user_id_users_id_fk": {
"name": "user_sessions_user_id_users_id_fk",
"tableFrom": "user_sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"user_sessions_id_user_id_pk": {
"columns": [
"id",
"user_id"
],
"name": "user_sessions_id_user_id_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users_data": {
"name": "users_data",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"signin": {
"name": "signin",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"lastTimestamp": {
"name": "lastTimestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"logCount": {
"name": "logCount",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {},
"foreignKeys": {
"users_data_id_users_id_fk": {
"name": "users_data_id_users_id_fk",
"tableFrom": "users_data",
"tableTo": "users",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"hash": {
"name": "hash",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"state": {
"name": "state",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {
"users_username_unique": {
"name": "users_username_unique",
"columns": [
"username"
],
"isUnique": true
},
"users_email_unique": {
"name": "users_email_unique",
"columns": [
"email"
],
"isUnique": true
},
"users_hash_unique": {
"name": "users_hash_unique",
"columns": [
"hash"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@ -0,0 +1,420 @@
{
"version": "6",
"dialect": "sqlite",
"id": "15ea15e0-3d44-4dff-a4cd-f8666c4aa5ed",
"prevId": "4e31a794-f0ae-4c44-a846-6e1bafa4b247",
"tables": {
"character": {
"name": "character",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"progress": {
"name": "progress",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"thumbnail": {
"name": "thumbnail",
"type": "blob",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_owner_users_id_fk": {
"name": "character_owner_users_id_fk",
"tableFrom": "character",
"tableTo": "users",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"email_validation": {
"name": "email_validation",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"explorer_content": {
"name": "explorer_content",
"columns": {
"path": {
"name": "path",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"content": {
"name": "content",
"type": "blob",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"navigable": {
"name": "navigable",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"private": {
"name": "private",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"order": {
"name": "order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"visit": {
"name": "visit",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"explorer_content_owner_users_id_fk": {
"name": "explorer_content_owner_users_id_fk",
"tableFrom": "explorer_content",
"tableTo": "users",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_permissions": {
"name": "user_permissions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"permission": {
"name": "permission",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_permissions_id_users_id_fk": {
"name": "user_permissions_id_users_id_fk",
"tableFrom": "user_permissions",
"tableTo": "users",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"user_permissions_id_permission_pk": {
"columns": [
"id",
"permission"
],
"name": "user_permissions_id_permission_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_sessions": {
"name": "user_sessions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_sessions_user_id_users_id_fk": {
"name": "user_sessions_user_id_users_id_fk",
"tableFrom": "user_sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"user_sessions_id_user_id_pk": {
"columns": [
"id",
"user_id"
],
"name": "user_sessions_id_user_id_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users_data": {
"name": "users_data",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"signin": {
"name": "signin",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"lastTimestamp": {
"name": "lastTimestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"logCount": {
"name": "logCount",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {},
"foreignKeys": {
"users_data_id_users_id_fk": {
"name": "users_data_id_users_id_fk",
"tableFrom": "users_data",
"tableTo": "users",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"hash": {
"name": "hash",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"state": {
"name": "state",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {
"users_username_unique": {
"name": "users_username_unique",
"columns": [
"username"
],
"isUnique": true
},
"users_email_unique": {
"name": "users_email_unique",
"columns": [
"email"
],
"isUnique": true
},
"users_hash_unique": {
"name": "users_hash_unique",
"columns": [
"hash"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {
"\"character\".\"options\"": "\"character\".\"progress\""
}
},
"internal": {
"indexes": {}
}
}

View File

@ -43,6 +43,20 @@
"when": 1734426608563, "when": 1734426608563,
"tag": "0005_panoramic_slayback", "tag": "0005_panoramic_slayback",
"breakpoints": true "breakpoints": true
},
{
"idx": 6,
"version": "6",
"when": 1745072860245,
"tag": "0006_clever_marvex",
"breakpoints": true
},
{
"idx": 7,
"version": "6",
"when": 1745074613379,
"tag": "0007_tearful_true_believers",
"breakpoints": true
} }
] ]
} }

View File

@ -32,11 +32,11 @@
<CollapsibleContent asChild forceMount> <CollapsibleContent asChild forceMount>
<div class="bg-light-0 dark:bg-dark-0 z-40 w-screen md:w-[18rem] border-r border-light-30 dark:border-dark-30 flex flex-col justify-between my-2 max-md:data-[state=closed]:hidden"> <div class="bg-light-0 dark:bg-dark-0 z-40 w-screen md:w-[18rem] border-r border-light-30 dark:border-dark-30 flex flex-col justify-between my-2 max-md:data-[state=closed]:hidden">
<div class="flex-1 px-2 max-w-full max-h-full overflow-y-auto overflow-x-hidden"> <div class="flex-1 px-2 max-w-full max-h-full overflow-y-auto overflow-x-hidden">
<div class="flex flex-row flex-1 justify-between items-center py-4 px-2"> <div v-if="user" class="flex flex-1 py-4 px-2 flex-row flex-1 justify-between items-center">
<NuxtLink :href="{ name: 'explore-path', params: { path: 'index' } }" class="flex flex-1 font-bold text-lg items-center border-light-35 dark:border-dark-35 hover:border-accent-blue" active-class="text-accent-blue border-s-2 !border-accent-blue"> <NuxtLink :href="{ name: 'character' }" class="flex flex-1 font-bold text-lg items-center border-light-35 dark:border-dark-35 hover:border-accent-blue" active-class="text-accent-blue border-s-2 !border-accent-blue">
<span class="pl-3 py-1 flex-1 truncate">Projet</span> <span class="pl-3 py-1 flex-1 truncate">Mes personnages</span>
</NuxtLink> </NuxtLink>
<NuxtLink v-if="user && hasPermissions(user.permissions, ['admin', 'editor'])" :to="{ name: 'explore-edit' }"><Button icon><Icon icon="radix-icons:pencil-2" /></Button></NuxtLink> <NuxtLink v-if="hasPermissions(user.permissions, ['admin', 'editor'])" :to="{ name: 'explore-edit' }"><Button icon><Icon icon="radix-icons:pencil-2" /></Button></NuxtLink>
</div> </div>
<Tree v-if="pages" v-model="pages" :getKey="(item) => item.path" class="ps-4"> <Tree v-if="pages" v-model="pages" :getKey="(item) => item.path" class="ps-4">
<template #default="{ item, isExpanded }"> <template #default="{ item, isExpanded }">

View File

@ -0,0 +1,300 @@
<script lang="ts">
function raceOptionToText(option: RaceOption): string
{
const text = [];
if(option.training) text.push(`+${option.training} point${option.training > 1 ? 's' : ''} de statistique${option.training > 1 ? 's' : ''}.`);
if(option.spec) text.push(`+${option.spec} spécialisation${option.spec > 1 ? 's' : ''}.`);
if(option.shaping) text.push(`+${option.shaping} transformation${option.shaping > 1 ? 's' : ''} par jour.`);
if(option.modifier) text.push(`+${option.modifier} au modifieur de votre choix.`);
if(option.health) text.push(`+${option.health} PV max.`);
if(option.mana) text.push(`+${option.mana} mana max.`);
return text.join('\n');
}
</script>
<script setup lang="ts">
import config from '#shared/character-config.json';
import { Icon } from '@iconify/vue/dist/iconify.js';
import PreviewA from '~/components/prose/PreviewA.vue';
import { clamp } from '~/shared/general.util';
import type { Character, CharacterConfig, Level, MainStat, RaceOption, TrainingLevel } from '~/types/character';
definePageMeta({
guestsGoesTo: '/user/login',
});
let id = useRouter().currentRoute.value.params.id;
const { add } = useToast();
const characterConfig = config as CharacterConfig;
const data = ref<Character>({
id: -1,
name: '',
progress: {
training: {
strength: [[1, 0], [2, 0], [3, 0], [4, 0]],
dexterity: [[1, 0], [2, 0], [3, 0], [4, 0]],
constitution: [[1, 0], [2, 0], [3, 0], [4, 0]],
intelligence: [[1, 0], [2, 0], [3, 0], [4, 0]],
curiosity: [[1, 0], [2, 0], [3, 0], [4, 0]],
charisma: [[1, 0], [2, 0], [3, 0], [4, 0]],
psyche: [[1, 0], [2, 0], [3, 0], [4, 0]],
},
level: 1,
race: {
index: undefined,
progress: [[1, 0]],
},
},
});
const peopleOpen = ref(false), trainingOpen = ref(false), abilityOpen = ref(false), trainingTab = ref(0);
const trainingPoints = computed(() => {
const options = data.value.progress.race.index !== undefined ? characterConfig.peoples[data.value.progress.race.index!].options : undefined;
return options ? data.value.progress.race.progress?.reduce((p, v) => {
return p + (options[v[0]][v[1]].training ?? 0)
}, 0) : 0;
});
const maxTraining = computed(() => Object.entries(data.value.progress.training).reduce((p, v) => { p[v[0]] = v[1].reduce((_p, _v) => Math.max(_p, _v[0]) , 0); return p; }, {} as Record<string, number>));
const trainingSpent = computed(() => Object.values(maxTraining.value).reduce((p, v) => p + v));
if(id !== 'new')
{
const character = await useRequestFetch()(`/api/character/${id}`);
if(!character)
{
throw new Error('Donnée du personnage introuvables');
}
data.value = { name: character.name, progress: character.progress } as Character;
}
function selectRaceOption(level: Level, choice: number)
{
const character = data.value;
if(level > character.progress.level)
return;
if(character.progress.race.progress === undefined)
character.progress.race.progress = [];
for(let i = 1; i < level; i++) //Check previous levels as a requirement
{
if(!character.progress.race.progress.some(e => e[0] == i))
return;
}
if(character.progress.race.progress.some(e => e[0] === level))
{
character.progress.race.progress.splice(character.progress.race.progress.findIndex(e => e[0] === level), 1, [level, choice]);
}
else
{
character.progress.race.progress.push([level, choice]);
}
data.value = character;
}
function switchTrainingOption(stat: MainStat, level: TrainingLevel, choice: number)
{
const character = data.value;
if(level == 0)
return;
for(let i = 1; i < level; i++) //Check previous levels as a requirement
{
if(!character.progress.training[stat].some(e => e[0] == i))
return;
}
if(character.progress.training[stat].some(e => e[0] === level))
{
if(character.progress.training[stat].some(e => e[0] === level && e[1] === choice))
{
for(let i = 15; i >= level; i --) //Invalidate higher levels
{
const index = character.progress.training[stat].findIndex(e => e[0] == i);
if(index !== -1)
character.progress.training[stat].splice(index, 1);
}
}
else
character.progress.training[stat].splice(character.progress.training[stat].findIndex(e => e[0] === level), 1, [level, choice]);
}
else if(trainingPoints.value && trainingPoints.value > 0)
{
character.progress.training[stat].push([level, choice]);
}
data.value = character;
}
function updateLevel()
{
const character = data.value;
if(character.progress.race.progress) //Invalidate higher levels
{
for(let level = 20; level > character.progress.level; level--)
{
const index = character.progress.race.progress.findIndex(e => e[0] == level);
if(index !== -1)
character.progress.race.progress.splice(index, 1);
}
}
data.value = character;
}
async function save(leave: boolean)
{
if(id === 'new')
{
id = await useRequestFetch()(`/api/character`, {
method: 'post',
body: data.value,
onResponseError: (e) => {
add({ title: 'Erreur d\enregistrement', content: e.response.status === 401 ? "Vous n'êtes pas autorisé à effectué cette opération" : e.response.statusText, type: 'error', duration: 25000, timer: true });
}
});
add({ content: 'Personnage créé', type: 'success', duration: 25000, timer: true });
useRouter().replace({ name: 'character-id-edit', params: { id: id } })
if(leave) useRouter().push({ name: 'character-id', params: { id: id } });
}
else
{
await useRequestFetch()(`/api/character/${id}`, {
method: 'post',
body: data.value,
onResponseError: (e) => {
add({ title: 'Erreur d\enregistrement', content: e.response.status === 401 ? "Vous n'êtes pas autorisé à effectué cette opération" : e.response.statusText, type: 'error', duration: 25000, timer: true });
}
});
add({ content: 'Personnage enregistré', type: 'success', duration: 25000, timer: true });
if(leave) useRouter().push({ name: 'character-id', params: { id: id } });
}
}
useShortcuts({
"Meta_S": () =>save(false),
})
</script>
<template>
<Head>
<Title>d[any] - Edition de {{ data.name || 'nouveau personnage' }}</Title>
</Head>
<div class="flex flex-col gap-8 align-center">
<div class="flex flex-row gap-4 align-center justify-between">
<div></div>
<div class="flex flex-row gap-4 align-center justify-center">
<Tooltip side="left" message="Developpement en cours"><Avatar src="" icon="radix-icons:person" size="large" /></Tooltip>
<Label class="flex items-start justify-between flex-col gap-2">
<span class="pb-1 mx-2 md:p-0">Nom du personnage</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="data.name">
</Label>
<Label class="flex items-start justify-between flex-col gap-2">
<span class="pb-1 mx-2 md:p-0">Niveau</span>
<NumberFieldRoot :min="1" :max="20" v-model="data.progress.level" @update:model-value="updateLevel" class="flex justify-center border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20
data-[disabled]:text-light-70 dark:data-[disabled]:text-dark-70 hover:border-light-50 dark:hover:border-dark-50 has-[:focus]:shadow-raw transition-[box-shadow] has-[:focus]:shadow-light-40 dark:has-[:focus]:shadow-dark-40">
<NumberFieldInput class="tabular-nums w-20 bg-transparent px-3 py-1 outline-none caret-light-50 dark:caret-dark-50" />
</NumberFieldRoot>
</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]">
<Collapsible class="border-b border-light-30 dark:border-dark-30 p-1" v-model="peopleOpen" @update:model-value="() => { trainingOpen = false; abilityOpen = false; }">
<template #label>
<span class="font-bold text-xl">Peuple</span>
</template>
<template #default>
<div class="m-2 overflow-auto">
<Select label="Peuple de votre personnage" :v-model="data.progress.race.index" @update:model-value="(index) => { data.progress.race.index = parseInt(index ?? '-1'); data.progress.race.progress = [[1, 0]]}">
<SelectItem v-for="(people, index) of characterConfig.peoples" :label="people.name" :value="index" :key="index" />
</Select>
<template v-if="data.progress.race.index !== undefined">
<div class="w-full border-b border-light-30 dark:border-dark-30 pb-4">
<span class="text-sm text-light-70 dark:text-dark-70">{{ characterConfig.peoples[data.progress.race.index].description }}</span>
</div>
<div class="flex flex-col gap-4 max-h-[50vh] pe-4 relative">
<span class="sticky top-0 py-1 bg-light-0 dark:bg-dark-0 z-10 text-xl">Niveaux restants: {{ data.progress.level - (data.progress.race.progress?.length ?? 0) }}</span>
<div class="flex flex-row gap-4 justify-center" v-for="(level, index) of characterConfig.peoples[data.progress.race.index].options" :class="{ 'opacity-30': index > data.progress.level }">
<div class="border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-64" v-for="(option, i) of level" @click="selectRaceOption(index as Level, i)" :class="{ 'hover:border-light-60 dark:hover:border-dark-60': index <= data.progress.level, '!border-accent-blue bg-accent-blue bg-opacity-20': data.progress.race.progress?.some(e => e[0] == index && e[1] === i) ?? false }"><MarkdownRenderer :content="raceOptionToText(option)" /></div>
</div>
</div>
</template>
</div>
</template>
</Collapsible>
<Collapsible class="border-b border-light-30 dark:border-dark-30 p-1" v-model="trainingOpen" :disabled="data.progress.race.index === undefined" @update:model-value="() => { peopleOpen = false; abilityOpen = false; }">
<template #label>
<span class="font-bold text-xl">Entrainement</span>
</template>
<template #default>
<div class="flex flex-col gap-4 max-h-[50vh] pe-4 relative overflow-y-auto overflow-x-hidden">
<div class="sticky top-0 py-2 bg-light-0 dark:bg-dark-0 z-10 flex justify-between">
<Icon icon="radix-icons:caret-left" class="w-6 h-6 border border-light-30 dark:border-dark-30 cursor-pointer" @click="() => trainingTab = clamp(trainingTab - 1, 0, 6)" />
<span class="text-xl" :class="{ 'text-light-red dark:text-dark-red': (trainingPoints ?? 0) < trainingSpent }">Points d'entrainement restants: {{ (trainingPoints ?? 0) - trainingSpent }}</span>
<Icon icon="radix-icons:caret-right" class="w-6 h-6 border border-light-30 dark:border-dark-30 cursor-pointer" @click="() => trainingTab = clamp(trainingTab + 1, 0, 6)" />
</div>
<div class="flex gap-4 relative" :style="`left: calc(calc(-100% - 1em) * ${trainingTab}); transition: left .5s ease;`">
<div class="flex w-full flex-shrink-0 flex-col gap-2 relative">
<div class="sticky top-1 left-16 py-1 px-3 bg-light-0 dark:bg-dark-0 z-10 text-xl font-bold self-start border border-light-30 dark:border-dark-30">Force<span v-if="maxTraining.strength >= 0">: Niveau {{ maxTraining.strength }} (+{{ Math.floor(maxTraining.strength / 3) }})</span></div>
<div class="flex flex-row gap-4 justify-center" v-for="(level, index) of characterConfig.training.strength" :class="{ 'opacity-30': index > maxTraining.strength + 1 }">
<div class="border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-1/3" v-for="(option, i) of level" @click="switchTrainingOption('strength', index as TrainingLevel, i)" :class="{ 'hover:border-light-60 dark:hover:border-dark-60': index <= maxTraining.strength + 1, '!border-accent-blue bg-accent-blue bg-opacity-20': index == 0 || (data.progress.training.strength?.some(e => e[0] == index && e[1] === i) ?? false) }"><MarkdownRenderer :proses="{ 'a': PreviewA }" :content="option.description.map(e => e.text).join('\n')" /></div>
</div>
</div>
<div class="flex w-full flex-shrink-0 flex-col gap-2 relative">
<div class="sticky top-1 left-16 py-1 px-3 bg-light-0 dark:bg-dark-0 z-10 text-xl font-bold self-start border border-light-30 dark:border-dark-30">Dextérité<span v-if="maxTraining.dexterity >= 0">: Niveau {{ maxTraining.dexterity }} (+{{ Math.floor(maxTraining.dexterity / 3) }})</span></div>
<div class="flex flex-row gap-4 justify-center" v-for="(level, index) of characterConfig.training.dexterity" :class="{ 'opacity-30': index > maxTraining.dexterity + 1 }">
<div class="border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-1/3" v-for="(option, i) of level" @click="switchTrainingOption('dexterity', index as TrainingLevel, i)" :class="{ 'hover:border-light-60 dark:hover:border-dark-60': index <= maxTraining.dexterity + 1, '!border-accent-blue bg-accent-blue bg-opacity-20': index == 0 || (data.progress.training.dexterity?.some(e => e[0] == index && e[1] === i) ?? false) }"><MarkdownRenderer :proses="{ 'a': PreviewA }" :content="option.description.map(e => e.text).join('\n')" /></div>
</div>
</div>
<div class="flex w-full flex-shrink-0 flex-col gap-2 relative">
<div class="sticky top-1 left-16 py-1 px-3 bg-light-0 dark:bg-dark-0 z-10 text-xl font-bold self-start border border-light-30 dark:border-dark-30">Constitution<span v-if="maxTraining.constitution >= 0">: Niveau {{ maxTraining.constitution }} (+{{ Math.floor(maxTraining.constitution / 3) }})</span></div>
<div class="flex flex-row gap-4 justify-center" v-for="(level, index) of characterConfig.training.constitution" :class="{ 'opacity-30': index > maxTraining.constitution + 1 }">
<div class="border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-1/3" v-for="(option, i) of level" @click="switchTrainingOption('constitution', index as TrainingLevel, i)" :class="{ 'hover:border-light-60 dark:hover:border-dark-60': index <= maxTraining.constitution + 1, '!border-accent-blue bg-accent-blue bg-opacity-20': index == 0 || (data.progress.training.constitution?.some(e => e[0] == index && e[1] === i) ?? false) }"><MarkdownRenderer :proses="{ 'a': PreviewA }" :content="option.description.map(e => e.text).join('\n')" /></div>
</div>
</div>
<div class="flex w-full flex-shrink-0 flex-col gap-2 relative">
<div class="sticky top-1 left-16 py-1 px-3 bg-light-0 dark:bg-dark-0 z-10 text-xl font-bold self-start border border-light-30 dark:border-dark-30">Intelligence<span v-if="maxTraining.intelligence >= 0">: Niveau {{ maxTraining.intelligence }} (+{{ Math.floor(maxTraining.intelligence / 3) }})</span></div>
<div class="flex flex-row gap-4 justify-center" v-for="(level, index) of characterConfig.training.intelligence" :class="{ 'opacity-30': index > maxTraining.intelligence + 1 }">
<div class="border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-1/3" v-for="(option, i) of level" @click="switchTrainingOption('intelligence', index as TrainingLevel, i)" :class="{ 'hover:border-light-60 dark:hover:border-dark-60': index <= maxTraining.intelligence + 1, '!border-accent-blue bg-accent-blue bg-opacity-20': index == 0 || (data.progress.training.intelligence?.some(e => e[0] == index && e[1] === i) ?? false) }"><MarkdownRenderer :proses="{ 'a': PreviewA }" :content="option.description.map(e => e.text).join('\n')" /></div>
</div>
</div>
<div class="flex w-full flex-shrink-0 flex-col gap-2 relative">
<div class="sticky top-1 left-16 py-1 px-3 bg-light-0 dark:bg-dark-0 z-10 text-xl font-bold self-start border border-light-30 dark:border-dark-30">Curiosité<span v-if="maxTraining.curiosity >= 0">: Niveau {{ maxTraining.curiosity }} (+{{ Math.floor(maxTraining.curiosity / 3) }})</span></div>
<div class="flex flex-row gap-4 justify-center" v-for="(level, index) of characterConfig.training.curiosity" :class="{ 'opacity-30': index > maxTraining.curiosity + 1 }">
<div class="border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-1/3" v-for="(option, i) of level" @click="switchTrainingOption('curiosity', index as TrainingLevel, i)" :class="{ 'hover:border-light-60 dark:hover:border-dark-60': index <= maxTraining.curiosity + 1, '!border-accent-blue bg-accent-blue bg-opacity-20': index == 0 || (data.progress.training.curiosity?.some(e => e[0] == index && e[1] === i) ?? false) }"><MarkdownRenderer :proses="{ 'a': PreviewA }" :content="option.description.map(e => e.text).join('\n')" /></div>
</div>
</div>
<div class="flex w-full flex-shrink-0 flex-col gap-2 relative">
<div class="sticky top-1 left-16 py-1 px-3 bg-light-0 dark:bg-dark-0 z-10 text-xl font-bold self-start border border-light-30 dark:border-dark-30">Charisme<span v-if="maxTraining.charisma >= 0">: Niveau {{ maxTraining.charisma }} (+{{ Math.floor(maxTraining.charisma / 3) }})</span></div>
<div class="flex flex-row gap-4 justify-center" v-for="(level, index) of characterConfig.training.charisma" :class="{ 'opacity-30': index > maxTraining.charisma + 1 }">
<div class="border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-1/3" v-for="(option, i) of level" @click="switchTrainingOption('charisma', index as TrainingLevel, i)" :class="{ 'hover:border-light-60 dark:hover:border-dark-60': index <= maxTraining.charisma + 1, '!border-accent-blue bg-accent-blue bg-opacity-20': index == 0 || (data.progress.training.charisma?.some(e => e[0] == index && e[1] === i) ?? false) }"><MarkdownRenderer :proses="{ 'a': PreviewA }" :content="option.description.map(e => e.text).join('\n')" /></div>
</div>
</div>
<div class="flex w-full flex-shrink-0 flex-col gap-2 relative">
<div class="sticky top-1 left-16 py-1 px-3 bg-light-0 dark:bg-dark-0 z-10 text-xl font-bold self-start border border-light-30 dark:border-dark-30">Psyché<span v-if="maxTraining.psyche >= 0">: Niveau {{ maxTraining.psyche }} (+{{ Math.floor(maxTraining.psyche / 3) }})</span></div>
<div class="flex flex-row gap-4 justify-center" v-for="(level, index) of characterConfig.training.psyche" :class="{ 'opacity-30': index > maxTraining.psyche + 1 }">
<div class="border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-1/3" v-for="(option, i) of level" @click="switchTrainingOption('psyche', index as TrainingLevel, i)" :class="{ 'hover:border-light-60 dark:hover:border-dark-60': index <= maxTraining.psyche + 1, '!border-accent-blue bg-accent-blue bg-opacity-20': index == 0 || (data.progress.training.psyche?.some(e => e[0] == index && e[1] === i) ?? false) }"><MarkdownRenderer :proses="{ 'a': PreviewA }" :content="option.description.map(e => e.text).join('\n')" /></div>
</div>
</div>
</div>
</div>
</template>
</Collapsible>
<Collapsible class="border-b border-light-30 dark:border-dark-30 p-1" v-model="abilityOpen" :disabled="data.progress.race.index === undefined" @update:model-value="() => { trainingOpen = false; peopleOpen = false; }">
<template #label>
<span class="font-bold text-xl">Compétences</span>
</template>
<template #default>
</template>
</Collapsible>
</div>
</div>
</template>

View File

@ -0,0 +1,134 @@
<script setup lang="ts">
import config from '#shared/character-config.json';
import { Icon } from '@iconify/vue/dist/iconify.js';
import PreviewA from '~/components/prose/PreviewA.vue';
const id = useRouter().currentRoute.value.params.id;
const { user } = useUserSession();
const { data: character, status, error } = await useAsyncData(() => useRequestFetch()(`/api/character/${id}/compiled`));
</script>
<template>
<div v-if="status === 'pending'">
<Head>
<Title>d[any] - Chargement ...</Title>
</Head>
</div>
<div v-else-if="status === 'success' && character">
<Head>
<Title>d[any] - {{ character.name }}</Title>
</Head>
<div class="flex flex-row gap-4 justify-between">
<div></div>
<div class="flex flex-row gap-6 items-center justify-center">
<Avatar src="" icon="radix-icons:person" size="large" />
<div class="flex flex-col">
<span class="text-xl font-bold">{{ character.name }}</span>
<span class="text-sm">De {{ character.username }}</span>
</div>
<div class="flex flex-col">
<span class="font-bold">Niveau {{ character.level }}</span>
<span>{{ character.race === -1 ? "Race inconnue" : config.peoples[character.race].name }}</span>
</div>
<span class="h-full border-l border-light-30 dark:border-dark-30"></span>
<span>PV: {{ character.health }}</span>
<span>Mana: {{ character.mana }}</span>
</div>
<div class="self-center">
<Tooltip side="right" message="Modifier" v-if="user && user.id === character.owner"><NuxtLink :to="{ name: 'character-id-edit', params: { id: character.id } }"><Button icon><Icon icon="radix-icons:pencil-2" /></Button></NuxtLink></Tooltip>
</div>
</div>
<div class="flex flex-1 flex-col justify-center gap-4 *:py-2">
<div class="flex flex-row gap-4 items-center justify-center border-b border-light-30 dark:border-dark-30">
<div class="flex relative ps-4">
<div class="flex flex-col px-2 items-center"><span class="text-2xl font-bold">+{{ character.modifier.strength }}</span><span>Force</span></div>
<div class="flex flex-col px-2 items-center"><span class="text-2xl font-bold">+{{ character.modifier.dexterity }}</span><span>Dextérité</span></div>
<div class="flex flex-col px-2 items-center"><span class="text-2xl font-bold">+{{ character.modifier.constitution }}</span><span>Constitution</span></div>
<div class="flex flex-col px-2 items-center"><span class="text-2xl font-bold">+{{ character.modifier.intelligence }}</span><span>Intelligence</span></div>
<div class="flex flex-col px-2 items-center"><span class="text-2xl font-bold">+{{ character.modifier.curiosity }}</span><span>Curiosité</span></div>
<div class="flex flex-col px-2 items-center"><span class="text-2xl font-bold">+{{ character.modifier.charisma }}</span><span>Charisme</span></div>
<div class="flex flex-col px-2 items-center"><span class="text-2xl font-bold">+{{ character.modifier.psyche }}</span><span>Psyché</span></div>
</div>
<div class="flex relative border-l border-light-30 dark:border-dark-30 ps-4">
<div class="flex flex-1 flex-row items-center justify-between">
<div class="flex flex-col px-2 items-center"><span class="text-2xl font-bold">+{{ character.initiative }}</span><span>Initiative</span></div>
<div class="flex flex-col px-2 items-center"><span class="text-2xl font-bold">{{ character.speed === false ? "Aucun déplacement" : `${character.speed} cases` }}</span><span>Course</span></div>
</div>
<!-- <div class="absolute top-0 left-0 bottom-0 right-0 bg-light-0 dark:bg-dark-0 bg-opacity-50 dark:bg-opacity-50 text-xl font-bold flex items-center justify-center">Les données secondaires arrivent bientôt.</div> -->
</div>
<div class="flex relative border-l border-light-30 dark:border-dark-30 ps-4">
<div class="flex flex-col px-2">
<span class="text-xl">Défense passive: <span class="text-2xl font-bold">{{ character.defense.static }}</span>/+<span class="text-2xl font-bold">{{ character.defense.passivedodge }}</span>/+<span class="text-2xl font-bold">{{ character.defense.passiveparry }}</span></span>
<span class="text-xl">Défense active: <span class="float-right">+<span class="text-2xl font-bold">{{ character.defense.activedodge }}</span>/+<span class="text-2xl font-bold">{{ character.defense.activeparry }}</span></span></span>
</div>
</div>
</div>
<div class="flex flex-1 px-8">
<div class="flex flex-col pe-8 gap-8 py-8 w-80">
<div class="flex flex-col">
<span class="text-lg font-semibold border-b border-light-30 dark:border-dark-30">Maitrise d'arme</span>
<div class="grid grid-cols-2 gap-x-3 gap-y-1">
<PreviewA v-if="character.mastery.strength + character.mastery.dexterity > 0" href="1. Règles/99. Annexes/4. Équipement#Les armes légères">Arme légère</PreviewA>
<PreviewA v-if="character.mastery.strength + character.mastery.dexterity > 0" href="1. Règles/99. Annexes/4. Équipement#Les armes de jet">Arme de jet</PreviewA>
<PreviewA v-if="character.mastery.strength + character.mastery.dexterity > 0" href="1. Règles/99. Annexes/4. Équipement#Les armes naturelles">Arme naturelle</PreviewA>
<PreviewA v-if="character.mastery.strength > 1" href="1. Règles/99. Annexes/4. Équipement#Les armes">Arme standard</PreviewA>
<PreviewA v-if="character.mastery.strength > 1" href="1. Règles/99. Annexes/4. Équipement#Les armes improvisées">Arme improvisée</PreviewA>
<PreviewA v-if="character.mastery.strength > 2" href="1. Règles/99. Annexes/4. Équipement#Les armes lourdes">Arme lourde</PreviewA>
<PreviewA v-if="character.mastery.strength > 3" href="1. Règles/99. Annexes/4. Équipement#Les armes à deux mains">Arme à deux mains</PreviewA>
<PreviewA v-if="character.mastery.dexterity > 0 && character.mastery.strength > 1" href="1. Règles/99. Annexes/4. Équipement#Les armes maniables">Arme maniable</PreviewA>
<PreviewA v-if="character.mastery.dexterity > 1 && character.mastery.strength > 1" href="1. Règles/99. Annexes/4. Équipement#Les armes à projectiles">Arme à projectiles</PreviewA>
<PreviewA v-if="character.mastery.dexterity > 1 && character.mastery.strength > 2" href="1. Règles/99. Annexes/4. Équipement#Les armes longues">Arme longue</PreviewA>
<PreviewA v-if="character.mastery.shield > 0" href="1. Règles/99. Annexes/4. Équipement#Les boucliers">Bouclier</PreviewA>
<PreviewA v-if="character.mastery.shield > 0 && character.mastery.strength > 3" href="1. Règles/99. Annexes/4. Équipement#Les boucliers à deux mains">Bouclier à deux mains</PreviewA>
</div>
</div>
<div v-if="character.mastery.armor > 0" class="flex flex-col">
<span class="text-lg font-semibold border-b border-light-30 dark:border-dark-30">Maitrise d'armure</span>
<div class="grid grid-cols-2 gap-x-3 gap-y-1">
<PreviewA v-if="character.mastery.armor > 0" href="1. Règles/99. Annexes/4. Équipement#Les armures légères">Armure légère</PreviewA>
<PreviewA v-if="character.mastery.armor > 1" href="1. Règles/99. Annexes/4. Équipement#Les armures">Armure standard</PreviewA>
<PreviewA v-if="character.mastery.armor > 2" href="1. Règles/99. Annexes/4. Équipement#Les armures lourdes">Armure lourde</PreviewA>
</div>
</div>
<div class="flex flex-col">
<span class="text-lg font-semibold border-b border-light-30 dark:border-dark-30">Maitrise de sorts</span>
<span>Sorts de précision: <span class="font-bold">{{ character.spellranks.precision }}</span></span>
<span>Sorts de savoir: <span class="font-bold">{{ character.spellranks.knowledge }}</span></span>
<span>Sorts d'instinct: <span class="font-bold">{{ character.spellranks.instinct }}</span></span>
</div>
</div>
<div class="flex flex-col border-l border-light-30 dark:border-dark-30 ps-8 gap-4 py-8 w-[80rem]">
<div class="grid grid-cols-3 gap-2">
<div class="flex flex-col">
<span class="text-lg font-semibold">Actions</span>
<span class="text-sm text-light-70 dark:text-dark-70">Attaquer - Saisir - Faire chuter - Déplacer - Courir - Pas de coté - Lancer un sort - S'interposer - Se transformer - Utiliser un objet - Anticiper une action - Improviser</span>
</div>
<div class="flex flex-col">
<span class="text-lg font-semibold">Réactions</span>
<span class="text-sm text-light-70 dark:text-dark-70">Parade - Esquive - Saisir une opportunité - Prendre en tenaille - Intercepter - Désarmer</span>
</div>
<div class="flex flex-col">
<span class="text-lg font-semibold">Actions libre</span>
<span class="text-sm text-light-70 dark:text-dark-70">Analyser une situation - Communiquer</span>
</div>
</div>
<div class="grid grid-cols-3 gap-2">
<div><MarkdownRenderer :content="character.features.action.join('\n')" /></div>
<div><MarkdownRenderer :content="character.features.reaction.join('\n')" /></div>
<div><MarkdownRenderer :content="character.features.freeaction.join('\n')" /></div>
</div>
<div class="flex flex-col">
<span class="text-lg font-semibold">Aptitudes</span>
<MarkdownRenderer :content="character.features.misc.map(e => `> ${e}`).join('\n\n')" />
</div>
</div>
</div>
</div>
</div>
<div v-else>
<Head>
<Title>d[any] - Erreur</Title>
</Head>
<div>Erreur de chargement</div>
</div>
</template>

View File

@ -0,0 +1,61 @@
<script setup lang="ts">
import type { Progression } from '~/types/character';
definePageMeta({
guestsGoesTo: '/user/login',
})
const { add } = useToast();
const { user } = useUserSession();
const loading = ref(true);
const characters = ref<Array<{ id: number, name: string, progress: Progression }>>([]);
characters.value = await useRequestFetch()('/api/character');
loading.value = false;
async function deleteCharacter(id: number)
{
loading.value = true;
await useRequestFetch()(`/api/character/${id}`, { method: 'delete' });
loading.value = false;
add({ content: 'Personnage supprimé', type: 'info', duration: 25000, timer: true, });
characters.value = characters.value?.filter(e => e.id !== id);
}
</script>
<template>
<Head>
<Title>d[any] - Mes personnages</Title>
</Head>
<div class="flex flex-col">
<NuxtLink v-if="user?.state === 1" :to="{ name: 'character-id-edit', params: { id: 'new' } }" class="flex align-center justify-center"><Button>Nouveau personnage</Button></NuxtLink>
<Tooltip v-else side="top" message="Veuillez valider votre email avant de pouvoir créer un personnage."><Button disabled>Nouveau personnage</Button></Tooltip>
<div v-if="loading" class="flex flex-1 justify-center align-center">
<Loading size="large" />
</div>
<div v-else class="grid p-6 grid-cols-4 gap-4">
<div class="border border-light-30 dark:border-dark-30 p-1 flex flex-row gap-4" v-for="character of characters">
<Avatar size="large" icon="radix-icons:person" src="" class="m-2" />
<div class="flex flex-col justify-between w-64">
<NuxtLink class="flex-1 text-xl font-bold hover:text-accent-blue truncate" :to="{ name: 'character-id', params: { id: character.id } }" :title="character.name">{{ character.name }}</NuxtLink>
<span class="flex-1 text-sm truncate">Niveau {{ character.progress.level }}</span>
<div class="flex flex-row gap-8">
<NuxtLink class="font-bold text-accent-blue hover:text-opacity-50" :to="{ name: 'character-id-edit', params: { id: character.id } }">Editer</NuxtLink>
<AlertDialogRoot>
<AlertDialogTrigger asChild><span class="font-bold text-light-red dark:text-dark-red hover:text-opacity-50 cursor-pointer">Supprimer</span></AlertDialogTrigger>
<AlertDialogPortal>
<AlertDialogOverlay class="bg-light-0 dark:bg-dark-0 opacity-70 fixed inset-0 z-40" />
<AlertDialogContent
class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[800px] translate-x-[-50%] translate-y-[-50%] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 z-50 text-light-100 dark:text-dark-100">
<AlertDialogTitle class="text-3xl font-light relative -top-2">Supprimer {{ character.name }} ?</AlertDialogTitle>
<div class="flex flex-1 justify-end gap-4">
<AlertDialogCancel asChild><Button>Non</Button></AlertDialogCancel>
<AlertDialogAction asChild><Button @click="() => deleteCharacter(character.id)" class="border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red hover:bg-light-redBack dark:hover:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red dark:focus:shadow-dark-red">Oui</Button></AlertDialogAction>
</div>
</AlertDialogContent>
</AlertDialogPortal>
</AlertDialogRoot>
</div>
</div>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,29 @@
import { and, eq, sql } from 'drizzle-orm';
import useDatabase from '~/composables/useDatabase';
import { characterTable } from '~/db/schema';
import type { Character } from '~/types/character';
export default defineEventHandler(async (e) => {
const session = await getUserSession(e);
if(!session.user)
{
setResponseStatus(e, 401);
return;
}
const db = useDatabase();
const character = db.select({
id: characterTable.id,
name: characterTable.name,
progress: characterTable.progress,
}).from(characterTable).where(eq(characterTable.owner, session.user.id)).all();
if(character !== undefined)
{
return character as Character[];
}
setResponseStatus(e, 404);
return;
});

View File

@ -0,0 +1,29 @@
import useDatabase from '~/composables/useDatabase';
import { characterTable } from '~/db/schema';
export default defineEventHandler(async (e) => {
const body = await readBody(e);
if(!body)
{
setResponseStatus(e, 400);
return;
}
const session = await getUserSession(e);
if(!session.user || session.user.state !== 1)
{
setResponseStatus(e, 401);
return;
}
const db = useDatabase();
const id = await db.insert(characterTable).values({
name: body.name,
progress: body.progress,
owner: session.user.id,
}).returning({ id: characterTable.id });
setResponseStatus(e, 201);
return id[0].id;
});

View File

@ -0,0 +1,33 @@
import { eq } from 'drizzle-orm';
import useDatabase from '~/composables/useDatabase';
import { characterTable } from '~/db/schema';
export default defineEventHandler(async (e) => {
const id = getRouterParam(e, "id");
if(!id)
{
setResponseStatus(e, 400);
return;
}
const db = useDatabase();
const old = db.select({ id: characterTable.id, owner: characterTable.owner }).from(characterTable).where(eq(characterTable.id, id)).get();
if(!old)
{
setResponseStatus(e, 404);
return;
}
const session = await getUserSession(e);
if(!session.user || old.owner !== session.user.id)
{
setResponseStatus(e, 401);
return;
}
db.delete(characterTable).where(eq(characterTable.id, id)).run();
setResponseStatus(e, 200);
return;
});

View File

@ -0,0 +1,38 @@
import { and, eq, sql } from 'drizzle-orm';
import useDatabase from '~/composables/useDatabase';
import { characterTable } from '~/db/schema';
import type { Character } from '~/types/character';
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;
}
const db = useDatabase();
const character = db.select({
id: characterTable.id,
name: characterTable.name,
progress: characterTable.progress,
owner: characterTable.owner
}).from(characterTable).where(and(eq(characterTable.id, id), eq(characterTable.owner, session.user.id))).get();
if(character !== undefined)
{
return character as Character;
}
setResponseStatus(e, 404);
return;
});

View File

@ -0,0 +1,45 @@
import { eq } from 'drizzle-orm';
import useDatabase from '~/composables/useDatabase';
import { characterTable } from '~/db/schema';
export default defineEventHandler(async (e) => {
const id = getRouterParam(e, "id");
if(!id)
{
setResponseStatus(e, 400);
return;
}
const body = await readBody(e);
if(!body)
{
setResponseStatus(e, 400);
return;
}
const db = useDatabase();
const old = db.select({ id: characterTable.id, owner: characterTable.owner }).from(characterTable).where(eq(characterTable.id, parseInt(id))).get();
if(!old)
{
setResponseStatus(e, 404);
return;
}
const session = await getUserSession(e);
if(!session.user || old.owner !== session.user.id || session.user.state !== 1)
{
setResponseStatus(e, 401);
return;
}
db.update(characterTable).set({
name: body.name,
progress: body.progress,
}).where(eq(characterTable.id, parseInt(id))).run();
await useStorage('cache').removeItem(`nitro:functions:character:${id}.json`);
setResponseStatus(e, 200);
return;
});

View File

@ -0,0 +1,264 @@
import { and, eq } from 'drizzle-orm';
import useDatabase from '~/composables/useDatabase';
import { characterTable } from '~/db/schema';
import type { Character, CharacterConfig, CompiledCharacter, DoubleIndex, Feature, MainStat, TrainingLevel, TrainingOption } from '~/types/character';
import characterData from '#shared/character-config.json';
import { users } from '~/drizzle/schema';
export default defineEventHandler(async (e) => {
const id = getRouterParam(e, "id");
if(!id)
{
setResponseStatus(e, 400);
return;
}
const db = useDatabase();
const character = db.select({
id: characterTable.id,
name: characterTable.name,
progress: characterTable.progress,
owner: characterTable.owner,
username: users.username
}).from(characterTable).leftJoin(users, eq(characterTable.owner, users.id)).where(and(eq(characterTable.id, parseInt(id)))).get();
if(character !== undefined)
{
return compileCharacter(character as Character & { username: string });
}
setResponseStatus(e, 404);
return;
}/* , { name: "character", getKey: (e) => getRouterParam(e, "id") || 'error' } */);
/*
Athlétisme
La capacité à effectuer un acte physique intense ou prolongé. Permet de pousser, contraindre, nager, courir.
Force + Constitution.
Acrobatique
La capacité à se mouvoir avec souplesse sous la contrainte. Permet d'escalader, d'enjamber, de sauter.
Force + Dextérité.
Intimidation
La capacité à intimider et inspirer la crainte.
Force + Charisme.
Doigté
La capacité à faire des actions précises avec ses mains. Permet de voler à la tire, de crocheter.
Dextérité + Dextérité.
Discrétion
La capacité à dissimuler sa présence. Permet de se cacher, de se mouvoir sans bruit.
Dextérité + Dextérité.
Survie
La capacité à survivre dans des conditions difficiles. Permet de pister, de collecter de la nourriture, de retrouver son chemin.
Constitution + Psyché.
Enquête
La capacité à demander au MJ de l'aide parce que vous puez la merde.
Intelligence + Curiosité.
Histoire
La capacité à connaitre le passé du monde.
Intelligence + Curiosité.
Religion
La capacité a connaitre les pratiques et les coutumes religieuses.
Intelligence + Curiosité.
Arcanes
La capacité à comprendre et percevoir la magie. Permet de comprendre un sort en cours, de détecter de la magie.
Intelligence + Psyché.
Compréhension
La capacité à déterminer les intentions des interlocuteurs. Permet de déceler des mensonges, de l'influence.
Intelligence + Charisme.
Perception
La capacité à observer le monde à travers ces sens. Permet d'observer, d'entendre, de sentir.
Curiosité + Curiosité.
Représentation
La capacité à se mettre en scène et à utiliser les arts. Permet de se produire en spectacle, de jouer d'un instrument, de chanter, de danser.
Curiosité + Charisme.
Médicine
La capacité à apporter des soins. Permet de stabiliser un joueur mourant, de soigner durant un repos.
Curiosité + Psyché.
Persuasion
Charisme + Psyché.
Dressage
Charisme + Psyché.
Mensonge
Charisme + Psyché.
*/
function compileCharacter(character: Character & { username?: string }): CompiledCharacter
{
const config = characterData as CharacterConfig;
const race = character.progress.race.index !== undefined ? config.peoples[character.progress.race.index] : undefined;
const raceOptions = race ? character.progress.race.progress!.map(e => race.options[e[0]][e[1]]) : [];
const features = Object.entries(config.training).map(e => [e[0], getFeaturesOf(e[0] as MainStat, character.progress.training[e[0] as MainStat])]) as [MainStat, TrainingOption[]][];
const compiled: CompiledCharacter = {
id: character.id,
owner: character.owner,
username: character.username,
name: character.name,
health: raceOptions.reduce((p, v) => p + (v.health ?? 0), 0),
mana: raceOptions.reduce((p, v) => p + (v.mana ?? 0), 0),
race: character.progress.race.index ?? -1,
modifier: features.map(e => [e[0], Math.floor(e[1].length / 3)] as [MainStat, number]).reduce((p, v) => { p[v[0]] = v[1]; return p }, {} as Record<MainStat, number>),
level: character.progress.level,
features: {
action: [],
reaction: [],
freeaction: [],
misc: [],
},
abilities: {
athletics: [0, 0],
acrobatics: [0, 0],
intimidation: [0, 0],
sleightofhand: [0, 0],
stealth: [0, 0],
survival: [0, 0],
investigation: [0, 0],
history: [0, 0],
religion: [0, 0],
arcana: [0, 0],
understanding: [0, 0],
perception: [0, 0],
performance: [0, 0],
medecine: [0, 0],
persuasion: [0, 0],
animalhandling: [0, 0],
deception: [0, 0]
},
spellslots: 0,
spellranks: {
instinct: 0,
knowledge: 0,
precision: 0
},
speed: false,
defense: {
static: 6,
activeparry: 0,
activedodge: 0,
passiveparry: 0,
passivedodge: 0,
},
mastery: {
strength: 0,
dexterity: 0,
shield: 0,
armor: 0,
multiattack: 1,
magicpower: 0,
magicspeed: 0,
magicelement: 0
},
resistance: {
stun: [0, 0],
bleed: [0, 0],
posion: [0, 0],
fear: [0, 0],
influence: [0, 0],
charm: [0, 0],
possesion: [0, 0],
precision: [0, 0],
knowledge: [0, 0],
instinct: [0, 0]
},
initiative: 0,
aspect: "",
};
features.forEach(e => e[1].forEach((_e, i) => applyTrainingOption(e[0], _e, compiled, i === e[1].length - 1)));
specialFeatures(compiled, character.progress.training);
return compiled;
}
function applyTrainingOption(stat: MainStat, option: TrainingOption, character: CompiledCharacter, last: boolean)
{
if(option.health) character.health += option.health;
if(option.mana) character.mana += option.mana;
if(option.modifier) option.modifier.forEach(e => character.modifier[e[0]] += e[1]);
if(option.ability) option.ability.forEach(e => character.abilities[e[0]]![0] += e[1]);
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]++);
option.description.forEach(line => !line.disposable && (last || !line.replaced) && character.features[line.category ?? "misc"].push(line.text));
//if(option.features) option.features.forEach(e => applyFeature(e, character));
}
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)
{
} */
function getFeaturesOf(stat: MainStat, progression: DoubleIndex<TrainingLevel>[]): TrainingOption[]
{
const config = characterData as CharacterConfig;
return progression.map(e => config.training[stat][e[0]][e[1]]);
}

2817
shared/character-config.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -18,6 +18,7 @@ export function parsePath(path: string): string
} }
export function parseId(id: string | undefined): string |undefined export function parseId(id: string | undefined): string |undefined
{ {
return id;
return id?.normalize('NFD')?.replace(/[\u0300-\u036f]/g, '')?.replace(/^\d\. */g, '')?.replace(/\s/g, "-")?.replace(/%/g, "-percent")?.replace(/\?/g, "-q")?.toLowerCase(); return id?.normalize('NFD')?.replace(/[\u0300-\u036f]/g, '')?.replace(/^\d\. */g, '')?.replace(/\s/g, "-")?.replace(/%/g, "-percent")?.replace(/\?/g, "-q")?.toLowerCase();
} }
export function padLeft(text: string, pad: string, length: number): string export function padLeft(text: string, pad: string, length: number): string

2723
shared/test.ts Normal file

File diff suppressed because it is too large Load Diff

145
types/character.d.ts vendored Normal file
View File

@ -0,0 +1,145 @@
export type MainStat = "strength" | "dexterity" | "constitution" | "intelligence" | "curiosity" | "charisma" | "psyche";
export type Ability = "athletics" | "acrobatics" | "intimidation" | "sleightofhand" | "stealth" | "survival" | "investigation" | "history" | "religion" | "arcana" | "understanding" | "perception" | "performance" | "medecine" | "persuasion" | "animalhandling" | "deception";
export type Level = | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20;
export type TrainingLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15;
export type SpellType = "precision" | "knowledge" | "instinct";
export type Category = "action" | "reaction" | "freeaction" | "misc";
export type Resistance = keyof CompiledCharacter["resistance"];
export type DoubleIndex<T extends number | string> = [T, number];
export type Progression = {
training: Record<MainStat, DoubleIndex<TrainingLevel>[]>;
race: {
index?: number;
progress?: DoubleIndex<Level>[];
};
level: number;
abilities?: Partial<Record<Ability, number>>;
spells?: string[]; //Spell ID
modifiers?: Partial<Record<MainStat, number>>;
aspect?: string;
};
export type Character = {
id: number;
name: string;
progress: Progression;
owner?: number;
};
export type CharacterConfig = {
peoples: Race[],
training: Record<MainStat, Record<TrainingLevel, TrainingOption[]>>;
};
export type Race = {
name: string;
description: string;
utils: {
maxOption: number;
};
options: Record<Level, RaceOption[]>;
};
export type RaceOption = {
training?: number;
health?: number;
mana?: number;
spec?: number;
shaping?: number;
modifier?: number;
};
export type Feature = {
text?: string;
} & (ActionFeature | ReactionFeature | FreeActionFeature | BonusFeature | MiscFeature);
type ActionFeature = {
type: "action";
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;
property: string;
};
type MiscFeature = {
type: "misc";
text: string;
};
export type TrainingOption = {
description: Array<{
text: string;
disposable?: boolean;
replaced?: boolean;
category?: Category;
}>;
mana?: number;
health?: number;
modifier?: [MainStat, number][];
ability?: [Ability, number][];
speed?: false | number;
initiative?: number;
mastery?: keyof CompiledCharacter["mastery"];
spellrank?: SpellType;
spellslot?: number | MainStat;
defense?: Array<keyof CompiledCharacter["defense"]>;
resistance?: [Resistance, "attack" | "defense"][];
features?: Feature[]; //TODO
};
export type CompiledCharacter = {
id: number;
owner?: number;
username?: string;
name: string;
health: number;
mana: number;
race: number;
spellslots: number;
spellranks: Record<SpellType, 0 | 1 | 2 | 3>;
aspect: string;
speed: number | false;
initiative: number;
defense: {
static: number;
activeparry: number;
activedodge: number;
passiveparry: number;
passivedodge: number;
};
mastery: {
strength: number;
dexterity: number;
shield: number;
armor: number;
multiattack: number;
magicpower: number;
magicspeed: number;
magicelement: number;
};
resistance: { //First is attack, second is defense
stun: [number, number];
bleed: [number, number];
posion: [number, number];
fear: [number, number];
influence: [number, number];
charm: [number, number];
possesion: [number, number];
precision: [number, number];
knowledge: [number, number];
instinct: [number, number];
};
modifier: Record<MainStat, number>;
abilities: Partial<Record<Ability, [number, number]>>;
level: number;
features: Record<Category, string[]>; //Currently: List of training option as text. TODO: Update to a more complex structure later
};