This commit is contained in:
Peaceultime 2025-12-08 18:50:51 +01:00
commit 97578132bb
20 changed files with 1499 additions and 311 deletions

View File

@ -51,7 +51,7 @@ export const characterTable = table("character", {
people: text().notNull(),
level: int().notNull().default(1),
variables: text({ mode: 'json' }).notNull().default('{"health": 0,"mana": 0,"spells": [],"items": [],"exhaustion": 0,"sickness": [],"poisons": []}'),
aspect: int(),
aspect: text().notNull(),
public_notes: text(),
private_notes: text(),

View File

@ -23,7 +23,7 @@ definePageMeta({
const job = ref<string>('');
const payload = reactive<Record<string, any>>({
data: JSON.stringify({ username: "Peaceultime", id: 1, timestamp: Date.now() }),
data: JSON.stringify({ username: "Peaceultime", id: 1, userId: 1, timestamp: Date.now() }),
to: 'clem31470@gmail.com',
});
const data = ref(), status = ref<'idle' | 'pending' | 'success' | 'error'>('idle'), success = ref(false), error = ref<Error | null>();

View File

@ -30,7 +30,7 @@ export type Character = {
name: string; //Free text
people?: string; //People ID
level: number;
aspect?: number;
aspect?: string; //Aspect ID
notes?: { public?: string, private?: string }; //Free text
training: Record<MainStat, Partial<Record<TrainingLevel, number>>>;
@ -68,8 +68,8 @@ type ItemState = {
export type CharacterConfig = {
peoples: Record<string, RaceConfig>;
training: Record<MainStat, Record<TrainingLevel, FeatureID[]>>;
spells: SpellConfig[];
aspects: AspectConfig[];
spells: Record<string, SpellConfig>;
aspects: Record<string, AspectConfig>;
features: Record<FeatureID, Feature>;
enchantments: Record<string, EnchantementConfig>; //TODO
items: Record<string, ItemConfig>;
@ -142,6 +142,7 @@ export type RaceConfig = {
options: Record<Level, FeatureID[]>;
};
export type AspectConfig = {
id: string;
name: string;
description: string; //TODO -> TextID
stat: MainStat | 'special';

BIN
db.sqlite

Binary file not shown.

View File

@ -5,8 +5,7 @@ CREATE TABLE `campaign_logs` (
`type` text,
`details` text NOT NULL,
PRIMARY KEY(`id`, `from`, `timestamp`),
FOREIGN KEY (`id`) REFERENCES `campaign`(`id`) ON UPDATE cascade ON DELETE cascade,
FOREIGN KEY (`from`)
FOREIGN KEY (`id`) REFERENCES `campaign`(`id`) ON UPDATE cascade ON DELETE cascade
);
--> statement-breakpoint
ALTER TABLE `campaign` ADD `status` text DEFAULT 'PREPARING';--> statement-breakpoint

View File

@ -6,8 +6,7 @@ CREATE TABLE `__new_campaign_logs` (
`type` text,
`details` text NOT NULL,
PRIMARY KEY(`id`, `from`, `timestamp`),
FOREIGN KEY (`id`) REFERENCES `campaign`(`id`) ON UPDATE cascade ON DELETE cascade,
FOREIGN KEY (`from`)
FOREIGN KEY (`id`) REFERENCES `campaign`(`id`) ON UPDATE cascade ON DELETE cascade
);
--> statement-breakpoint
INSERT INTO `__new_campaign_logs`("id", "from", "timestamp", "type", "details") SELECT "id", "from", "timestamp", "type", "details" FROM `campaign_logs`;--> statement-breakpoint

View File

@ -6,8 +6,7 @@ CREATE TABLE `__new_campaign_logs` (
`type` text,
`details` text NOT NULL,
PRIMARY KEY(`id`, `target`, `timestamp`),
FOREIGN KEY (`id`) REFERENCES `campaign`(`id`) ON UPDATE cascade ON DELETE cascade,
FOREIGN KEY (`target`)
FOREIGN KEY (`id`) REFERENCES `campaign`(`id`) ON UPDATE cascade ON DELETE cascade
);
--> statement-breakpoint
INSERT INTO `__new_campaign_logs`("id", "target", "timestamp", "type", "details") SELECT "id", "target", "timestamp", "type", "details" FROM `campaign_logs`;--> statement-breakpoint

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,
`variables` text DEFAULT '{"health": 0,"mana": 0,"spells": [],"items": [],"exhaustion": 0,"sickness": [],"poisons": []}' NOT NULL,
`aspect` text NOT NULL,
`public_notes` text,
`private_notes` text,
`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", "variables", "aspect", "public_notes", "private_notes", "visibility", "thumbnail") SELECT "id", "name", "owner", "people", "level", "variables", "aspect", "public_notes", "private_notes", "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,995 @@
{
"version": "6",
"dialect": "sqlite",
"id": "fdee27cd-0188-4e54-bc2c-a96a375e83a1",
"prevId": "4ef0ea2b-0c07-438c-901f-0ef64bb5f749",
"tables": {
"campaign_characters": {
"name": "campaign_characters",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"campaign_characters_id_campaign_id_fk": {
"name": "campaign_characters_id_campaign_id_fk",
"tableFrom": "campaign_characters",
"tableTo": "campaign",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
},
"campaign_characters_character_character_id_fk": {
"name": "campaign_characters_character_character_id_fk",
"tableFrom": "campaign_characters",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"campaign_characters_id_character_pk": {
"columns": [
"id",
"character"
],
"name": "campaign_characters_id_character_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"campaign_logs": {
"name": "campaign_logs",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"target": {
"name": "target",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"details": {
"name": "details",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"campaign_logs_id_campaign_id_fk": {
"name": "campaign_logs_id_campaign_id_fk",
"tableFrom": "campaign_logs",
"tableTo": "campaign",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"campaign_logs_id_target_timestamp_pk": {
"columns": [
"id",
"target",
"timestamp"
],
"name": "campaign_logs_id_target_timestamp_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"campaign_members": {
"name": "campaign_members",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user": {
"name": "user",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"campaign_members_id_campaign_id_fk": {
"name": "campaign_members_id_campaign_id_fk",
"tableFrom": "campaign_members",
"tableTo": "campaign",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
},
"campaign_members_user_users_id_fk": {
"name": "campaign_members_user_users_id_fk",
"tableFrom": "campaign_members",
"tableTo": "users",
"columnsFrom": [
"user"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"campaign_members_id_user_pk": {
"columns": [
"id",
"user"
],
"name": "campaign_members_id_user_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"campaign": {
"name": "campaign",
"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
},
"link": {
"name": "link",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'PREPARING'"
},
"settings": {
"name": "settings",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'{}'"
},
"inventory": {
"name": "inventory",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'[]'"
},
"money": {
"name": "money",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
},
"public_notes": {
"name": "public_notes",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "''"
},
"dm_notes": {
"name": "dm_notes",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "''"
}
},
"indexes": {},
"foreignKeys": {
"campaign_owner_users_id_fk": {
"name": "campaign_owner_users_id_fk",
"tableFrom": "campaign",
"tableTo": "users",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"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": {
"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
},
"variables": {
"name": "variables",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'{\"health\": 0,\"mana\": 0,\"spells\": [],\"items\": [],\"exhaustion\": 0,\"sickness\": [],\"poisons\": []}'"
},
"aspect": {
"name": "aspect",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"public_notes": {
"name": "public_notes",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"private_notes": {
"name": "private_notes",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"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

@ -176,6 +176,13 @@
"when": 1763479527696,
"tag": "0024_secret_arclight",
"breakpoints": true
},
{
"idx": 25,
"version": "6",
"when": 1764763792974,
"tag": "0025_majestic_grim_reaper",
"breakpoints": true
}
]
}

View File

@ -3,7 +3,6 @@ import { eq, or } from 'drizzle-orm';
import { z } from 'zod/v4';
import useDatabase from '~/composables/useDatabase';
import { usersTable } from '~/db/schema';
import sendMail from '~/../server/tasks/mail';
const schema = z.object({
profile: z.string(),
@ -33,7 +32,7 @@ export default defineEventHandler(async (e) => {
id, timestamp,
}
});
await sendMail({
await runTask('mail', {
payload: {
type: 'mail',
data: {

View File

@ -5,7 +5,6 @@ import { usersDataTable, usersTable } from '~/db/schema';
import { schema } from '~/schemas/registration';
import { checkSession, logSession } from '~/../server/utils/user';
import type { UserSession, UserSessionRequired } from '~/types/auth';
import sendMail from '~/../server/tasks/mail';
import type { $ZodIssue } from 'zod/v4/core';
interface SuccessHandler
@ -75,7 +74,7 @@ export default defineEventHandler(async (e): Promise<Return> => {
logSession(e, await setUserSession(e, { user: { id: id.id, username: body.data.username, email: body.data.email, state: 0, signin: new Date(), permissions: [], lastTimestamp: new Date() } }) as UserSessionRequired);
await sendMail({
await runTask('mail', {
payload: {
type: 'mail',
to: [body.data.email],

View File

@ -28,7 +28,7 @@ export default defineEventHandler(async (e) => {
const db = useDatabase();
const campaign = db.select({ id: campaignTable.id }).from(campaignMembersTable).innerJoin(campaignTable, eq(campaignMembersTable.id, campaignTable.id)).where(and(eq(campaignMembersTable.id, campaign_id), or(eq(campaignMembersTable.user, session.user.id), eq(campaignTable.owner, session.user.id)))).get();
const campaign = db.select({ id: campaignTable.id }).from(campaignTable).leftJoin(campaignMembersTable, eq(campaignTable.id, campaignMembersTable.id)).where(and(eq(campaignTable.id, campaign_id), or(eq(campaignMembersTable.user, session.user.id), eq(campaignTable.owner, session.user.id)))).get();
if(!campaign || campaign.id !== campaign_id)
return setResponseStatus(e, 404);

View File

@ -1,7 +1,6 @@
import { and, eq, notExists } from 'drizzle-orm';
import { and, eq, or } from 'drizzle-orm';
import useDatabase from '~/composables/useDatabase';
import { campaignCharactersTable, campaignMembersTable, campaignTable, characterTable } from '~/db/schema';
import { CharacterVariablesValidation } from '#shared/character.util';
export default defineEventHandler(async (e) => {
const _id = getRouterParam(e, "id");
@ -32,7 +31,7 @@ export default defineEventHandler(async (e) => {
if(!character || character.id !== id)
return setResponseStatus(e, 403);
const campaign = db.select({ id: campaignMembersTable.id }).from(campaignMembersTable).where(and(eq(campaignMembersTable.id, campaign_id), eq(campaignMembersTable.user, session.user.id))).get();
const campaign = db.select({ id: campaignTable.id }).from(campaignTable).leftJoin(campaignMembersTable, eq(campaignTable.id, campaignMembersTable.id)).where(and(eq(campaignTable.id, campaign_id), or(eq(campaignTable.owner, session.user.id), eq(campaignMembersTable.user, session.user.id)))).get();
if(!campaign || campaign.id !== campaign_id)
return setResponseStatus(e, 404);

View File

@ -2,7 +2,6 @@ import { hash } from "bun";
import { eq } from "drizzle-orm";
import useDatabase from "~/composables/useDatabase";
import { usersTable } from "~/db/schema";
import sendMail from '~/../server/tasks/mail';
export default defineEventHandler(async (e) => {
const session = await getUserSession(e);
@ -57,7 +56,7 @@ export default defineEventHandler(async (e) => {
id: emailId, timestamp,
}
});
await sendMail({
await runTask('mail', {
payload: {
type: 'mail',
to: [data.email],

View File

@ -56,7 +56,7 @@ export default defineTask({
throw new Error(`Données inconnues`);
}
const mailPayload = payload.data as MailPayload;
const mailPayload = payload as MailPayload;
const template = templates[mailPayload.template];
console.log(mailPayload);

File diff suppressed because one or more lines are too long

View File

@ -3,7 +3,7 @@ import { z } from "zod/v4";
import characterConfig from '#shared/character-config.json';
import proses, { preview } from "#shared/proses";
import { button, buttongroup, checkbox, floater, foldable, input, loading, multiselect, numberpicker, select, tabgroup, Toaster, toggle } from "#shared/components.util";
import { div, dom, icon, span, text } from "#shared/dom.util";
import { div, dom, icon, span, text, type DOMList } from "#shared/dom.util";
import { followermenu, fullblocker, tooltip } from "#shared/floating.util";
import { clamp } from "#shared/general.util";
import markdown from "#shared/markdown.util";
@ -51,7 +51,8 @@ export const defaultCharacter: Character = {
owner: -1,
visibility: "private",
};
const defaultCompiledCharacter: (character: Character) => CompiledCharacter = (character: Character) => ({
const defaultCompiledCharacter: (character: Character) => CompiledCharacter = (character: Character) => {
const compiled = {
id: character.id,
owner: character.owner,
username: character.username,
@ -94,12 +95,12 @@ const defaultCompiledCharacter: (character: Character) => CompiledCharacter = (c
spellslots: 0,
artslots: 0,
spellranks: {
instinct: 0,
knowledge: 0,
precision: 0,
arts: 0,
instinct: 0 as 0 | 1 | 2 | 3,
knowledge: 0 as 0 | 1 | 2 | 3,
precision: 0 as 0 | 1 | 2 | 3,
arts: 0 as 0 | 1 | 2 | 3,
},
speed: false,
speed: false as number | false,
defense: {
hardcap: Infinity,
static: 6,
@ -107,6 +108,9 @@ const defaultCompiledCharacter: (character: Character) => CompiledCharacter = (c
activedodge: 0,
passiveparry: 0,
passivedodge: 0,
get passive() { return clamp(compiled.defense.static + compiled.defense.passivedodge + compiled.defense.passiveparry, 0, compiled.defense.hardcap) },
get parry() { return clamp(compiled.defense.static + compiled.defense.passivedodge + compiled.defense.activeparry, 0, compiled.defense.hardcap) },
get dodge() { return clamp(compiled.defense.static + compiled.defense.activedodge + compiled.defense.passiveparry, 0, compiled.defense.hardcap) },
},
mastery: {
strength: 0,
@ -135,7 +139,9 @@ const defaultCompiledCharacter: (character: Character) => CompiledCharacter = (c
},
aspect: "",
notes: Object.assign({ public: '', private: '' }, character.notes),
});
}
return compiled;
};
export const mainStatTexts: Record<MainStat, string> = {
"strength": "Force",
"dexterity": "Dextérité",
@ -257,7 +263,7 @@ export const CharacterValidation = z.object({
name: z.string(),
people: z.string().nullable(),
level: z.number().min(1).max(20),
aspect: z.number().nullable().optional(),
aspect: z.string(),
notes: CharacterNotesValidation,
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()),
@ -286,6 +292,7 @@ export class CharacterCompiler
'modifier/psyche': { value: 0, _dirty: false, min: -Infinity, list: [] },
};
private _variableDirty: boolean = false;
private _variableDebounce: NodeJS.Timeout = setTimeout(() => {});
constructor(character: Character)
{
@ -353,11 +360,17 @@ export class CharacterCompiler
return substring;
})
}
variable<T extends keyof CharacterVariables>(prop: T, value: CharacterVariables[T])
variable<T extends keyof CharacterVariables>(prop: T, value: CharacterVariables[T], autosave: boolean = true)
{
this._character.variables[prop] = value;
this._result.variables[prop] = value;
this._variableDirty = true;
if(autosave)
{
clearTimeout(this._variableDebounce);
this._variableDebounce = setTimeout(() => this.saveVariables(), 2000);
}
}
saveVariables()
{
@ -716,7 +729,7 @@ export class CharacterBuilder extends CharacterCompiler
{
const feature = config.peoples[this._character.people!]!.options[level][this._character.leveling[level]!]!;
this.remove(feature);
if(this._character.choices.hasOwnProperty(feature)) delete this._character.choices[feature];
if(feature in this._character.choices) delete this._character.choices[feature];
this.add(config.peoples[this._character.people!]!.options[level][choice]);
this._character.leveling[level] = choice;
@ -868,8 +881,6 @@ class LevelPicker extends BuilderTab
{
private _levelInput: HTMLInputElement;
private _pointsInput: HTMLInputElement;
private _healthText: Text;
private _manaText: Text;
private _options: HTMLElement[][];
@ -886,7 +897,6 @@ class LevelPicker extends BuilderTab
this.updateLevel();
} });
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 = Object.entries(config.peoples[this._builder.character.people!]!.options).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 left-4" }, [ text(level[0]) ])]),
@ -913,11 +923,11 @@ class LevelPicker extends BuilderTab
]),
div("flex justify-center items-center gap-2 my-2 md:text-base text-sm", [
dom("span", { text: "Vie" }),
this._healthText,
text(this._builder, '{{compiled.health}}'),
]),
div("flex justify-center items-center gap-2 my-2 md:text-base text-sm", [
dom("span", { text: "Mana" }),
this._manaText,
text(this._builder, '{{compiled.mana}}'),
]),
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]))];
@ -926,12 +936,10 @@ class LevelPicker extends BuilderTab
}
override update()
{
const values = this._builder.values;
this._builder.compiled;
this._levelInput.value = this._builder.character.level.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';
this.updateLevel();
}
@ -957,8 +965,6 @@ class LevelPicker extends BuilderTab
class TrainingPicker extends BuilderTab
{
private _pointsInput: HTMLInputElement;
private _healthText: Text;
private _manaText: Text;
private _options: Record<MainStat, HTMLDivElement[][]>;
private _tab: number = 0;
@ -989,7 +995,6 @@ class TrainingPicker extends BuilderTab
}
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[][]>);
@ -1007,11 +1012,11 @@ class TrainingPicker extends BuilderTab
]),
div("flex justify-center items-center gap-2 my-2 md:text-base text-sm", [
dom("span", { text: "Vie" }),
this._healthText,
text(this._builder, '{{compiled.health}}'),
]),
div("flex justify-center items-center gap-2 my-2 md:text-base text-sm", [
dom("span", { text: "Mana" }),
this._manaText,
text(this._builder, '{{compiled.mana}}'),
]),
button(text('Suivant'), () => this._builder.display(3), 'h-[35px] px-[15px]'),
]), dom('span')
@ -1036,8 +1041,6 @@ class TrainingPicker extends BuilderTab
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;
@ -1146,8 +1149,8 @@ class AspectPicker extends BuilderTab
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 = Object.values(config.aspects).map((e, i) => dom('div', { attributes: { "data-aspect": e.id }, listeners: { click: () => {
this._builder.character.aspect = e.id;
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' }, [
@ -1211,10 +1214,10 @@ class AspectPicker extends BuilderTab
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]!;
const id = e.getAttribute('data-aspect')!;
const aspect = config.aspects[id]!;
e.setAttribute('data-state', this._builder.character.aspect === index ? 'active' : 'inactive');
e.setAttribute('data-state', this._builder.character.aspect === id ? 'active' : 'inactive');
if(!this._filter)
return true;
@ -1392,13 +1395,13 @@ export class CharacterSheet
};
this.tabs = tabgroup([
{ id: 'actions', title: [ text('Actions') ], content: () => this.actionsTab(character) },
{ id: 'actions', title: [ text('Actions') ], content: this.actionsTab(character) },
{ id: 'abilities', title: [ text('Aptitudes') ], content: () => this.abilitiesTab(character) },
{ id: 'abilities', title: [ text('Aptitudes') ], content: this.abilitiesTab(character) },
{ id: 'spells', title: [ text('Sorts') ], content: () => this.spellTab(character) },
{ id: 'spells', title: [ text('Sorts') ], content: this.spellTab(character) },
{ id: 'inventory', title: [ text('Inventaire') ], content: () => this.itemsTab(character) },
{ id: 'inventory', title: [ text('Inventaire') ], content: this.itemsTab(character) },
{ id: 'notes', title: [ text('Notes') ], content: () => [
div('flex flex-col gap-2', [
@ -1434,51 +1437,51 @@ export class CharacterSheet
dom("span", { class: "flex flex-row items-center gap-2 text-3xl font-light" }, [
text("PV: "),
health.readonly,
text(`/ ${character.health}`)
text(character, `/ {{health}}`),
]),
dom("span", { class: "flex flex-row items-center gap-2 text-3xl font-light" }, [
text("Mana: "),
mana.readonly,
text(`/ ${character.mana}`)
])
])
text(character, `/ {{mana}}`),
]),
]),
]),
div("self-center", [
this.user.value && this.user.value.id === character.owner ?
button(icon("radix-icons:pencil-2"), () => useRouter().push({ name: 'character-id-edit', params: { id: this.character?.character.id } }), "p-1")
: div()
])
: div(),
]),
]),
div("flex flex-row justify-center 2xl:gap-4 gap-2 p-4 border-b border-light-35 dark:border-dark-35", [
div("flex 2xl:gap-4 gap-2 flex-row items-center justify-between", [
div("flex flex-col items-center px-2", [
dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.modifier.strength}` }),
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(character, `+{{modifier.strength}}`) ]),
dom("span", { class: "text-sm 2xl:text-base", text: "Force" })
]),
div("flex flex-col items-center px-2", [
dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.modifier.dexterity}` }),
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(character, `+{{modifier.dexterity}}`) ]),
dom("span", { class: "text-sm 2xl:text-base", text: "Dextérité" })
]),
div("flex flex-col items-center px-2", [
dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.modifier.constitution}` }),
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(character, `+{{modifier.constitution}}`) ]),
dom("span", { class: "text-sm 2xl:text-base", text: "Constitution" })
]),
div("flex flex-col items-center px-2", [
dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.modifier.intelligence}` }),
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(character, `+{{modifier.intelligence}}`) ]),
dom("span", { class: "text-sm 2xl:text-base", text: "Intelligence" })
]),
div("flex flex-col items-center px-2", [
dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.modifier.curiosity}` }),
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(character, `+{{modifier.curiosity}}`) ]),
dom("span", { class: "text-sm 2xl:text-base", text: "Curiosité" })
]),
div("flex flex-col items-center px-2", [
dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.modifier.charisma}` }),
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(character, `+{{modifier.charisma}}`) ]),
dom("span", { class: "text-sm 2xl:text-base", text: "Charisme" })
]),
div("flex flex-col items-center px-2", [
dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.modifier.psyche}` }),
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(character, `+{{modifier.psyche}}`) ]),
dom("span", { class: "text-sm 2xl:text-base", text: "Psyché" })
])
]),
@ -1487,11 +1490,11 @@ export class CharacterSheet
div("flex 2xl:gap-4 gap-2 flex-row items-center justify-between", [
div("flex flex-col px-2 items-center", [
dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.initiative}` }),
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(character, `+{{initiative}}`) ]),
dom("span", { class: "text-sm 2xl:text-base", text: "Initiative" })
]),
div("flex flex-col px-2 items-center", [
dom("span", { class: "2xl:text-2xl text-xl font-bold", text: character.speed === false ? "Aucun déplacement" : `${character.speed} cases` }),
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(character, () => character.speed === false ? "Aucun déplacement" : `{{speed}} cases`) ]),
dom("span", { class: "text-sm 2xl:text-base", text: "Course" })
])
]),
@ -1501,24 +1504,15 @@ export class CharacterSheet
div("flex 2xl:gap-4 gap-2 flex-row items-center justify-between", [
icon("game-icons:checked-shield", { width: 32, height: 32 }),
div("flex flex-col px-2 items-center", [
dom("span", {
class: "2xl:text-2xl text-xl font-bold",
text: `${clamp(character.defense.static + character.defense.passivedodge + character.defense.passiveparry, 0, character.defense.hardcap)}`
}),
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(character, `{{defense.passive}}`) ]),
dom("span", { class: "text-sm 2xl:text-base", text: "Passive" })
]),
div("flex flex-col px-2 items-center", [
dom("span", {
class: "2xl:text-2xl text-xl font-bold",
text: `${clamp(character.defense.static + character.defense.passivedodge + character.defense.activeparry, 0, character.defense.hardcap)}`
}),
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(character, `{{defense.parry}}`) ]),
dom("span", { class: "text-sm 2xl:text-base", text: "Blocage" })
]),
div("flex flex-col px-2 items-center", [
dom("span", {
class: "2xl:text-2xl text-xl font-bold",
text: `${clamp(character.defense.static + character.defense.activedodge + character.defense.passiveparry, 0, character.defense.hardcap)}`
}),
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(character, `{{defense.dodge}}`) ]),
dom("span", { class: "text-sm 2xl:text-base", text: "Esquive" })
])
]),
@ -1533,10 +1527,10 @@ export class CharacterSheet
]),
div("grid grid-cols-2 gap-2",
Object.entries(character.abilities).map(([ability, value]) =>
Object.keys(character.abilities).map((ability) =>
div("flex flex-row px-1 justify-between items-center", [
span("text-sm text-light-70 dark:text-dark-70 max-w-20 truncate", abilityTexts[ability as Ability] || ability),
span("font-bold text-base text-light-100 dark:text-dark-100", `+${value}`),
span("font-bold text-base text-light-100 dark:text-dark-100", text(character.abilities, `+{{${ability}}}`)),
])
)
),
@ -1595,10 +1589,10 @@ export class CharacterSheet
div('flex flex-col gap-2', [
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Attaquer", "Désarmer", "Saisir", "Faire chuter", "Déplacer", "Courir", "Pas de coté", "Charger", "Lancer un sort", "S'interposer", "Se transformer", "Utiliser un objet", "Anticiper une action", "Improviser"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))),
...(character.lists.action?.map(e => div('flex flex-col gap-1', [
div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.action[e]?.name }), config.action[e]?.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: config.action[e]?.cost?.toString() }), text(`point${config.action[e]?.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
div('flex flex-col gap-2', { render: (e) => div('flex flex-col gap-1', [
div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.action[e]?.name }), config.action[e]?.cost ? div('flex flex-row gap-1', [dom('span', { class: 'font-bold', text: config.action[e]?.cost?.toString() }), text(`point${config.action[e]?.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
markdown(getText(config.action[e]?.description), undefined, { tags: { a: preview } }),
])) ?? [])
]), list: character.lists.action }),
]),
]),
div('flex flex-col gap-2', [
@ -1610,10 +1604,10 @@ export class CharacterSheet
div('flex flex-col gap-2', [
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Parer", "Esquiver", "Saisir une opportunité", "Prendre en tenaille", "Intercepter"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))),
...(character.lists.reaction?.map(e => div('flex flex-col gap-1', [
div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.reaction[e]?.name }), config.reaction[e]?.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: config.reaction[e]?.cost?.toString() }), text(`point${config.reaction[e]?.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
div('flex flex-col gap-2', { render: (e) => div('flex flex-col gap-1', [
div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.reaction[e]?.name }), config.reaction[e]?.cost ? div('flex flex-row gap-1', [dom('span', { class: 'font-bold', text: config.reaction[e]?.cost?.toString() }), text(`point${config.reaction[e]?.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
markdown(getText(config.reaction[e]?.description), undefined, { tags: { a: preview } }),
])) ?? [])
]), list: character.lists.reaction }),
]),
]),
div('flex flex-col gap-2', [
@ -1624,10 +1618,10 @@ export class CharacterSheet
div('flex flex-col gap-2', [
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Analyser une situation", "Communiquer", "Dégainer", "Attraper un objet"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))),
...(character.lists.freeaction?.map(e => div('flex flex-col gap-1', [
div('flex flex-col gap-2', { render: e => div('flex flex-col gap-1', [
div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.freeaction[e]?.name }) ]),
markdown(getText(config.freeaction[e]?.description), undefined, { tags: { a: preview } }),
])) ?? [])
]), list: character.lists.reaction })
]),
]),
]),
@ -1636,50 +1630,54 @@ export class CharacterSheet
abilitiesTab(character: CompiledCharacter)
{
return [
div('flex flex-col gap-2', [
...(character.lists.passive?.map(e => div('flex flex-col gap-1', [
div('flex flex-col gap-2', { render: e => div('flex flex-col gap-1', [
div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.passive[e]?.name }) ]),
markdown(getText(config.passive[e]?.description), undefined, { tags: { a: preview } }),
])) ?? []),
]),
]), list: character.lists.passive }),
];
}
spellTab(character: CompiledCharacter)
{
let sortPreference = (localStorage.getItem('character-sort') ?? 'rank') as 'rank' | 'type' | 'element';
const sort = (spells: Array<{ id: string, spell?: SpellConfig, source: string }>) => {
spells = spells.filter(e => !!e.spell);
const sort = () => {
switch(sortPreference)
{
case 'rank': return spells.sort((a, b) => a.spell!.rank - b.spell!.rank || SPELL_ELEMENTS.indexOf(a.spell!.elements[0]!) - SPELL_ELEMENTS.indexOf(b.spell!.elements[0]!));
case 'type': return spells.sort((a, b) => a.spell!.type.localeCompare(b.spell!.type) || a.spell!.rank - b.spell!.rank);
case 'element': return spells.sort((a, b) => SPELL_ELEMENTS.indexOf(a.spell!.elements[0]!) - SPELL_ELEMENTS.indexOf(b.spell!.elements[0]!) || a.spell!.rank - b.spell!.rank);
default: return spells;
case 'rank': return container.array.sort((a, b) => (config.spells[a]?.rank ?? 0) - (config.spells[b]?.rank ?? 0) || SPELL_ELEMENTS.indexOf(config.spells[a]?.elements[0]!) - SPELL_ELEMENTS.indexOf(config.spells[b]?.elements[0]!));
case 'type': return container.array.sort((a, b) => config.spells[a]?.type.localeCompare(config.spells[b]?.type ?? '') || (config.spells[a]?.rank ?? 0) - (config.spells[b]?.rank ?? 0));
case 'element': return container.array.sort((a, b) => SPELL_ELEMENTS.indexOf(config.spells[a]?.elements[0]!) - SPELL_ELEMENTS.indexOf(config.spells[b]?.elements[0]!) || (config.spells[a]?.rank ?? 0) - (config.spells[b]?.rank ?? 0));
default: return container.array;
}
};
const spells = sort([...(character.lists.spells ?? []).map(e => ({ id: e, spell: config.spells.find(_e => _e.id === e), source: 'feature' })), ...character.variables.spells.map(e => ({ id: e, spell: config.spells.find(_e => _e.id === e), source: 'player' }))]).map(e => ({...e, dom:
e.spell ? div('flex flex-col gap-2', [
div('flex flex-row items-center gap-4', [ dom('span', { class: 'font-semibold text-lg', text: e.spell.name ?? 'Inconnu' }), div('flex-1 border-b border-dashed border-light-50 dark:border-dark-50'), dom('span', { class: 'text-light-70 dark:text-dark-70', text: `${e.spell.cost ?? 0} mana` }) ]),
const container = div('flex flex-col gap-2', { render: e => {
const spell = config.spells[e];
if(!spell)
return;
return div('flex flex-col gap-2', [
div('flex flex-row items-center gap-4', [ dom('span', { class: 'font-semibold text-lg', text: spell.name ?? 'Inconnu' }), div('flex-1 border-b border-dashed border-light-50 dark:border-dark-50'), dom('span', { class: 'text-light-70 dark:text-dark-70', text: `${spell.cost ?? 0} mana` }) ]),
div('flex flex-row justify-between items-center gap-2 text-light-70 dark:text-dark-70', [
div('flex flex-row gap-2', [ span('flex flex-row', e.spell.rank === 4 ? 'Sort unique' : `Sort ${e.spell.type === 'instinct' ? 'd\'instinct' : e.spell.type === 'knowledge' ? 'de savoir' : 'de précision'} de rang ${e.spell.rank}`), ...(e.spell.elements ?? []).map(elementDom) ]),
div('flex flex-row gap-4 items-center', [ e.spell.concentration ? proses('a', preview, [span('italic text-sm', 'concentration')], { href: '' }) : undefined, span(undefined, typeof e.spell.range === 'number' && e.spell.range > 0 ? `${e.spell.range} case${e.spell.range > 1 ? 's' : ''}` : e.spell.range === 0 ? 'toucher' : 'personnel'), span(undefined, typeof e.spell.speed === 'number' ? `${e.spell.speed} minute${e.spell.speed > 1 ? 's' : ''}` : e.spell.speed) ])
div('flex flex-row gap-2', [ span('flex flex-row', spell.rank === 4 ? 'Sort unique' : `Sort ${spell.type === 'instinct' ? 'd\'instinct' : spell.type === 'knowledge' ? 'de savoir' : 'de précision'} de rang ${spell.rank}`), ...(spell.elements ?? []).map(elementDom) ]),
div('flex flex-row gap-4 items-center', [ spell.concentration ? proses('a', preview, [span('italic text-sm', 'concentration')], { href: '' }) : undefined, span(undefined, typeof spell.range === 'number' && spell.range > 0 ? `${spell.range} case${spell.range > 1 ? 's' : ''}` : spell.range === 0 ? 'toucher' : 'personnel'), span(undefined, typeof spell.speed === 'number' ? `${spell.speed} minute${spell.speed > 1 ? 's' : ''}` : spell.speed) ])
]),
div('flex flex-row ps-4 p-1 border-l-4 border-light-35 dark:border-dark-35', [ markdown(e.spell.description) ]),
]) : undefined }));
div('flex flex-row ps-4 p-1 border-l-4 border-light-35 dark:border-dark-35', [ markdown(spell.description) ]),
])
}, list: [...(character.lists.spells ?? []), ...character.variables.spells] });
sort().render();
return [
div('flex flex-col gap-2', [
div('flex flex-row justify-between items-center', [
div('flex flex-row gap-2 items-center', [
dom('span', { class: 'italic tracking-tight text-sm', text: 'Trier par' }),
buttongroup<'rank' | 'type' | 'element'>([{ text: 'Rang', value: 'rank' }, { text: 'Type', value: 'type' }, { text: 'Element', value: 'element' }], { value: sortPreference, class: { option: 'px-2 py-1 text-sm' }, onChange: (value) => { localStorage.setItem('character-sort', value); sortPreference = value; this.tabs?.refresh(); } }),
buttongroup<'rank' | 'type' | 'element'>([{ text: 'Rang', value: 'rank' }, { text: 'Type', value: 'type' }, { text: 'Element', value: 'element' }], { value: sortPreference, class: { option: 'px-2 py-1 text-sm' }, onChange: (value) => { localStorage.setItem('character-sort', value); sortPreference = value; sort().render(); } }),
]),
div('flex flex-row gap-2 items-center', [
dom('span', { class: ['italic text-sm', { 'text-light-red dark:text-dark-red': character.variables.spells.length !== character.spellslots }], text: `${character.variables.spells.length}/${character.spellslots} sort${character.variables.spells.length > 1 ? 's' : ''} maitrisé${character.variables.spells.length > 1 ? 's' : ''}` }),
button(text('Modifier'), () => this.spellPanel(character), 'py-1 px-4'),
])
]),
div('flex flex-col gap-2', spells.map(e => e.dom))
container,
])
]
}
@ -1752,66 +1750,59 @@ export class CharacterSheet
}
itemsTab(character: CompiledCharacter)
{
let debounceId: NodeJS.Timeout | undefined;
//TODO: Recompile values on "equip" checkbox change
const items = (character.variables.items.map(e => ({ ...e, item: config.items[e.id], ref: e })).filter(e => !!e.item) as Array<ItemState & { item: ItemConfig, ref: ItemState }>).map(e => {
const price = div(['flex flex-row min-w-16 gap-2 justify-between items-center px-2', { 'cursor-help': e.amount > 1 }], [ icon('ph:coin', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span({ 'underline decoration-1 decoration-dotted underline-offset-2': e.amount > 1 }, e.item.price ? `${e.item.price * e.amount}` : '-') ]);
const weight = div(['flex flex-row min-w-16 gap-2 justify-between items-center px-2', { 'cursor-help': e.amount > 1 }], [ icon('mdi:weight', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span({ 'underline decoration-1 decoration-dotted underline-offset-2': e.amount > 1 }, e.item.weight ? `${e.item.weight * e.amount}` : '-') ]);
const power = character.variables.items.filter(e => config.items[e.id]?.equippable && e.equipped).reduce((p, v) => p + ((config.items[v.id]?.powercost ?? 0) + (v.enchantments?.reduce((_p, _v) => (config.enchantments[_v]?.power ?? 0) + _p, 0) ?? 0) * v.amount), 0);
const weight = character.variables.items.reduce((p, v) => p + (config.items[v.id]?.weight ?? 0) * v.amount, 0);
const items = this.character!.character.variables.items;
return [
div('flex flex-col gap-2', [
div('flex flex-row justify-end items-center gap-8', [
dom('span', { class: () => ['italic text-sm', { 'text-light-red dark:text-dark-red': weight > character.itempower }], text: () => `Poids total: ${weight}/${character.itempower}` }),
dom('span', { class: () => ['italic text-sm', { 'text-light-red dark:text-dark-red': power > (character.capacity === false ? 0 : character.capacity) }], text: () => `Puissance magique: ${power}/${character.capacity}` }),
button(text('Modifier'), () => this.itemsPanel(character), 'py-1 px-4'),
]),
div('flex flex-col flex-1 divide-y divide-light-35 dark:divide-dark-35', { list: character.variables.items, redraw: true, render: e => {
const item = config.items[e.id];
if(!item) return;
const price = div(() => ['flex flex-row min-w-16 gap-2 justify-between items-center px-2', { 'cursor-help': e.amount > 1 && !!item.price }], [ icon('ph:coin', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span(() => ({ 'underline decoration-1 decoration-dotted underline-offset-2': e.amount > 1 && !!item.price }), () => item.price ? `${item.price * e.amount}` : '-') ]);
const weight = div(() => ['flex flex-row min-w-16 gap-2 justify-between items-center px-2', { 'cursor-help': e.amount > 1 && !!item.weight }], [ icon('mdi:weight', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span(() => ({ 'underline decoration-1 decoration-dotted underline-offset-2': e.amount > 1 && !!item.weight }), () => item.weight ? `${item.weight * e.amount}` : '-') ]);
return foldable(() => [
markdown(getText(e.item.description)),
markdown(getText(item.description)),
div('flex flex-row justify-center', [
this.character?.character.campaign ? button(text('Partager'), () => {
}, 'p-1') : undefined,
button(icon('radix-icons:trash'), () => {
const idx = this.character!.character.variables.items.findIndex(_e => _e.id === e.id);
const idx = items.findIndex(_e => _e === e);
if(idx === -1) return;
this.character!.character.variables.items[idx]!.amount--;
if(this.character!.character.variables.items[idx]!.amount >= 0) this.character!.character.variables.items.splice(idx, 1);
items[idx]!.amount--;
if(items[idx]!.amount >= 0) items.splice(idx, 1);
this.character!.variable('items', this.character!.character.variables.items);
debounceId && clearTimeout(debounceId);
debounceId = setTimeout(() => this.character?.saveVariables(), 2000);
this.tabs?.refresh();
this.character!.variable('items', items);
}, 'p-1'),
]) ], [div('flex flex-row justify-between', [
div('flex flex-row items-center gap-4', [
e.item.equippable ? checkbox({ defaultValue: e.equipped, change: v => {
e.equipped = e.ref.equipped = v;
item.equippable ? checkbox({ defaultValue: e.equipped, change: v => {
e.equipped = v;
this.character!.variable('items', this.character!.character.variables.items);
debounceId && clearTimeout(debounceId);
debounceId = setTimeout(() => this.character?.saveVariables(), 2000);
this.character!.variable('items', items);
}, class: { container: '!w-5 !h-5' } }) : undefined,
div('flex flex-row items-center gap-4', [ span([colorByRarity[e.item.rarity], 'text-lg'], e.item.name), div('flex flex-row gap-2 text-light-60 dark:text-dark-60 text-sm italic', subnameFactory(e.item).map(e => span('', e))) ]),
div('flex flex-row items-center gap-4', [ span([colorByRarity[item.rarity], 'text-lg'], item.name), div('flex flex-row gap-2 text-light-60 dark:text-dark-60 text-sm italic', subnameFactory(item).map(e => span('', e))) ]),
]),
div('flex flex-row items-center divide-x divide-light-50 dark:divide-dark-50 divide-dashed px-2', [
e.amount > 1 ? tooltip(price, `Prix unitaire: ${e.item.price}`, 'bottom') : price,
e.amount > 1 && !!item.price ? tooltip(price, `Prix unitaire: ${item.price}`, 'bottom') : price,
div('flex flex-row min-w-16 gap-2 justify-between items-center px-2', [ icon('radix-icons:cross-2', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', e.amount?.toString() ?? '-') ]),
div('flex flex-row min-w-16 gap-2 justify-between items-center px-2', [ icon('game-icons:bolt-drop', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', e.item.powercost || e.item.capacity ? `${e.item.powercost ?? 0}/${e.item.capacity ?? 0}` : '-') ]),
e.amount > 1 ? tooltip(weight, `Poids unitaire: ${e.item.weight}`, 'bottom') : weight,
div('flex flex-row min-w-16 gap-2 justify-between items-center px-2', [ icon('game-icons:battery-pack', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', e.item.charge ? `${e.item.charge}` : '-') ]),
div('flex flex-row min-w-16 gap-2 justify-between items-center px-2', [ icon('game-icons:bolt-drop', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', item.powercost || item.capacity ? `${item.powercost ?? 0}/${item.capacity ?? 0}` : '-') ]),
e.amount > 1 && !!item.weight ? tooltip(weight, `Poids unitaire: ${item.weight}`, 'bottom') : weight,
div('flex flex-row min-w-16 gap-2 justify-between items-center px-2', [ icon('game-icons:battery-pack', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', item.charge ? `${item.charge}` : '-') ]),
]),
])], { open: false, class: { icon: 'px-2', container: 'p-1 gap-2', content: 'px-4 pb-1 flex flex-col' } })
});
const power = character.variables.items.filter(e => config.items[e.id]?.equippable && e.equipped).reduce((p, v) => p + ((config.items[v.id]?.powercost ?? 0) + (v.enchantments?.reduce((_p, _v) => (config.enchantments[_v]?.power ?? 0) + _p, 0) ?? 0) * v.amount), 0);
const weight = character.variables.items.reduce((p, v) => p + (config.items[v.id]?.weight ?? 0) * v.amount, 0);
return [
div('flex flex-col gap-2', [
div('flex flex-row justify-end items-center gap-8', [
dom('span', { class: ['italic text-sm', { 'text-light-red dark:text-dark-red': weight > character.itempower }], text: `Poids total: ${weight}/${character.itempower}` }),
dom('span', { class: ['italic text-sm', { 'text-light-red dark:text-dark-red': power > (character.capacity === false ? 0 : character.capacity) }], text: `Puissance magique: ${power}/${character.capacity}` }),
button(text('Modifier'), () => this.itemsPanel(character), 'py-1 px-4'),
]),
div('flex flex-col flex-1 divide-y divide-light-35 dark:divide-dark-35', items)
}})
])
]
];
}
itemsPanel(character: CompiledCharacter)
{
@ -1825,12 +1816,12 @@ export class CharacterSheet
div('flex flex-row w-16 gap-2 justify-between items-center px-2', [ icon('game-icons:battery-pack', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', item.charge ? `${item.charge}` : '-') ]),
div('flex flex-row w-16 gap-2 justify-between items-center px-2', [ icon('ph:coin', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', item.price ? `${item.price}` : '-') ]),
button(icon('radix-icons:plus', { width: 16, height: 16 }), () => {
const list = [...this.character!.character.variables.items];
const list = this.character!.character.variables.items;
if(item.equippable) list.push({ id: item.id, amount: 1, charges: item.charge, enchantments: [], equipped: false });
else if(list.find(e => e.id === item.id)) this.character!.character.variables.items.find(e => e.id === item.id)!.amount++;
else if(list.find(e => e.id === item.id)) list.find(e => e.id === item.id)!.amount++;
else list.push({ id: item.id, amount: 1, charges: item.charge, enchantments: [] });
this.character!.variable('items', list); //TO REWORK
this.tabs?.refresh();
(list as DOMList<ItemState>)?.render();
this.character!.variable('items', list);
}, 'p-1 !border-solid !border-r'),
]),
])], { open: false, class: { icon: 'px-2', container: 'border border-light-35 dark:border-dark-35 p-1 gap-2', content: 'px-2 pb-1' } }) }));

View File

@ -1,44 +1,117 @@
import { iconLoaded, loadIcon } from 'iconify-icon';
export type Node = HTMLElement | SVGElement | Text | undefined;
export type RedrawableHTML<T extends keyof HTMLElementTagNameMap> = HTMLElementTagNameMap[T] & { update: (recursive: boolean) => void }
export type Node = RedrawableHTML<any> & { update: (recursive: boolean) => void } | SVGElement | Text | undefined;
export type NodeChildren = Array<Node> | undefined;
export type Class = string | Array<Class> | Record<string, boolean> | undefined;
export type Class = Reactive<string | Array<Class> | Record<string, boolean> | undefined>;
type Listener<K extends keyof HTMLElementEventMap> = | ((this: HTMLElement, ev: HTMLElementEventMap[K]) => any) | {
options?: boolean | AddEventListenerOptions;
listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any;
} | undefined;
export type DOMList = Node[] & {
remove(predicate: (item: Node, index: number, array: Node[]) => boolean): Node[];
export type Reactive<T> = T | (() => T);
export interface DOMList<T> extends Array<T>{
render(redraw?: boolean): void;
};
export interface NodeProperties
{
attributes?: Record<string, string | undefined | boolean | number>;
text?: string;
attributes?: Record<string, Reactive<string | undefined | boolean | number>>;
text?: Reactive<string | Text>;
class?: Class;
style?: Record<string, string | undefined | boolean | number> | string;
style?: Reactive<Record<string, string | undefined | boolean | number> | string>;
listeners?: {
[K in keyof HTMLElementEventMap]?: Listener<K>
};
}
export const cancelPropagation = (e: Event) => e.stopImmediatePropagation();
export function dom<K extends keyof HTMLElementTagNameMap>(tag: K, properties?: NodeProperties, children?: NodeChildren | DOMList): HTMLElementTagNameMap[K]
export function dom<T extends keyof HTMLElementTagNameMap>(tag: T, properties?: NodeProperties, children?: NodeChildren): RedrawableHTML<T>;
export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T, properties?: NodeProperties, children?: { render: (data: U) => Node, list?: Array<U>, redraw?: boolean }): RedrawableHTML<T> & { array?: DOMList<U> };
export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T, properties?: NodeProperties, children?: NodeChildren | { render: (data: U) => Node, list?: Array<U>, redraw?: boolean }): RedrawableHTML<T> & { array?: DOMList<U> }
{
const element = document.createElement(tag);
const element = document.createElement(tag) as HTMLElementTagNameMap[T] & { array?: DOMList<U>, update: (recursive: boolean) => void };
let setup = true, updating = false;
if(children && children.length > 0)
for(const c of children) if(c !== undefined) element.appendChild(c);
const _cache = new Map<U, Node>();
const update = (recursive: boolean) => {
updating = true;
if(children !== undefined && (setup || recursive))
{
element.replaceChildren();
if(Array.isArray(children))
{
for(const c of children)
{
if(c !== undefined)
{
element.appendChild(c);
recursive && 'update' in c && c.update(true);
}
}
}
else if(children.list !== undefined)
{
if(setup || recursive)
{
_cache.clear();
children.list.forEach(e => _cache.set(e, children.render(e)));
}
if(setup)
{
const _push = children.list.push;
children.list.push = (...items: U[]) => {
items.forEach(e => {
const dom = children.render(e);
_cache.set(e, dom);
dom && element.appendChild(dom);
});
if(children.redraw) update(false);
return _push.bind(children.list)(...items);
};
const _splice = children.list.splice;
children.list.splice = (start: number, deleteCount: number, ...items: U[]) => {
const list = _splice.bind(children.list)(start, deleteCount, ...items);
list.forEach(e => _cache.get(e)?.remove() || _cache.delete(e));
if(children.redraw) update(false);
return list;
};
}
element.array = children.list as DOMList<U>;
element.array.render = (redraw?: boolean) => {
element.replaceChildren(...children.list?.map(e => _cache.get(e)).filter(e => !!e) ?? []);
if((redraw !== undefined || children.redraw !== undefined) && !updating) update(redraw ?? children.redraw!);
}
element.array.render();
}
}
if(properties?.attributes)
{
for(const [k, v] of Object.entries(properties.attributes))
if(typeof v === 'string' || typeof v === 'number') element.setAttribute(k, v.toString(10));
else if(typeof v === 'boolean') element.toggleAttribute(k, v);
{
if(!setup && typeof v !== 'function') continue;
if(properties?.text)
element.textContent = properties.text;
const value = typeof v === 'function' ? v() : v;
if(typeof value === 'string' || typeof value === 'number') element.setAttribute(k, value.toString(10));
else if(typeof value === 'boolean') element.toggleAttribute(k, value);
}
}
if(properties?.text && (setup || typeof properties.text === 'function'))
{
const text = typeof properties.text === 'function' ? properties.text() : properties.text;
if(typeof text === 'string')
element.textContent = text;
else
element.appendChild(text as Text);
}
if(properties?.listeners)
{
@ -53,14 +126,23 @@ export function dom<K extends keyof HTMLElementTagNameMap>(tag: K, properties?:
}
styling(element, properties ?? {});
updating = false;
};
update(false);
setup = false;
element.update = update;
return element;
}
export function div(cls?: Class, children?: NodeChildren): HTMLDivElement
export function div<U extends any>(cls?: Class, children?: NodeChildren): RedrawableHTML<'div'>
export function div<U extends any>(cls?: Class, children?: { render: (data: U) => Node, list?: Array<U>, redraw?: boolean }): RedrawableHTML<'div'> & { array: DOMList<U> }
export function div<U extends any>(cls?: Class, children?: NodeChildren | { render: (data: U) => Node, list?: Array<U>, redraw?: boolean }): RedrawableHTML<'div'> & { array?: DOMList<U> }
{
//@ts-expect-error
return dom("div", { class: cls }, children);
}
export function span(cls?: Class, text?: string): HTMLSpanElement
export function span(cls?: Class, text?: Reactive<string | Text>): RedrawableHTML<'span'>
{
return dom("span", { class: cls, text: text });
}
@ -76,20 +158,115 @@ export function svg<K extends keyof SVGElementTagNameMap>(tag: K, properties?: N
if(typeof v === 'string') element.setAttribute(k, v);
else if(typeof v === 'boolean') element.toggleAttribute(k, v);
if(properties?.text)
if(properties?.text && typeof properties.text === 'string')
element.textContent = properties.text;
styling(element, properties ?? {});
return element;
}
export function text(data: string): Text
export function text(data: string): Text;
export function text(data: {}, _txt: Reactive<string>): Text;
export function text(data: any, _txt?: Reactive<string>): Text
{
if(typeof data === 'string')
return document.createTextNode(data);
else if(_txt)
{
const cache = new Map<string, number>();
let txtCache = (typeof _txt === 'function' ? _txt() : _txt);
const setup = (property: string) => {
const prop = property.split('.');
let obj = data;
for(let i = 0; i < prop.length - 1; i++)
{
if(prop[i]! in obj) obj = obj[prop[i]!];
else return 0;
}
const last = prop.slice(-1)[0]!;
if(last in obj)
{
const prototype = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(obj), last);
let clone = obj[last];
delete obj[last];
Object.defineProperty(obj, last, { ...prototype, get: () => prototype?.get ? prototype.get() : clone, set: (v) => { if(prototype?.set) { prototype.set(v); clone = obj[last]; } else if(!prototype?.get) { clone = v; } cache.set(property, v); replace(); }, enumerable: true, configurable: true, });
cache.set(property, clone);
return obj[last];
}
else return 0;
}
const apply = (_setup: boolean) => txtCache.replace(/\{\{(.+?)\}\}/g, (_, txt: string) => {
let i = 0, current = 0, property = '', nextOp = '';
const _compute = () => {
if(property.length > 0)
{
let value = 0;
if(_setup)
value = setup(property);
else
value = cache.get(property)!;
if(nextOp === '+')
current += value;
else if(nextOp === '-')
current -= value;
else if(nextOp === '*')
current *= value;
else if(nextOp === '/')
current /= value;
else if(nextOp === '%')
current /= value;
else if(nextOp === '')
current = value;
nextOp = '';
property = '';
}
}
while(i < txt.length)
{
switch(txt.charAt(i))
{
case '+':
case '-':
case '*':
case '/':
case '%':
_compute();
nextOp = txt.charAt(i).trim();
break;
case ' ':
break;
default:
property += txt.charAt(i);
break;
}
i++;
}
_compute();
return current.toString();
});
const replace = () => {
const txt = (typeof _txt === 'function' ? _txt() : _txt);
if(txt !== txtCache)
{
txtCache = txt;
node.textContent = apply(true);
}
else node.textContent = apply(false);
};
const node = document.createTextNode(apply(true));
return node;
}
else return document.createTextNode('');
}
export function styling(element: SVGElement | HTMLElement, properties: {
export function styling(element: SVGElement | RedrawableHTML<any>, properties: {
class?: Class;
style?: Record<string, string | undefined | boolean | number> | string;
style?: Reactive<Record<string, string | undefined | boolean | number> | string>;
}): SVGElement | HTMLElement
{
if(properties?.class)
@ -114,8 +291,8 @@ export interface IconProperties
mode?: string;
inline?: boolean;
noobserver?: boolean;
width?: string|number;
height?: string|number;
width?: string | number;
height?: string | number;
flip?: string;
rotate?: number|string;
style?: Record<string, string | undefined> | string;
@ -165,8 +342,9 @@ export function icon(name: string, properties?: IconProperties): HTMLElement
return element;
}
export function mergeClasses(classes: Class): string
export function mergeClasses(cls: Class): string
{
const classes = typeof cls === 'function' ? cls() : cls;
if(typeof classes === 'string')
{
return classes.trim();

View File

@ -94,7 +94,7 @@ export class HomebrewBuilder
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 })
]), [ 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));
const 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 ]) ];
@ -166,8 +166,8 @@ export class HomebrewBuilder
{
const render = (aspect: AspectConfig) => {
return {
name: input('text', { input: (value) => aspect.name = value, defaultValue: aspect.name, class: '!m-0 w-full' }),
description: input('text', { input: (value) => aspect.description = value, defaultValue: aspect.description, class: '!m-0 w-full' }),
name: input('text', { input: (value) => { aspect.name = value }, defaultValue: aspect.name, class: '!m-0 w-full' }),
description: input('text', { input: (value) => { aspect.description = value }, defaultValue: aspect.description, class: '!m-0 w-full' }),
stat: select(MAIN_STATS.map(f => ({ text: mainStatTexts[f], value: f })), { change: (value) => aspect.stat = value, defaultValue: aspect.stat, class: { container: '!m-0 w-full' } }),
alignment: select(ALIGNMENTS.map(f => ({ text: alignmentTexts[f], value: f })), { change: (value) => aspect.alignment = value, defaultValue: aspect.alignment, class: { container: '!m-0 w-full' } }),
magic: toggle({ defaultValue: aspect.magic, change: (value) => aspect.magic = value, class: { container: '' } }),
@ -179,7 +179,9 @@ export class HomebrewBuilder
};
}
const add = () => {
this._config.aspects.push({
const id = getID();
this._config.aspects[id] = {
id,
name: '',
description: '',
stat: 'strength',
@ -190,7 +192,7 @@ export class HomebrewBuilder
mental: { min: 0, max: 20 },
personality: { min: 0, max: 20 },
options: []
});
};
const element = redraw();
content.parentElement?.replaceChild(element, content);
@ -200,7 +202,7 @@ export class HomebrewBuilder
confirm('Voulez vous vraiment supprimer cet aspect ?').then(e => {
if(e)
{
config.aspects = config.aspects.filter(e => e !== aspect);
delete config.aspects[aspect.id];
const element = redraw();
content.parentElement?.replaceChild(element, content);
@ -208,7 +210,7 @@ export class HomebrewBuilder
}
})
}
const redraw = () => table(this._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' } });
const redraw = () => table(Object.values(this._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();
return [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), add, 'p-1') ]), content ] ) ];
}
@ -224,11 +226,12 @@ export class HomebrewBuilder
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Portée'), select<'personnal' | number>([{ text: 'Toucher', value: 0 }, { text: 'Personnel', value: 'personnal' }, { text: '3 cases', value: 3 }, { text: '6 cases', value: 6 }, { text: '9 cases', value: 9 }, { text: '12 cases', value: 12 }, { text: '18 cases', value: 18 }], { change: (value) => spell.range = value, defaultValue: spell.range, 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.description = value, defaultValue: spell.description, 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 });
], [ 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.description = value }, defaultValue: spell.description, 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 = () => {
this._config.spells.push({
id: getID(),
const id = getID();
this._config.spells[id] = {
id,
name: '',
rank: 1,
type: 'precision',
@ -239,7 +242,7 @@ export class HomebrewBuilder
concentration: false,
range: 0,
tags: [],
});
};
const element = redraw();
content.parentElement?.replaceChild(element, content);
@ -249,7 +252,7 @@ export class HomebrewBuilder
confirm('Voulez vous vraiment supprimer ce sort ?').then(e => {
if(e)
{
this._config.spells = this._config.spells.filter(e => e !== spell);
delete this._config.spells[spell.id];
const element = redraw();
content.parentElement?.replaceChild(element, content);
@ -257,7 +260,7 @@ export class HomebrewBuilder
}
});
}
const redraw = () => div('flex flex-col divide-y', this._config.spells.map(render));
const redraw = () => div('flex flex-col divide-y', Object.values(this._config.spells).map(render));
let content = redraw();
return [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), add, 'p-1') ]), content ] ) ];
}
@ -269,7 +272,7 @@ export class HomebrewBuilder
const buttons = div('flex flex-row items-center gap-2', [ span('text-sm text-light-70 dark:text-dark-70', type), tooltip(button(icon('radix-icons:pencil-1'), () => edit(type, feature.id), 'p-1'), 'Modifier', 'left'), tooltip(button(icon('radix-icons:trash'), () => remove(type, feature.id), 'p-1'), 'Supprimer', 'right') ]);
return {
dom: div('flex flex-col gap-2', [
div('flex flex-row justify-between', [ input('text', { defaultValue: feature.name, input: value => feature.name = value, placeholder: 'Nom', class: '!mx-0 w-80' }), div('flex flex-row gap-2 items-center', [ type === 'action' || type === 'reaction' ? div('flex flex-row items-center', [ numberpicker({ defaultValue: feature?.cost ?? 0, input: value => feature.cost = value, class: '!mx-1', max: type === 'action' ? 3 : 2, min: 0 }), text(`point${(feature?.cost ?? 0) > 1 ? 's' : ''}`)]) : undefined, buttons ])]),
div('flex flex-row justify-between', [ input('text', { defaultValue: feature.name, input: value => { feature.name = value }, placeholder: 'Nom', class: '!mx-0 w-80' }), div('flex flex-row gap-2 items-center', [ type === 'action' || type === 'reaction' ? div('flex flex-row items-center', [ numberpicker({ defaultValue: feature?.cost ?? 0, input: value => feature.cost = value, class: '!mx-1', max: type === 'action' ? 3 : 2, min: 0 }), text(`point${(feature?.cost ?? 0) > 1 ? 's' : ''}`)]) : undefined, buttons ])]),
md.current,
]),
buttons,
@ -564,7 +567,7 @@ class FeatureEditor
{
if(buffer.list === 'spells')
{
list = config.spells.map(e => ({ text: e.name, render: () => div('flex flex-col', [ div('flex flex-row justify-between', [ dom('span', { text: e.name, class: 'font-bold' }), div('flex flex-row gap-8', [ dom('span', { class: 'italic', text: `Rang ${e.rank === 4 ? 'spécial' : e.rank}` }), dom('span', { text: spellTypeTexts[e.type] }) ]) ]), div('text-sm text-light-70 dark:text-dark-70', [ text(renderMDAsText(e.description)) ]) ]), value: e.id }));
list = Object.values(config.spells).map(e => ({ text: e.name, render: () => div('flex flex-col', [ div('flex flex-row justify-between', [ dom('span', { text: e.name, class: 'font-bold' }), div('flex flex-row gap-8', [ dom('span', { class: 'italic', text: `Rang ${e.rank === 4 ? 'spécial' : e.rank}` }), dom('span', { text: spellTypeTexts[e.type] }) ]) ]), div('text-sm text-light-70 dark:text-dark-70', [ text(renderMDAsText(e.description)) ]) ]), value: e.id }));
}
else if(buffer.list)
{
@ -573,7 +576,7 @@ class FeatureEditor
}
else
{
list = (Object.values(config.features).flatMap(e => e.effect).filter(e => e.category === 'list' && e.list === buffer.list && e.action === 'add') as FeatureList[]).map((e) => e.list === 'spells' ? config.spells.find(f => f.id === e.item)! : config[e.list][e.item]!).map((e) => ({ text: e.name, render: () => div('flex flex-col', [ div('flex flex-row justify-between', [ dom('span', { text: e.name, class: 'font-bold' }) ]), div('text-sm text-light-70 dark:text-dark-70', [ text(renderMDAsText(getText(e.description))) ]) ]), value: e.id }));
list = (Object.values(config.features).flatMap(e => e.effect).filter(e => e.category === 'list' && e.list === buffer.list && e.action === 'add') as FeatureList[]).map((e) => config[e.list][e.item]!).map((e) => ({ text: e.name, render: () => div('flex flex-col', [ div('flex flex-row justify-between', [ dom('span', { text: e.name, class: 'font-bold' }) ]), div('text-sm text-light-70 dark:text-dark-70', [ text(renderMDAsText(getText(e.description))) ]) ]), value: e.id }));
}
return {
@ -619,7 +622,7 @@ class FeatureEditor
}
const renderOption = (option: { text: string; effects: (Partial<FeatureValue | FeatureList>)[] }, state: boolean) => {
const effects = div('flex flex-col -m-px flex flex-col ms-px ps-8 w-full', option.effects.map(e => renderEffect(option, e)));
let _content = foldable([ effects ], [ div('flex flex-row flex-1 justify-between', [ input('text', { defaultValue: option.text, input: (value) => option.text = value, placeholder: 'Nom de l\'option', class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] flex-shrink-1' }), div('flex flex-row flex-shrink-1', [ tooltip(button(icon('radix-icons:plus'), () => effects.appendChild(renderEffect(option, addEffect(option))), 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Nouvel effet', 'bottom'), , tooltip(button(icon('radix-icons:trash'), () => {
let _content = foldable([ effects ], [ div('flex flex-row flex-1 justify-between', [ input('text', { defaultValue: option.text, input: (value) => { option.text = value }, placeholder: 'Nom de l\'option', class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] flex-shrink-1' }), div('flex flex-row flex-shrink-1', [ tooltip(button(icon('radix-icons:plus'), () => effects.appendChild(renderEffect(option, addEffect(option))), 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Nouvel effet', 'bottom'), , tooltip(button(icon('radix-icons:trash'), () => {
_content.remove();
buffer.options?.splice(buffer.options.findIndex(e => e !== option), 1);
}, 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Supprimer', 'bottom') ]) ]) ], { class: { title: 'border-b border-light-35 dark:border-dark-35', icon: 'w-[34px] h-[34px]', content: 'border-b border-light-35 dark:border-dark-35' }, open: state });
@ -628,7 +631,7 @@ class FeatureEditor
const list = div('flex flex-col flex-1 divide-y divide-light-35 dark:divide-dark-35 gap-2', buffer.options?.map(e => renderOption(e, false)) ?? []);
return {
top: [ input('text', { defaultValue: buffer.text, input: (value) => (buffer as FeatureChoice).text = value, class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-full', placeholder: 'Description' }), tooltip(button(icon('radix-icons:plus'), () => list.appendChild(renderOption(addChoice(), true)), 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Nouvelle option', 'bottom') ],
top: [ input('text', { defaultValue: buffer.text, input: (value) => { (buffer as FeatureChoice).text = value }, class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-full', placeholder: 'Description' }), tooltip(button(icon('radix-icons:plus'), () => list.appendChild(renderOption(addChoice(), true)), 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Nouvelle option', 'bottom') ],
bottom: [ list ],
}
}
@ -764,7 +767,7 @@ export class ItemPanel
], [ span('text-lg font-bold', "Armure") ], { class: { content: 'group-data-[active]:grid grid-cols-2 my-2 gap-4', title: 'grid grid-cols-2 gap-4 mx-2', container: 'pb-2 border-b border-light-35 dark:border-dark-35' }, open: true } ) : undefined,
_item.category === 'weapon' ? foldable([
div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ span('', 'Type de dégâts'), ]), select(Object.keys(damageTypeTexts).map(e => ({ text: damageTypeTexts[e as DamageType], value: e as DamageType })), { defaultValue: _item.damage.type, change: (v) => _item.damage.type = v, class: { container: '!w-1/3' } }), ]),
div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ span('', 'Dégats'), ]), input('text', { defaultValue: _item.damage.value, input: (v) => _item.damage.value = v, class: '!w-1/3' }), ]),
div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ span('', 'Dégats'), ]), input('text', { defaultValue: _item.damage.value, input: (v) => { _item.damage.value = v }, class: '!w-1/3' }), ]),
], [ span('text-lg font-bold', "Propriétés"), div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ span('', 'Rareté'), ]), multiselect(Object.keys(weaponTypeTexts).map(e => ({ text: weaponTypeTexts[e as WeaponType], value: e as WeaponType })), { defaultValue: _item.type, change: (v) => _item.type = v, class: { container: '!w-1/2' } }), ]) ], { class: { content: 'group-data-[active]:grid grid-cols-2 my-2 gap-4', title: 'grid grid-cols-2 gap-4 mx-2', container: 'pb-2 border-b border-light-35 dark:border-dark-35' }, open: true } ) : undefined,
foldable([div('p-1 border border-light-40 dark:border-dark-40 w-full bg-light-25 dark:bg-dark-25 min-h-48 max-h-[32rem]', [ ItemPanel.descriptionEditor.dom ])], [ span('text-lg font-bold px-2', "Description des effets") ], { class: { container: 'gap-4 pb-2 border-b border-light-35 dark:border-dark-35' }, open: true, }),
foldable([div('p-1 border border-light-40 dark:border-dark-40 w-full bg-light-25 dark:bg-dark-25 min-h-48 max-h-[32rem]', [ ItemPanel.flavoringEditor.dom ])], [ span('text-lg font-bold px-2', "Lore") ], { class: { container: 'gap-4 pb-2 border-b border-light-35 dark:border-dark-35' }, open: true, }),
@ -1012,7 +1015,7 @@ function textFromEffect(effect: Partial<FeatureOption>): string
case 'passive':
return effect.action === 'add' ? `Gain du passif "${effect.item ? (config.passive[effect.item]?.name ?? 'Inconnu') : 'Inconnu'}"` : `Suppression du passif "${effect.item ? (config.passive[effect.item]?.name ?? 'Inconnu') : 'Inconnu'}"`;
case 'spells':
return effect.action === 'add' ? `Maitrise du sort "${config.spells.find(e => e.id === effect.item)?.name ?? 'Sort inconnu'}".` : `Perte de maitrise du sort "${config.spells.find(e => e.id === effect.item)?.name ?? 'Sort inconnu'}".`;
return effect.action === 'add' ? `Maitrise du sort "${effect.item ? (config.spells[effect.item]?.name ?? 'Sort inconnu') : 'Sort inconnu'}".` : `Perte de maitrise du sort "${effect.item ? (config.passive[effect.item]?.name ?? 'Sort inconnu') : 'Sort inconnu'}".`;
case 'sickness':
return effect.action === 'add' ? `Maladie "${effect.item ? (config.sickness[effect.item]?.name ?? 'inconnue') : 'inconnue'}" permanente.` : `Maladie "${effect.item ? (config.sickness[effect.item]?.name ?? 'inconnue') : 'inconnue'}" supprimée.`;
}