Skip to content

t1k:unity:dots-core:progression

FieldValue
Moduledots-core
Version2.3.2
Effortmedium
Tools

Keywords: dialogue, idle, level, meta-currency, prestige, progression, quest, RealmEra, reward, XP

/t1k:unity:dots-core:progression

DOTS Progression — Level, Quest, Dialogue, Idle, Meta

Section titled “DOTS Progression — Level, Quest, Dialogue, Idle, Meta”

Package: com.the1studio.dots-progression — namespace DOTSProgression.* Related skills: t1k:unity:dots-core:ecs (API) · t1k:unity:dots-combat:rpg (stats pipeline) · t1k:unity:dots-core:pickup (XP gem)

  • Implementing XP → Level-Up pipeline with formula-driven thresholds
  • Building quest objectives (kill count, zone visit, collection) with progress tracking
  • Adding dialogue trees with branching and speaker/text data
  • Implementing idle generators: offline catch-up, multiplier stacking, prestige reset
  • Meta-progression: permanent unlocks, meta-currency, encounter pool manipulation
  • Granting XP, currency, or combined rewards in response to any game event (kill, quest, daily login, combo)
  • Using RewardApplier, RewardSpec, RewardRequestEvent, or EntityEventRewardSystem
FileContents
reward-applier-guide.mdRewardApplier / RewardSpec / EntityEventRewardSystem — exact API signatures, 4 use-cases (RPG kill, puzzle combo, idle catch-up, battle-pass streak), pattern for domain reward systems, gotchas
ComponentTypePurpose
ExperienceIComponentDatafloat Current, float ToNextLevel, int Level
LevelUpTagIComponentData (tag)Added by level-up system; consumed by stats system
QuestObjectiveIBufferElementDataint ObjectiveId, ObjectiveType Type, int Target, int Progress
QuestRewardIBufferElementDataint ItemTypeId, float XPAmount, int MetaCurrency
QuestStateIComponentDataint ActiveQuestId, QuestStatus Status
DialogueNodeIBufferElementDataint NodeId, int NextNodeId, FixedString128Bytes Speaker, FixedString512Bytes Text
IdleGeneratorIBufferElementDataint ResourceId, float BaseRate, float Multiplier, double LastTickTime
PrestigeStateIComponentDataint PrestigeLevel, float MultiplierBonus, double PrestigeTimestamp
MetaCurrencyIComponentData (singleton)long Amount
MetaUnlockIBufferElementData (singleton entity)int UnlockId, bool IsUnlocked, int Cost
public enum ObjectiveType { KillCount, ZoneVisit, ItemCollect, BossDefeated }
public enum QuestStatus { Inactive, Active, Completed, Claimed }
SystemGroupRole
LevelUpSystemProgressionSystemGroupDetects Experience.Current >= ToNextLevel; increments Level; adds LevelUpTag
StatsDirtyOnLevelUpSystemProgressionSystemGroup (after LevelUp)Detects LevelUpTag; enables StatsDirtyTag → triggers stat recalc
QuestProgressSystemProgressionSystemGroupIncrements QuestObjective.Progress on matching events via singleton event buffer
QuestCompletionSystemProgressionSystemGroupDetects all objectives complete; sets QuestStatus.Completed; awards QuestReward
IdleTickSystemProgressionSystemGroupAccumulates IdleGenerator output based on elapsed real time
OfflineCatchUpSystemProgressionSystemGroup (OnCreate)Calculates delta since LastTickTime at world load; caps at MaxOfflineSeconds
MetaUnlockSystemProgressionSystemGroupChecks MetaCurrency >= UnlockCost; sets IsUnlocked; deducts currency
PrestigeSystemProgressionSystemGroupResets run state; increments PrestigeLevel; computes new MultiplierBonus
// LevelUpSystem — cascading level-up support:
while (xp.Current >= xp.ToNextLevel)
{
xp.Current -= xp.ToNextLevel;
xp.Level++;
xp.ToNextLevel = LevelFormula.ComputeThreshold(xp.Level); // e.g. base * pow(1.15f, level)
}
// Add LevelUpTag for downstream reaction
ecb.AddComponent<LevelUpTag>(entity);
// QuestProgressSystem — reads singleton event buffer:
var killEvents = SystemAPI.GetSingletonBuffer<KillEventElement>();
foreach (var e in killEvents)
{
// Find active quest with matching KillCount objective
// Increment progress, clamp at Target
}
// OfflineCatchUpSystem.OnCreate:
double elapsedSeconds = math.min(
SystemAPI.Time.ElapsedTime - lastTickTime,
MAX_OFFLINE_SECONDS); // cap at 8 hours by default
// Apply accumulated generator output in one batch
// PrestigeSystem — destroy run entities, keep meta:
ecb.DestroyEntity(runEntityQuery); // destroys all run-scoped entities
prestige.PrestigeLevel++;
prestige.MultiplierBonus = math.pow(PRESTIGE_BASE, prestige.PrestigeLevel);
DirectionPackageDetail
Depends onDOTSCore baseSystemAPI, ECB, FixedString
Depends onDOTSCore.StatsStatsDirtyTag signal triggered on LevelUp
Depends onDOTSCore.PickupExperience component that XPCollectionSystem writes
Used byDOTSCombatKill events feed QuestProgressSystem
  • BackpackCrawler — XP/Level pipeline + quest board with item-collect objectives
  • IdleRPGDemo — IdleGenerator × 6 resources + offline catch-up + prestige loop
  • RogueliteDemo — MetaUnlock tree, MetaCurrency from runs, encounter pool scaling

Package: DOTSProgression.Analytics · Asmdef: DOTSProgression.Analytics

Provides a Mono-side hook pattern for dispatching BattlePass XP events to analytics backends (Firebase, PlayFab, GameAnalytics, etc.) without each consumer implementing its own drain loop.

TypeKindPurpose
ProgressionEventTypeenumHigh-level classifier: TierUnlock, QuestComplete, AchievementEarned, DailyLoginStreak, MetaProgressionMilestone, SeasonComplete, Other
IProgressionAnalyticsHookinterfaceImplement + register with ProgressionAnalyticsBridge to receive events
ProgressionAnalyticsBridgeMonoBehaviourDrain coordinator — place on DontDestroyOnLoad root; register hooks via Register(hook)
ProgressionAnalyticsSnapshotSystemSystemBaseECS side — snapshots events before BattlePassXPSystem drains the buffer; runs [UpdateBefore(typeof(BattlePassXPSystem))]
SnapshotEntrystructCarries BattlePassXPEvent + TierBefore + TierUnlocked + TotalXPAfterGrant
// 1. Implement the hook in your game-specific assembly
public sealed class FirebaseProgressionHook : IProgressionAnalyticsHook
{
public void OnBattlePassXPEvent(in BattlePassXPEvent evt, ProgressionEventType eventType) =>
Firebase.Analytics.LogEvent("bp_xp_grant", "source", evt.Source.ToString());
public void OnTierUnlock(int tier, long totalXP) =>
Firebase.Analytics.LogEvent("bp_tier_unlock", "tier", tier);
public void OnQuestComplete(long xpAwarded) =>
Firebase.Analytics.LogEvent("quest_complete", "xp", xpAwarded);
}
// 2. Place ProgressionAnalyticsBridge on a DontDestroyOnLoad GameObject.
// 3. Register your hook after DI scope is built:
var bridge = FindObjectOfType<ProgressionAnalyticsBridge>();
bridge.Register(new FirebaseProgressionHook());
// 4. Override ClassifyEvent for game-specific source-to-type remapping:
public class MyBridge : ProgressionAnalyticsBridge
{
protected override ProgressionEventType ClassifyEvent(BattlePassXPSource source)
{
if (source == BattlePassXPSource.Loot) return ProgressionEventType.MetaProgressionMilestone;
return base.ClassifyEvent(source);
}
}
BattlePassXPSourceProgressionEventType
QuestQuestComplete
AchievementAchievementEarned
DailyLoginDailyLoginStreak
BossMetaProgressionMilestone
Encounter, Collectible, ShopPurchase, Loot, Tutorial, Event, UnknownOther

ProgressionAnalyticsSnapshotSystem runs before BattlePassXPSystem each ECS frame. It simulates the XP math locally to detect tier transitions. ProgressionAnalyticsBridge.LateUpdate drains the snapshot AFTER the ECS frame — so by LateUpdate the XP buffer is already cleared by BattlePassXPSystem. The bridge reads the pre-captured snapshot, not the live buffer.

Realm Unlock Pipeline (P7a — boss-gated realm progression)

Section titled “Realm Unlock Pipeline (P7a — boss-gated realm progression)”

Package: DOTSProgression.Realm · System: RealmUnlockSystem

Wires the boss-defeated → next-realm-unlock flow. RealmUnlockSystem reacts to DOTSCombat.Boss.BossCompletionFlag (an IEnableableComponent) firing on a player entity, reads CurrentRealm + ForgeLevel, and unlocks the next realm iff ForgeLevel.Value >= nextRealm.MinForgeLevelToUnlock.

TypeKindPurpose
CurrentRealmIComponentDataint CurrentRealmId — the realm whose boss was just defeated
ForgeLevelIComponentDataint Value — generic progression-tier marker; host populates alongside its own forge state (e.g. ChaosForgeDemo.ForgeState.CurrentForgeLevel)
RealmUnlockedTagIComponentData (IEnableableComponent)Per-realm-entity flag; enabled when that realm becomes available
BossUnlockProcessedTagIComponentData (tag)One-shot sentinel added via ECB so the same completion event never re-fires across frames
RealmConfigIComponentDataint MinForgeLevelToUnlock per realm
RealmRegistryIBufferElementData (singleton)int RealmId, int MinForgeLevelToUnlock, Entity RealmEntity — registry of all realms
RealmRegistrySingletonTagIComponentData (singleton)Marks the registry-carrier entity (RequireForUpdate)
RealmUnlockedEventIBufferElementData (on registry singleton)int UnlockedRealmId, int PreviousRealmId — payload event for game logic
RealmEraenum (byte)Opaque era index for a realm. Theme-neutral — values are Era0/Era1/Era2, NOT theme names. The library does not name or describe eras; the consumer demo maps each index to a display name, palette, and assets.
  1. RealmUnlockSystem runs [UpdateAfter(typeof(BossDefeatedEventSystem))] so it reads the freshly-enabled BossCompletionFlag the same frame (no one-frame UI lag).
  2. For each player with BossCompletionFlag enabled and NO BossUnlockProcessedTag: always add the processed tag (positive OR negative decision), compute nextRealmId = CurrentRealmId + 1, look it up in RealmRegistry.
  3. If ForgeLevel.Value >= MinForgeLevelToUnlock: enable RealmUnlockedTag on the next realm entity, append a RealmUnlockedEvent + a sibling UIEventBus entry (EventType = UIEventTypeRegistry.RealmUnlocked, Payload = nextRealmEntity) on the registry singleton, drained by the Mono UIModalDirector.
  4. A separate cleanup system clears the event buffer at OrderFirst next frame as a safety net.
  • Next-realm entity MUST be baked with a (disabled) RealmUnlockedTagRealmUnlockSystem calls HasComponent(nextRealmEntity) and bails (continue) if the realm entity lacks the RealmUnlockedTag component. If realm entities are baked WITHOUT the disabled enableable tag, unlocks silently no-op — no event, no toast, no error. Bake every realm entity with RealmUnlockedTag disabled (SetComponentEnabled(false) in the baker), not just the starting realm.
  • BossUnlockProcessedTag is the run-controller’s responsibility to remove — the system adds it but never removes it. On a new run, the run-controller must remove BossUnlockProcessedTag AND disable BossCompletionFlag together, or the next boss defeat won’t re-trigger the unlock check.
  • Final-realm completion still adds the processed tag — when the player clears the last realm (nextRealmId not in registry), the system adds BossUnlockProcessedTag and no-ops the unlock. This is intentional (stops per-frame rescanning) — do not treat the missing event as a bug.
  • Already-unlocked realm skips the event — if RealmUnlockedTag is already enabled on the next realm (e.g. a cheat/manual unlock), the system skips the event emission to avoid a duplicate UI toast, but still adds the processed tag.
  • RealmEra is theme-neutral (Era0/Era1/Era2) — never put theme names in the library (Wave-4). The enum was renamed from baked theme names (Primitive/Modern/Apex) to opaque Era0/Era1/Era2; the demo owns the display names. Per the naming-charter, genre/theme tokens belong in consumer demos, not in library enum values. The old Primitive/Modern/Apex names are kept as [Obsolete] forwarding shims (Primitive = Era0, etc.) so un-migrated demo code compiles — migrate to the EraN form and let the demo map indices to names/palettes/assets. Persistence caveat: RealmEra is stored as byte; changing an existing value’s ordinal is a breaking change for any persisted CurrentRealm. Add new eras as new values only.
  • LevelUpSystem cascades until stable — use while, not if. A large XP grant (e.g., dungeon boss) can level the entity multiple times in one frame. Using if silently discards overflow XP and leaves Level wrong.
  • OfflineCatchUpSystem runs in OnCreate, not OnUpdate — it fires once at world load. If you accidentally move it to OnUpdate, it will apply catch-up EVERY frame, causing exponential resource accumulation.
  • Cap offline seconds — without MAX_OFFLINE_SECONDS, players who return after 30 days receive astronomically large catch-up. Default cap is 8 hours (28,800 s). Expose in FloorConfig or a ScriptableObject authoring component.
  • QuestStatus.Claimed is finalQuestCompletionSystem ignores already-claimed quests. If rewards need to be re-granted (debug flow), destroy the quest entity and re-bake.
  • DialogueNode buffer uses FixedString512Bytes — 512 bytes per node. If localized text exceeds 512 bytes in any language, the baker silently truncates. Author dialogue in short segments or load from external localization tables.
  • MetaCurrency is a singleton — one entity only — use SystemAPI.GetSingletonRW<MetaCurrency>(). Adding MetaCurrency to multiple entities corrupts global economy. Singleton entity is baked from authoring scene; do not create in runtime ECB.
  • Prestige destroys ALL run entities — ensure run-scoped entities are tagged (e.g., RunScopedTag) and the destroy query is exact. Missing the tag on a newly added entity causes orphaned entities after prestige.