Glorious Alpha Two Testers!
Alpha Two Phase II testing is currently taking place 5+ days each week. More information about testing schedule can be found here
If you have Alpha Two, you can download the game launcher here, and we encourage you to join us on our Official Discord Server for the most up to date testing news.
Alpha Two Phase II testing is currently taking place 5+ days each week. More information about testing schedule can be found here
If you have Alpha Two, you can download the game launcher here, and we encourage you to join us on our Official Discord Server for the most up to date testing news.
Building a Smart Resource System: Skill, Gear, and Environmental Impact

in Artisanship
Lets talk about how to fix the gathering system.
The goal is to make gear matter, make gathering more interactive, reward player skill, and make rare materials feel truly rare.
Here’s how the system works:
- Each resource node (like trees or ores) has a base rarity table (Common, Uncommon, Rare, Heroic, Epic, Legendary).
- Player gathering skill modifies these rarity chances — higher skill players have better odds of finding rare or legendary nodes.
- Gear matters too — better gathering tools provide direct bonuses to rare+ chances, while legendary tools might even give a small chance at legendary nodes regardless of zone conditions.
- Zones track how much harvesting is happening — if an area is overfarmed, rare nodes become much less common.
- The zone gradually recovers if players stop harvesting, bringing better nodes back over time.
- The whole system could be handled by a NodeRarityManager class (or whatever), which:
- Calculates rarity based on node type, player skill, gear, and zone depletion.
- Tracks harvesting pressure in each zone.
- Handles natural resource recovery over time.
- This approach makes gathering more strategic — players have to decide whether to farm popular zones (where rares are scarce) or explore fresher areas for better rewards.
- This system ensures that player progression, gear investment, and player-driven world activity all directly shape the economy and gameplay experience.
Obviously this is placeholder code, but I wanted to show how straight forward a fix could be.
📂 NodeRarityManager.h
📂 NodeRarityManager.cpp
Example Usage
The goal is to make gear matter, make gathering more interactive, reward player skill, and make rare materials feel truly rare.
Here’s how the system works:
- Each resource node (like trees or ores) has a base rarity table (Common, Uncommon, Rare, Heroic, Epic, Legendary).
- Player gathering skill modifies these rarity chances — higher skill players have better odds of finding rare or legendary nodes.
- Gear matters too — better gathering tools provide direct bonuses to rare+ chances, while legendary tools might even give a small chance at legendary nodes regardless of zone conditions.
- Zones track how much harvesting is happening — if an area is overfarmed, rare nodes become much less common.
- The zone gradually recovers if players stop harvesting, bringing better nodes back over time.
- The whole system could be handled by a NodeRarityManager class (or whatever), which:
- Calculates rarity based on node type, player skill, gear, and zone depletion.
- Tracks harvesting pressure in each zone.
- Handles natural resource recovery over time.
- This approach makes gathering more strategic — players have to decide whether to farm popular zones (where rares are scarce) or explore fresher areas for better rewards.
- This system ensures that player progression, gear investment, and player-driven world activity all directly shape the economy and gameplay experience.
Obviously this is placeholder code, but I wanted to show how straight forward a fix could be.
📂 NodeRarityManager.h
#pragma once #include "CoreMinimal.h" #include "NodeRarityManager.generated.h" UENUM(BlueprintType) enum class ERarity : uint8 { Common UMETA(DisplayName = "Common"), Uncommon UMETA(DisplayName = "Uncommon"), Rare UMETA(DisplayName = "Rare"), Heroic UMETA(DisplayName = "Heroic"), Epic UMETA(DisplayName = "Epic"), Legendary UMETA(DisplayName = "Legendary") }; USTRUCT() struct FRarityTable { GENERATED_BODY() TMap<ERarity, float> BaseChances; }; USTRUCT() struct FZoneHarvestData { GENERATED_BODY() int32 TotalHarvests = 0; FDateTime LastReset = FDateTime::UtcNow(); }; UCLASS() class YOURGAME_API UNodeRarityManager : public UObject { GENERATED_BODY() public: UNodeRarityManager(); ERarity CalculateNodeRarity(const FString& NodeType, int32 GatheringSkill, float GearBonus, const FString& ZoneID); void RecordHarvest(const FString& ZoneID); void RecoverZones(); private: TMap<FString, FRarityTable> NodeRarityTables; TMap<FString, FZoneHarvestData> ZoneHarvestData; void InitializeBaseRarities(); float GetDepletionModifier(const FString& ZoneID) const; FRarityTable GetModifiedRarityTable(const FRarityTable& BaseTable, int32 GatheringSkill, float GearBonus, const FString& ZoneID) const; };
📂 NodeRarityManager.cpp
#include "NodeRarityManager.h" UNodeRarityManager::UNodeRarityManager() { InitializeBaseRarities(); } void UNodeRarityManager::InitializeBaseRarities() { FRarityTable GoldVeinRarity; GoldVeinRarity.BaseChances.Add(ERarity::Common, 50.0f); GoldVeinRarity.BaseChances.Add(ERarity::Uncommon, 30.0f); GoldVeinRarity.BaseChances.Add(ERarity::Rare, 15.0f); GoldVeinRarity.BaseChances.Add(ERarity::Heroic, 4.0f); GoldVeinRarity.BaseChances.Add(ERarity::Epic, 0.9f); GoldVeinRarity.BaseChances.Add(ERarity::Legendary, 0.1f); NodeRarityTables.Add("GoldVein", GoldVeinRarity); } float UNodeRarityManager::GetDepletionModifier(const FString& ZoneID) const { const FZoneHarvestData* ZoneData = ZoneHarvestData.Find(ZoneID); if (!ZoneData) return 1.0f; if (ZoneData->TotalHarvests < 50) return 1.0f; if (ZoneData->TotalHarvests < 100) return 0.8f; if (ZoneData->TotalHarvests < 200) return 0.6f; return 0.3f; // heavily depleted } FRarityTable UNodeRarityManager::GetModifiedRarityTable(const FRarityTable& BaseTable, int32 GatheringSkill, float GearBonus, const FString& ZoneID) const { FRarityTable ModifiedTable = BaseTable; // Base skill tier modifiers float CommonModifier = 1.0f; float UncommonModifier = 1.0f; float RarePlusModifier = 1.0f; if (GatheringSkill >= 30) { CommonModifier = 0.7f; UncommonModifier = 1.2f; RarePlusModifier = 1.5f; } else if (GatheringSkill >= 20) { CommonModifier = 0.8f; UncommonModifier = 1.1f; RarePlusModifier = 1.2f; } else if (GatheringSkill >= 10) { CommonModifier = 0.9f; UncommonModifier = 1.05f; RarePlusModifier = 1.1f; } // Apply player skill modifiers for (auto& Pair : ModifiedTable.BaseChances) { if (Pair.Key == ERarity::Common) { Pair.Value *= CommonModifier; } else if (Pair.Key == ERarity::Uncommon) { Pair.Value *= UncommonModifier; } else { Pair.Value *= RarePlusModifier; } } // Apply gear bonus directly to rare+ nodes for (auto& Pair : ModifiedTable.BaseChances) { if (Pair.Key >= ERarity::Rare) { Pair.Value *= (1.0f + GearBonus); } } // Apply zone depletion modifier to rare+ nodes float DepletionModifier = GetDepletionModifier(ZoneID); for (auto& Pair : ModifiedTable.BaseChances) { if (Pair.Key >= ERarity::Rare) { Pair.Value *= DepletionModifier; } } // Normalize all values to sum to 100% float TotalChance = 0.0f; for (const auto& Pair : ModifiedTable.BaseChances) { TotalChance += Pair.Value; } for (auto& Pair : ModifiedTable.BaseChances) { Pair.Value = (Pair.Value / TotalChance) * 100.0f; } return ModifiedTable; } ERarity UNodeRarityManager::CalculateNodeRarity(const FString& NodeType, int32 GatheringSkill, float GearBonus, const FString& ZoneID) { if (!NodeRarityTables.Contains(NodeType)) { return ERarity::Common; // Failsafe } const FRarityTable& BaseTable = NodeRarityTables[NodeType]; FRarityTable ModifiedTable = GetModifiedRarityTable(BaseTable, GatheringSkill, GearBonus, ZoneID); float Roll = FMath::FRand() * 100.0f; float CumulativeChance = 0.0f; for (const auto& Pair : ModifiedTable.BaseChances) { CumulativeChance += Pair.Value; if (Roll <= CumulativeChance) { return Pair.Key; } } return ERarity::Common; // Failsafe } void UNodeRarityManager::RecordHarvest(const FString& ZoneID) { FZoneHarvestData& ZoneData = ZoneHarvestData.FindOrAdd(ZoneID); ZoneData.TotalHarvests++; } void UNodeRarityManager::RecoverZones() { for (auto& Pair : ZoneHarvestData) { FZoneHarvestData& ZoneData = Pair.Value; ZoneData.TotalHarvests = FMath::Max(0, ZoneData.TotalHarvests - 10); } }
Example Usage
UNodeRarityManager* RarityManager = NewObject<UNodeRarityManager>(); RarityManager->RecordHarvest("Zone123"); // Example: Player has skill 25 and gathering gear that provides a 15% rare+ bonus. int32 PlayerGatheringSkill = 25; float GearBonus = 0.15f; // 15% bonus to rare+ ERarity Rarity = RarityManager->CalculateNodeRarity("GoldVein", PlayerGatheringSkill, GearBonus, "Zone123");
0