New SQL tables structure

This commit is contained in:
Clément Pons 2025-04-30 17:44:54 +02:00
parent 871861e66e
commit df3577f673
25 changed files with 2755 additions and 358 deletions

BIN
db.sqlite

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +1,6 @@
import { relations } from 'drizzle-orm';
import { int, text, sqliteTable, type SQLiteTableExtraConfig, primaryKey, blob } from 'drizzle-orm/sqlite-core';
import { int, text, sqliteTable, primaryKey, blob } from 'drizzle-orm/sqlite-core';
import { ABILITIES, MAIN_STATS } from '../types/character';
export const usersTable = sqliteTable("users", {
id: int().primaryKey({ autoIncrement: true }),
@ -20,20 +21,12 @@ export const userSessionsTable = sqliteTable("user_sessions", {
id: int().notNull(),
user_id: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
timestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
}, (table): SQLiteTableExtraConfig => {
return {
pk: primaryKey({ columns: [table.id, table.user_id] }),
}
});
}, (table) => [primaryKey({ columns: [table.id, table.user_id] })]);
export const userPermissionsTable = sqliteTable("user_permissions", {
id: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
permission: text().notNull(),
}, (table): SQLiteTableExtraConfig => {
return {
pk: primaryKey({ columns: [table.id, table.permission] }),
}
});
}, (table) => [primaryKey({ columns: [table.id, table.permission] })]);
export const explorerContentTable = sqliteTable("explorer_content", {
path: text().primaryKey(),
@ -57,11 +50,47 @@ export const characterTable = sqliteTable("character", {
id: int().primaryKey({ autoIncrement: true }),
name: text().notNull(),
owner: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
progress: text({ mode: 'json' }).notNull(),
values: text({ mode: 'json' }).notNull().default({}),
people: int().notNull(),
level: int().notNull().default(1),
aspect: int(),
notes: text(),
health: int().notNull().default(0),
mana: int().notNull().default(0),
visibility: text({ enum: ['private', 'public'] }).notNull().default('private'),
thumbnail: blob(),
})
});
export const characterTrainingTable = sqliteTable("character_training", {
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
stat: text({ enum: MAIN_STATS }).notNull(),
level: int().notNull(),
choice: int().notNull(),
}, (table) => [primaryKey({ columns: [table.character, table.stat, table.level] })]);
export const characterLevelingTable = sqliteTable("character_leveling", {
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
level: int().notNull(),
choice: int().notNull(),
}, (table) => [primaryKey({ columns: [table.character, table.level] })]);
export const characterAbilitiesTable = sqliteTable("character_abilities", {
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
ability: text({ enum: ABILITIES }).notNull(),
value: int().notNull().default(0),
max: int().notNull().default(0),
}, (table) => [primaryKey({ columns: [table.character, table.ability] })]);
export const characterModifiersTable = sqliteTable("character_modifiers", {
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
modifier: text({ enum: MAIN_STATS }).notNull(),
value: int().notNull().default(0),
}, (table) => [primaryKey({ columns: [table.character, table.modifier] })]);
export const characterSpellsTable = sqliteTable("character_spell", {
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
value: text().notNull(),
}, (table) => [primaryKey({ columns: [table.character, table.value] })]);
export const usersRelation = relations(usersTable, ({ one, many }) => ({
data: one(usersDataTable, { fields: [usersTable.id], references: [usersDataTable.id], }),
@ -81,6 +110,27 @@ export const userPermissionsRelation = relations(userPermissionsTable, ({ one })
export const explorerContentRelation = relations(explorerContentTable, ({ one }) => ({
users: one(usersTable, { fields: [explorerContentTable.owner], references: [usersTable.id], }),
}));
export const characterRelation = relations(characterTable, ({ one }) => ({
users: one(usersTable, { fields: [characterTable.owner], references: [usersTable.id], }),
export const characterRelation = relations(characterTable, ({ one, many }) => ({
user: one(usersTable, { fields: [characterTable.owner], references: [usersTable.id], }),
training: many(characterTrainingTable),
levels: many(characterLevelingTable),
abilities: many(characterAbilitiesTable),
modifiers: many(characterModifiersTable),
spells: many(characterSpellsTable)
}));
export const characterTrainingRelation = relations(characterTrainingTable, ({ one }) => ({
character: one(characterTable, { fields: [characterTrainingTable.character], references: [characterTable.id] })
}));
export const characterLevelingRelation = relations(characterLevelingTable, ({ one }) => ({
character: one(characterTable, { fields: [characterLevelingTable.character], references: [characterTable.id] })
}));
export const characterAbilitiesRelation = relations(characterAbilitiesTable, ({ one }) => ({
character: one(characterTable, { fields: [characterAbilitiesTable.character], references: [characterTable.id] })
}));
export const characterModifierRelation = relations(characterModifiersTable, ({ one }) => ({
character: one(characterTable, { fields: [characterModifiersTable.character], references: [characterTable.id] })
}));
export const characterSpellsRelation = relations(characterSpellsTable, ({ one }) => ({
character: one(characterTable, { fields: [characterSpellsTable.character], references: [characterTable.id] })
}));

View File

@ -0,0 +1,47 @@
CREATE TABLE `character_abilities` (
`character` integer NOT NULL,
`ability` text NOT NULL,
`value` integer DEFAULT 0 NOT NULL,
PRIMARY KEY(`character`, `ability`),
FOREIGN KEY (`character`) REFERENCES `character`(`id`) ON UPDATE cascade ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `character_leveling` (
`character` integer NOT NULL,
`level` integer NOT NULL,
`choice` integer NOT NULL,
PRIMARY KEY(`character`, `level`),
FOREIGN KEY (`character`) REFERENCES `character`(`id`) ON UPDATE cascade ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `character_modifiers` (
`character` integer NOT NULL,
`modifier` text NOT NULL,
`value` integer DEFAULT 0 NOT NULL,
PRIMARY KEY(`character`, `modifier`),
FOREIGN KEY (`character`) REFERENCES `character`(`id`) ON UPDATE cascade ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `character_spell` (
`character` integer PRIMARY KEY NOT NULL,
`value` text NOT NULL,
FOREIGN KEY (`character`) REFERENCES `character`(`id`) ON UPDATE cascade ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `character_training` (
`character` integer NOT NULL,
`stat` text NOT NULL,
`level` integer NOT NULL,
`choice` integer NOT NULL,
PRIMARY KEY(`character`, `stat`, `level`),
FOREIGN KEY (`character`) REFERENCES `character`(`id`) ON UPDATE cascade ON DELETE cascade
);
--> statement-breakpoint
ALTER TABLE `character` ADD `people` integer NOT NULL;--> statement-breakpoint
ALTER TABLE `character` ADD `level` integer DEFAULT 1 NOT NULL;--> statement-breakpoint
ALTER TABLE `character` ADD `aspect` integer;--> statement-breakpoint
ALTER TABLE `character` ADD `notes` text;--> statement-breakpoint
ALTER TABLE `character` ADD `health` integer DEFAULT 0 NOT NULL;--> statement-breakpoint
ALTER TABLE `character` ADD `mana` integer DEFAULT 0 NOT NULL;--> statement-breakpoint
ALTER TABLE `character` DROP COLUMN `progress`;--> statement-breakpoint
ALTER TABLE `character` DROP COLUMN `values`;

View File

@ -0,0 +1 @@
ALTER TABLE `character_abilities` ADD `max` integer DEFAULT 0 NOT NULL;

View File

@ -0,0 +1,12 @@
PRAGMA foreign_keys=OFF;--> statement-breakpoint
CREATE TABLE `__new_character_spell` (
`character` integer NOT NULL,
`value` text NOT NULL,
PRIMARY KEY(`character`, `value`),
FOREIGN KEY (`character`) REFERENCES `character`(`id`) ON UPDATE cascade ON DELETE cascade
);
--> statement-breakpoint
INSERT INTO `__new_character_spell`("character", "value") SELECT "character", "value" FROM `character_spell`;--> statement-breakpoint
DROP TABLE `character_spell`;--> statement-breakpoint
ALTER TABLE `__new_character_spell` RENAME TO `character_spell`;--> statement-breakpoint
PRAGMA foreign_keys=ON;

View File

@ -0,0 +1,724 @@
{
"version": "6",
"dialect": "sqlite",
"id": "af3d9e4f-cea6-42fa-8f8b-d743d97b9c37",
"prevId": "bffde16c-d716-40ec-9d92-cb49814815d7",
"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
}
},
"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_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": true,
"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": {},
"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": {}
},
"explorer_content": {
"name": "explorer_content",
"columns": {
"path": {
"name": "path",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"content": {
"name": "content",
"type": "blob",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"navigable": {
"name": "navigable",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"private": {
"name": "private",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"order": {
"name": "order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"visit": {
"name": "visit",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"explorer_content_owner_users_id_fk": {
"name": "explorer_content_owner_users_id_fk",
"tableFrom": "explorer_content",
"tableTo": "users",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_permissions": {
"name": "user_permissions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"permission": {
"name": "permission",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_permissions_id_users_id_fk": {
"name": "user_permissions_id_users_id_fk",
"tableFrom": "user_permissions",
"tableTo": "users",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"user_permissions_id_permission_pk": {
"columns": [
"id",
"permission"
],
"name": "user_permissions_id_permission_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_sessions": {
"name": "user_sessions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_sessions_user_id_users_id_fk": {
"name": "user_sessions_user_id_users_id_fk",
"tableFrom": "user_sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"user_sessions_id_user_id_pk": {
"columns": [
"id",
"user_id"
],
"name": "user_sessions_id_user_id_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users_data": {
"name": "users_data",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"signin": {
"name": "signin",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"lastTimestamp": {
"name": "lastTimestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"logCount": {
"name": "logCount",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {},
"foreignKeys": {
"users_data_id_users_id_fk": {
"name": "users_data_id_users_id_fk",
"tableFrom": "users_data",
"tableTo": "users",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"hash": {
"name": "hash",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"state": {
"name": "state",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {
"users_username_unique": {
"name": "users_username_unique",
"columns": [
"username"
],
"isUnique": true
},
"users_email_unique": {
"name": "users_email_unique",
"columns": [
"email"
],
"isUnique": true
},
"users_hash_unique": {
"name": "users_hash_unique",
"columns": [
"hash"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@ -0,0 +1,732 @@
{
"version": "6",
"dialect": "sqlite",
"id": "e0aaebf1-54e4-4f61-804b-7cce23c88069",
"prevId": "af3d9e4f-cea6-42fa-8f8b-d743d97b9c37",
"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_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": true,
"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": {},
"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": {}
},
"explorer_content": {
"name": "explorer_content",
"columns": {
"path": {
"name": "path",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"content": {
"name": "content",
"type": "blob",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"navigable": {
"name": "navigable",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"private": {
"name": "private",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"order": {
"name": "order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"visit": {
"name": "visit",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"explorer_content_owner_users_id_fk": {
"name": "explorer_content_owner_users_id_fk",
"tableFrom": "explorer_content",
"tableTo": "users",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_permissions": {
"name": "user_permissions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"permission": {
"name": "permission",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_permissions_id_users_id_fk": {
"name": "user_permissions_id_users_id_fk",
"tableFrom": "user_permissions",
"tableTo": "users",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"user_permissions_id_permission_pk": {
"columns": [
"id",
"permission"
],
"name": "user_permissions_id_permission_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_sessions": {
"name": "user_sessions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_sessions_user_id_users_id_fk": {
"name": "user_sessions_user_id_users_id_fk",
"tableFrom": "user_sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"user_sessions_id_user_id_pk": {
"columns": [
"id",
"user_id"
],
"name": "user_sessions_id_user_id_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users_data": {
"name": "users_data",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"signin": {
"name": "signin",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"lastTimestamp": {
"name": "lastTimestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"logCount": {
"name": "logCount",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {},
"foreignKeys": {
"users_data_id_users_id_fk": {
"name": "users_data_id_users_id_fk",
"tableFrom": "users_data",
"tableTo": "users",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"hash": {
"name": "hash",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"state": {
"name": "state",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {
"users_username_unique": {
"name": "users_username_unique",
"columns": [
"username"
],
"isUnique": true
},
"users_email_unique": {
"name": "users_email_unique",
"columns": [
"email"
],
"isUnique": true
},
"users_hash_unique": {
"name": "users_hash_unique",
"columns": [
"hash"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@ -0,0 +1,740 @@
{
"version": "6",
"dialect": "sqlite",
"id": "cb7a2b9c-1392-4f23-9fc2-9ce8de2e0231",
"prevId": "e0aaebf1-54e4-4f61-804b-7cce23c88069",
"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_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": {}
},
"explorer_content": {
"name": "explorer_content",
"columns": {
"path": {
"name": "path",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"content": {
"name": "content",
"type": "blob",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"navigable": {
"name": "navigable",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"private": {
"name": "private",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"order": {
"name": "order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"visit": {
"name": "visit",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"explorer_content_owner_users_id_fk": {
"name": "explorer_content_owner_users_id_fk",
"tableFrom": "explorer_content",
"tableTo": "users",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_permissions": {
"name": "user_permissions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"permission": {
"name": "permission",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_permissions_id_users_id_fk": {
"name": "user_permissions_id_users_id_fk",
"tableFrom": "user_permissions",
"tableTo": "users",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"user_permissions_id_permission_pk": {
"columns": [
"id",
"permission"
],
"name": "user_permissions_id_permission_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_sessions": {
"name": "user_sessions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_sessions_user_id_users_id_fk": {
"name": "user_sessions_user_id_users_id_fk",
"tableFrom": "user_sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"user_sessions_id_user_id_pk": {
"columns": [
"id",
"user_id"
],
"name": "user_sessions_id_user_id_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users_data": {
"name": "users_data",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"signin": {
"name": "signin",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"lastTimestamp": {
"name": "lastTimestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"logCount": {
"name": "logCount",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {},
"foreignKeys": {
"users_data_id_users_id_fk": {
"name": "users_data_id_users_id_fk",
"tableFrom": "users_data",
"tableTo": "users",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"hash": {
"name": "hash",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"state": {
"name": "state",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {
"users_username_unique": {
"name": "users_username_unique",
"columns": [
"username"
],
"isUnique": true
},
"users_email_unique": {
"name": "users_email_unique",
"columns": [
"email"
],
"isUnique": true
},
"users_hash_unique": {
"name": "users_hash_unique",
"columns": [
"hash"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@ -71,6 +71,27 @@
"when": 1745920443528,
"tag": "0009_thin_omega_sentinel",
"breakpoints": true
},
{
"idx": 10,
"version": "6",
"when": 1746014143374,
"tag": "0010_bored_sabra",
"breakpoints": true
},
{
"idx": 11,
"version": "6",
"when": 1746017162319,
"tag": "0011_demonic_titania",
"breakpoints": true
},
{
"idx": 12,
"version": "6",
"when": 1746027790969,
"tag": "0012_graceful_energizer",
"breakpoints": true
}
]
}

View File

@ -14,9 +14,21 @@
<span class="text-xl max-md:hidden">d[any]</span>
</NuxtLink>
</div>
<div class="flex items-center gap-8 max-md:hidden">
<NuxtLink :href="{ name: 'character' }" class="text-light-70 dark:text-dark-70" active-class="!text-accent-blue"><span class="pl-3 py-1 flex-1 truncate">Mes personnages</span></NuxtLink>
<NavigationMenuRoot class="relative">
<NavigationMenuList class="flex items-center gap-8 max-md:hidden">
<NavigationMenuItem>
<NavigationMenuTrigger>
<NuxtLink :href="{ name: 'character' }" class="text-light-70 dark:text-dark-70" active-class="!text-accent-blue"><span class="pl-3 py-1 flex-1 truncate">Personnages</span></NuxtLink>
</NavigationMenuTrigger>
<NavigationMenuContent class="absolute top-0 left-0 w-full sm:w-auto bg-light-0 dark:bg-dark-0 border border-light-30 dark:border-dark-30">
<NuxtLink :href="{ name: 'character-list' }" class="text-light-70 dark:text-dark-70" active-class="!text-accent-blue"><span class="py-2 px-3 flex-1 truncate">Tous les personnages</span></NuxtLink>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
<div class="absolute top-full left-0 flex w-full justify-center my-4">
<NavigationMenuViewport class="h-[var(--radix-navigation-menu-viewport-height)] w-full origin-[top_center] overflow-hidden rounded-[10px] bg-white transition-[width,_height] duration-300 sm:w-[var(--radix-navigation-menu-viewport-width)]" />
</div>
</NavigationMenuRoot>
<div class="flex items-center px-2 gap-4">
<template v-if="!loggedIn">
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70" :to="{ name: 'user-login' }">Se connecter</NuxtLink>

View File

@ -37,7 +37,7 @@ function abilitySpecialFeatures(type: "points" | "max", curiosity: DoubleIndex<T
import { Icon } from '@iconify/vue/dist/iconify.js';
import PreviewA from '~/components/prose/PreviewA.vue';
import { clamp } from '~/shared/general.util';
import { elementTexts, mainStatTexts, spellTypeTexts, type Ability, type Character, type CharacterConfig, type DoubleIndex, type Level, type MainStat, type RaceOption, type SpellConfig, type SpellElement, type SpellType, type TrainingLevel, type TrainingOption } from '~/types/character';
import { defaultCharacter, elementTexts, mainStatTexts, spellTypeTexts, type Ability, type Character, type CharacterConfig, type DoubleIndex, type Level, type MainStat, type RaceOption, type SpellConfig, type SpellElement, type SpellType, type TrainingLevel, type TrainingOption } from '~/types/character';
definePageMeta({
guestsGoesTo: '/user/login',
@ -45,36 +45,7 @@ definePageMeta({
let id = useRouter().currentRoute.value.params.id;
const { add } = useToast();
const characterConfig = config as CharacterConfig;
const data = ref<Character>({
id: -1,
name: '',
progress: {
training: {
strength: [[1, 0], [2, 0], [3, 0], [4, 0]],
dexterity: [[1, 0], [2, 0], [3, 0], [4, 0]],
constitution: [[1, 0], [2, 0], [3, 0], [4, 0]],
intelligence: [[1, 0], [2, 0], [3, 0], [4, 0]],
curiosity: [[1, 0], [2, 0], [3, 0], [4, 0]],
charisma: [[1, 0], [2, 0], [3, 0], [4, 0]],
psyche: [[1, 0], [2, 0], [3, 0], [4, 0]],
},
level: 1,
race: {
index: undefined,
progress: [[1, 0]],
},
abilities: {},
modifiers: {},
spells: [],
notes: "",
},
values: {
hp: 0,
armor: 0,
mana: 0,
},
visibility: "private"
});
const data = ref<Character>({ ...defaultCharacter });
const spellFilter = ref<{
ranks: Array<1 | 2 | 3>,
types: Array<SpellType>,
@ -90,18 +61,18 @@ const spellFilter = ref<{
});
const peopleOpen = ref(false), trainingOpen = ref(false), abilityOpen = ref(false), spellOpen = ref(false), notesOpen = ref(false), trainingTab = ref(0);
const raceOptions = computed(() => data.value.progress.race.index !== undefined ? characterConfig.peoples[data.value.progress.race.index!].options : undefined);
const selectedRaceOptions = computed(() => raceOptions !== undefined ? data.value.progress.race.progress!.map(e => raceOptions.value![e[0]][e[1]]) : undefined);
const trainingPoints = computed(() => raceOptions.value ? data.value.progress.race.progress?.reduce((p, v) => p + (raceOptions.value![v[0]][v[1]].training ?? 0), 0) : 0);
const training = computed(() => Object.entries(characterConfig.training).map(e => [e[0], getFeaturesOf(e[0] as MainStat, data.value.progress.training[e[0] as MainStat])]) as [MainStat, TrainingOption[]][]);
const maxTraining = computed(() => Object.entries(data.value.progress.training).reduce((p, v) => { p[v[0] as MainStat] = v[1].reduce((_p, _v) => Math.max(_p, _v[0]) , 0); return p; }, {} as Record<MainStat, number>));
const raceOptions = computed(() => data.value.people !== undefined ? characterConfig.peoples[data.value.people!].options : undefined);
const selectedRaceOptions = computed(() => raceOptions !== undefined ? data.value.leveling!.map(e => raceOptions.value![e[0]][e[1]]) : undefined);
const trainingPoints = computed(() => raceOptions.value ? data.value.leveling?.reduce((p, v) => p + (raceOptions.value![v[0]][v[1]].training ?? 0), 0) : 0);
const training = computed(() => Object.entries(characterConfig.training).map(e => [e[0], getFeaturesOf(e[0] as MainStat, data.value.training[e[0] as MainStat])]) as [MainStat, TrainingOption[]][]);
const maxTraining = computed(() => Object.entries(data.value.training).reduce((p, v) => { p[v[0] as MainStat] = v[1].reduce((_p, _v) => Math.max(_p, _v[0]) , 0); return p; }, {} as Record<MainStat, number>));
const trainingSpent = computed(() => Object.values(maxTraining.value).reduce((p, v) => p + v, 0));
const modifiers = computed(() => Object.entries(maxTraining.value).reduce((p, v) => { p[v[0] as MainStat] = Math.floor(v[1] / 3) + (data.value.progress.modifiers ? (data.value.progress.modifiers[v[0] as MainStat] ?? 0) : 0); return p; }, {} as Record<MainStat, number>))
const modifiers = computed(() => Object.entries(maxTraining.value).reduce((p, v) => { p[v[0] as MainStat] = Math.floor(v[1] / 3) + (data.value.modifiers ? (data.value.modifiers[v[0] as MainStat] ?? 0) : 0); return p; }, {} as Record<MainStat, number>))
const modifierPoints = computed(() => (selectedRaceOptions.value ? selectedRaceOptions.value.reduce((p, v) => p + (v?.modifier ?? 0), 0) : 0) + training.value.reduce((p, v) => p + v[1].reduce((_p, _v) => _p + (_v?.modifier ?? 0), 0), 0));
const modifierSpent = computed(() => Object.values(data.value.progress.modifiers ?? {}).reduce((p, v) => p + v, 0));
const modifierSpent = computed(() => Object.values(data.value.modifiers ?? {}).reduce((p, v) => p + v, 0));
const abilityPoints = computed(() => (selectedRaceOptions.value ? selectedRaceOptions.value.reduce((p, v) => p + (v?.abilities ?? 0), 0) : 0) + training.value.flatMap(e => e[1].filter(_e => _e.ability !== undefined)).reduce((p, v) => p + v.ability!, 0));
const abilityMax = computed(() => Object.entries(characterConfig.abilities).reduce((p, v) => { p[v[0] as Ability] = abilitySpecialFeatures("max", data.value.progress.training.curiosity, Math.floor(maxTraining.value[v[1].max[0]] / 3) + Math.floor(maxTraining.value[v[1].max[1]] / 3)); return p; }, {} as Record<Ability, number>));
const abilitySpent = computed(() => Object.values(data.value.progress.abilities ?? {}).reduce((p, v) => p + v[0], 0));
const abilityMax = computed(() => Object.entries(characterConfig.abilities).reduce((p, v) => { p[v[0] as Ability] = abilitySpecialFeatures("max", data.value.training.curiosity, Math.floor(maxTraining.value[v[1].max[0]] / 3) + Math.floor(maxTraining.value[v[1].max[1]] / 3)); return p; }, {} as Record<Ability, number>));
const abilitySpent = computed(() => Object.values(data.value.abilities ?? {}).reduce((p, v) => p + v[0], 0));
const spellranks = computed(() => training.value.flatMap(e => e[1].filter(_e => _e.spellrank !== undefined)).reduce((p, v) => { p[v.spellrank!]++; return p; }, { instinct: 0, precision: 0, knowledge: 0 } as Record<SpellType, 0 | 1 | 2 | 3>));
const spellsPoints = computed(() => training.value.flatMap(e => e[1].filter(_e => _e.spellslot !== undefined)).reduce((p, v) => p + (modifiers.value.hasOwnProperty(v.spellslot as MainStat) ? modifiers.value[v.spellslot as MainStat] : v.spellslot as number), 0));
@ -114,36 +85,34 @@ if(id !== 'new')
throw new Error('Donnée du personnage introuvables');
}
data.value = { name: character.name, progress: Object.assign(data.value.progress, character.progress) } as Character;
data.value = Object.assign(defaultCharacter, data.value, character);
}
console.log(data.value.progress);
function selectRaceOption(level: Level, choice: number)
{
const character = data.value;
if(level > character.progress.level)
if(level > character.level)
return;
if(character.progress.race.progress === undefined)
character.progress.race.progress = [[1, 0]];
if(character.leveling === undefined)
character.leveling = [[1, 0]];
if(level == 1)
return;
for(let i = 1; i < level; i++) //Check previous levels as a requirement
{
if(!character.progress.race.progress.some(e => e[0] == i))
if(!character.leveling.some(e => e[0] == i))
return;
}
if(character.progress.race.progress.some(e => e[0] == level))
if(character.leveling.some(e => e[0] == level))
{
character.progress.race.progress.splice(character.progress.race.progress.findIndex(e => e[0] == level), 1, [level, choice]);
character.leveling.splice(character.leveling.findIndex(e => e[0] == level), 1, [level, choice]);
}
else
{
character.progress.race.progress.push([level, choice]);
character.leveling.push([level, choice]);
}
data.value = character;
@ -157,27 +126,27 @@ function switchTrainingOption(stat: MainStat, level: TrainingLevel, choice: numb
for(let i = 1; i < level; i++) //Check previous levels as a requirement
{
if(!character.progress.training[stat].some(e => e[0] == i))
if(!character.training[stat].some(e => e[0] == i))
return;
}
if(character.progress.training[stat].some(e => e[0] == level))
if(character.training[stat].some(e => e[0] == level))
{
if(character.progress.training[stat].some(e => e[0] == level && e[1] === choice))
if(character.training[stat].some(e => e[0] == level && e[1] === choice))
{
for(let i = 15; i >= level; i --) //Invalidate higher levels
{
const index = character.progress.training[stat].findIndex(e => e[0] == i);
const index = character.training[stat].findIndex(e => e[0] == i);
if(index !== -1)
character.progress.training[stat].splice(index, 1);
character.training[stat].splice(index, 1);
}
}
else
character.progress.training[stat].splice(character.progress.training[stat].findIndex(e => e[0] == level), 1, [level, choice]);
character.training[stat].splice(character.training[stat].findIndex(e => e[0] == level), 1, [level, choice]);
}
else if(trainingPoints.value && trainingPoints.value > 0)
{
character.progress.training[stat].push([level, choice]);
character.training[stat].push([level, choice]);
}
data.value = character;
@ -186,13 +155,13 @@ function updateLevel()
{
const character = data.value;
if(character.progress.race.progress) //Invalidate higher levels
if(character.leveling) //Invalidate higher levels
{
for(let level = 20; level > character.progress.level; level--)
for(let level = 20; level > character.level; level--)
{
const index = character.progress.race.progress.findIndex(e => e[0] == level);
const index = character.leveling.findIndex(e => e[0] == level);
if(index !== -1)
character.progress.race.progress.splice(index, 1);
character.leveling.splice(index, 1);
}
}
@ -213,7 +182,7 @@ function filterSpells(spells: SpellConfig[])
}
async function save(leave: boolean)
{
if(data.value.name === '' || data.value.progress.race.index === undefined || data.value.progress.race.index === -1)
if(data.value.name === '' || data.value.people === undefined || data.value.people === -1)
{
add({ title: 'Données manquantes', content: "Merci de saisir un nom et une race avant de pouvoir enregistrer votre personnage", type: 'error', duration: 25000, timer: true });
return;
@ -224,7 +193,7 @@ async function save(leave: boolean)
method: 'post',
body: data.value,
onResponseError: (e) => {
add({ title: 'Erreur d\enregistrement', content: e.response.status === 401 ? "Vous n'êtes pas autorisé à effectué cette opération" : e.response.statusText, type: 'error', duration: 25000, timer: true });
add({ title: 'Erreur d\'enregistrement', content: e.response.status === 401 ? "Vous n'êtes pas autorisé à effectué cette opération" : e.response.statusText, type: 'error', duration: 25000, timer: true });
}
});
add({ content: 'Personnage créé', type: 'success', duration: 25000, timer: true });
@ -237,7 +206,7 @@ async function save(leave: boolean)
method: 'post',
body: data.value,
onResponseError: (e) => {
add({ title: 'Erreur d\enregistrement', content: e.response.status === 401 ? "Vous n'êtes pas autorisé à effectué cette opération" : e.response.statusText, type: 'error', duration: 25000, timer: true });
add({ title: 'Erreur d\'enregistrement', content: e.response.status === 401 ? "Vous n'êtes pas autorisé à effectué cette opération" : e.response.statusText, type: 'error', duration: 25000, timer: true });
}
});
add({ content: 'Personnage enregistré', type: 'success', duration: 25000, timer: true });
@ -268,7 +237,7 @@ useShortcuts({
</Label>
<Label class="flex items-start justify-between flex-col gap-2">
<span class="pb-1 mx-2 md:p-0">Niveau</span>
<NumberFieldRoot :min="1" :max="20" v-model="data.progress.level" @update:model-value="updateLevel" class="flex justify-center border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20
<NumberFieldRoot :min="1" :max="20" v-model="data.level" @update:model-value="updateLevel" class="flex justify-center border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20
data-[disabled]:text-light-70 dark:data-[disabled]:text-dark-70 hover:border-light-50 dark:hover:border-dark-50 has-[:focus]:shadow-raw transition-[box-shadow] has-[:focus]:shadow-light-40 dark:has-[:focus]:shadow-dark-40">
<NumberFieldInput class="tabular-nums w-20 bg-transparent px-3 py-1 outline-none caret-light-50 dark:caret-dark-50" />
</NumberFieldRoot>
@ -292,22 +261,22 @@ useShortcuts({
</template>
<template #default>
<div class="m-2 overflow-auto">
<Combobox label="Peuple de votre personnage" v-model="data.progress.race.index" :options="config.peoples.map((people, index) => [people.name, index])" @update:model-value="(index) => { data.progress.race.index = index as number | undefined; data.progress.race.progress = [[1, 0]]}" />
<template v-if="data.progress.race.index !== undefined">
<Combobox label="Peuple de votre personnage" v-model="data.people" :options="config.peoples.map((people, index) => [people.name, index])" @update:model-value="(index) => { data.people = index as number | undefined; data.leveling = [[1, 0]]}" />
<template v-if="data.people !== undefined">
<div class="w-full border-b border-light-30 dark:border-dark-30 pb-4">
<span class="text-sm text-light-70 dark:text-dark-70">{{ characterConfig.peoples[data.progress.race.index].description }}</span>
<span class="text-sm text-light-70 dark:text-dark-70">{{ characterConfig.peoples[data.people].description }}</span>
</div>
<div class="flex flex-col gap-4 max-h-[50vh] pe-4 relative">
<span class="sticky top-0 py-1 bg-light-0 dark:bg-dark-0 z-10 text-xl">Niveaux restants: {{ data.progress.level - (data.progress.race.progress?.length ?? 0) }}</span>
<div class="flex flex-row gap-4 justify-center" v-for="(level, index) of characterConfig.peoples[data.progress.race.index].options" :class="{ 'opacity-30': index > data.progress.level }">
<div class="border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-64" v-for="(option, i) of level" @click="selectRaceOption(index as Level, i)" :class="{ 'hover:border-light-60 dark:hover:border-dark-60': index <= data.progress.level, '!border-accent-blue bg-accent-blue bg-opacity-20': data.progress.race.progress?.some(e => e[0] == index && e[1] === i) ?? false }"><MarkdownRenderer :content="raceOptionToText(option)" /></div>
<span class="sticky top-0 py-1 bg-light-0 dark:bg-dark-0 z-10 text-xl">Niveaux restants: {{ data.level - (data.leveling?.length ?? 0) }}</span>
<div class="flex flex-row gap-4 justify-center" v-for="(level, index) of characterConfig.peoples[data.people].options" :class="{ 'opacity-30': index > data.level }">
<div class="border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-64" v-for="(option, i) of level" @click="selectRaceOption(parseInt(index as unknown as string, 10) as Level, i)" :class="{ 'hover:border-light-60 dark:hover:border-dark-60': index <= data.level, '!border-accent-blue bg-accent-blue bg-opacity-20': data.leveling?.some(e => e[0] == index && e[1] === i) ?? false }"><MarkdownRenderer :content="raceOptionToText(option)" /></div>
</div>
</div>
</template>
</div>
</template>
</Collapsible>
<Collapsible class="border-b border-light-30 dark:border-dark-30 p-1" v-model="trainingOpen" :disabled="data.progress.race.index === undefined" @update:model-value="() => { peopleOpen = false; abilityOpen = false; spellOpen = false; notesOpen = false; }">
<Collapsible class="border-b border-light-30 dark:border-dark-30 p-1" v-model="trainingOpen" :disabled="data.people === undefined" @update:model-value="() => { peopleOpen = false; abilityOpen = false; spellOpen = false; notesOpen = false; }">
<template #label>
<span class="font-bold text-xl">Entrainement</span>
</template>
@ -323,7 +292,7 @@ useShortcuts({
<div class="sticky top-1 mx-16 z-10 flex justify-between">
<div class="py-1 px-3 bg-light-0 dark:bg-dark-0 z-10 text-xl font-bold border border-light-30 dark:border-dark-30 flex">{{ text }}
<div class="flex gap-2" v-if="maxTraining[stat] >= 0">: Niveau {{ maxTraining[stat] }} (+{{ modifiers[stat] }}
<NumberFieldRoot :default-value="data.progress.modifiers[stat] ?? 0" v-model="data.progress.modifiers[stat]" class="flex justify-center border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20
<NumberFieldRoot :default-value="data.modifiers[stat] ?? 0" v-model="data.modifiers[stat]" class="flex justify-center border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20
data-[disabled]:text-light-70 dark:data-[disabled]:text-dark-70 hover:border-light-50 dark:hover:border-dark-50 has-[:focus]:shadow-raw transition-[box-shadow] has-[:focus]:shadow-light-40 dark:has-[:focus]:shadow-dark-40">
<NumberFieldInput class="tabular-nums w-8 text-base font-normal bg-transparent px-2 outline-none caret-light-50 dark:caret-dark-50" />
</NumberFieldRoot>
@ -331,14 +300,14 @@ useShortcuts({
<div class="py-1 px-3 bg-light-0 dark:bg-dark-0 z-10 flex gap-2 justify-center items-center" :class="{ 'text-light-red dark:text-dark-red': (modifierPoints ?? 0) < modifierSpent }">Modifieur bonus: {{ modifierPoints - modifierSpent }}</div>
</div>
<div class="flex flex-row gap-4 justify-center" v-for="(level, index) of characterConfig.training[stat]" :class="{ 'opacity-30': index > maxTraining[stat] + 1 }">
<div class="border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-1/3" v-for="(option, i) of level" @click="switchTrainingOption(stat, index as TrainingLevel, i)" :class="{ 'hover:border-light-60 dark:hover:border-dark-60': index <= maxTraining[stat] + 1, '!border-accent-blue bg-accent-blue bg-opacity-20': index == 0 || (data.progress.training[stat]?.some(e => e[0] == index && e[1] === i) ?? false) }"><MarkdownRenderer :proses="{ 'a': PreviewA }" :content="option.description.map(e => e.text).join('\n')" /></div>
<div class="border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-1/3" v-for="(option, i) of level" @click="switchTrainingOption(stat, parseInt(index as unknown as string, 10) as TrainingLevel, i)" :class="{ 'hover:border-light-60 dark:hover:border-dark-60': index <= maxTraining[stat] + 1, '!border-accent-blue bg-accent-blue bg-opacity-20': index == 0 || (data.training[stat]?.some(e => e[0] == index && e[1] === i) ?? false) }"><MarkdownRenderer :proses="{ 'a': PreviewA }" :content="option.description.map(e => e.text).join('\n')" /></div>
</div>
</div>
</div>
</div>
</template>
</Collapsible>
<Collapsible class="border-b border-light-30 dark:border-dark-30 p-1" v-model="abilityOpen" :disabled="data.progress.race.index === undefined" @update:model-value="() => { trainingOpen = false; peopleOpen = false; spellOpen = false; notesOpen = false; }">
<Collapsible class="border-b border-light-30 dark:border-dark-30 p-1" v-model="abilityOpen" :disabled="data.people === undefined" @update:model-value="() => { trainingOpen = false; peopleOpen = false; spellOpen = false; notesOpen = false; }">
<template #label>
<span class="font-bold text-xl">Compétences</span>
</template>
@ -350,7 +319,7 @@ useShortcuts({
<div class="grid gap-4 grid-cols-6">
<div v-for="(ability, index) of characterConfig.abilities" class="flex flex-col items-center border border-light-30 dark:border-dark-30 p-2">
<div class="flex items-center justify-center gap-4">
<NumberFieldRoot :min="0" :default-value="data.progress.abilities[index] ? data.progress.abilities[index][0] : 0" @update:model-value="(value) => { data.progress.abilities[index] = [value, data.progress.abilities[index] ? data.progress.abilities[index][1] : 0]; }" class="border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20
<NumberFieldRoot :min="0" :default-value="data.abilities[index] ? data.abilities[index][0] : 0" @update:model-value="(value) => { data.abilities[index] = [value, data.abilities[index] ? data.abilities[index][1] : 0]; }" class="border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20
data-[disabled]:text-light-70 dark:data-[disabled]:text-dark-70 hover:border-light-50 dark:hover:border-dark-50 has-[:focus]:shadow-raw transition-[box-shadow] has-[:focus]:shadow-light-40 dark:has-[:focus]:shadow-dark-40">
<NumberFieldInput class="tabular-nums w-8 bg-transparent px-3 py-1 outline-none caret-light-50 dark:caret-dark-50" />
</NumberFieldRoot>
@ -363,22 +332,22 @@ useShortcuts({
</div>
</template>
</Collapsible>
<Collapsible class="border-b border-light-30 dark:border-dark-30 p-1" v-model="spellOpen" :disabled="data.progress.race.index === undefined" @update:model-value="() => { trainingOpen = false; peopleOpen = false; abilityOpen = false; notesOpen = false; }">
<Collapsible class="border-b border-light-30 dark:border-dark-30 p-1" v-model="spellOpen" :disabled="data.people === undefined" @update:model-value="() => { trainingOpen = false; peopleOpen = false; abilityOpen = false; notesOpen = false; }">
<template #label>
<span class="font-bold text-xl">Sorts</span>
</template>
<template #default>
<div class="flex flex-col gap-2 max-h-[50vh] px-4 relative overflow-y-auto">
<div class="sticky top-0 py-2 bg-light-0 dark:bg-dark-0 z-10 flex gap-2 items-center">
<span class="text-xl pe-4" :class="{ 'text-light-red dark:text-dark-red': spellsPoints < (data.progress.spells?.length ?? 0) }">Sorts: {{ data.progress.spells?.length ?? 0 }}/{{ spellsPoints }}</span>
<span class="text-xl pe-4" :class="{ 'text-light-red dark:text-dark-red': spellsPoints < (data.spells?.length ?? 0) }">Sorts: {{ data.spells?.length ?? 0 }}/{{ spellsPoints }}</span>
<TextInput label="Nom" v-model="spellFilter.text" />
<Combobox label="Rang" v-model="spellFilter.ranks" multiple :options="[['Rang 1', 1], ['Rang 2', 2], ['Rang 3', 3]]" />
<Combobox label="Type" v-model="spellFilter.types" multiple :options="[['Précision', 'precision'], ['Savoir', 'knowledge'], ['Instinct', 'instinct']]" />
<Combobox label="Element" v-model="spellFilter.elements" multiple :options="[['Feu', 'fire'], ['Glace', 'ice'], ['Foudre', 'thunder'], ['Terre', 'earth'], ['Arcane', 'arcana'], ['Air', 'air'], ['Nature', 'nature'], ['Lumière', 'light'], ['Psy', 'psyche']]" />
</div>
<div class="grid gap-4 grid-cols-2">
<div class="py-1 px-2 border border-light-30 dark:border-dark-30 flex flex-col hover:border-light-50 dark:hover:border-dark-50 cursor-pointer" v-for="spell of filterSpells(config.spells)" :class="{ '!border-accent-blue bg-accent-blue bg-opacity-20': data.progress.spells?.find(e => e === spell.id) }"
@click="() => data.progress.spells?.includes(spell.id) ? data.progress.spells.splice(data.progress.spells.findIndex((e: string) => e === spell.id), 1) : data.progress.spells!.push(spell.id)">
<div class="py-1 px-2 border border-light-30 dark:border-dark-30 flex flex-col hover:border-light-50 dark:hover:border-dark-50 cursor-pointer" v-for="spell of filterSpells(characterConfig.spells)" :class="{ '!border-accent-blue bg-accent-blue bg-opacity-20': data.spells?.find(e => e === spell.id) }"
@click="() => data.spells?.includes(spell.id) ? data.spells.splice(data.spells.findIndex((e: string) => e === spell.id), 1) : data.spells!.push(spell.id)">
<div class="flex flex-row justify-between">
<span class="text-lg font-bold">{{ spell.name }}</span>
<div class="flex flex-row items-center gap-6">
@ -404,7 +373,7 @@ useShortcuts({
<span class="font-bold text-xl">Notes libres</span>
</template>
<template #default>
<Editor class="min-h-[400px] border border-light-30 dark:border-dark-30" v-model="data.progress.notes" />
<Editor class="min-h-[400px] border border-light-30 dark:border-dark-30" v-model="data.notes" />
</template>
</Collapsible>
</div>

View File

@ -11,19 +11,7 @@ const id = useRouter().currentRoute.value.params.id;
const { user } = useUserSession();
const { add } = useToast();
const { data: character, status, error } = await useAsyncData(() => useRequestFetch()(`/api/character/${id}/compiled`));
const { data: values } = await useAsyncData(() => useRequestFetch()(`/api/character/${id}/values`));
async function updateValues()
{
await useRequestFetch()(`/api/character/${id}/values`, {
method: "post",
body: values.value,
onResponseError: (e) => {
add({ duration: 25000, timer: true, type: "success", title: "Erreur", content: e.response.statusText });
}
});
}
const { data: character, status, error } = await useFetch(`/api/character/${id}/compiled`);
</script>
<template>
@ -38,7 +26,8 @@ async function updateValues()
</Head>
<div class="flex flex-row gap-4 justify-between">
<div></div>
<div class="flex flex-row gap-6 items-center justify-center">
<div class="flex lg:flex-row flex-col gap-6 items-center justify-center">
<div class="flex gap-6 items-center">
<Avatar src="" icon="radix-icons:person" size="large" />
<div class="flex flex-col">
<span class="text-xl font-bold">{{ character.name }}</span>
@ -48,43 +37,35 @@ async function updateValues()
<span class="font-bold">Niveau {{ character.level }}</span>
<span>{{ character.race === -1 ? "Race inconnue" : characterConfig.peoples[character.race].name }}</span>
</div>
<span class="h-full border-l border-light-30 dark:border-dark-30"></span>
<span class="flex flex-row items-center gap-2">PV: <NumberFieldRoot v-model="values.health" @change="updateValues" class="border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20
data-[disabled]:text-light-70 dark:data-[disabled]:text-dark-70 hover:border-light-50 dark:hover:border-dark-50 has-[:focus]:shadow-raw transition-[box-shadow] has-[:focus]:shadow-light-40 dark:has-[:focus]:shadow-dark-40">
<NumberFieldInput class="tabular-nums w-14 bg-transparent px-3 py-1 outline-none caret-light-50 dark:caret-dark-50" />
</NumberFieldRoot>/ {{ character.health }}</span>
<span class="flex flex-row items-center gap-2">Mana: <NumberFieldRoot v-model="values.mana" @change="updateValues" class="border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20
data-[disabled]:text-light-70 dark:data-[disabled]:text-dark-70 hover:border-light-50 dark:hover:border-dark-50 has-[:focus]:shadow-raw transition-[box-shadow] has-[:focus]:shadow-light-40 dark:has-[:focus]:shadow-dark-40">
<NumberFieldInput class="tabular-nums w-14 bg-transparent px-3 py-1 outline-none caret-light-50 dark:caret-dark-50" />
</NumberFieldRoot>/ {{ character.mana }}</span>
<span class="flex flex-row items-center gap-2">Armure: <NumberFieldRoot v-model="values.armor" @change="updateValues" class="border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20
data-[disabled]:text-light-70 dark:data-[disabled]:text-dark-70 hover:border-light-50 dark:hover:border-dark-50 has-[:focus]:shadow-raw transition-[box-shadow] has-[:focus]:shadow-light-40 dark:has-[:focus]:shadow-dark-40">
<NumberFieldInput class="tabular-nums w-14 bg-transparent px-3 py-1 outline-none caret-light-50 dark:caret-dark-50" />
</NumberFieldRoot></span>
</div>
<div class="flex gap-6 lg:border-l border-light-30 dark:border-dark-30 py-4 ps-4">
<span class="flex flex-row items-center gap-2">PV: {{ character.health - character.values.hp }}/{{ character.health }}</span>
<span class="flex flex-row items-center gap-2">Mana: {{ character.mana - character.values.mana }}/{{ character.mana }}</span>
</div>
</div>
<div class="self-center">
<Tooltip side="right" message="Modifier" v-if="user && user.id === character.owner"><NuxtLink :to="{ name: 'character-id-edit', params: { id: character.id } }"><Button icon><Icon icon="radix-icons:pencil-2" /></Button></NuxtLink></Tooltip>
</div>
</div>
<div class="flex flex-1 flex-col justify-center gap-4 *:py-2">
<div class="flex flex-row gap-4 items-center justify-center border-b border-light-30 dark:border-dark-30">
<div class="flex relative ps-4">
<div class="flex flex-col px-2 items-center"><span class="text-2xl font-bold">+{{ character.modifier.strength }}</span><span>Force</span></div>
<div class="flex flex-col px-2 items-center"><span class="text-2xl font-bold">+{{ character.modifier.dexterity }}</span><span>Dextérité</span></div>
<div class="flex flex-col px-2 items-center"><span class="text-2xl font-bold">+{{ character.modifier.constitution }}</span><span>Constitution</span></div>
<div class="flex flex-col px-2 items-center"><span class="text-2xl font-bold">+{{ character.modifier.intelligence }}</span><span>Intelligence</span></div>
<div class="flex flex-col px-2 items-center"><span class="text-2xl font-bold">+{{ character.modifier.curiosity }}</span><span>Curiosité</span></div>
<div class="flex flex-col px-2 items-center"><span class="text-2xl font-bold">+{{ character.modifier.charisma }}</span><span>Charisme</span></div>
<div class="flex flex-col px-2 items-center"><span class="text-2xl font-bold">+{{ character.modifier.psyche }}</span><span>Psyché</span></div>
<div class="grid 2xl:grid-cols-12 grid-cols-2 gap-4 items-center border-b border-light-30 dark:border-dark-30">
<div class="flex relative justify-between ps-4 gap-2 2xl:col-span-6 lg:col-span-2">
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.strength }}</span><span class="text-sm 2xl:text-base">Force</span></div>
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.dexterity }}</span><span class="text-sm 2xl:text-base">Dextérité</span></div>
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.constitution }}</span><span class="text-sm 2xl:text-base">Constitution</span></div>
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.intelligence }}</span><span class="text-sm 2xl:text-base">Intelligence</span></div>
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.curiosity }}</span><span class="text-sm 2xl:text-base">Curiosité</span></div>
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.charisma }}</span><span class="text-sm 2xl:text-base">Charisme</span></div>
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.psyche }}</span><span class="text-sm 2xl:text-base">Psyché</span></div>
</div>
<div class="flex relative border-l border-light-30 dark:border-dark-30 ps-4">
<div class="flex relative 2xl:border-l border-light-30 dark:border-dark-30 ps-4 2xl:col-span-2">
<div class="flex flex-1 flex-row items-center justify-between">
<div class="flex flex-col px-2 items-center"><span class="text-2xl font-bold">+{{ character.initiative }}</span><span>Initiative</span></div>
<div class="flex flex-col px-2 items-center"><span class="text-2xl font-bold">{{ character.speed === false ? "Aucun déplacement" : `${character.speed} cases` }}</span><span>Course</span></div>
</div>
<!-- <div class="absolute top-0 left-0 bottom-0 right-0 bg-light-0 dark:bg-dark-0 bg-opacity-50 dark:bg-opacity-50 text-xl font-bold flex items-center justify-center">Les données secondaires arrivent bientôt.</div> -->
</div>
<div class="flex relative border-l border-light-30 dark:border-dark-30 ps-4">
<div class="flex relative border-l border-light-30 dark:border-dark-30 ps-4 2xl:col-span-4">
<div class="flex flex-col px-2">
<span class="text-xl">Défense passive: <span class="text-2xl font-bold">{{ character.defense.static }}</span>/+<span class="text-2xl font-bold">{{ character.defense.passivedodge }}</span>/+<span class="text-2xl font-bold">{{ character.defense.passiveparry }}</span></span>
<span class="text-xl">Défense active: <span class="float-right">+<span class="text-2xl font-bold">{{ character.defense.activedodge }}</span>/+<span class="text-2xl font-bold">{{ character.defense.activeparry }}</span></span></span>

View File

@ -44,7 +44,7 @@ async function duplicateCharacter(id: number)
<Avatar size="large" icon="radix-icons:person" src="" />
<div class="flex flex-1 flex-shrink flex-col truncate">
<NuxtLink class="text-xl font-bold hover:text-accent-blue truncate" :to="{ name: 'character-id', params: { id: character.id } }" :title="character.name">{{ character.name }}</NuxtLink>
<span class="text-sm truncate">Niveau {{ character.progress.level }}</span>
<span class="text-sm truncate">Niveau {{ character.level }}</span>
</div>
<AlertDialogRoot>
<DropdownMenuRoot>

View File

@ -1,8 +1,9 @@
import { and, eq, sql } from 'drizzle-orm';
import { and, eq, SQL, sql, type Operators } from 'drizzle-orm';
import useDatabase from '~/composables/useDatabase';
import { characterTable, userPermissionsTable } from '~/db/schema';
import { hasPermissions } from '~/shared/auth.util';
import type { Character } from '~/types/character';
import { group } from '~/shared/general.util';
import type { Character, DoubleIndex, Level, MainStat, TrainingLevel } from '~/types/character';
export default defineEventHandler(async (e) => {
let { visibility } = getQuery(e) as { visibility?: "public" | "own" | "admin" };
@ -12,6 +13,9 @@ export default defineEventHandler(async (e) => {
visibility = "own";
}
let where: ((character: typeof characterTable._.config.columns, sql: Operators) => SQL | undefined) | undefined = undefined;
const db = useDatabase();
if(visibility === "own")
{
const session = await getUserSession(e);
@ -21,33 +25,11 @@ export default defineEventHandler(async (e) => {
return;
}
const db = useDatabase();
const characters = db.select({
id: characterTable.id,
name: characterTable.name,
progress: characterTable.progress,
visibility: characterTable.visibility,
}).from(characterTable).where(eq(characterTable.owner, session.user.id)).all();
if(characters !== undefined)
{
return characters as Character[];
}
where = (character, { eq, and }) => and(eq(character.owner, session.user!.id), eq(character.visibility, "private"));
}
else if(visibility === 'public')
{
const db = useDatabase();
const characters = db.select({
id: characterTable.id,
name: characterTable.name,
progress: characterTable.progress,
visibility: characterTable.visibility,
}).from(characterTable).where(eq(characterTable.visibility, "public")).all();
if(characters !== undefined)
{
return characters as Character[];
}
where = (character, { eq, and }) => eq(character.visibility, "public");
}
else if(visibility === 'admin')
{
@ -66,17 +48,46 @@ export default defineEventHandler(async (e) => {
return;
}
const characters = db.select({
id: characterTable.id,
name: characterTable.name,
progress: characterTable.progress,
visibility: characterTable.visibility,
}).from(characterTable).all();
where = undefined;
}
const characters = db.query.characterTable.findMany({
with: {
abilities: true,
levels: true,
modifiers: true,
spells: true,
training: true,
user: {
columns: { username: true }
}
},
where: where,
}).sync();
if(characters !== undefined)
{
return characters as Character[];
}
return characters.map(character => ({
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<MainStat, DoubleIndex<TrainingLevel>[]>),
leveling: character.levels.map(e => [e.level as Level, e.choice] as DoubleIndex<Level>),
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));
}
setResponseStatus(e, 404);

View File

@ -1,12 +1,15 @@
import { z } from 'zod';
import useDatabase from '~/composables/useDatabase';
import { characterTable } from '~/db/schema';
import { characterAbilitiesTable, characterLevelingTable, characterModifiersTable, characterSpellsTable, characterTable, characterTrainingTable } from '~/db/schema';
import { CharacterValidation, type Ability, type DoubleIndex, type MainStat, type TrainingLevel } from '~/types/character';
export default defineEventHandler(async (e) => {
const body = await readBody(e);
if(!body)
const body = await readValidatedBody(e, CharacterValidation.extend({ id: z.unknown(), }).safeParse);
if(!body.success)
{
setResponseStatus(e, 400);
return;
return body.error.message;
}
const session = await getUserSession(e);
@ -18,12 +21,46 @@ export default defineEventHandler(async (e) => {
const db = useDatabase();
const id = await db.insert(characterTable).values({
name: body.name,
progress: body.progress,
owner: session.user.id,
}).returning({ id: characterTable.id });
try
{
const id = db.transaction((tx) => {
const id = tx.insert(characterTable).values({
name: body.data.name,
owner: session.user!.id,
people: body.data.people!,
level: body.data.level,
aspect: body.data.aspect,
notes: body.data.notes,
health: body.data.health,
mana: body.data.mana,
visibility: body.data.visibility,
thumbnail: body.data.thumbnail,
}).returning({ id: characterTable.id }).get().id;
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 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] })));
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] }));
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] }));
if(abilities.length > 0) tx.insert(characterAbilitiesTable).values(abilities).run();
return id;
});
setResponseStatus(e, 201);
return id[0].id;
return id;
}
catch(_e)
{
console.error(_e);
setResponseStatus(e, 500);
return;
}
});

View File

@ -1,7 +1,8 @@
import { and, eq, sql } from 'drizzle-orm';
import useDatabase from '~/composables/useDatabase';
import { characterTable } from '~/db/schema';
import type { Character } from '~/types/character';
import { group } from '~/shared/general.util';
import type { Character, DoubleIndex, Level, MainStat, TrainingLevel } from '~/types/character';
export default defineEventHandler(async (e) => {
const id = getRouterParam(e, "id");
@ -21,17 +22,43 @@ export default defineEventHandler(async (e) => {
}
const db = useDatabase();
const character = db.select({
id: characterTable.id,
name: characterTable.name,
progress: characterTable.progress,
owner: characterTable.owner,
visibility: characterTable.visibility,
}).from(characterTable).where(and(eq(characterTable.id, id), eq(characterTable.owner, session.user.id))).get();
const character = db.query.characterTable.findFirst({
with: {
abilities: true,
levels: true,
modifiers: true,
spells: true,
training: true,
user: {
columns: { username: true }
}
},
where: (character, { eq, and }) => and(eq(character.id, parseInt(id, 10)), eq(characterTable.owner, session.user!.id)),
}).sync();
if(character !== undefined)
{
return character as Character;
return {
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<MainStat, DoubleIndex<TrainingLevel>[]>),
leveling: character.levels.map(e => [e.level as Level, e.choice] as DoubleIndex<Level>),
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;
}
setResponseStatus(e, 404);

View File

@ -1,24 +1,26 @@
import { eq } from 'drizzle-orm';
import useDatabase from '~/composables/useDatabase';
import { characterTable } from '~/db/schema';
import { characterAbilitiesTable, characterLevelingTable, characterModifiersTable, characterSpellsTable, characterTable, characterTrainingTable } from '~/db/schema';
import { CharacterValidation, type Ability, type MainStat } from '~/types/character';
export default defineEventHandler(async (e) => {
const id = getRouterParam(e, "id");
if(!id)
const params = getRouterParam(e, "id");
if(!params)
{
setResponseStatus(e, 400);
return;
}
const id = parseInt(params, 10);
const body = await readBody(e);
if(!body)
const body = await readValidatedBody(e, CharacterValidation.safeParse);
if(!body.success)
{
setResponseStatus(e, 400);
return;
return body.error.message;
}
const db = useDatabase();
const old = db.select({ id: characterTable.id, owner: characterTable.owner }).from(characterTable).where(eq(characterTable.id, parseInt(id))).get();
const old = db.select({ id: characterTable.id, owner: characterTable.owner }).from(characterTable).where(eq(characterTable.id, id)).get();
if(!old)
{
@ -33,10 +35,38 @@ export default defineEventHandler(async (e) => {
return;
}
db.update(characterTable).set({
name: body.name,
progress: body.progress,
}).where(eq(characterTable.id, parseInt(id))).run();
db.transaction((tx) => {
tx.update(characterTable).set({
name: body.data.name,
people: body.data.people!,
level: body.data.level,
aspect: body.data.aspect,
notes: body.data.notes,
health: body.data.health,
mana: body.data.mana,
visibility: body.data.visibility,
thumbnail: body.data.thumbnail,
}).where(eq(characterTable.id, id)).run();
tx.delete(characterLevelingTable).where(eq(characterLevelingTable.character, id)).run();
tx.delete(characterTrainingTable).where(eq(characterTrainingTable.character, id)).run();
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();
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 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] })));
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] }));
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] }));
if(abilities.length > 0) tx.insert(characterAbilitiesTable).values(abilities).run();
});
await useStorage('cache').removeItem(`nitro:functions:character:${id}.json`);

View File

@ -1,9 +1,7 @@
import { and, eq } from 'drizzle-orm';
import useDatabase from '~/composables/useDatabase';
import { characterTable } from '~/db/schema';
import type { Ability, Character, CharacterConfig, CompiledCharacter, DoubleIndex, Feature, MainStat, TrainingLevel, TrainingOption } from '~/types/character';
import { defaultCharacter, type Ability, type Character, type CharacterConfig, type CompiledCharacter, type DoubleIndex, type Feature, type Level, type MainStat, type TrainingLevel, type TrainingOption } from '~/types/character';
import characterData from '#shared/character-config.json';
import { users } from '~/drizzle/schema';
import { group } from '~/shared/general.util';
export default defineEventHandler(async (e) => {
const id = getRouterParam(e, "id");
@ -14,109 +12,55 @@ export default defineEventHandler(async (e) => {
}
const db = useDatabase();
const character = db.select({
id: characterTable.id,
name: characterTable.name,
progress: characterTable.progress,
owner: characterTable.owner,
username: users.username
}).from(characterTable).leftJoin(users, eq(characterTable.owner, users.id)).where(and(eq(characterTable.id, parseInt(id)))).get();
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(character as Character & { username: string });
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<MainStat, DoubleIndex<TrainingLevel>[]>),
leveling: character.levels.map(e => [e.level as Level, e.choice] as DoubleIndex<Level>),
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' } */);
/*
Athlétisme
La capacité à effectuer un acte physique intense ou prolongé. Permet de pousser, contraindre, nager, courir.
Force + Constitution.
Acrobatique
La capacité à se mouvoir avec souplesse sous la contrainte. Permet d'escalader, d'enjamber, de sauter.
Force + Dextérité.
Intimidation
La capacité à intimider et inspirer la crainte.
Force + Charisme.
Doigté
La capacité à faire des actions précises avec ses mains. Permet de voler à la tire, de crocheter.
Dextérité + Dextérité.
Discrétion
La capacité à dissimuler sa présence. Permet de se cacher, de se mouvoir sans bruit.
Dextérité + Dextérité.
Survie
La capacité à survivre dans des conditions difficiles. Permet de pister, de collecter de la nourriture, de retrouver son chemin.
Constitution + Psyché.
Enquête
La capacité à demander au MJ de l'aide parce que vous puez la merde.
Intelligence + Curiosité.
Histoire
La capacité à connaitre le passé du monde.
Intelligence + Curiosité.
Religion
La capacité a connaitre les pratiques et les coutumes religieuses.
Intelligence + Curiosité.
Arcanes
La capacité à comprendre et percevoir la magie. Permet de comprendre un sort en cours, de détecter de la magie.
Intelligence + Psyché.
Compréhension
La capacité à déterminer les intentions des interlocuteurs. Permet de déceler des mensonges, de l'influence.
Intelligence + Charisme.
Perception
La capacité à observer le monde à travers ces sens. Permet d'observer, d'entendre, de sentir.
Curiosité + Curiosité.
Représentation
La capacité à se mettre en scène et à utiliser les arts. Permet de se produire en spectacle, de jouer d'un instrument, de chanter, de danser.
Curiosité + Charisme.
Médicine
La capacité à apporter des soins. Permet de stabiliser un joueur mourant, de soigner durant un repos.
Curiosité + Psyché.
Persuasion
Charisme + Psyché.
Dressage
Charisme + Psyché.
Mensonge
Charisme + Psyché.
*/
function compileCharacter(character: Character & { username?: string }): CompiledCharacter
{
const config = characterData as CharacterConfig;
const race = character.progress.race.index !== undefined ? config.peoples[character.progress.race.index] : undefined;
const raceOptions = race ? character.progress.race.progress!.map(e => race.options[e[0]][e[1]]) : [];
const features = Object.entries(config.training).map(e => [e[0], getFeaturesOf(e[0] as MainStat, character.progress.training[e[0] as MainStat])]) as [MainStat, TrainingOption[]][];
const 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,
@ -125,9 +69,13 @@ function compileCharacter(character: Character & { username?: string }): Compile
name: character.name,
health: raceOptions.reduce((p, v) => p + (v.health ?? 0), 0),
mana: raceOptions.reduce((p, v) => p + (v.mana ?? 0), 0),
race: character.progress.race.index ?? -1,
modifier: features.map(e => [e[0], Math.floor((e[1].length - 1) / 3) + (character.progress.modifiers[e[0]] ?? 0)] as [MainStat, number]).reduce((p, v) => { p[v[0]] = v[1]; return p }, {} as Record<MainStat, number>),
level: character.progress.level,
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<MainStat, number>),
level: character.level,
values: {
health: character.health,
mana: character.mana
},
features: {
action: [],
reaction: [],
@ -161,7 +109,7 @@ function compileCharacter(character: Character & { username?: string }): Compile
precision: 0,
arts: 0,
},
spells: character.progress.spells ?? [],
spells: character.spells ?? [],
speed: false,
defense: {
static: 6,
@ -194,13 +142,13 @@ function compileCharacter(character: Character & { username?: string }): Compile
},
initiative: 0,
aspect: "",
notes: character.progress.notes,
notes: character.notes ?? "",
};
features.forEach(e => e[1].forEach((_e, i) => applyTrainingOption(e[0], _e, compiled, i === e[1].length - 1)));
specialFeatures(compiled, character.progress.training);
specialFeatures(compiled, character.training);
Object.entries(character.progress.abilities).forEach(e => compiled.abilities[e[0] as Ability]! += e[1][0]);
Object.entries(character.abilities).forEach(e => compiled.abilities[e[0] as Ability]! += e[1][0]);
return compiled;
}

View File

@ -11,7 +11,7 @@ export default defineEventHandler(async (e) => {
}
const db = useDatabase();
const old = db.select().from(characterTable).where(eq(characterTable.id, parseInt(id))).get();
const old = db.select().from(characterTable).where(eq(characterTable.id, parseInt(id, 10))).get();
if(!old)
{

View File

@ -22,7 +22,7 @@ export default defineEventHandler(async (e) => {
const db = useDatabase();
const character = db.select({
values: characterTable.values
}).from(characterTable).where(and(eq(characterTable.id, id), eq(characterTable.owner, session.user.id))).get();
}).from(characterTable).where(and(eq(characterTable.id, parseInt(id, 10)), eq(characterTable.owner, session.user.id))).get();
if(character !== undefined)
{

View File

@ -18,7 +18,7 @@ export default defineEventHandler(async (e) => {
}
const db = useDatabase();
const old = db.select({ id: characterTable.id, owner: characterTable.owner }).from(characterTable).where(eq(characterTable.id, parseInt(id))).get();
const old = db.select({ id: characterTable.id, owner: characterTable.owner }).from(characterTable).where(eq(characterTable.id, parseInt(id, 10))).get();
if(!old)
{
@ -35,7 +35,7 @@ export default defineEventHandler(async (e) => {
db.update(characterTable).set({
values: body,
}).where(eq(characterTable.id, parseInt(id))).run();
}).where(eq(characterTable.id, parseInt(id, 10))).run();
setResponseStatus(e, 200);
return;

View File

@ -12,6 +12,21 @@ export function unifySlug(slug: string | string[]): string
{
return (Array.isArray(slug) ? slug.join('/') : slug);
}
export function group<
T,
K extends keyof T,
V extends keyof T,
KeyType extends string | number | symbol = Extract<T[K], string | number | symbol>
>(
table: T[],
key: K & (T[K] extends string | number | symbol ? K : never),
value: V
): Record<KeyType, T[V]> {
return table.reduce((p, v) => {
p[v[key] as KeyType] = v[value];
return p;
}, {} as Record<KeyType, T[V]>);
}
export function parsePath(path: string): string
{
return path.toLowerCase().trim().replaceAll(" ", "-").normalize("NFD").replaceAll(/[\u0300-\u036f]/g, "").replaceAll('(', '').replaceAll(')', '');

View File

@ -1,14 +1,35 @@
export type MainStat = "strength" | "dexterity" | "constitution" | "intelligence" | "curiosity" | "charisma" | "psyche";
export type Ability = "athletics" | "acrobatics" | "intimidation" | "sleightofhand" | "stealth" | "survival" | "investigation" | "history" | "religion" | "arcana" | "understanding" | "perception" | "performance" | "medecine" | "persuasion" | "animalhandling" | "deception";
export type Level = | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20;
export type TrainingLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15;
export type SpellType = "precision" | "knowledge" | "instinct" | "arts";
export type Category = "action" | "reaction" | "freeaction" | "misc";
export type SpellElement = "fire" | "ice" | "thunder" | "earth" | "arcana" | "air" | "nature" | "light" | "psyche";
export type Resistance = keyof CompiledCharacter["resistance"];
import { z, type ZodRawShape } from "zod";
import { characterTable } from "~/db/schema";
export const MAIN_STATS = ["strength","dexterity","constitution","intelligence","curiosity","charisma","psyche"] as const; export type MainStat = typeof MAIN_STATS[number];
export const ABILITIES = ["athletics","acrobatics","intimidation","sleightofhand","stealth","survival","investigation","history","religion","arcana","understanding","perception","performance","medecine","persuasion","animalhandling","deception"] as const; export type Ability = typeof ABILITIES[number];
export const LEVELS = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20] as const; export type Level = typeof LEVELS[number];
export const TRAINING_LEVELS = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15] as const; export type TrainingLevel = typeof TRAINING_LEVELS[number];
export const SPELL_TYPES = ["precision","knowledge","instinct","arts"] as const; export type SpellType = typeof SPELL_TYPES[number];
export const CATEGORIES = ["action","reaction","freeaction","misc"] as const; export type Category = typeof CATEGORIES[number];
export const SPELL_ELEMENTS = ["fire","ice","thunder","earth","arcana","air","nature","light","psyche"] as const; export type SpellElement = typeof SPELL_ELEMENTS[number];
export const RESISTANCES = ["stun","bleed","poison","fear","influence","charm","possesion","precision","knowledge","instinct"] as const; export type Resistance = typeof RESISTANCES[number];
export type DoubleIndex<T extends number | string> = [T, number];
export const defaultCharacter: Character = {
id: -1,
name: "",
people: undefined,
level: 1,
health: 0,
mana: 0,
training: MAIN_STATS.reduce((p, v) => { p[v] = [[0, 0]]; return p; }, {} as Record<MainStat, DoubleIndex<TrainingLevel>[]>),
leveling: [[1, 0]],
abilities: {},
spells: [],
modifiers: {},
owner: -1,
visibility: "private",
};
export const mainStatTexts: Record<MainStat, string> = {
"strength": "Force",
"dexterity": "Dextérité",
@ -31,31 +52,58 @@ export const elementTexts: Record<SpellElement, { class: string, text: string }>
}
export const spellTypeTexts: Record<SpellType, string> = { "instinct": "Instinct", "knowledge": "Savoir", "precision": "Précision", "arts": "Oeuvres" };
export type Progression = {
training: Record<MainStat, DoubleIndex<TrainingLevel>[]>;
race: {
index?: number;
progress?: DoubleIndex<Level>[];
};
level: number;
abilities: Partial<Record<Ability, [number, number]>>; //First is the ability, second is the max increment
spells?: string[]; //Spell ID
modifiers: Partial<Record<MainStat, number>>;
aspect?: string;
notes: string;
};
export const CharacterValidation = z.object({
id: z.number(),
name: z.string(),
people: z.number().nullable(),
level: z.number().min(1).max(20),
aspect: z.number().nullable().optional(),
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<MainStat, z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>>)),
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<Ability, z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>)).partial(),
spells: z.string().array(),
modifiers: z.object(MAIN_STATS.reduce((p, v) => {
p[v] = z.number();
return p;
}, {} as Record<MainStat, z.ZodNumber>)).partial(),
owner: z.number(),
username: z.string().optional(),
visibility: z.enum(["public", "private"]),
thumbnail: z.any(),
})
export type Character = {
id: number;
name: string;
progress: Progression;
values: CharacterValues;
owner?: number;
people?: number;
level: number;
aspect?: number | null;
notes?: string | null;
health: number;
mana: number;
training: Record<MainStat, DoubleIndex<TrainingLevel>[]>;
leveling: DoubleIndex<Level>[];
abilities: Partial<Record<Ability, [number, number]>>; //First is the ability, second is the max increment
spells: string[]; //Spell ID
modifiers: Partial<Record<MainStat, number>>;
owner: number;
username?: string;
visibility: "private" | "public";
};
export type CharacterValues = {
hp: number;
health: number;
mana: number;
armor: number;
};
export type CharacterConfig = {
peoples: Race[],
@ -171,6 +219,8 @@ export type CompiledCharacter = {
initiative: number;
spells: string[];
values: CharacterValues,
defense: {
static: number;
activeparry: number;
@ -190,18 +240,8 @@ export type CompiledCharacter = {
magicelement: number;
};
resistance: { //First is attack, second is defense
stun: [number, number];
bleed: [number, number];
poison: [number, number];
fear: [number, number];
influence: [number, number];
charm: [number, number];
possesion: [number, number];
precision: [number, number];
knowledge: [number, number];
instinct: [number, number];
};
//First is attack, second is defense
resistance: Record<Resistance, [number, number]>;
modifier: Record<MainStat, number>;
abilities: Partial<Record<Ability, number>>;