import React, { useEffect, useMemo, useState } from "react"; import { createRoot } from "react-dom/client"; import { BrowserRouter, Link, NavLink, Route, Routes, useLocation, useParams, } from "react-router-dom"; import "./rules.css"; type Locale = "en" | "fr"; type RuleCard = { title: string; text?: string; meta?: string; bullets?: string[]; accent?: "brown" | "cyan" | "pink" | "green" | "red" | "yellow" | "orange" | "blue"; pills?: string[]; }; type TierCard = { title: string; tier: string; meta?: string; text: string; }; type RuleBlock = | { type: "prose"; paragraphs: string[] } | { type: "list"; title?: string; items: string[] } | { type: "cards"; title?: string; items: RuleCard[]; carousel?: boolean } | { type: "tierGrid"; title?: string; items: TierCard[]; carousel?: boolean } | { type: "table"; tableId?: "properties" | "drug-sales"; title?: string; columns: string[]; rows: (string | number | React.ReactNode)[][]; note?: string; } | { type: "callout"; title: string; text: string } | { type: "steps"; items: { title: string; text: string }[] }; type RuleSection = { id: string; title: string; lead?: string; blocks: RuleBlock[]; }; type RulesCopy = { badge: string; title: string; subtitle: string; source: string; heroCallout: { title: string; text: string }; backLabel: string; playLabel: string; searchPlaceholder: string; searchHint: string; quickFactsTitle: string; quickFacts: { label: string; value: string }[]; sections: RuleSection[]; footerNote: string; }; type PropertyRow = { name: string; price: number; houseCost: number; rent: number; rent1: number; rent2: number; rent3: number; rent4: number; rent5: number; mortgage: number; color: "brown" | "cyan" | "pink" | "green" | "red" | "yellow" | "orange" | "blue"; }; type TabConfig = { id: string; label: { fr: string; en: string }; sections: string[]; }; const tabConfig: TabConfig[] = [ { id: "basics", label: { fr: "Bases du jeu", en: "Game basics" }, sections: ["preamble", "principles", "flow", "communication"], }, { id: "movement", label: { fr: "Déplacement & mobilité", en: "Movement & mobility" }, sections: ["movement", "vehicles"], }, { id: "city", label: { fr: "Ville & économie", en: "City & economy" }, sections: ["real-estate", "institutions", "drugs"], }, { id: "conflict", label: { fr: "Conflits & contrats", en: "Conflict & deals" }, sections: ["weapons", "contracts"], }, ]; const properties: PropertyRow[] = [ { name: "Négotown", price: 60, houseCost: 50, rent: 2, rent1: 10, rent2: 30, rent3: 90, rent4: 160, rent5: 250, mortgage: 30, color: "brown" }, { name: "Black Arretxea", price: 60, houseCost: 50, rent: 4, rent1: 20, rent2: 60, rent3: 180, rent4: 320, rent5: 450, mortgage: 30, color: "brown" }, { name: "17 rue des patates", price: 100, houseCost: 50, rent: 6, rent1: 30, rent2: 90, rent3: 270, rent4: 400, rent5: 550, mortgage: 50, color: "cyan" }, { name: "69 rue des patates", price: 100, houseCost: 50, rent: 6, rent1: 30, rent2: 90, rent3: 270, rent4: 400, rent5: 550, mortgage: 50, color: "cyan" }, { name: "420 rue des patates", price: 120, houseCost: 50, rent: 8, rent1: 40, rent2: 100, rent3: 300, rent4: 450, rent5: 600, mortgage: 60, color: "cyan" }, { name: "Nuketown", price: 140, houseCost: 100, rent: 10, rent1: 50, rent2: 150, rent3: 450, rent4: 625, rent5: 750, mortgage: 70, color: "pink" }, { name: "Hijacked", price: 140, houseCost: 100, rent: 10, rent1: 50, rent2: 150, rent3: 450, rent4: 625, rent5: 750, mortgage: 70, color: "pink" }, { name: "Bassland", price: 160, houseCost: 100, rent: 12, rent1: 60, rent2: 180, rent3: 500, rent4: 700, rent5: 900, mortgage: 80, color: "pink" }, { name: "Numera Fight Club", price: 180, houseCost: 100, rent: 14, rent1: 70, rent2: 200, rent3: 550, rent4: 750, rent5: 950, mortgage: 90, color: "green" }, { name: "Snoopy's ID", price: 180, houseCost: 100, rent: 14, rent1: 70, rent2: 200, rent3: 550, rent4: 750, rent5: 950, mortgage: 90, color: "green" }, { name: "Jahland Dispensory", price: 200, houseCost: 100, rent: 16, rent1: 80, rent2: 220, rent3: 600, rent4: 800, rent5: 1000, mortgage: 100, color: "green" }, { name: "Pink Hoodie Studio", price: 220, houseCost: 150, rent: 18, rent1: 90, rent2: 250, rent3: 700, rent4: 875, rent5: 1050, mortgage: 110, color: "red" }, { name: "MP7 Studio", price: 220, houseCost: 150, rent: 18, rent1: 90, rent2: 250, rent3: 700, rent4: 875, rent5: 1050, mortgage: 110, color: "red" }, { name: "PCT Studio", price: 240, houseCost: 150, rent: 20, rent1: 100, rent2: 300, rent3: 750, rent4: 925, rent5: 1100, mortgage: 120, color: "red" }, { name: "l'Elysée", price: 260, houseCost: 150, rent: 22, rent1: 110, rent2: 330, rent3: 800, rent4: 975, rent5: 1150, mortgage: 130, color: "yellow" }, { name: "Bar Freak Show", price: 260, houseCost: 150, rent: 22, rent1: 110, rent2: 330, rent3: 800, rent4: 975, rent5: 1150, mortgage: 130, color: "yellow" }, { name: "Garage de Benoir", price: 280, houseCost: 150, rent: 24, rent1: 120, rent2: 360, rent3: 850, rent4: 1025, rent5: 1200, mortgage: 140, color: "yellow" }, { name: "Rue vendredi des noirs", price: 300, houseCost: 200, rent: 26, rent1: 130, rent2: 390, rent3: 900, rent4: 1100, rent5: 1275, mortgage: 150, color: "orange" }, { name: "Domaine de M. P", price: 300, houseCost: 200, rent: 26, rent1: 130, rent2: 390, rent3: 900, rent4: 1100, rent5: 1275, mortgage: 150, color: "orange" }, { name: "Villa du RJ", price: 320, houseCost: 200, rent: 28, rent1: 150, rent2: 450, rent3: 1000, rent4: 1200, rent5: 1400, mortgage: 160, color: "orange" }, { name: "Négoplaza", price: 350, houseCost: 200, rent: 35, rent1: 175, rent2: 500, rent3: 1100, rent4: 1300, rent5: 1500, mortgage: 175, color: "blue" }, { name: "LBTRD Tower", price: 400, houseCost: 200, rent: 50, rent1: 200, rent2: 600, rent3: 1400, rent4: 1700, rent5: 2000, mortgage: 200, color: "blue" }, ]; const copy: Record = { fr: { badge: "Manuel du participant", title: "Règles de Negopoly", subtitle: "Une réécriture web claire et accessible des règles officielles du Crédit Mabligop. Tout est ici pour trancher vite et jouer fort.", source: "Source : PDF officiel en français.", heroCallout: { title: "Autorité bancaire", text: "En cas de doute ou de litige, la version reconnue par le banquier fait foi. Les accords privés ne sont opposables qu’aux parties qui les acceptent.", }, backLabel: "Retour à NegoCity", playLabel: "Créer une partie", searchPlaceholder: "Rechercher une règle, un lieu, une arme…", searchHint: "Recherche sans accents, mots-clés acceptés.", quickFactsTitle: "Chiffres-clés", quickFacts: [ { label: "Kit de départ", value: "1500₦ + 1 bonus" }, { label: "Temps de tour", value: "5 minutes max" }, { label: "Paye Spawn", value: "250₦ − 10₦ / propriété" }, { label: "Licence d’armes", value: "70₦ jusqu’au prochain Spawn" }, { label: "Drogues", value: "50₦ la 1re dose, puis 70₦" }, { label: "Réunion privée", value: "20₦ / min (max 5 min)" }, { label: "Livraison express", value: "50₦" }, ], sections: [ { id: "preamble", title: "Préambule institutionnel", lead: "Negopoly est un chaos économique compétitif. La banque finance le chaos et le facture. Vos décisions ont des conséquences directes, souvent irréversibles.", blocks: [ { type: "prose", paragraphs: [ "Ici, tout est négociable, et ce qui ne l’est pas encore le deviendra dès qu’un accord financier satisfaisant sera trouvé.", "Le Crédit Mabligop n’intervient pas pour empêcher le chaos : il l’encadre, le finance, et tranche les litiges quand on le lui demande.", "La victoire récompense l’optimisation, pas l’équité. Manquer une opportunité par inattention est une erreur de gestion.", ], }, { type: "callout", title: "Clause de non-responsabilité", text: "La banque décline toute responsabilité en cas de ruine financière, d’incarcération, d’hospitalisation ou de trahison. Tout cela fait partie de l’expérience.", }, ], }, { id: "principles", title: "Principes fondamentaux", lead: "Ces règles prévalent sur toutes les autres. En cas de contradiction, elles s’appliquent sauf décision contraire du Crédit Mabligop.", blocks: [ { type: "cards", items: [ { title: "Tout est négociable", text: "Toute action, transaction ou décision peut faire l’objet d’une négociation entre joueurs, même en dehors des mécanismes prévus. Seul le prix accepté fait foi.", }, { title: "Rester dans son personnage", text: "L’immersion est une règle. Jouez le rôle et assumez les conséquences sociales du chaos.", }, { title: "Argent, dettes et propriété", text: "La monnaie officielle est le NegoDollar (₦). Pas de plafond de richesse, pas de limite de dettes. Un joueur peut continuer à jouer même sans argent, sauf élimination explicite.", }, { title: "Mensonge et vérité bancaire", text: "Le mensonge est autorisé. Les accords privés n’engagent que leurs signataires. La banque ne garantit ni sincérité ni exécution. En cas de litige, la version reconnue par le banquier fait foi si l’une des parties le demande.", }, ], }, ], }, { id: "flow", title: "Déroulement général d’une partie", lead: "La partie se déroule en rounds successifs. Un round correspond à un tour complet de table. L’ordre des joueurs est fixé en début de partie (sauf effet de carte ou décision du banquier).", blocks: [ { type: "cards", items: [ { title: "Kit de démarrage", text: "Chaque joueur reçoit 1500₦ sur son compte MyMabligop Standard, puis choisit un bonus :", pills: ["1500₦"], bullets: [ "RPG-7", "Trottinette au sans-plomb 75", "3 doses de drogue", "150₦ supplémentaires", ], }, { title: "Limites de temps", meta: "5 minutes", pills: ["5 min"], text: "Un tour se termine quand le joueur a fini ses actions ou quand la limite de 5 minutes est atteinte. Les discussions peuvent continuer pendant les tours des autres.", }, { title: "Hors de son tour", text: "En dehors de son tour, un joueur peut :", bullets: [ "Négocier verbalement et conclure des accords", "Utiliser des cartes « à tout moment »", "Lancer des réunions privées payantes", "Discuter pendant le tour des autres (sans pause automatique)", ], }, { title: "Conditions de victoire", text: "La partie se termine si :", bullets: [ "Une arme Game Ender est utilisée", "Un joueur a fait faillite à tous les autres", "La partie est trop longue (patrimoine personnel le plus élevé)", ], }, ], }, { type: "steps", items: [ { title: "Avant le lancer de dés", text: "Vérifiez les effets persistants. Vous pouvez activer les actions autorisées avant le lancer (notamment la consommation de drogues). Aucune action nécessitant un déplacement n’est possible avant le lancer.", }, { title: "Lancer et déplacement", text: "Lancez les dés, appliquez les effets de déplacement, avancez, puis appliquez les effets des cases traversées ou atteintes. Vous pouvez utiliser une carte Chance/Kamtar si autorisé et une seule carte Arme et/ou Véhicule.", }, { title: "Fin de tour", text: "Toutes vos actions sont terminées ou la limite de 5 minutes est atteinte.", }, ], }, ], }, { id: "movement", title: "Déplacements et mobilité", lead: "Le déplacement est l’action centrale d’un tour. Sauf exceptions, tout effet modifiant le déplacement doit être activé avant le lancer de dés.", blocks: [ { type: "cards", items: [ { title: "Effets de déplacement", text: "Hors véhicules, toute capacité ou carte qui modifie le déplacement doit être activée avant le lancer. Après le lancer, aucun nouvel effet ne peut être activé sauf mention contraire.", }, { title: "Drogues et mouvement", text: "Une drogue se consomme avant le lancer de dés. Lancez un dé Drogue : 1 = overdose, tour annulé, direction Hôpital. 2–4 = déplacement normal puis recul d’un nombre de cases égal au résultat des dés de déplacement. 5–6 = avancez d’un nombre de cases égal à la somme des dés de déplacement + dé Drogue. Effets obligatoires.", }, { title: "Téléportation", text: "Une téléportation annule le lancer de dés (sauf indication contraire). C’est un déplacement non conventionnel.", }, ], }, ], }, { id: "communication", title: "Communication, réunions, diplomatie", lead: "Les discussions, alliances, menaces et trahisons sont libres et permanentes.", blocks: [ { type: "cards", items: [ { title: "Discussions pendant les tours", text: "Discuter pendant le tour d’un autre joueur est autorisé. Le temps continue de s’écouler : aucune pause automatique pour négocier.", }, { title: "Réunions privées", pills: ["20₦/min", "5 min"], text: "Coût : 20₦ par minute, max 5 minutes, paiement immédiat. Les réunions privées sont gratuites pendant son propre tour mais le temps est décompté sur les 5 minutes.", }, { title: "Interférences", text: "Certains effets (ex : Bombe IEM, événements, décisions du banquier) perturbent la communication. Les effets s’appliquent immédiatement à tous les joueurs concernés.", }, ], }, ], }, { id: "real-estate", title: "Immobilier, propriétés, hypothèques", lead: "Le patrimoine immobilier est central. Les loyers s’appliquent immédiatement et les capacités de quartier peuvent tout changer.", blocks: [ { type: "prose", paragraphs: [ "Lorsqu’un joueur s’arrête sur une propriété non possédée, il peut l’acheter au prix du tableau. S’il refuse ou ne peut pas, la banque conserve la propriété.", "Lorsqu’un joueur s’arrête sur une propriété d’un autre joueur, il paie le loyer correspondant au nombre de maisons. Aucun loyer n’est dû si la propriété est hypothéquée, squattée ou réinitialisée.", ], }, { type: "cards", items: [ { title: "Hypothèques", text: "Une propriété hypothéquée ne génère plus de loyer, ses fonctionnalités sont désactivées et aucune maison ne peut y être construite. La déshypothèque nécessite de passer sur la propriété, sauf effet contraire.", }, { title: "Rachat forcé", text: "Un joueur adverse qui passe sur une propriété hypothéquée peut la racheter de force à la banque pour 2× le prix d’achat. Elle devient immédiatement non hypothéquée.", }, { title: "Maisons et destructions", text: "Une propriété peut accueillir jusqu’à 5 maisons, construites une par une pendant le tour du propriétaire. Les destructions par armes, explosifs ou actions mercenaires retirent une maison par effet.", }, { title: "Réinitialisation", text: "Une propriété réinitialisée retourne à la banque, perd ses maisons et hypothèques. Elle peut être rachetée normalement.", }, { title: "Interactions à distance", text: "Sans capacité spéciale, seules les actions suivantes sont autorisées à distance : hypothéquer, vendre, construire une maison, détruire une maison.", }, ], }, { type: "table", tableId: "properties", title: "Table des propriétés", columns: [ "Couleur", "Propriété", "Prix", "Coût maison", "Loyer", "1 maison", "2 maisons", "3 maisons", "4 maisons", "5 maisons", "Hypothèque", ], rows: [], note: "Les loyers sont indiqués en ₦. Les maisons se construisent une par une.", }, { type: "callout", title: "Quartier complet", text: "Quand un joueur possède toutes les propriétés d’une même couleur, il débloque la capacité spéciale du quartier. Elle reste active tant que la couleur est complète et disparaît dès qu’une propriété est vendue, rachetée, hypothéquée ou réinitialisée.", }, { type: "cards", items: [ { title: "Quartier Marron", text: "Le propriétaire perçoit 150₦ d’allocations pour les pauvres à chaque tour de terrain, en plus du passage au Spawn.", accent: "brown", }, { title: "Quartier Cyan (rue des patates)", text: "Le prix d’achat des drogues est fixé à 40₦. En tombant sur une propriété de la rue des patates, le joueur reçoit une dose gratuite.", accent: "cyan", }, { title: "Quartier Rose", text: "Le propriétaire devient propriétaire du Kamtar et perçoit 50% des paiements au Kamtar (y compris les siens), l’autre moitié allant à la banque.", accent: "pink", }, { title: "Quartier Vert", text: "Le propriétaire peut acheter des véhicules à distance sans s’arrêter sur NC Custom.", accent: "green", }, { title: "Quartier Rouge", text: "Le propriétaire pioche 3 cartes Chance, en applique 1 et défausse les 2 autres. Il peut aussi attribuer la carte choisie à un autre joueur.", accent: "red", }, { title: "Quartier Jaune", text: "Réduction de 25% sur toutes les armes sauf Game Enders. L’achat d’armes est possible en passant par-dessus un bar à mercenaires.", accent: "yellow", }, { title: "Quartier Orange", text: "Immunité totale à la police. Tout objet confisqué revient au propriétaire. Les joueurs tombant sur ce quartier sont automatiquement contrôlés. Le propriétaire peut acheter des capacités actives du Comico sur une propriété Orange et demander d’épargner un individu.", accent: "orange", }, { title: "Quartier Bleu foncé", text: "Interactions immobilières à distance (achat, déshypothèque, rachat forcé). Si le propriétaire commence son tour sur une propriété Bleu foncé, il peut se téléporter gratuitement vers n’importe quelle case.", accent: "blue", }, ], }, ], }, { id: "institutions", title: "Cases spéciales et institutions", lead: "Chaque case majeure impose ses propres règles, souvent liées à la police, aux drogues ou aux cartes.", blocks: [ { type: "cards", items: [ { title: "Spawn", text: "Au Spawn :", pills: ["250₦", "10₦/propriété", "Licence 70₦"], bullets: [ "Paye de 250₦ à chaque passage", "Frais administratifs de 10₦ par propriété possédée", "Achat de drogues et renouvellement de licence d’armes (70₦)", "Pioche d’une carte Météo à chaque passage (6 actives max)", ], }, { title: "Kamtar", pills: ["70₦"], text: "En passant par-dessus, vous pouvez acheter une carte Kamtar pour 70₦. En cas d’achat, vous vous arrêtez immédiatement et terminez votre déplacement. La défausse reforme la pioche quand elle est vide.", }, { title: "Hôpital", pills: ["140₦"], text: "Peut recevoir les victimes d’overdose, d’armes ou d’explosifs. En passant, vous pouvez vendre des drogues au personnel. En vous arrêtant, vous pouvez acheter une ordonnance trafiquée pour 140₦ (téléportation vers l’Hôpital à tout moment, objet consommé).", }, { title: "Prison", pills: ["100₦ → 150₦"], text: "En passant, vous pouvez vendre des drogues aux gardiens. En vous arrêtant, payez 100₦ à un prisonnier pour racketter 150₦ au prochain joueur envoyé en Prison (usage unique).", }, { title: "Comico", pills: ["170₦"], text: "En passant, vous pouvez vendre des objets ou des drogues aux forces de l’ordre. En vous arrêtant, payez 170₦ pour soudoyer le dresseur K9 : pendant le round suivant, tous les joueurs passant par un contrôle sont contrôlés.", }, { title: "Points de contrôle", text: "Un contrôle confisque drogues et armes sans licence et envoie le joueur en Prison. En cas de mandat d’arrêt, tout contrôle entraîne l’arrestation. Le mandat est levé après un passage au Spawn.", }, ], }, ], }, { id: "drugs", title: "Économie parallèle : drogues", lead: "La drogue est une ressource centrale : achat, vente, échange, consommation et confiscation.", blocks: [ { type: "cards", items: [ { title: "Achat au Spawn", pills: ["50₦", "70₦"], text: "Achat uniquement au Spawn : 50₦ la première dose. Si vous avez déjà au moins une dose, chaque dose supplémentaire coûte 70₦. Achat immédiat sans jet de dés.", }, { title: "Vente entre joueurs", text: "Libre par accord mutuel. La banque n’intervient pas et ne garantit ni sincérité ni exécution.", }, ], }, { type: "table", tableId: "drug-sales", title: "Vente aux coins", columns: ["Coin", "Risque", "Réussite", "Vente / dose"], rows: [ ["Prison", "Faible", "1–4", "100₦"], ["Hôpital", "Moyen", "1–3", "150₦"], ["Comico", "Élevé", "1–3", "250₦"], ], note: "Chaque dose nécessite un jet distinct. En cas d’échec : Prison/Hôpital → dose rendue ; Comico → dose confisquée.", }, ], }, { id: "vehicles", title: "Véhicules et livraisons", lead: "Les véhicules sont la seule exception majeure aux règles de timing du déplacement. Un seul véhicule par tour.", blocks: [ { type: "cards", items: [ { title: "Accès NC Custom", text: "Achat uniquement en s’arrêtant sur NC Custom/Concessionnaire. Stock limité : 2 exemplaires par niveau. Si un seul exemplaire reste, le prix augmente de 25%.", }, { title: "Activation", text: "Un véhicule peut être activé après le lancer de dés (ou avant, si souhaité). Les effets s’appliquent après le déplacement de base, sauf mention contraire.", }, ], }, { type: "cards", title: "Véhicules disponibles", carousel: true, items: [ { title: "Tier 0 — Trottinette au sans-plomb 75", meta: "150₦", text: "Lancer un dé : 1–2 panne (aucun effet). 3–5 avance d’1 case. 6 explosion, recule de 2 cases.", }, { title: "Tier 0 — Exosquelette à méga backflips", meta: "150₦", text: "Lancer un dé : 1–2 panne. 3–5 recule d’1 case. 6 explosion, avance de 2 cases.", }, { title: "Tier 1 — Automobile", meta: "250₦", text: "Avance d’1 case supplémentaire après le déplacement normal.", }, { title: "Tier 2 — Hélicoptère", meta: "400₦", text: "Choisir d’avancer ou reculer de 1 à 3 cases après le déplacement normal.", }, { title: "Tier 3 — Tank", meta: "450₦", text: "Avance de 2 ou 3 cases. Protège le joueur des effets négatifs de la case d’arrivée et de la case survolée. Si vous arrivez sur un joueur, il est envoyé à l’Hôpital.", }, { title: "Tier 4 — Avion de chasse", meta: "600₦", text: "Choisir d’avancer de 1 à 6 cases après le déplacement normal.", }, { title: "Tier 5 — Téléporteur de poche", meta: "800₦", text: "Téléportation immédiate vers n’importe quelle case, sans tenir compte des dés.", }, ], }, { type: "cards", title: "Livraisons", items: [ { title: "Règles de livraison", text: "Une livraison ne peut être initiée que pendant le tour de l’expéditeur. Par défaut, délai d’un round complet. Livraison express : 50₦. Un seul type d’objet par livraison, quantité illimitée. Les livraisons ne peuvent pas être bloquées sauf effet explicite.", }, ], }, ], }, { id: "weapons", title: "Armes, mercenariat et violence contractuelle", lead: "Le bar à mercenaires permet d’acheter armes et actions violentes. Accès uniquement en s’arrêtant sur la case ou une case adjacente.", blocks: [ { type: "cards", title: "Services mercenaires", carousel: true, items: [ { title: "Attaque de propriété", text: "Supprime une maison ou hypothèque si aucune maison. Coût = 3× le prix d’hypothèque. Impossible si la propriété est déjà hypothéquée.", }, { title: "Défense de propriété", text: "Empêche la prochaine attaque. Coût = prix de l’hypothèque. Usage unique.", }, { title: "Squat", text: "Désactive la propriété jusqu’à expulsion par passage ou effet spécial. Coût = moitié du prix d’hypothèque.", }, { title: "Hack / vol d’identité", text: "La prochaine arrestation de l’attaquant envoie la cible en Prison à sa place. Usage unique. Coût : 150₦.", }, { title: "Kidnapping", text: "Déplace un joueur vers le prochain ou le dernier bar à mercenaires. Utilisable à tout moment. Coût : 70₦.", }, { title: "Équipe de sécurité", text: "Protège contre une attaque directe ou indirecte. Consommée après usage. Coût : 200₦.", }, ], }, { type: "tierGrid", title: "Arsenal", carousel: true, items: [ { title: "RPG-7", tier: "Tier 0", meta: "150₦", text: "Stock illimité. Détruit une maison sur la propriété où vous êtes ou adjacente. Jusqu’à 2 tirs par tour. Jet : 1–4 réussite, 5 raté, 6 explosion (direction Hôpital). Sur propriété hypothéquée, un tir réussi la réinitialise.", }, { title: "C4 (charges)", tier: "Tier 0", meta: "40₦ la 1re, puis x2 / x3…", text: "Charges illimitées. Pose sur une maison en passant ou en s’arrêtant. Toute charge peut être retirée par n’importe quel joueur en passant (il la récupère). À partir de 2 rounds après la pose, le poseur peut déclencher : maison détruite, joueur sur place → Hôpital, joueur adjacent → Prison (terroriste). Bombe IEM désactive les C4 tant qu’elle est active.", }, { title: "Glock 26", tier: "Tier 1", meta: "200₦", text: "Racket d’un joueur sur la même case ou adjacente pour 300₦. Si la cible possède aussi une Glock, jet : 4–6 victoire de l’attaquant, 1–3 défaite. Le perdant est racketté et envoyé à l’Hôpital.", }, { title: "AR15", tier: "Tier 2", meta: "400₦", text: "Annule l’effet de n’importe quelle case sur laquelle le joueur se trouve.", }, { title: "Mortier", tier: "Tier 3", meta: "400₦", text: "3 tirs. Chaque tir détruit une maison sur une case adjacente ; s’il n’y a pas de maison, la propriété est hypothéquée ; si elle l’est déjà, elle est réinitialisée.", }, { title: "Bombe IEM", tier: "Tier 4", meta: "400₦", text: "Pendant un tour : toutes les cases sont désactivées, les actions à distance sont désactivées, les communications privées interdites (sauf même case), toutes les charges de C4 sont désactivées.", }, { title: "Pièce d’artillerie", tier: "Tier 5", meta: "600₦", text: "Supprime toutes les maisons d’une propriété appartenant à un joueur.", }, { title: "Rods From Gods", tier: "Tier 6", meta: "1500₦", text: "Rase une couleur entière : supprime toutes les maisons et réinitialise les propriétés.", }, { title: "Satan2", tier: "Game Ender", meta: "5000₦", text: "Détruit l’intégralité de NegoCity et donne la victoire immédiate au joueur qui l’utilise.", }, { title: "La Peste Nègre", tier: "Game Ender", meta: "7000₦", text: "La Peste Nègre (sélection naturelle) élimine tous les joueurs sauf deux. Les deux survivants remportent automatiquement la partie.", }, ], }, ], }, { id: "contracts", title: "Contrats, primes et accords privés", lead: "Les contrats formalisent des engagements financiers ou stratégiques entre joueurs. Ils sont facultatifs mais opposables aux signataires.", blocks: [ { type: "cards", items: [ { title: "Contrats", text: "Accord enregistré à la banque. Nécessite l’accord explicite des parties et un coût fixe. Coût : 30₦ par round de validité. La durée est définie à la création. Le banquier décide des sanctions en cas de non-respect.", }, { title: "Primes", text: "Une prime est une somme déposée à la banque pour récompenser la première personne qui réalise une action. Les conditions doivent être claires, vérifiables et acceptées par la banque.", }, { title: "Prêts à la banque", text: "Un joueur peut emprunter jusqu’à 500₦ à tout moment. Remboursement exigé dans un délai de 2 tours de terrain (2e passage au Spawn).", }, ], }, ], }, ], footerNote: "Negopoly est un chaos économique compétitif. Jouez vite, négociez plus vite.", }, en: { badge: "Participant manual", title: "Negopoly Rules", subtitle: "A clear, web-friendly rewrite of the official Crédit Mabligop rules. Everything you need to decide fast and play hard.", source: "Source: official French PDF.", heroCallout: { title: "Bank authority", text: "If there is any doubt or dispute, the banker’s version prevails. Private deals only bind the parties who accept them.", }, backLabel: "Back to NegoCity", playLabel: "Start a session", searchPlaceholder: "Search for a rule, place, weapon…", searchHint: "Accent-insensitive search, keywords welcome.", quickFactsTitle: "Key facts", quickFacts: [ { label: "Starting kit", value: "1500₦ + 1 bonus" }, { label: "Turn timer", value: "5 minutes max" }, { label: "Spawn salary", value: "250₦ − 10₦ / property" }, { label: "Weapons license", value: "70₦ until next Spawn" }, { label: "Drugs", value: "50₦ first dose, then 70₦" }, { label: "Private meeting", value: "20₦ / min (max 5 min)" }, { label: "Express delivery", value: "50₦" }, ], sections: [ { id: "preamble", title: "Institutional preamble", lead: "Negopoly is a competitive economic chaos. The bank funds the chaos and bills it. Your decisions have direct, often irreversible consequences.", blocks: [ { type: "prose", paragraphs: [ "Here, everything is negotiable, and whatever isn’t negotiable yet will become so once a satisfying financial deal is found.", "Crédit Mabligop does not intervene to prevent chaos: it frames it, finances it, and settles disputes when asked.", "Victory rewards optimization, not fairness. Missing an opportunity through inattention is a management error.", ], }, { type: "callout", title: "Disclaimer", text: "The bank declines all responsibility in case of financial ruin, incarceration, hospitalization, or betrayal. It’s all part of the experience.", }, ], }, { id: "principles", title: "Fundamental principles", lead: "These rules override all others. If there is a contradiction, they apply unless Crédit Mabligop decides otherwise.", blocks: [ { type: "cards", items: [ { title: "Everything is negotiable", text: "Any action, transaction, or decision can be negotiated between players, even outside explicit rule mechanisms. The only valid price is the price accepted.", }, { title: "Stay in character", text: "Immersion is part of the game. Play the role and accept the social consequences of chaos.", }, { title: "Money, debt, property", text: "The official currency is the NegoDollar (₦). No wealth cap and no debt limit. You may keep playing even with zero money, unless explicitly eliminated.", }, { title: "Lies and bank truth", text: "Lying is allowed. Private deals only bind their signers. The bank guarantees neither honesty nor execution. In disputes, the banker’s version prevails if requested.", }, ], }, ], }, { id: "flow", title: "Game flow", lead: "The game runs in successive rounds. A round is one full table rotation. Player order is fixed at the start (unless a card or banker decision changes it).", blocks: [ { type: "cards", items: [ { title: "Starting kit", text: "Each player receives 1500₦ in MyMabligop Standard, then chooses one bonus:", pills: ["1500₦"], bullets: [ "RPG-7", "Trottinette au sans-plomb 75", "3 drug doses", "+150₦", ], }, { title: "Time limits", meta: "5 minutes", pills: ["5 min"], text: "A turn ends when the player finishes their actions or when the 5-minute limit is reached. Discussions can happen during other players’ turns.", }, { title: "Out of turn", text: "Out of your turn, you may:", bullets: [ "Negotiate and make verbal deals", "Use cards marked “any time”", "Start paid private meetings", "Talk during other turns (no automatic pause)", ], }, { title: "Victory conditions", text: "The game ends if:", bullets: [ "A Game Ender weapon is used", "One player bankrupts everyone else", "The game runs too long (highest personal net worth wins)", ], }, ], }, { type: "steps", items: [ { title: "Before the roll", text: "Check persistent effects. You may perform pre-roll actions (notably drug use). No action requiring movement is allowed before rolling.", }, { title: "Roll and move", text: "Roll the dice, apply movement modifiers, move, then apply effects of spaces crossed or reached. You may use a Chance/Kamtar card if allowed and one Weapon and/or Vehicle.", }, { title: "End of turn", text: "You finish your actions or hit the 5-minute limit.", }, ], }, ], }, { id: "movement", title: "Movement and mobility", lead: "Movement is the core of a turn. With few exceptions, any movement modifier must be activated before the roll.", blocks: [ { type: "cards", items: [ { title: "Movement modifiers", text: "Except for vehicles, any ability or card that modifies movement must be activated before the dice roll. After rolling, no new movement effect can be activated unless explicitly stated.", }, { title: "Drugs and movement", text: "Drugs are consumed before rolling. Roll a Drug die: 1 = overdose, turn cancelled, go to Hospital. 2–4 = normal movement, then move back a number of spaces equal to the movement dice. 5–6 = move forward by movement dice + Drug die. Effects are mandatory.", }, { title: "Teleportation", text: "Teleportation cancels the dice roll unless stated otherwise.", }, ], }, ], }, { id: "communication", title: "Communication, meetings, diplomacy", lead: "Discussion, alliances, threats, and betrayal are always allowed.", blocks: [ { type: "cards", items: [ { title: "Talk during turns", text: "You may talk during other players’ turns. The turn timer keeps running; there is no automatic pause for negotiation.", }, { title: "Private meetings", pills: ["20₦/min", "5 min"], text: "Cost: 20₦ per minute, max 5 minutes, paid immediately. Meetings are free during your own turn but count against your 5-minute limit.", }, { title: "Interference", text: "Certain effects (EMP bomb, events, banker decisions) disrupt communication. Their effects apply immediately to affected players.", }, ], }, ], }, { id: "real-estate", title: "Real estate, properties, mortgages", lead: "Property ownership is central. Rents apply immediately and neighborhood abilities can flip the game.", blocks: [ { type: "prose", paragraphs: [ "When a player stops on an unowned property, they may buy it at the listed price. If they refuse or can’t pay, the bank keeps it.", "When a player stops on someone else’s property, they must pay rent based on the number of houses. No rent is paid if the property is mortgaged, squatted, or reset.", ], }, { type: "cards", items: [ { title: "Mortgages", text: "A mortgaged property generates no rent, all functions are disabled, and no houses can be built. Unmortgaging requires passing the property unless a special effect says otherwise.", }, { title: "Forced buyback", text: "An opponent passing a mortgaged property can buy it from the bank for 2× the purchase price. It immediately becomes active (not mortgaged).", }, { title: "Houses and destruction", text: "A property can hold up to 5 houses, built one at a time on the owner’s turn. Destruction by weapons, explosives, or mercenary actions removes one house per effect.", }, { title: "Reset", text: "A reset property returns to the bank, loses houses and mortgages, and can be repurchased normally.", }, { title: "Remote real estate", text: "Without a special ability, only these remote actions are allowed: mortgage, sell, build a house, destroy a house.", }, ], }, { type: "table", tableId: "properties", title: "Property table", columns: [ "Color", "Property", "Price", "House cost", "Rent", "1 house", "2 houses", "3 houses", "4 houses", "5 houses", "Mortgage", ], rows: [], note: "Rents are in ₦. Houses are built one by one.", }, { type: "callout", title: "Complete set", text: "When a player owns every property of a color set, they unlock that set’s special ability. It stays active as long as the set is complete and is lost if a property is sold, bought back, mortgaged, or reset.", }, { type: "cards", items: [ { title: "Brown set", text: "Owner receives 150₦ in poor relief each full lap, in addition to passing Spawn.", accent: "brown", }, { title: "Cyan set (rue des patates)", text: "Drug purchase price is fixed at 40₦. Landing on a rue des patates property grants a free dose.", accent: "cyan", }, { title: "Pink set", text: "Owner becomes the Kamtar owner and receives 50% of all Kamtar payments (including their own), with the other half going to the bank.", accent: "pink", }, { title: "Green set", text: "Owner can buy vehicles remotely without stopping on NC Custom.", accent: "green", }, { title: "Red set", text: "Owner draws 3 Chance cards, chooses 1 to apply, discards 2. They may also assign the chosen card to another player.", accent: "red", }, { title: "Yellow set", text: "25% discount on all weapons except Game Enders. Weapons can be purchased by passing over a mercenary bar.", accent: "yellow", }, { title: "Orange set", text: "Total immunity to police. Confiscated items return to the owner. Players landing on Orange are automatically checked. Owner can buy Comico active abilities on Orange properties and request to spare a specific player.", accent: "orange", }, { title: "Dark blue set", text: "Remote real estate interactions (buy, unmortgage, forced buyback). If the owner starts their turn on a dark blue property, they may teleport anywhere for free.", accent: "blue", }, ], }, ], }, { id: "institutions", title: "Special spaces and institutions", lead: "Every major space has its own rules, often tied to police, drugs, or cards.", blocks: [ { type: "cards", items: [ { title: "Spawn", text: "At Spawn:", pills: ["250₦", "10₦/property", "License 70₦"], bullets: [ "Salary of 250₦ each time you pass", "Administrative fee of 10₦ per property owned", "Buy drugs and renew weapons license (70₦)", "Draw a Weather card each pass (6 active max)", ], }, { title: "Kamtar", pills: ["70₦"], text: "When passing over, you may buy a Kamtar card for 70₦. If you buy, you immediately stop and end movement. When the deck is empty, discard is reshuffled.", }, { title: "Hospital", pills: ["140₦"], text: "Victims of overdose, weapons, or explosives may be sent here. When passing, you can sell drugs to staff. When stopping, you can buy a forged prescription for 140₦ (teleport to Hospital at any time, item consumed).", }, { title: "Prison", pills: ["100₦ → 150₦"], text: "When passing, you can sell drugs to guards. When stopping, pay 100₦ to a prisoner to extort 150₦ from the next player sent to Prison (single-use).", }, { title: "Comico", pills: ["170₦"], text: "When passing, you can sell items or drugs to police. When stopping, pay 170₦ to bribe the K9 handler: during the next round, all players passing a checkpoint are checked.", }, { title: "Checkpoints", text: "A check confiscates drugs and unlicensed firearms and sends the player to Prison. If a player has a warrant, any check triggers arrest. Warrants are cleared after passing Spawn.", }, ], }, ], }, { id: "drugs", title: "Parallel economy: drugs", lead: "Drugs are a core resource: buy, sell, trade, consume, confiscate.", blocks: [ { type: "cards", items: [ { title: "Buying at Spawn", pills: ["50₦", "70₦"], text: "Only at Spawn: 50₦ for the first dose. If you already own at least one dose, each additional dose costs 70₦. Immediate purchase, no dice roll.", }, { title: "Player-to-player", text: "Free trade by mutual agreement. The bank does not intervene or guarantee execution.", }, ], }, { type: "table", tableId: "drug-sales", title: "Selling at corners", columns: ["Corner", "Risk", "Success", "Payout / dose"], rows: [ ["Prison", "Low", "1–4", "100₦"], ["Hospital", "Medium", "1–3", "150₦"], ["Comico", "High", "1–3", "250₦"], ], note: "Each dose requires a separate roll. On failure: Prison/Hospital → dose returned; Comico → dose confiscated.", }, ], }, { id: "vehicles", title: "Vehicles and deliveries", lead: "Vehicles are the major timing exception for movement. One vehicle per turn.", blocks: [ { type: "cards", items: [ { title: "NC Custom access", text: "Purchase only by stopping on NC Custom/dealership. Stock limited to 2 per tier. If only one is left, price increases by 25%.", }, { title: "Activation", text: "A vehicle may be activated after rolling (or before, if you wish). Effects apply after base movement unless stated otherwise.", }, ], }, { type: "cards", title: "Vehicles", carousel: true, items: [ { title: "Tier 0 — Trottinette au sans-plomb 75", meta: "150₦", text: "Roll a die: 1–2 breakdown (no effect). 3–5 move forward 1 space. 6 explosion, move back 2 spaces.", }, { title: "Tier 0 — Exosquelette à méga backflips", meta: "150₦", text: "Roll a die: 1–2 breakdown. 3–5 move back 1 space. 6 explosion, move forward 2 spaces.", }, { title: "Tier 1 — Automobile", meta: "250₦", text: "Move forward 1 extra space after normal movement.", }, { title: "Tier 2 — Hélicoptère", meta: "400₦", text: "Choose to move forward or back by 1 to 3 spaces after normal movement.", }, { title: "Tier 3 — Tank", meta: "450₦", text: "Move forward 2 or 3 spaces. Protects you from negative effects of the landing space and the passed-over space. If you land on another player, they go to Hospital.", }, { title: "Tier 4 — Avion de chasse", meta: "600₦", text: "Choose to move forward 1 to 6 spaces after normal movement.", }, { title: "Tier 5 — Téléporteur de poche", meta: "800₦", text: "Teleport instantly to any space, ignoring dice and intermediate spaces.", }, ], }, { type: "cards", title: "Deliveries", items: [ { title: "Delivery rules", text: "Deliveries can only be initiated on the sender’s turn. Default delay is one full round. Express delivery costs 50₦. One item type per delivery, unlimited quantity. Deliveries cannot be blocked unless an effect explicitly allows it.", }, ], }, ], }, { id: "weapons", title: "Weapons, mercenaries, and contract violence", lead: "The mercenary bar sells weapons and violent actions. Access only by stopping on the space or an adjacent space.", blocks: [ { type: "cards", title: "Mercenary services", carousel: true, items: [ { title: "Property attack", text: "Removes one house, or mortgages the property if it has no houses. Cost = 3× the mortgage price. Cannot target an already mortgaged property.", }, { title: "Property defense", text: "Prevents the next attack. Cost = mortgage price. Single-use.", }, { title: "Squat", text: "Disables the property until removed by a pass or special effect. Cost = half the mortgage price.", }, { title: "Hack / identity theft", text: "The attacker’s next arrest sends the target to Prison instead. Single-use. Cost: 150₦.", }, { title: "Kidnapping", text: "Moves a target player to the next or last mercenary bar. Usable at any time. Cost: 70₦.", }, { title: "Security team", text: "Protects against a direct or indirect attack. Consumed after use. Cost: 200₦.", }, ], }, { type: "tierGrid", title: "Arsenal", carousel: true, items: [ { title: "RPG-7", tier: "Tier 0", meta: "150₦", text: "Unlimited stock. Destroys a house on your property or an adjacent one. Up to 2 shots per turn. Roll: 1–4 hit, 5 miss, 6 explosion (go to Hospital). On a mortgaged property, a hit resets it.", }, { title: "C4 (charges)", tier: "Tier 0", meta: "40₦ first, then x2 / x3…", text: "Unlimited charges. Place on a house by passing over or stopping. Any player passing can remove a charge and keep it. From two rounds after placement, the placer may detonate: destroy house, player on space → Hospital, player adjacent → Prison (terrorist). EMP disables C4 while active.", }, { title: "Glock 26", tier: "Tier 1", meta: "200₦", text: "Extort a player on the same or adjacent space for 300₦. If the target also has a Glock, roll: 4–6 attacker wins, 1–3 attacker loses. Loser is extorted and sent to Hospital.", }, { title: "AR15", tier: "Tier 2", meta: "400₦", text: "Cancels the effect of any space the player is on.", }, { title: "Mortar", tier: "Tier 3", meta: "400₦", text: "3 shots. Each shot destroys a house on an adjacent space; if none, the property is mortgaged; if already mortgaged, the property is reset.", }, { title: "EMP Bomb", tier: "Tier 4", meta: "400₦", text: "For one turn: all spaces are disabled, remote actions disabled, private communication forbidden (except same space), all C4 charges disabled.", }, { title: "Artillery piece", tier: "Tier 5", meta: "600₦", text: "Removes all houses from a single property.", }, { title: "Rods From Gods", tier: "Tier 6", meta: "1500₦", text: "Razes an entire color set: removes all houses and resets properties.", }, { title: "Satan2", tier: "Game Ender", meta: "5000₦", text: "Destroys all of NegoCity and grants immediate victory to the user.", }, { title: "La Peste Nègre", tier: "Game Ender", meta: "7000₦", text: "La Peste Nègre (natural selection) eliminates all players except two. The two survivors automatically win.", }, ], }, ], }, { id: "contracts", title: "Contracts, bounties, private deals", lead: "Contracts formalize financial or strategic commitments between players. Optional, but binding for signers.", blocks: [ { type: "cards", items: [ { title: "Contracts", text: "Agreement recorded at the bank. Requires explicit consent of all parties and a fixed cost. Cost: 30₦ per round of validity. Duration set at creation. The banker decides sanctions for breach.", }, { title: "Bounties", text: "A bounty is money deposited at the bank to reward the first player who completes a specific action. Conditions must be clear, verifiable, and accepted by the bank.", }, { title: "Bank loans", text: "Players may borrow up to 500₦ at any time. Must be repaid within 2 turns (second Spawn pass after borrowing).", }, ], }, ], }, ], footerNote: "Negopoly is competitive economic chaos. Play fast, negotiate faster.", }, }; const localeStorageKey = "negopoly.rules.locale"; function getDefaultLocale(): Locale { if (typeof window === "undefined") return "en"; try { const stored = window.localStorage.getItem(localeStorageKey); if (stored === "fr" || stored === "en") return stored; } catch { // ignore storage access errors } const lang = navigator.language?.toLowerCase(); return lang && lang.startsWith("fr") ? "fr" : "en"; } function formatNumber(value: number, locale: Locale) { return new Intl.NumberFormat(locale === "fr" ? "fr-FR" : "en-US").format(value); } function formatMoney(value: number, locale: Locale) { return `${formatNumber(value, locale)}₦`; } function normalizeText(value: string) { return value .toLowerCase() .normalize("NFD") .replace(/[\u0300-\u036f]/g, ""); } function escapeRegExp(value: string) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function highlight(text: string, query: string) { const tokens = query.trim().split(/\s+/).filter(Boolean); if (tokens.length === 0) return text; const needle = tokens[0]; if (!needle) return text; const regex = new RegExp(escapeRegExp(needle), "gi"); const matches = text.match(regex); if (!matches) return text; const parts = text.split(regex); return parts.reduce((acc, part, index) => { acc.push(part); const match = matches[index]; if (match) { acc.push({match}); } return acc; }, []); } function collectSearchLines(section: RuleSection) { const lines: string[] = [section.title]; if (section.lead) lines.push(section.lead); section.blocks.forEach((block) => { if (block.type === "prose") lines.push(...block.paragraphs); if (block.type === "list") lines.push(...block.items); if (block.type === "cards") { block.items.forEach((card) => { lines.push(card.title); if (card.text) lines.push(card.text); if (card.meta) lines.push(card.meta); if (card.bullets) lines.push(...card.bullets); if (card.pills) lines.push(...card.pills); }); } if (block.type === "tierGrid") { block.items.forEach((item) => { lines.push(item.title, item.tier); if (item.meta) lines.push(item.meta); lines.push(item.text); }); } if (block.type === "table") { block.rows.forEach((row) => { lines.push(row.map(String).join(" ")); }); } if (block.type === "callout") lines.push(block.title, block.text); if (block.type === "steps") { block.items.forEach((item) => lines.push(item.title, item.text)); } }); return lines; } function buildPropertyRows(locale: Locale) { return properties.map((property) => [ , property.name, formatMoney(property.price, locale), formatMoney(property.houseCost, locale), formatMoney(property.rent, locale), formatMoney(property.rent1, locale), formatMoney(property.rent2, locale), formatMoney(property.rent3, locale), formatMoney(property.rent4, locale), formatMoney(property.rent5, locale), formatMoney(property.mortgage, locale), ]); } function applyPropertyRows(sections: RuleSection[], locale: Locale) { return sections.map((section) => { if (!section.blocks.some((block) => block.type === "table")) return section; return { ...section, blocks: section.blocks.map((block) => { if (block.type !== "table") return block; if (block.tableId !== "properties") return block; return { ...block, rows: buildPropertyRows(locale), }; }), }; }); } function useHashScroll(sections: RuleSection[]) { const location = useLocation(); useEffect(() => { if (!location.hash) return; const id = decodeURIComponent(location.hash.replace("#", "")); const exists = sections.some((section) => section.id === id); if (!exists) return; const target = document.getElementById(id); if (target) { target.scrollIntoView({ behavior: "smooth", block: "start" }); } }, [location.hash, sections]); } function RulesLayout({ locale, setLocale }: { locale: Locale; setLocale: (locale: Locale) => void }) { const content = copy[locale]; return (
{content.badge}

{content.title}

{content.subtitle}

{content.source}
{content.backLabel} {content.playLabel}
{content.heroCallout.title} {content.heroCallout.text}
); } function RulesTabs({ locale }: { locale: Locale }) { return ( ); } function RulesTabContent({ locale, query, setQuery, sections, }: { locale: Locale; query: string; setQuery: (value: string) => void; sections: RuleSection[]; }) { useHashScroll(sections); const content = copy[locale]; const params = useParams(); const tabId = params.tabId || "basics"; const tab = tabConfig.find((item) => item.id === tabId) ?? tabConfig[0]; const sectionLookup = useMemo(() => { const map = new Map(); sections.forEach((section) => map.set(section.id, section)); return map; }, [sections]); const tabSections = tab.sections .map((sectionId) => sectionLookup.get(sectionId)) .filter((section): section is RuleSection => Boolean(section)); const searchResults = useMemo(() => { const trimmed = query.trim(); if (!trimmed) return [] as { id: string; title: string; matches: string[] }[]; const tokens = normalizeText(trimmed).split(/\s+/).filter(Boolean); if (tokens.length === 0) return []; return sections .map((section) => { const lines = collectSearchLines(section); const matches = lines.filter((line) => { const normalized = normalizeText(line); return tokens.every((token) => normalized.includes(token)); }); return { id: section.id, title: section.title, matches: matches.slice(0, 3) }; }) .filter((entry) => entry.matches.length > 0); }, [query, sections]); const sectionToTab = useMemo(() => { const map = new Map(); tabConfig.forEach((item) => { item.sections.forEach((sectionId) => map.set(sectionId, item.id)); }); return map; }, []); return (
setQuery(event.target.value)} aria-label={content.searchPlaceholder} /> {content.searchHint}
{searchResults.length > 0 && (

{locale === "fr" ? "Résultats" : "Results"}

{searchResults.map((result) => { const targetTab = sectionToTab.get(result.id) ?? "basics"; return (
{result.title}
    {result.matches.map((match, index) => (
  • {highlight(match, query)}
  • ))}
); })}
)} {tab.id === "basics" && (

{content.quickFactsTitle}

{content.quickFacts.map((fact) => (
{fact.label} {fact.value}
))}
)}
{tabSections.map((section) => ( {section.title} ))}
{tabSections.map((section) => (

{section.title}

{section.lead &&

{section.lead}

} {section.blocks.map((block, index) => { if (block.type === "prose") { return block.paragraphs.map((paragraph, paragraphIndex) => (

{paragraph}

)); } if (block.type === "list") { return (
{block.title &&

{block.title}

}
    {block.items.map((item) => (
  • {item}
  • ))}
); } if (block.type === "cards") { return (
{block.title &&

{block.title}

}
{block.items.map((card) => (
{card.meta &&
{card.meta}
}

{card.title}

{card.pills && (
{card.pills.map((pill) => ( {pill} ))}
)} {card.text &&

{card.text}

} {card.bullets && (
    {card.bullets.map((item) => (
  • {item}
  • ))}
)}
))}
{block.carousel && ( )}
); } if (block.type === "tierGrid") { return (
{block.title &&

{block.title}

}
{block.items.map((item) => { const normalizedTier = item.tier.toLowerCase(); const shortTier = normalizedTier.startsWith("tier") ? item.tier.replace(/tier\\s*/i, "T").trim() : "GE"; return (
{shortTier}
{item.tier}

{item.title}

{item.meta &&
{item.meta}
}

{item.text}

); })}
{block.carousel && ( )}
); } if (block.type === "table") { const isProperties = block.tableId === "properties"; const isDrugSales = block.tableId === "drug-sales"; return (
{block.title &&

{block.title}

}
{block.columns.map((column) => ( ))} {block.rows.map((row) => { const rowKey = String( isProperties ? row[1] : row[0] ?? row[1] ?? block.title ?? index ); return ( {row.map((cell, cellIndex) => ( ))} ); })}
{column}
{cell}
{block.note &&

{block.note}

}
{block.rows.map((row, rowIndex) => { const cardKey = String(row[1] ?? row[0] ?? `${block.title ?? "row"}-${rowIndex}`); const titleValue = isProperties ? row[1] : row[0]; const swatchValue = isProperties ? row[0] : null; const fields = block.columns .map((label, labelIndex) => ({ label, value: row[labelIndex] })) .filter((field, fieldIndex) => { if (isProperties) return fieldIndex !== 1; return fieldIndex !== 0; }); return (
{swatchValue && {swatchValue}} {titleValue}
{fields.map((field) => (
{field.label} {field.value}
))}
); })}
); } if (block.type === "callout") { return (
{block.title} {block.text}
); } if (block.type === "steps") { return (
{block.items.map((item) => (

{item.title}

{item.text}

))}
); } return null; })}
))} {!tabSections.length &&

{locale === "fr" ? "Section introuvable." : "Section not found."}

}
{content.footerNote}
); } function RulesApp() { const [locale, setLocale] = useState(getDefaultLocale()); const [query, setQuery] = useState(""); const content = useMemo(() => copy[locale], [locale]); const sections = useMemo(() => applyPropertyRows(content.sections, locale), [content, locale]); useEffect(() => { document.documentElement.lang = locale; try { window.localStorage.setItem(localeStorageKey, locale); } catch { // ignore storage access errors } }, [locale]); return (
} /> } />
); } function RulesRoot() { return ( ); } const root = createRoot(document.getElementById("root")!); root.render();