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 !== [] +); +?> + + + + + + <?php echo htmlspecialchars($character ? ((string) $character['cl_sccharacter_name'] . ' | Personnage partagé') : 'Personnage introuvable', ENT_QUOTES, 'UTF-8'); ?> + + + +
+ +
+

Personnage introuvable

+

Ce lien public est invalide, désactivé ou n’est plus disponible.

+
+ + +
+ + Avatar de <?php echo htmlspecialchars((string) $character['cl_sccharacter_name'], ENT_QUOTES, 'UTF-8'); ?> + +
+ +
+

+
+ + + + + + + Créé par + équipement(s) +
+

+
+
+ +

Équipement

+ +
+

Ce personnage n’a pas encore d’équipement attribué.

+
+ +
+ $category_items): ?> +
+
+

+ objet(s) +
+
+ + +
+ + Aperçu de <?php echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8'); ?> + +
+ +
+

+
+ + + + +
+ +
+ + + + +
+ + +

+ +
+
+ +
+
+ +
+ + +
+ + 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 + + + + + +
+
+
+

Personnages

+

Crée une fiche personnage, rattache-lui des objets depuis la Base d'Objets et tes Objets personnalisés, puis partage sa page publique uniquement via son lien dédié.

+
+
+
Connecté :
+
Rôle :
+
+
+ + + + +
+ +
+ + +
+ + +
+ + +
+
+ + Avatar de <?php echo htmlspecialchars((string) $selected_character['cl_sccharacter_name'], ENT_QUOTES, 'UTF-8'); ?> + +
+ +
+
+
+

+
+ + + + + + + équipement(s) +
+
+ + + +
+ +

+ +

+ + +
+
+
+ +
+ +
+

Modifier la fiche

+

Bloc repliable pour garder la fiche visible sans laisser le formulaire ouvert en permanence.

+
+ + +
+
+
+ + + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ > + +
+
+
+ +
+ +
+
+ +
+
+ + + + +
+
+
+
+ +
+ +
+

É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) +
+
+ + +
+ + Aperçu de <?php echo htmlspecialchars($item_name, ENT_QUOTES, 'UTF-8'); ?> + +
+ +
+
+

+
+ + + + +
+
+ + +
+ + + : + +
+ + + +
+ + +
+
+ + + + + +
+
+
+
+ +
+
+ +
+ +
+
+ +
> + +
+

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. + + +
+ +
+ + + + + + +
+
+ Ajout en lot +
Coche plusieurs objets, ajuste leurs catégories ou notes si besoin, puis valide tout en une seule fois.
+
+ +
+ + + +
+ + +
+
+ +
+
+
+ + + UUID : + + + stat(s) + +
+
+ +
+
+ + +
+
+
+ + +
+
La recherche reste en place après validation pour que tu puisses continuer sur le même set d’objets.
+ +
+
+ +
+
+ + +
+ 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. +
+ +
+
+ + +