Homebrew manager completed !

This commit is contained in:
Clément Pons 2025-08-26 15:27:47 +02:00
parent 80a94bee86
commit da93fcd82d
14 changed files with 1173 additions and 244 deletions

BIN
db.sqlite

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -52,7 +52,7 @@ export const characterTable = table("character", {
id: int().primaryKey({ autoIncrement: true }),
name: text().notNull(),
owner: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
people: int().notNull(),
people: text().notNull(),
level: int().notNull().default(1),
aspect: int(),
notes: text(),

View File

@ -0,0 +1,20 @@
PRAGMA foreign_keys=OFF;--> statement-breakpoint
CREATE TABLE `__new_character` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL,
`owner` integer NOT NULL,
`people` text NOT NULL,
`level` integer DEFAULT 1 NOT NULL,
`aspect` integer,
`notes` text,
`health` integer DEFAULT 0 NOT NULL,
`mana` integer DEFAULT 0 NOT NULL,
`visibility` text DEFAULT 'private' NOT NULL,
`thumbnail` blob,
FOREIGN KEY (`owner`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
);
--> statement-breakpoint
INSERT INTO `__new_character`("id", "name", "owner", "people", "level", "aspect", "notes", "health", "mana", "visibility", "thumbnail") SELECT "id", "name", "owner", "people", "level", "aspect", "notes", "health", "mana", "visibility", "thumbnail" FROM `character`;--> statement-breakpoint
DROP TABLE `character`;--> statement-breakpoint
ALTER TABLE `__new_character` RENAME TO `character`;--> statement-breakpoint
PRAGMA foreign_keys=ON;

View File

@ -0,0 +1,810 @@
{
"version": "6",
"dialect": "sqlite",
"id": "6651137c-a198-4538-86be-7cb8b88ca998",
"prevId": "8f89d284-71da-46ae-a282-538f8a901294",
"tables": {
"character_abilities": {
"name": "character_abilities",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"ability": {
"name": "ability",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"max": {
"name": "max",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {},
"foreignKeys": {
"character_abilities_character_character_id_fk": {
"name": "character_abilities_character_character_id_fk",
"tableFrom": "character_abilities",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_abilities_character_ability_pk": {
"columns": [
"character",
"ability"
],
"name": "character_abilities_character_ability_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character_choices": {
"name": "character_choices",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"id": {
"name": "id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"choice": {
"name": "choice",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_choices_character_character_id_fk": {
"name": "character_choices_character_character_id_fk",
"tableFrom": "character_choices",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_choices_character_id_choice_pk": {
"columns": [
"character",
"id",
"choice"
],
"name": "character_choices_character_id_choice_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character_leveling": {
"name": "character_leveling",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"level": {
"name": "level",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"choice": {
"name": "choice",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_leveling_character_character_id_fk": {
"name": "character_leveling_character_character_id_fk",
"tableFrom": "character_leveling",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_leveling_character_level_pk": {
"columns": [
"character",
"level"
],
"name": "character_leveling_character_level_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character_modifiers": {
"name": "character_modifiers",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"modifier": {
"name": "modifier",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {},
"foreignKeys": {
"character_modifiers_character_character_id_fk": {
"name": "character_modifiers_character_character_id_fk",
"tableFrom": "character_modifiers",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_modifiers_character_modifier_pk": {
"columns": [
"character",
"modifier"
],
"name": "character_modifiers_character_modifier_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character_spell": {
"name": "character_spell",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_spell_character_character_id_fk": {
"name": "character_spell_character_character_id_fk",
"tableFrom": "character_spell",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_spell_character_value_pk": {
"columns": [
"character",
"value"
],
"name": "character_spell_character_value_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character": {
"name": "character",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"people": {
"name": "people",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"level": {
"name": "level",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 1
},
"aspect": {
"name": "aspect",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"notes": {
"name": "notes",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"health": {
"name": "health",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"mana": {
"name": "mana",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"visibility": {
"name": "visibility",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'private'"
},
"thumbnail": {
"name": "thumbnail",
"type": "blob",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_owner_users_id_fk": {
"name": "character_owner_users_id_fk",
"tableFrom": "character",
"tableTo": "users",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character_training": {
"name": "character_training",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"stat": {
"name": "stat",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"level": {
"name": "level",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"choice": {
"name": "choice",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_training_character_character_id_fk": {
"name": "character_training_character_character_id_fk",
"tableFrom": "character_training",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_training_character_stat_level_pk": {
"columns": [
"character",
"stat",
"level"
],
"name": "character_training_character_stat_level_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"email_validation": {
"name": "email_validation",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"project_content": {
"name": "project_content",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"content": {
"name": "content",
"type": "blob",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"project_files": {
"name": "project_files",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"path": {
"name": "path",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"navigable": {
"name": "navigable",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"private": {
"name": "private",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"order": {
"name": "order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"project_files_path_unique": {
"name": "project_files_path_unique",
"columns": [
"path"
],
"isUnique": true
}
},
"foreignKeys": {
"project_files_owner_users_id_fk": {
"name": "project_files_owner_users_id_fk",
"tableFrom": "project_files",
"tableTo": "users",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_permissions": {
"name": "user_permissions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"permission": {
"name": "permission",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_permissions_id_users_id_fk": {
"name": "user_permissions_id_users_id_fk",
"tableFrom": "user_permissions",
"tableTo": "users",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"user_permissions_id_permission_pk": {
"columns": [
"id",
"permission"
],
"name": "user_permissions_id_permission_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_sessions": {
"name": "user_sessions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_sessions_user_id_users_id_fk": {
"name": "user_sessions_user_id_users_id_fk",
"tableFrom": "user_sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"user_sessions_id_user_id_pk": {
"columns": [
"id",
"user_id"
],
"name": "user_sessions_id_user_id_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users_data": {
"name": "users_data",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"signin": {
"name": "signin",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"lastTimestamp": {
"name": "lastTimestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"users_data_id_users_id_fk": {
"name": "users_data_id_users_id_fk",
"tableFrom": "users_data",
"tableTo": "users",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"hash": {
"name": "hash",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"state": {
"name": "state",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {
"users_username_unique": {
"name": "users_username_unique",
"columns": [
"username"
],
"isUnique": true
},
"users_email_unique": {
"name": "users_email_unique",
"columns": [
"email"
],
"isUnique": true
},
"users_hash_unique": {
"name": "users_hash_unique",
"columns": [
"hash"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@ -106,6 +106,13 @@
"when": 1753175811770,
"tag": "0014_careless_nick_fury",
"breakpoints": true
},
{
"idx": 15,
"version": "6",
"when": 1756214160038,
"tag": "0015_typical_blade",
"breakpoints": true
}
]
}

View File

@ -58,6 +58,7 @@ export default defineEventHandler(async (e) => {
modifiers: true,
spells: true,
training: true,
choices: true,
user: {
columns: { username: true }
}
@ -80,9 +81,10 @@ export default defineEventHandler(async (e) => {
training: character.training.reduce((p, v) => { if(!(v.stat in p)) p[v.stat] = []; p[v.stat].push([v.level as TrainingLevel, v.choice]); return p; }, {} as Record<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"),
abilities: group(character.abilities, "ability", "value"),
spells: character.spells.map(e => e.value),
modifiers: group(character.modifiers, "modifier", "value"),
choices: character.choices.reduce((p, v) => { p[v.id] ??= []; p[v.id]?.push(v.choice); return p; }, {} as Record<string, number[]>),
owner: character.owner,
username: character.user.username,

View File

@ -1,149 +0,0 @@
import useDatabase from '~/composables/useDatabase';
import { type Character, type CharacterConfig, type CompiledCharacter, type DoubleIndex, type Level, type MainStat, type TrainingLevel, type TrainingOption } from '~/types/character';
import characterData from '#shared/character-config.json';
import { group } from '#shared/general.util';
import { defaultCharacter, MAIN_STATS } from '#shared/character.util';
export default defineCachedEventHandler(async (e) => {
const id = getRouterParam(e, "id");
if(!id)
{
setResponseStatus(e, 400);
return;
}
const db = useDatabase();
const character = db.query.characterTable.findFirst({
with: {
abilities: true,
levels: true,
modifiers: true,
spells: true,
training: true,
user: {
columns: { username: true }
}
},
where: (character, { eq }) => eq(character.id, parseInt(id, 10)),
}).sync();
if(character !== undefined)
{
return compileCharacter(Object.assign(defaultCharacter, {
id: character.id,
name: character.name,
people: character.people,
level: character.level,
aspect: character.aspect,
notes: character.notes,
health: character.health,
mana: character.mana,
training: character.training.reduce((p, v) => { if(!(v.stat in p)) p[v.stat] = []; p[v.stat].push([v.level as TrainingLevel, v.choice]); return p; }, {} as Record<MainStat, DoubleIndex<TrainingLevel>[]>),
leveling: character.levels.map(e => [e.level as Level, e.choice] as DoubleIndex<Level>),
abilities: group(character.abilities.map(e => ({ ...e, value: [e.value, e.max] as [number, number] })), "ability", "value"),
spells: character.spells.map(e => e.value),
modifiers: group(character.modifiers, "modifier", "value"),
owner: character.owner,
username: character.user.username,
visibility: character.visibility,
} as Character) as Character);
}
setResponseStatus(e, 404);
return;
}, { name: "character", getKey: (e) => getRouterParam(e, "id") || 'error' });
function compileCharacter(character: Character & { username?: string }): CompiledCharacter
{
const config = characterData as CharacterConfig;
const race = character.people !== undefined ? config.peoples[character.people] : undefined;
const raceOptions = race ? character.leveling!.map(e => race.options[e[0]][e[1]]) : [];
const features = Object.entries(config.training).map(e => [e[0], getFeaturesOf(e[0] as MainStat, character.training[e[0] as MainStat])]) as [MainStat, TrainingOption[]][];
const compiled: CompiledCharacter = {
id: character.id,
owner: character.owner,
username: character.username,
name: character.name,
health: raceOptions.reduce((p, v) => p + (v.health ?? 0), 0),
mana: raceOptions.reduce((p, v) => p + (v.mana ?? 0), 0),
race: character.people!,
modifier: features.map(e => [e[0], Math.floor((e[1].length - 1) / 3) + (character.modifiers[e[0]] ?? 0)] as [MainStat, number]).reduce((p, v) => { p[v[0]] = v[1]; return p }, {} as Record<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,
},
resistance: {},//Object.fromEntries(MAIN_STATS.map(e => [e as MainStat, [0, 0]])) as Record<MainStat, [number, number]>,
initiative: 0,
aspect: "",
notes: character.notes ?? "",
};
//features.forEach(e => e[1].forEach(_e => _e.features?.forEach(f => applyFeature(compiled, f))));
return compiled;
}
export function getFeaturesOf(stat: MainStat, progression: DoubleIndex<TrainingLevel>[]): TrainingOption[]
{
const config = characterData as CharacterConfig;
return progression.map(e => config.training[stat][e[0]][e[1]]);
}

View File

@ -745,8 +745,9 @@
}
]
},
"peoples": [
{
"peoples": {
"e662m19q590kn4dowvssowi1qf8ia7sk": {
"id": "e662m19q590kn4dowvssowi1qf8ia7sk",
"name": "Humain",
"description": "Les humains, originaire d'un tout autre monde, ont subit un cataclysme qui les a projeté dans les terres d'Erina. En tant que civilisation dépourvue de magie, ils sont plus specialisés, gagnant moins de statistiques mais pouvant plus tôt ou plus fréquemment obtenir certains bonus.",
"options": {
@ -833,8 +834,75 @@
"9q8mf0u06oxxwqltyv58kbavs7qtoouw"
]
}
},
"3v3rwsn9bimpyd2fc95ml8wdrrmfsqb0": {
"id": "3v3rwsn9bimpyd2fc95ml8wdrrmfsqb0",
"name": "Quplothien",
"description": "Quploth est la région du monde abritant le plus de marchands et charlatans. Dû à la sur-désertification de leurs terres, ils ont appris à vivre en troquant les richesses. Leurs cités, denses et prospères, sont peu nombreuses et suscitent un tourisme culturel croissant.",
"options": {
"1": [
"bbuzw6awn2mkb05imdoecxfkc5zuj98i"
],
"2": [
"ub2ws6q8xdbngeouip02umvw2oox9r68"
],
"3": [
"5d7u2jvi4u0nnrzesderha3uo8kb3zjq"
],
"4": [
"8w4jthjrn3l8u4trmj46z6t6ab5rbgk3"
],
"5": [
"z9lux6nlhl8pjhcwst6bnhpn6cq6c77w"
],
"6": [
"dx5khvrhwkhhn8fv4b8pecuh8i5wtwij"
],
"7": [
"pfzopr4oyrsgxg0cbva16zzzly3kke9z"
],
"8": [
"fk0wmg94tlq78khq8zot2o5u4nnxr2gb"
],
"9": [
"u9vv3z280jgzab7pjwe9kexqjlpoxvax"
],
"10": [
"fuxn9ndabr5yl0rrdtilldssmjxso24p"
],
"11": [
"7kdxs6b6j9pqhgm3c8ydp8f9o074vp8q"
],
"12": [
"dwvjqspm8l0gnks6y7u9vty0563u20kd"
],
"13": [
"bmh55yfypfw8rezd16m2cuocrx0kkfl4"
],
"14": [
"hatuas1yl3armteqwjwm1gjpsjp3v97x"
],
"15": [
"0fg543b25uppvollu9oxtowyxjjw1x5a"
],
"16": [
"l770gvirwzfbtfgfl29dxhvdh95f1m71"
],
"17": [
"m1zkviiz3ow1g7rwpkyygmyggphvoz8b"
],
"18": [
"uuc8vci5bk5kkx23a7ks1gu778fmu9w1"
],
"19": [
"fd076hnyjagipaez166p9xp3wtlf5sgw"
],
"20": [
"qcp28eysi3l3n438v41kowisdpq4ht61"
]
}
}
],
},
"training": {
"strength": {
"0": [
@ -9452,6 +9520,106 @@
}
],
"id": "9q8mf0u06oxxwqltyv58kbavs7qtoouw"
},
"bbuzw6awn2mkb05imdoecxfkc5zuj98i": {
"id": "bbuzw6awn2mkb05imdoecxfkc5zuj98i",
"description": "Bonjour",
"effect": []
},
"ub2ws6q8xdbngeouip02umvw2oox9r68": {
"id": "ub2ws6q8xdbngeouip02umvw2oox9r68",
"description": "je",
"effect": []
},
"5d7u2jvi4u0nnrzesderha3uo8kb3zjq": {
"id": "5d7u2jvi4u0nnrzesderha3uo8kb3zjq",
"description": "suis",
"effect": []
},
"8w4jthjrn3l8u4trmj46z6t6ab5rbgk3": {
"id": "8w4jthjrn3l8u4trmj46z6t6ab5rbgk3",
"description": "Nicolas",
"effect": []
},
"z9lux6nlhl8pjhcwst6bnhpn6cq6c77w": {
"id": "z9lux6nlhl8pjhcwst6bnhpn6cq6c77w",
"description": "Sarkozy",
"effect": []
},
"dx5khvrhwkhhn8fv4b8pecuh8i5wtwij": {
"id": "dx5khvrhwkhhn8fv4b8pecuh8i5wtwij",
"description": "",
"effect": []
},
"pfzopr4oyrsgxg0cbva16zzzly3kke9z": {
"id": "pfzopr4oyrsgxg0cbva16zzzly3kke9z",
"description": "",
"effect": []
},
"fk0wmg94tlq78khq8zot2o5u4nnxr2gb": {
"id": "fk0wmg94tlq78khq8zot2o5u4nnxr2gb",
"description": "",
"effect": []
},
"u9vv3z280jgzab7pjwe9kexqjlpoxvax": {
"id": "u9vv3z280jgzab7pjwe9kexqjlpoxvax",
"description": "",
"effect": []
},
"fuxn9ndabr5yl0rrdtilldssmjxso24p": {
"id": "fuxn9ndabr5yl0rrdtilldssmjxso24p",
"description": "",
"effect": []
},
"7kdxs6b6j9pqhgm3c8ydp8f9o074vp8q": {
"id": "7kdxs6b6j9pqhgm3c8ydp8f9o074vp8q",
"description": "",
"effect": []
},
"dwvjqspm8l0gnks6y7u9vty0563u20kd": {
"id": "dwvjqspm8l0gnks6y7u9vty0563u20kd",
"description": "",
"effect": []
},
"bmh55yfypfw8rezd16m2cuocrx0kkfl4": {
"id": "bmh55yfypfw8rezd16m2cuocrx0kkfl4",
"description": "",
"effect": []
},
"hatuas1yl3armteqwjwm1gjpsjp3v97x": {
"id": "hatuas1yl3armteqwjwm1gjpsjp3v97x",
"description": "",
"effect": []
},
"0fg543b25uppvollu9oxtowyxjjw1x5a": {
"id": "0fg543b25uppvollu9oxtowyxjjw1x5a",
"description": "",
"effect": []
},
"l770gvirwzfbtfgfl29dxhvdh95f1m71": {
"id": "l770gvirwzfbtfgfl29dxhvdh95f1m71",
"description": "",
"effect": []
},
"m1zkviiz3ow1g7rwpkyygmyggphvoz8b": {
"id": "m1zkviiz3ow1g7rwpkyygmyggphvoz8b",
"description": "",
"effect": []
},
"uuc8vci5bk5kkx23a7ks1gu778fmu9w1": {
"id": "uuc8vci5bk5kkx23a7ks1gu778fmu9w1",
"description": "",
"effect": []
},
"fd076hnyjagipaez166p9xp3wtlf5sgw": {
"id": "fd076hnyjagipaez166p9xp3wtlf5sgw",
"description": "",
"effect": []
},
"qcp28eysi3l3n438v41kowisdpq4ht61": {
"id": "qcp28eysi3l3n438v41kowisdpq4ht61",
"description": "",
"effect": []
}
}
}

View File

@ -428,7 +428,7 @@ export class CharacterBuilder extends CharacterCompiler
}
private render()
{
/*this._steps = [
this._steps = [
PeoplePicker,
LevelPicker,
TrainingPicker,
@ -439,7 +439,7 @@ export class CharacterBuilder extends CharacterCompiler
dom("div", { class: "group flex items-center", }, [
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue disabled:text-light-50 dark:disabled:text-dark-50 disabled:hover:border-transparent group-data-[state=active]:text-accent-blue cursor-pointer", listeners: { click: () => this.display(i) } }, [text(e.header)]),
])
);*/
);
this._helperText = text("Choisissez un peuple afin de définir la progression de votre personnage au fil des niveaux.")
this._content = dom('div', { class: 'flex-1 outline-none max-w-full w-full overflow-y-auto', attributes: { id: 'characterEditorContainer' } });
this._container.appendChild(div('flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden', [
@ -460,15 +460,15 @@ export class CharacterBuilder extends CharacterCompiler
if(step < 0 || step >= this._stepsHeader.length)
return;
//if(step !== 0 && this._steps.slice(0, step).some(e => !e.validate(this)))
// return;
if(step !== 0 && this._steps.slice(0, step).some(e => !e.validate(this)))
return;
//this._stepsHeader.forEach(e => e.setAttribute('data-state', 'inactive'));
//this._stepsHeader[step]!.setAttribute('data-state', 'active');
this._stepsHeader.forEach(e => e.setAttribute('data-state', 'inactive'));
this._stepsHeader[step]!.setAttribute('data-state', 'active');
//this._content?.replaceChildren(...(new this._steps[step]!(this)).dom);
this._content?.replaceChildren(...(new this._steps[step]!(this)).dom);
//this._helperText.textContent = this._steps[step]!.description;
this._helperText.textContent = this._steps[step]!.description;
}
async save(leave: boolean = true)
{
@ -681,7 +681,7 @@ abstract class BuilderTab {
};
type BuilderTabConstructor = {
new (builder: CharacterBuilder): BuilderTab;
name: string;
header: string;
description: string;
validate(builder: CharacterBuilder): boolean;
}
@ -691,8 +691,6 @@ class PeoplePicker extends BuilderTab
private _visibilityInput: HTMLDivElement;
private _options: HTMLDivElement[];
private _activeOption?: HTMLDivElement;
static override header = 'Peuple';
static override description = 'Choisissez un peuple afin de définir la progression de votre personnage au fil des niveaux.';
@ -704,16 +702,15 @@ class PeoplePicker extends BuilderTab
input: (value) => {
this._builder.character.name = value ?? '';
document.title = `d[any] - Edition de ${this._builder.character.name || 'nouveau personnage'}`;
}
}, defaultValue: this._builder.character.name
});
this._visibilityInput = toggle({ defaultValue: this._builder.character.visibility === "private", change: (value) => this._builder.character.visibility = value ? "private" : "public" });
this._options = config.peoples.map(
this._options = Object.values(config.peoples).map(
(people, i) => dom("div", { class: "flex flex-col flex-nowrap gap-2 p-2 border border-light-35 dark:border-dark-35 cursor-pointer hover:border-light-70 dark:hover:border-dark-70 w-[320px]", listeners: { click: () => {
this._builder.character.people = i;
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, false));
this._activeOption = this._options[i]!;
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, true));
this._builder.character.people = people.id;
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._options.forEach(f => f?.classList.toggle(e, false)));
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._options[i]?.classList.toggle(e, true));
}
} }, [div("h-[320px]"), div("text-xl font-bold text-center", [text(people.name)]), div("w-full border-b border-light-50 dark:border-dark-50"), div("text-wrap word-break", [text(people.description)])]),
);
@ -734,13 +731,6 @@ class PeoplePicker extends BuilderTab
{
this._nameInput.value = this._builder.character.name;
this._visibilityInput.setAttribute('data-state', this._builder.character.visibility === "private" ? "checked" : "unchecked");
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, false));
if(this._builder.character.people !== undefined)
{
this._activeOption = this._options[this._builder.character.people]!;
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, true));
}
}
static override validate(builder: CharacterBuilder): boolean
{

View File

@ -47,10 +47,10 @@ export function select<T extends NonNullable<any>>(options: Array<{ text: string
if(e === undefined)
return;
return dom('div', { listeners: { click: () => {
return dom('div', { listeners: { click: (_e) => {
textValue.textContent = e.text;
settings?.change && settings?.change(e.value);
context && context.close && context.close();
context && context.close && !_e.ctrlKey && context.close();
}, mouseenter: (e) => focus(i) }, class: ['data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option] }, [ text(e.text) ]);
});
const select = dom('div', { listeners: { click: () => {
@ -116,12 +116,12 @@ export function multiselect<T extends NonNullable<any>>(options: Array<{ text: s
if(e === undefined)
return;
const element = dom('div', { listeners: { click: () => {
const element = dom('div', { listeners: { click: (_e) => {
selection = selection.includes(e.value) ? selection.filter(f => f !== e.value) : [...selection, e.value];
textValue.textContent = selection.length > 0 ? ((options.find(f => f?.value === selection[0])?.text ?? '') + (selection.length > 1 ? ` +${selection.length - 1}` : '')) : '';
element.toggleAttribute('data-selected', selection.includes(e.value));
settings?.change && settings?.change(selection);
context && context.close && context.close();
context && context.close && !_e.ctrlKey && context.close();
}, mouseenter: (e) => focus(i) }, class: ['group flex flex-row justify-between items-center data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option], attributes: { 'data-selected': selection.includes(e.value) } }, [ text(e.text), icon('radix-icons:check', { class: 'hidden group-data-[selected]:block', noobserver: true }) ]);
return element;
});
@ -240,11 +240,11 @@ export function combobox<T extends NonNullable<any>>(options: Option<T>[], setti
}
else
{
return { item: option, dom: dom('div', { listeners: { click: () => {
return { item: option, dom: dom('div', { listeners: { click: (_e) => {
select.value = option.text;
settings?.change && settings?.change(option.value as T);
selected = true;
hide();
!_e.ctrlKey && hide();
}, mouseenter: () => focus(option.value) }, class: ['data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option] }, [ option?.render ? option?.render() : text(option.text) ]) };
}
}
@ -347,10 +347,10 @@ export function numberpicker(settings?: { defaultValue?: number, change?: (value
switch(e.key)
{
case "ArrowUp":
validateAndChange(storedValue + (e.shiftKey ? 10 : 1)) && settings?.input && settings.input(storedValue);
validateAndChange(storedValue + (e.ctrlKey ? 10 : 1)) && settings?.input && settings.input(storedValue);
break;
case "ArrowDown":
validateAndChange(storedValue - (e.shiftKey ? 10 : 1)) && settings?.input && settings.input(storedValue);
validateAndChange(storedValue - (e.ctrlKey ? 10 : 1)) && settings?.input && settings.input(storedValue);
break;
case "PageUp":
settings?.max && validateAndChange(settings.max) && settings?.input && settings.input(storedValue);
@ -371,12 +371,23 @@ export function numberpicker(settings?: { defaultValue?: number, change?: (value
return field;
}
// Open by default
export function foldable(content: NodeChildren, title: NodeChildren, settings?: { open?: boolean, class?: { container?: Class, title?: Class, content?: Class, icon?: Class } })
export function foldable(content: NodeChildren | (() => NodeChildren), title: NodeChildren, settings?: { open?: boolean, class?: { container?: Class, title?: Class, content?: Class, icon?: Class } })
{
let _content: NodeChildren;
const display = (state: boolean) => {
if(state && !_content)
{
_content = typeof content === 'function' ? content() : content;
//@ts-ignore
contentContainer.replaceChildren(..._content);
}
}
const contentContainer = div(['hidden group-data-[active]:flex', settings?.class?.content]);
const fold = div(['group flex flex-1 w-full flex-col', settings?.class?.container], [
div('flex', [ dom('div', { listeners: { click: () => fold.toggleAttribute('data-active') }, class: ['flex justify-center items-center', settings?.class?.icon] }, [ icon('radix-icons:caret-right', { class: 'group-data-[active]:rotate-90 origin-center' }) ]), div(['flex-1', settings?.class?.title], title) ]),
div(['hidden group-data-[active]:flex', settings?.class?.content], content),
div('flex', [ dom('div', { listeners: { click: () => { display(fold.toggleAttribute('data-active')) } }, class: ['flex justify-center items-center', settings?.class?.icon] }, [ icon('radix-icons:caret-right', { class: 'group-data-[active]:rotate-90 origin-center', noobserver: true }) ]), div(['flex-1', settings?.class?.title], title) ]),
contentContainer
]);
display(settings?.open ?? true);
fold.toggleAttribute('data-active', settings?.open ?? true);
return fold;
}

View File

@ -1,10 +1,10 @@
import type { Ability, AspectConfig, CharacterConfig, Feature, FeatureEffect, FeatureItem, MainStat, Resistance, SpellConfig, TrainingLevel } from "~/types/character";
import type { Ability, AspectConfig, CharacterConfig, Feature, FeatureEffect, FeatureItem, Level, MainStat, RaceConfig, Resistance, SpellConfig, TrainingLevel } from "~/types/character";
import { div, dom, icon, text, type NodeChildren } from "#shared/dom.util";
import { MarkdownEditor } from "#shared/editor.util";
import { fakeA } from "#shared/proses";
import { button, combobox, foldable, input, multiselect, numberpicker, select, table, toggle, type Option } from "#shared/components.util";
import { confirm, contextmenu, fullblocker, tooltip } from "#shared/floating.util";
import { ALIGNMENTS, alignmentTexts, elementTexts, MAIN_STATS, mainStatShortTexts, mainStatTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts } from "#shared/character.util";
import { ALIGNMENTS, alignmentTexts, elementTexts, LEVELS, MAIN_STATS, mainStatShortTexts, mainStatTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts } from "#shared/character.util";
import characterConfig from "#shared/character-config.json";
import { getID, ID_SIZE } from "#shared/general.util";
import renderMarkdown, { renderText } from "#shared/markdown.util";
@ -100,24 +100,73 @@ abstract class BuilderTab {
};
class PeopleEditor extends BuilderTab
{
private _options: HTMLDivElement[];
private _activeOption?: HTMLDivElement;
constructor(builder: HomebrewBuilder, config: CharacterConfig)
{
super(builder, config);
this._options = config.peoples.map(
(people, i) => dom("div", { class: "flex flex-col flex-nowrap gap-2 p-2 border border-light-35 dark:border-dark-35 cursor-pointer hover:border-light-70 dark:hover:border-dark-70 w-[320px]", listeners: { click: () => {
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, false));
this._activeOption = this._options[i]!;
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, true));
const add = () => {
const people: RaceConfig = {
id: getID(ID_SIZE),
name: '',
description: '',
options: LEVELS.map(e => {
const feature: Feature = {
id: getID(ID_SIZE),
description: '',
effect: [],
}
config.features[feature.id] = feature;
return [e, [feature.id]] as [Level, string[]];
}).reduce((p, v) => { p[v[0]] = v[1]; return p }, {} as Record<Level, string[]>)
};
config.peoples[people.id] = people;
(this._content[0] as HTMLDivElement).appendChild(peopleRender(people));
}
const remove = (people: RaceConfig) => {
confirm('Voulez vous vraiment supprimer cet aspect ?').then(e => {
if(e)
{
Object.values(people.options).forEach(e => e.forEach(id => delete config.features[id]));
delete config.peoples[people.id];
}
} }, [div("h-[320px]"), div("text-xl font-bold text-center", [text(people.name)]), div("w-full border-b border-light-50 dark:border-dark-50"), div("text-wrap word-break", [text(people.description)])]),
);
this._content = [ div('flex flex-1 gap-4 p-2 overflow-x-auto justify-center', this._options) ];
})
}
const render = (people: string, level: Level, feature: string) => {
let element = dom("div", { class: ["border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-50 dark:hover:border-dark-50"], listeners: { click: e => {
this._builder.edit(config.features[feature]!).then(e => {
element.replaceChildren(markdownUtil(config.features[feature]!.description, undefined, { tags: { a: fakeA } }));
});
}, contextmenu: (e) => {
e.preventDefault();
const context = contextmenu(e.clientX, e.clientY, [
dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => {
context.close();
const _feature: Feature = { id: getID(ID_SIZE), description: '', effect: [] };
config.features[_feature.id] = _feature;
config.peoples[people]!.options[level]!.push(_feature.id);
element.parentElement?.appendChild(render(people, level, _feature.id));
} } }, [ text('Nouveau') ]),
config.peoples[people]!.options[level].length > 1 ? dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => {
context.close();
confirm('Voulez-vous vraiment supprimer cet element ?').then(e => { if(e) {
config.peoples[people]!.options[level] = config.peoples[people]!.options[level].filter(e => e !== feature);
delete config.features[feature];
element.remove();
}
}) } } }, [ text('Supprimer') ]) : undefined,
], { placement: "right-start", priority: false });
}}}, [ markdownUtil(config.features[feature]!.description, undefined, { tags: { a: fakeA } }) ]);
return element;
}
const peopleRender = (people: RaceConfig) => {
return foldable(() => Object.entries(people.options).flatMap(level => [ div("w-full flex h-px", [div("border-t border-dashed border-light-50 dark:border-dark-50 w-full"), dom('span', { class: "relative" }, [ text(level[0]) ])]),
div("flex flex-row gap-4 justify-center", level[1].map((option) => render(people.id, parseInt(level[0], 10) as Level, option))),
]), [ input('text', { defaultValue: people.name, input: (value) => people.name = value, class: 'w-32' }), input('text', { defaultValue: people.description, input: (value) => people.description = value, class: 'w-full' }) ], { class: { container: 'gap-2 max-h-full', title: 'flex flex-row', content: 'flex flex-shrink-0 flex-col gap-4 relative w-full overflow-y-auto px-8' }, open: false })
}
const container = div('flex flex-col gap-2', Object.values(config.peoples).map(peopleRender));
this._content = [ div('flex flex-col py-2 gap-2', [ div('w-full flex flex-row-reverse', [ button(icon('radix-icons:plus'), add, 'p-1') ]), container ]) ];
}
}
class TrainingEditor extends BuilderTab
@ -131,24 +180,37 @@ class TrainingEditor extends BuilderTab
constructor(builder: HomebrewBuilder, config: CharacterConfig)
{
super(builder, config);
const render = (stat: MainStat, level: TrainingLevel, feature: string) => {
let element = dom("div", { class: ["border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-50 dark:hover:border-dark-50"], listeners: { click: e => {
this._builder.edit(config.features[feature]!).then(e => {
element.replaceChildren(markdownUtil(config.features[feature]!.description, undefined, { tags: { a: fakeA } }));
});
}, contextmenu: (e) => {
e.preventDefault();
const context = contextmenu(e.clientX, e.clientY, [
dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => {
context.close();
const _feature: Feature = { id: getID(ID_SIZE), description: '', effect: [] };
config.features[_feature.id] = _feature;
config.training[stat][level].push(_feature.id);
element.parentElement?.appendChild(render(stat, level, _feature.id));
} } }, [ text('Nouveau') ]),
config.training[stat][level].length > 1 ? dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => {
context.close();
confirm('Voulez-vous vraiment supprimer cet element ?').then(e => { if(e) {
config.training[stat][level as any as TrainingLevel] = config.training[stat][level as any as TrainingLevel].filter(e => e !== feature);
delete config.features[feature];
element.remove();
}
}) } } }, [ text('Supprimer') ]) : undefined,
], { placement: "right-start", priority: false });
}}}, [ markdownUtil(config.features[feature]!.description, undefined, { tags: { a: fakeA } }) ]);
return element;
};
const statRenderBlock = (stat: MainStat) => {
return Object.entries(config.training[stat]).map(
(level) => [ div("w-full flex h-px", [div("border-t border-dashed border-light-50 dark:border-dark-50 w-full"), dom('span', { class: "relative" }, [ text(level[0]) ])]),
div("flex flex-row gap-4 justify-center", level[1].map((option, j) => {
let element = dom("div", { class: ["border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-50 dark:hover:border-dark-50"], listeners: { click: e => {
this._builder.edit(config.features[option]!).then(e => {
element.replaceChildren(markdownUtil(config.features[option]!.description, undefined, { tags: { a: fakeA } }));
});
}, contextmenu: (e) => {
e.preventDefault();
const context = contextmenu(e.clientX, e.clientY, [
dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => { context.close(); } } }, [ text('Nouveau avant') ]),
dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => { context.close(); } } }, [ text('Nouveau après') ]),
dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => { context.close(); confirm('Voulez-vous vraiment supprimer cet element ?').then(e => { if(e) { delete config.training[stat][level[0] as any as TrainingLevel]; /* redraw */ } }) } } }, [ text('Supprimer') ])
], { placement: "right-start", priority: false });
}}}, [ markdownUtil(config.features[option]!.description, undefined, { tags: { a: fakeA } }) ]);
return element;
})),
div("flex flex-row gap-4 justify-center", level[1].map((option) => render(stat, parseInt(level[0], 10) as TrainingLevel, option))),
])
}
@ -229,11 +291,16 @@ class AspectEditor extends BuilderTab
content = element;
};
const remove = (aspect: AspectConfig) => {
config.aspects = config.aspects.filter(e => e !== aspect);
confirm('Voulez vous vraiment supprimer cet aspect ?').then(e => {
if(e)
{
config.aspects = config.aspects.filter(e => e !== aspect);
const element = redraw();
content.parentElement?.replaceChild(element, content);
content = element;
const element = redraw();
content.parentElement?.replaceChild(element, content);
content = element;
}
})
}
const redraw = () => table(config.aspects.map(render), { name: 'Nom', description: 'Description', stat: 'Buff de stat', alignment: 'Alignement', magic: 'Magie', difficulty: 'Difficulté', physic: 'Physique', mental: 'Mental', personality: 'Caractère', action: 'Actions' }, { class: { table: 'flex-1' } });
let content = redraw();
@ -247,19 +314,15 @@ class SpellEditor extends BuilderTab
super(builder, config);
const render = (spell: SpellConfig) => {
return {
id: spell.id,
name: input('text', { input: (value) => spell.name = value, defaultValue: spell.name, class: '!m-0 w-full' }),
rank: select([{ text: 'Rang 1', value: 1 }, { text: 'Rang 2', value: 2 }, { text: 'Rang 3', value: 3 }, { text: 'Spécial', value: 4 }], { change: (value: 1 | 2 | 3 | 4) => spell.rank = value, defaultValue: spell.rank, class: { container: '!m-0 w-full' } }),
type: select(SPELL_TYPES.map(f => ({ text: spellTypeTexts[f], value: f })), { change: (value) => spell.type = value, defaultValue: spell.type, class: { container: '!m-0 w-full' } }),
cost: numberpicker({ defaultValue: spell.cost, input: (value) => spell.cost = value, class: '!m-0 w-full' }),
speed: select<'action' | 'reaction' | number>([{ text: 'Action', value: 'action' }, { text: 'Reaction', value: 'reaction' }, { text: '1 minute', value: 1 }, { text: '10 minutes', value: 10 }], { change: (value) => spell.speed = value, defaultValue: spell.speed, class: { container: '!m-0 w-full' } }),
elements: multiselect(SPELL_ELEMENTS.map(f => ({ text: elementTexts[f].text, value: f })), { change: (value) => spell.elements = value, defaultValue: spell.elements, class: { container: '!m-0 w-full' } }),
effect: input('text', { input: (value) => spell.effect = value, defaultValue: spell.effect, class: '!m-0 w-full' }),
tags: multiselect([{ text: 'Dégâts', value: 'damage' }, { text: 'Buff', value: 'buff' }, { text: 'Debuff', value: 'debuff' }, { text: 'Support', value: 'support' }, { text: 'Tank', value: 'tank' }, { text: 'Mouvement', value: 'movement' }, { text: 'Utilitaire', value: 'utilitary' }], { change: (value) => spell.tags = value, defaultValue: spell.tags, class: { container: '!m-0 w-full' } }),
concentration: toggle({ change: (value) => spell.concentration = value, defaultValue: spell.concentration, class: { container: '!m-0' } }),
action: div('flex flex-row justify-center gap-2', [ button(icon('radix-icons:trash'), () => remove(spell), 'p-1') ])
};
return foldable([
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Rang'), select([{ text: 'Rang 1', value: 1 }, { text: 'Rang 2', value: 2 }, { text: 'Rang 3', value: 3 }, { text: 'Spécial', value: 4 }], { change: (value: 1 | 2 | 3 | 4) => spell.rank = value, defaultValue: spell.rank, class: { container: '!m-0 !h-9 w-full' } }), ]),
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Type'), select(SPELL_TYPES.map(f => ({ text: spellTypeTexts[f], value: f })), { change: (value) => spell.type = value, defaultValue: spell.type, class: { container: '!m-0 !h-9 w-full' } }), ]),
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Coût'), numberpicker({ defaultValue: spell.cost, input: (value) => spell.cost = value, class: '!m-0 w-full' }), ]),
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Incantation'), select<'action' | 'reaction' | number>([{ text: 'Action', value: 'action' }, { text: 'Reaction', value: 'reaction' }, { text: '1 minute', value: 1 }, { text: '10 minutes', value: 10 }], { change: (value) => spell.speed = value, defaultValue: spell.speed, class: { container: '!m-0 !h-9 w-full' } }), ]),
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Elements'), multiselect(SPELL_ELEMENTS.map(f => ({ text: elementTexts[f].text, value: f })), { change: (value) => spell.elements = value, defaultValue: spell.elements, class: { container: '!m-0 !h-9 w-full' } }), ]),
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Tags'), multiselect([{ text: 'Dégâts', value: 'damage' }, { text: 'Buff', value: 'buff' }, { text: 'Debuff', value: 'debuff' }, { text: 'Support', value: 'support' }, { text: 'Tank', value: 'tank' }, { text: 'Mouvement', value: 'movement' }, { text: 'Utilitaire', value: 'utilitary' }], { change: (value) => spell.tags = value, defaultValue: spell.tags, class: { container: '!m-0 !h-9 w-full' } }), ]),
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Concentration'), toggle({ change: (value) => spell.concentration = value, defaultValue: spell.concentration, class: { container: '!m-0 !flex-none' } }), ]),
], [ div('gap-4 px-4 flex', [ input('text', { input: (value) => spell.name = value, defaultValue: spell.name, class: '!m-0 w-64' }), input('text', { input: (value) => spell.effect = value, defaultValue: spell.effect, class: '!m-0 w-full' }),div('flex flex-row justify-center gap-2', [ button(icon('radix-icons:trash', { noobserver: true }), () => remove(spell), 'p-1') ]) ]) ], { class: { container: 'border-light-35 dark:border-dark-35 py-1', content: 'gap-2 px-4 py-1 flex items-center *:flex-1' }, open: false });
}
const add = () => {
config.spells.push({
@ -280,13 +343,19 @@ class SpellEditor extends BuilderTab
content = element;
};
const remove = (spell: SpellConfig) => {
config.spells = config.spells.filter(e => e !== spell);
confirm('Voulez vous vraiment supprimer ce sort ?').then(e => {
if(e)
{
config.spells = config.spells.filter(e => e !== spell);
const element = redraw();
content.parentElement?.replaceChild(element, content);
content = element;
const element = redraw();
content.parentElement?.replaceChild(element, content);
content = element;
}
});
}
const redraw = () => table(config.spells.map(render), { id: 'ID', name: 'Nom', rank: 'Rang', type: 'Type', cost: 'Coût', speed: 'Incantation', elements: 'Elements', effect: 'Effet', tags: 'Tag', concentration: 'Concentration', action: 'Actions' }, { class: { table: 'flex-1' } });
const redraw = () => div('flex flex-col divide-y', config.spells.map(render));
//, { class: { table: 'flex-1' } });
let content = redraw();
this._content = [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), add, 'p-1') ]), content ] ) ];
}

View File

@ -16,7 +16,7 @@ export type Character = {
id: number;
name: string;
people?: number;
people?: string;
level: number;
aspect?: number;
notes?: string | null;
@ -44,7 +44,7 @@ export type CharacterVariables = {
equipment: Array<string>;
};
export type CharacterConfig = {
peoples: RaceConfig[];
peoples: Record<string, RaceConfig>;
resistances: Record<Resistance, { name: string, statistic: MainStat }>;
training: Record<MainStat, Record<TrainingLevel, FeatureID[]>>;
abilities: Record<Ability, AbilityConfig>;
@ -72,6 +72,7 @@ export type AbilityConfig = {
description: string;
};
export type RaceConfig = {
id: string;
name: string;
description: string;
options: Record<Level, FeatureID[]>;
@ -126,7 +127,7 @@ export type CompiledCharacter = {
name: string;
health: number; //Max
mana: number; //Max
race: number;
race: string;
spellslots: number; //Max
artslots: number; //Max
spellranks: Record<SpellType, 0 | 1 | 2 | 3>;