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 @@ -21,6 +21,7 @@ Svelte 5 + TypeScript + Vite. Public repository.
| Game data with sources | `src/data/` (decision 004) |
| Owned properties and upgrades | `data/properties.ts`, `lib/properties.ts` (011) |
| Business production (stock, supplies, value) | `data/production.ts`, `lib/production.ts` (012) |
| Nightclub warehouse goods | `data/nightclub.ts`, `lib/nightclub.ts` (015) |
| Business card order (drag, keys) | `lib/order.ts`, `ui/Panel` `move` (decision 014) |
| Theme tokens | `src/app.css` (decision 009) |
| CI, templates | `.github/` |
Expand Down
43 changes: 43 additions & 0 deletions docs/decisions/015-nightclub-warehouse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# 015: Nightclub warehouse goods

2026-10-14, issue #7. Builds on decisions 004, 011, 012 and 013.

## Decision

- **Data** (`src/data/nightclub.ts`): the seven goods types with value per crate, crates per
warehouse floor and minutes per crate without and with the Equipment Upgrade, from gta.wiki
(Nightclubs/Warehouse Management). Each lists the properties that unlock it; Cargo and
Shipments is unlocked by the Hangar or the Special Cargo Warehouse, which join the catalog as
owned/not owned only, with no card on the Businesses tab (`card: false`).
- **Settings:** the Nightclub property has Equipment (toggle), Warehouse floors (choice 1 to 5)
and five technician choices ("Not assigned"/"Not hired", then each goods type). Staff and
security upgrades do not change goods: left out.
- **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.
- **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.
- **Production is continuous** on play time, like decision 012; count and value use whole crates.
- **Every floor holds the same:** the source gives 1 and 5 floors (exactly five times as much);
2 to 4 floors were confirmed in game (issue #35). The source's "each subsequent floor doubles
that storage capacity" contradicts its own table and is not used.
- **UI:** collapsed, the card shows two lines per goods type that is being made or has stock:
name with crates and value, then the bar with its play time to full under one "Full in"
header ("Full", or "Idle" when no technician makes it). The next to fill gets an accent edge.
A total value follows. Technicians are left out of the card's upgrade summary, as the rows
show them. "Edit" lists all seven types as `EditableValue` fields in crates, with Sold and
Full, so goods from Business Battles can be entered too.
- **Best setup:** goods are ranked by value per hour of play time (value per crate over time
per crate, the source's own "value per hour" column). The card suggests putting the available
technicians (Yohan, plus those hired; it never hires) on the best unlocked goods that are not
full, keeping technicians already on one of them, and shows the gain per hour with an Apply
button ("Move"). Full goods earn nothing, so a full type frees its technician: the card says
"<goods> is full. Sell it, or move technician N to <goods>", or only "Sell it" when there is
nothing better to move to.

## Why

Crates match what the game shows per goods type and survive a change of floors. Listing the
unlocking properties keeps the rule ("owns the business") in data with its source.
1 change: 1 addition & 0 deletions docs/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ free number, add one line below. **Replacing one:** write a new decision, then m
- [012](012-business-production.md) Business production: rate per exact upgrade combination in `src/data/production.ts`, continuous on play time, unknown rates do not run
- [013](013-marked-estimates.md) Missing game values may be estimated by a stated rule from sourced ones, marked "(est.)" wherever shown
- [014](014-card-order.md) Business cards: drag a title bar to lift a card and swap it with another, or move it with arrow keys; order saved as property ids
- [015](015-nightclub-warehouse.md) Nightclub warehouse: goods per technician on play time, saved in crates, unlocked by owning the business, best technician setup by value per hour
5 changes: 5 additions & 0 deletions src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import SaveStatus from './components/SaveStatus.svelte'
import Settings from './components/Settings.svelte'
import TabBar from './components/ui/TabBar.svelte'
import { GOODS } from './data/nightclub'
import { PRODUCTION } from './data/production'
import { PROPERTIES } from './data/properties'
import { clocks } from './lib/clock.svelte'
Expand Down Expand Up @@ -55,6 +56,10 @@
(store.state.businesses = { ...store.state.businesses, [id]: bars })}
order={store.state.businessOrder}
onorder={(order) => (store.state.businessOrder = order)}
goods={GOODS}
nightclubStock={store.state.nightclub}
onnightclub={(id, crates) =>
(store.state.nightclub = { ...store.state.nightclub, [id]: crates })}
/>
{:else if active === 'settings'}
<Settings />
Expand Down
33 changes: 29 additions & 4 deletions src/components/Businesses.svelte
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
<script lang="ts">
// The owned businesses, each with a gear to change what is owned and upgraded (decision 011),
// and its stock and supplies if LSOS has production data for it (issue #6). Cards move by
// dragging their title bar or with its arrow keys (issue #30).
// dragging their title bar or with its arrow keys (issue #30). The nightclub shows its
// warehouse goods (issue #7).
import { tick } from 'svelte'
import type { HTMLAttributes } from 'svelte/elements'
import { flip } from 'svelte/animate'
import { cubicOut } from 'svelte/easing'
import { NIGHTCLUB_ID, type Goods } from '../data/nightclub'
import type { Production } from '../data/production'
import type { Property } from '../data/properties'
import { applyOrder, cardAt, moveId, saveOrder, swapAt, type CardBox } from '../lib/order'
import { emptyBars, matchingRate, rateFor, type BusinessBarsById } from '../lib/production'
import { ownedProperties, upgradeValue, type OwnedProperties } from '../lib/properties'
import type { GoodsStock } from '../lib/nightclub'
import type { Bars } from '../lib/resync'
import { hashForTab } from '../lib/tabs'
import BusinessProduction from './BusinessProduction.svelte'
import NightclubWarehouse from './NightclubWarehouse.svelte'
import PropertySettings from './PropertySettings.svelte'
import Button from './ui/Button.svelte'
import Panel from './ui/Panel.svelte'
Expand All @@ -31,6 +35,10 @@
/** Saved card order: property ids (issue #30). */
order?: readonly string[]
onorder?: (order: string[]) => void
/** Nightclub warehouse goods and their saved crates (issue #7). */
goods?: readonly Goods[]
nightclubStock?: GoodsStock
onnightclub?: (goodsId: string, crates: number) => void
}

let {
Expand All @@ -43,8 +51,13 @@
onresync = () => {},
order = [],
onorder = () => {},
goods = [],
nightclubStock = {},
onnightclub = () => {},
}: Props = $props()
const ownedList = $derived(ownedProperties(catalog, owned))
const ownedList = $derived(
ownedProperties(catalog, owned).filter((property) => property.card !== false),
)
const sorted = $derived(
applyOrder(
ownedList.map((property) => property.id),
Expand Down Expand Up @@ -180,7 +193,7 @@
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 []
if (upgrade.summary === false || value === upgrade.options[0]?.id) return []
const option = upgrade.options.find((o) => o.id === value)
return option ? [`${upgrade.label}: ${option.label}`] : []
})
Expand All @@ -203,6 +216,7 @@
<div class="grid">
{#each shown as property (property.id)}
{@const production = productions.find((p) => p.propertyId === property.id)}
{@const warehouse = property.id === NIGHTCLUB_ID && goods.length > 0}
<div class="card" class:slot={moving === property.id} data-card={property.id} animate:slide>
<div
class="lift"
Expand All @@ -227,8 +241,19 @@
onresync={(bars) => onresync(property.id, bars)}
/>
{/if}
{#if warehouse}
<NightclubWarehouse
{goods}
nightclub={property}
{owned}
{onchange}
stock={nightclubStock}
editing={editing === property.id}
onresync={onnightclub}
/>
{/if}
{#snippet footer()}
{#if production}
{#if production || warehouse}
<Button
aria-expanded={editing === property.id}
onclick={() => (editing = editing === property.id ? null : property.id)}
Expand Down
Loading
Loading