Loading recipe…
The Lightning Link / Dragon Link Hold & Win pattern, built with CellPin. The grid is not one 5×3 ReelSet. it’s 15 independent 1×1 ReelSets, one per cell. Each spins on its own, each holds coins on its own, and the CellPin primitive handles the “hold” part.
Why per-cell ReelSets
In a Hold & Win feature, locked cells don’t spin. only unlocked cells do. A single 5×3 ReelSet spins whole columns at once; you’d have to work around it. Per-cell mini-reels make the “spin only the unlocked cells” pattern trivial:
for (const cell of cells) {
if (cell.reelSet.getPin(0, 0)) continue; // already held, skip
spinPromises.push(cell.reelSet.spin());
}
The mechanic in full
// Create 15 mini ReelSets at build time (shown abbreviated)
for (let col = 0; col < 5; col++) {
for (let row = 0; row < 3; row++) {
const mini = new ReelSetBuilder()
.reels(1).visibleRows(1)
.symbolSize(CELL, CELL)
.symbols((r) => {
r.register('coin', BlurSpriteSymbol, { textures, blurTextures });
r.register('empty', EmptySymbol, {});
})
.weights({ coin: 1, empty: 3 })
.ticker(app.ticker)
.build();
cells.push({ col, row, reelSet: mini });
}
}
// Each round: spin only the unlocked cells, then pin any hits
async function playRound(hits) {
const spinning = cells.filter((c) => !c.reelSet.getPin(0, 0));
const promises = spinning.map((c) => c.reelSet.spin());
await new Promise((r) => setTimeout(r, 140));
for (const cell of spinning) {
const isHit = hits.some((h) => h.col === cell.col && h.row === cell.row);
cell.reelSet.setResult([[isHit ? 'coin' : 'empty']]);
}
await Promise.all(promises);
// Pin every hit with its payout value. the engine keeps the coin in place
for (const cell of spinning) {
if (!hits.some((h) => h.col === cell.col && h.row === cell.row)) continue;
cell.reelSet.pin(0, 0, 'coin', {
turns: 'permanent',
payload: { value: rollCoinValue() },
});
}
}
Reading the total
Because each ReelSet’s pins live on their own map, the total is a sum across all mini-reels:
function currentTotal(): number {
let total = 0;
for (const cell of cells) {
const pin = cell.reelSet.getPin(0, 0);
if (typeof pin?.payload?.value === 'number') total += pin.payload.value;
}
return total;
}
The demo recomputes this on every pin:placed and pin:expired, keeping the TOTAL display live.
Ending the feature
When the round counter runs out:
for (const cell of cells) {
const pin = cell.reelSet.getPin(0, 0);
if (pin) cell.reelSet.unpin(0, 0);
}
Each unpin fires pin:expired with reason 'explicit', clearing badges and zeroing the total.
Related
EmptySymbol. the blank-cell symbol class this recipe registers as the non-coin background- Collector symbol. same architecture + one collector that absorbs adjacent pin values
- Multiplier wild. payload pattern on a single full-grid ReelSet