Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Svelte 5 + TypeScript + Vite. Public repository.
| Base UI kit (panel, meter, button...), preview | `src/components/ui/`, `#/kit` in dev (dec. 009) |
| Resync values; business bars (Sold, Raided) | `ui/EditableValue`, `ui/BusinessBars` (dec. 010) |
| Game data with sources | `src/data/` (decision 004) |
| Owned properties and upgrades | `data/properties.ts`, `lib/properties.ts` (011) |
| Theme tokens | `src/app.css` (decision 009) |
| CI, templates | `.github/` |
| Skills: `handoff`, `finish-pr`; agent: `scout` | `.claude/` |
Expand Down
28 changes: 28 additions & 0 deletions docs/decisions/011-owned-properties.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# 011: Owned properties and upgrades

2026-10-14, issue #5. Builds on decisions 004 and 006.

## Decision

- **Catalog:** `src/data/properties.ts` lists the properties LSOS supports, each with a stable
`id` (saved in user data, never changed or reused), a label, its upgrades and a source. It
starts empty: each tracker issue adds its own property with sourced upgrades.
- **Two kinds of upgrade:** `toggle` (bought or not, e.g. an equipment or staff upgrade) and
`choice` (one of several options, the first being the default, e.g. what a nightclub
technician is assigned to, with "not hired" first). Several technicians are several choices.
- **Saved state** (version 3): `properties` maps a property id to `{ owned, upgrades }`, where an
upgrade is saved as `true`/`false` or the chosen option id. A missing entry means not owned,
no upgrades. Unchecking "owned" keeps the upgrades, so owning it again restores them. Entries
for ids the catalog does not list are kept; a saved value that no longer fits its upgrade
reads as the default. Logic: `src/lib/properties.ts`.
- **UI:** Settings has a Properties panel with every catalog entry (owned checkbox; upgrades
appear once owned). The Businesses tab shows only owned properties, each panel with a gear
button that opens the same settings in place; closed, it summarises the bought upgrades.
Both components take the catalog as a prop, so `#/kit` previews them with sample entries.

## Why

Production rates depend on ownership and upgrades, so trackers need them, and users must be
able to change them where they look at a business as well as in Settings. Two kinds cover every
upgrade named in the issue without a separate shape per business. Keeping upgrades on sale and
unknown ids on load avoids throwing away user input over a misclick or a catalog change.
1 change: 1 addition & 0 deletions docs/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ free number, add one line below. **Replacing one:** write a new decision, then m
- [008](008-clock-details.md) Clock details: session play time, opens paused (setting), throttled ticks counted, long gaps asked about
- [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
- [011](011-owned-properties.md) Owned properties and upgrades: catalog in `src/data/properties.ts`, toggle/choice upgrades, Settings panel and Businesses gear
9 changes: 9 additions & 0 deletions src/App.svelte
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
<script lang="ts">
import Boot from './components/Boot.svelte'
import Businesses from './components/Businesses.svelte'
import ClockBar from './components/ClockBar.svelte'
import SaveStatus from './components/SaveStatus.svelte'
import Settings from './components/Settings.svelte'
import TabBar from './components/ui/TabBar.svelte'
import { PROPERTIES } from './data/properties'
import { store } from './lib/store.svelte'
import { TABS, tabFromHash } from './lib/tabs'

let hash = $state(location.hash)
Expand Down Expand Up @@ -37,6 +40,12 @@
{#await kitModule then { default: Kit }}
<Kit />
{/await}
{:else if active === 'businesses'}
<Businesses
catalog={PROPERTIES}
owned={store.state.properties}
onchange={(owned) => (store.state.properties = owned)}
/>
{:else if active === 'settings'}
<Settings />
{:else}
Expand Down
97 changes: 97 additions & 0 deletions src/components/Businesses.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<script lang="ts">
// The owned businesses, each with a gear to change what is owned and upgraded (decision 011).
import type { Property } from '../data/properties'
import { ownedProperties, upgradeValue, type OwnedProperties } from '../lib/properties'
import { hashForTab } from '../lib/tabs'
import PropertySettings from './PropertySettings.svelte'
import Button from './ui/Button.svelte'
import Panel from './ui/Panel.svelte'

interface Props {
catalog: readonly Property[]
owned: OwnedProperties
onchange: (owned: OwnedProperties) => void
}

let { catalog, owned, onchange }: Props = $props()
const shown = $derived(ownedProperties(catalog, owned))
let open = $state<string | null>(null)

/** Short summary of the bought upgrades, e.g. "Equipment, Technician: Cash". */
function summary(property: Property): string {
const parts = property.upgrades.flatMap((upgrade) => {
const value = upgradeValue(owned, property.id, upgrade)
if (upgrade.kind === 'toggle') return value === true ? [upgrade.label] : []
if (value === upgrade.options[0]?.id) return []
const option = upgrade.options.find((o) => o.id === value)
return option ? [`${upgrade.label}: ${option.label}`] : []
})
return parts.length > 0 ? parts.join(', ') : 'No upgrades'
}
</script>

{#if shown.length === 0}
<p class="empty">
{#if catalog.length === 0}
No businesses are tracked yet.
{:else}
You do not own any business yet. Mark what you own in
<a href={hashForTab('settings')}>Settings</a>.
{/if}
</p>
{:else}
<div class="grid">
{#each shown as property (property.id)}
<Panel title={property.label}>
{#if open === property.id}
<PropertySettings {property} {owned} {onchange} />
{:else}
<p class="hint">{summary(property)}</p>
{/if}
{#snippet footer()}
<Button
aria-label="Settings for {property.label}"
aria-expanded={open === property.id}
onclick={() => (open = open === property.id ? null : property.id)}
>
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
<path
fill="currentColor"
d="M19.14 12.94a7.07 7.07 0 0 0 0-1.88l2.03-1.58a.5.5 0 0 0 .12-.64l-1.92-3.32a.5.5 0 0 0-.6-.22l-2.39.96a7 7 0 0 0-1.62-.94l-.36-2.54a.5.5 0 0 0-.5-.42h-3.84a.5.5 0 0 0-.5.42l-.36 2.54a7 7 0 0 0-1.62.94l-2.39-.96a.5.5 0 0 0-.6.22L2.71 8.84a.5.5 0 0 0 .12.64l2.03 1.58a7.07 7.07 0 0 0 0 1.88l-2.03 1.58a.5.5 0 0 0-.12.64l1.92 3.32a.5.5 0 0 0 .6.22l2.39-.96a7 7 0 0 0 1.62.94l.36 2.54a.5.5 0 0 0 .5.42h3.84a.5.5 0 0 0 .5-.42l.36-2.54a7 7 0 0 0 1.62-.94l2.39.96a.5.5 0 0 0 .6-.22l1.92-3.32a.5.5 0 0 0-.12-.64zM12 15.5A3.5 3.5 0 1 1 12 8.5a3.5 3.5 0 0 1 0 7z"
/>
</svg>
</Button>
{/snippet}
</Panel>
{/each}
</div>
{/if}

<style>
.grid {
display: grid;
gap: var(--space-5);
grid-template-columns: repeat(auto-fill, minmax(min(100%, 24rem), 1fr));
}

p {
margin: 0;
}

.empty {
color: var(--muted);
}

.empty a {
color: var(--accent-text);
}

.hint {
color: var(--muted);
font-size: var(--text-sm);
}

svg {
display: block;
}
</style>
35 changes: 35 additions & 0 deletions src/components/PropertyList.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<script lang="ts">
// Every property LSOS supports, to mark what the user owns (decision 011).
import type { Property } from '../data/properties'
import type { OwnedProperties } from '../lib/properties'
import PropertySettings from './PropertySettings.svelte'
import Panel from './ui/Panel.svelte'

interface Props {
catalog: readonly Property[]
owned: OwnedProperties
onchange: (owned: OwnedProperties) => void
}

let { catalog, owned, onchange }: Props = $props()
</script>

<Panel title="Properties">
{#each catalog as property (property.id)}
<PropertySettings {property} {owned} {onchange} />
{:else}
<p>No properties yet. They appear here as their trackers are built.</p>
{/each}
<p class="hint">Trackers show only what you own. Upgrades change how fast they count.</p>
</Panel>

<style>
p {
margin: 0;
}

.hint {
color: var(--muted);
font-size: var(--text-sm);
}
</style>
122 changes: 122 additions & 0 deletions src/components/PropertySettings.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<script lang="ts">
// Owned yes/no and the upgrades of one property (decision 011).
import type { Property } from '../data/properties'
import {
isOwned,
setOwned,
setUpgrade,
upgradeValue,
type OwnedProperties,
} from '../lib/properties'

interface Props {
property: Property
owned: OwnedProperties
onchange: (owned: OwnedProperties) => void
}

let { property, owned, onchange }: Props = $props()
const id = $props.id()
</script>

<div class="property">
<label class="owned">
<input
type="checkbox"
checked={isOwned(owned, property.id)}
onchange={(event) => onchange(setOwned(owned, property.id, event.currentTarget.checked))}
/>
<span>Own {property.label}</span>
</label>

{#if isOwned(owned, property.id) && property.upgrades.length > 0}
<div class="upgrades">
{#each property.upgrades as upgrade (upgrade.id)}
{#if upgrade.kind === 'toggle'}
<label>
<input
type="checkbox"
checked={upgradeValue(owned, property.id, upgrade) === true}
onchange={(event) =>
onchange(setUpgrade(owned, property.id, upgrade.id, event.currentTarget.checked))}
/>
{upgrade.label}
</label>
{:else}
<div class="choice">
<label for="{id}-{upgrade.id}">{upgrade.label}</label>
<select
id="{id}-{upgrade.id}"
value={upgradeValue(owned, property.id, upgrade)}
onchange={(event) =>
onchange(setUpgrade(owned, property.id, upgrade.id, event.currentTarget.value))}
>
{#each upgrade.options as option (option.id)}
<option value={option.id}>{option.label}</option>
{/each}
</select>
</div>
{/if}
{/each}
</div>
{/if}
</div>

<style>
.property {
display: flex;
flex-direction: column;
gap: var(--space-2);
}

label {
align-items: center;
cursor: pointer;
display: flex;
gap: var(--space-2);
}

.owned {
font-family: var(--font-display);
letter-spacing: var(--tracking);
text-transform: uppercase;
}

.upgrades {
border-left: 2px solid var(--line);
display: flex;
flex-direction: column;
gap: var(--space-2);
margin-left: 0.5rem;
padding-left: var(--space-3);
}

.choice {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
justify-content: space-between;
}

input[type='checkbox'] {
accent-color: var(--accent);
flex: none;
height: 1.125rem;
width: 1.125rem;
}

select {
background: var(--panel-raised);
border: 1px solid var(--line);
border-radius: var(--radius);
color: var(--fg);
font: inherit;
min-height: 2.25rem;
padding: var(--space-1) var(--space-2);
}

select:hover {
border-color: var(--accent);
}
</style>
8 changes: 8 additions & 0 deletions src/components/Settings.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<script lang="ts">
import { PROPERTIES } from '../data/properties'
import { exportFileName } from '../lib/state'
import { exportText, importText, store } from '../lib/store.svelte'
import PropertyList from './PropertyList.svelte'
import Button from './ui/Button.svelte'
import Panel from './ui/Panel.svelte'

Expand Down Expand Up @@ -36,6 +38,12 @@
</script>

<div class="grid">
<PropertyList
catalog={PROPERTIES}
owned={store.state.properties}
onchange={(owned) => (store.state.properties = owned)}
/>

<Panel title="Clock">
<label>
<input type="checkbox" bind:checked={store.state.clock.trackOnOpen} />
Expand Down
Loading
Loading