Character creation implementation. People and training ready, still need to work on abilities and spells
This commit is contained in:
parent
7beeed8a61
commit
f599b561af
4
app.vue
4
app.vue
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
11
db/schema.ts
11
db/schema.ts
|
|
@ -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], }),
|
||||||
|
}));
|
||||||
|
|
@ -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
|
||||||
|
);
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -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 }">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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;
|
||||||
|
});
|
||||||
|
|
@ -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;
|
||||||
|
});
|
||||||
|
|
@ -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;
|
||||||
|
});
|
||||||
|
|
@ -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;
|
||||||
|
});
|
||||||
|
|
@ -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;
|
||||||
|
});
|
||||||
|
|
@ -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]]);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue