A production-grade, database-driven loot generation system for an indie multiplayer RPG
This is a comprehensive loot generation system built for Re:Armed, a loot-driven multiplayer game featuring robots with transformation abilities. The system demonstrates enterprise-level architecture with PostgreSQL database integration, materialized views for performance optimization, and a sophisticated procedural generation algorithm.
Key Technologies: C# (.NET 8), PostgreSQL, Npgsql, JSON-based configuration
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ JSON Config │────────▶│ PostgreSQL DB │────────▶│ C# Generator │
│ Files │ │ + Materialized │ │ (This Repo) │
│ (Data Design) │ │ Views │ │ │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ Generated │
│ Loot Items │
└─────────────────┘
-
Configuration Layer (JSON Files)
- Designer-friendly format for game balance
- Version controlled, easy to iterate
- Imported into PostgreSQL for runtime use
-
Database Layer (PostgreSQL)
- Normalized relational schema
- Materialized views for query optimization
- Single source of truth for all game data
-
Generation Layer (C# Application)
- Procedural generation algorithms
- Magic Find and rarity calculations
- Real-time item creation with database persistence
CREATE TABLE tiers (
code VARCHAR(2) PRIMARY KEY, -- T1, T2, T3, T4
name VARCHAR(50),
min_level INT, -- Level requirements
max_level INT,
base_potential INT, -- For crafting system
base_drop_weight INT -- Drop frequency
);Design Philosophy: Tier-based progression ensures players always have meaningful upgrades as they level. Each tier has distinct power ranges and level requirements.
CREATE TABLE rarities (
code VARCHAR(20) PRIMARY KEY, -- normal, magic, rare, epic, legendary
prefix_count INT, -- Number of prefix affixes
suffix_count INT, -- Number of suffix affixes
base_stat_multiplier DECIMAL, -- 1.0 to 1.4x
base_drop_weight INT, -- Rarity in drops
potential_bonus INT, -- Extra crafting potential
can_be_crafted BOOLEAN, -- Legendaries cannot be crafted
is_handcrafted BOOLEAN -- Unique items flag
);Rarity Progression:
- Normal: 0 affixes, baseline stats
- Magic: 1 prefix + 1 suffix, 1.1x stats
- Rare: 2 prefixes + 2 suffixes, 1.2x stats
- Epic: 3 prefixes + 2 suffixes, 1.3x stats
- Legendary: Fixed unique items, 1.4x stats, special powers
CREATE TABLE affixes (
code TEXT PRIMARY KEY,
name VARCHAR(100),
kind VARCHAR(10), -- 'prefix' or 'suffix'
value_type VARCHAR(10), -- 'flat' or 'percent'
base_weight INT, -- Base drop probability
tags TEXT[], -- ['offensive', 'defensive', etc.]
description TEXT
);
-- Tier-specific affix names
CREATE TABLE affix_epithets (
affix_code TEXT,
tier_code VARCHAR(2),
epithet TEXT, -- e.g., "Vital" (T1) vs "Immortal Core" (T4)
PRIMARY KEY (affix_code, tier_code)
);
-- Item-slot and tier-specific value ranges
CREATE TABLE affix_spawn_ranges (
affix_code TEXT,
item_slot TEXT, -- 'helmet', '1h_sword', etc.
tier_code VARCHAR(2),
min_value INT,
max_value INT,
PRIMARY KEY (affix_code, item_slot, tier_code)
);Design Highlight: Affixes have different value ranges based on both the item slot AND tier. A T4 helmet gets more HP than a T1 helmet, and body armor gets more HP than gloves at the same tier.
CREATE TABLE weapons (
id SERIAL PRIMARY KEY,
code TEXT UNIQUE,
name VARCHAR(100),
category VARCHAR(50),
weapon_class VARCHAR(10), -- '1h', '2h', 'ranged'
slot VARCHAR(50),
description TEXT
);
CREATE TABLE weapon_tiers (
weapon_id INT,
tier VARCHAR(2),
tier_name TEXT, -- Tier-specific visual name
PRIMARY KEY (weapon_id, tier)
);
CREATE TABLE weapon_base_stats (
weapon_id INT,
tier VARCHAR(2),
stat_name VARCHAR(50), -- 'ATK', 'DEF', etc.
stat_type VARCHAR(10), -- 'flat' or 'percent'
value DECIMAL,
PRIMARY KEY (weapon_id, tier, stat_name)
);Similar structure for armor pieces (helmet, shoulder, body, gloves, pants, boots)
CREATE TABLE legendary_items (
code TEXT PRIMARY KEY,
name TEXT,
category VARCHAR(50),
tier_code VARCHAR(2),
min_level INT,
base_type TEXT, -- Reference to base item type
slot VARCHAR(50),
legendary_power_name TEXT,
legendary_power_description TEXT,
legendary_power_proc_chance INT,
legendary_power_cooldown INT,
legendary_power_effect_data JSONB
);
CREATE TABLE legendary_item_fixed_affixes (
legendary_item_code TEXT,
affix_code TEXT,
value DECIMAL, -- Fixed value, not random
display_text TEXT
);Design Philosophy: Legendaries are hand-crafted unique items with fixed stats and special powers, not procedurally generated.
The naive approach would query 10+ tables with multiple joins for every item generation:
-- BAD: Slow query every time
SELECT * FROM weapons
JOIN weapon_tiers ON ...
JOIN weapon_base_stats ON ...
JOIN weapon_allowed_affixes ON ...
JOIN affixes ON ...
JOIN affix_epithets ON ...
JOIN affix_spawn_ranges ON ...Instead, we pre-compute and cache these complex joins:
CREATE MATERIALIZED VIEW mv_weapon_relations AS
SELECT
w.code AS weapon_code,
w.name AS weapon_name,
wt.tier AS tier_code,
wbs.stat_name,
wbs.value AS base_stat_value,
a.code AS affix_code,
a.name AS affix_name,
ae.epithet AS affix_epithet_for_tier,
asr.min_value AS spawn_min_value,
asr.max_value AS spawn_max_value,
-- ... and 15+ more columns
FROM weapons w
LEFT JOIN weapon_tiers wt ON ...
LEFT JOIN weapon_base_stats wbs ON ...
LEFT JOIN weapon_allowed_affixes waa ON ...
LEFT JOIN affixes a ON ...
LEFT JOIN affix_epithets ae ON ...
LEFT JOIN affix_spawn_ranges asr ON ...
WITH NO DATA;
-- Refresh when data changes
REFRESH MATERIALIZED VIEW mv_weapon_relations;Performance Gain:
- Before: ~50-100ms per item (10+ joins)
- After: ~2-5ms per item (single materialized view query)
- 10-20x performance improvement for bulk generation
mv_weapon_relations- All weapon data with affixes and rangesmv_armor_relations- All armor data with affixes and rangesvw_legendary_items_full- Complete legendary item definitionsmv_tiers_optimized- Tier data with drop calculationsmv_rarities_optimized- Rarity data with probabilitiesmv_affixes_optimized- Complete affix definitions
1. Determine Tier (based on player level)
├─ Player level 1-10 → T1
├─ Player level 11-25 → T2
├─ Player level 26-40 → T3
└─ Player level 41+ → T4
2. Roll for Rarity (weighted random)
├─ Apply Magic Find multiplier
├─ Apply Monster Level scaling
├─ Apply Map Level scaling
└─ Select: Normal/Magic/Rare/Epic/Legendary
3. Select Base Item (from category)
└─ Filter by tier eligibility
4. IF Legendary
├─ Load fixed base stats
├─ Load fixed affixes
└─ Load unique power
ELSE
├─ Roll base stats from tier ranges
├─ Roll N prefixes (based on rarity)
├─ Roll N suffixes (based on rarity)
└─ Calculate final stats
5. Generate Display Name
└─ Format: "{prefix epithets} {base name} {suffix epithets}"
6. Calculate Potential
└─ potential = tier_base_potential + rarity_potential_bonus
double CalculateFinalWeight(Rarity rarity, int playerLevel, int monsterLevel,
int mapLevel, int magicFind)
{
double baseWeight = rarity.BaseDropWeight;
// Zone level scaling (+2% per level)
double zoneBonus = 1 + (mapLevel * 0.02);
// Monster level scaling (+5% per level above player)
int levelDiff = Math.Max(0, monsterLevel - playerLevel);
double monsterBonus = 1 + (levelDiff * 0.05);
// Magic Find scaling (direct percentage)
double mfBonus = 1 + (magicFind / 100.0);
return baseWeight * zoneBonus * monsterBonus * mfBonus;
}Example:
- T3 Rare helmet drop
- Player level 30, Monster level 35, Map level 25, 50% Magic Find
- Rare base weight: 30
- Zone bonus: 1 + (25 * 0.02) = 1.5
- Monster bonus: 1 + (5 * 0.05) = 1.25
- MF bonus: 1 + 0.5 = 1.5
- Final weight: 30 × 1.5 × 1.25 × 1.5 = 84.375
RolledAffix PickAffix(BaseItem item, string kind, string tier, Random rng)
{
// 1. Get allowed affixes for this item
var allowedAffixes = item.AllowedAffixes
.Where(code => affixes[code].Kind == kind);
// 2. Calculate effective weights
var weights = new List<double>();
foreach (var affix in allowedAffixes)
{
double weight = affix.BaseWeight;
// Apply item-specific weight multipliers
if (item.AffixWeightMultipliers?.ContainsKey(affix.Code) == true)
weight *= item.AffixWeightMultipliers[affix.Code];
weights.Add(weight);
}
// 3. Weighted random selection
var selectedAffix = WeightedChoice(allowedAffixes, weights, rng);
// 4. Roll value within tier-specific range
var range = selectedAffix.SpawnRanges[item.Slot][tier];
double value = RandomInRange(range.Min, range.Max, rng);
// 5. Get tier-specific epithet (flavor name)
string epithet = selectedAffix.EpithetsByTier[tier];
return new RolledAffix(selectedAffix, value, epithet);
}Design Highlight: Items can boost specific affix weights. For example, boots have a 2.0x multiplier for the "speed" affix, making speed boots more common than speed gloves.
Every item has a "Potential" value used for crafting:
Potential = Tier Base Potential + Rarity Bonus
Examples:
- T1 Normal: 20 + 0 = 20 potential
- T3 Rare: 60 + 20 = 80 potential
- T4 Epic: 80 + 30 = 110 potential
- T4 Legendary: 0 potential (cannot be crafted)
Crafting Operations:
- Remove affix: 10 potential
- Add random affix: 15 potential
- Reroll affix value: 5 potential
- Upgrade affix tier (T2→T3): 25 potential
Items have different visual names per tier:
{
"1h_sword": {
"tier_names": {
"T1": "Plasma Cutter",
"T2": "Photon Blade",
"T3": "Quantum Edge",
"T4": "Void Katana"
}
}
}A T4 Epic sword might be named:
"Quantum Surge Void Katana of the Collapsar"
- "Quantum Surge" = prefix epithet
- "Void Katana" = T4 tier name
- "of the Collapsar" = suffix epithet
Stats scale differently based on item slot:
| Affix | Body (T4) | Helmet (T4) | Gloves (T4) |
|---|---|---|---|
| HP | 50-100 | 25-50 | 13-25 |
| Crit | N/A | 8% | 12% |
| Speed | N/A | 5-10 | 8-15 |
Why? Balance! Body armor should give more HP but can't roll offensive stats. Gloves give less HP but better offensive rolls.
Legendary items have special effects stored as JSONB:
{
"name": "Void Rift",
"description": "Attacks create void rifts...",
"proc_chance": 25,
"cooldown": 0,
"effect_data": {
"damage": 75,
"duration": 3,
"pull_radius": 8,
"max_stacks": 3
}
}This allows game code to read and implement unique mechanics per legendary.
The system uses a two-step process:
-
Design Phase: Game designers edit JSON files
// armor.json { "helmet": { "code": "helmet", "base_stats": { "HP": { "T1": 50, "T2": 75, "T3": 100, "T4": 125 } } } }
-
Import Phase: Python/C# script normalizes and imports
# Pseudo-code for import for item in armor_data: insert_into_armors(item.code, item.name, ...) for tier, tier_name in item.tier_names: insert_into_armor_tiers(item.id, tier, tier_name) for stat_name, stat_values in item.base_stats: for tier, value in stat_values.tiers: insert_into_armor_base_stats(item.id, tier, stat_name, value)
-
Refresh Materialized Views:
REFRESH MATERIALIZED VIEW mv_armor_relations; REFRESH MATERIALIZED VIEW mv_weapon_relations;
// Load from .env file (NOT committed to git)
Env.Load();
string BuildConnectionString()
{
var host = Environment.GetEnvironmentVariable("DB_HOST");
var port = Environment.GetEnvironmentVariable("DB_PORT");
var database = Environment.GetEnvironmentVariable("DB_NAME");
var username = Environment.GetEnvironmentVariable("DB_USER");
var password = Environment.GetEnvironmentVariable("DB_PASSWORD");
return $"Host={host};Port={port};Database={database};" +
$"Username={username};Password={password}";
}Security: No hardcoded credentials. Uses .env file (gitignored).
static List<WeaponDef> LoadWeapons(string connectionString)
{
var weapons = new Dictionary<string, WeaponDef>();
using var conn = new NpgsqlConnection(connectionString);
conn.Open();
// Single query to materialized view (fast!)
var cmd = new NpgsqlCommand(
"SELECT * FROM mv_weapon_relations", conn);
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
string weaponCode = reader.GetString("weapon_code");
string tier = reader.GetString("tier_code");
// Group data by weapon+tier
if (!weapons.ContainsKey(weaponCode))
weapons[weaponCode] = new WeaponDef { Code = weaponCode };
// Build up the weapon definition from flattened rows
// ... (see Program.cs for full implementation)
}
return weapons.Values.ToList();
}Generated items can be saved back to the database:
static void SaveItemToDatabase(RolledItem item, string username,
string characterName, ...)
{
var statsJson = JsonSerializer.Serialize(item.FinalStats);
var affixesJson = JsonSerializer.Serialize(item.Affixes);
var cmd = new NpgsqlCommand(@"
INSERT INTO generated_items
(username, character_name, display_name, tier_code,
rarity_code, stats, affixes, ...)
VALUES
(@username, @character_name, @display_name, @tier_code,
@rarity_code, @stats::jsonb, @affixes::jsonb, ...)
RETURNING id, item_uuid", conn);
// ... parameter binding
var itemId = cmd.ExecuteScalar();
}This creates a persistent record of every dropped item with:
- Who got it (username, character)
- What it is (display name, base type, tier, rarity)
- Its stats (JSONB for flexible querying)
- Generation context (player level, magic find, monster level)
| Operation | Without Materialized Views | With Materialized Views |
|---|---|---|
| Load 1 weapon | ~50ms | ~2ms |
| Load all weapons (10) | ~500ms | ~20ms |
| Load all armor (6) | ~300ms | ~12ms |
| Load all affixes (25) | ~1000ms | ~40ms |
| Generate 1 item | ~150ms | ~5ms |
| Generate 100 items | ~15s | ~500ms |
- Loaded data (all definitions): ~5-10 MB
- Per generated item: ~1-2 KB
- Database size: ~500 KB (configuration data)
- Generated items table: ~2 KB per row
The system can easily handle:
- ✅ 1000s of items generated per second
- ✅ 100,000s of items stored in database
- ✅ Real-time item generation during gameplay
- ✅ Concurrent multi-player drop generation
Decision: PostgreSQL with materialized views
Why:
- ✅ Strong relational constraints (no invalid items)
- ✅ Complex queries for analytics (top items, drop rates)
- ✅ ACID compliance for trading/economy
- ✅ Materialized views solve performance concerns
- ❌ Requires schema migrations (but worth it)
Alternative: MongoDB would be easier to iterate but harder to query and validate.
Decision: JSON files → import script → PostgreSQL
Why:
- ✅ Version control friendly (Git diffs)
- ✅ Non-technical designers can edit
- ✅ Easy rollback to previous versions
- ✅ Can be exported and shared
- ❌ Requires import step (but automated)
Decision: Materialized (pre-computed) views
Why:
- ✅ 10-20x performance improvement
- ✅ Data rarely changes (game balance patches)
- ✅ Refresh can be manual or scheduled
- ❌ Need to remember to refresh after data changes
Decision: Seeded Random for testability
var rng = new Random(seed); // Can reproduce exact itemWhy:
- ✅ Can reproduce bugs ("Item #12345 is broken")
- ✅ Can create seasonal/event items deterministically
- ✅ Can preview items before committing
- ❌ More complex than
Random()
Decision: Discrete tiers (T1-T4) not continuous level scaling
Why:
- ✅ Clear upgrade moments (going from T3 to T4 feels great)
- ✅ Easier to balance (4 tiers vs 50 levels)
- ✅ Reduces item bloat (T3 always better than T2)
- ❌ Less granular progression
-
Set Items (Multiple items with bonuses)
CREATE TABLE item_sets ( set_code TEXT PRIMARY KEY, bonus_2_pieces TEXT, bonus_4_pieces TEXT, bonus_6_pieces TEXT );
-
Item Socketing (Gem/Chip slots)
- Add
socket_countto items - Create
socketed_gemstable - Bonus stats from slotted items
- Add
-
Corrupted Items (High risk/reward)
- 50% chance to improve/destroy item
- Can exceed tier limits
- Cannot be crafted after corruption
-
Smart Loot (Class-aware drops)
- Track player class
- Weight affixes by class preference
- E.g., Tank gets more DEF affixes
-
Trade System Integration
- Track item provenance
- Bind-on-equip system
- Trade history logging
Database Optimization:
- Use The Index, Luke! - SQL performance
- PostgreSQL Materialized Views
Game Design:
- Diablo 2 - The gold standard for item systems
- Path of Exile - Advanced crafting mechanics
- Game Developer Conference - Procedural Generation
C# / .NET:
- Npgsql Documentation - PostgreSQL for .NET
- Entity Framework Core - Alternative ORM approach
This project showcases:
- ✅ Complex database schema design
- ✅ SQL query optimization (materialized views)
- ✅ C# / .NET application architecture
- ✅ Environment-based configuration
- ✅ Data serialization (JSON, JSONB)
- ✅ Procedural content generation
- ✅ Weighted random algorithms
- ✅ Game balance systems design
- ✅ Stat scaling and progression curves
- ✅ Separation of concerns (data/logic/presentation)
- ✅ Performance optimization
- ✅ Security best practices (no hardcoded credentials)
- ✅ Documentation and code clarity
- ✅ Testable, reproducible systems
- ✅ Normalization (avoiding data duplication)
- ✅ Foreign key constraints
- ✅ Query performance tuning
- ✅ Data migration strategies
- ✅ JSONB for flexible data
For Hiring Managers / Recruiters:
This system demonstrates my ability to:
- Design and implement complex backend systems
- Optimize database queries for performance
- Write clean, maintainable, well-documented code
- Balance technical constraints with game design goals
Technical Deep Dives Available:
- Detailed walkthrough of the generation algorithm
- Database schema evolution story
- Performance testing methodology
- Alternative approaches considered
This project is part of the Re:Armed game development portfolio.
For job search / portfolio purposes only. Not licensed for commercial use by third parties.
Built with ❤️ for Re:Armed Showcasing production-ready game backend development
