diff --git a/db.sqlite b/db.sqlite index ba9eab2..ee2ea2e 100644 Binary files a/db.sqlite and b/db.sqlite differ diff --git a/db.sqlite-shm b/db.sqlite-shm index da6eab5..6a16c43 100644 Binary files a/db.sqlite-shm and b/db.sqlite-shm differ diff --git a/db.sqlite-wal b/db.sqlite-wal index 0b84fef..40e1d6f 100644 Binary files a/db.sqlite-wal and b/db.sqlite-wal differ diff --git a/db/schema.ts b/db/schema.ts index 7f9b55b..ffdfdd9 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -1,6 +1,5 @@ import { relations } from 'drizzle-orm'; import { int, text, sqliteTable as table, primaryKey, blob } from 'drizzle-orm/sqlite-core'; -import { ABILITIES, MAIN_STATS } from '../shared/character.util'; export const usersTable = table("users", { id: int().primaryKey({ autoIncrement: true }), @@ -66,7 +65,7 @@ export const characterTable = table("character", { export const characterTrainingTable = table("character_training", { character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }), - stat: text({ enum: MAIN_STATS }).notNull(), + stat: text({ enum: ["strength","dexterity","constitution","intelligence","curiosity","charisma","psyche"] }).notNull(), level: int().notNull(), choice: int().notNull(), }, (table) => [primaryKey({ columns: [table.character, table.stat, table.level] })]); @@ -79,14 +78,14 @@ export const characterLevelingTable = table("character_leveling", { export const characterAbilitiesTable = table("character_abilities", { character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }), - ability: text({ enum: ABILITIES }).notNull(), + ability: text({ enum: ["athletics","acrobatics","intimidation","sleightofhand","stealth","survival","investigation","history","religion","arcana","understanding","perception","performance","medecine","persuasion","animalhandling","deception"] }).notNull(), value: int().notNull().default(0), max: int().notNull().default(0), }, (table) => [primaryKey({ columns: [table.character, table.ability] })]); export const characterModifiersTable = table("character_modifiers", { character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }), - modifier: text({ enum: MAIN_STATS }).notNull(), + modifier: text({ enum: ["strength","dexterity","constitution","intelligence","curiosity","charisma","psyche"] }).notNull(), value: int().notNull().default(0), }, (table) => [primaryKey({ columns: [table.character, table.modifier] })]); @@ -95,6 +94,12 @@ export const characterSpellsTable = table("character_spell", { value: text().notNull(), }, (table) => [primaryKey({ columns: [table.character, table.value] })]); +export const characterChoicesTable = table("character_choices", { + character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }), + id: text().notNull(), + choice: int().notNull(), +}, (table) => [primaryKey({ columns: [table.character, table.id, table.choice] })]); + export const usersRelation = relations(usersTable, ({ one, many }) => ({ data: one(usersDataTable, { fields: [usersTable.id], references: [usersDataTable.id], }), session: many(userSessionsTable), @@ -119,7 +124,8 @@ export const characterRelation = relations(characterTable, ({ one, many }) => ({ levels: many(characterLevelingTable), abilities: many(characterAbilitiesTable), modifiers: many(characterModifiersTable), - spells: many(characterSpellsTable) + spells: many(characterSpellsTable), + choices: many(characterChoicesTable) })); export const characterTrainingRelation = relations(characterTrainingTable, ({ one }) => ({ @@ -136,4 +142,7 @@ export const characterModifierRelation = relations(characterModifiersTable, ({ o })); export const characterSpellsRelation = relations(characterSpellsTable, ({ one }) => ({ character: one(characterTable, { fields: [characterSpellsTable.character], references: [characterTable.id] }) +})); +export const characterChoicesRelation = relations(characterChoicesTable, ({ one }) => ({ + character: one(characterTable, { fields: [characterChoicesTable.character], references: [characterTable.id] }) })); \ No newline at end of file diff --git a/drizzle/0014_careless_nick_fury.sql b/drizzle/0014_careless_nick_fury.sql new file mode 100644 index 0000000..2cc3346 --- /dev/null +++ b/drizzle/0014_careless_nick_fury.sql @@ -0,0 +1,7 @@ +CREATE TABLE `character_choices` ( + `character` integer NOT NULL, + `id` text NOT NULL, + `choice` integer NOT NULL, + PRIMARY KEY(`character`, `id`, `choice`), + FOREIGN KEY (`character`) REFERENCES `character`(`id`) ON UPDATE cascade ON DELETE cascade +); diff --git a/drizzle/meta/0014_snapshot.json b/drizzle/meta/0014_snapshot.json new file mode 100644 index 0000000..0d28470 --- /dev/null +++ b/drizzle/meta/0014_snapshot.json @@ -0,0 +1,810 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "8f89d284-71da-46ae-a282-538f8a901294", + "prevId": "854c13bd-59bb-40bd-a046-69632b59557e", + "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": "integer", + "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 05407c9..2c42361 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -99,6 +99,13 @@ "when": 1753097020642, "tag": "0013_wakeful_lake", "breakpoints": true + }, + { + "idx": 14, + "version": "6", + "when": 1753175811770, + "tag": "0014_careless_nick_fury", + "breakpoints": true } ] } \ No newline at end of file diff --git a/server/api/character/[id].get.ts b/server/api/character/[id].get.ts index bb6c529..1d8a130 100644 --- a/server/api/character/[id].get.ts +++ b/server/api/character/[id].get.ts @@ -49,11 +49,12 @@ export default defineEventHandler(async (e) => { 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"), + training: character.training.reduce((p, v) => { p[v.stat] ??= {}; p[v.stat][v.level as TrainingLevel] = v.choice; return p; }, {} as Record>>), + leveling: character.levels.reduce((p, v) => { p[v.level as Level] = v.choice; return p; }, {} as Partial>), + abilities: group(character.abilities.map(e => ({ ...e, value: e.value })), "ability", "value"), spells: character.spells.map(e => e.value), modifiers: group(character.modifiers, "modifier", "value"), + choices: {}, owner: character.owner, username: character.user.username, diff --git a/server/api/character/[id].post.ts b/server/api/character/[id].post.ts index 4a625ea..9ca3f74 100644 --- a/server/api/character/[id].post.ts +++ b/server/api/character/[id].post.ts @@ -1,6 +1,6 @@ import { eq } from 'drizzle-orm'; import useDatabase from '~/composables/useDatabase'; -import { characterAbilitiesTable, characterLevelingTable, characterModifiersTable, characterSpellsTable, characterTable, characterTrainingTable } from '~/db/schema'; +import { characterAbilitiesTable, characterChoicesTable, characterLevelingTable, characterModifiersTable, characterSpellsTable, characterTable, characterTrainingTable } from '~/db/schema'; import { CharacterValidation } from '#shared/character.util'; import { type Ability, type MainStat } from '~/types/character'; @@ -54,23 +54,23 @@ export default defineEventHandler(async (e) => { tx.delete(characterModifiersTable).where(eq(characterModifiersTable.character, id)).run(); tx.delete(characterSpellsTable).where(eq(characterSpellsTable.character, id)).run(); tx.delete(characterAbilitiesTable).where(eq(characterAbilitiesTable.character, id)).run(); + tx.delete(characterChoicesTable).where(eq(characterChoicesTable.character, id)).run(); - if(body.data.leveling.length > 0) tx.insert(characterLevelingTable).values(body.data.leveling.map(e => ({ character: id, level: e[0], choice: e[1] }))).run(); + const leveling = Object.entries(body.data.leveling).filter(e => e[1] !== undefined).map(e => ({ character: id, level: parseInt(e[0]), choice: e[1]! })); + if(leveling.length > 0) tx.insert(characterLevelingTable).values(leveling).run(); - const training = Object.entries(body.data.training).flatMap(e => e[1].map(_e => ({ character: id, stat: e[0] as MainStat, level: _e[0], choice: _e[1] }))); + const training = Object.entries(body.data.training).flatMap(e => Object.entries(e[1]).filter(_e => _e[1] !== undefined).map(_e => ({ character: id, stat: e[0] as MainStat, level: parseInt(_e[0]), choice: _e[1]! }))); if(training.length > 0) tx.insert(characterTrainingTable).values(training).run(); - const modifiers = Object.entries(body.data.modifiers).map((e) => ({ character: id, modifier: e[0] as MainStat, value: e[1] })); + const modifiers = Object.entries(body.data.modifiers).filter(e => e[1] !== undefined).map((e) => ({ character: id, modifier: e[0] as MainStat, value: e[1] })); if(modifiers.length > 0) tx.insert(characterModifiersTable).values(modifiers).run(); if(body.data.spells.length > 0) tx.insert(characterSpellsTable).values(body.data.spells.map(e => ({ character: id, value: e }))).run(); - const abilities = Object.entries(body.data.abilities).map(e => ({ character: id, ability: e[0] as Ability, value: e[1][0], max: e[1][1] })); + const abilities = Object.entries(body.data.abilities).filter(e => e[1] !== undefined).map(e => ({ character: id, ability: e[0] as Ability, value: e[1], max: 0 })); if(abilities.length > 0) tx.insert(characterAbilitiesTable).values(abilities).run(); }); - await useStorage('cache').removeItem(`nitro:functions:character:${id}.json`); - setResponseStatus(e, 200); return; }); \ No newline at end of file diff --git a/shared/character-config.json b/shared/character-config.json index 2fde7a5..115e33b 100644 --- a/shared/character-config.json +++ b/shared/character-config.json @@ -186,36 +186,214 @@ "options": { "1": [ { - "description": "+35 points de statistiques.\n+14 PV max." + "description": "+35 points de statistiques.\n+14 PV max.", + "effect": [ + { + "category": "value", + "operation": "add", + "property": "training", + "value": 35 + }, + { + "category": "value", + "operation": "add", + "property": "health", + "value": 14 + } + ] } ], "2": [ { - "description": "+1 point de statistique.\n+3 PV max.\n+2 mana max." + "description": "+1 point de statistique.\n+3 PV max.\n+2 mana max.", + "effect": [ + { + "category": "value", + "operation": "add", + "property": "training", + "value": 1 + }, + { + "category": "value", + "operation": "add", + "property": "health", + "value": 3 + }, + { + "category": "value", + "operation": "add", + "property": "mana", + "value": 2 + } + ] }, { - "description": "+1 point de compétence.\n+6 PV max.\n+3 mana max." + "description": "+1 point de compétence.\n+6 PV max.\n+3 mana max.", + "effect": [ + { + "category": "value", + "operation": "add", + "property": "ability", + "value": 1 + }, + { + "category": "value", + "operation": "add", + "property": "health", + "value": 6 + }, + { + "category": "value", + "operation": "add", + "property": "mana", + "value": 3 + } + ] } ], "3": [ { - "description": "+2 points de statistiques.\n+1 point de compétence.\n+3 PV max.\n+1 mana max." + "description": "+2 points de statistiques.\n+1 point de compétence.\n+3 PV max.\n+1 mana max.", + "effect": [ + { + "category": "value", + "operation": "add", + "property": "training", + "value": 2 + }, + { + "category": "value", + "operation": "add", + "property": "ability", + "value": 1 + }, + { + "category": "value", + "operation": "add", + "property": "health", + "value": 3 + }, + { + "category": "value", + "operation": "add", + "property": "mana", + "value": 1 + } + ] } ], "4": [ { - "description": "+1 point de statistique.\n+2 points de compétences.\n+4 PV max.\n+2 mana max." + "description": "+1 point de statistique.\n+2 points de compétences.\n+4 PV max.\n+2 mana max.", + "effect": [ + { + "category": "value", + "operation": "add", + "property": "training", + "value": 1 + }, + { + "category": "value", + "operation": "add", + "property": "ability", + "value": 2 + }, + { + "category": "value", + "operation": "add", + "property": "health", + "value": 4 + }, + { + "category": "value", + "operation": "add", + "property": "mana", + "value": 2 + } + ] } ], "5": [ { - "description": "+1 point de statistique.\n+2 points de compétences.\n+4 PV max.\n+2 mana max." + "description": "+1 point de statistique.\n+2 points de compétences.\n+4 PV max.\n+2 mana max.", + "effect": [ + { + "category": "value", + "operation": "add", + "property": "training", + "value": 1 + }, + { + "category": "value", + "operation": "add", + "property": "ability", + "value": 2 + }, + { + "category": "value", + "operation": "add", + "property": "health", + "value": 4 + }, + { + "category": "value", + "operation": "add", + "property": "mana", + "value": 2 + } + ] }, { - "description": "+1 point de statistique.\n+1 transformation par jour.\n+8 PV max.\n+4 mana max." + "description": "+1 point de statistique.\n+1 transformation par jour.\n+8 PV max.\n+4 mana max.", + "effect": [ + { + "category": "value", + "operation": "add", + "property": "training", + "value": 1 + }, + { + "category": "value", + "operation": "add", + "property": "transformation", + "value": 2 + }, + { + "category": "value", + "operation": "add", + "property": "health", + "value": 8 + }, + { + "category": "value", + "operation": "add", + "property": "mana", + "value": 4 + } + ] }, { - "description": "+2 points de statistiques.\n+7 PV max.\n+2 mana max." + "description": "+2 points de statistiques.\n+7 PV max.\n+2 mana max.", + "effect": [ + { + "category": "value", + "operation": "add", + "property": "training", + "value": 2 + }, + { + "category": "value", + "operation": "add", + "property": "health", + "value": 7 + }, + { + "category": "value", + "operation": "add", + "property": "mana", + "value": 2 + } + ] } ], "6": [ @@ -1133,8 +1311,8 @@ "features": [ { "category": "value", - "type": "set", - "property": "defense.hardcap", + "operation": "set", + "property": "defense/hardcap", "value": 3 } ] @@ -1161,13 +1339,13 @@ "features": [ { "category": "value", - "type": "set", - "property": "defense.hardcap", + "operation": "set", + "property": "defense/hardcap", "value": 6 }, { "category": "value", - "type": "set", + "operation": "set", "property": "speed", "value": false } @@ -1194,20 +1372,20 @@ "features": [ { "category": "value", - "type": "set", - "property": "defense.hardcap", + "operation": "set", + "property": "defense/hardcap", "value": 9999 }, { "category": "value", - "type": "set", + "operation": "set", "property": "speed", "value": 0 }, { "category": "value", - "type": "add", - "property": "mastery.strength", + "operation": "add", + "property": "mastery/strength", "value": 1 } ] @@ -1232,20 +1410,26 @@ "features": [ { "category": "value", - "type": "set", + "operation": "set", "property": "speed", "value": 3 }, { "category": "value", - "type": "add", - "property": "mastery.armor", + "operation": "add", + "property": "mastery/armor", "value": 1 }, { "category": "value", - "type": "add", - "property": "defense.activeparry", + "operation": "add", + "property": "defense/activeparry", + "value": 1 + }, + { + "category": "value", + "operation": "add", + "property": "modifier/strength", "value": 1 } ] @@ -1270,26 +1454,26 @@ "features": [ { "category": "value", - "type": "set", + "operation": "set", "property": "speed", "value": 6 }, { "category": "value", - "type": "add", - "property": "mastery.strength", + "operation": "add", + "property": "mastery/strength", "value": 1 }, { "category": "value", - "type": "add", - "property": "defense.activeparry", + "operation": "add", + "property": "defense/activeparry", "value": 1 }, { "category": "value", - "type": "add", - "property": "defense.passiveparry", + "operation": "add", + "property": "defense/passiveparry", "value": 1 } ] @@ -1306,8 +1490,8 @@ "features": [ { "category": "value", - "type": "add", - "property": "mastery.strength", + "operation": "add", + "property": "mastery/strength", "value": 1 } ] @@ -1336,8 +1520,8 @@ "features": [ { "category": "value", - "type": "add", - "property": "mastery.shield", + "operation": "add", + "property": "mastery/shield", "value": 1 } ] @@ -1354,8 +1538,14 @@ "features": [ { "category": "value", - "type": "add", - "property": "mastery.strength", + "operation": "add", + "property": "mastery/strength", + "value": 1 + }, + { + "category": "value", + "operation": "add", + "property": "modifier/strength", "value": 1 } ] @@ -1371,6 +1561,12 @@ { "category": "misc", "text": "En infligeant des dégâts critique, vous pouvez choisir d'ignorer l'armure adverse." + }, + { + "category": "value", + "operation": "add", + "property": "modifier/strength", + "value": 1 } ] }, @@ -1384,8 +1580,14 @@ "features": [ { "category": "value", - "type": "add", - "property": "mastery.armor", + "operation": "add", + "property": "mastery/armor", + "value": 1 + }, + { + "category": "value", + "operation": "add", + "property": "modifier/strength", "value": 1 } ] @@ -1416,8 +1618,8 @@ "features": [ { "category": "value", - "type": "add", - "property": "mastery.strength", + "operation": "add", + "property": "mastery/strength", "value": 1 } ] @@ -1432,8 +1634,8 @@ "features": [ { "category": "value", - "type": "add", - "property": "mastery.armor", + "operation": "add", + "property": "mastery/armor", "value": 1 } ] @@ -1450,8 +1652,8 @@ "features": [ { "category": "value", - "type": "add", - "property": "mastery.armor", + "operation": "add", + "property": "mastery/armor", "value": 1 } ] @@ -1467,8 +1669,8 @@ "features": [ { "category": "value", - "type": "add", - "property": "mastery.armor", + "operation": "add", + "property": "mastery/armor", "value": 1 } ] @@ -1500,6 +1702,12 @@ { "category": "misc", "text": "Au prix d'un point de [[1. Règles/99. Annexes/3. Fatigue et repos#Fatigue temporaire|fatigue temporaire]], durant votre tour, les dégâts que vous infligerez avec une [[1. Règles/99. Annexes/4. Équipement#Les armes|arme standard]], [[1. Règles/99. Annexes/4. Équipement#Les armes lourdes|lourdes]] ou [[1. Règles/99. Annexes/4. Équipement#Les armes à deux mains|à deux mains]] vous permet de lancer un second dé de dégâts de votre arme. *Ce dé peut être doublé en cas de dégâts critique.*" + }, + { + "category": "value", + "operation": "add", + "property": "modifier/strength", + "value": 1 } ] }, @@ -1514,6 +1722,12 @@ { "category": "misc", "text": "Après avoir pris un adversaire en tenaille, si un allié parvient à le toucher, vous obtenez également un [[1. Règles/1. Introduction/2. Glossaire#Avantage et désavantage|avantage]] sur votre **première** attaque contre cet adversaire." + }, + { + "category": "value", + "operation": "add", + "property": "modifier/strength", + "value": 1 } ] }, @@ -1527,8 +1741,14 @@ "features": [ { "category": "value", - "type": "add", - "property": "mastery.shield", + "operation": "add", + "property": "mastery/shield", + "value": 1 + }, + { + "category": "value", + "operation": "add", + "property": "modifier/strength", "value": 1 } ] @@ -1634,6 +1854,12 @@ { "category": "misc", "text": "Vous êtes capable de tenir une [[1. Règles/99. Annexes/4. Équipement#Les armes à deux mains|arme à deux mains]] dans une seule main. Vous ne pouvez cependant pas tenir d'arme dans votre autre main, *même en ayant progressé dans l'[[1. Règles/99. Annexes/1. Les évolutions de valeur.canvas#Les armes multiples|arbre des armes multiples]]*." + }, + { + "category": "value", + "operation": "add", + "property": "modifier/strength", + "value": 1 } ] }, @@ -1648,6 +1874,12 @@ { "category": "misc", "text": "Au prix d'un point de [[1. Règles/99. Annexes/3. Fatigue et repos#Fatigue temporaire|fatigue temporaire]], durant tout un tour, faire une attaque ne demande que 1 point d'action." + }, + { + "category": "value", + "operation": "add", + "property": "modifier/strength", + "value": 1 } ] }, @@ -1663,6 +1895,12 @@ "category": "action", "text": "Vous pouvez frapper, puis vous [[1. Règles/3. Le combat/2. Actions en combat#S'interposer|interposer]] en 3 points d'action.", "cost": 3 + }, + { + "category": "value", + "operation": "add", + "property": "modifier/strength", + "value": 1 } ] } @@ -1723,7 +1961,14 @@ "category": "reaction" } ], - "features": [] + "features": [ + { + "category": "value", + "operation": "add", + "property": "modifier/strength", + "value": 1 + } + ] } ], "15": [ @@ -1734,7 +1979,14 @@ "disposable": false } ], - "features": [] + "features": [ + { + "category": "value", + "operation": "add", + "property": "modifier/strength", + "value": 1 + } + ] }, { "description": [ @@ -1743,7 +1995,14 @@ "disposable": false } ], - "features": [] + "features": [ + { + "category": "value", + "operation": "add", + "property": "modifier/strength", + "value": 1 + } + ] }, { "description": [ @@ -1752,7 +2011,14 @@ "disposable": false } ], - "features": [] + "features": [ + { + "category": "value", + "operation": "add", + "property": "modifier/strength", + "value": 1 + } + ] } ] }, @@ -2996,7 +3262,7 @@ "features": [ { "category": "asset", - "type": "add", + "operation": "add", "kind": "spells", "asset": "special-1" } @@ -5056,5 +5322,63 @@ ], "id": "special-1" } + ], + "aspects": [ + { + "name": "Digride", + "description": "", + "stat": "dexterity", + "alignment": { "loyalty": "loyal", "kindness": "evil" }, + "magic": true, + "difficulty": 10, + "physic": { "min": 12, "max": 22 }, + "mental": { "min": 8, "max": 15 }, + "personality": { "min": 12, "max": 20 }, + "options": [ + + ] + }, + { + "name": "Akkatom", + "description": "", + "stat": "strength", + "alignment": { "loyalty": "loyal", "kindness": "good" }, + "magic": true, + "difficulty": 9, + "physic": { "min": 18, "max": 25 }, + "mental": { "min": 8, "max": 12 }, + "personality": { "min": 8, "max": 12 }, + "options": [ + + ] + }, + { + "name": "Nolcalir", + "description": "", + "stat": "intelligence", + "alignment": { "loyalty": "loyal", "kindness": "neutral" }, + "magic": true, + "difficulty": 9, + "physic": { "min": 8, "max": 20 }, + "mental": { "min": 8, "max": 20 }, + "personality": { "min": 5, "max": 18 }, + "options": [ + + ] + }, + { + "name": "Brukaur", + "description": "", + "stat": "constitution", + "alignment": { "loyalty": "chaotic", "kindness": "neutral" }, + "magic": false, + "difficulty": 9, + "physic": { "min": 18, "max": 25 }, + "mental": { "min": 3, "max": 13 }, + "personality": { "min": 8, "max": 15 }, + "options": [ + + ] + } ] } \ No newline at end of file diff --git a/shared/character.util.ts b/shared/character.util.ts index adffc22..30bd6e7 100644 --- a/shared/character.util.ts +++ b/shared/character.util.ts @@ -1,10 +1,11 @@ -import type { Ability, Character, CharacterConfig, CompiledCharacter, DoubleIndex, Feature, FeatureItem, Level, MainStat, SpellElement, SpellType, TrainingLevel } from "~/types/character"; +import type { Ability, Alignment, Character, CharacterConfig, CompiledCharacter, DoubleIndex, Feature, FeatureItem, Level, MainStat, SpellElement, SpellType, TrainingLevel } from "~/types/character"; import { z } from "zod/v4"; import characterConfig from './character-config.json'; -import { button, loading } from "./proses"; +import { button, fakeA, loading } from "./proses"; import { div, dom, icon, text } from "./dom.util"; import { popper } from "./floating.util"; import { clamp } from "./general.util"; +import markdownUtil from "./markdown.util"; const config = characterConfig as CharacterConfig; @@ -25,8 +26,8 @@ export const defaultCharacter: Character = { health: 0, mana: 0, - training: MAIN_STATS.reduce((p, v) => { p[v] = [[0, 0]]; return p; }, {} as Record[]>), - leveling: [[1, 0]], + training: MAIN_STATS.reduce((p, v) => { p[v] = { 0: 0 }; return p; }, {} as Record>>), + leveling: { 1: 0 }, abilities: {}, spells: [], modifiers: {}, @@ -35,6 +36,80 @@ export const defaultCharacter: Character = { owner: -1, visibility: "private", }; +const defaultCompiledCharacter: (character: Character) => CompiledCharacter = (character: Character) => ({ + id: character.id, + owner: character.owner, + username: character.username, + name: character.name, + health: 0, + mana: 0, + race: character.people!, + modifier: MAIN_STATS.reduce((p, v) => { p[v] = 0; 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, + }, + bonus: {}, + resistance: {},//Object.fromEntries(MAIN_STATS.map(e => [e as MainStat, [0, 0]])) as Record, + initiative: 0, + aspect: "", + notes: character.notes ?? "", +}); export const mainStatTexts: Record = { "strength": "Force", @@ -58,6 +133,18 @@ export const elementTexts: Record psyche: { class: 'text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-purple bg-light-purple dark:bg-dark-purple', text: 'Psy' }, }; +export function alignmentToString(alignment: Alignment): string +{ + switch(alignment.loyalty) + { + case 'chaotic': + return alignment.kindness === 'evil' ? 'Chaotique mauvais' : alignment.kindness === 'neutral' ? 'Chaotique neutre' : 'Chaotique bon'; + case 'loyal': + return alignment.kindness === 'evil' ? 'Loyal mauvais' : alignment.kindness === 'neutral' ? 'Loyal neutre' : 'Loyal bon'; + case 'neutral': + return alignment.kindness === 'evil' ? 'Neutre mauvais' : alignment.kindness === 'neutral' ? 'Neutre' : 'Neutre bon'; + } +} export const spellTypeTexts: Record = { "instinct": "Instinct", "knowledge": "Savoir", "precision": "Précision", "arts": "Oeuvres" }; export const CharacterValidation = z.object({ @@ -69,20 +156,12 @@ export const CharacterValidation = z.object({ notes: z.string().nullable().optional(), health: z.number().default(0), mana: z.number().default(0), - training: z.object(MAIN_STATS.reduce((p, v) => { - p[v] = z.array(z.tuple([z.number().min(0).max(15), z.number()])); - return p; - }, {} as Record>>)), - leveling: z.array(z.tuple([z.number().min(1).max(20), z.number()])), - abilities: z.object(ABILITIES.reduce((p, v) => { - p[v] = z.tuple([z.number(), z.number()]); - return p; - }, {} as Record>)).partial(), + training: z.record(z.enum(MAIN_STATS), z.record(z.enum(TRAINING_LEVELS.map(String)), z.number().optional())), + leveling: z.record(z.enum(LEVELS.map(String)), z.number().optional()), + abilities: z.record(z.enum(ABILITIES), z.number().optional()), spells: z.string().array(), - modifiers: z.object(MAIN_STATS.reduce((p, v) => { - p[v] = z.number(); - return p; - }, {} as Record)).partial(), + modifiers: z.record(z.enum(MAIN_STATS), z.number().optional()), + choices: z.record(z.string(), z.array(z.number())), owner: z.number(), username: z.string().optional(), visibility: z.enum(["public", "private"]), @@ -97,13 +176,15 @@ const stepTexts: Record = { 4: 'Déterminez l\'Aspect qui vous corresponds et benéficiez de puissants bonus.' }; -type PropertySum = { list: Array, value: number, _dirty: boolean }; +type Property = { value: number | string, operation: "set" | "add" }; +type PropertySum = { list: Array, value: number, _dirty: boolean }; export class CharacterBuilder { private _container: HTMLDivElement; private _content?: HTMLDivElement; private _stepsHeader: HTMLDivElement[] = []; private _stepsContent: BuilderTab[] = []; + private _helperText!: Text; private id?: string; private _character!: Character; @@ -126,18 +207,20 @@ export class CharacterBuilder document.title = `d[any] - Edition de ${character.name ?? 'nouveau personnage'}`; - if(character.people) + if(character.people !== undefined) { const people = config.peoples[character.people]!; - character.leveling.forEach(e => { - const feature = people.options[e[0]][e[1]]!; + this._result = defaultCompiledCharacter(this._character); + + Object.entries(character.leveling).forEach(e => { + const feature = people.options[parseInt(e[0]) as Level][e[1]]!; feature.effect.map(e => this.apply(e)); }); MAIN_STATS.forEach(stat => { - character.training[stat].forEach(option => { - config.training[stat][option[0]][option[1]]!.features?.forEach(this.apply.bind(this)); + Object.entries(character.training[stat]).forEach(option => { + config.training[stat][parseInt(option[0]) as TrainingLevel][option[1]]!.features?.forEach(this.apply.bind(this)); }) }); } @@ -152,6 +235,8 @@ export class CharacterBuilder { this._character = Object.assign({}, defaultCharacter); + this._result = defaultCompiledCharacter(this._character); + document.title = `d[any] - Edition de nouveau personnage`; this.render(); @@ -184,17 +269,21 @@ export class CharacterBuilder this._stepsContent = [ new PeoplePicker(this), new LevelPicker(this), + new TrainingPicker(this), + new AbilityPicker(this), + new AspectPicker(this), ]; + this._helperText = text("Choisissez un peuple afin de définir la progression de votre personnage au fil des niveaux.") this._content = div('flex-1 outline-none max-w-full w-full overflow-y-auto'); this._container.appendChild(div('flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden', [ div("flex w-full flex-row gap-4 items-center justify-between px-4 bg-light-0 dark:bg-dark-0 z-20", [ - div(), div("flex w-full flex-row gap-4 items-center justify-center relative", this._stepsHeader), div(undefined, [popper(icon("radix-icons:question-mark-circled", { height: 20, width: 20 }), { + div(), div("flex w-full flex-row gap-4 items-center justify-center relative", this._stepsHeader), div(undefined, [ popper(icon("radix-icons:question-mark-circled", { height: 20, width: 20 }), { arrow: true, offset: 8, - content: [ text("Choisissez un peuple afin de définir la progression de votre personnage au fil des niveaux.") ], + content: [ this._helperText ], placement: "bottom-end", class: "max-w-96 fixed hidden TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50" - })]), + }) ]), ]), this._content, ])); @@ -212,13 +301,15 @@ export class CharacterBuilder this._stepsContent[step]!.update(); this._content?.replaceChildren(...this._stepsContent[step]!.dom); + + this._helperText.textContent = stepTexts[step]!; } async save(leave: boolean = true) { if(this.id === 'new') { //@ts-ignore - this.id = this._character.id = await useRequestFetch()(`/api/character`, { + this.id = this._character.id = this._result.id = await useRequestFetch()(`/api/character`, { method: 'post', body: this._character, onResponseError: (e) => { @@ -232,7 +323,7 @@ export class CharacterBuilder else { //@ts-ignore - await useRequestFetch()(`/api/character/${id}`, { + await useRequestFetch()(`/api/character/${this._character.id}`, { method: 'post', body: this._character, onResponseError: (e) => { @@ -279,25 +370,37 @@ export class CharacterBuilder let sum = 0; for(let i = 0; i < buffer.list.length; i++) { - if(typeof buffer.list[i] === 'string') + if(typeof buffer.list[i]!.value === 'string') { - if(this._buffer[buffer.list[i]!]!._dirty) + if(this._buffer[buffer.list[i]!.value]!._dirty) { //Put it back in queue since its dependencies haven't been resolved yet queue.push(property); return; } else - sum += this._buffer[buffer.list[i]!]!.value; + { + if(buffer.list[i]?.operation === 'add') + sum += this._buffer[buffer.list[i]!.value]!.value; + else if(buffer.list[i]?.operation === 'set') + sum = this._buffer[buffer.list[i]!.value]!.value; + } } else - sum += buffer.list[i] as number; + { + if(buffer.list[i]?.operation === 'add') + sum += buffer.list[i]!.value as number; + else if(buffer.list[i]?.operation === 'set') + sum = buffer.list[i]!.value as number; + } } const path = property.split("/"); - const object = path.slice(0, -1).reduce((p, v) => p[v], this._result as any); + const object = path.length === 1 ? this._result : path.slice(0, -1).reduce((p, v) => p[v], this._result as any); + + if(object.hasOwnProperty(path.slice(-1)[0]!)) + object[path.slice(-1)[0]!] = sum; - object[path.slice(-1)[0]!] = sum; this._buffer[property]!.value = sum; this._buffer[property]!._dirty = false; } @@ -309,15 +412,15 @@ export class CharacterBuilder if(this._character.leveling) //Invalidate higher levels { - for(let level = 20; level > this._character.level; level--) + for(let _level = 20; _level > this._character.level; _level--) { - const index = this._character.leveling.findIndex(e => e[0] == level); - if(index !== -1) + const level = _level as Level; + if(this._character.leveling.hasOwnProperty(level)) { const option = this._character.leveling[level]!; - this._character.leveling.splice(index, 1); + delete this._character.leveling[level]; - this.remove(config.peoples[this._character.people!]!.options[option[0]][option[1]]!); + this.remove(config.peoples[this._character.people!]!.options[level][option]); } } } @@ -329,7 +432,7 @@ export class CharacterBuilder if(this._character.leveling === undefined) //Add level 1 if missing { - this._character.leveling = [[1, 0]]; + this._character.leveling = { 1: 0 }; this.add(config.peoples[this._character.people!]!.options[1][0]!); } @@ -338,43 +441,80 @@ export class CharacterBuilder for(let i = 1; i < level; i++) //Check previous levels as a requirement { - if(!this._character.leveling.some(e => e[0] == i)) + if(!this._character.leveling.hasOwnProperty(i)) return; } - const option = this._character.leveling.find(e => e[0] == level); - if(option && option[1] !== choice) //If the given level is already selected, switch to the new choice + if(this._character.leveling.hasOwnProperty(level) && this._character.leveling[level] !== choice) //If the given level is already selected, switch to the new choice { - this._character.leveling.splice(this._character.leveling.findIndex(e => e[0] == level), 1, [level, choice]); + this.remove(config.peoples[this._character.people!]!.options[level][this._character.leveling[level]!]); + this.add(config.peoples[this._character.people!]!.options[level][choice]); - this.remove(config.peoples[this._character.people!]!.options[option[0]][option[1]]!); - this.add(config.peoples[this._character.people!]!.options[level][choice]!); + this._character.leveling[level] = choice; } - else if(!option) + else if(!this._character.leveling.hasOwnProperty(level)) { - this._character.leveling.push([level, choice]); + this._character.leveling[level] = choice; this.add(config.peoples[this._character.people!]!.options[level][choice]!); } } - toggleTrainingOption(stat: MainStat, level: TrainingLevel, option: number) + toggleTrainingOption(stat: MainStat, level: TrainingLevel, choice: number) { + if(level == 0) //Cannot remove the initial level + return; - } - private add(feature: Feature) - { - feature.effect.forEach(this.apply.bind(this)); - } - private remove(feature: Feature) - { + for(let i = 1; i < level; i++) //Check previous levels as a requirement + { + if(!this._character.training[stat].hasOwnProperty(i as TrainingLevel)) + return; + } + if(this._character.training[stat].hasOwnProperty(level)) + { + if(this._character.training[stat][level] === choice) + { + for(let i = 15; i >= level; i --) //Invalidate higher levels + { + if(this._character.training[stat].hasOwnProperty(i)) + { + config.training[stat][i as TrainingLevel][this._character.training[stat][i as TrainingLevel]!]?.features?.forEach(this.undo.bind(this)); + delete this._character.training[stat][i as TrainingLevel]; + } + } + } + else + { + config.training[stat][level][this._character.training[stat][level]!]?.features?.forEach(this.undo.bind(this)); + this._character.training[stat][level] = choice; + config.training[stat][level][choice]?.features?.forEach(this.apply.bind(this)); + } + } + else + { + this._character.training[stat][level] = choice; + config.training[stat][level][choice]?.features?.forEach(this.apply.bind(this)); + } } - private apply(feature: FeatureItem) + private add(feature?: Feature) { + feature?.effect.forEach(this.apply.bind(this)); + } + private remove(feature?: Feature) + { + feature?.effect.forEach(this.undo.bind(this)); + } + private apply(feature?: FeatureItem) + { + if(!feature) + return; + switch(feature.category) { case "feature": - this._result.features[feature.kind].push(feature.text); + this._result.features[feature.kind] ??= []; + + this._result.features[feature.kind]!.push(feature.text); return; case "list": @@ -387,10 +527,7 @@ export class CharacterBuilder case "value": this._buffer[feature.property] ??= { list: [], value: 0, _dirty: true }; - if(feature.operation === 'add') - this._buffer[feature.property]!.list.push(feature.value); - else if(feature.operation === 'set') - this._buffer[feature.property]!.list = [feature.value]; + this._buffer[feature.property]!.list.push({ operation: feature.operation, value: feature.value }); this._buffer[feature.property]!._dirty = true; @@ -399,6 +536,42 @@ export class CharacterBuilder const choice = this._character.choices[feature.id]!; choice.forEach(e => this.apply(feature.options[e]!)); + return; + default: + return; + } + } + private undo(feature?: FeatureItem) + { + if(!feature) + return; + + switch(feature.category) + { + case "feature": + this._result.features[feature.kind] = this._result.features[feature.kind]!.filter(e => e !== feature.text); + + return; + case "list": + if(feature.action === 'remove' && !this._result[feature.list].includes(feature.item)) + this._result[feature.list].push(feature.item); + else + this._result[feature.list] = this._result[feature.list].filter(e => e !== feature.item); + + return; + case "value": + this._buffer[feature.property] ??= { list: [], value: 0, _dirty: true }; + + this._buffer[feature.property]!.list.splice(this._buffer[feature.property]!.list.findIndex(e => e.operation === feature.operation && e.value === feature.value), 1); + + this._buffer[feature.property]!._dirty = true; + + return; + case "choice": + const choice = this._character.choices[feature.id]!; + choice.forEach(e => this.undo(feature.options[e]!)); + delete this._character.choices[feature.id]; + return; default: return; @@ -439,7 +612,6 @@ class PeoplePicker implements BuilderTab data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 relative py-[2px]`, attributes: { "data-state": "unckecked" }, listeners: { click: (e: Event) => { this._builder.character.visibility = this._builder.character.visibility === "private" ? "public" : "private"; - console.log(this._builder.character.visibility); this._visibilityInput.setAttribute('data-state', this._builder.character.visibility === "private" ? "checked" : "unchecked"); } }}, [ div('block w-[18px] h-[18px] translate-x-[2px] will-change-transform transition-transform bg-light-50 dark:bg-dark-50 group-data-[state=checked]:translate-x-[26px] group-data-[disabled]:bg-light-30 dark:group-data-[disabled]:bg-dark-30 group-data-[disabled]:border-light-30 dark:group-data-[disabled]:border-dark-30') ]); @@ -533,9 +705,10 @@ class LevelPicker implements BuilderTab this._healthText = text("0"), this._manaText = text("0"); this._options = Object.entries(config.peoples[this._builder.character.people!]!.options).map( - (level) => [ div("w-full h-px", [div("border-t border-dashed border-light-50 dark:border-dark-50 w-full"), div("sticky top-0", [ text(level[0]) ])]), - div("flex flex-row gap-4 justify-center", level[1].map((option, j) => dom("div", { class: ["flex border border-light-50 dark:border-dark-50 px-4 py-2 w-[400px]", { 'hover:border-light-70 dark:hover:border-dark-70 cursor-pointer': (level[0] as any as Level) <= this._builder.character.level, '!border-accent-blue bg-accent-blue bg-opacity-20': this._builder.character.leveling?.some(e => e[0] == (level[0] as any as Level) && e[1] === j) ?? false }], listeners: { click: e => { - this._builder.toggleLevelOption(level[0] as any as Level, j); + (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 left-4" }, [ text(level[0]) ])]), + div("flex flex-row gap-4 justify-center", level[1].map((option, j) => dom("div", { class: ["flex border border-light-50 dark:border-dark-50 px-4 py-2 w-[400px]", { 'hover:border-light-70 dark:hover:border-dark-70 cursor-pointer': (level[0] as any as Level) <= this._builder.character.level, '!border-accent-blue bg-accent-blue bg-opacity-20': this._builder.character.leveling[level[0] as any as Level] === j }], listeners: { click: e => { + this._builder.toggleLevelOption(parseInt(level[0]) as Level, j); + this.update(); }}}, [ dom('span', { class: "text-wrap whitespace-pre", text: option.description }) ]))) ]); @@ -556,7 +729,7 @@ class LevelPicker implements BuilderTab dom("span", { text: "Mana" }), this._manaText, ]), - button(text('Suivant'), () => this._builder.display(1), 'h-[35px] px-[15px]'), + button(text('Suivant'), () => this._builder.display(2), 'h-[35px] px-[15px]'), ]), div('flex flex-col flex-1 gap-4 mx-8 my-4', this._options.flatMap(e => [...e]))]; } update() @@ -564,7 +737,7 @@ class LevelPicker implements BuilderTab const values = this._builder.values; this._levelInput.value = this._builder.character.level.toString(); - this._pointsInput.value = (this._builder.character.level - this._builder.character.leveling.length).toString(); + this._pointsInput.value = (this._builder.character.level - Object.keys(this._builder.character.leveling).length).toString(); this._healthText.textContent = values.health?.toString() ?? '0'; this._manaText.textContent = values.mana?.toString() ?? '0'; @@ -574,18 +747,358 @@ class LevelPicker implements BuilderTab { this._builder.updateLevel(this._builder.character.level as Level); + this._pointsInput.value = (this._builder.character.level - Object.keys(this._builder.character.leveling).length).toString(); this._options.forEach((e, i) => { e[0]?.classList.toggle("opacity-30", ((i + 1) as Level) > this._builder.character.level); e[1]?.classList.toggle("opacity-30", ((i + 1) as Level) > this._builder.character.level); e[1]?.childNodes.forEach((option, j) => { 'hover:border-light-70 dark:hover:border-dark-70 cursor-pointer'.split(" ").forEach(_e => (option as HTMLDivElement).classList.toggle(_e, ((i + 1) as Level) <= this._builder.character.level)); - '!border-accent-blue bg-accent-blue bg-opacity-20'.split(" ").forEach(_e => (option as HTMLDivElement).classList.toggle(_e, this._builder.character.leveling?.some(e => e[0] == ((i + 1) as Level) && e[1] === j) ?? false)); + '!border-accent-blue bg-accent-blue bg-opacity-20'.split(" ").forEach(_e => (option as HTMLDivElement).classList.toggle(_e, this._builder.character.leveling[((i + 1) as Level)] === j)); }) }); } validate(): boolean { - return this._builder.character.people !== undefined; + return this._builder.character.level - Object.keys(this._builder.character.leveling).length >= 0; + } + get dom() + { + return this._content; + } +} +class TrainingPicker implements BuilderTab +{ + private _builder: CharacterBuilder; + private _content: Array; + + private _pointsInput: HTMLInputElement; + private _healthText: Text; + private _manaText: Text; + private _options: Record; + + private _tab: number = 0; + private _statIndicator: HTMLSpanElement; + private _statContainer: HTMLDivElement; + + constructor(builder: CharacterBuilder) + { + 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) => 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.toggleTrainingOption(stat, parseInt(level[0]) as TrainingLevel, j); + this.update(); + }}}, [ markdownUtil(option.description.map(e => e.text).join('\n'), undefined, { tags: { a: fakeA } }) ]))) + ]) + } + this._builder = builder; + + this._pointsInput = dom("input", { class: `w-14 mx-4 text-light-70 dark:text-dark-70 tabular-nums bg-light-10 dark:bg-dark-10 appearance-none outline-none ps-3 pe-1 py-1 focus:shadow-raw transition-[box-shadow] border bg-light-20 bg-dark-20 border-light-20 dark:border-dark-20`, attributes: { type: "number", disabled: true }}); + this._healthText = text("0"), this._manaText = text("0"); + + this._options = MAIN_STATS.reduce((p, v) => { p[v] = statRenderBlock(v); return p; }, {} as Record); + + this._statIndicator = dom('span', { class: 'rounded-full w-3 h-3 bg-accent-blue absolute transition-[left] after:content-[attr(data-text)] after:absolute after:-translate-x-1/2 after:top-4 after:p-px after:bg-light-0 dark:after:bg-dark-0 after:text-center' }); + this._statContainer = div('relative select-none transition-[left] flex flex-1 flex-row max-w-full', Object.values(this._options).map(e => div('flex flex-shrink-0 flex-col gap-4 relative w-full overflow-y-auto px-8', e.flatMap(_e => [..._e])))); + this._content = [ div("flex flex-1 gap-12 px-2 py-4 justify-center items-center sticky top-0 bg-light-0 dark:bg-dark-0 w-full z-10 min-h-20", [ + div('flex flex-shrink gap-3 items-center relative w-48 ms-12', [ + ...MAIN_STATS.map((e, i) => dom('span', { listeners: { click: () => this.switchTab(i) }, class: 'block w-2.5 h-2.5 m-px outline outline-1 outline-transparent hover:outline-light-70 dark:hover:outline-dark-70 rounded-full bg-light-40 dark:bg-dark-40 cursor-pointer' })), + this._statIndicator, + ]), + div('flex flex-1 gap-12 px-2 py-4 justify-center items-center sticky top-0 bg-light-0 dark:bg-dark-0 w-full z-10', [ + dom("label", { class: "flex justify-center items-center my-2" }, [ + dom("span", { class: "md:text-base text-sm", text: "Points restantes" }), + this._pointsInput, + ]), + div("flex justify-center items-center gap-2 my-2 md:text-base text-sm", [ + dom("span", { text: "Vie" }), + this._healthText, + ]), + div("flex justify-center items-center gap-2 my-2 md:text-base text-sm", [ + dom("span", { text: "Mana" }), + this._manaText, + ]), + button(text('Suivant'), () => this._builder.display(3), 'h-[35px] px-[15px]'), + ]), dom('span') + ]), div('flex flex-1 px-6 overflow-hidden max-w-full', [ this._statContainer ])]; + + this.switchTab(0); + } + switchTab(tab: number) + { + this._tab = tab; + + this._statIndicator.setAttribute('data-text', mainStatTexts[MAIN_STATS[tab] as MainStat]); + this._statIndicator.style.left = `${tab * 1.5}em`; + + this._statContainer.style.left = `-${tab * 100}%`; + } + update() + { + const values = this._builder.values; + const training = Object.values(this._builder.character.training).reduce((p, v) => p + Object.values(v).filter(e => e !== undefined).length, 0); + + this._pointsInput.value = ((values.training ?? 0) - training).toString(); + this._healthText.textContent = values.health?.toString() ?? '0'; + this._manaText.textContent = values.mana?.toString() ?? '0'; + + Object.keys(this._options).forEach(stat => { + const max = Object.keys(this._builder.character.training[stat as MainStat]).length; + this._options[stat as MainStat].forEach((e, i) => { + e[0]?.classList.toggle("opacity-30", (i as TrainingLevel) > max); + e[1]?.classList.toggle("opacity-30", (i as TrainingLevel) > max); + e[1]?.childNodes.forEach((option, j) => { + '!border-accent-blue bg-accent-blue bg-opacity-20'.split(" ").forEach(_e => (option as HTMLDivElement).classList.toggle(_e, i == 0 || (this._builder.character.training[stat as MainStat][i as TrainingLevel] === j))); + }) + }) + }); + } + validate(): boolean + { + const values = this._builder.values; + const training = Object.values(this._builder.character.training).reduce((p, v) => p + Object.values(v).filter(e => e !== undefined).length, 0); + + return (values.training ?? 0) - training >= 0; + } + get dom() + { + return this._content; + } +} +class AbilityPicker implements BuilderTab +{ + private _builder: CharacterBuilder; + private _content: Array; + + private _pointsInput: HTMLInputElement; + private _options: HTMLDivElement[]; + + private _tooltips: Text[] = []; + private _maxs: HTMLElement[] = []; + + constructor(builder: CharacterBuilder) + { + const numberInput = (value?: number, update?: (value: number) => number | undefined) => { + const input = dom("input", { class: `w-14 mx-4 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 appearance-none 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`, listeners: { + input: (e: Event) => { + input.value = (update && update(parseInt(input.value))?.toString()) ?? input.value; + }, + keydown: (e: KeyboardEvent) => { + let value = isNaN(parseInt(input.value)) ? '0' : input.value; + switch(e.key) + { + case "ArrowUp": + value = clamp(parseInt(value) + 1, 0, 99).toString(); + break; + case "ArrowDown": + value = clamp(parseInt(value) - 1, 0, 99).toString(); + break; + default: + break; + } + + if(input.value !== value) + { + input.value = (update && update(parseInt(value))?.toString()) ?? value; + } + } + }}); + + input.value = value?.toString() ?? "0"; + return input; + }; + function pushAndReturn(arr: Array, value: T): T + { + arr.push(value); + return value; + } + this._builder = builder; + + this._pointsInput = dom("input", { class: `w-14 mx-4 text-light-70 dark:text-dark-70 tabular-nums bg-light-10 dark:bg-dark-10 appearance-none outline-none ps-3 pe-1 py-1 focus:shadow-raw transition-[box-shadow] border bg-light-20 bg-dark-20 border-light-20 dark:border-dark-20`, attributes: { type: "number", disabled: true }}); + + this._options = ABILITIES.map((e, i) => div('flex flex-col border border-light-50 dark:border-dark-50 p-4 gap-2 w-[200px] relative', [ + div('flex justify-between', [ numberInput(this._builder.character.abilities[e], (value) => { + const values = this._builder.values; + const max = (values[`abilities/${e}/max`] ?? 0) + (values[`modifier/${config.abilities[e].max[0]}`] ?? 0) + (values[`modifier/${config.abilities[e].max[1]}`] ?? 0); + + this._builder.character.abilities[e] = clamp(value, 0, max); + Object.assign((this._options[i]?.lastElementChild as HTMLSpanElement | undefined)?.style ?? {}, { width: `${(max === 0 ? 0 : (this._builder.character.abilities[e] ?? 0) / max) * 100}%` }); + this._tooltips[i]!.textContent = `${mainStatTexts[config.abilities[e].max[0]]} (${values[`modifier/${config.abilities[e].max[0]}`] ?? 0}) + ${mainStatTexts[config.abilities[e].max[1]]} (${values[`modifier/${config.abilities[e].max[1]}`] ?? 0}) + ${values[`abilities/${e}/max`] ?? 0}`; + this._maxs[i]!.textContent = `/ ${max ?? 0}`; + + const abilities = Object.values(this._builder.character.abilities).reduce((p, v) => p + v, 0); + this._pointsInput.value = ((values.ability ?? 0) - abilities).toString(); + + return this._builder.character.abilities[e]; + }), popper(pushAndReturn(this._maxs, dom('span', { class: 'text-lg text-end cursor-pointer', text: '' })), { + arrow: true, + offset: 6, + placement: 'bottom-end', + class: 'max-w-96 fixed hidden TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50', + content: [ pushAndReturn(this._tooltips, text('')) ] + })]), + dom('span', { class: "text-xl text-center font-bold", text: config.abilities[e].name }), + dom('span', { class: "absolute -bottom-px -left-px h-[3px] bg-accent-blue" }), + ])); + + this._content = [ div("flex flex-1 gap-12 px-2 py-4 justify-center items-center sticky top-0 bg-light-0 dark:bg-dark-0 w-full z-10", [ + dom("label", { class: "flex justify-center items-center my-2" }, [ + dom("span", { class: "md:text-base text-sm", text: "Points restantes" }), + this._pointsInput, + ]), + button(text('Suivant'), () => this._builder.display(4), 'h-[35px] px-[15px]'), + ]), div('flex flex-row flex-wrap justify-center items-center flex-1 gap-12 mx-8 my-4 px-48', this._options)]; + } + update() + { + const values = this._builder.values; + const abilities = Object.values(this._builder.character.abilities).reduce((p, v) => p + v, 0); + + this._pointsInput.value = ((values.ability ?? 0) - abilities).toString(); + + ABILITIES.forEach((e, i) => { + const max = (values[`abilities/${e}/max`] ?? 0) + (values[`modifier/${config.abilities[e].max[0]}`] ?? 0) + (values[`modifier/${config.abilities[e].max[1]}`] ?? 0); + + Object.assign((this._options[i]?.lastElementChild as HTMLSpanElement | undefined)?.style ?? {}, { width: `${(max === 0 ? 0 : (this._builder.character.abilities[e] ?? 0) / max) * 100}%` }); + this._tooltips[i]!.textContent = `${mainStatTexts[config.abilities[e].max[0]]} (${values[`modifier/${config.abilities[e].max[0]}`] ?? 0}) + ${mainStatTexts[config.abilities[e].max[1]]} (${values[`modifier/${config.abilities[e].max[1]}`] ?? 0}) + ${values[`abilities/${e}/max`] ?? 0}`; + this._maxs[i]!.textContent = `/ ${max ?? 0}`; + + return this._builder.character.abilities[e]; + }) + } + validate(): boolean + { + const values = this._builder.values; + const abilities = Object.values(this._builder.character.abilities).reduce((p, v) => p + v, 0); + + return (values.ability ?? 0) - abilities >= 0; + } + get dom() + { + return this._content; + } +} +class AspectPicker implements BuilderTab +{ + private _builder: CharacterBuilder; + private _content: Array; + + private _physicInput: HTMLInputElement; + private _mentalInput: HTMLInputElement; + private _personalityInput: HTMLInputElement; + + private _filter: boolean = true; + + private _options: HTMLDivElement[]; + + constructor(builder: CharacterBuilder) + { + this._builder = builder; + + this._physicInput = dom("input", { class: `w-14 mx-4 text-light-70 dark:text-dark-70 tabular-nums bg-light-10 dark:bg-dark-10 appearance-none outline-none ps-3 pe-1 py-1 focus:shadow-raw transition-[box-shadow] border bg-light-20 bg-dark-20 border-light-20 dark:border-dark-20`, attributes: { type: "number", disabled: true }}); + this._mentalInput = dom("input", { class: `w-14 mx-4 text-light-70 dark:text-dark-70 tabular-nums bg-light-10 dark:bg-dark-10 appearance-none outline-none ps-3 pe-1 py-1 focus:shadow-raw transition-[box-shadow] border bg-light-20 bg-dark-20 border-light-20 dark:border-dark-20`, attributes: { type: "number", disabled: true }}); + this._personalityInput = dom("input", { class: `w-14 mx-4 text-light-70 dark:text-dark-70 tabular-nums bg-light-10 dark:bg-dark-10 appearance-none outline-none ps-3 pe-1 py-1 focus:shadow-raw transition-[box-shadow] border bg-light-20 bg-dark-20 border-light-20 dark:border-dark-20`, attributes: { type: "number", disabled: true }}); + + this._options = config.aspects.map((e, i) => dom('div', { attributes: { "data-aspect": i.toString() }, listeners: { click: () => { + this._builder.character.aspect = i; + this._options.forEach(_e => _e.setAttribute('data-state', 'inactive')); + this._options[i]?.setAttribute('data-state', 'active'); + }}, class: 'group flex flex-col w-[360px] border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 cursor-pointer' }, [ + div('bg-light-10 dark:bg-dark-10 border-b border-light-35 dark:border-dark-35 p-2 flex flex-col gap-2 group-data-[state=active]:bg-accent-blue group-data-[state=active]:bg-opacity-10', [ + div('flex flex-row gap-8 ps-4 items-center', [ + div("flex flex-1 flex-col gap-2 justify-center", [ div('text-lg font-bold', [ text(e.name) ]), dom('span', { class: 'border-b w-full border-light-50 dark:border-dark-50 group-data-[state=active]:border-b-[4px] group-data-[state=active]:border-accent-blue' }) ]), + div('rounded-full w-[96px] h-[96px] border border-light-50 dark:border-dark-50 bg-light-100 dark:bg-dark-100 !bg-opacity-10') + ]) + ]), + div('flex justify-stretch items-stretch py-2 px-4 gap-4', [ + div('flex flex-col flex-1 items-stretch gap-4', [ + div('flex flex-1 justify-between', [ text('Difficulté'), div('text-sm font-bold', [ text(e.difficulty.toString()) ]) ]), + div('flex flex-1 justify-between', [ text('Bonus'), div('text-sm font-bold', [ text(e.stat === 'special' ? 'Special' : mainStatTexts[e.stat]) ]) ]) + ]), + div('w-px h-full bg-light-50 dark:bg-dark-50'), + div('flex flex-col items-center justify-between py-2', [ + div('text-sm italic', [ text(alignmentToString(e.alignment)) ]), + div(['text-sm font-bold', { "text-light-purple dark:text-dark-purple italic": e.magic, "text-light-orange dark:text-dark-orange": !e.magic }], [ text(e.magic ? 'Magie autorisée' : 'Magie interdite') ]), + ]), + ]) + ])); + + const filterSwitch = dom("div", { class: `group mx-3 w-12 h-6 select-none transition-all border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 outline-none data-[state=checked]:bg-light-35 dark:data-[state=checked]:bg-dark-35 hover:border-light-50 dark:hover:border-dark-50 focus:shadow-raw focus:shadow-light-40 dark:focus:shadow-dark-40 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 relative py-[2px]`, attributes: { "data-state": this._filter ? "ckecked" : "unchecked" }, listeners: { + click: (e: Event) => { + this._filter = !this._filter; + filterSwitch.setAttribute('data-state', this._filter ? "ckecked" : "unchecked"); + this.update(); + } + }}, [ div('block w-[18px] h-[18px] translate-x-[2px] will-change-transform transition-transform bg-light-50 dark:bg-dark-50 group-data-[state=checked]:translate-x-[26px] group-data-[disabled]:bg-light-30 dark:group-data-[disabled]:bg-dark-30 group-data-[disabled]:border-light-30 dark:group-data-[disabled]:border-dark-30') ]); + + this._content = [ div("flex flex-1 gap-12 px-2 py-4 justify-center items-center sticky top-0 bg-light-0 dark:bg-dark-0 w-full z-10", [ + dom("label", { class: "flex justify-center items-center my-2" }, [ + dom("span", { class: "md:text-base text-sm", text: "Physique" }), + this._physicInput, + ]), + dom("label", { class: "flex justify-center items-center my-2" }, [ + dom("span", { class: "md:text-base text-sm", text: "Mental" }), + this._mentalInput, + ]), + dom("label", { class: "flex justify-center items-center my-2" }, [ + dom("span", { class: "md:text-base text-sm", text: "Caractère" }), + this._personalityInput, + ]), + dom("label", { class: "flex justify-center items-center my-2" }, [ + dom("span", { class: "md:text-base text-sm", text: "Filtrer ?" }), + filterSwitch, + ]), + button(text('Enregistrer'), () => this._builder.save(), 'h-[35px] px-[15px]'), + ]), div('flex flex-row flex-wrap justify-center items-center flex-1 gap-8 mx-8 my-4 px-8', this._options)]; + } + update() + { + const physic = Object.values(this._builder.character.training['strength']).length + Object.values(this._builder.character.training['dexterity']).length + Object.values(this._builder.character.training['constitution']).length; + const mental = Object.values(this._builder.character.training['intelligence']).length + Object.values(this._builder.character.training['curiosity']).length; + const personality = Object.values(this._builder.character.training['charisma']).length + Object.values(this._builder.character.training['psyche']).length; + + this._physicInput.value = physic.toString(); + this._mentalInput.value = mental.toString(); + this._personalityInput.value = personality.toString(); + + (this._content[1] as HTMLElement).replaceChildren(...this._options.filter(e => { + const index = parseInt(e.getAttribute('data-aspect')!); + const aspect = config.aspects[index]!; + + e.setAttribute('data-state', this._builder.character.aspect === index ? 'active' : 'inactive'); + + if(!this._filter) + return true; + + if(physic > aspect.physic.max || physic < aspect.physic.min) + return false; + if(mental > aspect.mental.max || mental < aspect.mental.min) + return false; + if(personality > aspect.personality.max || personality < aspect.personality.min) + return false; + + return true; + })); + } + validate(): boolean + { + const physic = Object.values(this._builder.character.training['strength']).length + Object.values(this._builder.character.training['dexterity']).length + Object.values(this._builder.character.training['constitution']).length; + const mental = Object.values(this._builder.character.training['intelligence']).length + Object.values(this._builder.character.training['curiosity']).length; + const personality = Object.values(this._builder.character.training['charisma']).length + Object.values(this._builder.character.training['psyche']).length; + + if(this._builder.character.aspect === undefined) + return false; + + const aspect = config.aspects[this._builder.character.aspect]! + + if(physic > aspect.physic.max || physic < aspect.physic.min) + return false; + if(mental > aspect.mental.max || mental < aspect.mental.min) + return false; + if(personality > aspect.personality.max || personality < aspect.personality.min) + return false; + + return true; } get dom() { diff --git a/shared/floating.util.ts b/shared/floating.util.ts index 1835528..c009ea6 100644 --- a/shared/floating.util.ts +++ b/shared/floating.util.ts @@ -46,7 +46,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H strategy: 'fixed', middleware: [ properties?.offset ? FloatingUI.offset(properties?.offset) : undefined, - FloatingUI.autoPlacement({ rootBoundary: rect }), + FloatingUI.shift({ rootBoundary: rect }), properties?.offset ? FloatingUI.shift({ padding: properties?.offset, rootBoundary: rect }) : undefined, properties?.offset && properties?.arrow ? FloatingUI.arrow({ element: arrow, padding: 8 }) : undefined, FloatingUI.hide({ rootBoundary: rect }), diff --git a/types/character.d.ts b/types/character.d.ts index 3e60415..c61c542 100644 --- a/types/character.d.ts +++ b/types/character.d.ts @@ -9,6 +9,7 @@ export type Category = typeof CATEGORIES[number]; export type SpellElement = typeof SPELL_ELEMENTS[number]; export type DoubleIndex = [T, number]; +export type Alignment = { loyalty: 'loyal' | 'neutral' | 'chaotic', kindness: 'good' | 'neutral' | 'evil' }; export type Character = { id: number; @@ -21,9 +22,9 @@ export type Character = { health: number; mana: number; - training: Record[]>; - leveling: DoubleIndex[]; - abilities: Partial>; //First is the ability, second is the max increment + training: Record>>; + leveling: Partial>; + abilities: Partial>; //First is the ability, second is the max increment spells: string[]; //Spell ID modifiers: Partial>; @@ -38,10 +39,11 @@ export type CharacterValues = { mana: number; }; export type CharacterConfig = { - peoples: Race[], + peoples: RaceConfig[], training: Record>; abilities: Record; spells: SpellConfig[]; + aspects: AspectConfig[]; }; export type SpellConfig = { id: string; @@ -59,11 +61,23 @@ export type AbilityConfig = { name: string; description: string; }; -export type Race = { +export type RaceConfig = { name: string; description: string; options: Record; }; +export type AspectConfig = { + name: string; + description: string; + stat: MainStat | 'special'; + alignment: Alignment; + magic: boolean; + difficulty: number; + physic: { min: number, max: number }; + mental: { min: number, max: number }; + personality: { min: number, max: number }; + options: FeatureEffect[]; +}; export type FeatureEffect = { category: "value"; @@ -169,7 +183,7 @@ export type CompiledCharacter = { modifier: Record; abilities: Partial>; level: number; - features: { [K in Extract["kind"]]: string[] }; + features: { [K in Extract["kind"]]?: string[] }; notes: string; }; \ No newline at end of file