diff --git a/README.md b/README.md index 4aea002..02f5904 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,8 @@ emptying. Nothing is sent anywhere. Export/import lets you back up or move your data. - **Time-based estimates.** LSOS can't see the game. Business production runs while LSOS is open and tracking ("play time"). LSOS opens paused: press Start once you have loaded into a - session, or turn on tracking on open in Settings. Real-world cooldowns such as the Lucky Wheel + session, or turn on tracking on open in Settings. Played a while without LSOS? Skip time + moves every play-time tracker forward by however long you enter. Real-world cooldowns such as the Lucky Wheel run on the wall clock even when LSOS is closed. - **Sourced game data.** Rates and values come from sources such as gta.wiki. Where none exists for your upgrades, LSOS uses a best estimate and labels those numbers "(est.)". diff --git a/docs/decisions/008-clock-details.md b/docs/decisions/008-clock-details.md index 61e2d54..21e427c 100644 --- a/docs/decisions/008-clock-details.md +++ b/docs/decisions/008-clock-details.md @@ -33,6 +33,11 @@ browser events, listeners), `src/components/ClockBar.svelte` (indicator, toggle, - **Leaving the page** (`pagehide`) counts up to that moment and stops measuring, so time away is not counted if the page returns from the back/forward cache. `visibilitychange` fires after `pagehide` and must not restart the clock. +- **Skip time** (issue #70): the clock bar's "Skip time" button takes a duration the user played + without LSOS tracking (same formats as resync: `10`, `1h 30m`, `1:30`) and reports it through + `onPlayTime` like any counted stretch, so every play-time tracker catches up at once. Real-time + timers are left alone: real time already passed. There is no undo, because trackers only take + positive stretches; the button shows the exact amount (`Skip 1:30:00`) before it is pressed. ## Why diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 1dcdd09..043b149 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -18,7 +18,7 @@ free number, add one line below. **Replacing one:** write a new decision, then m - [005](005-weekly-data.md) Weekly events come from a JSON file updated by PR, not a live API - [006](006-persisted-state.md) Persisted state is versioned, migrated on load, exportable as JSON - [007](007-state-store.md) State store: unreadable data blocks saving like a newer version; import overrides -- [008](008-clock-details.md) Clock details: session play time, opens paused (setting), throttled ticks counted, long gaps asked about +- [008](008-clock-details.md) Clock details: session play time, opens paused (setting), throttled ticks counted, long gaps asked about, Skip time for play without LSOS - [009](009-terminal-design.md) Terminal look: tokens in `app.css`, self-hosted Oswald (OFL), browser-style tabs, base components in `src/components/ui/` - [010](010-resync.md) Resync: `EditableValue` for every tracked value, `BusinessBars` with Sold/Raided/Full and supply deliveries, undo keeps progress since, five-segment bars you can drag (duration format amended by 022; presets and edit view amended by 029) - [011](011-owned-properties.md) Owned properties and upgrades: catalog in `src/data/properties.ts`, toggle/choice upgrades (count: 016), Settings panel and Businesses gear diff --git a/src/components/ClockBar.svelte b/src/components/ClockBar.svelte index 4311ae2..5e3abf1 100644 --- a/src/components/ClockBar.svelte +++ b/src/components/ClockBar.svelte @@ -1,6 +1,8 @@ @@ -32,9 +68,48 @@ + +{#if skipping} +
+{:else if skipped} ++ Skipped {skipped} of play time. + +
+{/if} + {#if paused}Press Start once you have loaded into a GTA Online session, not during loading screens: that is @@ -119,6 +194,43 @@ padding: var(--space-2) var(--space-4); } + .skip { + align-items: center; + border-bottom: 1px solid var(--line); + display: flex; + flex-wrap: wrap; + font-size: var(--text-sm); + gap: var(--space-2) var(--space-3); + padding: var(--space-2) var(--space-4); + } + + .skip input { + background: var(--surface); + border: 1px solid var(--line); + border-radius: var(--radius); + color: var(--fg); + font-family: var(--font-display); + font-size: var(--text-md); + letter-spacing: var(--tracking); + min-height: 2.25rem; + min-width: 0; + padding: 0 var(--space-2); + width: 8rem; + } + + .skip input[aria-invalid='true'] { + border-color: var(--danger); + } + + .skip-hint { + color: var(--muted); + flex-basis: 100%; + } + + .skip-hint.invalid { + color: var(--danger); + } + .gap { align-items: center; border-bottom: 1px solid var(--warn); diff --git a/src/lib/clock.svelte.ts b/src/lib/clock.svelte.ts index 52eee88..f61929c 100644 --- a/src/lib/clock.svelte.ts +++ b/src/lib/clock.svelte.ts @@ -7,6 +7,7 @@ import { dismissGaps, pause, resume, + skipAhead, startSession, tick, type ClockState, @@ -71,6 +72,12 @@ export function answerGaps(playing: boolean): void { else dismissGaps(clocks.play) } +/** Counts `ms` of play time the user spent without LSOS tracking, so every tracker catches up. */ +export function skipPlayTime(ms: number): void { + report(skipAhead(clocks.play, ms)) + saveNow() +} + setInterval(onTick, TICK_INTERVAL_MS) // A hidden tab may not have ticked for up to a minute. Count that time whenever the page is // hidden, frozen (Chrome Energy Saver) or shown again, and save trackers' progress before the diff --git a/src/lib/clock.test.ts b/src/lib/clock.test.ts index 2cb7ac5..d021f79 100644 --- a/src/lib/clock.test.ts +++ b/src/lib/clock.test.ts @@ -8,6 +8,7 @@ import { pause, realElapsed, resume, + skipAhead, startSession, tick, type ClockState, @@ -104,6 +105,27 @@ describe('confirming long gaps', () => { }) }) +describe('skipping ahead', () => { + it('counts time played without LSOS and reports it, paused or not', () => { + const t = setup() + t.tickAfter(MINUTE) + expect(skipAhead(t.clock, 10 * MINUTE)).toBe(10 * MINUTE) + expect(t.clock.playMs).toBe(11 * MINUTE) + pause(t.clock, t.now()) + expect(skipAhead(t.clock, 5 * MINUTE)).toBe(5 * MINUTE) + expect(t.clock).toMatchObject({ playMs: 16 * MINUTE, paused: true }) + }) + + it('does not move the next tick or count negative time', () => { + const t = setup() + skipAhead(t.clock, 10 * MINUTE) + t.tickAfter(SECOND) + expect(t.reported()).toBe(SECOND) + expect(skipAhead(t.clock, -MINUTE)).toBe(0) + expect(t.clock.playMs).toBe(10 * MINUTE + SECOND) + }) +}) + describe('pause and resume', () => { it('does not count paused time, and counts up to the pause', () => { const t = setup() diff --git a/src/lib/clock.ts b/src/lib/clock.ts index 5ec57a7..86ffa1c 100644 --- a/src/lib/clock.ts +++ b/src/lib/clock.ts @@ -99,6 +99,16 @@ export function dismissGaps(clock: ClockState): void { clock.unconfirmedMs = 0 } +/** + * The user played without LSOS tracking (issue #70): count `ms` as play time, as if LSOS had + * been running. Returns the play time counted, for trackers to add to their progress. + */ +export function skipAhead(clock: ClockState, ms: number): number { + const counted = Math.max(0, ms) + clock.playMs += counted + return counted +} + /** Real time since `sinceMs` (epoch ms). Never negative, even if the clock was set back. */ export function realElapsed(sinceMs: number, now: number): number { return Math.max(0, now - sinceMs)