1- import { LANES } from "../tube/space" ;
1+ // Pattern events: instead of rolling density dice per cell, sections place
2+ // deliberate "events" — a gate, a slalom cluster, a rail — separated by
3+ // blank cells. The player always reads one intentional thing at a time, and
4+ // the section can stretch the gaps when it is generated at boost speed.
5+
26import {
3- clearLane ,
47 corridorMask ,
5- hasLane ,
68 invertLaneMask ,
79 laneMask ,
810 normalizeLane ,
@@ -12,100 +14,118 @@ import {
1214import type { Rng } from "./rng" ;
1315
1416export const PATTERN_FAMILIES = [
15- "semiRandom " ,
16- "staggered " ,
17+ "slalom " ,
18+ "gates " ,
1719 "spiral" ,
18- "line " ,
19- "wall " ,
20+ "rail " ,
21+ "weave " ,
2022] as const ;
2123
2224export type PatternFamily = ( typeof PATTERN_FAMILIES ) [ number ] ;
2325
24- export type PatternInput = {
26+ export type PatternEvent = {
27+ // One mask per cell of the event's span, relative to the event's start.
28+ readonly masks : readonly LaneMask [ ] ;
29+ // Blank cells after the event at base speed; scaled up while boosted.
30+ readonly gapAfter : number ;
31+ } ;
32+
33+ export type EventInput = {
2534 readonly rng : Rng ;
2635 readonly family : PatternFamily ;
27- readonly cellIndex : number ;
28- readonly safeLane : Lane ;
36+ readonly eventIndex : number ;
37+ // Safe path from the event's first cell onward.
38+ readonly safeLanes : readonly Lane [ ] ;
2939 readonly safeWidth : number ;
30- readonly density : number ;
31- readonly sectionId : number ;
40+ // 0 (warmup) … 1 (late game).
41+ readonly difficulty : number ;
3242} ;
3343
34- const addRandomBlockers = (
35- rng : Rng ,
36- availableMask : LaneMask ,
37- density : number ,
38- ) : LaneMask =>
39- Array . from ( { length : LANES } , ( _ , lane ) => lane )
40- . filter ( ( lane ) => hasLane ( availableMask , lane ) )
41- . reduce < LaneMask > (
42- ( mask , lane ) => ( rng . next ( ) < density ? mask | laneMask ( lane ) : mask ) ,
43- 0 ,
44- ) ;
45-
46- const createSemiRandomMask = ( input : PatternInput , availableMask : LaneMask ) : LaneMask =>
47- addRandomBlockers ( input . rng , availableMask , input . density ) ;
48-
49- const createStaggeredMask = ( input : PatternInput , availableMask : LaneMask ) : LaneMask => {
50- const side = Math . floor ( input . cellIndex / 2 ) % 2 === 0 ? - 1 : 1 ;
51- const offsets = [ side * 2 , side * 3 , - side * 5 ] ;
52-
53- return offsets . reduce < LaneMask > ( ( mask , offset ) => {
54- const lane = normalizeLane ( input . safeLane + offset ) ;
55- return hasLane ( availableMask , lane ) ? mask | laneMask ( lane ) : mask ;
56- } , 0 ) ;
57- } ;
44+ const lanesToMask = ( lanes : readonly number [ ] ) : LaneMask =>
45+ lanes . reduce < LaneMask > ( ( mask , lane ) => mask | laneMask ( normalizeLane ( lane ) ) , 0 ) ;
46+
47+ const safeLaneAt = ( input : EventInput , offset : number ) : Lane =>
48+ input . safeLanes [ Math . min ( offset , input . safeLanes . length - 1 ) ] ?? 0 ;
5849
59- const createSpiralMask = ( input : PatternInput , availableMask : LaneMask ) : LaneMask => {
60- const direction = input . sectionId % 2 === 0 ? 1 : - 1 ;
61- const baseLane = normalizeLane ( input . sectionId * 3 ) ;
62- const leadLane = normalizeLane ( baseLane + input . cellIndex * direction ) ;
63- const mask = [ leadLane , leadLane + direction , leadLane + direction * 3 ]
64- . reduce < LaneMask > ( ( acc , lane ) => acc | laneMask ( lane ) , 0 ) ;
50+ // One small cluster right beside the corridor, alternating sides: cubes
51+ // flick past the player's shoulder and punish drifting.
52+ const slalomEvent = ( input : EventInput ) : PatternEvent => {
53+ const side = input . eventIndex % 2 === 0 ? 1 : - 1 ;
54+ const edge = input . safeWidth + 1 ;
55+ const safe = safeLaneAt ( input , 0 ) ;
56+ const offsets = input . difficulty > 0.4 ? [ edge , edge + 1 ] : [ edge ] ;
6557
66- return mask & availableMask ;
58+ return {
59+ masks : [ lanesToMask ( offsets . map ( ( offset ) => safe + side * offset ) ) ] ,
60+ gapAfter : input . difficulty > 0.7 ? 2 : 3 ,
61+ } ;
6762} ;
6863
69- const createLineMask = ( input : PatternInput , availableMask : LaneMask ) : LaneMask => {
70- const firstLine = normalizeLane ( input . sectionId + Math . floor ( input . cellIndex / 10 ) ) ;
71- const secondLine = normalizeLane ( firstLine + 6 ) ;
64+ // A full ring with a gap centered on the safe path: see the wall, find the
65+ // hole, steer to it.
66+ const gatesEvent = ( input : EventInput ) : PatternEvent => {
67+ const gapHalf = input . difficulty > 0.5 ? 1 : 2 ;
68+ const safe = safeLaneAt ( input , 0 ) ;
7269
73- return [ firstLine , secondLine ] . reduce < LaneMask > (
74- ( mask , lane ) => ( hasLane ( availableMask , lane ) ? mask | laneMask ( lane ) : mask ) ,
75- 0 ,
76- ) ;
70+ return {
71+ masks : [ invertLaneMask ( corridorMask ( safe , Math . max ( gapHalf , input . safeWidth ) ) ) ] ,
72+ gapAfter : 5 - Math . round ( input . difficulty * 2 ) ,
73+ } ;
7774} ;
7875
79- const createWallMask = ( input : PatternInput , safeMask : LaneMask ) : LaneMask => {
80- if ( input . cellIndex % 9 !== 0 ) {
81- return createSemiRandomMask ( input , invertLaneMask ( safeMask ) ) & 0b001_001_001_001 ;
82- }
76+ // A short two-lane-thick helix sweeping around the ring beside the corridor,
77+ // alternating sweep direction between events.
78+ const spiralEvent = ( input : EventInput ) : PatternEvent => {
79+ const direction = input . eventIndex % 2 === 0 ? 1 : - 1 ;
80+ const span = 5 ;
81+ const masks = Array . from ( { length : span } , ( _ , step ) => {
82+ const base = safeLaneAt ( input , step ) + direction * ( input . safeWidth + 1 + step ) ;
83+ return lanesToMask ( [ base , base + direction ] ) ;
84+ } ) ;
85+
86+ return { masks, gapAfter : 4 } ;
87+ } ;
8388
84- const widerSafeMask = corridorMask ( input . safeLane , Math . max ( input . safeWidth , 1 ) ) ;
85- const wallMask = invertLaneMask ( widerSafeMask ) ;
86- const reliefLane = normalizeLane ( input . safeLane + ( input . sectionId % 2 === 0 ? 4 : - 4 ) ) ;
89+ // A solid two-lane ribbon riding one side of the corridor for several cells:
90+ // a wall that travels with you and frames the path.
91+ const railEvent = ( input : EventInput ) : PatternEvent => {
92+ const side = input . eventIndex % 2 === 0 ? - 1 : 1 ;
93+ const span = 8 + input . rng . nextInt ( 0 , 4 ) ;
94+ const masks = Array . from ( { length : span } , ( _ , step ) => {
95+ const safe = safeLaneAt ( input , step ) ;
96+ return lanesToMask ( [
97+ safe + side * ( input . safeWidth + 1 ) ,
98+ safe + side * ( input . safeWidth + 2 ) ,
99+ ] ) ;
100+ } ) ;
101+
102+ return { masks, gapAfter : 4 } ;
103+ } ;
87104
88- return clearLane ( wallMask , reliefLane ) ;
105+ // A pinch: cubes flanking both sides of the corridor at once — hold the line.
106+ const weaveEvent = ( input : EventInput ) : PatternEvent => {
107+ const edge = input . safeWidth + 1 ;
108+ const safe = safeLaneAt ( input , 0 ) ;
109+ const offsets =
110+ input . difficulty > 0.5 ? [ edge , edge + 1 , - edge , - edge - 1 ] : [ edge , - edge ] ;
111+
112+ return {
113+ masks : [ lanesToMask ( offsets . map ( ( offset ) => safe + offset ) ) ] ,
114+ gapAfter : 3 ,
115+ } ;
89116} ;
90117
91- export const createPatternMask = ( input : PatternInput ) : LaneMask => {
92- const safeMask = corridorMask ( input . safeLane , input . safeWidth ) ;
93- const availableMask = invertLaneMask ( safeMask ) ;
94-
95- const rawMask = ( ( ) => {
96- switch ( input . family ) {
97- case "semiRandom" :
98- return createSemiRandomMask ( input , availableMask ) ;
99- case "staggered" :
100- return createStaggeredMask ( input , availableMask ) ;
101- case "spiral" :
102- return createSpiralMask ( input , availableMask ) ;
103- case "line" :
104- return createLineMask ( input , availableMask ) ;
105- case "wall" :
106- return createWallMask ( input , safeMask ) ;
107- }
108- } ) ( ) ;
109-
110- return rawMask & availableMask ;
118+ export const createPatternEvent = ( input : EventInput ) : PatternEvent => {
119+ switch ( input . family ) {
120+ case "slalom" :
121+ return slalomEvent ( input ) ;
122+ case "gates" :
123+ return gatesEvent ( input ) ;
124+ case "spiral" :
125+ return spiralEvent ( input ) ;
126+ case "rail" :
127+ return railEvent ( input ) ;
128+ case "weave" :
129+ return weaveEvent ( input ) ;
130+ }
111131} ;
0 commit comments