diff --git a/front/rules.tsx b/front/rules.tsx index f19ca6c..1ae3fc3 100644 --- a/front/rules.tsx +++ b/front/rules.tsx @@ -9,6 +9,12 @@ import { useLocation, useParams, } from "react-router-dom"; +import { + getLocalizedText, + helpProperties, + helpVehicles, + helpWeapons, +} from "../shared/help-catalog"; import "./rules.css"; type Locale = "en" | "fr"; @@ -68,20 +74,6 @@ type RulesCopy = { 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 }; @@ -111,31 +103,6 @@ const tabConfig: TabConfig[] = [ }, ]; -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", @@ -573,44 +540,7 @@ const copy: Record = { 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.", - }, - ], + items: buildVehicleCards("fr"), }, { type: "cards", @@ -667,73 +597,7 @@ const copy: Record = { 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.", - }, - ], + items: buildWeaponCards("fr"), }, ], }, @@ -1202,44 +1066,7 @@ const copy: Record = { 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.", - }, - ], + items: buildVehicleCards("en"), }, { type: "cards", @@ -1296,73 +1123,7 @@ const copy: Record = { 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.", - }, - ], + items: buildWeaponCards("en"), }, ], }, @@ -1486,14 +1247,31 @@ function collectSearchLines(section: RuleSection) { return lines; } +function buildVehicleCards(locale: Locale): RuleCard[] { + return helpVehicles.map((vehicle) => ({ + title: `${getLocalizedText(vehicle.tier, locale)} — ${getLocalizedText(vehicle.name, locale)}`, + meta: getLocalizedText(vehicle.price, locale), + text: getLocalizedText(vehicle.text, locale), + })); +} + +function buildWeaponCards(locale: Locale): TierCard[] { + return helpWeapons.map((weapon) => ({ + title: getLocalizedText(weapon.name, locale), + tier: getLocalizedText(weapon.tier, locale), + meta: getLocalizedText(weapon.price, locale), + text: getLocalizedText(weapon.text, locale), + })); +} + function buildPropertyRows(locale: Locale) { - return properties.map((property) => [ + return helpProperties.map((property) => [ , - property.name, + getLocalizedText(property.name, locale), formatMoney(property.price, locale), formatMoney(property.houseCost, locale), formatMoney(property.rent, locale), diff --git a/mobile/metro.config.js b/mobile/metro.config.js new file mode 100644 index 0000000..18c0fed --- /dev/null +++ b/mobile/metro.config.js @@ -0,0 +1,15 @@ +const path = require("node:path"); +const { getDefaultConfig } = require("expo/metro-config"); + +const projectRoot = __dirname; +const workspaceRoot = path.resolve(projectRoot, ".."); + +const config = getDefaultConfig(projectRoot); + +config.watchFolders = [workspaceRoot]; +config.resolver.nodeModulesPaths = [ + path.resolve(projectRoot, "node_modules"), + path.resolve(workspaceRoot, "node_modules"), +]; + +module.exports = config; diff --git a/mobile/scripts/generate-screenshots.mjs b/mobile/scripts/generate-screenshots.mjs index 9e2a650..ea863c6 100644 --- a/mobile/scripts/generate-screenshots.mjs +++ b/mobile/scripts/generate-screenshots.mjs @@ -33,6 +33,7 @@ const scenes = [ { slug: "lobby", fileName: "Lobby.png" }, { slug: "home", fileName: "Home.png" }, { slug: "transfers", fileName: "Transfers.png" }, + { slug: "help", fileName: "Help.png" }, { slug: "chat", fileName: "Chat.png" }, ]; diff --git a/mobile/src/dev/screenshot-fixtures.ts b/mobile/src/dev/screenshot-fixtures.ts index eaac58b..2ddf592 100644 --- a/mobile/src/dev/screenshot-fixtures.ts +++ b/mobile/src/dev/screenshot-fixtures.ts @@ -1,6 +1,12 @@ import type { SessionSnapshot } from "../shared/types"; -export type ScreenshotScene = "start" | "lobby" | "home" | "transfers" | "chat"; +export type ScreenshotScene = + | "start" + | "lobby" + | "home" + | "transfers" + | "help" + | "chat"; export type ScreenshotNavigationState = { index: number; @@ -278,6 +284,7 @@ export function normalizeScreenshotScene(value: string | null | undefined): Scre if (normalized === "lobby") return "lobby"; if (normalized === "home") return "home"; if (normalized === "transfers") return "transfers"; + if (normalized === "help") return "help"; if (normalized === "chat") return "chat"; return null; } @@ -352,6 +359,27 @@ export function buildScreenshotFixture(scene: ScreenshotScene): ScreenshotFixtur }; } + if (scene === "help") { + return { + scene, + sessionId: activeSessionId, + sessionCode, + playerId: meId, + session: createActiveSession(), + navigationTarget: { + root: "PlayerTabs", + state: { + index: 0, + routes: [{ name: "PlayerHelp" }], + }, + followUp: { + name: "PlayerTabs", + params: { screen: "PlayerHelp" }, + }, + }, + }; + } + return { scene, sessionId: activeSessionId, diff --git a/mobile/src/i18n.ts b/mobile/src/i18n.ts index 7f9e536..7b62bfb 100644 --- a/mobile/src/i18n.ts +++ b/mobile/src/i18n.ts @@ -131,6 +131,22 @@ const translations = { "home.balance": "Balance", "home.recent": "Recent operations", "home.noActivity": "No activity yet.", + "help.introTitle": "Quick rules reference", + "help.introBody": + "Swipe through the main assets and equipment without leaving the game. This tab is a condensed mobile version of the web rules guide.", + "help.swipeHint": "Swipe sideways to browse every card.", + "help.section.properties": "Real estate", + "help.section.vehicles": "Vehicles", + "help.section.weapons": "Weapons", + "help.field.price": "Price", + "help.field.houseCost": "House cost", + "help.field.baseRent": "Base rent", + "help.field.rent1": "1 house", + "help.field.rent2": "2 houses", + "help.field.rent3": "3 houses", + "help.field.rent4": "4 houses", + "help.field.rent5": "5 houses", + "help.field.mortgage": "Mortgage", "blackout.title": "EMP", "blackout.defaultReason": "EMP in effect", "blackout.active": "EMP active", @@ -199,6 +215,7 @@ const translations = { "chat.messagePlaceholder": "Message", "tabs.home": "Accounts", "tabs.transfers": "Payments", + "tabs.help": "Help", "tabs.chat": "Messages", "tabs.dashboard": "Agency", "tabs.tools": "Control", @@ -343,6 +360,22 @@ const translations = { "home.balance": "Solde", "home.recent": "Opérations récentes", "home.noActivity": "Aucune activité.", + "help.introTitle": "Repères rapides", + "help.introBody": + "Faites défiler les principaux biens et équipements sans quitter la partie. Cet onglet reprend une version condensée du guide web.", + "help.swipeHint": "Faites glisser horizontalement pour voir toutes les cartes.", + "help.section.properties": "Immobilier", + "help.section.vehicles": "Véhicules", + "help.section.weapons": "Armes", + "help.field.price": "Prix", + "help.field.houseCost": "Coût maison", + "help.field.baseRent": "Loyer", + "help.field.rent1": "1 maison", + "help.field.rent2": "2 maisons", + "help.field.rent3": "3 maisons", + "help.field.rent4": "4 maisons", + "help.field.rent5": "5 maisons", + "help.field.mortgage": "Hypothèque", "blackout.title": "EMP", "blackout.defaultReason": "EMP en cours", "blackout.active": "EMP actif", @@ -411,6 +444,7 @@ const translations = { "chat.messagePlaceholder": "Message", "tabs.home": "Comptes", "tabs.transfers": "Paiements", + "tabs.help": "Aide", "tabs.chat": "Messages", "tabs.dashboard": "Agence", "tabs.tools": "Pilotage", diff --git a/mobile/src/navigation/AppNavigator.tsx b/mobile/src/navigation/AppNavigator.tsx index d9691e9..5f9218e 100644 --- a/mobile/src/navigation/AppNavigator.tsx +++ b/mobile/src/navigation/AppNavigator.tsx @@ -9,6 +9,7 @@ import AgencyCreateScreen from "../screens/AgencyCreateScreen"; import LobbyScreen from "../screens/LobbyScreen"; import PlayerHomeScreen from "../screens/PlayerHomeScreen"; import PlayerTransfersScreen from "../screens/PlayerTransfersScreen"; +import PlayerHelpScreen from "../screens/PlayerHelpScreen"; import BankerDashboardScreen from "../screens/BankerDashboardScreen"; import BankerToolsScreen from "../screens/BankerToolsScreen"; import ChatListScreen from "../screens/chat/ChatListScreen"; @@ -108,6 +109,17 @@ export function PlayerTabsNavigator() { ), }} /> + ( + + ), + }} + /> = { + brown: "#714826", + cyan: "#87d8d9", + pink: "#bc46ca", + green: "#5b933c", + red: "#ab3d31", + yellow: "#f8ff72", + orange: "#e9b054", + blue: "#0e08a2", +}; + +const PROPERTY_LIGHT_TEXT: Record = { + brown: true, + cyan: false, + pink: true, + green: true, + red: true, + yellow: false, + orange: false, + blue: true, +}; + +const SCREEN_PADDING = 20; +const CARD_GAP = 12; + +function formatMoney(amount: number, locale: HelpLocale) { + const intlLocale = locale === "fr" ? "fr-FR" : "en-US"; + return `₦${new Intl.NumberFormat(intlLocale, { + maximumFractionDigits: 0, + }).format(amount)}`; +} + +function PropertyCard({ + property, + locale, + cardWidth, + theme, + t, +}: { + property: HelpProperty; + locale: HelpLocale; + cardWidth: number; + theme: AppTheme; + t: ReturnType["t"]; +}) { + const styles = useMemo(() => createStyles(theme), [theme]); + const propertyName = getLocalizedText(property.name, locale); + const swatchColor = PROPERTY_SWATCHES[property.color]; + const swatchTextColor = PROPERTY_LIGHT_TEXT[property.color] ? "#fff8ee" : "#241a08"; + const stats = [ + { label: t("help.field.price"), value: formatMoney(property.price, locale) }, + { label: t("help.field.houseCost"), value: formatMoney(property.houseCost, locale) }, + { label: t("help.field.baseRent"), value: formatMoney(property.rent, locale) }, + { label: t("help.field.rent1"), value: formatMoney(property.rent1, locale) }, + { label: t("help.field.rent2"), value: formatMoney(property.rent2, locale) }, + { label: t("help.field.rent3"), value: formatMoney(property.rent3, locale) }, + { label: t("help.field.rent4"), value: formatMoney(property.rent4, locale) }, + { label: t("help.field.rent5"), value: formatMoney(property.rent5, locale) }, + { label: t("help.field.mortgage"), value: formatMoney(property.mortgage, locale) }, + ]; + + return ( + + + + + {property.color.toUpperCase()} + + + {propertyName} + + + {stats.map((stat) => ( + + {stat.label} + {stat.value} + + ))} + + + ); +} + +function AssetCard({ + item, + locale, + cardWidth, + theme, +}: { + item: HelpVehicle | HelpWeapon; + locale: HelpLocale; + cardWidth: number; + theme: AppTheme; +}) { + const styles = useMemo(() => createStyles(theme), [theme]); + + return ( + + + + + {getLocalizedText(item.tier, locale)} + + + {getLocalizedText(item.price, locale)} + + + {getLocalizedText(item.name, locale)} + + {getLocalizedText(item.text, locale)} + + ); +} + +function CarouselSection({ + title, + hint, + styles, + children, +}: { + title: string; + hint: string; + styles: ReturnType; + children: React.ReactNode; +}) { + return ( + + + {title} + {hint} + + {children} + + ); +} + +export default function PlayerHelpScreen() { + const manager = useSession(); + const { t, locale } = useI18n(); + const theme = useTheme(); + const styles = useMemo(() => createStyles(theme), [theme]); + const { width } = useWindowDimensions(); + const cardWidth = Math.max(280, Math.min(width - SCREEN_PADDING * 2 - 18, 420)); + const snapInterval = cardWidth + CARD_GAP; + const showEmp = Boolean(manager.session?.blackoutActive) && !manager.isBanker; + + if (!manager.session || !manager.me) { + return ( + + {t("common.loading")} + + ); + } + + return ( + + + + {t("help.introTitle")} + {t("help.introBody")} + + + + item.id} + horizontal + nestedScrollEnabled + showsHorizontalScrollIndicator={false} + decelerationRate="fast" + snapToInterval={snapInterval} + snapToAlignment="start" + disableIntervalMomentum + contentContainerStyle={styles.carouselContent} + ItemSeparatorComponent={() => } + renderItem={({ item }) => ( + + )} + /> + + + + item.id} + horizontal + nestedScrollEnabled + showsHorizontalScrollIndicator={false} + decelerationRate="fast" + snapToInterval={snapInterval} + snapToAlignment="start" + disableIntervalMomentum + contentContainerStyle={styles.carouselContent} + ItemSeparatorComponent={() => } + renderItem={({ item }) => ( + + )} + /> + + + + item.id} + horizontal + nestedScrollEnabled + showsHorizontalScrollIndicator={false} + decelerationRate="fast" + snapToInterval={snapInterval} + snapToAlignment="start" + disableIntervalMomentum + contentContainerStyle={styles.carouselContent} + ItemSeparatorComponent={() => } + renderItem={({ item }) => ( + + )} + /> + + + + + ); +} + +const createStyles = (theme: AppTheme) => + StyleSheet.create({ + wrapper: { + flex: 1, + position: "relative", + backgroundColor: theme.colors.background, + }, + scroll: { + flex: 1, + backgroundColor: theme.colors.background, + }, + content: { + padding: SCREEN_PADDING, + gap: 24, + backgroundColor: theme.colors.background, + }, + loadingContainer: { + flex: 1, + padding: SCREEN_PADDING, + justifyContent: "center", + backgroundColor: theme.colors.background, + }, + helper: { + color: theme.colors.textMuted, + }, + heroCard: { + backgroundColor: theme.colors.surface, + borderRadius: 18, + padding: 18, + gap: 8, + borderWidth: 1, + borderColor: theme.colors.borderMuted, + }, + heroTitle: { + fontSize: 20, + fontWeight: "700", + color: theme.colors.text, + }, + heroBody: { + color: theme.colors.textMuted, + lineHeight: 20, + }, + sectionHeader: { + gap: 4, + marginBottom: 12, + }, + sectionTitle: { + fontSize: 18, + fontWeight: "700", + color: theme.colors.text, + }, + sectionHint: { + fontSize: 13, + color: theme.colors.textMuted, + }, + carouselContent: { + paddingRight: SCREEN_PADDING, + }, + carouselGap: { + width: CARD_GAP, + }, + propertyCard: { + backgroundColor: theme.colors.surface, + borderRadius: 18, + padding: 16, + borderWidth: 1, + gap: 16, + }, + assetCard: { + backgroundColor: theme.colors.surface, + borderRadius: 18, + padding: 16, + borderWidth: 1, + borderColor: theme.colors.border, + gap: 14, + }, + cardHeader: { + gap: 10, + }, + colorBadge: { + alignSelf: "flex-start", + paddingHorizontal: 10, + paddingVertical: 6, + borderRadius: 999, + }, + colorBadgeText: { + fontSize: 11, + fontWeight: "800", + letterSpacing: 0.6, + }, + propertyTitle: { + fontSize: 20, + fontWeight: "700", + color: theme.colors.text, + }, + statsList: { + gap: 10, + }, + statRow: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + gap: 12, + paddingBottom: 10, + borderBottomWidth: 1, + borderBottomColor: theme.colors.borderMuted, + }, + statLabel: { + flex: 1, + color: theme.colors.textMuted, + fontSize: 13, + }, + statValue: { + color: theme.colors.text, + fontWeight: "700", + fontSize: 13, + }, + assetPills: { + flexDirection: "row", + gap: 8, + flexWrap: "wrap", + }, + tierPill: { + paddingHorizontal: 10, + paddingVertical: 6, + borderRadius: 999, + backgroundColor: theme.colors.brandSurface, + }, + tierPillText: { + color: theme.colors.brandText, + fontSize: 12, + fontWeight: "700", + }, + pricePill: { + paddingHorizontal: 10, + paddingVertical: 6, + borderRadius: 999, + backgroundColor: theme.colors.accentSurface, + }, + pricePillText: { + color: theme.colors.accentText, + fontSize: 12, + fontWeight: "700", + }, + assetTitle: { + fontSize: 20, + fontWeight: "700", + color: theme.colors.text, + }, + assetBody: { + color: theme.colors.textMuted, + lineHeight: 20, + }, + }); diff --git a/shared/help-catalog.test.ts b/shared/help-catalog.test.ts new file mode 100644 index 0000000..13bf078 --- /dev/null +++ b/shared/help-catalog.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it } from "bun:test"; +import { helpProperties, helpVehicles, helpWeapons } from "./help-catalog"; + +describe("help catalog", () => { + it("exports the expected item counts", () => { + expect(helpProperties).toHaveLength(22); + expect(helpVehicles).toHaveLength(7); + expect(helpWeapons).toHaveLength(10); + }); + + it("provides both locales for every exported item", () => { + helpProperties.forEach((property) => { + expect(property.name.en.length).toBeGreaterThan(0); + expect(property.name.fr.length).toBeGreaterThan(0); + }); + + helpVehicles.forEach((vehicle) => { + expect(vehicle.name.en.length).toBeGreaterThan(0); + expect(vehicle.name.fr.length).toBeGreaterThan(0); + expect(vehicle.tier.en.length).toBeGreaterThan(0); + expect(vehicle.tier.fr.length).toBeGreaterThan(0); + expect(vehicle.price.en.length).toBeGreaterThan(0); + expect(vehicle.price.fr.length).toBeGreaterThan(0); + expect(vehicle.text.en.length).toBeGreaterThan(0); + expect(vehicle.text.fr.length).toBeGreaterThan(0); + }); + + helpWeapons.forEach((weapon) => { + expect(weapon.name.en.length).toBeGreaterThan(0); + expect(weapon.name.fr.length).toBeGreaterThan(0); + expect(weapon.tier.en.length).toBeGreaterThan(0); + expect(weapon.tier.fr.length).toBeGreaterThan(0); + expect(weapon.price.en.length).toBeGreaterThan(0); + expect(weapon.price.fr.length).toBeGreaterThan(0); + expect(weapon.text.en.length).toBeGreaterThan(0); + expect(weapon.text.fr.length).toBeGreaterThan(0); + }); + }); + + it("matches key spot values from the rules data", () => { + expect(helpProperties[0]).toMatchObject({ + id: "negotown", + price: 60, + houseCost: 50, + mortgage: 30, + color: "brown", + }); + expect(helpProperties[21]).toMatchObject({ + id: "lbtrd-tower", + rent5: 2000, + color: "blue", + }); + expect(helpVehicles[6]).toMatchObject({ + id: "teleporteur-de-poche", + price: { en: "800₦", fr: "800₦" }, + }); + expect(helpWeapons[5]).toMatchObject({ + id: "emp-bomb", + name: { en: "EMP Bomb", fr: "Bombe IEM" }, + }); + }); +}); diff --git a/shared/help-catalog.ts b/shared/help-catalog.ts new file mode 100644 index 0000000..8931dea --- /dev/null +++ b/shared/help-catalog.ts @@ -0,0 +1,539 @@ +export type HelpLocale = "en" | "fr"; + +export type LocalizedText = { + en: string; + fr: string; +}; + +export type HelpPropertyColor = "brown" | "cyan" | "pink" | "green" | "red" | "yellow" | "orange" | "blue"; + +export type HelpProperty = { + id: string; + color: HelpPropertyColor; + name: LocalizedText; + price: number; + houseCost: number; + rent: number; + rent1: number; + rent2: number; + rent3: number; + rent4: number; + rent5: number; + mortgage: number; +}; + +export type HelpVehicle = { + id: string; + name: LocalizedText; + tier: LocalizedText; + price: LocalizedText; + text: LocalizedText; +}; + +export type HelpWeapon = { + id: string; + name: LocalizedText; + tier: LocalizedText; + price: LocalizedText; + text: LocalizedText; +}; + +export function getLocalizedText(value: LocalizedText, locale: HelpLocale): string { + return value[locale]; +} + +export const helpProperties: HelpProperty[] = [ + { + id: "negotown", + name: { en: "Negotown", fr: "Négotown" }, + price: 60, + houseCost: 50, + rent: 2, + rent1: 10, + rent2: 30, + rent3: 90, + rent4: 160, + rent5: 250, + mortgage: 30, + color: "brown", + }, + { + id: "black-arretxea", + name: { en: "Black Arretxea", fr: "Black Arretxea" }, + price: 60, + houseCost: 50, + rent: 4, + rent1: 20, + rent2: 60, + rent3: 180, + rent4: 320, + rent5: 450, + mortgage: 30, + color: "brown", + }, + { + id: "17-rue-des-patates", + name: { en: "17 rue des patates", fr: "17 rue des patates" }, + price: 100, + houseCost: 50, + rent: 6, + rent1: 30, + rent2: 90, + rent3: 270, + rent4: 400, + rent5: 550, + mortgage: 50, + color: "cyan", + }, + { + id: "69-rue-des-patates", + name: { en: "69 rue des patates", fr: "69 rue des patates" }, + price: 100, + houseCost: 50, + rent: 6, + rent1: 30, + rent2: 90, + rent3: 270, + rent4: 400, + rent5: 550, + mortgage: 50, + color: "cyan", + }, + { + id: "420-rue-des-patates", + name: { en: "420 rue des patates", fr: "420 rue des patates" }, + price: 120, + houseCost: 50, + rent: 8, + rent1: 40, + rent2: 100, + rent3: 300, + rent4: 450, + rent5: 600, + mortgage: 60, + color: "cyan", + }, + { + id: "nuketown", + name: { en: "Nuketown", fr: "Nuketown" }, + price: 140, + houseCost: 100, + rent: 10, + rent1: 50, + rent2: 150, + rent3: 450, + rent4: 625, + rent5: 750, + mortgage: 70, + color: "pink", + }, + { + id: "hijacked", + name: { en: "Hijacked", fr: "Hijacked" }, + price: 140, + houseCost: 100, + rent: 10, + rent1: 50, + rent2: 150, + rent3: 450, + rent4: 625, + rent5: 750, + mortgage: 70, + color: "pink", + }, + { + id: "bassland", + name: { en: "Bassland", fr: "Bassland" }, + price: 160, + houseCost: 100, + rent: 12, + rent1: 60, + rent2: 180, + rent3: 500, + rent4: 700, + rent5: 900, + mortgage: 80, + color: "pink", + }, + { + id: "numera-fight-club", + name: { en: "Numera Fight Club", fr: "Numera Fight Club" }, + price: 180, + houseCost: 100, + rent: 14, + rent1: 70, + rent2: 200, + rent3: 550, + rent4: 750, + rent5: 950, + mortgage: 90, + color: "green", + }, + { + id: "snoopys-id", + name: { en: "Snoopy's ID", fr: "Snoopy's ID" }, + price: 180, + houseCost: 100, + rent: 14, + rent1: 70, + rent2: 200, + rent3: 550, + rent4: 750, + rent5: 950, + mortgage: 90, + color: "green", + }, + { + id: "jahland-dispensory", + name: { en: "Jahland Dispensory", fr: "Jahland Dispensory" }, + price: 200, + houseCost: 100, + rent: 16, + rent1: 80, + rent2: 220, + rent3: 600, + rent4: 800, + rent5: 1000, + mortgage: 100, + color: "green", + }, + { + id: "pink-hoodie-studio", + name: { en: "Pink Hoodie Studio", fr: "Pink Hoodie Studio" }, + price: 220, + houseCost: 150, + rent: 18, + rent1: 90, + rent2: 250, + rent3: 700, + rent4: 875, + rent5: 1050, + mortgage: 110, + color: "red", + }, + { + id: "mp7-studio", + name: { en: "MP7 Studio", fr: "MP7 Studio" }, + price: 220, + houseCost: 150, + rent: 18, + rent1: 90, + rent2: 250, + rent3: 700, + rent4: 875, + rent5: 1050, + mortgage: 110, + color: "red", + }, + { + id: "pct-studio", + name: { en: "PCT Studio", fr: "PCT Studio" }, + price: 240, + houseCost: 150, + rent: 20, + rent1: 100, + rent2: 300, + rent3: 750, + rent4: 925, + rent5: 1100, + mortgage: 120, + color: "red", + }, + { + id: "elysee", + name: { en: "l'Elysée", fr: "l'Elysée" }, + price: 260, + houseCost: 150, + rent: 22, + rent1: 110, + rent2: 330, + rent3: 800, + rent4: 975, + rent5: 1150, + mortgage: 130, + color: "yellow", + }, + { + id: "bar-freak-show", + name: { en: "Bar Freak Show", fr: "Bar Freak Show" }, + price: 260, + houseCost: 150, + rent: 22, + rent1: 110, + rent2: 330, + rent3: 800, + rent4: 975, + rent5: 1150, + mortgage: 130, + color: "yellow", + }, + { + id: "garage-de-benoir", + name: { en: "Garage de Benoir", fr: "Garage de Benoir" }, + price: 280, + houseCost: 150, + rent: 24, + rent1: 120, + rent2: 360, + rent3: 850, + rent4: 1025, + rent5: 1200, + mortgage: 140, + color: "yellow", + }, + { + id: "rue-vendredi-des-noirs", + name: { en: "Rue vendredi des noirs", fr: "Rue vendredi des noirs" }, + price: 300, + houseCost: 200, + rent: 26, + rent1: 130, + rent2: 390, + rent3: 900, + rent4: 1100, + rent5: 1275, + mortgage: 150, + color: "orange", + }, + { + id: "domaine-de-m-p", + name: { en: "Domaine de M. P", fr: "Domaine de M. P" }, + price: 300, + houseCost: 200, + rent: 26, + rent1: 130, + rent2: 390, + rent3: 900, + rent4: 1100, + rent5: 1275, + mortgage: 150, + color: "orange", + }, + { + id: "villa-du-rj", + name: { en: "Villa du RJ", fr: "Villa du RJ" }, + price: 320, + houseCost: 200, + rent: 28, + rent1: 150, + rent2: 450, + rent3: 1000, + rent4: 1200, + rent5: 1400, + mortgage: 160, + color: "orange", + }, + { + id: "negoplaza", + name: { en: "Negoplaza", fr: "Négoplaza" }, + price: 350, + houseCost: 200, + rent: 35, + rent1: 175, + rent2: 500, + rent3: 1100, + rent4: 1300, + rent5: 1500, + mortgage: 175, + color: "blue", + }, + { + id: "lbtrd-tower", + name: { en: "LBTRD Tower", fr: "LBTRD Tower" }, + price: 400, + houseCost: 200, + rent: 50, + rent1: 200, + rent2: 600, + rent3: 1400, + rent4: 1700, + rent5: 2000, + mortgage: 200, + color: "blue", + }, +]; + +export const helpVehicles: HelpVehicle[] = [ + { + id: "trottinette-sans-plomb-75", + name: { + en: "Trottinette au sans-plomb 75", + fr: "Trottinette au sans-plomb 75", + }, + tier: { en: "Tier 0", fr: "Tier 0" }, + price: { en: "150₦", fr: "150₦" }, + text: { + en: "Roll a die: 1–2 breakdown (no effect). 3–5 move forward 1 space. 6 explosion, move back 2 spaces.", + fr: "Lancer un dé : 1–2 panne (aucun effet). 3–5 avance d’1 case. 6 explosion, recule de 2 cases.", + }, + }, + { + id: "exosquelette-backflips", + name: { + en: "Exosquelette à méga backflips", + fr: "Exosquelette à méga backflips", + }, + tier: { en: "Tier 0", fr: "Tier 0" }, + price: { en: "150₦", fr: "150₦" }, + text: { + en: "Roll a die: 1–2 breakdown. 3–5 move back 1 space. 6 explosion, move forward 2 spaces.", + fr: "Lancer un dé : 1–2 panne. 3–5 recule d’1 case. 6 explosion, avance de 2 cases.", + }, + }, + { + id: "automobile", + name: { en: "Automobile", fr: "Automobile" }, + tier: { en: "Tier 1", fr: "Tier 1" }, + price: { en: "250₦", fr: "250₦" }, + text: { + en: "Move forward 1 extra space after normal movement.", + fr: "Avance d’1 case supplémentaire après le déplacement normal.", + }, + }, + { + id: "helicoptere", + name: { en: "Hélicoptère", fr: "Hélicoptère" }, + tier: { en: "Tier 2", fr: "Tier 2" }, + price: { en: "400₦", fr: "400₦" }, + text: { + en: "Choose to move forward or back by 1 to 3 spaces after normal movement.", + fr: "Choisir d’avancer ou reculer de 1 à 3 cases après le déplacement normal.", + }, + }, + { + id: "tank", + name: { en: "Tank", fr: "Tank" }, + tier: { en: "Tier 3", fr: "Tier 3" }, + price: { en: "450₦", fr: "450₦" }, + text: { + en: "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.", + fr: "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.", + }, + }, + { + id: "avion-de-chasse", + name: { en: "Avion de chasse", fr: "Avion de chasse" }, + tier: { en: "Tier 4", fr: "Tier 4" }, + price: { en: "600₦", fr: "600₦" }, + text: { + en: "Choose to move forward 1 to 6 spaces after normal movement.", + fr: "Choisir d’avancer de 1 à 6 cases après le déplacement normal.", + }, + }, + { + id: "teleporteur-de-poche", + name: { en: "Téléporteur de poche", fr: "Téléporteur de poche" }, + tier: { en: "Tier 5", fr: "Tier 5" }, + price: { en: "800₦", fr: "800₦" }, + text: { + en: "Teleport instantly to any space, ignoring dice and intermediate spaces.", + fr: "Téléportation immédiate vers n’importe quelle case, sans tenir compte des dés.", + }, + }, +]; + +export const helpWeapons: HelpWeapon[] = [ + { + id: "rpg-7", + name: { en: "RPG-7", fr: "RPG-7" }, + tier: { en: "Tier 0", fr: "Tier 0" }, + price: { en: "150₦", fr: "150₦" }, + text: { + en: "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.", + fr: "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.", + }, + }, + { + id: "c4", + name: { en: "C4 (charges)", fr: "C4 (charges)" }, + tier: { en: "Tier 0", fr: "Tier 0" }, + price: { + en: "40₦ first, then x2 / x3…", + fr: "40₦ la 1re, puis x2 / x3…", + }, + text: { + en: "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.", + fr: "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.", + }, + }, + { + id: "glock-26", + name: { en: "Glock 26", fr: "Glock 26" }, + tier: { en: "Tier 1", fr: "Tier 1" }, + price: { en: "200₦", fr: "200₦" }, + text: { + en: "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.", + fr: "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.", + }, + }, + { + id: "ar15", + name: { en: "AR15", fr: "AR15" }, + tier: { en: "Tier 2", fr: "Tier 2" }, + price: { en: "400₦", fr: "400₦" }, + text: { + en: "Cancels the effect of any space the player is on.", + fr: "Annule l’effet de n’importe quelle case sur laquelle le joueur se trouve.", + }, + }, + { + id: "mortar", + name: { en: "Mortar", fr: "Mortier" }, + tier: { en: "Tier 3", fr: "Tier 3" }, + price: { en: "400₦", fr: "400₦" }, + text: { + en: "3 shots. Each shot destroys a house on an adjacent space; if none, the property is mortgaged; if already mortgaged, the property is reset.", + fr: "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.", + }, + }, + { + id: "emp-bomb", + name: { en: "EMP Bomb", fr: "Bombe IEM" }, + tier: { en: "Tier 4", fr: "Tier 4" }, + price: { en: "400₦", fr: "400₦" }, + text: { + en: "For one turn: all spaces are disabled, remote actions disabled, private communication forbidden (except same space), all C4 charges disabled.", + fr: "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.", + }, + }, + { + id: "artillery-piece", + name: { en: "Artillery piece", fr: "Pièce d’artillerie" }, + tier: { en: "Tier 5", fr: "Tier 5" }, + price: { en: "600₦", fr: "600₦" }, + text: { + en: "Removes all houses from a single property.", + fr: "Supprime toutes les maisons d’une propriété appartenant à un joueur.", + }, + }, + { + id: "rods-from-gods", + name: { en: "Rods From Gods", fr: "Rods From Gods" }, + tier: { en: "Tier 6", fr: "Tier 6" }, + price: { en: "1500₦", fr: "1500₦" }, + text: { + en: "Razes an entire color set: removes all houses and resets properties.", + fr: "Rase une couleur entière : supprime toutes les maisons et réinitialise les propriétés.", + }, + }, + { + id: "satan2", + name: { en: "Satan2", fr: "Satan2" }, + tier: { en: "Game Ender", fr: "Game Ender" }, + price: { en: "5000₦", fr: "5000₦" }, + text: { + en: "Destroys all of NegoCity and grants immediate victory to the user.", + fr: "Détruit l’intégralité de NegoCity et donne la victoire immédiate au joueur qui l’utilise.", + }, + }, + { + id: "la-peste-negre", + name: { en: "La Peste Nègre", fr: "La Peste Nègre" }, + tier: { en: "Game Ender", fr: "Game Ender" }, + price: { en: "7000₦", fr: "7000₦" }, + text: { + en: "La Peste Nègre (natural selection) eliminates all players except two. The two survivors automatically win.", + fr: "La Peste Nègre (sélection naturelle) élimine tous les joueurs sauf deux. Les deux survivants remportent automatiquement la partie.", + }, + }, +];