
Rogue of Gems
Ludum Dare 58 Game Jam 2025, 2D top-down roguelike game Team Project (2 people)
Rogue of Gems (Ludum Dare 58 Game Jam 2025, Theme: Collector), a 2D top-down roguelike game where you fight randomly generated monsters on a procedurally generated map, search for treasure chests, collect different types of magical gems, and ultimately defeat the boss to bring those gems back to your collection cabinet.
Link to Web Play:
Link to Game Jam:
https://ldjam.com/events/ludum-dare/58/rogue-of-gems
Team & Responsibilities:
Weiqi Liu — Lead & Sole Programmer
Jiawei Zhang — Artist
Develop Tools:
C#, Unity
🎬 Gameplay Video
🌌 Background Story
Long ago, there was a mysterious figure known only as The Collector —a wanderer obsessed with the magical gems scattered across forgotten realms.Each gem held a unique power — fire that burns through darkness, ice that freezes time, lightning that splits the sky.
The Collector dreamed of mastering them all.But one day, he vanished deep within the ruins, leaving behind only his Exhibition Hall,a place filled with empty glass cases waiting to be filled again.
Now, a new adventurer (you) steps into his footsteps.Guided by curiosity and greed alike, you venture into the ever-shifting dungeons,seeking the same gems that once drove The Collector mad.
🔮 Gameplay Description
Every run begins at the Exhibition Hall,where you can view the magical gems you’ve collected so far — your growing legacy.Before each new expedition, you may choose one collected gem to carry with you.Each gem grants a different magical ability — maybe a fire blast, a healing aura, or a lightning dash.
Then, you descend into a procedurally generated world filled with monsters, traps, and treasure chests.Your goal:
Find treasure chests hidden throughout the dungeon.
Collect new magical gems that grant powerful abilities.
Fight through random encounters and mini-bosses.
Finally, defeat the main Boss guarding the dungeon’s core gem.
When you return victorious, you place your newly acquired gem into the Exhibition Hall. It becomes a permanent trophy,and also a new power you can select for your next adventure.
The more you collect, the stronger and more creative your future runs become —but beware: every journey resets,and death means losing everything except your collected gems.
🔁 Game Flow

Each room is procedurally generated, with an algorithm ensuring there are no obstructions to the portals to the next room, and the entire room corresponds to a minimap.

Branching room network, green for current room, yellow for treasure room, red for boss room. You can know whether the rooms you connect to next are treasure room.

You will receive a boost every time you enter a new room.

Treasure chests may drop random gems (weapons).

Go to the next room

Weapons with enhanced power have high knockback and recoil.

Boss charge attack

Boss vertical railgun attack

The boss launches missile attacks in rounds.

The boss launched a series of missile attacks.

The boss generates obstacles.

Try to collect all the gems in the Exhibition Hall!

💡Design Innovation: Slot-Based Enhancement System
Problem:
Traditional roguelike enhancement systems require:
Gem/rune decomposition mechanics
Enhancement transfer UI between weapons
Complex weapon upgrade trees
~1000+ LOC overhead
Challenge: 3-day game jam timeline
Implementation
csharp
// Enhancements persist in slots, not weapons
private EnhancementData leftHandEnhancement;
// Survives weapon swaps
private EnhancementData rightHandEnhancement;
// Applied dynamically when firing
public override void Use(Vector3 direction, Vector3 origin)
{
EnhancementData e = EnhancementManager.Instance.GetEnhancement(slotIndex);
ApplyEnhancementToBullet(bullet, e); // Real-time application
}
Solution:
Enhancements attach to hand slots, not weapons
Traditional: [Weapon A + Enhancement X] → Swap → [Weapon B] (loses Enhancement X)
Our Design: [Left Slot: Enhancement X] + [Any Weapon] → Swap → [New Weapon] (keeps Enhancement X)Benefits
✅ Frictionless weapon swapping
Replace weapons without losing power progression
Match weapon types to slot specialization (e.g., shotgun → AoE slot, sniper → single-target slot)
✅ Eliminated decomposition subsystem
No gem extraction mechanics needed
Saved ~500 LOC development time
✅ Deep build diversity
9 enhancement types with dual-hand selection per room
5+ rooms per round × 3 rounds = 30+ enhancement decisions
Specialized builds: Left hand (Multi-shot + Bounce + Explosion) for mob clear, Right hand (Damage + Fire Rate) for boss DPS
Weapon flexibility: Swap shotgun to AoE-enhanced left hand, swap rifle to single-target right hand
✅ Minimal implementation
Complete system in ~200 LOC
New enhancement = 1 switch case
Result: Production-ready roguelike lite with rich endgame build diversity, achieved under extreme time constraint through architectural innovation rather than feature cutting.
⚙️ Technical Showcase
1. Weapon System
Dual-wielding equipment with interface-driven architecture(csharp)
// IEquippable.cspublic interface IEquippable
{
string EquipmentName { get; }
Sprite Icon { get; }
void Use(Vector3 direction, Vector3 origin);
bool CanUse();
}
// EquipmentManager.cspublic class EquipmentManager : MonoBehaviour
{
private IEquippable leftHandEquipment;
private IEquippable rightHandEquipment;
private GameObject leftHandPrefab; // Store for swap/drop
private GameObject rightHandPrefab;
public void EquipToSlot(GameObject equipmentPrefab, int slot)
{
GameObject equipmentObj = Instantiate(equipmentPrefab, firePoint);
IEquippable equipment = equipmentObj.GetComponent<IEquippable>();
if (slot == 0) {
leftHandEquipment = equipment;
leftHandPrefab = equipmentPrefab;
} else {
rightHandEquipment = equipment;
rightHandPrefab = equipmentPrefab;
}
}
}
2. Enhancement System
Slot-based enhancement with runtime bullet modification(csharp)
// EnhancementData.cs
[System.Serializable]
public class EnhancementData
{
public float damageMultiplier = 1f;
public int bonusBounces = 0;
public float fireRateMultiplier = 1f;
public int bulletsPerShotBonus = 0;
public float slowMultiplierBonus = 0f;
public float bulletSpeedMultiplier = 1f;
public bool enableHoming = false;
public bool enableExplosion = false;
}
// EnhancementManager.cspublic class EnhancementManager : MonoBehaviour
{
private EnhancementData leftHandEnhancement;
private EnhancementData rightHandEnhancement;
public void AddWeaponEnhancement(int slot, string type, float value)
{
EnhancementData e = GetEnhancement(slot);
switch (type)
{
case "Damage": e.damageMultiplier *= value; break;
case "FireRate": e.fireRateMultiplier *= value; break;
case "BulletsPerShot": e.bulletsPerShotBonus += (int)value; break;
case "Bounce": e.bonusBounces += (int)value; break;
case "SlowEffect": e.slowMultiplierBonus += value; break;
case "BulletSpeed": e.bulletSpeedMultiplier *= value; break;
case "Homing": e.enableHoming = true; break;
case "Explosion": e.enableExplosion = true; break;
}
}
}
// Gun.cs - Apply enhancements at runtimepublic override void Use(Vector3 direction, Vector3 origin)
{
EnhancementData e = EnhancementManager.Instance.GetEnhancement(slotIndex);
float enhancedDamage = damage * e.damageMultiplier;
int enhancedBullets = bulletsPerShot + e.bulletsPerShotBonus;
float enhancedSpeed = bulletSpeed * e.bulletSpeedMultiplier;
for (int i = 0; i < enhancedBullets; i++)
{
GameObject bullet = Instantiate(bulletPrefab);
bullet.GetComponent<Bullet>().Initialize(direction, enhancedSpeed, enhancedDamage);
ApplyEnhancementToBullet(bullet, e);
}
}
void ApplyEnhancementToBullet(Bullet bullet, EnhancementData e)
{
if (e.enableExplosion)
SetField(bullet, "isExplosive", true);
if (e.bonusBounces > 0) {
SetField(bullet, "isBouncy", true);
int baseBounces = GetField<int>(bullet, "maxBounces");
SetField(bullet, "maxBounces", baseBounces + e.bonusBounces);
}
if (e.enableHoming)
SetField(bullet, "isHoming", true);
if (e.slowMultiplierBonus > 0) {
SetField(bullet, "hasSlowEffect", true);
float baseSlow = GetField<float>(bullet, "slowMultiplier");
SetField(bullet, "slowMultiplier", baseSlow * (1f - e.slowMultiplierBonus));
}
}
3. Procedural Generation
QuadTree subdivision + A pathfinding validation*(csharp)
// RoomGenerator.cs - QuadTree spatial partitioningvoid BuildQuadTree(QuadTreeNode node, int depth)
{
if (depth >= maxDepth || node.bounds.width < minRegionSize
|| Random.value > splitChance)
{
node.isLeaf = true;
node.isWalkable = Random.value > obstacleDensity;
return;
}
node.children = new QuadTreeNode[4];
float halfW = node.bounds.width / 2f;
float halfH = node.bounds.height / 2f;
node.children[0] = new QuadTreeNode(
new Rect(node.bounds.x, node.bounds.y, halfW, halfH));
// ... create other 3 quadrants
foreach (var child in node.children)
BuildQuadTree(child, depth + 1);
}
// A* pathfinding ensures reachabilityList<Vector2Int> FindPath(Vector2Int start, Vector2Int end)
{
List<PathNode> openList = new List<PathNode>();
HashSet<Vector2Int> closedSet = new HashSet<Vector2Int>();
openList.Add(new PathNode {
position = start,
gCost = 0,
hCost = Vector2Int.Distance(start, end)
});
while (openList.Count > 0)
{
PathNode current = openList.OrderBy(n => n.fCost).First();
if (current.position == end)
return ReconstructPath(current);
openList.Remove(current);
closedSet.Add(current.position);
foreach (Vector2Int neighbor in GetNeighbors(current.position))
{
if (closedSet.Contains(neighbor) || !IsWalkable(neighbor))
continue;
float tentativeG = current.gCost +
Vector2Int.Distance(current.position, neighbor);
PathNode neighborNode = openList
.FirstOrDefault(n => n.position == neighbor);
if (neighborNode == null)
{
openList.Add(new PathNode {
position = neighbor,
gCost = tentativeG,
hCost = Vector2Int.Distance(neighbor, end),
parent = current
});
}
}
}
return null;
}
// Guarantee all exits are reachablevoid EnsurePathsReachable()
{
Vector2Int spawnGrid = WorldToGrid(playerSpawnPosition);
foreach (var door in exitDoors)
{
Vector2Int doorGrid = WorldToGrid(door.position);
List<Vector2Int> path = FindPath(spawnGrid, doorGrid);
if (path == null || path.Count == 0)
path = CreateForcedPath(spawnGrid, doorGrid);
foreach (var tile in path)
WidenPath(tile, pathWidth);
}
}Branching room network(csharp)
// RoomMapSystem.csvoid GenerateMap()
{
// Start: 1 room
List<Room> startColumn = new List<Room> {
new Room(0, RoomType.Normal, 0, 0)
};
roomColumns.Add(startColumn);
// Expansion phase: 1→2→3→4 rooms
for (int i = 1; i <= maxRoomHalf; i++)
{
int nextCount = previousColumn.Count + Random.Range(1, 3);
List<Room> newColumn = GenerateRooms(i, nextCount);
ConnectRoomsExpanding(previousColumn, newColumn);
roomColumns.Add(newColumn);
}
// Convergence phase: 4→2→1 Boss
while (previousColumn.Count > 3)
{
int nextCount = previousColumn.Count / 2;
List<Room> newColumn = GenerateRooms(currentColumn, nextCount);
ConnectRoomsConverging(previousColumn, newColumn);
roomColumns.Add(newColumn);
}
// Final Boss room
Room bossRoom = new Room(roomIdCounter++, RoomType.Boss, finalColumn, 0);
foreach (Room room in previousColumn)
room.connectedRooms.Add(bossRoom);
}

Project Gallery



