A comprehensive gang management tool for Necromunda tabletop game with features like:
- 🎮 Interactive gang management
- 👥 Fighter roster tracking
- 💰 Resource management
- ⚔️ Equipment and weapons system
- 📈 Experience and advancement tracking
- 📋 Comprehensive activity logging
- Framework: Next.js 15.4.3 (App Router)
- Database: PostgreSQL
- Authentication: Supabase Auth
- Styling: Tailwind CSS
- Components: shadcn/ui
- Type Safety: TypeScript
For bug reports, feature requests, or help, please join our Discord Community.
If you enjoy using Munda Manager, consider:
- Supporting us on Patreon
- Buying us a coffee at Buy Me a Coffee
For questions about contributing, feel free to ask in our Discord server.
- Clone the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Added some amazing feature') - Push to the branch (
git push -u origin HEAD) - Open a Pull Request
-
Prerequisites
- Node.js 18+
- Supabase project url and key
- Cloudflare Turnstile keys
-
Environment Setup
cp .env.example .env.local
Configure the following variables:
NEXT_PUBLIC_SUPABASE_URL=mundamanager-project-url NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key NEXT_PUBLIC_TURNSTILE_SITE_KEY=mundamanager-turnstile-key TURNSTILE_SECRET_KEY=mundamanager-turnstile-secret-key NODE_ENV=development -
Running the environment
npm install npm run dev
-
Mobile device testing On your desktop, make sure you have the NODE_ENV variable setup with the value 'development'. Then run the following:
npm run dev --host
On your mobile device, when connected to your wifi, access the website using the IP of your desktop on your local network (e.g. 192.168.1.5): http://192.168.1.5:3000
-
Access to the DB Schema The Supabase DB schema can be accessed through https://supabase-schema.vercel.app/ Use the Supabase URL and the and the anon key to connect to it
GangPage (Server Component)
└── GangPageContent (Client Component)
└── Gang
├── MyFighters
│ └── FighterCard
│ ├── StatsTable
│ └── WeaponTable
└── GangStashModal
GangPageContent: Main wrapper for gang managementGang: Core gang management componentGangStashModal: Equipment stash management
MyFighters: Fighter roster displayFighterCard: Individual fighter displayWeaponTable: Equipment and weapons displayFighterStatsTable: Stats display component
- Comprehensive error boundaries
- Loading states
- Fallback UI components
- Type-safe error handling
- Context-based gang state (
GangsContext) - Local component state for UI
- Optimistic updates
- Real-time data synchronization
- Server-side data fetching (RPC calls)
- Data processing and transformation
- Client-side state management
- Real-time updates
- Optimistic UI updates
- Type safety throughout
- Component memoization
- Error boundary implementation
- Hydration-safe rendering
- Proper loading states
- Create and manage multiple gangs
- Track gang resources:
- Credits
- Reputation
- Meat
- Exploration points
- Manage gang alignment (Law Abiding/Outlaw)
- Equipment stash system
- Campaign integration
- Comprehensive fighter management:
- Stats tracking (M, WS, BS, S, T, W, I, A, Ld, Cl, Wil, Int)
- Experience and advancements
- Equipment and weapons
- Skills and special rules
- Status tracking (killed, retired, enslaved, starved)
- Automatic stat calculations
- Equipment transfer system
The fighter effects system manages all modifications to fighter statistics through a unified interface. Effects can come from various sources:
- Injuries
- Advancements
- Bionics
- Cyberteknika
- Gene-smithing
- Rig-glitches
- Power-boosts (Spyrer enhancements)
- Augmentations
- Equipment
- Skills
- Vehicle Lasting Damages
- User modifications
// Core effect interface
interface FighterEffect {
id?: string;
effect_name: string;
fighter_effect_modifiers: Array<{
id: string;
fighter_effect_id: string;
stat_name: string;
numeric_value: number;
}>;
}
// Fighter effects structure
interface Fighter {
effects: {
injuries: FighterEffect[];
advancements: FighterEffect[];
bionics: FighterEffect[];
cyberteknika: FighterEffect[];
'gene-smithing': FighterEffect[];
'rig-glitches': FighterEffect[];
'power-boosts': FighterEffect[];
augmentations: FighterEffect[];
equipment: FighterEffect[];
skills: FighterEffect[];
vehicle_damages: FighterEffect[];
user: FighterEffect[];
}
}-
Effect Categories
- Each effect belongs to a specific category (injury, advancement, vehicle damage, etc.)
- Categories are stored in the
fighter_effect_categoriestable - Each category can have different business rules and UI treatments
-
Stat Modifications
- Effects modify fighter stats through
fighter_effect_modifiers - Each modifier specifies:
- Which stat to modify (
stat_name) - How much to modify it by (
numeric_value) - Reference to its parent effect (
fighter_effect_id)
- Which stat to modify (
- Effects modify fighter stats through
-
Stat Calculation
function calculateAdjustedStats(fighter: Fighter) { // Start with base stats const adjustedStats = { ...fighter.base_stats }; // Process all effect categories ['injuries', 'advancements', 'bionics', 'cyberteknika', 'gene-smithing', 'rig-glitches', 'power-boosts', 'augmentations', 'equipment', 'skills', 'vehicle_damages', 'user'].forEach(category => { fighter.effects[category]?.forEach(effect => { effect.fighter_effect_modifiers?.forEach(modifier => { const statName = modifier.stat_name.toLowerCase(); adjustedStats[statName] += modifier.numeric_value; }); }); }); return adjustedStats; }
-
Database Schema
-- Effect categories CREATE TABLE fighter_effect_categories ( id UUID PRIMARY KEY, category_name TEXT NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ ); -- Effects CREATE TABLE fighter_effects ( id UUID PRIMARY KEY, fighter_id UUID REFERENCES fighters(id), vehicle_id UUID REFERENCES vehicles(id), effect_name TEXT NOT NULL, category_id UUID REFERENCES fighter_effect_categories(id), created_at TIMESTAMPTZ DEFAULT NOW() ); -- Effect modifiers CREATE TABLE fighter_effect_modifiers ( id UUID PRIMARY KEY, fighter_effect_id UUID REFERENCES fighter_effects(id), stat_name TEXT NOT NULL, numeric_value INTEGER NOT NULL );
The fighter effects system is implemented across several key files:
utils/effect-modifiers.ts: Core logic for applying effect modifiers to fighter statscalculateAdjustedStats(): Main function that processes all effect categoriesapplyWeaponModifiers(): Applies equipment-to-equipment effects on weapon profiles- Supports both 'add' and 'set' operations for stat modifications
app/lib/shared/fighter-data.ts: Server-side functions for fetching fighter datagetFighterEffects(): Queries and groups effects by category from database- Returns effects organized by category:
{ injuries: [], equipment: [], skills: [], ... } - Uses caching with proper cache tags for performance
types/fighter.ts: TypeScript interfaces for fighter dataFighterEffect: Interface for individual effectsFighterProps.effects: Complete effects structure with all categoriesEffectCategory: Union type of all valid effect categories
-
components/fighter/fighter-details-card.tsx: Displays fighter stats with effects applied on the fighter detail page- Receives effects from parent via props
- Constructs
fighterDataobject with all effect categories (line 284-294) - Calls
calculateAdjustedStats()to compute modified stats (line 344-347) - IMPORTANT: Must include all effect categories in the
fighterData.effectsobject
-
components/gang/fighter-card.tsx: Displays fighter stats with effects applied on the gang roster- Used in gang page to show fighter cards in the roster
- Constructs
fighterDataobject with all effect categories (line 249-260) - Calls
calculateAdjustedStats()to compute modified stats (line 296) - IMPORTANT: Must include all effect categories in the
fighterData.effectsobject (same as fighter-details-card)
-
components/fighter/fighter-page.tsx: Main fighter page component- Fetches initial fighter data including effects
- Transforms and passes effects to child components (line 215-226)
- Manages fighter state and updates
-
app/actions/equipment.ts: Handles equipment purchases and associated effectsbuyEquipmentForFighter(): Automatically creates fighter_effects when equipment with effects is purchased- Queries
fighter_effect_typesto find effects associated with equipment - Creates both
fighter_effectsandfighter_effect_modifiersrecords
-
app/actions/fighter-advancement.ts: Handles skill advancements and associated effectsaddSkillAdvancement(): Automatically creates fighter_effects when skills with effects are added (line 325-373)- Queries
fighter_effect_typeswheretype_specific_data->>'skill_id'matches - Creates both
fighter_effectsandfighter_effect_modifiersrecords - Invalidates caches to trigger re-rendering
-
Adding an Injury
const injury: FighterEffect = { effect_name: "Head Wound", fighter_effect_modifiers: [{ stat_name: "ballistic_skill", numeric_value: -1 }] }; fighter.effects.injuries.push(injury);
-
Adding a Bionic Enhancement
const bionic: FighterEffect = { effect_name: "Bionic Arm", fighter_effect_modifiers: [{ stat_name: "strength", numeric_value: 1 }] }; fighter.effects.bionics.push(bionic);
-
Adding Equipment Effects
const equipmentEffect: FighterEffect = { effect_name: "Psychomancer's harness", fighter_effect_modifiers: [ { stat_name: "movement", numeric_value: 2 } ] }; fighter.effects.equipment.push(equipmentEffect);
-
Adding Skill Effects
const skillEffect: FighterEffect = { effect_name: "Extra Appendages", fighter_effect_modifiers: [ { stat_name: "attacks", numeric_value: 1 } ] }; fighter.effects.skills.push(skillEffect);
-
Adding a Power Boost (Spyrer)
const powerBoost: FighterEffect = { effect_name: "Improved Motive Power", fighter_effect_modifiers: [{ stat_name: "movement", numeric_value: 1 }], type_specific_data: { kill_cost: 4, credits_increase: 10 } }; fighter.effects['power-boosts'].push(powerBoost);
-
Adding a Vehicle Lasting Damage
const vehicleDamage: FighterEffect = { effect_name: "Loss of Power", vehicle_id: "vehicle-uuid", fighter_effect_modifiers: [{ stat_name: "movement", numeric_value: -1 }] }; fighter.effects.vehicle_damages.push(vehicleDamage);
-
User Modification
const userMod: FighterEffect = { effect_name: "Custom Bonus", fighter_effect_modifiers: [{ stat_name: "movement", numeric_value: 1 }] }; fighter.effects.user.push(userMod);
The gang logging system provides comprehensive tracking of all changes and activities within your gang. Every action is automatically logged with timestamps, creating a complete audit trail of your gang's history.
- Automatic Logging: All gang activities are tracked automatically through database triggers
- Comprehensive Coverage: Logs credits, reputation, fighters, equipment, vehicles, and more
- Detailed Descriptions: Human-readable log entries with before/after values
- Real-time Updates: Logs appear immediately after actions are performed
- Paginated Display: Clean interface with 10 logs per page for easy browsing
- Credits: "Credits increased from 500 to 600" or "Credits decreased from 600 to 500"
- Reputation: "Reputation changed from 5 to 10"
- Resources: Meat and exploration points changes
- Gang Type: Gang alignment and type modifications
- Fighter Management: "Added fighter 'Juve' (65 credits). New gang rating: 365"
- Fighter Removal: "Removed fighter 'Ganger' (95 credits). New gang rating: 270"
- Status Changes: Fighter deaths, retirements, enslavement with context
- Cost Adjustments: Manual fighter cost modifications
- Experience & Kills: XP gains and kill count changes
- Purchases: "Fighter 'Ganger' bought Lasgun for 15 credits. New gang rating: 280"
- Sales: "Fighter 'Heavy' sold Plasma gun for 100 credits. New gang rating: 380"
- Stash Operations:
- "Fighter moved Heavy bolter to gang stash. New gang rating: 265"
- "Fighter took Plasma gun from gang stash. New gang rating: 365"
- Vehicle Management: "Added vehicle 'Cargo-8 Ridgehauler' (130 credits). New gang rating: 495"
- Vehicle Equipment: "Vehicle 'Ridgehauler' bought Heavy bolter for 160 credits. New gang rating: 655"
- Vehicle Modifications: Upgrades, repairs, and customizations
The logging system uses PostgreSQL triggers that fire automatically on data changes:
-- Gang changes trigger
CREATE TRIGGER gang_changes_trigger
AFTER UPDATE ON gangs
FOR EACH ROW
EXECUTE FUNCTION auto_log_gang_changes();
-- Fighter changes trigger
CREATE TRIGGER fighter_changes_trigger
AFTER INSERT OR UPDATE OR DELETE ON fighters
FOR EACH ROW
EXECUTE FUNCTION fighter_logs();The system prevents duplicate logging by checking for recent related activities:
- Credit decreases from equipment purchases don't create separate credit logs
- Fighter additions don't duplicate credit change logs
- Equipment stash operations are distinguished from regular sales/purchases
interface GangLog {
id: string;
gang_id: string;
user_id: string;
action_type: string;
description: string;
fighter_id?: string;
vehicle_id?: string;
created_at: string;
}- Modal Display: Logs open in a responsive modal dialog
- Table Format: Clean 3-column layout (Date, Type, Description)
- Pagination: Navigate through logs with page controls
- Responsive Design: Optimized for both desktop and mobile viewing
- Real-time Updates: New logs appear immediately without page refresh
Gang logs are accessible via the "Logs" button on each gang page, positioned next to the Edit button. The logs are private to the gang owner and provide a complete history of all gang activities.
The notification system provides real-time notifications to users for various application events. Notifications support different types (info, warning, error, invite) with appropriate visual indicators.
- Real-time notifications using Supabase Realtime
- Different notification types with distinct visual styling
- Automatic marking of notifications as read when viewed
- Notification deletion capability
- Database-driven notification storage
// Core notification interface
interface Notification {
id: string;
text: string;
type: 'info' | 'warning' | 'error' | 'invite';
created_at: string;
dismissed: boolean;
link: string | null;
}- Notifications are stored in the
notificationstable - Database triggers automatically create notifications for specific events
- Example functions:
notify_campaign_member_added(): Creates notifications when users are invited to campaigns
- Example triggers:
trigger_campaign_member_notification: Fires the notification function when new campaign members are added
- Global notification store manages application-wide notification state
- Notification hooks provide real-time updates and management functions
- Notification API endpoints for secure server-side operations
- Profile page integration for reviewing all notifications
// Core Data Types
interface GangData {
id: string
name: string
credits: number
reputation: number
alignment: 'Law Abiding' | 'Outlaw'
meat: number
exploration_points: number
fighters: FighterProps[]
stash: StashItem[]
}
interface FighterProps {
id: string
fighter_name: string
fighter_type: string
fighter_class: string
credits: number
// Stats and equipment
movement: number
weapon_skill: number
ballistic_skill: number
// ... other stats
weapons: Weapon[]
wargear: Wargear[]
advancements: Advancement
}