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