Appearance
Progression Counters
Progression counters track player progress across gamerounds. When a counter reaches 100% (value 1.0), it triggers an award and resets. The counter carries over any excess value. Each option in the counter's awards array has one of two kinds: a feature spin award ({ count, feature }) or a cash award ({ winMultiplier } × stake). The two kinds can mix freely in the same array.
This page covers how the engine accumulates, completes, and resets counters at runtime. For how to increment counters and configure them at generation time, see Generator · Progression Counters.
Do you need a jackpot that fills and pays within a single gameround instead of persisting across rounds for a player? That is a different, similarly-shaped mechanic. See Single-Gameround Jackpots.
Configuration
Define counters in your game's config.json. Selection mode (randomChoice or playerChoice) is at the top level. Each option inside awards chooses its own kind by the field it carries.
Feature spins only
json
{
"progressionCounters": [
{
"name": "scatter-collection",
"onComplete": {
"type": "randomChoice",
"awards": [{ "count": 10, "feature": "freespins" }]
},
"stakeSpecific": false
}
]
}Cash only
winMultiplier is in stake-multiplier units. When the counter completes, the engine pays winMultiplier × stake directly.
json
{
"progressionCounters": [
{
"name": "cashbox",
"onComplete": {
"type": "randomChoice",
"awards": [
{ "winMultiplier": 5, "weighting": 3 },
{ "winMultiplier": 20, "weighting": 1 }
]
},
"stakeSpecific": false
}
]
}Mixed (feature + cash in the same awards array)
A single counter completion can offer the player a choice between a feature spin and a cash payout. Alternatively, the counter can randomly pick between the two:
json
{
"progressionCounters": [
{
"name": "treasure",
"onComplete": {
"type": "playerChoice",
"awards": [
{ "count": 10, "feature": "freespins" },
{ "winMultiplier": 25 }
]
},
"stakeSpecific": false
}
]
}For playerChoice, the client receives the option list in engineData.playerChoice. The client submits a playerChoiceIndex on the next placeBet call. The engine resolves the pick by its kind. A cash option adds winMultiplier × stake to totalWin. The engine then resumes any pending feature spin, or ends the round. A feature option starts the awarded feature spin.
Fields
| Field | Type | Description |
|---|---|---|
name | string | Counter identifier. It must match keys in entry progressionAwards. |
onComplete | TProgressionAwarded | The award that the engine triggers when the counter reaches 1.0. |
onComplete.type | "randomChoice" | "playerChoice" | Whether the engine picks a weighted option or the player picks from a list. |
onComplete.awards | TPlayerChoiceAward[] / TRandomChoiceAward[] | Mixed list of feature-spin and cash options. Each option carries either { count, feature } or { winMultiplier }, plus an optional weighting for randomChoice. |
stakeSpecific | boolean | If true, each stake level maintains its own counter. |
resetOnFeatureEnd is not live at runtime
The generator's IProgressionCounterConfig also carries an optional resetOnFeatureEnd?: string[] (feature names whose end would reset the counter to 0). The engine briefly implemented this feature. The engine team then removed it from the runtime. The currently-deployed engine ignores the field entirely. Nothing resets the counter when a listed feature ends. loadConfig strips the field before it sends config.progressionCounters to the client. A counter always persists across rounds until it completes.
The generator's KPI-math RTP calculation still accounts for resetOnFeatureEnd. It treats tagged increment mass as discarded at the reset boundary. So a config that sets this field can produce an RTP report that assumes reset behaviour. The live game never performs this reset behaviour. Do not set resetOnFeatureEnd on a shipping game's progressionCounters today.
If you need a meter that fills and resets within a single feature run or gameround, use a different, already-supported mechanic. See Single-Gameround Jackpots.
Entry-Level Setup
During game generation, assign progressionAwards to entries that should increment counters:
typescript
await generator.addResult(scenario, {
win,
metaTags,
featureAwards,
weight,
progressionAwards: { 'scatter-collection': { 0: 0.01 } }, // 1% increment, attributed to scenario step 0
});The inner key identifies the 0-based scenario step for the increment. Use 0 for a single-object scenario. For a multi-result scenario, use the step that produced the increment. See Generator · Recording increments with addResult() for the full shape and how it interacts with entry matching.
In the editor, use the Progression Awards section on each entry to set pairs of counter name and increment value.
How It Works
- Round start: The engine loads current counter values from player data.
- Each spin: If the selected entry has
progressionAwards, the engine increments the corresponding counters. - Completion: When a counter reaches 1.0 or more, the engine picks one option from the
onComplete.awardsarray. Or the engine asks the player to pick one. The engine then dispatches the option by kind. A feature option merges spins intospinInfo. A cash option addswinMultiplier × staketototalWin. The counter then resets and carries over any excess. - Round end: The engine saves updated counter values to player data.
engineData.progressionCounters holds the counter values, keyed by counter name. This field appears only after at least one counter receives an increment. Before that, the engine omits the field entirely instead of sending {}:
typescript
const counters = gameResult.engineData.progressionCounters;
// { "scatter-collection": 0.55 } // 55% progressClient Integration
loadConfig Response
When a game has progression counters, loadConfig returns:
json
{
"config": {
"progressionCounters": [
{ "name": "scatter-collection", "onComplete": { ... }, "stakeSpecific": false }
],
"progressionCounterValues": {
"scatter-collection": 0.55
}
}
}placeBet Response
Each placeBet response includes current counter values in engineData:
json
{
"result": {
"engineData": {
"progressionCounters": { "scatter-collection": 0.56 },
"inProgress": true
}
}
}Progression Events
engineData.progressionCounters carries each counter's value at the current step. On its own, this only lets a client read the fill level. It cannot tell which step delivered an increment or an award. engineData.progressionEvents provides that information. It has one entry per counter that changed on the current scenario step. The client can then animate the fill and play the award at the exact moment they happen. The client does not need to infer them from the end-of-round state.
Each placeBet response carries the events for that step:
typescript
interface IProgressionEvent {
counter: string; // Counter name (matches progressionCounters)
delta: number; // Increment applied on this step
value: number; // Counter value after this step's delta and any reset
cashWin?: number; // Cash awarded on this step, in stake-multiplier units
featureAwards?: TRandomChoiceFeatureAward[]; // Feature spins merged into spinInfo on this step
playerChoiceRequested?: boolean; // A completion on this step needs player input
}delta and value are always present. The award fields appear only when the increment carries the counter across 1.0. The fields also require that onComplete fires:
cashWin: cash paid by a resolved cash award. The value is already included intotalWin. Use it only to present the win.featureAwards: feature spins merged into the round'sspinInfo.playerChoiceRequested: the completion needs the player to choose. The options also arrive onengineData.playerChoice.
A single step can complete a counter more than once. This can happen with a large increment, or a high carried-in value. Every crossing on that step aggregates into one event.
typescript
for (const event of gameResult.engineData.progressionEvents ?? []) {
animateCounter(event.counter, event.value); // fill to the new value
if (event.cashWin) showJackpotWin(event.counter, event.cashWin * stake);
if (event.featureAwards) showFeatureAward(event.counter);
if (event.playerChoiceRequested) awaitPlayerChoice(); // options on engineData.playerChoice
}The events are scoped to the current step. They arrive on the placeBet continuation for the exact step where the counter changed. This includes a step inside a multi-step round, such as a free spin sequence. For example, an award that lands on the third free spin signals on that spin, not at round end. progressionEvents is absent or empty on steps where no counter changed.
Example
Feature counter
A slot game where collecting 100 scatter symbols awards 10 free spins:
- Config: Define a counter
scatter-collectionwithonComplete: { type: "randomChoice", awards: [{ count: 10, feature: "freespins" }] }. - Generation: On entries where a scatter lands, set
progressionAwards: { "scatter-collection": { 0: 0.01 } }. - Gameplay: After 100 scatter lands (100 x 0.01 = 1.0), the counter triggers 10 free spins. The counter then resets to 0.
Cash counter
A coin-collect mechanic where every 20 coin landings pays out 5× stake:
- Config: Define a counter
coin-collectwithonComplete: { type: "randomChoice", awards: [{ winMultiplier: 5 }] }. - Generation: On entries where a coin lands, set
progressionAwards: { "coin-collect": { 0: 0.05 } }. - Gameplay: After 20 coin lands (20 × 0.05 = 1.0), the counter pays
5 × stakeintototalWin. The counter then resets to 0.
Mixed treasure-chest counter
This is a "pick your prize" mechanic. Every 50 chest landings offers the player a choice between free spins and a flat cash payout:
- Config:json
{ "name": "chest-pick", "onComplete": { "type": "playerChoice", "awards": [ { "count": 8, "feature": "freespins" }, { "winMultiplier": 30 } ] }, "stakeSpecific": false } - Generation: On each chest land, set
progressionAwards: { "chest-pick": { 0: 0.02 } }. - Gameplay: After 50 chest lands, the engine surfaces both options in
engineData.playerChoice. The client renders both kinds (use theisCashChoice/isFeatureChoiceguards). The client then postsplayerChoiceIndex. The engine pays out based on the picked option's kind.
Float Precision
Counter arithmetic uses Math.round(val * 1e6) / 1e6 to avoid floating-point drift. This keeps behavior consistent across rounds.
Backward Compatibility
- All progression fields are optional. Existing games without progression counters are unaffected.
- The engine handles old output files without
progressionAwardsfields gracefully. progressionCountersonengineDatais optional. Clients that do not use it can ignore it.