diff --git a/database/full.sql b/database/full.sql
index 7aeac8e..3e2cbb5 100644
--- a/database/full.sql
+++ b/database/full.sql
@@ -18726,3 +18726,52 @@ CREATE TABLE IF NOT EXISTS tbl_page_access (
cl_allow_member TINYINT(1) NOT NULL DEFAULT 0,
cl_updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+
+CREATE TABLE IF NOT EXISTS tbl_sccharacters (
+ cl_sccharacter_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ cl_sccharacter_owner_auth_id INT UNSIGNED NOT NULL,
+ cl_sccharacter_name VARCHAR(190) NOT NULL,
+ cl_sccharacter_role VARCHAR(190) NOT NULL DEFAULT '',
+ cl_sccharacter_faction VARCHAR(190) NOT NULL DEFAULT '',
+ cl_sccharacter_avatar_url VARCHAR(255) NOT NULL DEFAULT '',
+ cl_sccharacter_description TEXT DEFAULT NULL,
+ cl_sccharacter_notes TEXT DEFAULT NULL,
+ cl_sccharacter_share_token VARCHAR(64) NOT NULL,
+ cl_sccharacter_share_enabled TINYINT(1) NOT NULL DEFAULT 0,
+ cl_sccharacter_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ cl_sccharacter_updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ UNIQUE KEY uq_sccharacter_share_token (cl_sccharacter_share_token),
+ KEY idx_sccharacter_owner (cl_sccharacter_owner_auth_id),
+ KEY idx_sccharacter_name (cl_sccharacter_name),
+ CONSTRAINT fk_sccharacter_owner_auth FOREIGN KEY (cl_sccharacter_owner_auth_id)
+ REFERENCES tbl_auth (cl_auth_id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+CREATE TABLE IF NOT EXISTS tbl_sccharacteritems (
+ cl_sccharacteritem_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ cl_sccharacteritem_character_id INT UNSIGNED NOT NULL,
+ cl_sccharacteritem_source ENUM('base', 'custom') NOT NULL DEFAULT 'base',
+ cl_sccharacteritem_scobjs_id INT UNSIGNED DEFAULT NULL,
+ cl_sccharacteritem_scitemcustom_id INT(11) DEFAULT NULL,
+ cl_sccharacteritem_slot VARCHAR(120) NOT NULL DEFAULT '',
+ cl_sccharacteritem_note TEXT DEFAULT NULL,
+ cl_sccharacteritem_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ KEY idx_sccharacteritem_character (cl_sccharacteritem_character_id),
+ KEY idx_sccharacteritem_scobjs (cl_sccharacteritem_scobjs_id),
+ KEY idx_sccharacteritem_scitemcustom (cl_sccharacteritem_scitemcustom_id),
+ CONSTRAINT fk_sccharacteritem_character FOREIGN KEY (cl_sccharacteritem_character_id)
+ REFERENCES tbl_sccharacters (cl_sccharacter_id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT fk_sccharacteritem_scobjs FOREIGN KEY (cl_sccharacteritem_scobjs_id)
+ REFERENCES tbl_scobjs (cl_scobjs_id)
+ ON DELETE SET NULL
+ ON UPDATE CASCADE,
+ CONSTRAINT fk_sccharacteritem_scitemcustom FOREIGN KEY (cl_sccharacteritem_scitemcustom_id)
+ REFERENCES tbl_scitemcustom (cl_scitemcustom_id)
+ ON DELETE SET NULL
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/database/schema.sql b/database/schema.sql
index afc23ff..9b0ee00 100644
--- a/database/schema.sql
+++ b/database/schema.sql
@@ -205,3 +205,52 @@ CREATE TABLE IF NOT EXISTS tbl_page_access (
cl_allow_member TINYINT(1) NOT NULL DEFAULT 0,
cl_updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+
+CREATE TABLE IF NOT EXISTS tbl_sccharacters (
+ cl_sccharacter_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ cl_sccharacter_owner_auth_id INT UNSIGNED NOT NULL,
+ cl_sccharacter_name VARCHAR(190) NOT NULL,
+ cl_sccharacter_role VARCHAR(190) NOT NULL DEFAULT '',
+ cl_sccharacter_faction VARCHAR(190) NOT NULL DEFAULT '',
+ cl_sccharacter_avatar_url VARCHAR(255) NOT NULL DEFAULT '',
+ cl_sccharacter_description TEXT DEFAULT NULL,
+ cl_sccharacter_notes TEXT DEFAULT NULL,
+ cl_sccharacter_share_token VARCHAR(64) NOT NULL,
+ cl_sccharacter_share_enabled TINYINT(1) NOT NULL DEFAULT 0,
+ cl_sccharacter_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ cl_sccharacter_updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ UNIQUE KEY uq_sccharacter_share_token (cl_sccharacter_share_token),
+ KEY idx_sccharacter_owner (cl_sccharacter_owner_auth_id),
+ KEY idx_sccharacter_name (cl_sccharacter_name),
+ CONSTRAINT fk_sccharacter_owner_auth FOREIGN KEY (cl_sccharacter_owner_auth_id)
+ REFERENCES tbl_auth (cl_auth_id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+CREATE TABLE IF NOT EXISTS tbl_sccharacteritems (
+ cl_sccharacteritem_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ cl_sccharacteritem_character_id INT UNSIGNED NOT NULL,
+ cl_sccharacteritem_source ENUM('base', 'custom') NOT NULL DEFAULT 'base',
+ cl_sccharacteritem_scobjs_id INT UNSIGNED DEFAULT NULL,
+ cl_sccharacteritem_scitemcustom_id INT(11) DEFAULT NULL,
+ cl_sccharacteritem_slot VARCHAR(120) NOT NULL DEFAULT '',
+ cl_sccharacteritem_note TEXT DEFAULT NULL,
+ cl_sccharacteritem_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ KEY idx_sccharacteritem_character (cl_sccharacteritem_character_id),
+ KEY idx_sccharacteritem_scobjs (cl_sccharacteritem_scobjs_id),
+ KEY idx_sccharacteritem_scitemcustom (cl_sccharacteritem_scitemcustom_id),
+ CONSTRAINT fk_sccharacteritem_character FOREIGN KEY (cl_sccharacteritem_character_id)
+ REFERENCES tbl_sccharacters (cl_sccharacter_id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT fk_sccharacteritem_scobjs FOREIGN KEY (cl_sccharacteritem_scobjs_id)
+ REFERENCES tbl_scobjs (cl_scobjs_id)
+ ON DELETE SET NULL
+ ON UPDATE CASCADE,
+ CONSTRAINT fk_sccharacteritem_scitemcustom FOREIGN KEY (cl_sccharacteritem_scitemcustom_id)
+ REFERENCES tbl_scitemcustom (cl_scitemcustom_id)
+ ON DELETE SET NULL
+ ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/db/auth.php b/db/auth.php
index 13c704c..3ea1f13 100644
--- a/db/auth.php
+++ b/db/auth.php
@@ -469,6 +469,7 @@ function auth_navigation_items(): array
['file' => 'scitems.php', 'label' => 'Base d\'Objets'],
['file' => 'scstatsitem.php', 'label' => 'Stats Item'],
['file' => 'scitemcustom.php', 'label' => 'Objets perso.'],
+ ['file' => 'sccharacters.php', 'label' => 'Personnages'],
['file' => 'scmining.php', 'label' => 'Scanner Minage'],
['file' => 'scmanufactures.php', 'label' => 'Manufactures'],
['file' => 'scvaisseaux.php', 'label' => 'Vaisseaux'],
diff --git a/db/sccharacters.php b/db/sccharacters.php
new file mode 100644
index 0000000..0c012d0
--- /dev/null
+++ b/db/sccharacters.php
@@ -0,0 +1,456 @@
+query("SHOW COLUMNS FROM `{$table}` LIKE " . $db->quote($column));
+
+ return (bool) $stmt->fetch();
+}
+
+function sccharacters_index_exists(PDO $db, string $table, string $index): bool
+{
+ $stmt = $db->query("SHOW INDEX FROM `{$table}` WHERE Key_name = " . $db->quote($index));
+
+ return (bool) $stmt->fetch();
+}
+
+function sccharacters_foreign_key_exists(PDO $db, string $table, string $constraint): bool
+{
+ $stmt = $db->prepare(
+ "SELECT COUNT(*)
+ FROM information_schema.TABLE_CONSTRAINTS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = :table_name
+ AND CONSTRAINT_NAME = :constraint_name
+ AND CONSTRAINT_TYPE = 'FOREIGN KEY'"
+ );
+ $stmt->execute([
+ 'table_name' => $table,
+ 'constraint_name' => $constraint,
+ ]);
+
+ return (int) $stmt->fetchColumn() > 0;
+}
+
+function sccharacters_bootstrap(): void
+{
+ static $bootstrapped = false;
+
+ if ($bootstrapped) {
+ return;
+ }
+
+ $db = db();
+
+ $db->exec(
+ "CREATE TABLE IF NOT EXISTS tbl_sccharacters (
+ cl_sccharacter_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
+ cl_sccharacter_owner_auth_id INT UNSIGNED NOT NULL,
+ cl_sccharacter_name VARCHAR(190) NOT NULL,
+ cl_sccharacter_role VARCHAR(190) NOT NULL DEFAULT '',
+ cl_sccharacter_faction VARCHAR(190) NOT NULL DEFAULT '',
+ cl_sccharacter_avatar_url VARCHAR(255) NOT NULL DEFAULT '',
+ cl_sccharacter_description TEXT DEFAULT NULL,
+ cl_sccharacter_notes TEXT DEFAULT NULL,
+ cl_sccharacter_share_token VARCHAR(64) NOT NULL,
+ cl_sccharacter_share_enabled TINYINT(1) NOT NULL DEFAULT 0,
+ cl_sccharacter_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ cl_sccharacter_updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (cl_sccharacter_id),
+ UNIQUE KEY uq_sccharacter_share_token (cl_sccharacter_share_token),
+ KEY idx_sccharacter_owner (cl_sccharacter_owner_auth_id),
+ KEY idx_sccharacter_name (cl_sccharacter_name),
+ CONSTRAINT fk_sccharacter_owner_auth FOREIGN KEY (cl_sccharacter_owner_auth_id)
+ REFERENCES tbl_auth (cl_auth_id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
+ );
+
+ if (!sccharacters_column_exists($db, 'tbl_sccharacters', 'cl_sccharacter_owner_auth_id')) {
+ $db->exec(
+ 'ALTER TABLE tbl_sccharacters
+ ADD COLUMN cl_sccharacter_owner_auth_id INT UNSIGNED NULL AFTER cl_sccharacter_id'
+ );
+ }
+
+ if (!sccharacters_column_exists($db, 'tbl_sccharacters', 'cl_sccharacter_share_token')) {
+ $db->exec(
+ "ALTER TABLE tbl_sccharacters
+ ADD COLUMN cl_sccharacter_share_token VARCHAR(64) NOT NULL DEFAULT '' AFTER cl_sccharacter_notes"
+ );
+ }
+
+ if (!sccharacters_column_exists($db, 'tbl_sccharacters', 'cl_sccharacter_share_enabled')) {
+ $db->exec(
+ 'ALTER TABLE tbl_sccharacters
+ ADD COLUMN cl_sccharacter_share_enabled TINYINT(1) NOT NULL DEFAULT 0 AFTER cl_sccharacter_share_token'
+ );
+ }
+
+ if (!sccharacters_index_exists($db, 'tbl_sccharacters', 'idx_sccharacter_owner')) {
+ $db->exec(
+ 'ALTER TABLE tbl_sccharacters
+ ADD INDEX idx_sccharacter_owner (cl_sccharacter_owner_auth_id)'
+ );
+ }
+
+ if (!sccharacters_index_exists($db, 'tbl_sccharacters', 'uq_sccharacter_share_token')) {
+ $db->exec(
+ 'ALTER TABLE tbl_sccharacters
+ ADD UNIQUE KEY uq_sccharacter_share_token (cl_sccharacter_share_token)'
+ );
+ }
+
+ if (!sccharacters_foreign_key_exists($db, 'tbl_sccharacters', 'fk_sccharacter_owner_auth')) {
+ $db->exec(
+ 'ALTER TABLE tbl_sccharacters
+ ADD CONSTRAINT fk_sccharacter_owner_auth FOREIGN KEY (cl_sccharacter_owner_auth_id)
+ REFERENCES tbl_auth (cl_auth_id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE'
+ );
+ }
+
+ $db->exec(
+ "CREATE TABLE IF NOT EXISTS tbl_sccharacteritems (
+ cl_sccharacteritem_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
+ cl_sccharacteritem_character_id INT UNSIGNED NOT NULL,
+ cl_sccharacteritem_source ENUM('base', 'custom') NOT NULL DEFAULT 'base',
+ cl_sccharacteritem_scobjs_id INT UNSIGNED DEFAULT NULL,
+ cl_sccharacteritem_scitemcustom_id INT(11) DEFAULT NULL,
+ cl_sccharacteritem_slot VARCHAR(120) NOT NULL DEFAULT '',
+ cl_sccharacteritem_note TEXT DEFAULT NULL,
+ cl_sccharacteritem_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (cl_sccharacteritem_id),
+ KEY idx_sccharacteritem_character (cl_sccharacteritem_character_id),
+ KEY idx_sccharacteritem_scobjs (cl_sccharacteritem_scobjs_id),
+ KEY idx_sccharacteritem_scitemcustom (cl_sccharacteritem_scitemcustom_id),
+ CONSTRAINT fk_sccharacteritem_character FOREIGN KEY (cl_sccharacteritem_character_id)
+ REFERENCES tbl_sccharacters (cl_sccharacter_id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
+ CONSTRAINT fk_sccharacteritem_scobjs FOREIGN KEY (cl_sccharacteritem_scobjs_id)
+ REFERENCES tbl_scobjs (cl_scobjs_id)
+ ON DELETE SET NULL
+ ON UPDATE CASCADE,
+ CONSTRAINT fk_sccharacteritem_scitemcustom FOREIGN KEY (cl_sccharacteritem_scitemcustom_id)
+ REFERENCES tbl_scitemcustom (cl_scitemcustom_id)
+ ON DELETE SET NULL
+ ON UPDATE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
+ );
+
+ if (!sccharacters_column_exists($db, 'tbl_sccharacteritems', 'cl_sccharacteritem_scobjs_id')) {
+ $db->exec(
+ 'ALTER TABLE tbl_sccharacteritems
+ ADD COLUMN cl_sccharacteritem_scobjs_id INT UNSIGNED NULL AFTER cl_sccharacteritem_source'
+ );
+ }
+
+ if (!sccharacters_column_exists($db, 'tbl_sccharacteritems', 'cl_sccharacteritem_scitemcustom_id')) {
+ $db->exec(
+ 'ALTER TABLE tbl_sccharacteritems
+ ADD COLUMN cl_sccharacteritem_scitemcustom_id INT(11) NULL AFTER cl_sccharacteritem_scobjs_id'
+ );
+ }
+
+ if (!sccharacters_index_exists($db, 'tbl_sccharacteritems', 'idx_sccharacteritem_character')) {
+ $db->exec(
+ 'ALTER TABLE tbl_sccharacteritems
+ ADD INDEX idx_sccharacteritem_character (cl_sccharacteritem_character_id)'
+ );
+ }
+
+ if (!sccharacters_index_exists($db, 'tbl_sccharacteritems', 'idx_sccharacteritem_scobjs')) {
+ $db->exec(
+ 'ALTER TABLE tbl_sccharacteritems
+ ADD INDEX idx_sccharacteritem_scobjs (cl_sccharacteritem_scobjs_id)'
+ );
+ }
+
+ if (!sccharacters_index_exists($db, 'tbl_sccharacteritems', 'idx_sccharacteritem_scitemcustom')) {
+ $db->exec(
+ 'ALTER TABLE tbl_sccharacteritems
+ ADD INDEX idx_sccharacteritem_scitemcustom (cl_sccharacteritem_scitemcustom_id)'
+ );
+ }
+
+ if (!sccharacters_foreign_key_exists($db, 'tbl_sccharacteritems', 'fk_sccharacteritem_character')) {
+ $db->exec(
+ 'ALTER TABLE tbl_sccharacteritems
+ ADD CONSTRAINT fk_sccharacteritem_character FOREIGN KEY (cl_sccharacteritem_character_id)
+ REFERENCES tbl_sccharacters (cl_sccharacter_id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE'
+ );
+ }
+
+ if (!sccharacters_foreign_key_exists($db, 'tbl_sccharacteritems', 'fk_sccharacteritem_scobjs')) {
+ $db->exec(
+ 'ALTER TABLE tbl_sccharacteritems
+ ADD CONSTRAINT fk_sccharacteritem_scobjs FOREIGN KEY (cl_sccharacteritem_scobjs_id)
+ REFERENCES tbl_scobjs (cl_scobjs_id)
+ ON DELETE SET NULL
+ ON UPDATE CASCADE'
+ );
+ }
+
+ if (!sccharacters_foreign_key_exists($db, 'tbl_sccharacteritems', 'fk_sccharacteritem_scitemcustom')) {
+ $db->exec(
+ 'ALTER TABLE tbl_sccharacteritems
+ ADD CONSTRAINT fk_sccharacteritem_scitemcustom FOREIGN KEY (cl_sccharacteritem_scitemcustom_id)
+ REFERENCES tbl_scitemcustom (cl_scitemcustom_id)
+ ON DELETE SET NULL
+ ON UPDATE CASCADE'
+ );
+ }
+
+ $stmt_missing_tokens = $db->query(
+ "SELECT cl_sccharacter_id
+ FROM tbl_sccharacters
+ WHERE cl_sccharacter_share_token IS NULL
+ OR cl_sccharacter_share_token = ''"
+ );
+
+ $stmt_update_token = $db->prepare(
+ 'UPDATE tbl_sccharacters
+ SET cl_sccharacter_share_token = :token
+ WHERE cl_sccharacter_id = :id'
+ );
+
+ foreach ($stmt_missing_tokens->fetchAll() as $row) {
+ $stmt_update_token->execute([
+ 'token' => sccharacters_generate_share_token($db),
+ 'id' => (int) $row['cl_sccharacter_id'],
+ ]);
+ }
+
+ $bootstrapped = true;
+}
+
+function sccharacters_generate_share_token(PDO $db): string
+{
+ do {
+ $token = bin2hex(random_bytes(16));
+ $stmt = $db->prepare(
+ 'SELECT cl_sccharacter_id
+ FROM tbl_sccharacters
+ WHERE cl_sccharacter_share_token = :token
+ LIMIT 1'
+ );
+ $stmt->execute(['token' => $token]);
+ } while ($stmt->fetch());
+
+ return $token;
+}
+
+
+function sccharacters_item_category_options(): array
+{
+ return [
+ 'weapon' => 'Armes',
+ 'armor' => 'Armures',
+ 'tools' => 'Outils',
+ 'consumables' => 'Consommables',
+ 'ammunition' => 'Munitions',
+ 'attachments' => 'Accessoires',
+ 'clothing' => 'Vêtements',
+ 'cargo' => 'Cargo / Conteneurs',
+ 'ship' => 'Composants / Véhicule',
+ 'access' => 'Accès / Mobilier',
+ 'misc' => 'Divers',
+ ];
+}
+
+function sccharacters_item_category_label(string $category): string
+{
+ $options = sccharacters_item_category_options();
+
+ return $options[$category] ?? $options['misc'];
+}
+
+function sccharacters_item_category_slug(string $value): string
+{
+ $value = trim($value);
+ if ($value == '') {
+ return '';
+ }
+
+ if (function_exists('iconv')) {
+ $converted = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value);
+ if ($converted !== false) {
+ $value = $converted;
+ }
+ }
+
+ $value = strtolower($value);
+ $value = preg_replace('/[^a-z0-9]+/', '_', $value) ?? '';
+
+ return trim($value, '_');
+}
+
+function sccharacters_string_contains_any(string $haystack, array $needles): bool
+{
+ foreach ($needles as $needle) {
+ if ($needle !== '' && strpos($haystack, $needle) !== false) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+function sccharacters_normalize_item_category(?string $value): string
+{
+ $slug = sccharacters_item_category_slug((string) $value);
+ if ($slug === '' || in_array($slug, ['auto', 'automatic', 'automatique', 'default', 'defaut'], true)) {
+ return '';
+ }
+
+ $options = sccharacters_item_category_options();
+ if (isset($options[$slug])) {
+ return $slug;
+ }
+
+ $exact_aliases = [
+ 'arme' => 'weapon',
+ 'armes' => 'weapon',
+ 'weapon' => 'weapon',
+ 'weapons' => 'weapon',
+ 'armure' => 'armor',
+ 'armures' => 'armor',
+ 'armor' => 'armor',
+ 'armors' => 'armor',
+ 'outil' => 'tools',
+ 'outils' => 'tools',
+ 'tool' => 'tools',
+ 'tools' => 'tools',
+ 'consommable' => 'consumables',
+ 'consommables' => 'consumables',
+ 'usable' => 'consumables',
+ 'food' => 'consumables',
+ 'drink' => 'consumables',
+ 'munition' => 'ammunition',
+ 'munitions' => 'ammunition',
+ 'ammo' => 'ammunition',
+ 'ammunition' => 'ammunition',
+ 'accessoire' => 'attachments',
+ 'accessoires' => 'attachments',
+ 'attachment' => 'attachments',
+ 'attachments' => 'attachments',
+ 'vetement' => 'clothing',
+ 'vetements' => 'clothing',
+ 'clothing' => 'clothing',
+ 'clothes' => 'clothing',
+ 'cargo' => 'cargo',
+ 'container' => 'cargo',
+ 'conteneur' => 'cargo',
+ 'conteneurs' => 'cargo',
+ 'composant' => 'ship',
+ 'composants' => 'ship',
+ 'component' => 'ship',
+ 'components' => 'ship',
+ 'vaisseau' => 'ship',
+ 'vaisseaux' => 'ship',
+ 'vehicule' => 'ship',
+ 'vehicules' => 'ship',
+ 'acces' => 'access',
+ 'access' => 'access',
+ 'mobilier' => 'access',
+ 'divers' => 'misc',
+ 'misc' => 'misc',
+ 'autre' => 'misc',
+ 'autres' => 'misc',
+ 'other' => 'misc',
+ ];
+
+ if (isset($exact_aliases[$slug])) {
+ return $exact_aliases[$slug];
+ }
+
+ $contains_aliases = [
+ 'weapon' => ['arme', 'weapon', 'pistol', 'rifle', 'shotgun', 'sniper', 'knife', 'blade'],
+ 'armor' => ['armure', 'armor', 'plating'],
+ 'tools' => ['outil', 'tool', 'tractor', 'mining', 'multitool', 'multi_tool', 'gadget'],
+ 'consumables' => ['consommable', 'consumable', 'usable', 'food', 'drink', 'med', 'medical', 'heal'],
+ 'ammunition' => ['munition', 'ammo', 'ammunition', 'magazine', 'grenade', 'rocket'],
+ 'attachments' => ['attachment', 'accessoire', 'scope', 'optic', 'silencer', 'sight', 'barrel'],
+ 'clothing' => ['clothing', 'vetement', 'apparel', 'outfit', 'char_clothing', 'char_head'],
+ 'cargo' => ['cargo', 'container', 'crate', 'box'],
+ 'ship' => ['component', 'composant', 'vaisseau', 'vehicule', 'thruster', 'quantum', 'powerplant', 'cooler', 'radar', 'sensor', 'shield', 'turret', 'missilelauncher', 'docking', 'flightcontroller', 'fueltank', 'fuelintake', 'aimodule'],
+ 'access' => ['door', 'seat', 'access', 'display', 'controlpanel', 'dashboard', 'mobilier'],
+ ];
+
+ foreach ($contains_aliases as $category => $needles) {
+ if (sccharacters_string_contains_any($slug, $needles)) {
+ return $category;
+ }
+ }
+
+ return '';
+}
+
+function sccharacters_guess_item_category(?string $type, ?string $subtype = null): string
+{
+ $haystack = strtolower(trim(((string) $type) . ' ' . ((string) $subtype)));
+ $slug = sccharacters_item_category_slug($haystack);
+
+ if ($slug === '') {
+ return 'misc';
+ }
+
+ if (sccharacters_string_contains_any($slug, ['char_clothing', 'char_head', 'clothing', 'apparel', 'outfit', 'hat', 'beard', 'piercing'])) {
+ return 'clothing';
+ }
+
+ if (sccharacters_string_contains_any($slug, ['weaponattachment', 'attachment', 'scope', 'optic', 'sight', 'silencer', 'barrel', 'mag_mount'])) {
+ return 'attachments';
+ }
+
+ if (sccharacters_string_contains_any($slug, ['missile', 'ammo', 'ammunition', 'magazine', 'rocket'])) {
+ return 'ammunition';
+ }
+
+ if (sccharacters_string_contains_any($slug, ['armor', 'armure'])) {
+ return 'armor';
+ }
+
+ if (sccharacters_string_contains_any($slug, ['weapon', 'gun', 'rifle', 'pistol', 'shotgun', 'sniper', 'knife', 'blade'])) {
+ return 'weapon';
+ }
+
+ if (sccharacters_string_contains_any($slug, ['usable', 'consumable', 'food', 'drink', 'fps_consumable', 'med', 'medical'])) {
+ return 'consumables';
+ }
+
+ if (sccharacters_string_contains_any($slug, ['tool', 'outil', 'mining', 'tractor', 'gadget', 'utility'])) {
+ return 'tools';
+ }
+
+ if (sccharacters_string_contains_any($slug, ['cargo', 'container', 'crate', 'box'])) {
+ return 'cargo';
+ }
+
+ if (sccharacters_string_contains_any($slug, ['door', 'seat', 'access', 'display', 'controlpanel', 'dashboard', 'shopdisplay', 'player'])) {
+ return 'access';
+ }
+
+ if (sccharacters_string_contains_any($slug, ['thruster', 'quantum', 'powerplant', 'cooler', 'radar', 'sensor', 'shield', 'turret', 'flightcontroller', 'fueltank', 'fuelintake', 'a_module', 'aimodule', 'docking', 'attachedpart'])) {
+ return 'ship';
+ }
+
+ return 'misc';
+}
+
+function sccharacters_resolve_item_category(?string $storedValue, ?string $type, ?string $subtype = null): string
+{
+ $normalized = sccharacters_normalize_item_category($storedValue);
+ if ($normalized !== '') {
+ return $normalized;
+ }
+
+ return sccharacters_guess_item_category($type, $subtype);
+}
diff --git a/sccharacter.php b/sccharacter.php
new file mode 100644
index 0000000..ee6b642
--- /dev/null
+++ b/sccharacter.php
@@ -0,0 +1,430 @@
+prepare(
+ 'SELECT c.*, COALESCE(NULLIF(TRIM(a.cl_auth_user), \'\'), \'Inconnu\') AS cl_sccharacter_creator_name
+ FROM tbl_sccharacters c
+ LEFT JOIN tbl_auth a ON a.cl_auth_id = c.cl_sccharacter_owner_auth_id
+ WHERE c.cl_sccharacter_share_token = :share_token
+ AND c.cl_sccharacter_share_enabled = 1
+ LIMIT 1'
+ );
+ $stmt_character->execute(['share_token' => $share_token]);
+ $character = $stmt_character->fetch() ?: null;
+}
+
+if ($character) {
+ $stmt_items = $db->prepare(
+ "SELECT
+ ci.*,
+ bo.cl_scobjs_name AS cl_sccharacteritem_base_name,
+ bo.cl_scobjs_type AS cl_sccharacteritem_base_type,
+ bo.cl_scobjs_subtype AS cl_sccharacteritem_base_subtype,
+ bo.cl_scobjs_uuid AS cl_sccharacteritem_base_uuid,
+ oo.cl_scobjs_name AS cl_sccharacteritem_custom_name,
+ oo.cl_scobjs_type AS cl_sccharacteritem_custom_type,
+ oo.cl_scobjs_subtype AS cl_sccharacteritem_custom_subtype,
+ oo.cl_scobjs_uuid AS cl_sccharacteritem_custom_uuid
+ FROM tbl_sccharacteritems ci
+ LEFT JOIN tbl_scobjs bo ON bo.cl_scobjs_id = ci.cl_sccharacteritem_scobjs_id
+ LEFT JOIN tbl_scitemcustom co ON co.cl_scitemcustom_id = ci.cl_sccharacteritem_scitemcustom_id
+ LEFT JOIN tbl_scobjs oo ON oo.cl_scobjs_id = co.cl_scitemcustom_obj_id
+ WHERE ci.cl_sccharacteritem_character_id = :character_id
+ ORDER BY
+ CASE WHEN TRIM(ci.cl_sccharacteritem_slot) = '' THEN 1 ELSE 0 END,
+ ci.cl_sccharacteritem_slot ASC,
+ COALESCE(oo.cl_scobjs_name, bo.cl_scobjs_name, 'ZZZ') ASC,
+ ci.cl_sccharacteritem_id ASC"
+ );
+ $stmt_items->execute(['character_id' => (int) $character['cl_sccharacter_id']]);
+ $character_items = $stmt_items->fetchAll();
+
+ $custom_item_ids = [];
+ foreach ($character_items as $row) {
+ if (($row['cl_sccharacteritem_source'] ?? '') === 'custom' && !empty($row['cl_sccharacteritem_scitemcustom_id'])) {
+ $custom_item_ids[] = (int) $row['cl_sccharacteritem_scitemcustom_id'];
+ }
+ }
+ $custom_item_ids = array_values(array_unique(array_filter($custom_item_ids)));
+
+ if ($custom_item_ids !== []) {
+ $placeholders = implode(',', array_fill(0, count($custom_item_ids), '?'));
+ $stmt_stats = $db->prepare(
+ "SELECT
+ cs.cl_scitemcustomstat_itemcustom_id,
+ st.cl_scstatsitem_name,
+ st.cl_scstatsitem_unit,
+ cs.cl_scitemcustomstat_sign,
+ cs.cl_scitemcustomstat_value
+ FROM tbl_scitemcustomstat cs
+ INNER JOIN tbl_scstatsitem st ON st.cl_scstatsitem_id = cs.cl_scitemcustomstat_stat_id
+ WHERE cs.cl_scitemcustomstat_itemcustom_id IN ({$placeholders})
+ ORDER BY st.cl_scstatsitem_name ASC, cs.cl_scitemcustomstat_id ASC"
+ );
+ $stmt_stats->execute($custom_item_ids);
+
+ foreach ($stmt_stats->fetchAll() as $stat_row) {
+ $itemcustom_id = (int) $stat_row['cl_scitemcustomstat_itemcustom_id'];
+ if (!isset($custom_stats_by_itemcustom[$itemcustom_id])) {
+ $custom_stats_by_itemcustom[$itemcustom_id] = [];
+ }
+ $custom_stats_by_itemcustom[$itemcustom_id][] = $stat_row;
+ }
+ }
+} else {
+ http_response_code(404);
+}
+
+$item_category_options = sccharacters_item_category_options();
+$character_items_by_category = [];
+foreach (array_keys($item_category_options) as $category_key) {
+ $character_items_by_category[$category_key] = [];
+}
+foreach ($character_items as $item_row) {
+ $is_custom = ($item_row['cl_sccharacteritem_source'] ?? '') === 'custom';
+ $type = $is_custom
+ ? (string) ($item_row['cl_sccharacteritem_custom_type'] ?? '')
+ : (string) ($item_row['cl_sccharacteritem_base_type'] ?? '');
+ $subtype = $is_custom
+ ? (string) ($item_row['cl_sccharacteritem_custom_subtype'] ?? '')
+ : (string) ($item_row['cl_sccharacteritem_base_subtype'] ?? '');
+ $category_key = sccharacters_resolve_item_category(
+ (string) ($item_row['cl_sccharacteritem_slot'] ?? ''),
+ $type,
+ $subtype
+ );
+ if (!isset($character_items_by_category[$category_key])) {
+ $character_items_by_category[$category_key] = [];
+ }
+ $character_items_by_category[$category_key][] = $item_row;
+}
+$character_items_by_category = array_filter(
+ $character_items_by_category,
+ static fn(array $items): bool => $items !== []
+);
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
Personnage introuvable
+
Ce lien public est invalide, désactivé ou n’est plus disponible.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Créé par
+ équipement(s)
+
+
+
+
+
+
Équipement
+
+
+
Ce personnage n’a pas encore d’équipement attribué.
+
+
+
+ $category_items): ?>
+
+
+
+ objet(s)
+
+
+
+
+
+
+
+
+ ◈
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sccharacters.php b/sccharacters.php
new file mode 100644
index 0000000..94115b3
--- /dev/null
+++ b/sccharacters.php
@@ -0,0 +1,2099 @@
+prepare(
+ 'SELECT cl_auth_id
+ FROM tbl_auth
+ WHERE cl_auth_user = :user
+ LIMIT 1'
+ );
+ $stmt->execute(['user' => $session_user]);
+
+ return (int) $stmt->fetchColumn();
+}
+
+function sccharacters_clean_text(?string $value): string
+{
+ return trim((string) $value);
+}
+
+function sccharacters_is_valid_url(string $value): bool
+{
+ if ($value === '') {
+ return true;
+ }
+
+ return filter_var($value, FILTER_VALIDATE_URL) !== false;
+}
+
+function sccharacters_excerpt(string $value, int $limit = 120): string
+{
+ $value = trim($value);
+ if ($value === '') {
+ return '';
+ }
+
+ if (function_exists('mb_strimwidth')) {
+ return mb_strimwidth($value, 0, $limit, '…', 'UTF-8');
+ }
+
+ if (strlen($value) <= $limit) {
+ return $value;
+ }
+
+ return rtrim(substr($value, 0, max(0, $limit - 1))) . '…';
+}
+
+function sccharacters_share_url(string $token): string
+{
+ $is_https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
+ || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https')
+ || ((string) ($_SERVER['SERVER_PORT'] ?? '') === '443');
+
+ $scheme = $is_https ? 'https' : 'http';
+ $host = trim((string) ($_SERVER['HTTP_HOST'] ?? ''));
+
+ if ($host === '') {
+ $host = '127.0.0.1';
+ }
+
+ return $scheme . '://' . $host . '/sccharacter.php?share=' . rawurlencode($token);
+}
+
+function sccharacters_find_owned_character(PDO $db, int $character_id, int $owner_auth_id): ?array
+{
+ if ($character_id <= 0 || $owner_auth_id <= 0) {
+ return null;
+ }
+
+ $stmt = $db->prepare(
+ 'SELECT *
+ FROM tbl_sccharacters
+ WHERE cl_sccharacter_id = :id
+ AND cl_sccharacter_owner_auth_id = :owner_auth_id
+ LIMIT 1'
+ );
+ $stmt->execute([
+ 'id' => $character_id,
+ 'owner_auth_id' => $owner_auth_id,
+ ]);
+
+ $row = $stmt->fetch();
+
+ return $row ?: null;
+}
+
+function sccharacters_build_return_url(int $character_id = 0, string $item_source = '', string $item_search = '', bool $keep_item_panel = false): string
+{
+ $params = [];
+
+ if ($character_id > 0) {
+ $params['character'] = $character_id;
+ }
+
+ if (in_array($item_source, ['base', 'custom'], true)) {
+ $params['item_source'] = $item_source;
+ }
+
+ if ($item_search !== '') {
+ $params['item_search'] = $item_search;
+ }
+
+ if ($keep_item_panel) {
+ $params['item_panel'] = '1';
+ }
+
+ return 'sccharacters.php' . ($params !== [] ? '?' . http_build_query($params) : '');
+}
+
+function sccharacters_attach_item(
+ PDO $db,
+ int $owner_auth_id,
+ int $character_id,
+ string $source,
+ int $source_id,
+ string $requested_category,
+ string $note,
+ ?string &$error_message = null
+): bool {
+ if ($character_id <= 0 || $owner_auth_id <= 0 || $source_id <= 0) {
+ $error_message = 'Paramètres d’ajout invalides.';
+ return false;
+ }
+
+ $character = sccharacters_find_owned_character($db, $character_id, $owner_auth_id);
+ if (!$character) {
+ $error_message = 'Personnage introuvable.';
+ return false;
+ }
+
+ if ($source === 'base') {
+ $stmt_source = $db->prepare(
+ 'SELECT cl_scobjs_id AS item_id, cl_scobjs_type, cl_scobjs_subtype
+ FROM tbl_scobjs
+ WHERE cl_scobjs_id = :id
+ LIMIT 1'
+ );
+ $stmt_source->execute(['id' => $source_id]);
+ $item_row = $stmt_source->fetch();
+
+ if (!$item_row) {
+ $error_message = 'Objet de base introuvable.';
+ return false;
+ }
+
+ $category = sccharacters_resolve_item_category(
+ $requested_category,
+ (string) ($item_row['cl_scobjs_type'] ?? ''),
+ (string) ($item_row['cl_scobjs_subtype'] ?? '')
+ );
+
+ $stmt_insert = $db->prepare(
+ 'INSERT INTO tbl_sccharacteritems (
+ cl_sccharacteritem_character_id,
+ cl_sccharacteritem_source,
+ cl_sccharacteritem_scobjs_id,
+ cl_sccharacteritem_scitemcustom_id,
+ cl_sccharacteritem_slot,
+ cl_sccharacteritem_note
+ ) VALUES (
+ :character_id,
+ :source,
+ :scobjs_id,
+ NULL,
+ :slot,
+ :note
+ )'
+ );
+ $stmt_insert->execute([
+ 'character_id' => $character_id,
+ 'source' => 'base',
+ 'scobjs_id' => $source_id,
+ 'slot' => $category,
+ 'note' => $note !== '' ? $note : null,
+ ]);
+
+ return true;
+ }
+
+ if ($source === 'custom') {
+ $stmt_source = $db->prepare(
+ 'SELECT c.cl_scitemcustom_id AS item_id, o.cl_scobjs_type, o.cl_scobjs_subtype
+ FROM tbl_scitemcustom c
+ INNER JOIN tbl_scobjs o ON o.cl_scobjs_id = c.cl_scitemcustom_obj_id
+ WHERE c.cl_scitemcustom_id = :id
+ AND c.cl_scitemcustom_owner_auth_id = :owner_auth_id
+ LIMIT 1'
+ );
+ $stmt_source->execute([
+ 'id' => $source_id,
+ 'owner_auth_id' => $owner_auth_id,
+ ]);
+ $item_row = $stmt_source->fetch();
+
+ if (!$item_row) {
+ $error_message = 'Objet personnalisé introuvable ou non autorisé.';
+ return false;
+ }
+
+ $category = sccharacters_resolve_item_category(
+ $requested_category,
+ (string) ($item_row['cl_scobjs_type'] ?? ''),
+ (string) ($item_row['cl_scobjs_subtype'] ?? '')
+ );
+
+ $stmt_insert = $db->prepare(
+ 'INSERT INTO tbl_sccharacteritems (
+ cl_sccharacteritem_character_id,
+ cl_sccharacteritem_source,
+ cl_sccharacteritem_scobjs_id,
+ cl_sccharacteritem_scitemcustom_id,
+ cl_sccharacteritem_slot,
+ cl_sccharacteritem_note
+ ) VALUES (
+ :character_id,
+ :source,
+ NULL,
+ :scitemcustom_id,
+ :slot,
+ :note
+ )'
+ );
+ $stmt_insert->execute([
+ 'character_id' => $character_id,
+ 'source' => 'custom',
+ 'scitemcustom_id' => $source_id,
+ 'slot' => $category,
+ 'note' => $note !== '' ? $note : null,
+ ]);
+
+ return true;
+ }
+
+ $error_message = 'Source d’objet invalide.';
+ return false;
+}
+
+$flash = auth_flash_get();
+$flash_type = $flash['type'] ?? '';
+$flash_message = $flash['message'] ?? '';
+$db = db();
+$csrf_token = auth_csrf_token();
+$current_owner_auth_id = sccharacters_current_owner_auth_id($db);
+$current_session_user = auth_current_user();
+$current_session_role = auth_current_role();
+$role_label = auth_role_label($current_session_role);
+
+if ($current_owner_auth_id <= 0) {
+ auth_flash_set('error', 'Impossible d’identifier le compte connecté.');
+ header('Location: index.php');
+ exit;
+}
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $submitted_csrf = (string) ($_POST['csrf_token'] ?? '');
+ if (!auth_validate_csrf($submitted_csrf)) {
+ auth_flash_set('error', 'Jeton CSRF invalide.');
+ header('Location: sccharacters.php');
+ exit;
+ }
+
+ $action = (string) ($_POST['action'] ?? '');
+
+ if ($action === 'create_character') {
+ $name = sccharacters_clean_text($_POST['character_name'] ?? '');
+ $role = sccharacters_clean_text($_POST['character_role'] ?? '');
+ $faction = sccharacters_clean_text($_POST['character_faction'] ?? '');
+ $avatar_url = sccharacters_clean_text($_POST['character_avatar_url'] ?? '');
+ $description = sccharacters_clean_text($_POST['character_description'] ?? '');
+ $notes = sccharacters_clean_text($_POST['character_notes'] ?? '');
+ $share_enabled = isset($_POST['character_share_enabled']) ? 1 : 0;
+
+ if ($name === '') {
+ auth_flash_set('error', 'Le nom du personnage est obligatoire.');
+ header('Location: sccharacters.php?mode=create');
+ exit;
+ }
+
+ if (!sccharacters_is_valid_url($avatar_url)) {
+ auth_flash_set('error', 'L’URL de l’avatar n’est pas valide.');
+ header('Location: sccharacters.php?mode=create');
+ exit;
+ }
+
+ $stmt = $db->prepare(
+ 'INSERT INTO tbl_sccharacters (
+ cl_sccharacter_owner_auth_id,
+ cl_sccharacter_name,
+ cl_sccharacter_role,
+ cl_sccharacter_faction,
+ cl_sccharacter_avatar_url,
+ cl_sccharacter_description,
+ cl_sccharacter_notes,
+ cl_sccharacter_share_token,
+ cl_sccharacter_share_enabled
+ ) VALUES (
+ :owner_auth_id,
+ :name,
+ :role,
+ :faction,
+ :avatar_url,
+ :description,
+ :notes,
+ :share_token,
+ :share_enabled
+ )'
+ );
+ $stmt->execute([
+ 'owner_auth_id' => $current_owner_auth_id,
+ 'name' => $name,
+ 'role' => $role,
+ 'faction' => $faction,
+ 'avatar_url' => $avatar_url,
+ 'description' => $description !== '' ? $description : null,
+ 'notes' => $notes !== '' ? $notes : null,
+ 'share_token' => sccharacters_generate_share_token($db),
+ 'share_enabled' => $share_enabled,
+ ]);
+
+ $new_character_id = (int) $db->lastInsertId();
+ auth_flash_set('success', 'Personnage créé avec succès.');
+ header('Location: sccharacters.php?character=' . $new_character_id);
+ exit;
+ }
+
+ if ($action === 'update_character') {
+ $character_id = (int) ($_POST['character_id'] ?? 0);
+ $character = sccharacters_find_owned_character($db, $character_id, $current_owner_auth_id);
+
+ if (!$character) {
+ auth_flash_set('error', 'Personnage introuvable.');
+ header('Location: sccharacters.php');
+ exit;
+ }
+
+ $name = sccharacters_clean_text($_POST['character_name'] ?? '');
+ $role = sccharacters_clean_text($_POST['character_role'] ?? '');
+ $faction = sccharacters_clean_text($_POST['character_faction'] ?? '');
+ $avatar_url = sccharacters_clean_text($_POST['character_avatar_url'] ?? '');
+ $description = sccharacters_clean_text($_POST['character_description'] ?? '');
+ $notes = sccharacters_clean_text($_POST['character_notes'] ?? '');
+ $share_enabled = isset($_POST['character_share_enabled']) ? 1 : 0;
+
+ if ($name === '') {
+ auth_flash_set('error', 'Le nom du personnage est obligatoire.');
+ header('Location: sccharacters.php?character=' . $character_id);
+ exit;
+ }
+
+ if (!sccharacters_is_valid_url($avatar_url)) {
+ auth_flash_set('error', 'L’URL de l’avatar n’est pas valide.');
+ header('Location: sccharacters.php?character=' . $character_id);
+ exit;
+ }
+
+ $stmt = $db->prepare(
+ 'UPDATE tbl_sccharacters
+ SET cl_sccharacter_name = :name,
+ cl_sccharacter_role = :role,
+ cl_sccharacter_faction = :faction,
+ cl_sccharacter_avatar_url = :avatar_url,
+ cl_sccharacter_description = :description,
+ cl_sccharacter_notes = :notes,
+ cl_sccharacter_share_enabled = :share_enabled
+ WHERE cl_sccharacter_id = :id
+ AND cl_sccharacter_owner_auth_id = :owner_auth_id'
+ );
+ $stmt->execute([
+ 'name' => $name,
+ 'role' => $role,
+ 'faction' => $faction,
+ 'avatar_url' => $avatar_url,
+ 'description' => $description !== '' ? $description : null,
+ 'notes' => $notes !== '' ? $notes : null,
+ 'share_enabled' => $share_enabled,
+ 'id' => $character_id,
+ 'owner_auth_id' => $current_owner_auth_id,
+ ]);
+
+ auth_flash_set('success', 'Personnage mis à jour.');
+ header('Location: sccharacters.php?character=' . $character_id);
+ exit;
+ }
+
+ if ($action === 'delete_character') {
+ $character_id = (int) ($_POST['character_id'] ?? 0);
+ $character = sccharacters_find_owned_character($db, $character_id, $current_owner_auth_id);
+
+ if (!$character) {
+ auth_flash_set('error', 'Personnage introuvable.');
+ header('Location: sccharacters.php');
+ exit;
+ }
+
+ $stmt = $db->prepare(
+ 'DELETE FROM tbl_sccharacters
+ WHERE cl_sccharacter_id = :id
+ AND cl_sccharacter_owner_auth_id = :owner_auth_id'
+ );
+ $stmt->execute([
+ 'id' => $character_id,
+ 'owner_auth_id' => $current_owner_auth_id,
+ ]);
+
+ auth_flash_set('success', 'Personnage supprimé.');
+ header('Location: sccharacters.php');
+ exit;
+ }
+
+ if ($action === 'regenerate_share_token') {
+ $character_id = (int) ($_POST['character_id'] ?? 0);
+ $character = sccharacters_find_owned_character($db, $character_id, $current_owner_auth_id);
+
+ if (!$character) {
+ auth_flash_set('error', 'Personnage introuvable.');
+ header('Location: sccharacters.php');
+ exit;
+ }
+
+ $stmt = $db->prepare(
+ 'UPDATE tbl_sccharacters
+ SET cl_sccharacter_share_token = :token
+ WHERE cl_sccharacter_id = :id
+ AND cl_sccharacter_owner_auth_id = :owner_auth_id'
+ );
+ $stmt->execute([
+ 'token' => sccharacters_generate_share_token($db),
+ 'id' => $character_id,
+ 'owner_auth_id' => $current_owner_auth_id,
+ ]);
+
+ auth_flash_set('success', 'Lien public régénéré.');
+ header('Location: sccharacters.php?character=' . $character_id);
+ exit;
+ }
+
+ if ($action === 'add_base_item') {
+ $character_id = (int) ($_POST['character_id'] ?? 0);
+ $obj_id = (int) ($_POST['base_obj_id'] ?? 0);
+ $requested_category = sccharacters_clean_text($_POST['item_slot'] ?? '');
+ $note = sccharacters_clean_text($_POST['item_note'] ?? '');
+ $item_source_context = sccharacters_clean_text($_POST['item_source_context'] ?? 'base');
+ $item_search_context = sccharacters_clean_text($_POST['item_search_context'] ?? '');
+ $error_message = null;
+
+ if (!sccharacters_attach_item(
+ $db,
+ $current_owner_auth_id,
+ $character_id,
+ 'base',
+ $obj_id,
+ $requested_category,
+ $note,
+ $error_message
+ )) {
+ auth_flash_set('error', $error_message ?? 'Impossible d’ajouter l’objet.');
+ header('Location: ' . sccharacters_build_return_url($character_id, $item_source_context, $item_search_context, true));
+ exit;
+ }
+
+ auth_flash_set('success', 'Objet de la base ajouté au personnage.');
+ header('Location: ' . sccharacters_build_return_url($character_id, $item_source_context, $item_search_context, true));
+ exit;
+ }
+
+ if ($action === 'add_custom_item') {
+ $character_id = (int) ($_POST['character_id'] ?? 0);
+ $itemcustom_id = (int) ($_POST['custom_item_id'] ?? 0);
+ $requested_category = sccharacters_clean_text($_POST['item_slot'] ?? '');
+ $note = sccharacters_clean_text($_POST['item_note'] ?? '');
+ $item_source_context = sccharacters_clean_text($_POST['item_source_context'] ?? 'custom');
+ $item_search_context = sccharacters_clean_text($_POST['item_search_context'] ?? '');
+ $error_message = null;
+
+ if (!sccharacters_attach_item(
+ $db,
+ $current_owner_auth_id,
+ $character_id,
+ 'custom',
+ $itemcustom_id,
+ $requested_category,
+ $note,
+ $error_message
+ )) {
+ auth_flash_set('error', $error_message ?? 'Impossible d’ajouter l’objet.');
+ header('Location: ' . sccharacters_build_return_url($character_id, $item_source_context, $item_search_context, true));
+ exit;
+ }
+
+ auth_flash_set('success', 'Objet personnalisé ajouté au personnage.');
+ header('Location: ' . sccharacters_build_return_url($character_id, $item_source_context, $item_search_context, true));
+ exit;
+ }
+
+ if ($action === 'add_selected_items') {
+ $character_id = (int) ($_POST['character_id'] ?? 0);
+ $item_source_context = sccharacters_clean_text($_POST['item_source_context'] ?? 'base');
+ $item_search_context = sccharacters_clean_text($_POST['item_search_context'] ?? '');
+ $selected_items = $_POST['selected_items'] ?? [];
+ $return_url = sccharacters_build_return_url($character_id, $item_source_context, $item_search_context, true);
+ $character = sccharacters_find_owned_character($db, $character_id, $current_owner_auth_id);
+
+ if (!$character) {
+ auth_flash_set('error', 'Personnage introuvable.');
+ header('Location: sccharacters.php');
+ exit;
+ }
+
+ if (!is_array($selected_items) || $selected_items === []) {
+ auth_flash_set('error', 'Sélectionne au moins un objet avant de valider.');
+ header('Location: ' . $return_url);
+ exit;
+ }
+
+ $selected_items = array_values(array_unique(array_map('strval', $selected_items)));
+ $item_slots = isset($_POST['item_slot']) && is_array($_POST['item_slot']) ? $_POST['item_slot'] : [];
+ $item_notes = isset($_POST['item_note']) && is_array($_POST['item_note']) ? $_POST['item_note'] : [];
+ $added_count = 0;
+ $error_count = 0;
+
+ foreach ($selected_items as $item_key) {
+ if (!preg_match('/^(base|custom):(\d+)$/', $item_key, $matches)) {
+ $error_count++;
+ continue;
+ }
+
+ $source = (string) $matches[1];
+ $source_id = (int) $matches[2];
+ $requested_category = sccharacters_clean_text($item_slots[$item_key] ?? '');
+ $note = sccharacters_clean_text($item_notes[$item_key] ?? '');
+ $error_message = null;
+
+ if (sccharacters_attach_item(
+ $db,
+ $current_owner_auth_id,
+ $character_id,
+ $source,
+ $source_id,
+ $requested_category,
+ $note,
+ $error_message
+ )) {
+ $added_count++;
+ continue;
+ }
+
+ $error_count++;
+ }
+
+ if ($added_count <= 0) {
+ auth_flash_set('error', 'Aucun objet n’a pu être ajouté à la sélection.');
+ header('Location: ' . $return_url);
+ exit;
+ }
+
+ if ($error_count > 0) {
+ auth_flash_set('success', $added_count . ' objet(s) ajouté(s). Certains éléments sélectionnés ont été ignorés.');
+ header('Location: ' . $return_url);
+ exit;
+ }
+
+ auth_flash_set('success', $added_count . ' objet(s) ajouté(s) au personnage.');
+ header('Location: ' . $return_url);
+ exit;
+ }
+
+ if ($action === 'delete_character_item') {
+ $character_item_id = (int) ($_POST['character_item_id'] ?? 0);
+ $character_id = (int) ($_POST['character_id'] ?? 0);
+
+ $stmt = $db->prepare(
+ 'DELETE ci
+ FROM tbl_sccharacteritems ci
+ INNER JOIN tbl_sccharacters c ON c.cl_sccharacter_id = ci.cl_sccharacteritem_character_id
+ WHERE ci.cl_sccharacteritem_id = :character_item_id
+ AND c.cl_sccharacter_id = :character_id
+ AND c.cl_sccharacter_owner_auth_id = :owner_auth_id'
+ );
+ $stmt->execute([
+ 'character_item_id' => $character_item_id,
+ 'character_id' => $character_id,
+ 'owner_auth_id' => $current_owner_auth_id,
+ ]);
+
+ auth_flash_set('success', 'Objet retiré du personnage.');
+ header('Location: sccharacters.php?character=' . $character_id);
+ exit;
+ }
+}
+
+$stmt_characters = $db->prepare(
+ 'SELECT c.*, COUNT(ci.cl_sccharacteritem_id) AS cl_sccharacter_item_count
+ FROM tbl_sccharacters c
+ LEFT JOIN tbl_sccharacteritems ci ON ci.cl_sccharacteritem_character_id = c.cl_sccharacter_id
+ WHERE c.cl_sccharacter_owner_auth_id = :owner_auth_id
+ GROUP BY c.cl_sccharacter_id
+ ORDER BY c.cl_sccharacter_updated_at DESC, c.cl_sccharacter_name ASC'
+);
+$stmt_characters->execute(['owner_auth_id' => $current_owner_auth_id]);
+$characters = $stmt_characters->fetchAll();
+
+$character_lookup = [];
+foreach ($characters as $character_row) {
+ $character_lookup[(int) $character_row['cl_sccharacter_id']] = $character_row;
+}
+
+$mode = (string) ($_GET['mode'] ?? '');
+$selected_character_id = (int) ($_GET['character'] ?? 0);
+$selected_character = null;
+
+if ($selected_character_id > 0 && isset($character_lookup[$selected_character_id])) {
+ $selected_character = $character_lookup[$selected_character_id];
+} elseif ($characters !== []) {
+ $selected_character = $characters[0];
+ $selected_character_id = (int) $selected_character['cl_sccharacter_id'];
+}
+
+$create_panel_open = ($mode === 'create') || ($characters === []);
+$item_source = sccharacters_clean_text($_GET['item_source'] ?? 'base');
+if (!in_array($item_source, ['base', 'custom'], true)) {
+ $item_source = 'base';
+}
+
+$item_search = sccharacters_clean_text($_GET['item_search'] ?? '');
+$item_panel_open = (string) ($_GET['item_panel'] ?? '') === '1';
+$item_results = [];
+
+if ($selected_character) {
+ if ($item_source === 'custom') {
+ $sql = "SELECT c.cl_scitemcustom_id,
+ o.cl_scobjs_name,
+ o.cl_scobjs_type,
+ o.cl_scobjs_subtype,
+ o.cl_scobjs_uuid,
+ COUNT(cs.cl_scitemcustomstat_id) AS cl_scitemcustom_stat_count
+ FROM tbl_scitemcustom c
+ INNER JOIN tbl_scobjs o ON o.cl_scobjs_id = c.cl_scitemcustom_obj_id
+ LEFT JOIN tbl_scitemcustomstat cs ON cs.cl_scitemcustomstat_itemcustom_id = c.cl_scitemcustom_id
+ WHERE c.cl_scitemcustom_owner_auth_id = :owner_auth_id";
+ $params = ['owner_auth_id' => $current_owner_auth_id];
+
+ if ($item_search !== '') {
+ $sql .= " AND (
+ o.cl_scobjs_name LIKE :search
+ OR o.cl_scobjs_type LIKE :search
+ OR o.cl_scobjs_subtype LIKE :search
+ OR o.cl_scobjs_uuid LIKE :search
+ )";
+ $params['search'] = '%' . $item_search . '%';
+ }
+
+ $sql .= "
+ GROUP BY c.cl_scitemcustom_id
+ ORDER BY o.cl_scobjs_name ASC, c.cl_scitemcustom_id ASC
+ LIMIT 25";
+
+ $stmt_item_results = $db->prepare($sql);
+ $stmt_item_results->execute($params);
+ $item_results = $stmt_item_results->fetchAll();
+ } else {
+ $sql = "SELECT cl_scobjs_id, cl_scobjs_name, cl_scobjs_type, cl_scobjs_subtype, cl_scobjs_uuid
+ FROM tbl_scobjs
+ WHERE 1 = 1";
+ $params = [];
+
+ if ($item_search !== '') {
+ $sql .= " AND (
+ cl_scobjs_name LIKE :search
+ OR cl_scobjs_type LIKE :search
+ OR cl_scobjs_subtype LIKE :search
+ OR cl_scobjs_uuid LIKE :search
+ )";
+ $params['search'] = '%' . $item_search . '%';
+ }
+
+ $sql .= "
+ ORDER BY cl_scobjs_name ASC
+ LIMIT 25";
+
+ $stmt_item_results = $db->prepare($sql);
+ $stmt_item_results->execute($params);
+ $item_results = $stmt_item_results->fetchAll();
+ }
+}
+
+$selected_character_items = [];
+$custom_stats_by_itemcustom = [];
+if ($selected_character) {
+ $stmt_character_items = $db->prepare(
+ "SELECT
+ ci.*,
+ bo.cl_scobjs_name AS cl_sccharacteritem_base_name,
+ bo.cl_scobjs_type AS cl_sccharacteritem_base_type,
+ bo.cl_scobjs_subtype AS cl_sccharacteritem_base_subtype,
+ bo.cl_scobjs_uuid AS cl_sccharacteritem_base_uuid,
+ co.cl_scitemcustom_id AS cl_sccharacteritem_custom_ref_id,
+ oo.cl_scobjs_name AS cl_sccharacteritem_custom_name,
+ oo.cl_scobjs_type AS cl_sccharacteritem_custom_type,
+ oo.cl_scobjs_subtype AS cl_sccharacteritem_custom_subtype,
+ oo.cl_scobjs_uuid AS cl_sccharacteritem_custom_uuid
+ FROM tbl_sccharacteritems ci
+ LEFT JOIN tbl_scobjs bo ON bo.cl_scobjs_id = ci.cl_sccharacteritem_scobjs_id
+ LEFT JOIN tbl_scitemcustom co ON co.cl_scitemcustom_id = ci.cl_sccharacteritem_scitemcustom_id
+ LEFT JOIN tbl_scobjs oo ON oo.cl_scobjs_id = co.cl_scitemcustom_obj_id
+ WHERE ci.cl_sccharacteritem_character_id = :character_id
+ ORDER BY
+ CASE WHEN TRIM(ci.cl_sccharacteritem_slot) = '' THEN 1 ELSE 0 END,
+ ci.cl_sccharacteritem_slot ASC,
+ COALESCE(oo.cl_scobjs_name, bo.cl_scobjs_name, 'ZZZ') ASC,
+ ci.cl_sccharacteritem_id ASC"
+ );
+ $stmt_character_items->execute(['character_id' => (int) $selected_character['cl_sccharacter_id']]);
+ $selected_character_items = $stmt_character_items->fetchAll();
+
+ $custom_item_ids = [];
+ foreach ($selected_character_items as $character_item_row) {
+ if (($character_item_row['cl_sccharacteritem_source'] ?? '') === 'custom' && !empty($character_item_row['cl_sccharacteritem_scitemcustom_id'])) {
+ $custom_item_ids[] = (int) $character_item_row['cl_sccharacteritem_scitemcustom_id'];
+ }
+ }
+ $custom_item_ids = array_values(array_unique(array_filter($custom_item_ids)));
+
+ if ($custom_item_ids !== []) {
+ $placeholders = implode(',', array_fill(0, count($custom_item_ids), '?'));
+ $stmt_custom_stats = $db->prepare(
+ "SELECT
+ cs.cl_scitemcustomstat_itemcustom_id,
+ st.cl_scstatsitem_name,
+ st.cl_scstatsitem_unit,
+ cs.cl_scitemcustomstat_sign,
+ cs.cl_scitemcustomstat_value
+ FROM tbl_scitemcustomstat cs
+ INNER JOIN tbl_scstatsitem st ON st.cl_scstatsitem_id = cs.cl_scitemcustomstat_stat_id
+ WHERE cs.cl_scitemcustomstat_itemcustom_id IN ({$placeholders})
+ ORDER BY st.cl_scstatsitem_name ASC, cs.cl_scitemcustomstat_id ASC"
+ );
+ $stmt_custom_stats->execute($custom_item_ids);
+
+ foreach ($stmt_custom_stats->fetchAll() as $custom_stat_row) {
+ $itemcustom_id = (int) $custom_stat_row['cl_scitemcustomstat_itemcustom_id'];
+ if (!isset($custom_stats_by_itemcustom[$itemcustom_id])) {
+ $custom_stats_by_itemcustom[$itemcustom_id] = [];
+ }
+ $custom_stats_by_itemcustom[$itemcustom_id][] = $custom_stat_row;
+ }
+ }
+}
+
+$item_category_options = sccharacters_item_category_options();
+$selected_character_items_by_category = [];
+foreach (array_keys($item_category_options) as $category_key) {
+ $selected_character_items_by_category[$category_key] = [];
+}
+foreach ($selected_character_items as $character_item_row) {
+ $is_custom = ($character_item_row['cl_sccharacteritem_source'] ?? '') === 'custom';
+ $item_type = $is_custom
+ ? (string) ($character_item_row['cl_sccharacteritem_custom_type'] ?? '')
+ : (string) ($character_item_row['cl_sccharacteritem_base_type'] ?? '');
+ $item_subtype = $is_custom
+ ? (string) ($character_item_row['cl_sccharacteritem_custom_subtype'] ?? '')
+ : (string) ($character_item_row['cl_sccharacteritem_base_subtype'] ?? '');
+ $category_key = sccharacters_resolve_item_category(
+ (string) ($character_item_row['cl_sccharacteritem_slot'] ?? ''),
+ $item_type,
+ $item_subtype
+ );
+ if (!isset($selected_character_items_by_category[$category_key])) {
+ $selected_character_items_by_category[$category_key] = [];
+ }
+ $selected_character_items_by_category[$category_key][] = $character_item_row;
+}
+$selected_character_items_by_category = array_filter(
+ $selected_character_items_by_category,
+ static fn(array $items): bool => $items !== []
+);
+
+$create_character = [
+ 'cl_sccharacter_name' => '',
+ 'cl_sccharacter_role' => '',
+ 'cl_sccharacter_faction' => '',
+ 'cl_sccharacter_avatar_url' => '',
+ 'cl_sccharacter_description' => '',
+ 'cl_sccharacter_notes' => '',
+ 'cl_sccharacter_share_enabled' => 0,
+];
+?>
+
+
+
+
+
+ PERSONNAGES | R.E.A.C.T. Admin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Partage par lien uniquement
+ Page publique dédiée
+
+
+
+ Copier le lien
+
+
+
+
+ Régénérer
+
+
+
+
+
+
+
+
+
+
+
Modifier la fiche
+
Bloc repliable pour garder la fiche visible sans laisser le formulaire ouvert en permanence.
+
+ +
+
+
+
+
+
+
+
+
+
+
+ Enregistrer
+
+
+
+
+
+
+
+
+ Supprimer le personnage
+
+
+
+
+
+
+
+
+
Équipement attribué
+
Bloc repliable pour consulter rapidement l’équipement et le refermer quand tu veux dégager la vue.
+
+ +
+
+
+
+
+
+ Aucun équipement attribué
+ Utilise le module de recherche plus bas pour ajouter un objet de la base ou un objet personnalisé.
+
+
+
+ $category_items): ?>
+
+
+
+ objet(s)
+
+
+
+
+
+
+
+
+ ◈
+
+
+
+
+
+
+
+
+ :
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Retirer
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+
+
+
Ajouter des objets
+
Recherche repliable : tu l’ouvres quand tu équipes le personnage, tu la refermes dès que c’est fait.
+
+ +
+
+
+
+
+
+
+
+
+
+
+ Aucun objet personnalisé ne correspond à « ».
+
+ Commence par créer des objets dans l’onglet « Objets perso. » pour pouvoir les attribuer à tes personnages.
+
+
+
+ Aucun objet de la base ne correspond à « ».
+
+ La recherche est prête. Tu peux aussi laisser le champ vide pour parcourir les 25 premiers objets de la base.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sélectionner
+
+
+
+
+
+
+
+ UUID :
+
+
+ stat(s)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Aucun personnage sélectionné
+ Crée un personnage via le bloc repliable de gauche, puis sélectionne-le dans la liste pour gérer sa fiche et son équipement.
+
+
+
+
+
+
+