diff --git a/changelog.js b/changelog.js
index b8da734..a516651 100644
--- a/changelog.js
+++ b/changelog.js
@@ -1,7 +1,21 @@
// Version tracking
-export const APP_VERSION = '1.0.26';
+export const APP_VERSION = '1.0.27';
export const CHANGELOG = {
+ '1.0.27': {
+ date: '2026-02-03',
+ changes: {
+ features: [
+ 'Revised accelerate-time (skip-ahead) flow: skipped preset days are styled as past, hidden when not current, and correctly numbered when starting a new day with Wake Up',
+ 'Add keyboard shortcut for Accelerate Time',
+ ],
+ improvements: [
+ 'Sound choices in settings are now applied correctly; sound loading is more reliable',
+ 'Nominations countdown resets when starting a new day; wake-up countdown shortened to 6 seconds',
+ 'Timer display no longer flickers when reloading',
+ ],
+ },
+ },
'1.0.26': {
date: '2026-02-03',
changes: {
diff --git a/cypress/e2e/02-first-time-user.cy.js b/cypress/e2e/02-first-time-user.cy.js
index a82cc4a..0f21d26 100644
--- a/cypress/e2e/02-first-time-user.cy.js
+++ b/cypress/e2e/02-first-time-user.cy.js
@@ -39,9 +39,8 @@ describe('First Time User Flow', () => {
cy.get('#startBtn .button-text').should('have.text', 'β° Wake Up!');
cy.get('#startBtn').should('not.be.disabled');
cy.get('#resetBtn').should('have.text', 'π Reset Day').and('be.disabled');
- cy.get('#accelerateBtn')
- .should('have.text', 'β© Accelerate Time')
- .and('be.disabled');
+ cy.get('#accelerateBtn .button-text').should('have.text', 'β© Accelerate Time');
+ cy.get('#accelerateBtn').should('be.disabled');
// Verify navigation buttons exist
cy.get('#settingsBtn').should('exist');
diff --git a/cypress/e2e/05-timer-behavior.cy.js b/cypress/e2e/05-timer-behavior.cy.js
index 0ee517f..68fd164 100644
--- a/cypress/e2e/05-timer-behavior.cy.js
+++ b/cypress/e2e/05-timer-behavior.cy.js
@@ -11,6 +11,10 @@ describe('Timer Behavior', () => {
// Settings dialog should be open on fresh start
cy.get('#settingsDialog').should('be.visible');
+ // Turn OFF effects (trigger change so playSoundEffects is updated before save)
+ cy.get('.tab-button[data-tab="effects"]').click();
+ cy.get('#playSoundEffects').uncheck().trigger('change');
+
// Save and Close settings
cy.get('#closeSettings').click();
cy.get('#settingsDialog').should('not.be.visible');
@@ -49,6 +53,11 @@ describe('Timer Behavior', () => {
cy.visit('/');
cy.get('#settingsDialog').should('be.visible');
+
+ // Turn OFF effects (trigger change so playSoundEffects is updated before save)
+ cy.get('.tab-button[data-tab="effects"]').click();
+ cy.get('#playSoundEffects').uncheck().trigger('change');
+
cy.get('#closeSettings').click();
cy.get('#settingsDialog').should('not.be.visible');
diff --git a/cypress/e2e/07-keyboard-shortcuts.cy.js b/cypress/e2e/07-keyboard-shortcuts.cy.js
index 06ed2df..a8271d8 100644
--- a/cypress/e2e/07-keyboard-shortcuts.cy.js
+++ b/cypress/e2e/07-keyboard-shortcuts.cy.js
@@ -21,6 +21,7 @@ describe('Keyboard Shortcuts', () => {
cy.get('#shortcutSettings').should('exist');
cy.get('#shortcutWakeUp').should('exist');
cy.get('#shortcutReset').should('exist');
+ cy.get('#shortcutAccelerate').should('exist');
cy.get('#shortcutFullscreen').should('exist');
cy.get('#shortcutInfo').should('exist');
@@ -28,6 +29,7 @@ describe('Keyboard Shortcuts', () => {
cy.get('#shortcutSettings').should('have.value', 'q');
cy.get('#shortcutWakeUp').should('have.value', 'Space');
cy.get('#shortcutReset').should('have.value', 'r');
+ cy.get('#shortcutAccelerate').should('have.value', 'a');
cy.get('#shortcutFullscreen').should('have.value', 'f');
cy.get('#shortcutInfo').should('have.value', 'i');
@@ -53,6 +55,7 @@ describe('Keyboard Shortcuts', () => {
// Verify other inputs are disabled during recording
cy.get('#shortcutSettings').should('be.disabled');
cy.get('#shortcutReset').should('be.disabled');
+ cy.get('#shortcutAccelerate').should('be.disabled');
cy.get('#shortcutFullscreen').should('be.disabled');
cy.get('#shortcutInfo').should('be.disabled');
@@ -66,6 +69,7 @@ describe('Keyboard Shortcuts', () => {
// Verify other inputs are re-enabled
cy.get('#shortcutSettings').should('not.be.disabled');
cy.get('#shortcutReset').should('not.be.disabled');
+ cy.get('#shortcutAccelerate').should('not.be.disabled');
cy.get('#shortcutFullscreen').should('not.be.disabled');
cy.get('#shortcutInfo').should('not.be.disabled');
});
@@ -161,6 +165,7 @@ describe('Keyboard Shortcuts', () => {
cy.get('#shortcutSettings').should('have.value', 'q');
cy.get('#shortcutWakeUp').should('have.value', 'Space');
cy.get('#shortcutReset').should('have.value', 'r');
+ cy.get('#shortcutAccelerate').should('have.value', 'a');
cy.get('#shortcutFullscreen').should('have.value', 'f');
cy.get('#shortcutInfo').should('have.value', 'i');
});
@@ -182,6 +187,7 @@ describe('Keyboard Shortcuts', () => {
cy.get('#shortcutSettings').should('have.value', '');
cy.get('#shortcutWakeUp').should('have.value', '');
cy.get('#shortcutReset').should('have.value', '');
+ cy.get('#shortcutAccelerate').should('have.value', '');
cy.get('#shortcutFullscreen').should('have.value', '');
cy.get('#shortcutInfo').should('have.value', '');
});
@@ -245,10 +251,10 @@ describe('Keyboard Shortcuts', () => {
cy.get('.shortcuts-container').should('have.css', 'grid-template-columns');
// Verify all shortcut items are present
- cy.get('.shortcut-item').should('have.length', 5);
+ cy.get('.shortcut-item').should('have.length', 6);
- // Verify the 5th item spans both columns
- cy.get('.shortcut-item:nth-child(5)').should(
+ // Verify the 6th item spans both columns
+ cy.get('.shortcut-item:nth-child(6)').should(
'have.css',
'grid-column',
'1 / -1'
diff --git a/cypress/e2e/08-preset-skips.cy.js b/cypress/e2e/08-preset-skips.cy.js
new file mode 100644
index 0000000..655b2a4
--- /dev/null
+++ b/cypress/e2e/08-preset-skips.cy.js
@@ -0,0 +1,172 @@
+describe('Preset skips and accelerate behaviour', () => {
+ beforeEach(() => {
+ cy.clearLocalStorage();
+ });
+
+ it('keeps all days while hiding only appropriate skipped presets', () => {
+ // Load page
+ cy.visit('/');
+
+ // Settings dialog open on first load
+ cy.get('#settingsDialog').should('be.visible');
+
+ // Game settings: 12 players
+ cy.get('#playerCount').clear().type('12');
+
+ // Turn OFF effects (trigger change so playSoundEffects is updated before save)
+ cy.get('.tab-button[data-tab="effects"]').click();
+ cy.get('#playSoundEffects').uncheck().trigger('change');
+
+ // Turn OFF music (trigger change so playMusic is updated before save)
+ cy.get('.tab-button[data-tab="music"]').click();
+ cy.get('#playMusic').uncheck().trigger('change');
+
+ // Start new game
+ cy.get('#startNewGame').click();
+ cy.get('#settingsDialog').should('not.be.visible');
+
+ // Capture the initial set of visible day labels (Day 1..Day N for this player count)
+ let initialDayLabels = [];
+ cy.get('#clocktowerPresets .clocktower-btn:visible .day').then(($spans) => {
+ const labels = Array.from($spans, (el) => el.textContent.trim());
+ initialDayLabels = labels.filter((t) => t.startsWith('Day '));
+ expect(initialDayLabels.length).to.be.greaterThan(0);
+ });
+
+ // Helper: assert visible day labels stay the same set as at the start
+ const expectSameDaySet = () => {
+ cy.get('#clocktowerPresets .clocktower-btn:visible .day').then(
+ ($spans) => {
+ const labels = Array.from($spans, (el) => el.textContent.trim());
+ const dayLabels = labels.filter((t) => t.startsWith('Day '));
+ expect(dayLabels).to.deep.equal(initialDayLabels);
+ }
+ );
+ };
+
+ // Initially we should have a stable set of Day presets
+ expectSameDaySet();
+
+ // Select the Day 4 preset while on Day 1
+ cy.contains('#clocktowerPresets .clocktower-btn .day', 'Day 4')
+ .closest('button')
+ .click();
+
+ // First three presets visible and marked as skipped (π)
+ cy.get('#clocktowerPresets .clocktower-btn')
+ .filter(':visible')
+ .eq(0)
+ .should('have.class', 'skipped-day');
+ cy.get('#clocktowerPresets .clocktower-btn')
+ .filter(':visible')
+ .eq(1)
+ .should('have.class', 'skipped-day');
+ cy.get('#clocktowerPresets .clocktower-btn')
+ .filter(':visible')
+ .eq(2)
+ .should('have.class', 'skipped-day');
+
+ // Still have the same visible day presets
+ expectSameDaySet();
+
+ // Day 4 preset click already started the timer; accelerate is enabled
+ // Use Accelerate Time (click then confirm)
+ cy.get('#accelerateBtn', { timeout: 20000 }).should('not.be.disabled');
+ cy.get('#accelerateBtn').click();
+ cy.get('#accelerateBtn').click();
+
+ // Wait for accelerated day to complete (Wake Up active again)
+ cy.contains('#startBtn .button-text', 'β° Wake Up!', {
+ timeout: 20000,
+ }).should('be.visible');
+
+ // After day end: no visible skipped presets, same Day set
+ cy.get('#clocktowerPresets .clocktower-btn.skipped-day:visible').should(
+ 'have.length',
+ 0
+ );
+ expectSameDaySet();
+
+ // In dusk with Wake Up active, click Day 4 for the next day
+ cy.contains('#clocktowerPresets .clocktower-btn .day', 'Day 4')
+ .closest('button')
+ .click();
+
+ // Now on Day 2: pattern Day 1, π, π, Day 2, Day 3..Day 9
+ cy.get('#clocktowerPresets .clocktower-btn')
+ .filter(':visible')
+ .then(($btns) => {
+ const labels = Array.from($btns, (btn) =>
+ btn.querySelector('.day').textContent.trim()
+ );
+ const classes = Array.from($btns, (btn) => btn.className);
+
+ expect(labels[0]).to.equal('Day 1');
+ expect(classes[1]).to.include('skipped-day');
+ expect(classes[2]).to.include('skipped-day');
+ expect(labels[3]).to.equal('Day 2');
+ // The remaining visible day labels should still include all later days
+ const remainingDays = labels
+ .slice(3)
+ .filter((t) => t.startsWith('Day '))
+ .map((t) => Number(t.replace('Day ', '')));
+ expect(remainingDays).to.include.members([3, 4, 5, 6, 7, 8, 9]);
+ });
+
+ // Day 4 preset click already started the timer; accelerate is enabled
+ cy.get('#accelerateBtn', { timeout: 20000 }).should('not.be.disabled');
+ cy.get('#accelerateBtn').click();
+ cy.get('#accelerateBtn').click();
+
+ cy.contains('#startBtn .button-text', 'β° Wake Up!', {
+ timeout: 20000,
+ }).should('be.visible');
+
+ // After second day end: no skulls, same Day set
+ cy.get('#clocktowerPresets .clocktower-btn.skipped-day:visible').should(
+ 'have.length',
+ 0
+ );
+ expectSameDaySet();
+
+ // In dusk again, click Day 4 for the next day
+ cy.contains('#clocktowerPresets .clocktower-btn .day', 'Day 4')
+ .closest('button')
+ .click();
+
+ // Now on Day 3: Day 1, Day 2, π, Day 3..Day 9
+ cy.get('#clocktowerPresets .clocktower-btn')
+ .filter(':visible')
+ .then(($btns) => {
+ const labels = Array.from($btns, (btn) =>
+ btn.querySelector('.day').textContent.trim()
+ );
+ const classes = Array.from($btns, (btn) => btn.className);
+
+ expect(labels[0]).to.equal('Day 1');
+ expect(labels[1]).to.equal('Day 2');
+ expect(classes[2]).to.include('skipped-day');
+ expect(labels[3]).to.equal('Day 3');
+ const remainingDays = labels
+ .slice(3)
+ .filter((t) => t.startsWith('Day '));
+ expect(remainingDays.length).to.be.greaterThan(0);
+ });
+
+ // Day 4 preset click already started the timer; accelerate is enabled
+ cy.get('#accelerateBtn', { timeout: 20000 }).should('not.be.disabled');
+ cy.get('#accelerateBtn').click();
+ cy.get('#accelerateBtn').click();
+
+ cy.contains('#startBtn .button-text', 'β° Wake Up!', {
+ timeout: 20000,
+ }).should('be.visible');
+
+ // After third day end: no skulls, same Day set
+ cy.get('#clocktowerPresets .clocktower-btn.skipped-day:visible').should(
+ 'have.length',
+ 0
+ );
+ expectSameDaySet();
+ });
+});
diff --git a/index.html b/index.html
index 400f13c..c31f036 100644
--- a/index.html
+++ b/index.html
@@ -31,6 +31,11 @@
+
-
+
00:00
-
Nominations open in 0s
-
+
-
@@ -313,9 +318,15 @@
-
-
@@ -645,6 +656,18 @@ Keyboard Shortcuts
/>
+
+
+