Ajout d'un onglet "aide" dans la version mobile de l'appli qui reprend les propriétés, les armes et le véhicules

This commit is contained in:
Feror 2026-03-30 12:01:10 +02:00
parent 01a77b5fc9
commit f374d184c4
10 changed files with 1174 additions and 253 deletions

View file

@ -9,6 +9,12 @@ import {
useLocation, useLocation,
useParams, useParams,
} from "react-router-dom"; } from "react-router-dom";
import {
getLocalizedText,
helpProperties,
helpVehicles,
helpWeapons,
} from "../shared/help-catalog";
import "./rules.css"; import "./rules.css";
type Locale = "en" | "fr"; type Locale = "en" | "fr";
@ -68,20 +74,6 @@ type RulesCopy = {
footerNote: string; 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 = { type TabConfig = {
id: string; id: string;
label: { fr: string; en: 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<Locale, RulesCopy> = { const copy: Record<Locale, RulesCopy> = {
fr: { fr: {
badge: "Manuel du participant", badge: "Manuel du participant",
@ -573,44 +540,7 @@ const copy: Record<Locale, RulesCopy> = {
type: "cards", type: "cards",
title: "Véhicules disponibles", title: "Véhicules disponibles",
carousel: true, carousel: true,
items: [ items: buildVehicleCards("fr"),
{
title: "Tier 0 — Trottinette au sans-plomb 75",
meta: "150₦",
text: "Lancer un dé : 12 panne (aucun effet). 35 avance d1 case. 6 explosion, recule de 2 cases.",
},
{
title: "Tier 0 — Exosquelette à méga backflips",
meta: "150₦",
text: "Lancer un dé : 12 panne. 35 recule d1 case. 6 explosion, avance de 2 cases.",
},
{
title: "Tier 1 — Automobile",
meta: "250₦",
text: "Avance d1 case supplémentaire après le déplacement normal.",
},
{
title: "Tier 2 — Hélicoptère",
meta: "400₦",
text: "Choisir davancer 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 darrivée et de la case survolée. Si vous arrivez sur un joueur, il est envoyé à lHôpital.",
},
{
title: "Tier 4 — Avion de chasse",
meta: "600₦",
text: "Choisir davancer 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 nimporte quelle case, sans tenir compte des dés.",
},
],
}, },
{ {
type: "cards", type: "cards",
@ -667,73 +597,7 @@ const copy: Record<Locale, RulesCopy> = {
type: "tierGrid", type: "tierGrid",
title: "Arsenal", title: "Arsenal",
carousel: true, carousel: true,
items: [ items: buildWeaponCards("fr"),
{
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 : 14 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 sarrêtant. Toute charge peut être retirée par nimporte 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 quelle est active.",
},
{
title: "Glock 26",
tier: "Tier 1",
meta: "200₦",
text:
"Racket dun joueur sur la même case ou adjacente pour 300₦. Si la cible possède aussi une Glock, jet : 46 victoire de lattaquant, 13 défaite. Le perdant est racketté et envoyé à lHôpital.",
},
{
title: "AR15",
tier: "Tier 2",
meta: "400₦",
text: "Annule leffet de nimporte 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 ; sil ny a pas de maison, la propriété est hypothéquée ; si elle lest 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 dartillerie",
tier: "Tier 5",
meta: "600₦",
text: "Supprime toutes les maisons dune 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 lintégralité de NegoCity et donne la victoire immédiate au joueur qui lutilise.",
},
{
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.",
},
],
}, },
], ],
}, },
@ -1202,44 +1066,7 @@ const copy: Record<Locale, RulesCopy> = {
type: "cards", type: "cards",
title: "Vehicles", title: "Vehicles",
carousel: true, carousel: true,
items: [ items: buildVehicleCards("en"),
{
title: "Tier 0 — Trottinette au sans-plomb 75",
meta: "150₦",
text: "Roll a die: 12 breakdown (no effect). 35 move forward 1 space. 6 explosion, move back 2 spaces.",
},
{
title: "Tier 0 — Exosquelette à méga backflips",
meta: "150₦",
text: "Roll a die: 12 breakdown. 35 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", type: "cards",
@ -1296,73 +1123,7 @@ const copy: Record<Locale, RulesCopy> = {
type: "tierGrid", type: "tierGrid",
title: "Arsenal", title: "Arsenal",
carousel: true, carousel: true,
items: [ items: buildWeaponCards("en"),
{
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: 14 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: 46 attacker wins, 13 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.",
},
],
}, },
], ],
}, },
@ -1486,14 +1247,31 @@ function collectSearchLines(section: RuleSection) {
return lines; 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) { function buildPropertyRows(locale: Locale) {
return properties.map((property) => [ return helpProperties.map((property) => [
<span <span
key={`${property.name}-color`} key={`${property.id}-color`}
className={`property-swatch ${property.color}`} className={`property-swatch ${property.color}`}
aria-label={property.color} aria-label={property.color}
/>, />,
property.name, getLocalizedText(property.name, locale),
formatMoney(property.price, locale), formatMoney(property.price, locale),
formatMoney(property.houseCost, locale), formatMoney(property.houseCost, locale),
formatMoney(property.rent, locale), formatMoney(property.rent, locale),

15
mobile/metro.config.js Normal file
View file

@ -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;

View file

@ -33,6 +33,7 @@ const scenes = [
{ slug: "lobby", fileName: "Lobby.png" }, { slug: "lobby", fileName: "Lobby.png" },
{ slug: "home", fileName: "Home.png" }, { slug: "home", fileName: "Home.png" },
{ slug: "transfers", fileName: "Transfers.png" }, { slug: "transfers", fileName: "Transfers.png" },
{ slug: "help", fileName: "Help.png" },
{ slug: "chat", fileName: "Chat.png" }, { slug: "chat", fileName: "Chat.png" },
]; ];

View file

@ -1,6 +1,12 @@
import type { SessionSnapshot } from "../shared/types"; 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 = { export type ScreenshotNavigationState = {
index: number; index: number;
@ -278,6 +284,7 @@ export function normalizeScreenshotScene(value: string | null | undefined): Scre
if (normalized === "lobby") return "lobby"; if (normalized === "lobby") return "lobby";
if (normalized === "home") return "home"; if (normalized === "home") return "home";
if (normalized === "transfers") return "transfers"; if (normalized === "transfers") return "transfers";
if (normalized === "help") return "help";
if (normalized === "chat") return "chat"; if (normalized === "chat") return "chat";
return null; 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 { return {
scene, scene,
sessionId: activeSessionId, sessionId: activeSessionId,

View file

@ -131,6 +131,22 @@ const translations = {
"home.balance": "Balance", "home.balance": "Balance",
"home.recent": "Recent operations", "home.recent": "Recent operations",
"home.noActivity": "No activity yet.", "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.title": "EMP",
"blackout.defaultReason": "EMP in effect", "blackout.defaultReason": "EMP in effect",
"blackout.active": "EMP active", "blackout.active": "EMP active",
@ -199,6 +215,7 @@ const translations = {
"chat.messagePlaceholder": "Message", "chat.messagePlaceholder": "Message",
"tabs.home": "Accounts", "tabs.home": "Accounts",
"tabs.transfers": "Payments", "tabs.transfers": "Payments",
"tabs.help": "Help",
"tabs.chat": "Messages", "tabs.chat": "Messages",
"tabs.dashboard": "Agency", "tabs.dashboard": "Agency",
"tabs.tools": "Control", "tabs.tools": "Control",
@ -343,6 +360,22 @@ const translations = {
"home.balance": "Solde", "home.balance": "Solde",
"home.recent": "Opérations récentes", "home.recent": "Opérations récentes",
"home.noActivity": "Aucune activité.", "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.title": "EMP",
"blackout.defaultReason": "EMP en cours", "blackout.defaultReason": "EMP en cours",
"blackout.active": "EMP actif", "blackout.active": "EMP actif",
@ -411,6 +444,7 @@ const translations = {
"chat.messagePlaceholder": "Message", "chat.messagePlaceholder": "Message",
"tabs.home": "Comptes", "tabs.home": "Comptes",
"tabs.transfers": "Paiements", "tabs.transfers": "Paiements",
"tabs.help": "Aide",
"tabs.chat": "Messages", "tabs.chat": "Messages",
"tabs.dashboard": "Agence", "tabs.dashboard": "Agence",
"tabs.tools": "Pilotage", "tabs.tools": "Pilotage",

View file

@ -9,6 +9,7 @@ import AgencyCreateScreen from "../screens/AgencyCreateScreen";
import LobbyScreen from "../screens/LobbyScreen"; import LobbyScreen from "../screens/LobbyScreen";
import PlayerHomeScreen from "../screens/PlayerHomeScreen"; import PlayerHomeScreen from "../screens/PlayerHomeScreen";
import PlayerTransfersScreen from "../screens/PlayerTransfersScreen"; import PlayerTransfersScreen from "../screens/PlayerTransfersScreen";
import PlayerHelpScreen from "../screens/PlayerHelpScreen";
import BankerDashboardScreen from "../screens/BankerDashboardScreen"; import BankerDashboardScreen from "../screens/BankerDashboardScreen";
import BankerToolsScreen from "../screens/BankerToolsScreen"; import BankerToolsScreen from "../screens/BankerToolsScreen";
import ChatListScreen from "../screens/chat/ChatListScreen"; import ChatListScreen from "../screens/chat/ChatListScreen";
@ -108,6 +109,17 @@ export function PlayerTabsNavigator() {
), ),
}} }}
/> />
<PlayerTabs.Screen
name="PlayerHelp"
component={PlayerHelpScreen}
options={{
title: t("tabs.help"),
headerTitle: buildHeaderTitle(t("tabs.help")),
tabBarIcon: ({ color, size }) => (
<Ionicons name="help-circle-outline" size={size} color={color} />
),
}}
/>
<PlayerTabs.Screen <PlayerTabs.Screen
name="PlayerChat" name="PlayerChat"
component={ChatStackNavigator} component={ChatStackNavigator}

View file

@ -16,6 +16,7 @@ export type ChatStackParamList = {
export type PlayerTabsParamList = { export type PlayerTabsParamList = {
PlayerHome: undefined; PlayerHome: undefined;
PlayerTransfers: undefined; PlayerTransfers: undefined;
PlayerHelp: undefined;
PlayerChat: undefined; PlayerChat: undefined;
}; };

View file

@ -0,0 +1,451 @@
import React, { useMemo } from "react";
import {
FlatList,
ScrollView,
StyleSheet,
Text,
View,
useWindowDimensions,
} from "react-native";
import {
getLocalizedText,
helpProperties,
helpVehicles,
helpWeapons,
type HelpLocale,
type HelpProperty,
type HelpPropertyColor,
type HelpVehicle,
type HelpWeapon,
} from "../../../shared/help-catalog";
import EmpOverlay from "../components/EmpOverlay";
import { useI18n } from "../i18n";
import { useSession } from "../state/session-context";
import { useTheme } from "../theme";
import type { AppTheme } from "../theme";
const PROPERTY_SWATCHES: Record<HelpPropertyColor, string> = {
brown: "#714826",
cyan: "#87d8d9",
pink: "#bc46ca",
green: "#5b933c",
red: "#ab3d31",
yellow: "#f8ff72",
orange: "#e9b054",
blue: "#0e08a2",
};
const PROPERTY_LIGHT_TEXT: Record<HelpPropertyColor, boolean> = {
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<typeof useI18n>["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 (
<View
style={[
styles.propertyCard,
{
width: cardWidth,
borderColor: swatchColor,
},
]}
>
<View style={styles.cardHeader}>
<View
style={[
styles.colorBadge,
{
backgroundColor: swatchColor,
},
]}
>
<Text style={[styles.colorBadgeText, { color: swatchTextColor }]}>
{property.color.toUpperCase()}
</Text>
</View>
<Text style={styles.propertyTitle}>{propertyName}</Text>
</View>
<View style={styles.statsList}>
{stats.map((stat) => (
<View key={`${property.id}-${stat.label}`} style={styles.statRow}>
<Text style={styles.statLabel}>{stat.label}</Text>
<Text style={styles.statValue}>{stat.value}</Text>
</View>
))}
</View>
</View>
);
}
function AssetCard({
item,
locale,
cardWidth,
theme,
}: {
item: HelpVehicle | HelpWeapon;
locale: HelpLocale;
cardWidth: number;
theme: AppTheme;
}) {
const styles = useMemo(() => createStyles(theme), [theme]);
return (
<View style={[styles.assetCard, { width: cardWidth }]}>
<View style={styles.cardHeader}>
<View style={styles.assetPills}>
<View style={styles.tierPill}>
<Text style={styles.tierPillText}>{getLocalizedText(item.tier, locale)}</Text>
</View>
<View style={styles.pricePill}>
<Text style={styles.pricePillText}>{getLocalizedText(item.price, locale)}</Text>
</View>
</View>
<Text style={styles.assetTitle}>{getLocalizedText(item.name, locale)}</Text>
</View>
<Text style={styles.assetBody}>{getLocalizedText(item.text, locale)}</Text>
</View>
);
}
function CarouselSection({
title,
hint,
styles,
children,
}: {
title: string;
hint: string;
styles: ReturnType<typeof createStyles>;
children: React.ReactNode;
}) {
return (
<View>
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>{title}</Text>
<Text style={styles.sectionHint}>{hint}</Text>
</View>
{children}
</View>
);
}
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 (
<View style={styles.loadingContainer}>
<Text style={styles.helper}>{t("common.loading")}</Text>
</View>
);
}
return (
<View style={styles.wrapper}>
<ScrollView
style={styles.scroll}
contentContainerStyle={styles.content}
showsVerticalScrollIndicator={false}
>
<View style={styles.heroCard}>
<Text style={styles.heroTitle}>{t("help.introTitle")}</Text>
<Text style={styles.heroBody}>{t("help.introBody")}</Text>
</View>
<CarouselSection
title={t("help.section.properties")}
hint={t("help.swipeHint")}
styles={styles}
>
<FlatList
data={helpProperties}
keyExtractor={(item) => item.id}
horizontal
nestedScrollEnabled
showsHorizontalScrollIndicator={false}
decelerationRate="fast"
snapToInterval={snapInterval}
snapToAlignment="start"
disableIntervalMomentum
contentContainerStyle={styles.carouselContent}
ItemSeparatorComponent={() => <View style={styles.carouselGap} />}
renderItem={({ item }) => (
<PropertyCard
property={item}
locale={locale}
cardWidth={cardWidth}
theme={theme}
t={t}
/>
)}
/>
</CarouselSection>
<CarouselSection
title={t("help.section.vehicles")}
hint={t("help.swipeHint")}
styles={styles}
>
<FlatList
data={helpVehicles}
keyExtractor={(item) => item.id}
horizontal
nestedScrollEnabled
showsHorizontalScrollIndicator={false}
decelerationRate="fast"
snapToInterval={snapInterval}
snapToAlignment="start"
disableIntervalMomentum
contentContainerStyle={styles.carouselContent}
ItemSeparatorComponent={() => <View style={styles.carouselGap} />}
renderItem={({ item }) => (
<AssetCard
item={item}
locale={locale}
cardWidth={cardWidth}
theme={theme}
/>
)}
/>
</CarouselSection>
<CarouselSection
title={t("help.section.weapons")}
hint={t("help.swipeHint")}
styles={styles}
>
<FlatList
data={helpWeapons}
keyExtractor={(item) => item.id}
horizontal
nestedScrollEnabled
showsHorizontalScrollIndicator={false}
decelerationRate="fast"
snapToInterval={snapInterval}
snapToAlignment="start"
disableIntervalMomentum
contentContainerStyle={styles.carouselContent}
ItemSeparatorComponent={() => <View style={styles.carouselGap} />}
renderItem={({ item }) => (
<AssetCard
item={item}
locale={locale}
cardWidth={cardWidth}
theme={theme}
/>
)}
/>
</CarouselSection>
</ScrollView>
<EmpOverlay visible={showEmp} reason={manager.session.blackoutReason} />
</View>
);
}
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,
},
});

View file

@ -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" },
});
});
});

539
shared/help-catalog.ts Normal file
View file

@ -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: 12 breakdown (no effect). 35 move forward 1 space. 6 explosion, move back 2 spaces.",
fr: "Lancer un dé : 12 panne (aucun effet). 35 avance d1 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: 12 breakdown. 35 move back 1 space. 6 explosion, move forward 2 spaces.",
fr: "Lancer un dé : 12 panne. 35 recule d1 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 d1 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 davancer 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 darrivée et de la case survolée. Si vous arrivez sur un joueur, il est envoyé à lHô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 davancer 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 nimporte 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: 14 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 : 14 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 sarrêtant. Toute charge peut être retirée par nimporte 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 quelle 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: 46 attacker wins, 13 attacker loses. Loser is extorted and sent to Hospital.",
fr: "Racket dun joueur sur la même case ou adjacente pour 300₦. Si la cible possède aussi une Glock, jet : 46 victoire de lattaquant, 13 défaite. Le perdant est racketté et envoyé à lHô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 leffet de nimporte 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 ; sil ny a pas de maison, la propriété est hypothéquée ; si elle lest 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 dartillerie" },
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 dune 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 lintégralité de NegoCity et donne la victoire immédiate au joueur qui lutilise.",
},
},
{
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.",
},
},
];