git-watchtower 1.6.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/git-watchtower.js +49 -2
- package/package.json +6 -1
- package/sounds/README.md +34 -0
- package/src/casino/index.js +721 -0
- package/src/casino/sounds.js +245 -0
- package/src/cli/args.js +239 -0
- package/src/config/loader.js +329 -0
- package/src/config/schema.js +305 -0
- package/src/git/branch.js +428 -0
- package/src/git/commands.js +416 -0
- package/src/git/pr.js +111 -0
- package/src/git/remote.js +127 -0
- package/src/index.js +179 -0
- package/src/polling/engine.js +157 -0
- package/src/server/process.js +329 -0
- package/src/server/static.js +95 -0
- package/src/state/store.js +527 -0
- package/src/telemetry/analytics.js +142 -0
- package/src/telemetry/config.js +123 -0
- package/src/telemetry/index.js +93 -0
- package/src/ui/actions.js +425 -0
- package/src/ui/ansi.js +498 -0
- package/src/ui/keybindings.js +198 -0
- package/src/ui/renderer.js +1326 -0
- package/src/utils/async.js +219 -0
- package/src/utils/browser.js +40 -0
- package/src/utils/errors.js +490 -0
- package/src/utils/gitignore.js +174 -0
- package/src/utils/sound.js +33 -0
- package/src/utils/time.js +27 -0
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Casino Mode - Vegas-style feedback for git-watchtower
|
|
3
|
+
*
|
|
4
|
+
* Adds slot machine animations, marquee lights, win celebrations,
|
|
5
|
+
* and gamification stats to make waiting for CI/AI updates feel
|
|
6
|
+
* like hitting the jackpot.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { ansi, box } = require('../ui/ansi');
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Casino Mode State
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
let casinoEnabled = false;
|
|
16
|
+
let casinoStats = {
|
|
17
|
+
totalLinesAdded: 0,
|
|
18
|
+
totalLinesDeleted: 0,
|
|
19
|
+
consecutivePolls: 0,
|
|
20
|
+
pollsWithUpdates: 0,
|
|
21
|
+
bigWins: 0, // 500+ line changes
|
|
22
|
+
jackpots: 0, // 1000+ line changes
|
|
23
|
+
megaJackpots: 0, // 5000+ line changes
|
|
24
|
+
sessionStart: Date.now(),
|
|
25
|
+
totalPolls: 0, // Total lever pulls
|
|
26
|
+
nearMisses: 0, // Polls with no changes
|
|
27
|
+
lastHitTime: null, // Timestamp of last update
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Marquee animation state
|
|
31
|
+
let marqueeFrame = 0;
|
|
32
|
+
let marqueeInterval = null;
|
|
33
|
+
|
|
34
|
+
// Slot reel animation state
|
|
35
|
+
let slotReelFrame = 0;
|
|
36
|
+
let slotReelInterval = null;
|
|
37
|
+
let isSpinning = false;
|
|
38
|
+
let slotResult = null; // Final symbols to display
|
|
39
|
+
let slotResultIsWin = false; // Whether result was a win
|
|
40
|
+
let slotResultFlashFrame = 0; // Flash animation frame
|
|
41
|
+
let slotResultInterval = null; // Interval for result display/flash
|
|
42
|
+
let slotResultRenderCallback = null; // Callback for re-rendering
|
|
43
|
+
let slotResultLabel = null; // "NOTHING", "WIN", "BIG WIN", "JACKPOT" etc
|
|
44
|
+
|
|
45
|
+
// Win animation state
|
|
46
|
+
let winAnimationFrame = 0;
|
|
47
|
+
let winAnimationInterval = null;
|
|
48
|
+
let currentWinLevel = null;
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// Configuration
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
const SLOT_SYMBOLS = ['🍒', '🍋', '🍊', '🍇', '🔔', '💎', '7️⃣', '🎰'];
|
|
55
|
+
const MARQUEE_CHARS = ['◆', '◇', '●', '○', '★', '☆'];
|
|
56
|
+
const MARQUEE_COLORS = [
|
|
57
|
+
ansi.brightRed,
|
|
58
|
+
ansi.brightYellow,
|
|
59
|
+
ansi.brightGreen,
|
|
60
|
+
ansi.brightCyan,
|
|
61
|
+
ansi.brightBlue,
|
|
62
|
+
ansi.brightMagenta,
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
// Win level thresholds (lines added + deleted)
|
|
66
|
+
const WIN_LEVELS = {
|
|
67
|
+
small: { min: 1, max: 49, label: 'WIN', color: ansi.green },
|
|
68
|
+
medium: { min: 50, max: 199, label: 'NICE WIN!', color: ansi.yellow },
|
|
69
|
+
large: { min: 200, max: 499, label: 'BIG WIN!', color: ansi.brightYellow },
|
|
70
|
+
huge: { min: 500, max: 999, label: 'HUGE WIN!', color: ansi.brightMagenta },
|
|
71
|
+
jackpot: { min: 1000, max: 4999, label: '💰 JACKPOT! 💰', color: ansi.brightCyan },
|
|
72
|
+
mega: { min: 5000, max: Infinity, label: '🎰 MEGA JACKPOT!!! 🎰', color: ansi.brightRed },
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// Mode Control
|
|
77
|
+
// ============================================================================
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check if casino mode is enabled
|
|
81
|
+
* @returns {boolean}
|
|
82
|
+
*/
|
|
83
|
+
function isEnabled() {
|
|
84
|
+
return casinoEnabled;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Enable casino mode
|
|
89
|
+
*/
|
|
90
|
+
function enable() {
|
|
91
|
+
casinoEnabled = true;
|
|
92
|
+
startMarquee();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Disable casino mode
|
|
97
|
+
*/
|
|
98
|
+
function disable() {
|
|
99
|
+
casinoEnabled = false;
|
|
100
|
+
stopMarquee();
|
|
101
|
+
stopSlotReels();
|
|
102
|
+
stopWinAnimation();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Toggle casino mode
|
|
107
|
+
* @returns {boolean} New state
|
|
108
|
+
*/
|
|
109
|
+
function toggle() {
|
|
110
|
+
if (casinoEnabled) {
|
|
111
|
+
disable();
|
|
112
|
+
} else {
|
|
113
|
+
enable();
|
|
114
|
+
}
|
|
115
|
+
return casinoEnabled;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ============================================================================
|
|
119
|
+
// Marquee Border Animation
|
|
120
|
+
// ============================================================================
|
|
121
|
+
|
|
122
|
+
let marqueeCallback = null;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Set the render callback for marquee updates
|
|
126
|
+
* @param {Function} callback
|
|
127
|
+
*/
|
|
128
|
+
function setRenderCallback(callback) {
|
|
129
|
+
marqueeCallback = callback;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Start the marquee animation
|
|
134
|
+
*/
|
|
135
|
+
function startMarquee() {
|
|
136
|
+
if (marqueeInterval) return;
|
|
137
|
+
marqueeInterval = setInterval(() => {
|
|
138
|
+
marqueeFrame = (marqueeFrame + 1) % (MARQUEE_CHARS.length * MARQUEE_COLORS.length);
|
|
139
|
+
// Only trigger re-render if there's a callback and we're enabled
|
|
140
|
+
if (casinoEnabled && marqueeCallback) {
|
|
141
|
+
marqueeCallback();
|
|
142
|
+
}
|
|
143
|
+
}, 150);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Stop the marquee animation
|
|
148
|
+
*/
|
|
149
|
+
function stopMarquee() {
|
|
150
|
+
if (marqueeInterval) {
|
|
151
|
+
clearInterval(marqueeInterval);
|
|
152
|
+
marqueeInterval = null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get the current marquee border characters for a position
|
|
158
|
+
* @param {number} position - Position along the border (0-based)
|
|
159
|
+
* @param {number} total - Total border length
|
|
160
|
+
* @returns {string} Colored character
|
|
161
|
+
*/
|
|
162
|
+
function getMarqueeChar(position, total) {
|
|
163
|
+
const offset = (marqueeFrame + position) % MARQUEE_COLORS.length;
|
|
164
|
+
const charOffset = Math.floor((marqueeFrame + position) / 2) % MARQUEE_CHARS.length;
|
|
165
|
+
return MARQUEE_COLORS[offset] + MARQUEE_CHARS[charOffset] + ansi.reset;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get a single marquee character for a specific position on the border
|
|
170
|
+
* @param {number} row - Current row (for side borders)
|
|
171
|
+
* @param {number} height - Terminal height
|
|
172
|
+
* @param {'left' | 'right'} side - Which side
|
|
173
|
+
* @returns {string}
|
|
174
|
+
*/
|
|
175
|
+
function getMarqueeSideChar(row, height, side) {
|
|
176
|
+
if (!casinoEnabled) return ' ';
|
|
177
|
+
|
|
178
|
+
// For side borders, offset based on row position
|
|
179
|
+
const position = side === 'left' ? row : (height - row);
|
|
180
|
+
const offset = (marqueeFrame + position) % MARQUEE_COLORS.length;
|
|
181
|
+
const charOffset = Math.floor((marqueeFrame + position) / 2) % MARQUEE_CHARS.length;
|
|
182
|
+
return MARQUEE_COLORS[offset] + MARQUEE_CHARS[charOffset] + ansi.reset;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get casino mode header badge
|
|
187
|
+
* @returns {string}
|
|
188
|
+
*/
|
|
189
|
+
function getHeaderBadge() {
|
|
190
|
+
if (!casinoEnabled) return '';
|
|
191
|
+
|
|
192
|
+
// Flashing "MAX ADDICTION" badge
|
|
193
|
+
const flash = Math.floor(marqueeFrame / 2) % 2 === 0;
|
|
194
|
+
const colors = flash
|
|
195
|
+
? ansi.bgBrightMagenta + ansi.brightYellow + ansi.bold
|
|
196
|
+
: ansi.bgBrightYellow + ansi.brightMagenta + ansi.bold;
|
|
197
|
+
|
|
198
|
+
return ` ${colors} 🎰 MAX ADDICTION 🎰 ${ansi.reset}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Render a marquee border line
|
|
203
|
+
* @param {number} width - Terminal width
|
|
204
|
+
* @param {'top' | 'bottom'} position - Border position
|
|
205
|
+
* @returns {string}
|
|
206
|
+
*/
|
|
207
|
+
function renderMarqueeLine(width, position) {
|
|
208
|
+
if (!casinoEnabled) return '';
|
|
209
|
+
|
|
210
|
+
let line = '';
|
|
211
|
+
|
|
212
|
+
for (let i = 0; i < width; i++) {
|
|
213
|
+
// Top goes right (reverse direction), bottom goes left
|
|
214
|
+
const pos = position === 'top' ? (width - 1 - i) : i;
|
|
215
|
+
line += getMarqueeChar(pos, width * 2);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return line;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ============================================================================
|
|
222
|
+
// Slot Reel Animation
|
|
223
|
+
// ============================================================================
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Start slot reel spinning animation
|
|
227
|
+
* @param {Function} renderCallback - Called on each frame
|
|
228
|
+
*/
|
|
229
|
+
function startSlotReels(renderCallback) {
|
|
230
|
+
if (slotReelInterval || !casinoEnabled) return;
|
|
231
|
+
|
|
232
|
+
isSpinning = true;
|
|
233
|
+
slotReelFrame = 0;
|
|
234
|
+
|
|
235
|
+
slotReelInterval = setInterval(() => {
|
|
236
|
+
slotReelFrame++;
|
|
237
|
+
if (renderCallback) renderCallback();
|
|
238
|
+
}, 100); // 25% slower than original 80ms
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Stop slot reel animation and show result
|
|
243
|
+
* @param {boolean} hadUpdates - Whether this poll found updates (win)
|
|
244
|
+
* @param {Function} renderCallback - Called to re-render display
|
|
245
|
+
* @param {Object|null} winLevel - The win level object from getWinLevel()
|
|
246
|
+
*/
|
|
247
|
+
function stopSlotReels(hadUpdates = false, renderCallback = null, winLevel = null) {
|
|
248
|
+
if (slotReelInterval) {
|
|
249
|
+
clearInterval(slotReelInterval);
|
|
250
|
+
slotReelInterval = null;
|
|
251
|
+
}
|
|
252
|
+
isSpinning = false;
|
|
253
|
+
|
|
254
|
+
// Clear any existing result display
|
|
255
|
+
if (slotResultInterval) {
|
|
256
|
+
clearInterval(slotResultInterval);
|
|
257
|
+
slotResultInterval = null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
slotResultIsWin = hadUpdates;
|
|
261
|
+
slotResultFlashFrame = 0;
|
|
262
|
+
slotResultRenderCallback = renderCallback;
|
|
263
|
+
|
|
264
|
+
// Set result label based on win level
|
|
265
|
+
if (!hadUpdates) {
|
|
266
|
+
slotResultLabel = { text: 'NOTHING', color: ansi.gray, emoji: '😴' };
|
|
267
|
+
} else if (!winLevel || winLevel.key === 'small') {
|
|
268
|
+
slotResultLabel = { text: 'WIN', color: ansi.green, emoji: '✨' };
|
|
269
|
+
} else if (winLevel.key === 'medium') {
|
|
270
|
+
slotResultLabel = { text: 'NICE WIN', color: ansi.yellow, emoji: '🎉' };
|
|
271
|
+
} else if (winLevel.key === 'large') {
|
|
272
|
+
slotResultLabel = { text: 'BIG WIN', color: ansi.brightYellow, emoji: '🔥' };
|
|
273
|
+
} else if (winLevel.key === 'huge') {
|
|
274
|
+
slotResultLabel = { text: 'HUGE WIN', color: ansi.brightMagenta, emoji: '💥' };
|
|
275
|
+
} else if (winLevel.key === 'jackpot') {
|
|
276
|
+
slotResultLabel = { text: '💰 JACKPOT 💰', color: ansi.brightCyan, emoji: '7️⃣', isJackpot: true };
|
|
277
|
+
} else if (winLevel.key === 'mega') {
|
|
278
|
+
slotResultLabel = { text: '🎰💰 MEGA JACKPOT 💰🎰', color: ansi.brightRed, emoji: '7️⃣', isJackpot: true };
|
|
279
|
+
} else {
|
|
280
|
+
slotResultLabel = { text: 'WIN', color: ansi.green, emoji: '✨' };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (hadUpdates) {
|
|
284
|
+
// WIN: Pick symbol based on win level
|
|
285
|
+
let winSymbol;
|
|
286
|
+
if (slotResultLabel.isJackpot) {
|
|
287
|
+
winSymbol = '7️⃣'; // Classic jackpot sevens
|
|
288
|
+
} else {
|
|
289
|
+
winSymbol = SLOT_SYMBOLS[Math.floor(Math.random() * SLOT_SYMBOLS.length)];
|
|
290
|
+
}
|
|
291
|
+
slotResult = [winSymbol, winSymbol, winSymbol, winSymbol, winSymbol];
|
|
292
|
+
|
|
293
|
+
// Flash animation for wins (longer for jackpots)
|
|
294
|
+
const flashDuration = slotResultLabel.isJackpot ? 40 : 20;
|
|
295
|
+
slotResultInterval = setInterval(() => {
|
|
296
|
+
slotResultFlashFrame++;
|
|
297
|
+
if (slotResultFlashFrame > flashDuration) {
|
|
298
|
+
clearInterval(slotResultInterval);
|
|
299
|
+
slotResultInterval = null;
|
|
300
|
+
slotResult = null;
|
|
301
|
+
slotResultLabel = null;
|
|
302
|
+
if (slotResultRenderCallback) slotResultRenderCallback();
|
|
303
|
+
} else if (slotResultRenderCallback) {
|
|
304
|
+
slotResultRenderCallback();
|
|
305
|
+
}
|
|
306
|
+
}, 150);
|
|
307
|
+
} else {
|
|
308
|
+
// NO WIN: Show random final symbols
|
|
309
|
+
slotResult = [];
|
|
310
|
+
for (let i = 0; i < 5; i++) {
|
|
311
|
+
const idx = (slotReelFrame + i * 3) % SLOT_SYMBOLS.length;
|
|
312
|
+
slotResult.push(SLOT_SYMBOLS[idx]);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Display for 2 seconds then fade
|
|
316
|
+
setTimeout(() => {
|
|
317
|
+
slotResult = null;
|
|
318
|
+
slotResultLabel = null;
|
|
319
|
+
if (slotResultRenderCallback) slotResultRenderCallback();
|
|
320
|
+
}, 2000);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Get the current slot result label
|
|
326
|
+
* @returns {Object|null}
|
|
327
|
+
*/
|
|
328
|
+
function getSlotResultLabel() {
|
|
329
|
+
return slotResultLabel;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Check if slot reels are spinning
|
|
334
|
+
* @returns {boolean}
|
|
335
|
+
*/
|
|
336
|
+
function isSlotSpinning() {
|
|
337
|
+
return isSpinning;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Check if there's a slot result being displayed
|
|
342
|
+
* @returns {boolean}
|
|
343
|
+
*/
|
|
344
|
+
function hasSlotResult() {
|
|
345
|
+
return slotResult !== null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Check if slots are active (spinning or showing result)
|
|
350
|
+
* @returns {boolean}
|
|
351
|
+
*/
|
|
352
|
+
function isSlotsActive() {
|
|
353
|
+
return isSpinning || slotResult !== null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Get current slot reel display (5 reels) with emojis on white backgrounds
|
|
358
|
+
* @returns {string}
|
|
359
|
+
*/
|
|
360
|
+
function getSlotReelDisplay() {
|
|
361
|
+
// Show result if we have one
|
|
362
|
+
if (slotResult) {
|
|
363
|
+
const reels = [];
|
|
364
|
+
for (let i = 0; i < 5; i++) {
|
|
365
|
+
if (slotResultIsWin) {
|
|
366
|
+
// Flashing effect for wins - alternate between bright and dim
|
|
367
|
+
const flash = slotResultFlashFrame % 2 === 0;
|
|
368
|
+
const bg = flash ? ansi.bgBrightYellow : ansi.bgBrightWhite;
|
|
369
|
+
reels.push(`${bg} ${slotResult[i]} ${ansi.reset}`);
|
|
370
|
+
} else {
|
|
371
|
+
// Static pure white background for no-win results
|
|
372
|
+
reels.push(`${ansi.bgBrightWhite} ${slotResult[i]} ${ansi.reset}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return reels.join(`${ansi.bgBlack} ${ansi.reset}`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Show spinning reels
|
|
379
|
+
if (!isSpinning) return '';
|
|
380
|
+
|
|
381
|
+
const reels = [];
|
|
382
|
+
for (let i = 0; i < 5; i++) {
|
|
383
|
+
const idx = (slotReelFrame + i * 3) % SLOT_SYMBOLS.length;
|
|
384
|
+
// Each emoji on pure white background
|
|
385
|
+
reels.push(`${ansi.bgBrightWhite} ${SLOT_SYMBOLS[idx]} ${ansi.reset}`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Join with black background space between
|
|
389
|
+
return reels.join(`${ansi.bgBlack} ${ansi.reset}`);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ============================================================================
|
|
393
|
+
// Win Animations
|
|
394
|
+
// ============================================================================
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Get win level based on total lines changed
|
|
398
|
+
* @param {number} linesChanged - Total lines (added + deleted)
|
|
399
|
+
* @returns {Object|null}
|
|
400
|
+
*/
|
|
401
|
+
function getWinLevel(linesChanged) {
|
|
402
|
+
for (const [key, level] of Object.entries(WIN_LEVELS)) {
|
|
403
|
+
if (linesChanged >= level.min && linesChanged <= level.max) {
|
|
404
|
+
return { key, ...level };
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Trigger a win animation
|
|
412
|
+
* @param {number} linesAdded
|
|
413
|
+
* @param {number} linesDeleted
|
|
414
|
+
* @param {Function} renderCallback
|
|
415
|
+
*/
|
|
416
|
+
function triggerWin(linesAdded, linesDeleted, renderCallback) {
|
|
417
|
+
if (!casinoEnabled) return;
|
|
418
|
+
|
|
419
|
+
const totalLines = linesAdded + linesDeleted;
|
|
420
|
+
currentWinLevel = getWinLevel(totalLines);
|
|
421
|
+
|
|
422
|
+
if (!currentWinLevel) return;
|
|
423
|
+
|
|
424
|
+
// Update stats
|
|
425
|
+
casinoStats.totalLinesAdded += linesAdded;
|
|
426
|
+
casinoStats.totalLinesDeleted += linesDeleted;
|
|
427
|
+
casinoStats.pollsWithUpdates++;
|
|
428
|
+
|
|
429
|
+
if (totalLines >= 500) casinoStats.bigWins++;
|
|
430
|
+
if (totalLines >= 1000) casinoStats.jackpots++;
|
|
431
|
+
if (totalLines >= 5000) casinoStats.megaJackpots++;
|
|
432
|
+
|
|
433
|
+
// Start animation
|
|
434
|
+
winAnimationFrame = 0;
|
|
435
|
+
stopWinAnimation();
|
|
436
|
+
|
|
437
|
+
winAnimationInterval = setInterval(() => {
|
|
438
|
+
winAnimationFrame++;
|
|
439
|
+
if (winAnimationFrame > 20) {
|
|
440
|
+
stopWinAnimation();
|
|
441
|
+
currentWinLevel = null;
|
|
442
|
+
}
|
|
443
|
+
if (renderCallback) renderCallback();
|
|
444
|
+
}, 100);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Stop win animation
|
|
449
|
+
*/
|
|
450
|
+
function stopWinAnimation() {
|
|
451
|
+
if (winAnimationInterval) {
|
|
452
|
+
clearInterval(winAnimationInterval);
|
|
453
|
+
winAnimationInterval = null;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Get current win animation display
|
|
459
|
+
* @param {number} width - Available width
|
|
460
|
+
* @returns {string}
|
|
461
|
+
*/
|
|
462
|
+
function getWinDisplay(width) {
|
|
463
|
+
if (!currentWinLevel || !casinoEnabled) return '';
|
|
464
|
+
|
|
465
|
+
const flashOn = winAnimationFrame % 2 === 0;
|
|
466
|
+
const label = currentWinLevel.label;
|
|
467
|
+
const color = flashOn ? currentWinLevel.color : ansi.dim + currentWinLevel.color;
|
|
468
|
+
|
|
469
|
+
const padding = Math.max(0, Math.floor((width - label.length) / 2));
|
|
470
|
+
|
|
471
|
+
return color + ' '.repeat(padding) + label + ' '.repeat(padding) + ansi.reset;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Check if win animation is active
|
|
476
|
+
* @returns {boolean}
|
|
477
|
+
*/
|
|
478
|
+
function isWinAnimating() {
|
|
479
|
+
return currentWinLevel !== null;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// ============================================================================
|
|
483
|
+
// Loss/Failure Effects
|
|
484
|
+
// ============================================================================
|
|
485
|
+
|
|
486
|
+
let lossAnimationFrame = 0;
|
|
487
|
+
let lossAnimationInterval = null;
|
|
488
|
+
let lossMessage = null;
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Trigger a loss animation (merge conflict, switch failure)
|
|
492
|
+
* @param {string} message - Loss message
|
|
493
|
+
* @param {Function} renderCallback
|
|
494
|
+
*/
|
|
495
|
+
function triggerLoss(message, renderCallback) {
|
|
496
|
+
if (!casinoEnabled) return;
|
|
497
|
+
|
|
498
|
+
lossMessage = message;
|
|
499
|
+
lossAnimationFrame = 0;
|
|
500
|
+
stopLossAnimation();
|
|
501
|
+
|
|
502
|
+
lossAnimationInterval = setInterval(() => {
|
|
503
|
+
lossAnimationFrame++;
|
|
504
|
+
if (lossAnimationFrame > 15) {
|
|
505
|
+
stopLossAnimation();
|
|
506
|
+
lossMessage = null;
|
|
507
|
+
}
|
|
508
|
+
if (renderCallback) renderCallback();
|
|
509
|
+
}, 120);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Stop loss animation
|
|
514
|
+
*/
|
|
515
|
+
function stopLossAnimation() {
|
|
516
|
+
if (lossAnimationInterval) {
|
|
517
|
+
clearInterval(lossAnimationInterval);
|
|
518
|
+
lossAnimationInterval = null;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Get loss animation display
|
|
524
|
+
* @param {number} width
|
|
525
|
+
* @returns {string}
|
|
526
|
+
*/
|
|
527
|
+
function getLossDisplay(width) {
|
|
528
|
+
if (!lossMessage || !casinoEnabled) return '';
|
|
529
|
+
|
|
530
|
+
const flashOn = lossAnimationFrame % 2 === 0;
|
|
531
|
+
const symbols = '💀 ';
|
|
532
|
+
const display = `${symbols}${lossMessage}${symbols}`;
|
|
533
|
+
const color = flashOn ? ansi.bgRed + ansi.white : ansi.bgBlack + ansi.red;
|
|
534
|
+
|
|
535
|
+
const padding = Math.max(0, Math.floor((width - display.length) / 2));
|
|
536
|
+
|
|
537
|
+
return color + ' '.repeat(padding) + display + ' '.repeat(padding) + ansi.reset;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Check if loss animation is active
|
|
542
|
+
* @returns {boolean}
|
|
543
|
+
*/
|
|
544
|
+
function isLossAnimating() {
|
|
545
|
+
return lossMessage !== null;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ============================================================================
|
|
549
|
+
// Stats Tracking
|
|
550
|
+
// ============================================================================
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Record a poll (each pull of the lever)
|
|
554
|
+
* @param {boolean} hadUpdates - Whether updates were found
|
|
555
|
+
*/
|
|
556
|
+
function recordPoll(hadUpdates) {
|
|
557
|
+
if (!casinoEnabled) return;
|
|
558
|
+
|
|
559
|
+
casinoStats.totalPolls++;
|
|
560
|
+
|
|
561
|
+
if (hadUpdates) {
|
|
562
|
+
casinoStats.consecutivePolls++;
|
|
563
|
+
casinoStats.lastHitTime = Date.now();
|
|
564
|
+
} else {
|
|
565
|
+
casinoStats.nearMisses++;
|
|
566
|
+
// Reset streak on miss
|
|
567
|
+
casinoStats.consecutivePolls = 0;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Get current session stats
|
|
573
|
+
* @returns {Object}
|
|
574
|
+
*/
|
|
575
|
+
function getStats() {
|
|
576
|
+
const elapsed = Date.now() - casinoStats.sessionStart;
|
|
577
|
+
const hours = Math.floor(elapsed / 3600000);
|
|
578
|
+
const minutes = Math.floor((elapsed % 3600000) / 60000);
|
|
579
|
+
|
|
580
|
+
// Calculate hit rate (percentage of polls that had updates)
|
|
581
|
+
const hitRate = casinoStats.totalPolls > 0
|
|
582
|
+
? Math.round((casinoStats.pollsWithUpdates / casinoStats.totalPolls) * 100)
|
|
583
|
+
: 0;
|
|
584
|
+
|
|
585
|
+
// Time since last hit
|
|
586
|
+
let timeSinceLastHit = 'Never';
|
|
587
|
+
if (casinoStats.lastHitTime) {
|
|
588
|
+
const sinceHit = Date.now() - casinoStats.lastHitTime;
|
|
589
|
+
const hitMins = Math.floor(sinceHit / 60000);
|
|
590
|
+
const hitSecs = Math.floor((sinceHit % 60000) / 1000);
|
|
591
|
+
timeSinceLastHit = hitMins > 0 ? `${hitMins}m ${hitSecs}s` : `${hitSecs}s`;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Luck meter - weighted random that trends with recent activity
|
|
595
|
+
const baseLuck = 50 + Math.random() * 30;
|
|
596
|
+
const streakBonus = Math.min(casinoStats.consecutivePolls * 5, 20);
|
|
597
|
+
const luckMeter = Math.min(Math.round(baseLuck + streakBonus), 99);
|
|
598
|
+
|
|
599
|
+
// House edge - oscillates between 55% and 100%
|
|
600
|
+
const houseEdge = Math.round(55 + Math.random() * 45);
|
|
601
|
+
|
|
602
|
+
// Vibes quality - random emoji that changes slowly (based on seconds)
|
|
603
|
+
const vibesEmojis = ['😎', '🔥', '✨', '💫', '🌟', '⚡', '🎯', '💪', '🚀', '💯'];
|
|
604
|
+
const vibesIndex = Math.floor(Date.now() / 3000) % vibesEmojis.length;
|
|
605
|
+
const vibesQuality = vibesEmojis[vibesIndex];
|
|
606
|
+
|
|
607
|
+
// Dopamine hits - based on updates received, with multiplier for big wins
|
|
608
|
+
const baseHits = casinoStats.pollsWithUpdates;
|
|
609
|
+
const bonusHits = casinoStats.bigWins * 2 + casinoStats.jackpots * 5 + casinoStats.megaJackpots * 10;
|
|
610
|
+
const dopamineHits = baseHits + bonusHits;
|
|
611
|
+
|
|
612
|
+
// Net winnings: total lines gained minus poll cost (1 per poll)
|
|
613
|
+
const totalLines = casinoStats.totalLinesAdded + casinoStats.totalLinesDeleted;
|
|
614
|
+
const netWinnings = totalLines - casinoStats.totalPolls;
|
|
615
|
+
|
|
616
|
+
return {
|
|
617
|
+
...casinoStats,
|
|
618
|
+
sessionDuration: hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`,
|
|
619
|
+
totalLines,
|
|
620
|
+
hitRate,
|
|
621
|
+
timeSinceLastHit,
|
|
622
|
+
luckMeter,
|
|
623
|
+
houseEdge,
|
|
624
|
+
vibesQuality,
|
|
625
|
+
dopamineHits,
|
|
626
|
+
netWinnings,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Reset session stats
|
|
632
|
+
*/
|
|
633
|
+
function resetStats() {
|
|
634
|
+
casinoStats = {
|
|
635
|
+
totalLinesAdded: 0,
|
|
636
|
+
totalLinesDeleted: 0,
|
|
637
|
+
consecutivePolls: 0,
|
|
638
|
+
pollsWithUpdates: 0,
|
|
639
|
+
bigWins: 0,
|
|
640
|
+
jackpots: 0,
|
|
641
|
+
megaJackpots: 0,
|
|
642
|
+
sessionStart: Date.now(),
|
|
643
|
+
totalPolls: 0,
|
|
644
|
+
nearMisses: 0,
|
|
645
|
+
lastHitTime: null,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Get stats display for footer
|
|
651
|
+
* @returns {string}
|
|
652
|
+
*/
|
|
653
|
+
function getStatsDisplay() {
|
|
654
|
+
if (!casinoEnabled) return '';
|
|
655
|
+
|
|
656
|
+
const stats = getStats();
|
|
657
|
+
const linesDisplay = stats.totalLines > 0
|
|
658
|
+
? `${ansi.brightGreen}+${stats.totalLinesAdded}${ansi.reset}/${ansi.brightRed}-${stats.totalLinesDeleted}${ansi.reset}`
|
|
659
|
+
: '0';
|
|
660
|
+
|
|
661
|
+
let display = `${ansi.yellow}🎰 Winnings: ${linesDisplay} lines`;
|
|
662
|
+
|
|
663
|
+
if (stats.consecutivePolls > 1) {
|
|
664
|
+
display += ` ${ansi.brightMagenta}(${stats.consecutivePolls}x streak!)${ansi.reset}`;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (stats.jackpots > 0) {
|
|
668
|
+
display += ` ${ansi.brightCyan}💰×${stats.jackpots}${ansi.reset}`;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return display + ansi.reset;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// ============================================================================
|
|
675
|
+
// Module Exports
|
|
676
|
+
// ============================================================================
|
|
677
|
+
|
|
678
|
+
module.exports = {
|
|
679
|
+
// Mode control
|
|
680
|
+
isEnabled,
|
|
681
|
+
enable,
|
|
682
|
+
disable,
|
|
683
|
+
toggle,
|
|
684
|
+
|
|
685
|
+
// Render callback
|
|
686
|
+
setRenderCallback,
|
|
687
|
+
|
|
688
|
+
// Marquee
|
|
689
|
+
renderMarqueeLine,
|
|
690
|
+
getMarqueeSideChar,
|
|
691
|
+
getHeaderBadge,
|
|
692
|
+
startMarquee,
|
|
693
|
+
stopMarquee,
|
|
694
|
+
|
|
695
|
+
// Slot reels
|
|
696
|
+
startSlotReels,
|
|
697
|
+
stopSlotReels,
|
|
698
|
+
isSlotSpinning,
|
|
699
|
+
hasSlotResult,
|
|
700
|
+
isSlotsActive,
|
|
701
|
+
getSlotReelDisplay,
|
|
702
|
+
getSlotResultLabel,
|
|
703
|
+
|
|
704
|
+
// Win effects
|
|
705
|
+
triggerWin,
|
|
706
|
+
getWinDisplay,
|
|
707
|
+
isWinAnimating,
|
|
708
|
+
getWinLevel,
|
|
709
|
+
WIN_LEVELS,
|
|
710
|
+
|
|
711
|
+
// Loss effects
|
|
712
|
+
triggerLoss,
|
|
713
|
+
getLossDisplay,
|
|
714
|
+
isLossAnimating,
|
|
715
|
+
|
|
716
|
+
// Stats
|
|
717
|
+
recordPoll,
|
|
718
|
+
getStats,
|
|
719
|
+
resetStats,
|
|
720
|
+
getStatsDisplay,
|
|
721
|
+
};
|