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 @@ -27,6 +27,7 @@ Svelte 5 + TypeScript + Vite. Public repository.
| Acid Lab, production boost | `data/production.ts`, `lib/production.ts` (017) |
| Money Fronts safe and heat | `data/fronts.ts`, `lib/fronts.ts` (018) |
| Value bonus for owning a front | `ownedBonus` in `data/production.ts` (019) |
| Shut-down businesses (owned, not running) | `isRunning` in `lib/properties.ts` (020) |
| Theme tokens | `src/app.css` (decision 009) |
| CI, templates | `.github/` |
| Skills: `handoff`, `finish-pr`; agent: `scout` | `.claude/` |
Expand Down
4 changes: 3 additions & 1 deletion docs/decisions/011-owned-properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
- **UI:** Settings has a Properties panel with every catalog entry: the name as the row label and
a toggle button next to it, "Owned" (accent, filled) or "Not owned" (muted, outlined), with
`aria-pressed` and the name as its accessible label, so the state reads at a glance (issue
#31; it replaced an "Own <name>" checkbox). Upgrades appear once owned. The Businesses tab shows only owned properties, each panel with a gear
#31; it replaced an "Own <name>" checkbox). Upgrades appear once owned. The panel spans the
page below Clock and Backup, with the properties in as many columns as fit (issue #38). 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.

Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/015-nightclub-warehouse.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
- **Only ownership is needed.** The source says technicians need no supplies, so goods do not
depend on the linked business's stock or supplies. Goods whose business is not owned do not
run and the card says "Locked". Two technicians on one type count once (the game allows one).
A shut-down business locks its goods too; marking one shut down is issue #38.
A shut-down business locks its goods too (decision 020).
- **Saved state** (version 6): `nightclub` maps a goods id to crates, fractions included. Crates,
not percent, so changing the floors keeps the count the game shows. Stock above the capacity
(fewer floors set) stays until changed.
Expand Down
5 changes: 2 additions & 3 deletions docs/decisions/018-money-fronts.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@
- **Payout** (`src/data/fronts.ts`), every in-game day (48 minutes of play time), up to
$100,000: $750 per front ($1,500 after 15 laundering missions) plus a fixed amount for each
other business owned, from the in-game safe screen (data comment has the sources). A front at
100% heat pays nothing; the Car Wash at 100% stops the whole safe. LSOS counts every owned
business; the game skips biker businesses and the Bunker while shut down or not set up, which
the card says (shutting down: issue #38).
100% heat pays nothing; the Car Wash at 100% stops the whole safe. A shut-down business
adds nothing (decision 020); one bought but not set up still counts in LSOS.
- **Heat** per front, saved with the safe (`heat` by front id, data version 9). At the end of
each day, after the payout, every front below 100% gains 10%, one less per other front below
100% (8% with all three). A laundering mission or Mr. Faber job adds 10% to the front it was
Expand Down
32 changes: 32 additions & 0 deletions docs/decisions/020-shut-down.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# 020: Shut-down businesses

2026-10-15, issue #38. Builds on decisions 011, 012, 015 and 018.

## Decision

- **A flag apart from owned:** `shutDown` in a property's saved settings, next to `owned` and
the upgrades (data version 10). Absent means running, so older entries need no change. Owning
or selling keeps it, like the upgrades.
- **Only businesses the game can shut down** get a "Running"/"Shut down" toggle button next to
"Owned", shown while owned (`canShutDown` in `src/data/properties.ts`): the five biker
businesses and the Bunker, the ones decision 018 names. The Acid Lab is left out: no source
says it can be shut down. A button like "Owned", not an upgrade checkbox: it is a state of the
business, not something bought.
- **Shutting down empties the stock and keeps the supplies** (checked in game, 2026-10-15):
`clearShutDownStock` in `lib/production.ts`, applied by `setProperties` in the store whenever a
business goes from running to shut down. Starting it again changes nothing.
- **Shut down means "not running" everywhere** (`isRunning` in `lib/properties.ts`):
- production (decision 012) makes no stock and uses no supplies; the card is dimmed (darker,
colours kept) but readable, keeps its bars and value, has a grey "Shut down" note (not the yellow warning),
and hides the countdowns and the boost;
- nightclub goods (decision 015) whose only unlocking business is shut down count as locked,
so the Locked note and the best-setup suggestion follow;
- the Money Fronts safe (decision 018) gets nothing from it.
- **Not modelled:** whether a nightclub technician must be reassigned after the business starts
again. LSOS keeps the assignment and resumes counting.

## Why

Marking a shut-down business "Not owned" hid its stock and lost the card. The flag keeps both.
gta.wiki does not describe shutting down from the laptop (checked 2026-10-15); the nightclub
and Money Fronts rules rest on the sources in decisions 015 and 018 and the issue.
1 change: 1 addition & 0 deletions docs/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ free number, add one line below. **Replacing one:** write a new decision, then m
- [017](017-production-boost.md) Acid Lab: optional far sale, value bonus (named product), no raids; production boost by units made or 24 real hours, once a day
- [018](018-money-fronts.md) Money Fronts: one Car Wash safe card on the Safes tab, paid by the fronts and other businesses, heat per front (est.), missions and legal jobs
- [019](019-owned-value-bonus.md) Money Front value bonus: owning Car Wash or Smoke on the Water adds 35% to Counterfeit Cash or Weed, far value adds the bonuses (est.), Helitours left out
- [020](020-shut-down.md) Shut-down businesses: a Running/Shut down button next to Owned for biker businesses and the Bunker; shutting down clears the stock; no production, card greyed, nightclub goods locked, nothing to the Money Fronts safe
6 changes: 3 additions & 3 deletions src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import { clocks } from './lib/clock.svelte'
import './lib/production.svelte'
import './lib/safes.svelte'
import { store } from './lib/store.svelte'
import { setProperties, store } from './lib/store.svelte'
import { TABS, tabFromHash } from './lib/tabs'

let hash = $state(location.hash)
Expand Down Expand Up @@ -51,7 +51,7 @@
<Businesses
catalog={PROPERTIES}
owned={store.state.properties}
onchange={(owned) => (store.state.properties = owned)}
onchange={setProperties}
productions={PRODUCTION}
businesses={store.state.businesses}
now={clocks.now}
Expand All @@ -74,7 +74,7 @@
safes={SAFES}
catalog={PROPERTIES}
owned={store.state.properties}
onchange={(owned) => (store.state.properties = owned)}
onchange={setProperties}
stored={store.state.safes}
onresync={(id, safe) => (store.state.safes = { ...store.state.safes, [id]: safe })}
/>
Expand Down
23 changes: 20 additions & 3 deletions src/components/BusinessProduction.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
rate: ProductionRate | undefined
/** The same rate if it can run on its own (its supply use is known). */
running: KnownRate | undefined
/** Shut down from its laptop: nothing is made or used until it runs again (issue #38). */
shutDown?: boolean
bars: Bars
/** Wall-clock time, for supply deliveries. */
now: number
Expand All @@ -47,6 +49,7 @@
production,
rate,
running,
shutDown = false,
bars,
now,
editing,
Expand All @@ -71,7 +74,9 @@
const boosting = $derived(boostRunning(boost, production.boost, now))
const nextBoost = $derived(production.boost ? nextBoostAt(boost, production.boost) : 0)
const ahead = $derived(
running ? forecast(current, production, running, boosting ? boost!.unitsLeft : 0) : null,
running && !shutDown
? forecast(current, production, running, boosting ? boost!.unitsLeft : 0)
: null,
)
const percent = (value: number) => formatValue(value, 'percent')
// Numbers resting on estimated rates say so (decision 013). The value also counts units made
Expand Down Expand Up @@ -115,7 +120,7 @@
</div>
{/if}

{#if production.boost && running?.boostedMinutesPerUnit !== undefined}
{#if production.boost && running?.boostedMinutesPerUnit !== undefined && !shutDown}
<!-- Play time while it runs; the next one is a real-time wait (daily reset). -->
<div class="facts">
{#if boosting}
Expand All @@ -137,7 +142,11 @@
{/if}
{/if}

{#if ahead}
{#if shutDown}
<p class="note quiet" role="note">
<strong>Shut down.</strong> No stock is made and no supplies are used until you start it again.
</p>
{:else if ahead}
<!-- Only what happens first: the other one cannot happen before it. Play time. -->
{#if ahead.fullInMs !== null}
<Countdown
Expand Down Expand Up @@ -203,7 +212,15 @@
padding-left: var(--space-2);
}

.note.quiet {
border-left-color: var(--muted);
}

.note strong {
color: var(--warn);
}

.note.quiet strong {
color: inherit;
}
</style>
14 changes: 13 additions & 1 deletion src/components/Businesses.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@
type BoostsById,
type BusinessBarsById,
} from '../lib/production'
import { ownedProperties, upgradeValue, type OwnedProperties } from '../lib/properties'
import {
isShutDown,
ownedProperties,
upgradeValue,
type OwnedProperties,
} from '../lib/properties'
import type { GoodsStock } from '../lib/nightclub'
import type { Bars } from '../lib/resync'
import { hashForTab } from '../lib/tabs'
Expand Down Expand Up @@ -238,6 +243,7 @@
class="lift"
class:lifted={moving === property.id}
class:settling={settling === property.id}
class:shut={isShutDown(owned, property.id)}
style:translate={moving === property.id ? `${lift.x}px ${lift.y}px` : undefined}
>
<Panel title={property.label} move={handle(property.id)} moving={moving === property.id}>
Expand All @@ -251,6 +257,7 @@
{production}
rate={matchingRate(production, owned, property)}
running={rateFor(production, owned, property)}
shutDown={isShutDown(owned, property.id)}
bars={businesses[property.id] ?? emptyBars()}
{now}
editing={editing === property.id}
Expand Down Expand Up @@ -314,6 +321,11 @@
outline-offset: -1px;
}

/* A shut-down business is dimmed like a disabled control: same colours, darker (issue #38). */
.lift.shut {
filter: brightness(0.65);
}

.lift.lifted {
box-shadow: 0 0.75rem 2rem rgb(0 0 0 / 0.5);
pointer-events: none;
Expand Down
4 changes: 2 additions & 2 deletions src/components/MoneyFrontsCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@
<span class="value">{money(parts.fronts + parts.others)} a day</span>
</div>
<p class="hint">
Fronts {money(parts.fronts)} + your other businesses {money(parts.others)}. Biker businesses and
the Bunker only count while set up and not shut down.
Fronts {money(parts.fronts)} + your other businesses {money(parts.others)}. A shut-down business
adds nothing.
</p>

<Countdown label="Next payout in" remainingMs={nextPayoutInMs(current)} />
Expand Down
2 changes: 1 addition & 1 deletion src/components/NightclubWarehouse.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@
<p class="note" role="note">
<strong>Locked.</strong>
{locked.map((g) => g.label).join(', ')}: own the business that makes
{locked.length === 1 ? 'it' : 'them'} to start production.
{locked.length === 1 ? 'it' : 'them'}, and keep it running, to start production.
</p>
{/if}
{#if setup.doubled}
Expand Down
19 changes: 14 additions & 5 deletions src/components/PropertyList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,24 @@
</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}
<div class="list">
{#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}
</div>
<p class="hint">Trackers show only what you own. Upgrades change how fast they count.</p>
</Panel>

<style>
.list {
align-items: start;
display: grid;
gap: var(--space-3) var(--space-6);
grid-template-columns: repeat(auto-fill, minmax(min(100%, 22rem), 1fr));
}

p {
margin: 0;
}
Expand Down
42 changes: 33 additions & 9 deletions src/components/PropertySettings.svelte
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<script lang="ts">
// Owned yes/no and the upgrades of one property (decision 011).
// Owned yes/no, shut down (issue #38) and the upgrades of one property (decision 011).
import type { Property } from '../data/properties'
import Button from './ui/Button.svelte'
import NumberField from './ui/NumberField.svelte'
import {
isOwned,
isShutDown,
setOwned,
setShutDown,
setUpgrade,
upgradeValue,
type OwnedProperties,
Expand All @@ -20,19 +22,32 @@
let { property, owned, onchange }: Props = $props()
const id = $props.id()
const isPropertyOwned = $derived(isOwned(owned, property.id))
const isPropertyRunning = $derived(!isShutDown(owned, property.id))
</script>

<div class="property">
<div class="owned">
<span id="{id}-name" class="name">{property.label}</span>
<Button
variant={isPropertyOwned ? 'primary' : 'ghost'}
aria-labelledby="{id}-name"
aria-pressed={isPropertyOwned}
onclick={() => onchange(setOwned(owned, property.id, !isPropertyOwned))}
>
{isPropertyOwned ? 'Owned' : 'Not owned'}
</Button>
<div class="toggles">
{#if isPropertyOwned && property.canShutDown}
<Button
variant={isPropertyRunning ? 'primary' : 'ghost'}
aria-label="{property.label} running"
aria-pressed={isPropertyRunning}
onclick={() => onchange(setShutDown(owned, property.id, isPropertyRunning))}
>
{isPropertyRunning ? 'Running' : 'Shut down'}
</Button>
{/if}
<Button
variant={isPropertyOwned ? 'primary' : 'ghost'}
aria-labelledby="{id}-name"
aria-pressed={isPropertyOwned}
onclick={() => onchange(setOwned(owned, property.id, !isPropertyOwned))}
>
{isPropertyOwned ? 'Owned' : 'Not owned'}
</Button>
</div>
</div>

{#if isPropertyOwned && property.upgrades.length > 0}
Expand Down Expand Up @@ -100,6 +115,15 @@
justify-content: space-between;
}

/* Right-aligned, also when a long name pushes them onto their own line. */
.toggles {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
justify-content: flex-end;
margin-left: auto;
}

.name {
font-family: var(--font-display);
letter-spacing: var(--tracking);
Expand Down
18 changes: 11 additions & 7 deletions src/components/Settings.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import { PROPERTIES } from '../data/properties'
import { exportFileName } from '../lib/state'
import { exportText, importText, store } from '../lib/store.svelte'
import { exportText, importText, setProperties, 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 @@ -38,12 +38,6 @@
</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 Expand Up @@ -75,6 +69,11 @@
<p class:error={message.error} role="status">{message.text}</p>
{/if}
</Panel>

<!-- The long list goes last, across the whole width, in as many columns as fit. -->
<div class="wide">
<PropertyList catalog={PROPERTIES} owned={store.state.properties} onchange={setProperties} />
</div>
</div>

<style>
Expand All @@ -84,6 +83,11 @@
grid-template-columns: repeat(auto-fill, minmax(min(100%, 24rem), 1fr));
}

.wide {
display: grid;
grid-column: 1 / -1;
}

p {
margin: 0;
}
Expand Down
Loading
Loading