Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2fc9352
chore(changelog): update changelog for version 1.0.27
chizmeeple Feb 3, 2026
978a455
chore: change accelerate time process
chizmeeple Feb 3, 2026
5fe75d1
chore: dim past day clocktower presets
chizmeeple Feb 3, 2026
6e3e04c
chore(tests): turn off effects in 05-timer-behavior.cy.js; make tests…
chizmeeple Feb 4, 2026
3665b72
chore(tests): add cypress spec to test preset skipping
chizmeeple Feb 4, 2026
dc91c29
chore: shorten wake-up countdown to 6 seconds
chizmeeple Feb 3, 2026
a3c0fb0
fix: prevent font flicker on timer display reload
chizmeeple Feb 3, 2026
790c7d8
fix: reset nominations countdown when starting a new day
chizmeeple Feb 3, 2026
91fc09c
feat: make it clear if we skipped a preset day
chizmeeple Feb 3, 2026
a00bcdb
fix: add 'past day' class to skipped presets
chizmeeple Feb 3, 2026
c97f0ac
chore: use different style for skipped days
chizmeeple Feb 3, 2026
69abc36
fix: honour skip-ahead numbering when starting a new day with Wake Up
chizmeeple Feb 3, 2026
9d691c5
fix: use the sounds we chose in the settings
chizmeeple Feb 3, 2026
a19cbdb
chore: fix sound loading (YT init)
chizmeeple Feb 3, 2026
960c77f
chore: add end days for each skipped preset
chizmeeple Feb 3, 2026
8e939df
feat: hide skipped presets when they're not the current day
chizmeeple Feb 3, 2026
ac152e6
fix: refine behaviour for hiding skipped presets
chizmeeple Feb 3, 2026
a45ac06
fix: more fixes for the skip preset feature
chizmeeple Feb 4, 2026
d3126f9
fix: address new sonarqube issues
chizmeeple Feb 3, 2026
68d3bbb
feat: add accelerate time keyboard shortcut
chizmeeple Feb 4, 2026
7ef6aec
feat: add shortcut hints to accelerate button
chizmeeple Feb 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion changelog.js
Original file line number Diff line number Diff line change
@@ -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: {
Expand Down
5 changes: 2 additions & 3 deletions cypress/e2e/02-first-time-user.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
9 changes: 9 additions & 0 deletions cypress/e2e/05-timer-behavior.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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');

Expand Down
12 changes: 9 additions & 3 deletions cypress/e2e/07-keyboard-shortcuts.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ 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');

// Verify default values are displayed (Space is converted to 'Space' for display)
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');

Expand All @@ -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');

Expand All @@ -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');
});
Expand Down Expand Up @@ -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');
});
Expand All @@ -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', '');
});
Expand Down Expand Up @@ -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'
Expand Down
172 changes: 172 additions & 0 deletions cypress/e2e/08-preset-skips.cy.js
Original file line number Diff line number Diff line change
@@ -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();
});
});
43 changes: 33 additions & 10 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@

<!-- Preload critical resources -->
<link rel="preload" href="styles.css" as="style" />
<link
rel="preload"
href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=Azeret+Mono:wght@600&display=swap"
as="style"
/>
<link
rel="preload"
href="sounds/end-of-day/cathedral-bell-v2.mp3"
Expand Down Expand Up @@ -128,35 +133,35 @@

<div class="container">
<div class="timer-display" role="timer" aria-live="polite">
<div class="day-display" role="status" aria-label="Current game day">
<output class="day-display" aria-label="Current game day">
<span id="currentDay">-</span>
<div class="pace-indicator">Normal</div>
</div>
</output>
<div class="time" aria-label="Timer display">
<span id="minutes" aria-label="minutes">00</span>:<span
id="seconds"
aria-label="seconds"
>00</span
>
</div>
<div
<output
id="nominationsCountdown"
class="nominations-countdown"
role="status"
aria-live="polite"
aria-label="Time until nominations open"
hidden
>
Nominations open in <span id="nominationsCountdownSeconds">0</span>s
</div>
</output>
<button id="startBtn" aria-label="Start or pause timer">
<span class="button-content">
<span class="button-text">⏰ Wake Up!</span>
<span id="wakeUpShortcutHint" class="shortcut-hint"></span>
</span>
</button>
</div>
<div class="controls" role="group" aria-label="Timer controls">
<fieldset class="controls">
<legend>Timer controls</legend>
<div class="time-inputs">
<div id="regularTimerControls">
<div
Expand Down Expand Up @@ -288,7 +293,7 @@
<!-- Day presets will be inserted here dynamically -->
</div>
</div>
</div>
</fieldset>
</div>

<!-- Bottom Controls Container -->
Expand All @@ -313,9 +318,15 @@
</div>

<div class="timer-buttons">
<button id="resetBtn" aria-label="Reset Day">↺ Reset Day</button>
<button id="accelerateBtn" aria-label="Hold to accelerate time">
⏩ Accelerate Time
<button id="resetBtn" aria-label="↺ Reset Day">↺ Reset Day</button>
<button
id="accelerateBtn"
aria-label="⏩ Accelerate Time (click then confirm)"
>
<span class="button-content">
<span class="button-text">⏩ Accelerate Time</span>
<span id="accelerateShortcutHint" class="shortcut-hint"></span>
</span>
</button>
</div>

Expand Down Expand Up @@ -645,6 +656,18 @@ <h3>Keyboard Shortcuts</h3>
/>
</label>
</div>
<div class="shortcut-item">
<label>
<span>Accelerate Time</span>
<input
type="text"
id="shortcutAccelerate"
class="shortcut-input"
placeholder="Press a key..."
readonly
/>
</label>
</div>
<div class="shortcut-item">
<label>
<span>Toggle Fullscreen</span>
Expand Down
Loading