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
10 changes: 10 additions & 0 deletions .claude/skills/game-design/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,13 @@ Every design pass yields an observed journey, a north star (promise, pillars, an
An audit is not completion for a build/improve request. Finish when the slice is playable, a fresh player can state goal/choice/consequence, failure/recovery behave as intended, and evidence supports the pillars. Use `jgengine-verify` and `workflow` to ship.

**Required: player death is a designed, visible moment.** If the game can kill or down the player, death is part of the experience contract, not an invisible state reset. A lethal hit must resolve into an authored beat the player perceives and understands — a death or downed moment, the stakes it carries, and a legible path back into play (respawn, revive, restart) — never a silent teleport to spawn. Design what death means for this pitch (permadeath, checkpoint, bleed-out-and-revive, run reset) and treat "player dies with no acknowledged consequence" as a failure/recovery defect. `jgengine-ui` owns building the screen and respawn feedback.

## Persistent-builder checks

Before content, write a first-hour balance sheet: starting cash, time to first income, net income per hour at starting hardware and at ten times that hardware, bill cadence versus income, and the insolvency recovery path. Insolvency resets the affected player or changes their options; it never deletes the world. Express clicks and event rewards as a fraction of the designed income rate, never an unrelated flat payout.

Declare currency `decimals` for income paid per second. Anchor time-based decay to `game.createdAt` or a persisted per-instance creation time, never a fixed wall-clock epoch. Test fractional income over real tick cadences before tuning prices.

The first-60-seconds gate requires the first tutorial verb to be executable from spawn with starting inventory. Grant dependencies such as land, power, and slots or let players buy them in-context; the tutorial cannot lock the surface needed to complete itself. Placement previews show cost and affordability inline.

For a persistent shared-world builder, compose neighborhood presence, declared command read scopes, membership storage, online-player batched ticks, territory, chat, and join retry UI through their engine owners. Verify capacity and first-player onboarding before scaling the content catalog.
10 changes: 10 additions & 0 deletions .claude/skills/jgengine-gameplay/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,13 @@ A character kit-bashed from primitives/`ModelPart`s (no skeleton, no clips) anim
- Do not fuse save semantics with a specific cloud/backend adapter.
- Targeting, damage, effects, abilities, and loot resolution route to `jgengine-combat`.
- World movement/input/interaction route to `jgengine-world`; HUD rendering routes to `jgengine-ui`.

## Persistent economies and input

- Declare `CurrencyDefinition.decimals` for per-second income. Use the definition in wallet and `ctx.game.economy` operations; public amounts are major units, arithmetic rounds in integer minor units at each write. Accrue sub-minor income over elapsed time before writing, or choose finer precision.
- `formatCurrency(definition, value)` shares that precision with display. Currency strings keep the existing wallet behavior; pass the definition to enforce precision.
- Anchor elapsed production and decay to game creation or a persisted last-run time, never a fixed epoch. `accrueSince(anchorMs, nowMs, { capMs })` returns elapsed time and the next anchor; persist both earnings and that anchor together.
- Validate purchase counts with `readQuantity(value, { min, max })`; NaN, Infinity, fractions, strings, and out-of-range values reject without coercion.
- Reset a profile with `initialPlayerState(runtime, userId)` from the same `onNewPlayer` hook used at join; insolvency never deletes a world.
- `ctx.game.chat.send(userId, text)` targets global chat; `recent({ limit })` returns at most 100 messages. The default is 240 characters and one message per author per channel every two seconds. Rejected messages need visible UI feedback.
- Hosts reuse `validateChatMessage` and `decideRateWindow`, storing the rate window transactionally with each accepted operation. Preserve chat rate windows when saving/restoring chat state.
25 changes: 15 additions & 10 deletions .claude/skills/jgengine-gameplay/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@
- `CurrencyAdjustment` (type): type CurrencyAdjustment = | { success: true; newBalance: number; appliedDelta: number } | { success: false; reason: string } — ⚠ undocumented
- `CurrencyDefinition` (interface): interface CurrencyDefinition<TCurrencyId extends string = string> — ⚠ undocumented
- `CurrencyOperation` (type): type CurrencyOperation = "add" | "deduct" — ⚠ undocumented
- `formatCurrency` (function): function formatCurrency(currency: CurrencyDefinition, value: number): string — Format a major-unit value using the currency's declared precision.
- `fromMinorUnits` (function): function fromMinorUnits(currency: Pick<CurrencyDefinition, "decimals"> | undefined, value: number): number — Convert stored integer minor units to major units for display or existing balance records.
- `toMinorUnits` (function): function toMinorUnits(currency: Pick<CurrencyDefinition, "decimals"> | undefined, value: number): number — Convert major units to integer minor units, rounding once at the write boundary.

## @jgengine/core/economy/listingBook

Expand Down Expand Up @@ -269,13 +272,13 @@
- `ChargeResult` (type): type ChargeResult = { status: "ok"; state: WalletState } | { status: "rejected"; reason: "insufficient-funds" } — Outcome of a {@link charge}/{@link chargeAll} attempt: `status: "ok"` carries the debited {@link WalletState}, while `status: "rejected"` leaves the wallet untouched and reports why (currently only `"insufficient-funds"`). Discriminate on `status` before reading `state`.
- `Overdraft` (type): type Overdraft = boolean | { max: number } — Opt-in debt affordance for {@link charge}/{@link chargeAll}: `true` allows the balance to go arbitrarily negative, a number caps how far into the red it may go (the charge is rejected once `balance - amount` would fall below `-max`). Omitted (the default) keeps the strict no-debt rule.
- `WalletState` (interface): interface WalletState — ⚠ undocumented
- `balance` (function): function balance(state: WalletState, currency: string): number — ⚠ undocumented
- `balance` (function): function balance(state: WalletState, currency: string | CurrencyDefinition): number — ⚠ undocumented
- `canAfford` (function): function canAfford(state: WalletState, costs: Readonly<Record<string, number>>): boolean — True when every currency in `costs` has at least that much balance (a pure, non-mutating check).
- `charge` (function): function charge(state: WalletState, currency: string, amount: number, options?: ChargeOptions): ChargeResult — Deduct `amount`, rejecting when it would leave the balance negative unless `options.overdraft` opts into carrying debt (`true` unlimited, `{ max }` capped) — the strict same-tick affordability check stays the default with `options` omitted.
- `charge` (function): function charge(state: WalletState, currency: string | CurrencyDefinition, amount: number, options?: ChargeOptions): ChargeResult — Deduct `amount`, rejecting when it would leave the balance negative unless `options.overdraft` opts into carrying debt (`true` unlimited, `{ max }` capped) — the strict same-tick affordability check stays the default with `options` omitted.
- `chargeAll` (function): function chargeAll(state: WalletState, costs: Readonly<Record<string, number>>, options?: ChargeOptions): ChargeResult — ⚠ undocumented
- `createEmptyWallet` (function): function createEmptyWallet(): WalletState — Hold per-currency balances with affordability checks and charge/grant operations.
- `grant` (function): function grant(state: WalletState, currency: string, amount: number): WalletState — ⚠ undocumented
- `isOverdrawn` (function): function isOverdrawn(state: WalletState, currency: string): boolean — True once `balance(state, currency)` has gone negative under an overdraft-enabled charge.
- `grant` (function): function grant(state: WalletState, currency: string | CurrencyDefinition, amount: number): WalletState — ⚠ undocumented
- `isOverdrawn` (function): function isOverdrawn(state: WalletState, currency: string | CurrencyDefinition): boolean — True once `balance(state, currency)` has gone negative under an overdraft-enabled charge.

## @jgengine/core/game/achievements

Expand Down Expand Up @@ -343,13 +346,15 @@
- `ChatRecipients` (type): type ChatRecipients = readonly string[] | "all" — ⚠ undocumented
- `ChatSendResult` (type): type ChatSendResult = | { message: ChatMessage; recipients: ChatRecipients } | { reason: string } — ⚠ undocumented
- `ChatSnapshot` (interface): interface ChatSnapshot — ⚠ undocumented
- `DEFAULT_CHAT_BODY_LENGTH` (const): const DEFAULT_CHAT_BODY_LENGTH: 500 — ⚠ undocumented
- `ChatValidation` (type): type ChatValidation = { ok: true; text: string } | { ok: false; reason: string } — Sanitized chat text or a displayable validation failure.
- `DEFAULT_CHAT_BODY_LENGTH` (const): const DEFAULT_CHAT_BODY_LENGTH: 240 — ⚠ undocumented
- `DEFAULT_CHAT_HISTORY_LIMIT` (const): const DEFAULT_CHAT_HISTORY_LIMIT: 100 — ⚠ undocumented
- `DEFAULT_CHAT_RATE_LIMIT` (const): const DEFAULT_CHAT_RATE_LIMIT: ChatRateLimit — ⚠ undocumented
- `DEFAULT_PROXIMITY_CHAT_RADIUS` (const): const DEFAULT_PROXIMITY_CHAT_RADIUS: 20 — ⚠ undocumented
- `WHISPER_CHANNEL_PREFIX` (const): const WHISPER_CHANNEL_PREFIX: "whisper:" — ⚠ undocumented
- `createChat` (function): function createChat(deps: ChatDeps): Chat — ⚠ undocumented
- `createChatRateLimiter` (function): function createChatRateLimiter(limit: ChatRateLimit): ChatRateLimiter — ⚠ undocumented
- `validateChatMessage` (function): function validateChatMessage(value: unknown, options: { maxLength?: number } = {}): ChatValidation — Strip control characters and enforce the shared chat message policy before storage or broadcast.
- `whisperChannelId` (function): function whisperChannelId(a: string, b: string): string — ⚠ undocumented

## @jgengine/core/game/chatFilter
Expand Down Expand Up @@ -1084,7 +1089,7 @@
- `CropTileState` (interface): interface CropTileState — ⚠ undocumented
- `CrossThresholdsOptions` (interface): interface CrossThresholdsOptions — Exact-boundary and dead-band policy for {@link crossThresholds}.
- `Curve` (type): type Curve = CurveDef & CurveShape — A fully specified progression curve — a {@link CurveDef} growth shape plus optional {@link CurveShape} rounding/clamp.
- `DEFAULT_CHAT_BODY_LENGTH` (const): const DEFAULT_CHAT_BODY_LENGTH: 500 — ⚠ undocumented
- `DEFAULT_CHAT_BODY_LENGTH` (const): const DEFAULT_CHAT_BODY_LENGTH: 240 — ⚠ undocumented
- `DEFAULT_CHAT_HISTORY_LIMIT` (const): const DEFAULT_CHAT_HISTORY_LIMIT: 100 — ⚠ undocumented
- `DEFAULT_CHAT_RATE_LIMIT` (const): const DEFAULT_CHAT_RATE_LIMIT: ChatRateLimit — ⚠ undocumented
- `DEFAULT_FIXED_STAGES` (const): const DEFAULT_FIXED_STAGES: readonly ["input", "movement", "combat", "ai", "activities", "cleanup"] — Default fixed-sim stage order — systems pick a stage; most need only this.
Expand Down Expand Up @@ -1415,7 +1420,7 @@
- `applyBindingOverrides` (function): function applyBindingOverrides<TAction extends string, TCode extends string>(input: ActionCodesMap<TAction, TCode>, overrides: BindingOverrides): ActionCodesMap<TAction, TCode> — Merge player rebinds over a game's authored `input` map. Only actions the game already declares can be overridden; unknown override keys are ignored so a stale localStorage entry can't inject phantom actions.
- `applySetBonuses` (function): function applySetBonuses(stats: Record<string, number>, bonuses: readonly SetBonus[]): Record<string, number> — Fold a set of active bonuses' additive stats onto a stat map, returning a new map (the input is not mutated).
- `applyWear` (function): function applyWear(state: DurabilityState, amount: number): DurabilityState — Apply wear to an item, tracking breakage and repair eligibility.
- `balance` (function): function balance(state: WalletState, currency: string): number — ⚠ undocumented
- `balance` (function): function balance(state: WalletState, currency: string | CurrencyDefinition): number — ⚠ undocumented
- `balanceOf` (function): function balanceOf(ledger: ResourceLedger, account: string, currency: string): number — Read a single balance; unknown account/currency pairs read as `0`.
- `canAfford` (function): function canAfford(state: WalletState, costs: Readonly<Record<string, number>>): boolean — True when every currency in `costs` has at least that much balance (a pure, non-mutating check).
- `canCraft` (function): function canCraft(state: InventoryState, layout: InventoryLayout, traits: ItemTraits, recipe: RecipeDef, context: CraftContext = {}): CraftCheck — ⚠ undocumented
Expand All @@ -1424,7 +1429,7 @@
- `candidateViolatesForbid` (function): function candidateViolatesForbid(partial: ItemIdentity, candidate: CandidatePlacement, rules: readonly CompatibilityRule[]): ForbidRule | null — The generic backtracking contract for procedural generation (see #908): given a partial identity and a candidate part, return the first forbid rule the placement would violate, or null if it stays viable. Require rules are ignored here because they may still be satisfied by a later placement.
- `capAmount` (function): function capAmount(max: number | ((ctx: PolicyContext) => number)): ResourcePolicy — Clamp a transaction's amount to at most `max` (a fixed number or a function of context).
- `captureProvenance` (function): function captureProvenance(identity: ItemIdentity, activeBonuses: readonly SetBonus[], seed?: number): ItemProvenance — Capture the provenance of a generated item — family, tags, per-slot parts, active bonus ids, and optional seed — as a JSON-safe record.
- `charge` (function): function charge(state: WalletState, currency: string, amount: number, options?: ChargeOptions): ChargeResult — Deduct `amount`, rejecting when it would leave the balance negative unless `options.overdraft` opts into carrying debt (`true` unlimited, `{ max }` capped) — the strict same-tick affordability check stays the default with `options` omitted.
- `charge` (function): function charge(state: WalletState, currency: string | CurrencyDefinition, amount: number, options?: ChargeOptions): ChargeResult — Deduct `amount`, rejecting when it would leave the balance negative unless `options.overdraft` opts into carrying debt (`true` unlimited, `{ max }` capped) — the strict same-tick affordability check stays the default with `options` omitted.
- `chargeAll` (function): function chargeAll(state: WalletState, costs: Readonly<Record<string, number>>, options?: ChargeOptions): ChargeResult — ⚠ undocumented
- `clampValue` (function): function clampValue(value: number, bounds?: NumericBounds): number — Clamp a scalar to `bounds` (identity when `bounds` is omitted). Pure — touches no record.
- `clearBindingOverride` (function): function clearBindingOverride(gameId: string, action: string, storage: Pick<WebStorageLike, "getItem" | "setItem" | "removeItem"> | null | undefined = defaultStorage()): BindingOverrides — ⚠ undocumented
Expand Down Expand Up @@ -1533,7 +1538,7 @@
- `generate` (function): function generate(schema: GenSchema, rng: () => number, options: GenerateOptions = {}): GenOutcome — Run a caller-defined {@link GenSchema} against an injected `rng` into a deterministic, serializable {@link GenResult} with full provenance. Composes weighted/uniform choice, dependent choice, constraints with bounded backtracking, field transforms, and validation reroll over plain data — the generic seam procedural loot, affix, and modular-part rollers assemble on. Identical schema, seed, and pins reproduce an identical result across server/client and save/load.
- `getRuleEffect` (function): function getRuleEffect(id: string): RuleEffectDefinition | undefined — Look up a declared rule effect, or `undefined` when the id was never registered — lets callers reject unknown effect references in authored content.
- `getValue` (function): function getValue(record: Record<string, number>, key: string, fallback = 0): number — Current value for `key`, or `fallback` (default `0`) when the record has no entry.
- `grant` (function): function grant(state: WalletState, currency: string, amount: number): WalletState — ⚠ undocumented
- `grant` (function): function grant(state: WalletState, currency: string | CurrencyDefinition, amount: number): WalletState — ⚠ undocumented
- `identityOf` (function): function identityOf(family: string, tags: readonly string[], parts: readonly InstalledPart[]): ItemIdentity — Assemble an {@link ItemIdentity} from a family, tags, and installed parts.
- `idleRaceSession` (function): function idleRaceSession(): RaceSessionState — The pre-race session on the grid: `idle`, both clocks at zero. Call {@link startRaceCountdown} to light the lights, or hold here until the field is ready.
- `initDecayMeters` (function): function initDecayMeters(defs: readonly DecayMeterConfig[]): DecayMeterValues — Starting values for `defs` — each meter's `start ?? max`, clamped to its range. Seed a serialized state record with this instead of holding a {@link createDecayMeterSet} closure.
Expand All @@ -1542,7 +1547,7 @@
- `isComplete` (function): function isComplete(def: ModularItemDef, installed: readonly InstalledPart[]): boolean — ⚠ undocumented
- `isDisabled` (function): function isDisabled(spec: DurabilitySpec, state: DurabilityState): boolean — ⚠ undocumented
- `isIdentityValid` (function): function isIdentityValid(identity: ItemIdentity, rules: readonly CompatibilityRule[]): boolean — Convenience predicate: true when {@link validateIdentity} finds no violations.
- `isOverdrawn` (function): function isOverdrawn(state: WalletState, currency: string): boolean — True once `balance(state, currency)` has gone negative under an overdraft-enabled charge.
- `isOverdrawn` (function): function isOverdrawn(state: WalletState, currency: string | CurrencyDefinition): boolean — True once `balance(state, currency)` has gone negative under an overdraft-enabled charge.
- `jobById` (function): function jobById<TSpec, TReserve>(state: WorkQueueState<TSpec, TReserve>, id: JobId): Job<TSpec, TReserve> | null — Look up a job by id, or `null` if absent/terminal.
- `jobProgress` (function): function jobProgress<TSpec, TReserve>(job: Job<TSpec, TReserve>): number — Fractional progress of a job (0…1); a zero-duration job reads as complete.
- `lapDurations` (function): function lapDurations(splits: readonly number[], gatesPerLap: number): number[] — Per-lap durations from a cumulative split book with `gatesPerLap` checkpoints per lap — each lap's time is its finish-gate split minus the previous lap's finish. Only complete laps are returned.
Expand Down
12 changes: 12 additions & 0 deletions .claude/skills/jgengine-gameplay/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p

- `breedOffspring` (function) · `import { breedOffspring } from "@jgengine/core/game/breeding"`

## currency-major-units — convert safe minor-unit integers into currency amounts

- `fromMinorUnits` (function) · `import { fromMinorUnits } from "@jgengine/core/economy/currency"`

## currency-minor-units — round decimal currency into safe integer minor units

- `toMinorUnits` (function) · `import { toMinorUnits } from "@jgengine/core/economy/currency"`

## currency-precision-format — display a currency using its declared decimal precision

- `formatCurrency` (function) · `import { formatCurrency } from "@jgengine/core/economy/currency"`

## decay-meter — survival meters that drain/refill over game time (hunger, water, oxygen, stamina)

- `createDecayMeterSet` (function) · `import { createDecayMeterSet } from "@jgengine/core/gameplay"`
Expand Down
Loading
Loading