{% extends 'role/member/player/sheet/prophecy/player_form_figure_edit_template.html.twig' %} {% block stylesheets %} {{ parent() }} /* formulaire */ #member_template_layout { flex: 1; display: flex; flex-direction: column; justify-content: flex-start; align-items: center; /*background: linear-gradient(to bottom, #D2B48C, #D2B48C);*/ background-color: #E3D2BF; min-height: unset; margin: 20px; border: solid; } form { border-radius: 20px; padding: 40px 30px; width: 100%; display: flex; flex-direction: column; gap: 20px; //espace entre chaque section } /* est ce utile???? form { background: linear-gradient(to bottom, #94746B, #A78D86); border-radius: 20px; padding: 40px 30px; width: 100%; box-shadow: 8px 8px 12px rgba(105, 94, 75, 1); display: flex; flex-direction: column; gap: 20px; margin-bottom: 20px; border: 1px solid #EBA307; } */ /* Champs de saisie (email, mot de passe) */ .input-wrapper { border-bottom: 1px solid #743B07; padding-bottom: 5px; } input.form-control { background: transparent; border: none; outline: none; color: white; font-size: 14px; width: 100%; padding: 10px 0; } input::placeholder { color: white; opacity: 0.7; } /* Jeton CSRF — non stylé */ .no-style { all: unset; } */ /* fin de formulaire */ .hidden-fields { display: none; } h1 { display: flex; flex-direction: row; justify-content: center; } .tooltip { position: relative; display: inline-block; cursor: pointer; font-weight: 900; } .tooltip .tooltiptext { visibility: hidden; background-color: #1765DA; color: #fff; text-align: left; border-radius: 6px; padding: 8px 12px; position: absolute; z-index: 5; bottom: 125%; /* Position au-dessus de l’élément */ left: 250%; transform: translateX(-50%); opacity: 0; transition: opacity 0.3s ease; min-width: 250px; max-width: 500px; /* Empêche la boîte d’être trop large */ width: max-content; /* S’ajuste au contenu */ white-space: normal; /* Permet les retours à la ligne */ box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.15); font-size: 14px; line-height: 1.4; } .tooltip:hover .tooltiptext { visibility: visible; opacity: 1; } table th { color: red; } #section-campaign { display: flex; flex-direction: row; justify-content: space-between; } #section-campaign p { flex: 1; } #section-status { margin-top: 1em; } #section-status h3 { margin-top: 1.5em; font-size: 1.2em; border-bottom: 2px solid #ccc; padding-bottom: 4px; } #section-status ul { list-style: none; margin: 0; padding: 0; } #section-status li { display: flex; align-items: center; justify-content: flex-start; gap: 12px; /* espace entre les éléments */ border: 1px solid #ddd; border-radius: 6px; padding: 8px 12px; margin-bottom: 6px; background: #fafafa; transition: background 0.2s ease; } #section-status li:hover { background: #f0f0f0; } #section-status label { display: flex; align-items: center; gap: 8px; cursor: pointer; width: 100%; } #section-status input[type="radio"] { transform: scale(1.2); } #section-status strong { font-weight: 600; } #section-status .level { color: #555; font-size: 0.9em; } #section-status .description { color: #666; font-style: italic; font-size: 0.9em; } #section-status .description, #section-status .level, #section-status div { margin-left: 5px; } /* Conteneur */ #section-status { margin-top: 1em; } /* En-têtes de castes (clic accordéon) */ #section-status h3 { cursor: pointer; background: #eee; padding: 10px 14px; border-radius: 6px; margin: 8px 0; user-select: none; position: relative; transition: background 0.3s ease; } /* Flèche indicatrice à droite */ #section-status h3::after { content: "▸"; position: absolute; right: 14px; font-size: 0.9em; transition: transform 0.3s ease; } /* Quand la section est ouverte */ #section-status h3.open { background: #ddd; } #section-status h3.open::after { transform: rotate(90deg); } /* Contenu replié par défaut */ #section-status h3 + ul { display: none; margin: 0 0 10px 0; padding: 0; } /* Quand la section est ouverte */ #section-status h3.open + ul { display: block; } /* Toggle invisible */ .omen-toggle { display: none; } .omen-label { display: block; background: #eee; border-radius: 6px; padding: 10px 14px; margin: 6px 0; cursor: pointer; font-weight: 600; position: relative; transition: background 0.3s ease; } .omen-label::after { content: "▸"; position: absolute; right: 14px; top: 50%; transform: translateY(-50%); transition: transform 0.3s ease; } .omen-details { display: none; background: #f9f9f9; border-left: 3px solid #bbb; margin: -2px 0 8px 0; padding: 10px 14px; border-radius: 0 0 6px 6px; } /* Accordéon ouvert */ .omen-toggle:checked + .omen-label::after { transform: translateY(-50%) rotate(90deg); } .omen-toggle:checked + .omen-label { background: #ddd; } .omen-toggle:checked + .omen-label + .real-radio-omen + .omen-details { display: block; } /* Radio invisible */ .real-radio-omen input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; } /* Toggle invisibles */ .age-toggle { display: none; } /* Label accordéon */ .age-label { display: block; background: #eee; border-radius: 6px; padding: 10px 14px; margin: 6px 0; cursor: pointer; font-weight: 600; position: relative; transition: background 0.3s ease; } /* Petite flèche */ .age-label::after { content: "▸"; position: absolute; right: 14px; top: 50%; transform: translateY(-50%); transition: transform 0.3s ease; } /* Détails cachés */ .age-details { display: none; background: #f9f9f9; border-left: 3px solid #bbb; margin: -2px 0 8px 0; padding: 10px 14px; border-radius: 0 0 6px 6px; } /* Accordéon ouvert */ .age-toggle:checked + .age-label::after { transform: translateY(-50%) rotate(90deg); } .age-toggle:checked + .age-label { background: #ddd; } .age-toggle:checked + .age-label + .real-radio-age + .age-details { display: block; } /* Radio réel invisible */ .real-radio-age input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; } /* Toggle = seulement l'accordéon */ .caste-toggle { display: none; } .caste-label { display: block; background: #eee; border-radius: 6px; padding: 10px 14px; margin: 6px 0; cursor: pointer; font-weight: 600; position: relative; transition: background 0.3s ease; } .caste-label::after { content: "▸"; position: absolute; right: 14px; top: 50%; transform: translateY(-50%); transition: transform 0.3s ease; } .caste-details { display: none; background: #f9f9f9; border-left: 3px solid #bbb; margin: -2px 0 8px 0; padding: 10px 14px; border-radius: 0 0 6px 6px; } /* Accordéon ouvert */ .caste-toggle:checked + .caste-label::after { transform: translateY(-50%) rotate(90deg); } .caste-toggle:checked + .caste-label { background: #ddd; } .caste-toggle:checked + .caste-label + .real-radio + .caste-details { display: block; } /* Radio invisible mais actif */ .real-radio input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; } .nation-radio { display: none; } .nation-label { display: block; background: #eee; border-radius: 6px; padding: 10px 14px; margin: 6px 0; cursor: pointer; font-weight: 600; position: relative; transition: background 0.3s ease; } .nation-label::after { content: "▸"; position: absolute; right: 14px; top: 50%; transform: translateY(-50%); transition: transform 0.3s ease; } .nation-details { display: none; background: #f9f9f9; border-left: 3px solid #bbb; margin: -2px 0 8px 0; padding: 10px 14px; border-radius: 0 0 6px 6px; } /* Highlight + ouverture */ .nation-radio:checked + .nation-label { background: #ddd; } .nation-radio:checked + .nation-label::after { transform: translateY(-50%) rotate(90deg); } .nation-radio:checked + .nation-label + .nation-details { display: block; } .spell-group { margin-bottom: 0.8em; border: 1px solid #ddd; border-radius: 6px; overflow: hidden; background: #fff; } .spell-toggle { display: none; } .spell-label { display: block; background: #e9e9e9; padding: 10px 14px; cursor: pointer; position: relative; font-weight: 600; border-bottom: 1px solid #ccc; user-select: none; transition: background 0.3s ease; } .spell-label::after { content: "▸"; position: absolute; right: 14px; top: 50%; transform: translateY(-50%); font-size: 0.9em; color: #555; transition: transform 0.3s ease; } .spell-label:hover { background: #dcdcdc; } .spells-details { display: none; padding: 10px 14px; background: #f9f9f9; } .spell-toggle:checked + .spell-label::after { transform: translateY(-50%) rotate(90deg); } .spell-toggle:checked + .spell-label + .spells-details { display: block; } /* Conteneur principal */ #section-favours { margin-top: 1em; } /* Bloc contenant les faveurs (catégories) */ #section-favours #prophecy_edit_figure_sheet_form_favours { display: flex; flex-direction: column; margin-top: 8px; } /* On cache les cases principales (catégories) */ #section-favours #prophecy_edit_figure_sheet_form_favours input[type="checkbox"] { display: none; } /* Label = bouton d’option stylisé */ #section-favours #prophecy_edit_figure_sheet_form_favours label { display: block; background: #eee; border-radius: 6px; padding: 10px 14px; margin: 6px 0; cursor: pointer; position: relative; transition: background 0.3s ease, box-shadow 0.3s ease; user-select: none; } /* Flèche indicatrice à droite */ #section-favours #prophecy_edit_figure_sheet_form_favours label::after { content: "▸"; position: absolute; right: 14px; top: 50%; transform: translateY(-50%); font-size: 0.9em; color: #666; transition: transform 0.3s ease; } /* Hover */ #section-favours #prophecy_edit_figure_sheet_form_favours label:hover { background: #ddd; } /* Quand le bouton est sélectionné */ #section-favours #prophecy_edit_figure_sheet_form_favours input[type="checkbox"]:checked + label { background: #ddd; font-weight: 600; } /* Quand sélectionné, la flèche tourne */ #section-favours #prophecy_edit_figure_sheet_form_favours input[type="checkbox"]:checked + label::after { transform: translateY(-50%) rotate(90deg); } /* Contenu caché/visible simulant l’accordéon */ #section-favours #prophecy_edit_figure_sheet_form_favours input[type="checkbox"] + label + .favours-details { display: none; background: #f9f9f9; border-left: 3px solid #bbb; margin: -2px 0 8px 0; padding: 8px 14px; border-radius: 0 0 6px 6px; } /* Quand le label correspondant est sélectionné → afficher le contenu */ #section-favours #prophecy_edit_figure_sheet_form_favours input[type="checkbox"]:checked + label + .favours-details { display: block; } /* ================================ STRUCTURE INTERNE PAR CATÉGORIE ================================ */ .favour-group { margin-bottom: 0.8em; border: 1px solid #ddd; border-radius: 6px; overflow: hidden; background: #fff; } .favour-toggle { display: none; } .favour-label { display: block; background: #e9e9e9; padding: 10px 14px; cursor: pointer; position: relative; font-weight: 600; border-bottom: 1px solid #ccc; user-select: none; transition: background 0.3s ease; } /* Flèche sur le label principal */ .favour-label::after { content: "▸"; position: absolute; right: 14px; top: 50%; transform: translateY(-50%); font-size: 0.9em; color: #555; transition: transform 0.3s ease; } /* Survol */ .favour-label:hover { background: #dcdcdc; } /* Bloc détaillé caché/visible */ .favours-details { display: none; padding: 10px 14px; background: #f9f9f9; } /* Rotation flèche quand ouvert */ .favour-toggle:checked + .favour-label::after { transform: translateY(-50%) rotate(90deg); } /* Afficher le contenu quand coché */ .favour-toggle:checked + .favour-label + .favours-details { display: block; } /* ================================ STYLISATION DU CONTENU INTERNE ================================ */ .favours-details ul { list-style: none; margin: 0; padding: 0; } .favours-details li { margin-bottom: 0.6em; background: #fff; /*border: 1px solid #ddd; border-radius: 4px;*/ padding: 8px 10px; background: #f9f9f9; } .favours-details strong { display: block; color: #333; } .favours-details em { color: #666; font-size: 0.9em; } .favours-details .description { font-size: 0.9em; color: #444; margin-top: 4px; } .favours-details input[type="checkbox"] { margin-right: 6px; transform: translateY(1px); } /* Accordéon */ .caste-toggle { display: none; } .caste-label { display: block; background: #eee; border-radius: 6px; padding: 10px 14px; margin: 6px 0; cursor: pointer; font-weight: 600; position: relative; transition: background 0.3s ease; } .caste-label::after { content: "▸"; position: absolute; right: 14px; top: 50%; transform: translateY(-50%); transition: transform 0.3s ease; } .caste-details { display: none; background: #f9f9f9; border-left: 3px solid #bbb; margin: -2px 0 8px 0; padding: 10px 14px; border-radius: 0 0 6px 6px; } /* Accordéon ouvert */ .caste-toggle:checked + .caste-label::after { transform: translateY(-50%) rotate(90deg); } .caste-toggle:checked + .caste-label { background: #ddd; } .caste-toggle:checked + .caste-label + .caste-details { display: block; } /* Radio invisible */ .real-radio input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; } /* ================================ SECTION DÉSAVANTAGES — ACCORDÉON ================================ */ /* Conteneur principal */ #section-disadvantages { margin-top: 1em; } /* Bloc principal du formulaire */ #section-disadvantages #prophecy_edit_figure_sheet_form_disadvantages { display: flex; flex-direction: column; margin-top: 8px; } /* On cache les checkboxes principales */ #section-disadvantages #prophecy_edit_figure_sheet_form_disadvantages input[type="checkbox"] { display: none; } /* Label = bouton d’accordéon */ #section-disadvantages #prophecy_edit_figure_sheet_form_disadvantages label { display: block; background: #eee; border-radius: 6px; padding: 10px 14px; margin: 6px 0; cursor: pointer; position: relative; transition: background 0.3s ease, box-shadow 0.3s ease; user-select: none; } /* Flèche indicatrice à droite */ #section-disadvantages #prophecy_edit_figure_sheet_form_disadvantages label::after { content: "▸"; position: absolute; right: 14px; top: 50%; transform: translateY(-50%); font-size: 0.9em; color: #666; transition: transform 0.3s ease; } /* Hover */ #section-disadvantages #prophecy_edit_figure_sheet_form_disadvantages label:hover { background: #ddd; } /* Quand le bouton est sélectionné */ #section-disadvantages #prophecy_edit_figure_sheet_form_disadvantages input[type="checkbox"]:checked + label { background: #ddd; font-weight: 600; } /* Quand sélectionné, la flèche tourne */ #section-disadvantages #prophecy_edit_figure_sheet_form_disadvantages input[type="checkbox"]:checked + label::after { transform: translateY(-50%) rotate(90deg); } /* Contenu caché/visible simulant l’accordéon */ #section-disadvantages #prophecy_edit_figure_sheet_form_disadvantages input[type="checkbox"] + label + .disadvantages-details { display: none; background: #f9f9f9; border-left: 3px solid #bbb; margin: -2px 0 8px 0; padding: 8px 14px; border-radius: 0 0 6px 6px; } /* Quand le label correspondant est sélectionné → afficher le contenu */ #section-disadvantages #prophecy_edit_figure_sheet_form_disadvantages input[type="checkbox"]:checked + label + .disadvantages-details { display: block; } /* ================================ STRUCTURE INTERNE PAR CATÉGORIE ================================ */ .disadvantage-group { margin-bottom: 0.8em; border: 1px solid #ddd; border-radius: 6px; overflow: hidden; background: #fff; } .disadvantage-toggle { display: none; } .disadvantage-label { display: block; background: #e9e9e9; padding: 10px 14px; cursor: pointer; position: relative; font-weight: 600; border-bottom: 1px solid #ccc; user-select: none; transition: background 0.3s ease; } /* Flèche sur le label principal */ .disadvantage-label::after { content: "▸"; position: absolute; right: 14px; top: 50%; transform: translateY(-50%); font-size: 0.9em; color: #555; transition: transform 0.3s ease; } /* Survol */ .disadvantage-label:hover { background: #dcdcdc; } /* Bloc détaillé caché/visible */ .disadvantages-details { display: none; padding: 10px 14px; background: #f9f9f9; } /* Rotation flèche quand ouvert */ .disadvantage-toggle:checked + .disadvantage-label::after { transform: translateY(-50%) rotate(90deg); } /* Afficher le contenu quand coché */ .disadvantage-toggle:checked + .disadvantage-label + .disadvantages-details { display: block; } /* ================================ STYLISATION DU CONTENU INTERNE ================================ */ .disadvantages-details ul { list-style: none; margin: 0; padding: 0; } .disadvantages-details li { margin-bottom: 0.6em; background: #fff; border-radius: 4px; padding: 8px 10px; } .disadvantages-details strong { display: block; color: #333; } .disadvantages-details em { color: #666; font-size: 0.9em; } .disadvantages-details .description { font-size: 0.9em; color: #444; margin-top: 4px; } .disadvantages-details input[type="checkbox"] { margin-right: 6px; transform: translateY(1px); } /* ca marche jusqu'ici tester plus bas */ /* ================================ SECTION ARMES — ACCORDÉON CSS ================================ */ /* Conteneur principal */ #section-weapons { margin-top: 1em; } #section-weapons legend { font-weight: bold; font-size: 1.1em; color: #222; margin-bottom: 0.6em; } #weapon-collection-wrapper { display: flex; flex-direction: column; gap: 1em; margin-top: 8px; } /* ------------------------------- GROUPE D'ARMES (catégorie) ------------------------------- */ .weapon-group { border: 1px solid #ddd; border-radius: 6px; background: #fff; overflow: hidden; } /* Checkbox toggle caché */ .weapon-toggle { display: none; } /* Label = en-tête repliable */ .weapon-label { display: block; background: #e9e9e9; padding: 10px 14px; font-weight: 600; border-bottom: 1px solid #ccc; color: #333; cursor: pointer; position: relative; user-select: none; transition: background 0.3s ease; } /* Flèche à droite */ .weapon-label::after { content: "▸"; position: absolute; right: 14px; top: 50%; transform: translateY(-50%); font-size: 0.9em; color: #555; transition: transform 0.3s ease; } /* Hover effet */ .weapon-label:hover { background: #dcdcdc; } /* Flèche rotation quand ouvert */ .weapon-toggle:checked + .weapon-label::after { transform: translateY(-50%) rotate(90deg); } /* ------------------------------- CONTENU DÉPLIABLE (table) ------------------------------- */ .weapons-details { display: none; background: #f9f9f9; padding: 10px 14px; border-top: 1px solid #ccc; } /* Quand le toggle est activé → afficher le contenu */ .weapon-toggle:checked + .weapon-label + .weapons-details { display: block; } /* ------------------------------- TABLE STYLING ------------------------------- */ .weapons-details table { width: 100%; border-collapse: collapse; margin-top: 8px; font-size: 0.95em; } .weapons-details th, .weapons-details td { padding: 6px 8px; border: 1px solid #ddd; vertical-align: top; } .weapons-details thead { background: #eee; font-weight: 600; } .weapons-details tbody tr:nth-child(even) { background: #fafafa; } .weapons-details td em { color: #777; } /* Champs du formulaire */ .weapons-details select, .weapons-details input[type="number"], .weapons-details input[type="text"] { width: 100%; padding: 5px; font-size: 0.9em; border: 1px solid #ccc; border-radius: 4px; background: #fff; transition: border-color 0.3s ease; } .weapons-details select:focus, .weapons-details input:focus { border-color: #999; outline: none; } /* ------------------------------- SECTION LOCK ------------------------------- */ #weapon-collection-wrapper .lock { display: block; margin-top: 10px; text-align: right; font-size: 0.85em; color: #777; } /* ================================ SECTION ARMURES — ACCORDÉON CSS ================================ */ /* Conteneur principal */ #section-armors { margin-top: 1em; } #section-armors legend { font-weight: bold; font-size: 1.1em; color: #222; margin-bottom: 0.6em; } #armor-collection-wrapper { display: flex; flex-direction: column; gap: 1em; margin-top: 8px; } /* ------------------------------- GROUPE D’ARMURES (catégorie) ------------------------------- */ .armor-group { border: 1px solid #ddd; border-radius: 6px; background: #fff; overflow: hidden; } /* Toggle caché */ .armor-toggle { display: none; } /* Label = en-tête repliable */ .armor-label { display: block; background: #e9e9e9; padding: 10px 14px; font-weight: 600; border-bottom: 1px solid #ccc; color: #333; cursor: pointer; position: relative; user-select: none; transition: background 0.3s ease; } /* Flèche à droite */ .armor-label::after { content: "▸"; position: absolute; right: 14px; top: 50%; transform: translateY(-50%); font-size: 0.9em; color: #555; transition: transform 0.3s ease; } /* Hover effet */ .armor-label:hover { background: #dcdcdc; } /* Flèche rotation quand ouvert */ .armor-toggle:checked + .armor-label::after { transform: translateY(-50%) rotate(90deg); } /* ------------------------------- CONTENU DÉPLIABLE ------------------------------- */ .armors-details { display: none; background: #f9f9f9; padding: 10px 14px; border-top: 1px solid #ccc; } /* Quand le toggle est activé → afficher le contenu */ .armor-toggle:checked + .armor-label + .armors-details { display: block; } /* ------------------------------- TABLE STYLING ------------------------------- */ .armors-details table { width: 100%; border-collapse: collapse; margin-top: 8px; font-size: 0.95em; } .armors-details th, .armors-details td { padding: 6px 8px; border: 1px solid #ddd; vertical-align: top; } .armors-details thead { background: #eee; font-weight: 600; } .armors-details tbody tr:nth-child(even) { background: #fafafa; } .armors-details td em { color: #777; } /* Champs du formulaire */ .armors-details select, .armors-details input[type="number"], .armors-details input[type="text"] { width: 100%; padding: 5px; font-size: 0.9em; border: 1px solid #ccc; border-radius: 4px; background: #fff; transition: border-color 0.3s ease; } .armors-details select:focus, .armors-details input:focus { border-color: #999; outline: none; } /* ------------------------------- SECTION LOCK ------------------------------- */ #armor-collection-wrapper .lock { display: block; margin-top: 10px; text-align: right; font-size: 0.85em; color: #777; } /* ================================ SECTION BOUCLIERS — ACCORDÉON CSS ================================ */ /* Conteneur principal */ #section-shields { margin-top: 1em; } #section-shields legend { font-weight: bold; font-size: 1.1em; color: #222; margin-bottom: 0.6em; } #shield-collection-wrapper { display: flex; flex-direction: column; gap: 1em; margin-top: 8px; } /* ------------------------------- GROUPE DE BOUCLIERS (catégorie) ------------------------------- */ .shield-group { border: 1px solid #ddd; border-radius: 6px; background: #fff; overflow: hidden; } /* Toggle caché */ .shield-toggle { display: none; } /* Label = en-tête repliable */ .shield-label { display: block; background: #e9e9e9; padding: 10px 14px; font-weight: 600; border-bottom: 1px solid #ccc; color: #333; cursor: pointer; position: relative; user-select: none; transition: background 0.3s ease; } /* Flèche à droite */ .shield-label::after { content: "▸"; position: absolute; right: 14px; top: 50%; transform: translateY(-50%); font-size: 0.9em; color: #555; transition: transform 0.3s ease; } /* Hover effet */ .shield-label:hover { background: #dcdcdc; } /* Flèche rotation quand ouvert */ .shield-toggle:checked + .shield-label::after { transform: translateY(-50%) rotate(90deg); } /* ------------------------------- CONTENU DÉPLIABLE ------------------------------- */ .shields-details { display: none; background: #f9f9f9; padding: 10px 14px; border-top: 1px solid #ccc; } /* Quand le toggle est activé → afficher le contenu */ .shield-toggle:checked + .shield-label + .shields-details { display: block; } /* ------------------------------- TABLE STYLING ------------------------------- */ .shields-details table { width: 100%; border-collapse: collapse; margin-top: 8px; font-size: 0.95em; } .shields-details th, .shields-details td { padding: 6px 8px; border: 1px solid #ddd; vertical-align: top; } .shields-details thead { background: #eee; font-weight: 600; } .shields-details tbody tr:nth-child(even) { background: #fafafa; } .shields-details td em { color: #777; } /* Champs du formulaire */ .shields-details select, .shields-details input[type="number"], .shields-details input[type="text"] { width: 100%; padding: 5px; font-size: 0.9em; border: 1px solid #ccc; border-radius: 4px; background: #fff; transition: border-color 0.3s ease; } .shields-details select:focus, .shields-details input:focus { border-color: #999; outline: none; } /* ------------------------------- SECTION LOCK ------------------------------- */ #shield-collection-wrapper .lock { display: block; margin-top: 10px; text-align: right; font-size: 0.85em; color: #777; } /* ================================ SECTION OBJETS DIVERS — ACCORDÉON CSS ================================ */ #section-items { margin-top: 1em; } #section-items legend { font-weight: bold; font-size: 1.1em; color: #222; margin-bottom: 0.6em; } #item-collection-wrapper { display: flex; flex-direction: column; gap: 1em; } /* ------------------------------- GROUPE D’OBJETS (catégorie) ------------------------------- */ .item-group { border: 1px solid #ddd; border-radius: 6px; background: #fff; overflow: hidden; } .item-toggle { display: none; } .item-label { display: block; background: #e9e9e9; padding: 10px 14px; font-weight: 600; border-bottom: 1px solid #ccc; color: #333; cursor: pointer; position: relative; user-select: none; transition: background 0.3s ease; } .item-label::after { content: "▸"; position: absolute; right: 14px; top: 50%; transform: translateY(-50%); font-size: 0.9em; color: #555; transition: transform 0.3s ease; } .item-label:hover { background: #dcdcdc; } .item-toggle:checked + .item-label::after { transform: translateY(-50%) rotate(90deg); } .items-details { display: none; background: #f9f9f9; padding: 10px 14px; border-top: 1px solid #ccc; } .item-toggle:checked + .item-label + .items-details { display: block; } .items-details table { width: 100%; border-collapse: collapse; margin-top: 8px; font-size: 0.95em; } .items-details th, .items-details td { padding: 6px 8px; border: 1px solid #ddd; vertical-align: top; } .items-details thead { background: #eee; font-weight: 600; } .items-details tbody tr:nth-child(even) { background: #fafafa; } .items-details select, .items-details input[type="number"], .items-details input[type="text"] { width: 100%; padding: 5px; font-size: 0.9em; border: 1px solid #ccc; border-radius: 4px; background: #fff; } .items-details select:focus, .items-details input:focus { border-color: #999; outline: none; } #item-collection-wrapper .lock { display: block; margin-top: 10px; text-align: right; font-size: 0.85em; color: #777; } #startXP, #startXPSpent { visibility: hidden; } {% endblock %} {% block breadcrumbs %} {{ parent() }} > creation de personnage {% endblock %} {% block member_navigation %} {{ parent() }} {% endblock %} {% block title_page %} {{ parent() }} {% endblock %} {% block member_template_layout %} {# #} {% set startXP = figureSheet.xperience %}

{{ startXP }}

0

{{ form_start(form) }} {# ==================== FORM DISPLAYED BY SECTIONS ===================== SECTIONS DISPLAYED : - section-description - section-poster - section-description - section-background - section-omen - section-age - section-caste - section-status (utile???) - section-bans - section-caracteristics - section-majorAttributes - section-minorAttributes - section nation - section-disadvantages - section-advantages - section-reputation - section-skills - section-favours - section-disciplines - section-spheres - section-spells - section-currencies - section-weapons - section-armors - section-shields ==================================================================== #}
CAMPAGNE

{{ campaign.name }} pour {{ campaign.game.name }}

campagne dirigée par {{ campaign.owner }}

PORTRAIT
{{ form_row(form.poster) }}{% if figureSheet.poster is not null %}poster{% else %}poster {% endif %}
{# DESCRIPTION PHYSIQUE #} DESCRIPTION PHYSIQUE

Définissez quelques traits physiques qui décrivent votre personnage.

Vous pouvez revenir sur les différents champs à tout moment.

{{ form_row(form.name) }} {{ form_row(form.nickname) }} {{ form_row(form.weight) }} {{ form_row(form.size) }} {{ form_row(form.hairs) }} {{ form_row(form.eyes) }}
{# HISTORIQUE #} HISTORIQUE Rédigez un historique du vécu de votre personnage, en accord avec les critères du maître du jeu et ou la description de la campagne. {{ form_row(form.background) }}
EXPERIENCE
{{ form_row(form.xperience, {'attr': {'class': 'startXPRemaining'}}) }}
AUGURE DE NAISSANCE À la façon des signes du zodiaque, tous les personnages de Prophecy…
{% for key, choice in form.omen.vars.choices %} {% set omen = choice.data %} {% set radio = form.omen[key] %} {% set isChecked = radio.vars.checked %}
{{ form_widget(radio) }}
{% if omen.description %}
{{ omen.description|raw }}
{% endif %} {% if omen.personnality %}
Personnalité : {{ omen.personnality }}
{% endif %} {% if omen.motivation %}
Motivation : {{ omen.motivation }}
{% endif %} {% if omen.quality %}
Qualité : {{ omen.quality }}
{% endif %} {% if omen.fault %}
Défaut : {{ omen.fault }}
{% endif %}
{% endfor %}
AGE L'âge joue un rôle majeur dans le processus de création d'un personnage...
{% for key, choice in form.age.vars.choices %} {% set age = choice.data %} {% set radio = form.age[key] %} {% set isChecked = radio.vars.checked %}
{{ form_widget(radio) }}
{% if age.description %}
{{ age.description|raw }}
{% endif %} {% if age.minimumYears %}
Age minimal : {{ age.minimumYears }}
{% endif %} {% if age.maximumYears %}
Age maximal : {{ age.maximumYears }}
{% endif %}
{% endfor %}
CASTE Le système propose huit castes très différentes…
{% for choice in form.caste.vars.choices %} {% set caste = choice.data %} {% set radio = form.caste[choice.value] %} {% set isChecked = radio.vars.checked %}
{{ form_widget(radio) }}
{% if caste.description %}
{{ caste.description|raw }}
{% endif %} {% if caste.bans is defined and caste.bans|length > 0 %}
Interdits de caste :
{{ caste.bans|map(b => b.name)|join(', ') }}
{% endif %}
{% endfor %}
STATUT DE CASTE {# On va rendre manuellement les boutons radio, mais une seule fois via form_widget(form.status) #} {% set statusesByCaste = {} %} {% for choice in form.status.vars.choices %} {% set status = choice.data %} {% set casteName = status.caste ? status.caste.name : 'Autres' %} {% if statusesByCaste[casteName] is not defined %} {% set statusesByCaste = statusesByCaste|merge({ (casteName): [] }) %} {% endif %} {% set statusesByCaste = statusesByCaste|merge({ (casteName): statusesByCaste[casteName] | merge([{ 'value': choice.value, 'label': choice.label, 'status': status }]) }) %} {% endfor %} {% for casteName, items in statusesByCaste %}

{{ casteName }}

{% endfor %} {# Marquer le champ comme rendu pour éviter erreur #} {% do form.status.setRendered %} {# #}
INTERDITS DE CASTE {% set bansByCaste = {} %} {% for key, choice in form.ban.vars.choices %} {% set ban = choice.data %} {% set radio = form.ban[key] %} {% set casteName = ban.caste ? ban.caste.name : 'Autres' %} {% if bansByCaste[casteName] is not defined %} {% set bansByCaste = bansByCaste|merge({ (casteName): [] }) %} {% endif %} {% set bansByCaste = bansByCaste|merge({ (casteName): bansByCaste[casteName] | merge([{ 'radio': radio, 'ban': ban }]) }) %} {% endfor %} {% for casteName, bans in bansByCaste %}
    {% for item in bans %}
  • {{ form_widget(item.radio) }} {{ item.ban.name }} {% if item.ban.description %}
    {{ item.ban.description|raw }}
    {% endif %}
  • {% endfor %}
{% endfor %}
CARACTERISTIQUES Le système propose huit castes très différentes les unes des autres. Au sein de Prophecy, il n'est pas possible de changer de caste, une fois celle-ci choisie, le personnage en suivra la philosophie et les voies proposées. Chaque caste donne accès à des compétences, des capacités, des privilèges en grande partie uniques. Rapprochez-vous du maître de jeu pour vous assurer que le choix effectué est compatible avec ce qu'il propose. Pour un descriptif plus détaillé, veuillez suivre le lien proposé. {{ form_row(form.caracteristics) }} {# #} {% for value in caracteristicsStart %} {# #} {% endfor %} {# {{ form.caracteristics.vars.data[0].caracteristic.minimumValue }} #}
ATTRIBUTS MAJEURS {{ form_row(form.majorAttributes) }} {% for value in majorAttributesStart %} {# #} {% endfor %}
ATTRIBUTS MINEURS {{ form_widget(form.minorAttributes, { attr: {'class': 'minor-field'} }) }}

points libres à répartir : {{ free_minorAttributes }}

{{ form_widget(form.freeMinorPoints) }} {% for value in free_minorAttributes %} {# #} {% endfor %}
NATION Le système propose plusieurs nations d'origine d'un personnage
{% for choice in form.nation.vars.choices %} {% set nation = choice.data %} {% set radio = form.nation[choice.value] %} {% set id = 'nation-radio-' ~ loop.index %}
{% if nation.description %}
{{ nation.description|raw }}
{% endif %}
{% endfor %}
TENDANCES {{ form_row(form.tendencies) }}

Points libres à répartir : {{ free_tendencies }}

{% for value in free_tendencies %} {# #} {% endfor %}
DÉSAVANTAGES
Points gagnés : 0
{# Regrouper les désavantages par catégorie #} {% set disadvantagesByCategory = {} %} {% for key, choice in form.disadvantages.vars.choices %} {% set disadvantage = choice.data %} {% set checkbox = form.disadvantages[key] %} {% set categoryName = disadvantage.disadvantageCategory ? disadvantage.disadvantageCategory.name : 'Autres' %} {% if disadvantagesByCategory[categoryName] is not defined %} {% set disadvantagesByCategory = disadvantagesByCategory|merge({ (categoryName): [] }) %} {% endif %} {% set disadvantagesByCategory = disadvantagesByCategory|merge({ (categoryName): disadvantagesByCategory[categoryName] | merge([{ 'checkbox': checkbox, 'disadvantage': disadvantage }]) }) %} {% endfor %} {# Afficher par catégorie #} {% for categoryName, disadvantages in disadvantagesByCategory %}
    {% for item in disadvantages %}
  • {{ form_widget(item.checkbox, { attr: { 'class': 'disadvantage-checkbox', 'data-cost': item.disadvantage.cost } }) }} {{ item.disadvantage.name }} {% if item.disadvantage.cost is not null %} (coût : {{ item.disadvantage.cost }}) {% endif %} {% if item.disadvantage.description %}
    {{ item.disadvantage.description|raw }}
    {% endif %}
  • {% endfor %}
{% endfor %} {# #}
AVANTAGES
Points restants : 0
{# Regrouper les avantages par catégorie #} {% set advantagesByCategory = {} %} {% for key, choice in form.advantages.vars.choices %} {% set advantage = choice.data %} {% set checkbox = form.advantages[key] %} {% set categoryName = advantage.advantageCategory ? advantage.advantageCategory.name : 'Autres' %} {% if advantagesByCategory[categoryName] is not defined %} {% set advantagesByCategory = advantagesByCategory|merge({ (categoryName): [] }) %} {% endif %} {% set advantagesByCategory = advantagesByCategory|merge({ (categoryName): advantagesByCategory[categoryName] | merge([{ 'checkbox': checkbox, 'advantage': advantage }]) }) %} {% endfor %} {# Afficher par catégorie #} {% for categoryName, advantages in advantagesByCategory %}

{{ categoryName }}

{% endfor %} {# #}
REPUTATION XP restant:
{{ startXP }}

Coût :

{{ form_row(form.fame) }}
COMPÉTENCES XP restant:
{{ startXP }}
{% set groupedSkills = {} %} {% for skillForm in form.skills %} {% set selectedSkill = skillForm.skill.vars.data %} {% if selectedSkill %} {% set category = selectedSkill.Skillcategory.name %} {% else %} {% set category = 'Sans catégorie' %} {% endif %} {% if groupedSkills[category] is not defined %} {% set groupedSkills = groupedSkills|merge({ (category): [skillForm] }) %} {% else %} {% set groupedSkills = groupedSkills|merge({ (category): groupedSkills[category]|merge([skillForm]) }) %} {% endif %} {% endfor %} {% for category, skills in groupedSkills %}

Catégorie : {{ category }}

{% for skillForm in skills %} {% set selectedSkill = skillForm.skill.vars.data %} {% endfor %}
Compétence Coût
{{ selectedSkill.name }} {{ form_widget(skillForm.value, { attr: { class: 'skill-value-input' } }) }} {% if selectedSkill is defined and selectedSkill.cost is defined and selectedSkill.cost is not null %} {{ selectedSkill.cost }} {% else %} {% endif %}
{% endfor %}
FAVEURS XP restant:
{{ startXP }}
{# Regrouper les faveurs par caste #} {% set favoursByCaste = {} %} {% for key, choice in form.favours.vars.choices %} {% set favour = choice.data %} {% set checkbox = form.favours[key] %} {% set casteName = favour.caste ? favour.caste.name : 'Autres' %} {% if favoursByCaste[casteName] is not defined %} {% set favoursByCaste = favoursByCaste|merge({ (casteName): [] }) %} {% endif %} {% set favoursByCaste = favoursByCaste|merge({ (casteName): favoursByCaste[casteName] | merge([{ 'checkbox': checkbox, 'favour': favour }]) }) %} {% endfor %} {% for casteName, favours in favoursByCaste %} {# --- Début accordéon --- #}
    {% for item in favours %}
  • {{ form_widget(item.checkbox, { attr: { 'data-cost': item.favour.xpCost ?? 0 } }) }} {{ item.favour.name }} {% if item.favour.xpCost is not null %} (coût : {{ item.favour.xpCost }}) {% endif %} {% if item.favour.description %}
    {{ item.favour.description|raw }}
    {% endif %}
  • {% endfor %}
{% endfor %} {# #}
DISCIPLINES XP restant:
{{ startXP }}
{% for discipline in form.disciplines %} {% if discipline.vars.prototype is not defined %} {% endif %} {% endfor %}
Discipline Niveau Coût
{{ discipline.vars.label }} {{ form_widget(discipline, { attr: { class: 'discipline-input' } }) }} 1
SPHERES XP restant:
{{ startXP }}
{% for sphere in form.spheres %} {% if sphere.vars.prototype is not defined %} {% endif %} {% endfor %}
Sphere Niveau Coût
{{ sphere.vars.label }} {{ form_widget(sphere, { attr: { class: 'sphere-input' } }) }} 1
SORTS {# Regrouper les sorts par sphere #} {% set spellsBySphere = {} %} {% for key, choice in form.spells.vars.choices %} {% set spell = choice.data %} {% set checkbox = form.spells[key] %} {% set sphereName = spell.sphere ? spell.sphere.name : 'Autres' %} {% if spellsBySphere[sphereName] is not defined %} {% set spellsBySphere = spellsBySphere|merge({ (sphereName): [] }) %} {% endif %} {% set spellsBySphere = spellsBySphere|merge({ (sphereName): spellsBySphere[sphereName] | merge([{ 'checkbox': checkbox, 'spell': spell }]) }) %} {% endfor %} {# Afficher par sphere #} {% for sphereName, spells in spellsBySphere %}
    {% for item in spells %}
  • {{ form_widget(item.checkbox) }} {{ item.spell.name }} {% if item.spell.manaCost is not null %} (coût : {{ item.spell.manaCost }}) {% endif %} {% if item.spell.description %}
    {{ item.spell.description|raw }}
    {% endif %}
  • {% endfor %}
{% endfor %} {# #}
ARGENT {{ form_row(form.currencies) }}

PROBLEME CONSTATE AVEC ARMES ARMURES BOUCLIERS : CONFUSION ENTRE SHOP ET EQUIPEMENT DETENU PAR LE PERSONNAGE. L AJOUT FONCTIONNE BIEN SUR FIGURESHEET MAIS LE RENDU N EST PAS BON CAR ON AFFICHE LE SHOP ET PAS LE POSSEDE

ARMES
{% set groupedWeapons = {} %} {# Étape 1 : Grouper les armes par catégorie #} {% for weaponForm in form.weapons %} {% set selectedWeapon = weaponForm.weapon.vars.data %} {% set category = selectedWeapon and selectedWeapon.weaponCategory ? selectedWeapon.weaponCategory.name : 'Sans catégorie' %} {% if groupedWeapons[category] is not defined %} {% set groupedWeapons = groupedWeapons|merge({ (category): [weaponForm] }) %} {% else %} {% set groupedWeapons = groupedWeapons|merge({ (category): groupedWeapons[category]|merge([weaponForm]) }) %} {% endif %} {% endfor %} {# Étape 2 : Afficher les groupes sous forme d’accordéons CSS #} {% for category, weapons in groupedWeapons %}
{% for weaponForm in weapons %} {% set selectedWeapon = weaponForm.weapon.vars.data %} {% if selectedWeapon %} {% else %} {% endif %} {% endfor %}
arme quantités prix dégâts initiative de mêlée initiative de contact portée effective portée maximale spécial description
{{ form_row(weaponForm.weapon) }} {{ form_row(weaponForm.quantity) }}{{ selectedWeapon.cityPrice }} {{ selectedWeapon.damages }} {{ selectedWeapon.meleeInitiative }} {{ selectedWeapon.contactInitiative }} {{ selectedWeapon.effectiveRange }} {{ selectedWeapon.maximumRange }} {{ selectedWeapon.special }} {{ selectedWeapon.description|raw }}Aucune arme sélectionnée
{% endfor %}
ARMURES
{% set groupedArmors = {} %} {# Grouper les armures par catégorie #} {% for armorForm in form.armors %} {% set selectedArmor = armorForm.armor.vars.data %} {% set category = selectedArmor and selectedArmor.Armorcategory ? selectedArmor.Armorcategory.name : 'Sans catégorie' %} {% if groupedArmors[category] is not defined %} {% set groupedArmors = groupedArmors|merge({ (category): [armorForm] }) %} {% else %} {% set groupedArmors = groupedArmors|merge({ (category): groupedArmors[category]|merge([armorForm]) }) %} {% endif %} {% endfor %} {# Affichage des groupes sous forme d’accordéon CSS #} {% for category, armors in groupedArmors %}
{% for armorForm in armors %} {% set armor = armorForm.armor.vars.data %} {% if armor %} {% else %} {% endif %} {% endfor %}
Armure Quantité Prix Protection Pénalité de mouvement Spécial Description
{{ form_widget(armorForm.armor, {'attr': {'disabled': 'disabled'}}) }} {{ form_widget(armorForm.quantity) }}{{ armor.cityPrice }} {{ armor.protection }} {{ armor.penaltyMovement }} {{ armor.special }} {{ armor.description|raw }}Aucune armure sélectionnée
{% endfor %}
BOUCLIERS
{% set groupedShields = {} %} {# Grouper tous les boucliers sous une seule catégorie (si besoin) #} {% for shieldForm in form.shields %} {% set selectedShield = shieldForm.shield.vars.data %} {% set category = 'Boucliers' %} {% if groupedShields[category] is not defined %} {% set groupedShields = groupedShields|merge({ (category): [shieldForm] }) %} {% else %} {% set groupedShields = groupedShields|merge({ (category): groupedShields[category]|merge([shieldForm]) }) %} {% endif %} {% endfor %} {# Affichage sous forme d’accordéon CSS #} {% for category, shields in groupedShields %}
{% for shieldForm in shields %} {% set shield = shieldForm.shield.vars.data %} {% if shield %} {% else %} {% endif %} {% endfor %}
Bouclier Quantité Prix Protection Pénalité de mouvement Spécial Description
{{ form_widget(shieldForm.shield, {'attr': {'disabled': 'disabled'}}) }} {{ form_widget(shieldForm.quantity) }}{{ shield.cityPrice }} {{ shield.protection }} {{ shield.penaltyMovement }} {{ shield.special }} {{ shield.description|raw }}Aucun bouclier sélectionné
{% endfor %}
OBJETS DIVERS
{% set groupedItems = {} %} {# Grouper par catégorie #} {% for itemForm in form.items %} {% set selectedItem = itemForm.item.vars.data %} {% set category = selectedItem ? selectedItem.Itemcategory.name : 'Sans catégorie' %} {% if groupedItems[category] is not defined %} {% set groupedItems = groupedItems|merge({ (category): [itemForm] }) %} {% else %} {% set groupedItems = groupedItems|merge({ (category): groupedItems[category]|merge([itemForm]) }) %} {% endif %} {% endfor %} {# Affichage accordéon CSS #} {% for category, items in groupedItems %}
{% for itemForm in items %} {% set item = itemForm.item.vars.data %} {% endfor %}
Objet Quantité Prix
{{ form_widget(itemForm.item, {'attr': {'disabled': 'disabled'}}) }} {{ form_widget(itemForm.quantity) }} {{ item ? item.cityPrice : '' }}
{% endfor %}

Total de points dépensés : 0

{{ form_row(form.submit) }}
{# {{ form_rest(form, { 'status': false }) }} #}
{{ form_row(form._token) }} {# inclut explicitement le CSRF #} {{ form_end(form, { render_rest: false }) }} {% endblock %} {% block javascript %} {{ parent() }} /* JS sheet xp manage */ /** * Function to decrease a value reputation, skill, * discipline, sphere, minor or major attribute, * * @param {number} value value of decrease * @param {number} currentValue value to decrease * @param {number} minValue minimum value * @returns {number} new value or -1 */ function decreaseXP (value, currentValue, minValue) { let currentXP = -1; if(currentValue - value >= 0 && currentValue - value >= minValue) { currentXP = currentValue - value; } return currentXP; } /** * Function to decrease a value reputation, skill, * discipline, sphere, minor or major attribute, * * @param {number} value value of increase * @param {number} currentValue value to increase * @param {number} maxValue maximum value * @returns {number} new value or -1 */ function increaseXP (value, currentValue, maxValue) { let currentXP = -1; if(currentValue + value >= 0 && currentValue + value <= maxValue) { currentXP = currentValue + value; } return currentXP; } /** * Function to increase total xp spent * * @param {number} currentValue value of current xp * @param {number} value value to increase xp * @returns {number} total */ function totalXPSpent (currentValue, value) { let total = currentValue + value; return total; } /** * Function to calculate xp remaining * * @param {number} currentValue value of current xp * @param {number} value value to decrease currentValue * @returns {number} remainingXP */ function xpRemaining (currentValue, value) { let remainingXP = currentValue - value; return remainingXP; } /** * Function to read a value from a DOM element * * @param {number} id element * @returns {number} readed value */ function readValue(element) { let value = parseInt(document.getElementById(element).innerHTML); return value; } /** * Function to write a value in a DOM element * * @param {number} id element where to write value * @param {number} value to write * @returns {number} null */ function writeValue(element, value) { const elementDOM = document.getElementById(element); elementDOM.textContent = value; } /* declenchement d'une action a chaque clic */ document.addEventListener("DOMContentLoaded", () => { // —> input de la Réputation document.querySelectorAll('#section-reputation input[type="number"]') .forEach(input => input.addEventListener('input', handleReputationChange) ); // —> input des skills document.querySelectorAll('.skill-entry .skill-value input') .forEach(input => input.addEventListener('input', (event) => { handleSkillChange(event); // otherFunction(); // ... }) ); // —> input des disciplines document.querySelectorAll('.discipline-entry input[type="number"]') .forEach(input => input.addEventListener('input', (event) => { handleDisciplineChange(event); // otherFunction(); // ... }) ); // —> input des sphères document.querySelectorAll('.sphere-entry input[type="number"]') .forEach(input => input.addEventListener('input', (event) => { handleSphereChange(event); // otherFunction(); // ... }) ); }); function handleSkillChange(event) { const input = event.target; const row = input.closest('.skill-entry'); let previousValue = input.dataset.prevValue !== undefined ? Number(input.dataset.prevValue) : 0; let currentValue = Number(input.value); if (currentValue < 0) currentValue = 0; input.value = currentValue; let direction = 0; if (currentValue > previousValue) direction = 1; else if (currentValue < previousValue) direction = -1; let previousCost = previousValue + 1; const startXPValue = parseInt(document.getElementById('startXP').textContent); let currentXPSpentValue = parseInt(document.getElementById('startXPSpent').textContent); const xpRemaining = startXPValue - currentXPSpentValue; // Blocage si augmentation impossible if (direction === 1 && xpRemaining < previousCost) { input.value = previousValue; return; } // Ajustement XP dépensé if (direction === 1) { currentXPSpentValue += previousCost; } else if (direction === -1) { currentXPSpentValue -= previousCost - 1; if (currentXPSpentValue < 0) currentXPSpentValue = 0; } document.getElementById('startXPSpent').textContent = currentXPSpentValue; //document.getElementById('startXPRemaining').textContent = startXPValue - currentXPSpentValue; document.querySelectorAll('.startXPRemaining') .forEach(el => el.textContent = startXPValue - currentXPSpentValue); //mise a jour du champ xperience document.querySelector('input.startXPRemaining').value = startXPValue - currentXPSpentValue; // Mise à jour du coût affiché const costCell = row.querySelector('.skill-cost'); if (costCell) { costCell.textContent = currentValue + 1; } // Sauvegarde pour la prochaine fois input.dataset.prevValue = currentValue; } // Attacher le listener à chaque input number dans les Skills document.querySelectorAll('.skill-entry input[type="number"]').forEach(input => { input.addEventListener('input', handleSkillChange); input.dataset.prevValue = Number(input.value) || 0; // initialiser prevValue }); function handleDisciplineChange(event) { const input = event.target; const row = input.closest('.discipline-entry'); let previousValue = input.dataset.prevValue !== undefined ? Number(input.dataset.prevValue) : 0; let currentValue = Number(input.value); if (currentValue < 0) currentValue = 0; input.value = currentValue; let direction = 0; if (currentValue > previousValue) direction = 1; else if (currentValue < previousValue) direction = -1; let previousCost = previousValue + 1; let startXPValue = parseInt(document.getElementById('startXP').textContent); let currentXPSpentValue = parseInt(document.getElementById('startXPSpent').textContent); // Blocage si augmentation impossible if (direction === 1 && (startXPValue - currentXPSpentValue) < previousCost) { input.value = previousValue; return; } // Ajustement XP dépensé if (direction === 1) { currentXPSpentValue += previousCost; } else if (direction === -1) { currentXPSpentValue -= previousCost - 1; if (currentXPSpentValue < 0) currentXPSpentValue = 0; } document.getElementById('startXPSpent').textContent = currentXPSpentValue; //document.getElementById('startXPRemaining').textContent = startXPValue - currentXPSpentValue; document.querySelectorAll('.startXPRemaining') .forEach(el => el.textContent = startXPValue - currentXPSpentValue); //mise a jour du champ xperience document.querySelector('input.startXPRemaining').value = startXPValue - currentXPSpentValue; // Mise à jour du coût affiché const costCell = row.querySelector('.discipline-cost'); if (costCell) { costCell.textContent = currentValue + 1; } input.dataset.prevValue = currentValue; } document.querySelectorAll('.discipline-entry input[type="number"]').forEach(input => { input.addEventListener('input', handleDisciplineChange); }); function handleSphereChange(event) { const input = event.target; const row = input.closest('.sphere-entry'); let previousValue = input.dataset.prevValue !== undefined ? Number(input.dataset.prevValue) : 0; let currentValue = Number(input.value); if (currentValue < 0) currentValue = 0; input.value = currentValue; let direction = 0; if (currentValue > previousValue) direction = 1; else if (currentValue < previousValue) direction = -1; let previousCost = previousValue + 1; let startXPValue = parseInt(document.getElementById('startXP').textContent); let currentXPSpentValue = parseInt(document.getElementById('startXPSpent').textContent); // Blocage si augmentation impossible if (direction === 1 && (startXPValue - currentXPSpentValue) < previousCost) { input.value = previousValue; return; } // Ajustement XP dépensé if (direction === 1) { currentXPSpentValue += previousCost; } else if (direction === -1) { currentXPSpentValue -= previousCost - 1; if (currentXPSpentValue < 0) currentXPSpentValue = 0; } document.getElementById('startXPSpent').textContent = currentXPSpentValue; //document.getElementById('startXPRemaining').textContent = startXPValue - currentXPSpentValue; document.querySelectorAll('.startXPRemaining') .forEach(el => el.textContent = startXPValue - currentXPSpentValue); //mise a jour du champ xperience document.querySelector('input.startXPRemaining').value = startXPValue - currentXPSpentValue; // Mise à jour du coût affiché const costCell = row.querySelector('.sphere-cost'); if (costCell) { costCell.textContent = currentValue + 1; } input.dataset.prevValue = currentValue; } document.querySelectorAll('.sphere-entry input[type="number"]').forEach(input => { input.addEventListener('input', handleSphereChange); }); function handleReputationChange(event) { const input = event.target; let prevValue = input.dataset.prevValue !== undefined ? Number(input.dataset.prevValue) : 0; let currentValue = Number(input.value); if (currentValue < 0) currentValue = 0; input.value = currentValue; let direction = 0; if (currentValue > prevValue) direction = 1; else if (currentValue < prevValue) direction = -1; // Coût d’augmentation : valeur précédente + 1 let previousCost = prevValue + 1; let startXPValue = parseInt(document.getElementById('startXP').textContent); let currentXPSpentValue = parseInt(document.getElementById('startXPSpent').textContent); // — Blocage si l’augmentation dépasse l’XP disponible if (direction === 1 && (startXPValue - currentXPSpentValue) < previousCost) { input.value = prevValue; return; } // — Mise à jour XP dépensé if (direction === 1) { currentXPSpentValue += previousCost; } else if (direction === -1) { currentXPSpentValue -= previousCost - 1; if (currentXPSpentValue < 0) currentXPSpentValue = 0; } document.getElementById('startXPSpent').textContent = currentXPSpentValue; // Mise à jour de tous les affichages d’XP restant document.querySelectorAll('.startXPRemaining') .forEach(el => el.textContent = startXPValue - currentXPSpentValue); // Mise à jour du coût affiché document.getElementById('fame_cost').textContent = currentValue + 1; //mise a jour du champ xperience document.querySelector('input.startXPRemaining').value = startXPValue - currentXPSpentValue; input.dataset.prevValue = currentValue; } /* REPUTATION document.addEventListener("DOMContentLoaded", function () { const input = document.querySelector("#section-reputation input[type='number']"); if (!input) return; const fameCostLabel = document.getElementById("fame_cost"); const xpRemainingEls = document.querySelectorAll(".xp_remaining"); const xpSpentEl = document.getElementById("xp_spent"); let previousValue = Number(input.dataset.prevValue) || 0; // On récupère l'XP initial depuis le premier élément let baseXP = Number(xpRemainingEls[0].textContent); function updateReputation(event) { let currentValue = Number(input.value); if (currentValue < 0) currentValue = 0; input.value = currentValue; // direction : 1 = montée, -1 = descente let direction = currentValue > previousValue ? 1 : (currentValue < previousValue ? -1 : 0); // coût d'augmentation = ancien niveau + 1 let previousCost = previousValue + 1; let xpSpent = Number(xpSpentEl.textContent); let xpRemaining = baseXP - xpSpent; // ⛔ Pas assez d’XP pour monter ? if (direction === 1 && xpRemaining < previousCost) { input.value = previousValue; return; } // Mise à jour XP dépensée if (direction === 1) { xpSpent += previousCost; } else if (direction === -1) { xpSpent -= previousCost - 1; if (xpSpent < 0) xpSpent = 0; } xpSpentEl.textContent = xpSpent; // Mise à jour XP restante (tous les éléments) xpRemainingEls.forEach(el => { el.textContent = baseXP - xpSpent; }); // Mise à jour coût (niveau actuel + 1) fameCostLabel.textContent = currentValue + 1; previousValue = currentValue; input.dataset.prevValue = currentValue; } input.addEventListener("input", updateReputation); }); */ function handleFavourChange(event) { const checkbox = event.target; const cost = Number(checkbox.dataset.cost) || 0; // Récupération des valeurs globales XP const startXPValue = parseInt(document.getElementById('startXP').textContent); let xpSpent = parseInt(document.getElementById('startXPSpent').textContent); const xpRemaining = startXPValue - xpSpent; // Si on coche ⇒ tenter de consommer le coût if (checkbox.checked) { // Pas assez d'XP ? On empêche de cocher if (xpRemaining < cost) { checkbox.checked = false; // rollback return; } xpSpent += cost; } else { // Si on décoche ⇒ on rend l’XP xpSpent -= cost; if (xpSpent < 0) xpSpent = 0; } // Mise à jour de l’XP dépensé document.getElementById('startXPSpent').textContent = xpSpent; // Mise à jour de tous les affichages XP restant document.querySelectorAll('.startXPRemaining') .forEach(el => el.textContent = startXPValue - xpSpent); //mise a jour du champ xperience document.querySelector('input.startXPRemaining').value = startXPValue - xpSpent; } // ----------------------------------------------------- // Ajout des listeners sur TOUTES les faveurs // ----------------------------------------------------- document.querySelectorAll('#section-favours input[type="checkbox"]').forEach(cb => { cb.addEventListener('change', handleFavourChange); }); document.addEventListener("DOMContentLoaded", function () { // IDs uniques : logique const disadvantageCounter = document.getElementById("disadvantagePoints"); const advantageCounter = document.getElementById("advantagePoints"); // Valeurs internes let disadvantagePoints = 0; const startingAdvantagePoints = parseInt(advantageCounter.textContent) || 0; let advantagePoints = startingAdvantagePoints; // Tous les checkbox désavantages const checkboxes = document.querySelectorAll(".disadvantage-checkbox"); checkboxes.forEach(cb => { const cost = parseInt(cb.dataset.cost) || 0; cb.addEventListener("change", () => { // Ajustement points désavantages if (cb.checked) { disadvantagePoints += cost; } else { disadvantagePoints -= cost; } // Mise à jour affichage disadvantageCounter.textContent = disadvantagePoints; // Ajuste automatiquement les points disponibles pour acheter des avantages advantagePoints = startingAdvantagePoints + disadvantagePoints; advantageCounter.textContent = advantagePoints; }); }); }); document.addEventListener('DOMContentLoaded', () => { const items = document.querySelectorAll('.omen-group-item'); items.forEach((item) => { const toggle = item.querySelector('.omen-toggle'); const radio = item.querySelector('.real-radio-omen input[type="radio"]'); // Quand on ouvre l'accordéon → on coche le radio toggle.addEventListener('change', () => { if (toggle.checked) { // 1) Sélectionner le radio correspondant if (radio) { radio.checked = true; radio.dispatchEvent(new Event('change')); } // 2) Fermer les autres accordéons items.forEach((other) => { if (other !== item) { const otherToggle = other.querySelector('.omen-toggle'); if (otherToggle) otherToggle.checked = false; } }); } }); }); }); document.addEventListener('DOMContentLoaded', () => { const items = document.querySelectorAll('.age-group-item'); items.forEach((item) => { const toggle = item.querySelector('.age-toggle'); const radio = item.querySelector('.real-radio-age input[type="radio"]'); // Quand on ouvre un accordéon → on coche le radio toggle.addEventListener('change', () => { if (toggle.checked) { // Sélectionner le radio individuel if (radio) { radio.checked = true; radio.dispatchEvent(new Event('change')); } // Fermer tous les autres accordéons items.forEach((other) => { if (other !== item) { const otherToggle = other.querySelector('.age-toggle'); if (otherToggle) otherToggle.checked = false; } }); } }); }); }); document.addEventListener('DOMContentLoaded', () => { const items = document.querySelectorAll('.caste-group-item'); items.forEach((item) => { const toggle = item.querySelector('.caste-toggle'); const radio = item.querySelector('.real-radio input[type="radio"]'); // Quand on ouvre l’accordéon -> coche le radio toggle.addEventListener('change', () => { if (toggle.checked) { // 1) Cocher le radio correspondant if (radio) { radio.checked = true; radio.dispatchEvent(new Event('change')); } // 2) Fermer tous les autres accordéons items.forEach((other) => { if (other !== item) { const t = other.querySelector('.caste-toggle'); if (t) t.checked = false; } }); } }); }); }); document.addEventListener('DOMContentLoaded', () => { const items = document.querySelectorAll('.caste-group'); items.forEach((item) => { const toggle = item.querySelector('.caste-toggle'); const radios = item.querySelectorAll('.real-radio input[type="radio"]'); toggle.addEventListener('change', () => { if (toggle.checked) { // coche le premier radio du groupe if (radios.length > 0) { radios[0].checked = true; radios[0].dispatchEvent(new Event('change', { bubbles: true })); } // ferme tous les autres accordéons items.forEach((other) => { if (other !== item) { const t = other.querySelector('.caste-toggle'); if (t) t.checked = false; } }); } }); }); }); document.addEventListener('DOMContentLoaded', () => { const valueSelects = document.querySelectorAll( '#section-caracteristics select[id$="_value"]' ); if (valueSelects.length === 0) return; function updateValues() { const selections = Array.from(valueSelects) .map(s => s.value) .filter(v => v !== ""); valueSelects.forEach(select => { const currentValue = select.value; const remaining = selections.filter(v => v !== currentValue); const usedCounts = {}; remaining.forEach(v => usedCounts[v] = (usedCounts[v] || 0) + 1); Array.from(select.options).forEach(option => { if (option.value === "") { option.disabled = false; return; } const used = usedCounts[option.value] || 0; if (used > 0) { option.disabled = true; usedCounts[option.value]--; } else { option.disabled = false; } }); }); } updateValues(); valueSelects.forEach(select => { select.addEventListener('change', updateValues); }); }); document.addEventListener('DOMContentLoaded', () => { const valueSelects = document.querySelectorAll( '#section-majorAttributes select[id$="_value"]' ); if (valueSelects.length === 0) return; function updateValues() { const selections = Array.from(valueSelects) .map(s => s.value) .filter(v => v !== ""); valueSelects.forEach(select => { const currentValue = select.value; const remaining = selections.filter(v => v !== currentValue); const usedCounts = {}; remaining.forEach(v => usedCounts[v] = (usedCounts[v] || 0) + 1); Array.from(select.options).forEach(option => { if (option.value === "") { option.disabled = false; return; } const used = usedCounts[option.value] || 0; if (used > 0) { option.disabled = true; usedCounts[option.value]--; } else { option.disabled = false; } }); }); } updateValues(); valueSelects.forEach(select => { select.addEventListener('change', updateValues); }); }); // Tendencies document.addEventListener('DOMContentLoaded', function() { const inputs = document.querySelectorAll('#section-tendencies input[type="number"]'); const maxTotal = 5; // Récupération des points libres en attribut data const freePointsElement = document.getElementById('free-points'); let freePoints = parseInt(freePointsElement.dataset.free) || 0; function getCurrentSum() { let sum = 0; inputs.forEach(input => { sum += parseInt(input.value) || 0; }); return sum; } function updateInput(changedInput) { const oldSum = getCurrentSum(); const newVal = parseInt(changedInput.value) || 0; // Max autorisé = somme actuelle - valeur avant + freePoints const oldVal = parseInt(changedInput.dataset.oldValue) || 0; const diff = newVal - oldVal; // Si on essaie d'augmenter au-delà des points libres if (diff > freePoints) { changedInput.value = oldVal + freePoints; // on limite } const newSum = getCurrentSum(); // Si la somme dépasse le maxTotal, correction if (newSum > maxTotal) { const overflow = newSum - maxTotal; changedInput.value = (parseInt(changedInput.value) || 0) - overflow; } // Mettre à jour freePoints après changement réel const finalVal = parseInt(changedInput.value) || 0; freePoints -= (finalVal - oldVal); if (freePoints < 0) freePoints = 0; changedInput.dataset.oldValue = finalVal; // Mise à jour affichage freePointsElement.textContent = "Points libres à répartir : " + freePoints; } // Initialisation des oldValue inputs.forEach(input => { input.dataset.oldValue = input.value; input.addEventListener('input', function() { updateInput(this); }); }); }); const AGE_RULES = { "enfants": { minor: { 0: 4, 1: 0, }, freePoints: 3 }, "adolescents": { minor: { 0: 2, 1: 0, }, freePoints: 4 }, "adultes": { minor: { 0: 0, 1: 0, }, freePoints: 6 }, "anciens": { minor: { 0: 0, 1: 2, }, freePoints: 4 }, "venerables": { minor: { 0: 0, 1: 4, }, freePoints: 3 } }; document.addEventListener("DOMContentLoaded", () => { const freeSpan = document.getElementById("free-minor"); const freeHiddenInput = document.querySelector( "input[name='prophecy_edit_figure_sheet_form[freeMinorPoints]']" ); const minorInputs = document.querySelectorAll( "input[name^='prophecy_edit_figure_sheet_form[minorAttributes]'][name$='[value]']" ); const ageRadios = document.querySelectorAll(".real-radio-age input[type=radio]"); // ------------------------------------------------- // 🔥 CHARGEMENT INITIAL (valeurs venant de Symfony) // ------------------------------------------------- let currentAgeName = null; // Trouver l'âge sélectionné au chargement ageRadios.forEach(radio => { if (radio.checked) { const ageLabel = radio.closest(".age-group-item").querySelector(".age-label"); currentAgeName = ageLabel.innerText.trim(); } }); const currentRules = AGE_RULES[currentAgeName]; // Minima à appliquer pour le calcul const minAgeValues = currentRules ? currentRules.minor : {}; // Base totale des points libres let baseFree = currentRules ? currentRules.freePoints : parseInt(freeSpan.textContent); // Recalcule les points libres réels (si la page est rechargée) function computeUsed() { let used = 0; minorInputs.forEach(inp => { const match = inp.name.match(/\[(\d+)]\[value]/); if (!match) return; const index = match[1]; const minVal = minAgeValues[index] ?? 0; const val = parseInt(inp.value) || 0; used += Math.max(0, val - minVal); }); return used; } // Mise à jour affichage + champ caché function updateFree() { const used = computeUsed(); const free = baseFree - used; freeSpan.textContent = free; freeSpan.style.color = free < 0 ? "red" : ""; if (freeHiddenInput) { freeHiddenInput.value = free; } return free; } // ------------------------------ // Gestion des modifications // ------------------------------ minorInputs.forEach(input => { input.addEventListener("input", () => { const match = input.name.match(/\[(\d+)]\[value]/); if (!match) return; const index = match[1]; const minVal = minAgeValues[index] ?? 0; let newVal = parseInt(input.value) || 0; // Interdiction de descendre sous le minimum if (newVal < minVal) { input.value = minVal; newVal = minVal; } // Vérifier qu'on ne dépasse pas les points libres const usedAfter = computeUsed(); if (usedAfter > baseFree) { // Rebascule à la valeur minimum acceptable input.value = minVal; } updateFree(); }); }); // ------------------------------ // Changement d'âge // ------------------------------ ageRadios.forEach(radio => { radio.addEventListener("change", () => { const ageLabel = radio.closest(".age-group-item").querySelector(".age-label"); const ageName = ageLabel.innerText.trim(); const rules = AGE_RULES[ageName]; if (!rules) return; baseFree = rules.freePoints; // Mettre à jour les minima d'âge Object.assign(minAgeValues, rules.minor); // Appliquer les minima minorInputs.forEach(input => { const match = input.name.match(/\[(\d+)]\[value]/); if (!match) return; const index = match[1]; const startVal = rules.minor[index] ?? 0; input.value = startVal; }); updateFree(); }); }); // ------------------------------ // Initialisation finale // ------------------------------ updateFree(); }); /* A UTILISER LORS DE LA VALIDATION DEFINITIVE DU FORMULAIRE document.addEventListener("DOMContentLoaded", () => { const form = document.getElementById("characterForm"); form.addEventListener("submit", function (event) { event.preventDefault(); // ⛔ On bloque l'envoi pour vérifier d'abord if (verifierFormulaire()) { console.log("Validation OK → Envoi du formulaire."); form.submit(); // ✔ On envoie réellement le formulaire } else { console.log("Validation KO → Envoi bloqué."); // Ici tu peux afficher un message sur la page } }); }); */ // 🔍 Fonction de vérification personnalisée function verifierFormulaire() { // Exemple : vérifier que les points ne sont pas négatifs const avantageRestants = parseInt(document.getElementById("advantagePoints").textContent); if (avantageRestants < 0) { alert("Vous n'avez pas assez de points pour valider ce personnage !"); return false; // ❌ validation échoue → le formulaire ne part pas } // Exemple : vérifier qu’au moins un désavantage est choisi const checkboxes = document.querySelectorAll(".disadvantage-checkbox"); const auMoinsUnCoche = Array.from(checkboxes).some(cb => cb.checked); if (!auMoinsUnCoche) { alert("Vous devez sélectionner au moins un désavantage."); return false; } // Si tout est OK : return true; // ✔ validation ok → le formulaire sera envoyé } {% endblock %}