opencode-overclock 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +252 -111
- package/package.json +6 -4
- package/skills/codebase-design/DEEPENING.md +35 -0
- package/skills/codebase-design/DESIGN-IT-TWICE.md +34 -0
- package/skills/codebase-design/SKILL.md +93 -0
- package/skills/diagnosing-bugs/SKILL.md +123 -0
- package/skills/domain-modeling/ADR-FORMAT.md +55 -0
- package/skills/domain-modeling/CONTEXT-FORMAT.md +32 -0
- package/skills/domain-modeling/SKILL.md +102 -0
- package/skills/doubt/SKILL.md +80 -0
- package/skills/grilling/SKILL.md +96 -0
- package/skills/source-discipline/SKILL.md +78 -0
- package/skills/tdd/SKILL.md +87 -0
- package/skills/to-spec/SKILL.md +69 -0
- package/skills/to-spec/SPEC-TEMPLATE.md +50 -0
- package/skills/to-tickets/SKILL.md +74 -0
- package/skills/to-tickets/TICKET-TEMPLATE.md +41 -0
- package/src/bridge.ts +1 -0
- package/src/buddy/companion.ts +104 -5
- package/src/buddy/sprites.ts +4 -4
- package/src/buddy/tui.ts +175 -65
- package/src/core/bridge.ts +34 -0
- package/src/core/lifecycle.ts +67 -0
- package/src/core/policy.ts +128 -0
- package/src/core/summary.ts +33 -0
- package/src/core/types.ts +193 -0
- package/src/features/buddy.ts +1 -2
- package/src/features/guard.ts +421 -37
- package/src/features/index.ts +18 -4
- package/src/features/recovery.ts +153 -0
- package/src/features/safety.ts +147 -0
- package/src/features/sched.ts +183 -89
- package/src/features/tasks.ts +134 -33
- package/src/features/truncator.ts +116 -0
- package/src/features/usage.ts +46 -65
- package/src/features/workflow.ts +256 -0
- package/src/index.ts +96 -67
- package/src/lib/busy.ts +1 -25
- package/src/lib/exec.ts +13 -0
- package/src/lib/inject.ts +10 -56
- package/src/lib/mirror.ts +13 -0
- package/src/lib/probe.ts +1 -15
- package/src/lib/state.ts +10 -39
- package/src/lib/tmux.ts +1 -0
- package/src/lib/ui.ts +208 -0
- package/src/merge.ts +2 -66
- package/src/platform/probe.ts +25 -0
- package/src/platform/process/exec.ts +317 -0
- package/src/platform/process/tmux.ts +60 -0
- package/src/platform/session/busy.ts +33 -0
- package/src/platform/session/inject.ts +89 -0
- package/src/platform/session/notify.ts +20 -0
- package/src/platform/storage/state.ts +99 -0
- package/src/platform/storage/store.ts +61 -0
- package/src/summary.ts +1 -0
- package/src/tools.ts +8 -244
- package/src/tui.ts +57 -186
- package/src/types.ts +1 -73
- package/src/v2/context.ts +470 -0
- package/src/v2/host.ts +120 -0
- package/src/v2/loader.ts +150 -0
- package/src/workflow/agents/codebase-researcher.ts +27 -0
- package/src/workflow/agents/design-explorer.ts +33 -0
- package/src/workflow/agents/doubt-reviewer.ts +26 -0
- package/src/workflow/agents/engineering-coach.ts +23 -0
- package/src/workflow/agents/performance-auditor.ts +29 -0
- package/src/workflow/agents/security-auditor.ts +23 -0
- package/src/workflow/agents/spec-reviewer.ts +15 -0
- package/src/workflow/agents/standards-reviewer.ts +24 -0
- package/src/workflow/agents/test-engineer.ts +28 -0
- package/src/workflow/catalog.ts +210 -0
- package/src/workflow/templates/build.ts +47 -0
- package/src/workflow/templates/define.ts +45 -0
- package/src/workflow/templates/diagnose.ts +58 -0
- package/src/workflow/templates/plan.ts +52 -0
- package/src/workflow/templates/ship.ts +64 -0
- package/src/buddy/reactions.ts +0 -41
- package/src/buddy/types.ts +0 -30
- package/src/config.ts +0 -19
- package/src/features/checkpoints.ts +0 -128
- package/src/features/sandbox.ts +0 -104
- package/src/validate.ts +0 -197
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: to-spec
|
|
3
|
+
description: Synthesizes conversation context and requirements into a structured specification without reopening interviews. Use when requirements have been discussed and agreed upon, and you need to document the formal spec.
|
|
4
|
+
pack: core
|
|
5
|
+
license: MIT
|
|
6
|
+
attribution: Adapted from mattpocock/skills (MIT License)
|
|
7
|
+
references:
|
|
8
|
+
- SPEC-TEMPLATE.md
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# To Spec: Requirements Synthesis
|
|
12
|
+
|
|
13
|
+
Transform agreed conversation context, architecture boundaries, and requirements into a clear, testable specification.
|
|
14
|
+
|
|
15
|
+
## Core Rule: No Renewed Interview
|
|
16
|
+
|
|
17
|
+
Do **NOT** reopen the interview loop. The time for grilling was during requirements elicitation (`grilling`). Now is the time for **synthesis**: consolidate what has already been agreed upon into an actionable document.
|
|
18
|
+
|
|
19
|
+
- Only pause to confirm testing seams if multiple viable seams exist.
|
|
20
|
+
- Do not ask open-ended questions about things already discussed.
|
|
21
|
+
|
|
22
|
+
## When to Use
|
|
23
|
+
|
|
24
|
+
- Requirements have been gathered and agreed upon (e.g. following a grilling session).
|
|
25
|
+
- Drafting `SPEC.md` or updating an existing project specification.
|
|
26
|
+
- Translating high-level feature requests into concrete acceptance criteria and seams.
|
|
27
|
+
|
|
28
|
+
## When NOT to Use
|
|
29
|
+
|
|
30
|
+
- Requirements are still vague or contradictory (use `grilling` first).
|
|
31
|
+
- Decomposing an already completed spec into executable tasks (use `to-tickets` instead).
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Synthesis Process
|
|
36
|
+
|
|
37
|
+
### 1. Survey Codebase Context & Conventions
|
|
38
|
+
|
|
39
|
+
Inspect existing code to ground the spec:
|
|
40
|
+
|
|
41
|
+
- Check existing ubiquitous language (`CONTEXT.md`).
|
|
42
|
+
- Respect any relevant Architecture Decision Records (`docs/adr/`).
|
|
43
|
+
- Identify established test patterns and libraries in the repository.
|
|
44
|
+
|
|
45
|
+
### 2. Identify the Public Testing Seams
|
|
46
|
+
|
|
47
|
+
Determine the cleanest boundaries at which automated tests will verify the feature:
|
|
48
|
+
|
|
49
|
+
- **Prefer existing seams:** Use existing module interfaces or API endpoints rather than creating artificial test-only hooks.
|
|
50
|
+
- **Prefer high seams:** Test through the public interface of the module or service.
|
|
51
|
+
- **Fewer seams is better:** The ideal number of external test seams across a feature is one or two.
|
|
52
|
+
|
|
53
|
+
Check with the user that the selected test seams match expectations before finalizing the document.
|
|
54
|
+
|
|
55
|
+
### 3. Draft the Specification
|
|
56
|
+
|
|
57
|
+
Write `SPEC.md` (or the project's designated spec location) using the structured format in [SPEC-TEMPLATE.md](SPEC-TEMPLATE.md):
|
|
58
|
+
|
|
59
|
+
- **Problem Statement & Solution:** User-perspective framing.
|
|
60
|
+
- **Ubiquitous Language:** Canonical names and entities.
|
|
61
|
+
- **User Stories:** Extensive, numbered list with verifiable acceptance criteria.
|
|
62
|
+
- **Public Seams & Interfaces:** Explicit signatures, types, and invariants.
|
|
63
|
+
- **3-Tier Boundaries:** Always Do / Ask First / Never Do.
|
|
64
|
+
- **Out of Scope:** Concrete non-goals preventing scope creep.
|
|
65
|
+
- **Verification Strategy:** Automated commands that prove completion.
|
|
66
|
+
|
|
67
|
+
### 4. Present for Confirmation
|
|
68
|
+
|
|
69
|
+
Present the synthesized specification to the human architect for approval. Once confirmed, proceed to task decomposition via `to-tickets`.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Specification Template
|
|
2
|
+
|
|
3
|
+
```markdown
|
|
4
|
+
# Specification: [Feature Name]
|
|
5
|
+
|
|
6
|
+
## 1. Problem Statement
|
|
7
|
+
|
|
8
|
+
The problem that the user or system is facing, stated from the user's perspective. What friction, limitation, or deficiency currently exists?
|
|
9
|
+
|
|
10
|
+
## 2. Proposed Solution
|
|
11
|
+
|
|
12
|
+
The high-level solution and intended user experience. How does this resolve the problem statement?
|
|
13
|
+
|
|
14
|
+
## 3. Ubiquitous Language & Entities
|
|
15
|
+
|
|
16
|
+
Key terms defined precisely (referencing `CONTEXT.md` where established).
|
|
17
|
+
|
|
18
|
+
- **[Entity 1]:** [Definition]
|
|
19
|
+
- **[Entity 2]:** [Definition]
|
|
20
|
+
|
|
21
|
+
## 4. User Stories & Acceptance Criteria
|
|
22
|
+
|
|
23
|
+
Numbered list of user stories covering all functional aspects:
|
|
24
|
+
|
|
25
|
+
1. **As a** [actor], **I want** [capability], **so that** [outcome/benefit].
|
|
26
|
+
- **Given:** [initial state]
|
|
27
|
+
- **When:** [action taken]
|
|
28
|
+
- **Then:** [expected verifiable outcome]
|
|
29
|
+
|
|
30
|
+
## 5. Public Seams & Interfaces
|
|
31
|
+
|
|
32
|
+
Explicit declarations of public types, API endpoints, function signatures, or CLI contracts:
|
|
33
|
+
|
|
34
|
+
- Prefer existing high-level seams over creating new low-level seams.
|
|
35
|
+
- Document inputs, outputs, errors, and invariants.
|
|
36
|
+
|
|
37
|
+
## 6. 3-Tier Boundaries
|
|
38
|
+
|
|
39
|
+
- **Always Do:** Non-negotiables (invariants, validations, mandatory logging).
|
|
40
|
+
- **Ask First:** Irreversible actions requiring explicit human confirmation.
|
|
41
|
+
- **Never Do:** Prohibited patterns or anti-patterns.
|
|
42
|
+
|
|
43
|
+
## 7. Out of Scope (Non-Goals)
|
|
44
|
+
|
|
45
|
+
Explicit list of items intentionally excluded from this increment.
|
|
46
|
+
|
|
47
|
+
## 8. Verification Strategy
|
|
48
|
+
|
|
49
|
+
How the feature will be proven before merging (unit tests, integration smoke tests, automated reproduction commands).
|
|
50
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: to-tickets
|
|
3
|
+
description: Decomposes a specification or plan into a directed acyclic graph (DAG) of independently verifiable, tracer-bullet tasks. Use when planning implementation steps from a spec or readying work for execution.
|
|
4
|
+
pack: core
|
|
5
|
+
license: MIT
|
|
6
|
+
attribution: Adapted from mattpocock/skills (MIT License)
|
|
7
|
+
references:
|
|
8
|
+
- TICKET-TEMPLATE.md
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# To Tickets: Task Decomposition & Dependency Planning
|
|
12
|
+
|
|
13
|
+
Decompose a specification, feature plan, or design into an executable dependency DAG of **tracer-bullet tasks**.
|
|
14
|
+
|
|
15
|
+
## Core Principles
|
|
16
|
+
|
|
17
|
+
### 1. Vertical Tracer Bullets vs Horizontal Layers
|
|
18
|
+
|
|
19
|
+
- **Anti-pattern (Horizontal Slicing):** "Task 1: Build all database tables; Task 2: Build all API routes; Task 3: Build UI." Slices cannot be tested end-to-end, defer integration risks to the end, and leave software broken between steps.
|
|
20
|
+
- **Tracer Bullet (Vertical Slicing):** Each task cuts a narrow but COMPLETE path through schema, logic, interface, and tests. Each completed task delivers working, verifiable software at that seam.
|
|
21
|
+
|
|
22
|
+
### 2. Context-Sized Increments
|
|
23
|
+
|
|
24
|
+
- Size each increment so it can be implemented, verified, and reasoned about within a single, fresh context window.
|
|
25
|
+
- Thin increments (~50-150 lines of focused diff) minimize regression risk and make rollbacks trivial.
|
|
26
|
+
|
|
27
|
+
### 3. Explicit Blocking Edges & The Ready Frontier
|
|
28
|
+
|
|
29
|
+
- Every task explicitly declares its prerequisite blockers: `Blocked By: [Task IDs]`.
|
|
30
|
+
- Tasks with no blockers form the **Ready Frontier** and can be worked on immediately or in parallel.
|
|
31
|
+
- Maintain an accurate DAG so tasks are never started before their true foundations exist.
|
|
32
|
+
|
|
33
|
+
### 4. The Wide-Refactor Exception: Expand-and-Contract
|
|
34
|
+
|
|
35
|
+
A **wide refactor** (e.g. renaming a ubiquitous column, changing a core function signature used across hundreds of files) cannot be landed in a single vertical slice without breaking the entire test suite.
|
|
36
|
+
|
|
37
|
+
Do NOT force wide refactors into single tracer bullets. Sequence them as **Expand-and-Contract**:
|
|
38
|
+
|
|
39
|
+
1. **Expand:** Introduce the new interface or column alongside the old one. Both exist simultaneously; existing tests remain green.
|
|
40
|
+
2. **Migrate:** Migrate callers in bounded batches (by directory or module). Each batch is an independent task blocked by Expand, keeping CI green at every step.
|
|
41
|
+
3. **Contract:** Once all callers use the new interface, delete the old implementation and remove deprecation warnings. Blocked by all migration batches.
|
|
42
|
+
4. _(Optional)_ When intermediate batches cannot stay green in isolation, execute on a dedicated integration branch with a final integrate-and-verify gate.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## The Decomposition Process
|
|
47
|
+
|
|
48
|
+
### 1. Prefactoring First
|
|
49
|
+
|
|
50
|
+
Look for opportunities to refactor existing code before adding new logic:
|
|
51
|
+
|
|
52
|
+
> _"Make the change easy, then make the easy change."_ (Kent Beck)
|
|
53
|
+
> If prefactoring is needed, make it Task 1 on the frontier.
|
|
54
|
+
|
|
55
|
+
### 2. Draft Tasks with Public Seams
|
|
56
|
+
|
|
57
|
+
For each task, define:
|
|
58
|
+
|
|
59
|
+
- **Title:** Concise imperative action.
|
|
60
|
+
- **Blocked By:** Explicit prerequisites.
|
|
61
|
+
- **Seam:** File path to the automated test suite or verification assertion that will prove completion.
|
|
62
|
+
- **Acceptance Criteria:** Verifiable conditions satisfying requirements.
|
|
63
|
+
|
|
64
|
+
### 3. Output Location
|
|
65
|
+
|
|
66
|
+
By default, output the plan to `tasks/plan.md` in the workspace using the format in [TICKET-TEMPLATE.md](TICKET-TEMPLATE.md). If integrated with an external issue tracker (GitHub, Linear), format each task as an issue and link blocking dependencies.
|
|
67
|
+
|
|
68
|
+
### 4. Confirm with Human Architect
|
|
69
|
+
|
|
70
|
+
Present the task breakdown to the user. Confirm:
|
|
71
|
+
|
|
72
|
+
- Granularity: Are any tasks too large or too trivial?
|
|
73
|
+
- Blocking Edges: Are dependencies minimal and strictly gating?
|
|
74
|
+
- Once confirmed, proceed to execution via `/build` or the `tdd` skill.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Ticket & Plan Templates
|
|
2
|
+
|
|
3
|
+
## Single Task Format (Markdown)
|
|
4
|
+
|
|
5
|
+
```markdown
|
|
6
|
+
### [TASK-NN]: [Concise Title in Imperative Mood]
|
|
7
|
+
|
|
8
|
+
- **Blocked By:** [TASK-XX, TASK-YY | None (Can start immediately)]
|
|
9
|
+
- **Seam:** [File path to test file, e.g. tests/unit/auth-token.test.ts]
|
|
10
|
+
- **Deliverable:** The end-to-end behavior this task makes work from the caller's perspective.
|
|
11
|
+
- **Acceptance Criteria:**
|
|
12
|
+
- [ ] Automated test at seam fails before implementation (Red).
|
|
13
|
+
- [ ] Implementation passes test without skips or suppressions (Green).
|
|
14
|
+
- [ ] Linters and typechecks pass clean.
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Tasks Plan Document (`tasks/plan.md`)
|
|
20
|
+
|
|
21
|
+
```markdown
|
|
22
|
+
# Implementation Plan: [Feature Name]
|
|
23
|
+
|
|
24
|
+
## Frontier (Ready to Execute)
|
|
25
|
+
|
|
26
|
+
Tasks whose dependencies are completely satisfied.
|
|
27
|
+
|
|
28
|
+
- [ ] **Task 1: [Title]**
|
|
29
|
+
- **Blocked By:** None
|
|
30
|
+
- **Seam:** `test/feature-core.test.ts`
|
|
31
|
+
- **Scope:** [Description of vertical slice]
|
|
32
|
+
|
|
33
|
+
## Sequence (Blocked)
|
|
34
|
+
|
|
35
|
+
Tasks waiting on prerequisite tasks.
|
|
36
|
+
|
|
37
|
+
- [ ] **Task 2: [Title]**
|
|
38
|
+
- **Blocked By:** Task 1
|
|
39
|
+
- **Seam:** `test/feature-api.test.ts`
|
|
40
|
+
- **Scope:** [Description of vertical slice]
|
|
41
|
+
```
|
package/src/bridge.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createHybridPlugin, type HybridPlugin, type HybridPluginDefinition } from "./core/bridge.ts"
|
package/src/buddy/companion.ts
CHANGED
|
@@ -1,4 +1,74 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { SpriteState } from "./sprites.ts"
|
|
2
|
+
|
|
3
|
+
export const SPECIES = [
|
|
4
|
+
"cat",
|
|
5
|
+
"dog",
|
|
6
|
+
"bunny",
|
|
7
|
+
"owl",
|
|
8
|
+
"bat",
|
|
9
|
+
"penguin",
|
|
10
|
+
"duck",
|
|
11
|
+
"ghost",
|
|
12
|
+
"slime",
|
|
13
|
+
] as const
|
|
14
|
+
export type Species = (typeof SPECIES)[number]
|
|
15
|
+
|
|
16
|
+
export type Rarity = "common" | "uncommon" | "rare" | "legendary"
|
|
17
|
+
|
|
18
|
+
export interface CompanionStats {
|
|
19
|
+
patience: number
|
|
20
|
+
chaos: number
|
|
21
|
+
wisdom: number
|
|
22
|
+
snark: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface Companion {
|
|
26
|
+
species: Species
|
|
27
|
+
rarity: Rarity
|
|
28
|
+
name: string
|
|
29
|
+
stats: CompanionStats
|
|
30
|
+
hatchedAt: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type ReactionKind = "done" | "error" | "permission" | "question" | "pet"
|
|
34
|
+
|
|
35
|
+
/** Speech-bubble line + which face the sprite pulls while it shows. */
|
|
36
|
+
export interface Reaction {
|
|
37
|
+
text: string
|
|
38
|
+
state: SpriteState
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Lines render into the sprite's 12-col effect row -- keep every line <= 12 chars.
|
|
42
|
+
const POOLS: Record<ReactionKind, { lines: string[]; state: SpriteState }> = {
|
|
43
|
+
done: { lines: ["cooked", "ate that", "big W", "say less", "we ball"], state: "idle" },
|
|
44
|
+
error: { lines: ["bruh", "ur cooked", "skill issue", "aint no way", "big L"], state: "alarmed" },
|
|
45
|
+
permission: { lines: ["let me cook", "valid?", "we good?", "bet?", "vibe check"], state: "curious" },
|
|
46
|
+
question: { lines: ["wym?", "the move?", "u tell me", "spill", "hbu?"], state: "curious" },
|
|
47
|
+
pet: { lines: ["pookie!", "w rizz", "ur valid", "slay", "ily"], state: "pet" },
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Pure: pick a random line for a reaction kind. */
|
|
51
|
+
export function pickReaction(kind: ReactionKind, rng: () => number = Math.random): Reaction {
|
|
52
|
+
const pool = POOLS[kind]
|
|
53
|
+
return { text: pool.lines[Math.floor(rng() * pool.lines.length)]!, state: pool.state }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ReactionGate {
|
|
57
|
+
/** True + arms the cooldown if enough time has passed since the last fire. */
|
|
58
|
+
tryFire(now?: number): boolean
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Debounce for event-driven reactions -- keeps a busy session from spamming the bubble. */
|
|
62
|
+
export function createReactionGate(cooldownMs = 8000): ReactionGate {
|
|
63
|
+
let last = -Infinity
|
|
64
|
+
return {
|
|
65
|
+
tryFire(now: number = Date.now()): boolean {
|
|
66
|
+
if (now - last < cooldownMs) return false
|
|
67
|
+
last = now
|
|
68
|
+
return true
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
}
|
|
2
72
|
|
|
3
73
|
const RARITY_WEIGHTS: { rarity: Rarity; weight: number }[] = [
|
|
4
74
|
{ rarity: "common", weight: 60 },
|
|
@@ -55,16 +125,45 @@ function rollStat(rng: () => number): number {
|
|
|
55
125
|
}
|
|
56
126
|
|
|
57
127
|
/**
|
|
58
|
-
*
|
|
59
|
-
* hatched a species we have since retired would look it up and find no art. Move
|
|
60
|
-
* it onto a species we still draw, keeping the identity that isn't the drawing:
|
|
61
|
-
* same name, rarity, stats and hatch date. Returns undefined when nothing to do.
|
|
128
|
+
* Move companion onto an active species if its hatched species was retired.
|
|
62
129
|
*/
|
|
63
130
|
export function migrateSpecies(c: Companion, rng: () => number = Math.random): Companion | undefined {
|
|
64
131
|
if ((SPECIES as readonly string[]).includes(c.species)) return undefined
|
|
65
132
|
return { ...c, species: pick(rng, SPECIES) }
|
|
66
133
|
}
|
|
67
134
|
|
|
135
|
+
/** Short character descriptions for each species based on their idle fidget. */
|
|
136
|
+
export const SPECIES_DESCRIPTIONS: Record<Species, string> = {
|
|
137
|
+
cat: "tail flick",
|
|
138
|
+
dog: "ear perk",
|
|
139
|
+
bunny: "ear wiggle",
|
|
140
|
+
owl: "feather ruffle",
|
|
141
|
+
bat: "wing beat",
|
|
142
|
+
penguin: "waddle",
|
|
143
|
+
duck: "ripples",
|
|
144
|
+
ghost: "float",
|
|
145
|
+
slime: "squash",
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Pure: formatted description for dialog options. */
|
|
149
|
+
export function speciesDescription(species: Species, isCurrent = false): string {
|
|
150
|
+
const desc = SPECIES_DESCRIPTIONS[species] ?? species
|
|
151
|
+
return isCurrent ? `${desc} (current)` : desc
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Move companion to a new species, preserving name, stats, and hatchedAt. */
|
|
155
|
+
export function switchSpecies(c: Companion, species: Species): Companion {
|
|
156
|
+
if (!(SPECIES as readonly string[]).includes(species)) return c
|
|
157
|
+
return { ...c, species }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Pure: get the next species in the SPECIES rotation. */
|
|
161
|
+
export function cycleSpecies(current: Species): Species {
|
|
162
|
+
const idx = SPECIES.indexOf(current)
|
|
163
|
+
if (idx === -1) return SPECIES[0]
|
|
164
|
+
return SPECIES[(idx + 1) % SPECIES.length]!
|
|
165
|
+
}
|
|
166
|
+
|
|
68
167
|
/** One-line card for the /oc-buddy toast. */
|
|
69
168
|
export function describeCompanion(c: Companion): string {
|
|
70
169
|
const s = c.stats
|
package/src/buddy/sprites.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Species } from "./
|
|
1
|
+
import type { Species } from "./companion.ts"
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Every frame is padded to exactly SPRITE_WIDTH columns so animation never
|
|
@@ -35,8 +35,8 @@ interface SpeciesArt {
|
|
|
35
35
|
/**
|
|
36
36
|
* Which of the two poses a tick shows. Per-species on purpose: alternating
|
|
37
37
|
* every tick reads as a strobe, and it makes every species move alike. A cat
|
|
38
|
-
* flicks its tail rarely, a dog wags nonstop, a
|
|
39
|
-
*
|
|
38
|
+
* flicks its tail rarely, a dog wags nonstop, a bat's wingbeat flutters rapidly,
|
|
39
|
+
* a ghost never quite lands.
|
|
40
40
|
*/
|
|
41
41
|
beat: (t: number) => 0 | 1
|
|
42
42
|
}
|
|
@@ -148,7 +148,7 @@ export function withBubble(frame: string, bubble: string | undefined): string {
|
|
|
148
148
|
/** Pure: render one frame. `tick` drives idle fidget + blink; other states are static. */
|
|
149
149
|
export function spriteFrame(species: Species, state: SpriteState, tick: number): string {
|
|
150
150
|
const art = ART[species]
|
|
151
|
-
const t = (
|
|
151
|
+
const t = Math.max(0, tick)
|
|
152
152
|
const body = art.idle[art.beat(t)]!
|
|
153
153
|
const rest = art.idle[0]!
|
|
154
154
|
|