| name | basementuniverse-stopwatch |
| description | Use this skill when implementing timers, countdowns, or time-tracking features in browser-based games using the @basementuniverse/stopwatch library. Covers game loop integration, timer configuration, and time-based events.
|
Basement Universe Stopwatch
Use this skill when working with @basementuniverse/stopwatch.
Overview
The @basementuniverse/stopwatch library provides a high-precision timer designed for browser game loops. It uses performance.now() for accurate timing and supports both count-up (forward) and count-down (backward) modes with configurable rates, lifecycle callbacks, and milestone tracking.
When to Use This Skill
- Implementing timers or countdowns in browser-based games
- Creating time-limited game mechanics (e.g., timed challenges, power-up durations)
- Building speed-run or time-attack game modes
- Managing cooldowns, delays, or scheduled events in game systems
- Tracking elapsed/remaining time with pause/resume functionality
- Creating time-based progression systems with milestones
Core Concepts
Game Loop Integration
Critical requirement: Call stopwatch.update() every frame in your game loop:
import Stopwatch from '@basementuniverse/stopwatch';
const stopwatch = new Stopwatch({ unit: 's', direction: 'forward' });
function gameLoop() {
stopwatch.update();
requestAnimationFrame(gameLoop);
}
stopwatch.start();
gameLoop();
Without update() calls, the stopwatch won't:
- Check for finish conditions
- Trigger milestone callbacks
- Update internal state properly
Direction Modes
Forward (count-up):
- Starts at 0, counts up to
max
- Finishes when
time >= max (unless max is Infinity)
- Typical use: elapsed time, speedrun timers, stopwatches
Backward (count-down):
- Starts at
max, counts down to 0
- Finishes when
time <= 0
max must be finite (throws error if Infinity)
- Typical use: countdown timers, time limits, cooldowns
Time Units
All time values use the configured unit ('ms' or 's'):
- Constructor options (
max, milestones)
- Getters (
time, elapsed, remaining, pausedDuration)
adjustTime() parameter
Internally uses milliseconds via performance.now().
Common Patterns
Basic Timer (Count-up)
const timer = new Stopwatch({
unit: 's',
direction: 'forward',
max: Infinity,
});
timer.start();
console.log(timer.time);
Countdown Timer
const countdown = new Stopwatch({
unit: 's',
direction: 'backward',
max: 60,
onFinish: () => console.log('Time is up!'),
});
countdown.start();
console.log(countdown.remaining);
Timed Challenge with Milestones
const challenge = new Stopwatch({
unit: 's',
direction: 'forward',
max: 120,
milestones: [30, 60, 90],
onMilestone: (index) => {
console.log(`Checkpoint ${index + 1} reached!`);
},
onFinish: () => console.log('Challenge complete!'),
});
Slow-Motion / Fast-Forward
const slowMo = new Stopwatch({
unit: 's',
rate: 0.5,
direction: 'forward',
});
const fastForward = new Stopwatch({
unit: 's',
rate: 2.0,
direction: 'forward',
});
Time-Limited Power-Up
class PowerUp {
private stopwatch: Stopwatch;
activate(duration: number) {
this.stopwatch = new Stopwatch({
unit: 's',
direction: 'backward',
max: duration,
onFinish: () => this.deactivate(),
});
this.stopwatch.start();
}
update() {
this.stopwatch.update();
}
getRemainingTime(): number {
return this.stopwatch.remaining;
}
deactivate() {
}
}
State Management
Lifecycle Control
stopwatch.start();
stopwatch.stop();
stopwatch.reset();
stopwatch.pause();
stopwatch.resume();
stopwatch.running = true;
stopwatch.running = false;
stopwatch.paused = true;
stopwatch.paused = false;
Stop Behavior
Control reset behavior when stopping:
stopwatch.stop();
stopwatch.stop({ resetTime: false, resetPausedDuration: false });
stopwatch.stop({ resetTime: true, resetPausedDuration: false });
Time Adjustments
Manually adjust time without triggering callbacks:
stopwatch.adjustTime(5);
stopwatch.adjustTime(-2);
Reading State
stopwatch.running
stopwatch.paused
stopwatch.time
stopwatch.elapsed
stopwatch.remaining
stopwatch.progress
stopwatch.pausedDuration
Callbacks
All callbacks are optional:
const stopwatch = new Stopwatch({
onStart: () => console.log('Started'),
onStop: () => console.log('Stopped'),
onPause: () => console.log('Paused'),
onResume: () => console.log('Resumed'),
onFinish: () => console.log('Finished'),
onMilestone: (index) => console.log(`Milestone ${index}`),
});
Callback triggers:
onStart: First start (not when resuming from pause)
onResume: Resuming from paused state
onStop: When stopwatch is stopped (if actually stopping)
onPause: When paused
onFinish: When finish condition is met during update()
onMilestone: When milestone is crossed during update()
Milestones
Track specific time points:
const stopwatch = new Stopwatch({
unit: 's',
direction: 'forward',
milestones: [10, 20, 30],
onMilestone: (index) => {
const value = stopwatch.options.milestones[index];
console.log(`Reached milestone: ${value}s`);
},
});
Milestone behavior:
- Values use the configured
unit
onMilestone receives the index (not the value)
- Only fires once per milestone
- Fires during
update() when crossing threshold
- Multiple milestones crossed in one frame fire in traversal order
- Milestones already behind current time are marked as reached
Progress Tracking
For UI elements (progress bars, etc.):
const timer = new Stopwatch({
direction: 'forward',
max: 60,
});
timer.update();
const percent = timer.progress * 100;
const countdown = new Stopwatch({
direction: 'backward',
max: 60,
});
countdown.update();
const percent = countdown.progress * 100;
Progress edge cases:
- Forward with
max: Infinity → always returns 0
- When
max === 0 → returns 1
Best Practices
- Always call
update() in your game loop - Critical for proper operation
- Choose appropriate units - Use 's' for gameplay timers, 'ms' for high-precision needs
- Set finite
max for backward timers - Required, throws error otherwise
- Use milestones for discrete events - Better than polling time values
- Prefer lifecycle callbacks - More efficient than checking state changes manually
- Consider
paused time - Use elapsed for active time, time for displayed time
- Use
progress for UI - Pre-calculated 0-1 ratio for progress bars
Common Gotchas
- Forgetting
update(): Stopwatch won't finish or trigger milestones without it
- Backward with
Infinity: Throws error - backward timers need finite max
- Milestone indices:
onMilestone receives index, not value
- Stop behavior: Default
stop() resets time; use options to preserve state
- Time adjustments: Don't trigger callbacks; useful for external time manipulation
- Rate changes: Affects speed but not unit;
rate: 2 = 2x speed
Troubleshooting
Timer not finishing
- Ensure
update() is called every frame
- Check that
max is not Infinity for forward timers that should finish
- Verify direction matches expected behavior
Milestones not firing
- Ensure
update() is called every frame
- Check milestone values match configured
unit
- Verify
onMilestone callback is defined
Unexpected time values
- Check configured
unit matches expected unit
- Verify
rate is set correctly (default: 1)
- Consider
adjustTime() calls or manual time manipulation
References