From da93fcd82da7c165257825ef7110dab92f5a3ce4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pons?= Date: Tue, 26 Aug 2025 15:27:47 +0200 Subject: [PATCH] Homebrew manager completed ! --- db.sqlite | Bin 761856 -> 761856 bytes db.sqlite-shm | Bin 32768 -> 32768 bytes db.sqlite-wal | Bin 0 -> 8272 bytes db/schema.ts | 2 +- drizzle/0015_typical_blade.sql | 20 + drizzle/meta/0015_snapshot.json | 810 ++++++++++++++++++++++ drizzle/meta/_journal.json | 7 + server/api/character.get.ts | 4 +- server/api/character/[id]/compiled.get.ts | 149 ---- shared/character-config.json | 174 ++++- shared/character.util.ts | 38 +- shared/components.util.ts | 33 +- shared/feature.util.ts | 173 +++-- types/character.d.ts | 7 +- 14 files changed, 1173 insertions(+), 244 deletions(-) create mode 100644 drizzle/0015_typical_blade.sql create mode 100644 drizzle/meta/0015_snapshot.json delete mode 100644 server/api/character/[id]/compiled.get.ts diff --git a/db.sqlite b/db.sqlite index e8c0a12edc73deacc34b481de2ed9a5359a83491..7448ef7cf9ec5a8f88159175bfd630585927497c 100644 GIT binary patch delta 918 zcmah{OK1~87@o%_vzyIkwzQ>asR>#UNojXC$tH^;D)peXSWpx}NjKZA#%hu_+XqN% zY}NVzt*)4Y7H?7nsco=}q99Zdq!&+u2fc_VFCG-62c7t+-W>R5{@3^aGygxO=dGpZ zty7IP3IN~1+6P9A> zMk1rz@l?{#Ej6X98OzX8@t9>~bS3nN%nU>V-}S0M9pq z19s7|bzM898vO}F>C44?^20;LVtzQQ_V=c;X0q6a3j^69)3LWmL9m7~p;Tq=9qh?E z$N$HzL%+$an?}7rq58M3@+UF#ty{x<|0Y&enTra7U6jRC$3!2{SM&p#W%HOule6Ph zU_dyMA@>5Lb#1*M7BnCQ#72%rD!Y`9$az)IF8@avG3*3a8u1}rx zrX%D$nx9jkcWsm$g`BhxzVmK6wx&VTdg$g`lqKjvgUiPHQSd=D0+j?lX5V}32Z z%~j=+w@@JOFvp``mG58}EvqAB7YZzSBO{~(anepWRNfaMyFK|d(GZfZmfJ_ko@!gB z8e%bc?xWJTpjYn*&0(SuEI=(;dm?9Q&9((85f)yki%;BaS`dFtz-8kTer!K4s=i vcbl&X1q3(5wYhI_OGD6aet~zM0u?*V)-`+%!XrU9n}6ba!WFXJ?0?GOQq7ETTh*#q7)ub*W_$ z5klCMTububR4a*i=^#Y@8`v*GNa_|Ix){+RJa~*C!659=C%hl;@pwEQyz*9}yp@<~ zsZTNt^PYN6Nt8C*q4G&4@ie?{FJOOIq&+N*xJZhSxiy~xy$ez-;El9YI2PlYi=q*) zzBq%uS~8!6FW-30clM$*ZLsnGF$eXG`UJbHWKz!=B z&gB-0YCdHaWJAqbnnQ=}eA?D6)z%$Nk+W(+C;x8pe|=FUt!Zf`opBURH}h&nb{yN# zos1Ky9}qo#Nqj1N@r zl~4rW>XQ;`^*CPzkcl{2WI3^q`RcX{kB*A#-iq)VfKB&?_>Kq(^r!2XA2JQh2Ed)6 zpWKW%CG6LIs``uQq$^9KWTS|Fu-sVx2}D+6=sO_yhtNj=+N<&~x&+0GWOx|$!1dK6 zAo~x|H30F4YWE1bN_tPD+W?+#bf3{pUxYR{6`JGdo4VX-)a>qt+Q!SMR`#D=3$)Sh SIZCf|rvg+R=}rlo5B>wUQ?eld diff --git a/db.sqlite-shm b/db.sqlite-shm index fe9ac2845eca6fe6da8a63cd096d9cf9e24ece10..198aaff789e269d124d78b915bda0bd087a5799a 100644 GIT binary patch delta 159 zcmZo@U}|V!s+V}A%K!pQK+MR%AONCw0r9``?u$pfw=nPcocVF}s;ym2mcDY|OxP}w psvc+*7=XDz^o@1e@vP+ni9o2#n7F1^nUtbcvyBp(I&_W9<*(39DX3k7uW8=kuq_MD^uf1~-u`>ijCD^E7 z1S`?b5b;I}!7nVtLTp4K+SvJ32_fhkuneAkLH~!q4D9YOJm<_azh}2udTD*WgWpee z-HC1)UtB-@`|{DJuYG&wpYM)cJ9j@Xt@--$zx5xwUw6O1^7V85@MbseZMq>2I9?pq z{m=bT|3d#{|KQ$ce16;??;EZTUkz7T(ncfD2s8qXKqJryGy;u4BhUym0yi0f>CV+t z$5*pLlf+D5LezlTCU;Z_rW4!VQaPywf|)({wuK;OYd~Rg zG7B%Pp!)8|=caK{2e#`2A+RI>My~TlTArzhArWQa>N{utoQ6f6FryOMfhDZ~krcR0 z8lrY07e7ZNTi@QCo%%&xQYp5C42H-7dbS+c8#{4{dHCd=trspoFx|eWJ(EJsZi|lW zTX0Dry_AHcwD2VuQ{OmvX1Z$2$F&e(P~ZT>nne}Je^v4i@;Dd8jdmYt#CTtWC_tLFE|IGJ=PC@ zx;EXrsN?KxF%wX!xa>`?Gh*k!RO7!K=$MlCW}et`oWFs(_M?&#f&Z&4a&}(Ah&~H zvIAB+3_&DMroLZ3pZ2!uBC{^Mb=V#ixg><(fcV{Xc}`i8tQQUk{v4Sgms<*&-#q8o zw_@l)@T{OBv6vA8>x<_fV)Ze;{{Js<<@K{SZu#W&H$5-zjGJ!UjKA@;jYgmmXapL8 zMxYUB1R8-xpb=;U8i7Wj5x5x%^t-oqtJU#?{bx_s_2KDZXK!c43kQ!sHY|TX{sm`* B0?PmZ literal 0 HcmV?d00001 diff --git a/db/schema.ts b/db/schema.ts index ffdfdd9..eba5829 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -52,7 +52,7 @@ export const characterTable = table("character", { id: int().primaryKey({ autoIncrement: true }), name: text().notNull(), owner: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }), - people: int().notNull(), + people: text().notNull(), level: int().notNull().default(1), aspect: int(), notes: text(), diff --git a/drizzle/0015_typical_blade.sql b/drizzle/0015_typical_blade.sql new file mode 100644 index 0000000..edcd1ac --- /dev/null +++ b/drizzle/0015_typical_blade.sql @@ -0,0 +1,20 @@ +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, + `people` text NOT NULL, + `level` integer DEFAULT 1 NOT NULL, + `aspect` integer, + `notes` text, + `health` integer DEFAULT 0 NOT NULL, + `mana` integer DEFAULT 0 NOT NULL, + `visibility` text DEFAULT 'private' 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", "people", "level", "aspect", "notes", "health", "mana", "visibility", "thumbnail") SELECT "id", "name", "owner", "people", "level", "aspect", "notes", "health", "mana", "visibility", "thumbnail" FROM `character`;--> statement-breakpoint +DROP TABLE `character`;--> statement-breakpoint +ALTER TABLE `__new_character` RENAME TO `character`;--> statement-breakpoint +PRAGMA foreign_keys=ON; \ No newline at end of file diff --git a/drizzle/meta/0015_snapshot.json b/drizzle/meta/0015_snapshot.json new file mode 100644 index 0000000..f66d49e --- /dev/null +++ b/drizzle/meta/0015_snapshot.json @@ -0,0 +1,810 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "6651137c-a198-4538-86be-7cb8b88ca998", + "prevId": "8f89d284-71da-46ae-a282-538f8a901294", + "tables": { + "character_abilities": { + "name": "character_abilities", + "columns": { + "character": { + "name": "character", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ability": { + "name": "ability", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "max": { + "name": "max", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "character_abilities_character_character_id_fk": { + "name": "character_abilities_character_character_id_fk", + "tableFrom": "character_abilities", + "tableTo": "character", + "columnsFrom": [ + "character" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "character_abilities_character_ability_pk": { + "columns": [ + "character", + "ability" + ], + "name": "character_abilities_character_ability_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "character_choices": { + "name": "character_choices", + "columns": { + "character": { + "name": "character", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "choice": { + "name": "choice", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "character_choices_character_character_id_fk": { + "name": "character_choices_character_character_id_fk", + "tableFrom": "character_choices", + "tableTo": "character", + "columnsFrom": [ + "character" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "character_choices_character_id_choice_pk": { + "columns": [ + "character", + "id", + "choice" + ], + "name": "character_choices_character_id_choice_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "character_leveling": { + "name": "character_leveling", + "columns": { + "character": { + "name": "character", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "choice": { + "name": "choice", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "character_leveling_character_character_id_fk": { + "name": "character_leveling_character_character_id_fk", + "tableFrom": "character_leveling", + "tableTo": "character", + "columnsFrom": [ + "character" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "character_leveling_character_level_pk": { + "columns": [ + "character", + "level" + ], + "name": "character_leveling_character_level_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "character_modifiers": { + "name": "character_modifiers", + "columns": { + "character": { + "name": "character", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "modifier": { + "name": "modifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "character_modifiers_character_character_id_fk": { + "name": "character_modifiers_character_character_id_fk", + "tableFrom": "character_modifiers", + "tableTo": "character", + "columnsFrom": [ + "character" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "character_modifiers_character_modifier_pk": { + "columns": [ + "character", + "modifier" + ], + "name": "character_modifiers_character_modifier_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "character_spell": { + "name": "character_spell", + "columns": { + "character": { + "name": "character", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "character_spell_character_character_id_fk": { + "name": "character_spell_character_character_id_fk", + "tableFrom": "character_spell", + "tableTo": "character", + "columnsFrom": [ + "character" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "character_spell_character_value_pk": { + "columns": [ + "character", + "value" + ], + "name": "character_spell_character_value_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "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 + }, + "people": { + "name": "people", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "aspect": { + "name": "aspect", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "health": { + "name": "health", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "mana": { + "name": "mana", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'private'" + }, + "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": {} + }, + "character_training": { + "name": "character_training", + "columns": { + "character": { + "name": "character", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stat": { + "name": "stat", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "choice": { + "name": "choice", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "character_training_character_character_id_fk": { + "name": "character_training_character_character_id_fk", + "tableFrom": "character_training", + "tableTo": "character", + "columnsFrom": [ + "character" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "character_training_character_stat_level_pk": { + "columns": [ + "character", + "stat", + "level" + ], + "name": "character_training_character_stat_level_pk" + } + }, + "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": {} + }, + "project_content": { + "name": "project_content", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "blob", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_files": { + "name": "project_files", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "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 + }, + "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 + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_files_path_unique": { + "name": "project_files_path_unique", + "columns": [ + "path" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_files_owner_users_id_fk": { + "name": "project_files_owner_users_id_fk", + "tableFrom": "project_files", + "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 + } + }, + "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": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 2c42361..933c6f2 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -106,6 +106,13 @@ "when": 1753175811770, "tag": "0014_careless_nick_fury", "breakpoints": true + }, + { + "idx": 15, + "version": "6", + "when": 1756214160038, + "tag": "0015_typical_blade", + "breakpoints": true } ] } \ No newline at end of file diff --git a/server/api/character.get.ts b/server/api/character.get.ts index e19ef9b..ed1f20d 100644 --- a/server/api/character.get.ts +++ b/server/api/character.get.ts @@ -58,6 +58,7 @@ export default defineEventHandler(async (e) => { modifiers: true, spells: true, training: true, + choices: true, user: { columns: { username: true } } @@ -80,9 +81,10 @@ export default defineEventHandler(async (e) => { training: character.training.reduce((p, v) => { if(!(v.stat in p)) p[v.stat] = []; p[v.stat].push([v.level as TrainingLevel, v.choice]); return p; }, {} as Record[]>), leveling: character.levels.map(e => [e.level as Level, e.choice] as DoubleIndex), - abilities: group(character.abilities.map(e => ({ ...e, value: [e.value, e.max] as [number, number] })), "ability", "value"), + abilities: group(character.abilities, "ability", "value"), spells: character.spells.map(e => e.value), modifiers: group(character.modifiers, "modifier", "value"), + choices: character.choices.reduce((p, v) => { p[v.id] ??= []; p[v.id]?.push(v.choice); return p; }, {} as Record), owner: character.owner, username: character.user.username, diff --git a/server/api/character/[id]/compiled.get.ts b/server/api/character/[id]/compiled.get.ts deleted file mode 100644 index 0dffa3f..0000000 --- a/server/api/character/[id]/compiled.get.ts +++ /dev/null @@ -1,149 +0,0 @@ -import useDatabase from '~/composables/useDatabase'; -import { type Character, type CharacterConfig, type CompiledCharacter, type DoubleIndex, type Level, type MainStat, type TrainingLevel, type TrainingOption } from '~/types/character'; -import characterData from '#shared/character-config.json'; -import { group } from '#shared/general.util'; -import { defaultCharacter, MAIN_STATS } from '#shared/character.util'; - -export default defineCachedEventHandler(async (e) => { - const id = getRouterParam(e, "id"); - if(!id) - { - setResponseStatus(e, 400); - return; - } - - const db = useDatabase(); - const character = db.query.characterTable.findFirst({ - with: { - abilities: true, - levels: true, - modifiers: true, - spells: true, - training: true, - user: { - columns: { username: true } - } - }, - where: (character, { eq }) => eq(character.id, parseInt(id, 10)), - }).sync(); - - if(character !== undefined) - { - return compileCharacter(Object.assign(defaultCharacter, { - id: character.id, - - name: character.name, - people: character.people, - level: character.level, - aspect: character.aspect, - notes: character.notes, - health: character.health, - mana: character.mana, - - training: character.training.reduce((p, v) => { if(!(v.stat in p)) p[v.stat] = []; p[v.stat].push([v.level as TrainingLevel, v.choice]); return p; }, {} as Record[]>), - leveling: character.levels.map(e => [e.level as Level, e.choice] as DoubleIndex), - abilities: group(character.abilities.map(e => ({ ...e, value: [e.value, e.max] as [number, number] })), "ability", "value"), - spells: character.spells.map(e => e.value), - modifiers: group(character.modifiers, "modifier", "value"), - - owner: character.owner, - username: character.user.username, - visibility: character.visibility, - } as Character) as Character); - } - - setResponseStatus(e, 404); - return; -}, { name: "character", getKey: (e) => getRouterParam(e, "id") || 'error' }); - -function compileCharacter(character: Character & { username?: string }): CompiledCharacter -{ - const config = characterData as CharacterConfig; - const race = character.people !== undefined ? config.peoples[character.people] : undefined; - const raceOptions = race ? character.leveling!.map(e => race.options[e[0]][e[1]]) : []; - const features = Object.entries(config.training).map(e => [e[0], getFeaturesOf(e[0] as MainStat, character.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.people!, - modifier: features.map(e => [e[0], Math.floor((e[1].length - 1) / 3) + (character.modifiers[e[0]] ?? 0)] as [MainStat, number]).reduce((p, v) => { p[v[0]] = v[1]; return p }, {} as Record), - level: character.level, - values: { - health: character.health, - mana: character.mana - }, - features: { - action: [], - reaction: [], - freeaction: [], - passive: [], - }, - abilities: { - athletics: 0, - acrobatics: 0, - intimidation: 0, - sleightofhand: 0, - stealth: 0, - survival: 0, - investigation: 0, - history: 0, - religion: 0, - arcana: 0, - understanding: 0, - perception: 0, - performance: 0, - medecine: 0, - persuasion: 0, - animalhandling: 0, - deception: 0 - }, - spellslots: 0, - artslots: 0, - spellranks: { - instinct: 0, - knowledge: 0, - precision: 0, - arts: 0, - }, - spells: character.spells ?? [], - speed: false, - defense: { - hardcap: Infinity, - 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, - magicinstinct: 0, - }, - resistance: {},//Object.fromEntries(MAIN_STATS.map(e => [e as MainStat, [0, 0]])) as Record, - initiative: 0, - aspect: "", - notes: character.notes ?? "", - }; - - //features.forEach(e => e[1].forEach(_e => _e.features?.forEach(f => applyFeature(compiled, f)))); - - return compiled; -} - -export function getFeaturesOf(stat: MainStat, progression: DoubleIndex[]): TrainingOption[] -{ - const config = characterData as CharacterConfig; - return progression.map(e => config.training[stat][e[0]][e[1]]); -} \ No newline at end of file diff --git a/shared/character-config.json b/shared/character-config.json index 64437bb..3a0156a 100644 --- a/shared/character-config.json +++ b/shared/character-config.json @@ -745,8 +745,9 @@ } ] }, - "peoples": [ - { + "peoples": { + "e662m19q590kn4dowvssowi1qf8ia7sk": { + "id": "e662m19q590kn4dowvssowi1qf8ia7sk", "name": "Humain", "description": "Les humains, originaire d'un tout autre monde, ont subit un cataclysme qui les a projeté dans les terres d'Erina. En tant que civilisation dépourvue de magie, ils sont plus specialisés, gagnant moins de statistiques mais pouvant plus tôt ou plus fréquemment obtenir certains bonus.", "options": { @@ -833,8 +834,75 @@ "9q8mf0u06oxxwqltyv58kbavs7qtoouw" ] } + }, + "3v3rwsn9bimpyd2fc95ml8wdrrmfsqb0": { + "id": "3v3rwsn9bimpyd2fc95ml8wdrrmfsqb0", + "name": "Quplothien", + "description": "Quploth est la région du monde abritant le plus de marchands et charlatans. Dû à la sur-désertification de leurs terres, ils ont appris à vivre en troquant les richesses. Leurs cités, denses et prospères, sont peu nombreuses et suscitent un tourisme culturel croissant.", + "options": { + "1": [ + "bbuzw6awn2mkb05imdoecxfkc5zuj98i" + ], + "2": [ + "ub2ws6q8xdbngeouip02umvw2oox9r68" + ], + "3": [ + "5d7u2jvi4u0nnrzesderha3uo8kb3zjq" + ], + "4": [ + "8w4jthjrn3l8u4trmj46z6t6ab5rbgk3" + ], + "5": [ + "z9lux6nlhl8pjhcwst6bnhpn6cq6c77w" + ], + "6": [ + "dx5khvrhwkhhn8fv4b8pecuh8i5wtwij" + ], + "7": [ + "pfzopr4oyrsgxg0cbva16zzzly3kke9z" + ], + "8": [ + "fk0wmg94tlq78khq8zot2o5u4nnxr2gb" + ], + "9": [ + "u9vv3z280jgzab7pjwe9kexqjlpoxvax" + ], + "10": [ + "fuxn9ndabr5yl0rrdtilldssmjxso24p" + ], + "11": [ + "7kdxs6b6j9pqhgm3c8ydp8f9o074vp8q" + ], + "12": [ + "dwvjqspm8l0gnks6y7u9vty0563u20kd" + ], + "13": [ + "bmh55yfypfw8rezd16m2cuocrx0kkfl4" + ], + "14": [ + "hatuas1yl3armteqwjwm1gjpsjp3v97x" + ], + "15": [ + "0fg543b25uppvollu9oxtowyxjjw1x5a" + ], + "16": [ + "l770gvirwzfbtfgfl29dxhvdh95f1m71" + ], + "17": [ + "m1zkviiz3ow1g7rwpkyygmyggphvoz8b" + ], + "18": [ + "uuc8vci5bk5kkx23a7ks1gu778fmu9w1" + ], + "19": [ + "fd076hnyjagipaez166p9xp3wtlf5sgw" + ], + "20": [ + "qcp28eysi3l3n438v41kowisdpq4ht61" + ] + } } - ], + }, "training": { "strength": { "0": [ @@ -9452,6 +9520,106 @@ } ], "id": "9q8mf0u06oxxwqltyv58kbavs7qtoouw" + }, + "bbuzw6awn2mkb05imdoecxfkc5zuj98i": { + "id": "bbuzw6awn2mkb05imdoecxfkc5zuj98i", + "description": "Bonjour", + "effect": [] + }, + "ub2ws6q8xdbngeouip02umvw2oox9r68": { + "id": "ub2ws6q8xdbngeouip02umvw2oox9r68", + "description": "je", + "effect": [] + }, + "5d7u2jvi4u0nnrzesderha3uo8kb3zjq": { + "id": "5d7u2jvi4u0nnrzesderha3uo8kb3zjq", + "description": "suis", + "effect": [] + }, + "8w4jthjrn3l8u4trmj46z6t6ab5rbgk3": { + "id": "8w4jthjrn3l8u4trmj46z6t6ab5rbgk3", + "description": "Nicolas", + "effect": [] + }, + "z9lux6nlhl8pjhcwst6bnhpn6cq6c77w": { + "id": "z9lux6nlhl8pjhcwst6bnhpn6cq6c77w", + "description": "Sarkozy", + "effect": [] + }, + "dx5khvrhwkhhn8fv4b8pecuh8i5wtwij": { + "id": "dx5khvrhwkhhn8fv4b8pecuh8i5wtwij", + "description": "", + "effect": [] + }, + "pfzopr4oyrsgxg0cbva16zzzly3kke9z": { + "id": "pfzopr4oyrsgxg0cbva16zzzly3kke9z", + "description": "", + "effect": [] + }, + "fk0wmg94tlq78khq8zot2o5u4nnxr2gb": { + "id": "fk0wmg94tlq78khq8zot2o5u4nnxr2gb", + "description": "", + "effect": [] + }, + "u9vv3z280jgzab7pjwe9kexqjlpoxvax": { + "id": "u9vv3z280jgzab7pjwe9kexqjlpoxvax", + "description": "", + "effect": [] + }, + "fuxn9ndabr5yl0rrdtilldssmjxso24p": { + "id": "fuxn9ndabr5yl0rrdtilldssmjxso24p", + "description": "", + "effect": [] + }, + "7kdxs6b6j9pqhgm3c8ydp8f9o074vp8q": { + "id": "7kdxs6b6j9pqhgm3c8ydp8f9o074vp8q", + "description": "", + "effect": [] + }, + "dwvjqspm8l0gnks6y7u9vty0563u20kd": { + "id": "dwvjqspm8l0gnks6y7u9vty0563u20kd", + "description": "", + "effect": [] + }, + "bmh55yfypfw8rezd16m2cuocrx0kkfl4": { + "id": "bmh55yfypfw8rezd16m2cuocrx0kkfl4", + "description": "", + "effect": [] + }, + "hatuas1yl3armteqwjwm1gjpsjp3v97x": { + "id": "hatuas1yl3armteqwjwm1gjpsjp3v97x", + "description": "", + "effect": [] + }, + "0fg543b25uppvollu9oxtowyxjjw1x5a": { + "id": "0fg543b25uppvollu9oxtowyxjjw1x5a", + "description": "", + "effect": [] + }, + "l770gvirwzfbtfgfl29dxhvdh95f1m71": { + "id": "l770gvirwzfbtfgfl29dxhvdh95f1m71", + "description": "", + "effect": [] + }, + "m1zkviiz3ow1g7rwpkyygmyggphvoz8b": { + "id": "m1zkviiz3ow1g7rwpkyygmyggphvoz8b", + "description": "", + "effect": [] + }, + "uuc8vci5bk5kkx23a7ks1gu778fmu9w1": { + "id": "uuc8vci5bk5kkx23a7ks1gu778fmu9w1", + "description": "", + "effect": [] + }, + "fd076hnyjagipaez166p9xp3wtlf5sgw": { + "id": "fd076hnyjagipaez166p9xp3wtlf5sgw", + "description": "", + "effect": [] + }, + "qcp28eysi3l3n438v41kowisdpq4ht61": { + "id": "qcp28eysi3l3n438v41kowisdpq4ht61", + "description": "", + "effect": [] } } } \ No newline at end of file diff --git a/shared/character.util.ts b/shared/character.util.ts index db730ed..2fad7a0 100644 --- a/shared/character.util.ts +++ b/shared/character.util.ts @@ -428,7 +428,7 @@ export class CharacterBuilder extends CharacterCompiler } private render() { - /*this._steps = [ + this._steps = [ PeoplePicker, LevelPicker, TrainingPicker, @@ -439,7 +439,7 @@ export class CharacterBuilder extends CharacterCompiler dom("div", { class: "group flex items-center", }, [ dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue disabled:text-light-50 dark:disabled:text-dark-50 disabled:hover:border-transparent group-data-[state=active]:text-accent-blue cursor-pointer", listeners: { click: () => this.display(i) } }, [text(e.header)]), ]) - );*/ + ); this._helperText = text("Choisissez un peuple afin de définir la progression de votre personnage au fil des niveaux.") this._content = dom('div', { class: 'flex-1 outline-none max-w-full w-full overflow-y-auto', attributes: { id: 'characterEditorContainer' } }); this._container.appendChild(div('flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden', [ @@ -460,15 +460,15 @@ export class CharacterBuilder extends CharacterCompiler if(step < 0 || step >= this._stepsHeader.length) return; - //if(step !== 0 && this._steps.slice(0, step).some(e => !e.validate(this))) - // return; + if(step !== 0 && this._steps.slice(0, step).some(e => !e.validate(this))) + return; - //this._stepsHeader.forEach(e => e.setAttribute('data-state', 'inactive')); - //this._stepsHeader[step]!.setAttribute('data-state', 'active'); + this._stepsHeader.forEach(e => e.setAttribute('data-state', 'inactive')); + this._stepsHeader[step]!.setAttribute('data-state', 'active'); - //this._content?.replaceChildren(...(new this._steps[step]!(this)).dom); + this._content?.replaceChildren(...(new this._steps[step]!(this)).dom); - //this._helperText.textContent = this._steps[step]!.description; + this._helperText.textContent = this._steps[step]!.description; } async save(leave: boolean = true) { @@ -681,7 +681,7 @@ abstract class BuilderTab { }; type BuilderTabConstructor = { new (builder: CharacterBuilder): BuilderTab; - name: string; + header: string; description: string; validate(builder: CharacterBuilder): boolean; } @@ -691,8 +691,6 @@ class PeoplePicker extends BuilderTab private _visibilityInput: HTMLDivElement; private _options: HTMLDivElement[]; - private _activeOption?: HTMLDivElement; - static override header = 'Peuple'; static override description = 'Choisissez un peuple afin de définir la progression de votre personnage au fil des niveaux.'; @@ -704,16 +702,15 @@ class PeoplePicker extends BuilderTab input: (value) => { this._builder.character.name = value ?? ''; document.title = `d[any] - Edition de ${this._builder.character.name || 'nouveau personnage'}`; - } + }, defaultValue: this._builder.character.name }); this._visibilityInput = toggle({ defaultValue: this._builder.character.visibility === "private", change: (value) => this._builder.character.visibility = value ? "private" : "public" }); - this._options = config.peoples.map( + this._options = Object.values(config.peoples).map( (people, i) => dom("div", { class: "flex flex-col flex-nowrap gap-2 p-2 border border-light-35 dark:border-dark-35 cursor-pointer hover:border-light-70 dark:hover:border-dark-70 w-[320px]", listeners: { click: () => { - this._builder.character.people = i; - "border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, false)); - this._activeOption = this._options[i]!; - "border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, true)); + this._builder.character.people = people.id; + "border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._options.forEach(f => f?.classList.toggle(e, false))); + "border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._options[i]?.classList.toggle(e, true)); } } }, [div("h-[320px]"), div("text-xl font-bold text-center", [text(people.name)]), div("w-full border-b border-light-50 dark:border-dark-50"), div("text-wrap word-break", [text(people.description)])]), ); @@ -734,13 +731,6 @@ class PeoplePicker extends BuilderTab { this._nameInput.value = this._builder.character.name; this._visibilityInput.setAttribute('data-state', this._builder.character.visibility === "private" ? "checked" : "unchecked"); - "border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, false)); - - if(this._builder.character.people !== undefined) - { - this._activeOption = this._options[this._builder.character.people]!; - "border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, true)); - } } static override validate(builder: CharacterBuilder): boolean { diff --git a/shared/components.util.ts b/shared/components.util.ts index 0d3c011..078d271 100644 --- a/shared/components.util.ts +++ b/shared/components.util.ts @@ -47,10 +47,10 @@ export function select>(options: Array<{ text: string if(e === undefined) return; - return dom('div', { listeners: { click: () => { + return dom('div', { listeners: { click: (_e) => { textValue.textContent = e.text; settings?.change && settings?.change(e.value); - context && context.close && context.close(); + context && context.close && !_e.ctrlKey && context.close(); }, mouseenter: (e) => focus(i) }, class: ['data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option] }, [ text(e.text) ]); }); const select = dom('div', { listeners: { click: () => { @@ -116,12 +116,12 @@ export function multiselect>(options: Array<{ text: s if(e === undefined) return; - const element = dom('div', { listeners: { click: () => { + const element = dom('div', { listeners: { click: (_e) => { selection = selection.includes(e.value) ? selection.filter(f => f !== e.value) : [...selection, e.value]; textValue.textContent = selection.length > 0 ? ((options.find(f => f?.value === selection[0])?.text ?? '') + (selection.length > 1 ? ` +${selection.length - 1}` : '')) : ''; element.toggleAttribute('data-selected', selection.includes(e.value)); settings?.change && settings?.change(selection); - context && context.close && context.close(); + context && context.close && !_e.ctrlKey && context.close(); }, mouseenter: (e) => focus(i) }, class: ['group flex flex-row justify-between items-center data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option], attributes: { 'data-selected': selection.includes(e.value) } }, [ text(e.text), icon('radix-icons:check', { class: 'hidden group-data-[selected]:block', noobserver: true }) ]); return element; }); @@ -240,11 +240,11 @@ export function combobox>(options: Option[], setti } else { - return { item: option, dom: dom('div', { listeners: { click: () => { + return { item: option, dom: dom('div', { listeners: { click: (_e) => { select.value = option.text; settings?.change && settings?.change(option.value as T); selected = true; - hide(); + !_e.ctrlKey && hide(); }, mouseenter: () => focus(option.value) }, class: ['data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option] }, [ option?.render ? option?.render() : text(option.text) ]) }; } } @@ -347,10 +347,10 @@ export function numberpicker(settings?: { defaultValue?: number, change?: (value switch(e.key) { case "ArrowUp": - validateAndChange(storedValue + (e.shiftKey ? 10 : 1)) && settings?.input && settings.input(storedValue); + validateAndChange(storedValue + (e.ctrlKey ? 10 : 1)) && settings?.input && settings.input(storedValue); break; case "ArrowDown": - validateAndChange(storedValue - (e.shiftKey ? 10 : 1)) && settings?.input && settings.input(storedValue); + validateAndChange(storedValue - (e.ctrlKey ? 10 : 1)) && settings?.input && settings.input(storedValue); break; case "PageUp": settings?.max && validateAndChange(settings.max) && settings?.input && settings.input(storedValue); @@ -371,12 +371,23 @@ export function numberpicker(settings?: { defaultValue?: number, change?: (value return field; } // Open by default -export function foldable(content: NodeChildren, title: NodeChildren, settings?: { open?: boolean, class?: { container?: Class, title?: Class, content?: Class, icon?: Class } }) +export function foldable(content: NodeChildren | (() => NodeChildren), title: NodeChildren, settings?: { open?: boolean, class?: { container?: Class, title?: Class, content?: Class, icon?: Class } }) { + let _content: NodeChildren; + const display = (state: boolean) => { + if(state && !_content) + { + _content = typeof content === 'function' ? content() : content; + //@ts-ignore + contentContainer.replaceChildren(..._content); + } + } + const contentContainer = div(['hidden group-data-[active]:flex', settings?.class?.content]); const fold = div(['group flex flex-1 w-full flex-col', settings?.class?.container], [ - div('flex', [ dom('div', { listeners: { click: () => fold.toggleAttribute('data-active') }, class: ['flex justify-center items-center', settings?.class?.icon] }, [ icon('radix-icons:caret-right', { class: 'group-data-[active]:rotate-90 origin-center' }) ]), div(['flex-1', settings?.class?.title], title) ]), - div(['hidden group-data-[active]:flex', settings?.class?.content], content), + div('flex', [ dom('div', { listeners: { click: () => { display(fold.toggleAttribute('data-active')) } }, class: ['flex justify-center items-center', settings?.class?.icon] }, [ icon('radix-icons:caret-right', { class: 'group-data-[active]:rotate-90 origin-center', noobserver: true }) ]), div(['flex-1', settings?.class?.title], title) ]), + contentContainer ]); + display(settings?.open ?? true); fold.toggleAttribute('data-active', settings?.open ?? true); return fold; } diff --git a/shared/feature.util.ts b/shared/feature.util.ts index 5d482ad..c08be15 100644 --- a/shared/feature.util.ts +++ b/shared/feature.util.ts @@ -1,10 +1,10 @@ -import type { Ability, AspectConfig, CharacterConfig, Feature, FeatureEffect, FeatureItem, MainStat, Resistance, SpellConfig, TrainingLevel } from "~/types/character"; +import type { Ability, AspectConfig, CharacterConfig, Feature, FeatureEffect, FeatureItem, Level, MainStat, RaceConfig, Resistance, SpellConfig, TrainingLevel } from "~/types/character"; import { div, dom, icon, text, type NodeChildren } from "#shared/dom.util"; import { MarkdownEditor } from "#shared/editor.util"; import { fakeA } from "#shared/proses"; import { button, combobox, foldable, input, multiselect, numberpicker, select, table, toggle, type Option } from "#shared/components.util"; import { confirm, contextmenu, fullblocker, tooltip } from "#shared/floating.util"; -import { ALIGNMENTS, alignmentTexts, elementTexts, MAIN_STATS, mainStatShortTexts, mainStatTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts } from "#shared/character.util"; +import { ALIGNMENTS, alignmentTexts, elementTexts, LEVELS, MAIN_STATS, mainStatShortTexts, mainStatTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts } from "#shared/character.util"; import characterConfig from "#shared/character-config.json"; import { getID, ID_SIZE } from "#shared/general.util"; import renderMarkdown, { renderText } from "#shared/markdown.util"; @@ -100,24 +100,73 @@ abstract class BuilderTab { }; class PeopleEditor extends BuilderTab { - private _options: HTMLDivElement[]; - - private _activeOption?: HTMLDivElement; - constructor(builder: HomebrewBuilder, config: CharacterConfig) { super(builder, config); - this._options = config.peoples.map( - (people, i) => dom("div", { class: "flex flex-col flex-nowrap gap-2 p-2 border border-light-35 dark:border-dark-35 cursor-pointer hover:border-light-70 dark:hover:border-dark-70 w-[320px]", listeners: { click: () => { - "border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, false)); - this._activeOption = this._options[i]!; - "border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, true)); + const add = () => { + const people: RaceConfig = { + id: getID(ID_SIZE), + name: '', + description: '', + options: LEVELS.map(e => { + const feature: Feature = { + id: getID(ID_SIZE), + description: '', + effect: [], + } + config.features[feature.id] = feature; + return [e, [feature.id]] as [Level, string[]]; + }).reduce((p, v) => { p[v[0]] = v[1]; return p }, {} as Record) + }; + config.peoples[people.id] = people; + (this._content[0] as HTMLDivElement).appendChild(peopleRender(people)); + } + const remove = (people: RaceConfig) => { + confirm('Voulez vous vraiment supprimer cet aspect ?').then(e => { + if(e) + { + Object.values(people.options).forEach(e => e.forEach(id => delete config.features[id])); + delete config.peoples[people.id]; + + } - } }, [div("h-[320px]"), div("text-xl font-bold text-center", [text(people.name)]), div("w-full border-b border-light-50 dark:border-dark-50"), div("text-wrap word-break", [text(people.description)])]), - ); - - this._content = [ div('flex flex-1 gap-4 p-2 overflow-x-auto justify-center', this._options) ]; + }) + } + const render = (people: string, level: Level, feature: string) => { + let element = dom("div", { class: ["border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-50 dark:hover:border-dark-50"], listeners: { click: e => { + this._builder.edit(config.features[feature]!).then(e => { + element.replaceChildren(markdownUtil(config.features[feature]!.description, undefined, { tags: { a: fakeA } })); + }); + }, contextmenu: (e) => { + e.preventDefault(); + const context = contextmenu(e.clientX, e.clientY, [ + dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => { + context.close(); + const _feature: Feature = { id: getID(ID_SIZE), description: '', effect: [] }; + config.features[_feature.id] = _feature; + config.peoples[people]!.options[level]!.push(_feature.id); + element.parentElement?.appendChild(render(people, level, _feature.id)); + } } }, [ text('Nouveau') ]), + config.peoples[people]!.options[level].length > 1 ? dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => { + context.close(); + confirm('Voulez-vous vraiment supprimer cet element ?').then(e => { if(e) { + config.peoples[people]!.options[level] = config.peoples[people]!.options[level].filter(e => e !== feature); + delete config.features[feature]; + element.remove(); + } + }) } } }, [ text('Supprimer') ]) : undefined, + ], { placement: "right-start", priority: false }); + }}}, [ markdownUtil(config.features[feature]!.description, undefined, { tags: { a: fakeA } }) ]); + return element; + } + const peopleRender = (people: RaceConfig) => { + return foldable(() => Object.entries(people.options).flatMap(level => [ div("w-full flex h-px", [div("border-t border-dashed border-light-50 dark:border-dark-50 w-full"), dom('span', { class: "relative" }, [ text(level[0]) ])]), + div("flex flex-row gap-4 justify-center", level[1].map((option) => render(people.id, parseInt(level[0], 10) as Level, option))), + ]), [ input('text', { defaultValue: people.name, input: (value) => people.name = value, class: 'w-32' }), input('text', { defaultValue: people.description, input: (value) => people.description = value, class: 'w-full' }) ], { class: { container: 'gap-2 max-h-full', title: 'flex flex-row', content: 'flex flex-shrink-0 flex-col gap-4 relative w-full overflow-y-auto px-8' }, open: false }) + } + const container = div('flex flex-col gap-2', Object.values(config.peoples).map(peopleRender)); + this._content = [ div('flex flex-col py-2 gap-2', [ div('w-full flex flex-row-reverse', [ button(icon('radix-icons:plus'), add, 'p-1') ]), container ]) ]; } } class TrainingEditor extends BuilderTab @@ -131,24 +180,37 @@ class TrainingEditor extends BuilderTab constructor(builder: HomebrewBuilder, config: CharacterConfig) { super(builder, config); + const render = (stat: MainStat, level: TrainingLevel, feature: string) => { + let element = dom("div", { class: ["border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-50 dark:hover:border-dark-50"], listeners: { click: e => { + this._builder.edit(config.features[feature]!).then(e => { + element.replaceChildren(markdownUtil(config.features[feature]!.description, undefined, { tags: { a: fakeA } })); + }); + }, contextmenu: (e) => { + e.preventDefault(); + const context = contextmenu(e.clientX, e.clientY, [ + dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => { + context.close(); + const _feature: Feature = { id: getID(ID_SIZE), description: '', effect: [] }; + config.features[_feature.id] = _feature; + config.training[stat][level].push(_feature.id); + element.parentElement?.appendChild(render(stat, level, _feature.id)); + } } }, [ text('Nouveau') ]), + config.training[stat][level].length > 1 ? dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => { + context.close(); + confirm('Voulez-vous vraiment supprimer cet element ?').then(e => { if(e) { + config.training[stat][level as any as TrainingLevel] = config.training[stat][level as any as TrainingLevel].filter(e => e !== feature); + delete config.features[feature]; + element.remove(); + } + }) } } }, [ text('Supprimer') ]) : undefined, + ], { placement: "right-start", priority: false }); + }}}, [ markdownUtil(config.features[feature]!.description, undefined, { tags: { a: fakeA } }) ]); + return element; + }; const statRenderBlock = (stat: MainStat) => { return Object.entries(config.training[stat]).map( (level) => [ div("w-full flex h-px", [div("border-t border-dashed border-light-50 dark:border-dark-50 w-full"), dom('span', { class: "relative" }, [ text(level[0]) ])]), - div("flex flex-row gap-4 justify-center", level[1].map((option, j) => { - let element = dom("div", { class: ["border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-50 dark:hover:border-dark-50"], listeners: { click: e => { - this._builder.edit(config.features[option]!).then(e => { - element.replaceChildren(markdownUtil(config.features[option]!.description, undefined, { tags: { a: fakeA } })); - }); - }, contextmenu: (e) => { - e.preventDefault(); - const context = contextmenu(e.clientX, e.clientY, [ - dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => { context.close(); } } }, [ text('Nouveau avant') ]), - dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => { context.close(); } } }, [ text('Nouveau après') ]), - dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => { context.close(); confirm('Voulez-vous vraiment supprimer cet element ?').then(e => { if(e) { delete config.training[stat][level[0] as any as TrainingLevel]; /* redraw */ } }) } } }, [ text('Supprimer') ]) - ], { placement: "right-start", priority: false }); - }}}, [ markdownUtil(config.features[option]!.description, undefined, { tags: { a: fakeA } }) ]); - return element; - })), + div("flex flex-row gap-4 justify-center", level[1].map((option) => render(stat, parseInt(level[0], 10) as TrainingLevel, option))), ]) } @@ -229,11 +291,16 @@ class AspectEditor extends BuilderTab content = element; }; const remove = (aspect: AspectConfig) => { - config.aspects = config.aspects.filter(e => e !== aspect); + confirm('Voulez vous vraiment supprimer cet aspect ?').then(e => { + if(e) + { + config.aspects = config.aspects.filter(e => e !== aspect); - const element = redraw(); - content.parentElement?.replaceChild(element, content); - content = element; + const element = redraw(); + content.parentElement?.replaceChild(element, content); + content = element; + } + }) } const redraw = () => table(config.aspects.map(render), { name: 'Nom', description: 'Description', stat: 'Buff de stat', alignment: 'Alignement', magic: 'Magie', difficulty: 'Difficulté', physic: 'Physique', mental: 'Mental', personality: 'Caractère', action: 'Actions' }, { class: { table: 'flex-1' } }); let content = redraw(); @@ -247,19 +314,15 @@ class SpellEditor extends BuilderTab super(builder, config); const render = (spell: SpellConfig) => { - return { - id: spell.id, - name: input('text', { input: (value) => spell.name = value, defaultValue: spell.name, class: '!m-0 w-full' }), - rank: select([{ text: 'Rang 1', value: 1 }, { text: 'Rang 2', value: 2 }, { text: 'Rang 3', value: 3 }, { text: 'Spécial', value: 4 }], { change: (value: 1 | 2 | 3 | 4) => spell.rank = value, defaultValue: spell.rank, class: { container: '!m-0 w-full' } }), - type: select(SPELL_TYPES.map(f => ({ text: spellTypeTexts[f], value: f })), { change: (value) => spell.type = value, defaultValue: spell.type, class: { container: '!m-0 w-full' } }), - cost: numberpicker({ defaultValue: spell.cost, input: (value) => spell.cost = value, class: '!m-0 w-full' }), - speed: select<'action' | 'reaction' | number>([{ text: 'Action', value: 'action' }, { text: 'Reaction', value: 'reaction' }, { text: '1 minute', value: 1 }, { text: '10 minutes', value: 10 }], { change: (value) => spell.speed = value, defaultValue: spell.speed, class: { container: '!m-0 w-full' } }), - elements: multiselect(SPELL_ELEMENTS.map(f => ({ text: elementTexts[f].text, value: f })), { change: (value) => spell.elements = value, defaultValue: spell.elements, class: { container: '!m-0 w-full' } }), - effect: input('text', { input: (value) => spell.effect = value, defaultValue: spell.effect, class: '!m-0 w-full' }), - tags: multiselect([{ text: 'Dégâts', value: 'damage' }, { text: 'Buff', value: 'buff' }, { text: 'Debuff', value: 'debuff' }, { text: 'Support', value: 'support' }, { text: 'Tank', value: 'tank' }, { text: 'Mouvement', value: 'movement' }, { text: 'Utilitaire', value: 'utilitary' }], { change: (value) => spell.tags = value, defaultValue: spell.tags, class: { container: '!m-0 w-full' } }), - concentration: toggle({ change: (value) => spell.concentration = value, defaultValue: spell.concentration, class: { container: '!m-0' } }), - action: div('flex flex-row justify-center gap-2', [ button(icon('radix-icons:trash'), () => remove(spell), 'p-1') ]) - }; + return foldable([ + dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Rang'), select([{ text: 'Rang 1', value: 1 }, { text: 'Rang 2', value: 2 }, { text: 'Rang 3', value: 3 }, { text: 'Spécial', value: 4 }], { change: (value: 1 | 2 | 3 | 4) => spell.rank = value, defaultValue: spell.rank, class: { container: '!m-0 !h-9 w-full' } }), ]), + dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Type'), select(SPELL_TYPES.map(f => ({ text: spellTypeTexts[f], value: f })), { change: (value) => spell.type = value, defaultValue: spell.type, class: { container: '!m-0 !h-9 w-full' } }), ]), + dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Coût'), numberpicker({ defaultValue: spell.cost, input: (value) => spell.cost = value, class: '!m-0 w-full' }), ]), + dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Incantation'), select<'action' | 'reaction' | number>([{ text: 'Action', value: 'action' }, { text: 'Reaction', value: 'reaction' }, { text: '1 minute', value: 1 }, { text: '10 minutes', value: 10 }], { change: (value) => spell.speed = value, defaultValue: spell.speed, class: { container: '!m-0 !h-9 w-full' } }), ]), + dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Elements'), multiselect(SPELL_ELEMENTS.map(f => ({ text: elementTexts[f].text, value: f })), { change: (value) => spell.elements = value, defaultValue: spell.elements, class: { container: '!m-0 !h-9 w-full' } }), ]), + dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Tags'), multiselect([{ text: 'Dégâts', value: 'damage' }, { text: 'Buff', value: 'buff' }, { text: 'Debuff', value: 'debuff' }, { text: 'Support', value: 'support' }, { text: 'Tank', value: 'tank' }, { text: 'Mouvement', value: 'movement' }, { text: 'Utilitaire', value: 'utilitary' }], { change: (value) => spell.tags = value, defaultValue: spell.tags, class: { container: '!m-0 !h-9 w-full' } }), ]), + dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Concentration'), toggle({ change: (value) => spell.concentration = value, defaultValue: spell.concentration, class: { container: '!m-0 !flex-none' } }), ]), + ], [ div('gap-4 px-4 flex', [ input('text', { input: (value) => spell.name = value, defaultValue: spell.name, class: '!m-0 w-64' }), input('text', { input: (value) => spell.effect = value, defaultValue: spell.effect, class: '!m-0 w-full' }),div('flex flex-row justify-center gap-2', [ button(icon('radix-icons:trash', { noobserver: true }), () => remove(spell), 'p-1') ]) ]) ], { class: { container: 'border-light-35 dark:border-dark-35 py-1', content: 'gap-2 px-4 py-1 flex items-center *:flex-1' }, open: false }); } const add = () => { config.spells.push({ @@ -280,13 +343,19 @@ class SpellEditor extends BuilderTab content = element; }; const remove = (spell: SpellConfig) => { - config.spells = config.spells.filter(e => e !== spell); + confirm('Voulez vous vraiment supprimer ce sort ?').then(e => { + if(e) + { + config.spells = config.spells.filter(e => e !== spell); - const element = redraw(); - content.parentElement?.replaceChild(element, content); - content = element; + const element = redraw(); + content.parentElement?.replaceChild(element, content); + content = element; + } + }); } - const redraw = () => table(config.spells.map(render), { id: 'ID', name: 'Nom', rank: 'Rang', type: 'Type', cost: 'Coût', speed: 'Incantation', elements: 'Elements', effect: 'Effet', tags: 'Tag', concentration: 'Concentration', action: 'Actions' }, { class: { table: 'flex-1' } }); + const redraw = () => div('flex flex-col divide-y', config.spells.map(render)); + //, { class: { table: 'flex-1' } }); let content = redraw(); this._content = [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), add, 'p-1') ]), content ] ) ]; } diff --git a/types/character.d.ts b/types/character.d.ts index 9da96fa..29ca385 100644 --- a/types/character.d.ts +++ b/types/character.d.ts @@ -16,7 +16,7 @@ export type Character = { id: number; name: string; - people?: number; + people?: string; level: number; aspect?: number; notes?: string | null; @@ -44,7 +44,7 @@ export type CharacterVariables = { equipment: Array; }; export type CharacterConfig = { - peoples: RaceConfig[]; + peoples: Record; resistances: Record; training: Record>; abilities: Record; @@ -72,6 +72,7 @@ export type AbilityConfig = { description: string; }; export type RaceConfig = { + id: string; name: string; description: string; options: Record; @@ -126,7 +127,7 @@ export type CompiledCharacter = { name: string; health: number; //Max mana: number; //Max - race: number; + race: string; spellslots: number; //Max artslots: number; //Max spellranks: Record;