opencode-overclock 0.3.0 → 0.4.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 +206 -111
- package/package.json +2 -2
- 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 +53 -0
- package/src/core/policy.ts +128 -0
- package/src/core/summary.ts +33 -0
- package/src/core/types.ts +164 -0
- package/src/features/buddy.ts +1 -2
- package/src/features/guard.ts +168 -30
- package/src/features/index.ts +6 -4
- package/src/features/recovery.ts +143 -0
- package/src/features/sched.ts +147 -89
- package/src/features/tasks.ts +52 -18
- package/src/features/truncator.ts +99 -0
- package/src/features/usage.ts +26 -65
- package/src/index.ts +96 -67
- package/src/lib/busy.ts +1 -25
- package/src/lib/exec.ts +7 -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 +76 -0
- package/src/platform/process/tmux.ts +60 -0
- package/src/platform/session/busy.ts +33 -0
- package/src/platform/session/inject.ts +82 -0
- package/src/platform/session/notify.ts +20 -0
- package/src/platform/storage/state.ts +77 -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 +117 -0
- package/src/v2/loader.ts +150 -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
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# opencode-overclock
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
The modular workflow suite and power-ups for [opencode](https://opencode.ai): background
|
|
4
|
+
tasks, cron-style scheduling, quality-gate hooks, cost telemetry — and an ASCII companion.
|
|
5
5
|
|
|
6
6
|
Everything is a separate module you can turn off individually, so you can take one feature and
|
|
7
7
|
ignore the rest. When opencode ships a native equivalent, the matching module goes away.
|
|
@@ -13,18 +13,18 @@ opencode plugin -g opencode-overclock # every project
|
|
|
13
13
|
|
|
14
14
|
## What you get
|
|
15
15
|
|
|
16
|
-
| Module
|
|
17
|
-
|
|
|
18
|
-
| `tasks`
|
|
19
|
-
| `sched`
|
|
20
|
-
| `guard`
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
23
|
-
| `
|
|
24
|
-
| `buddy`
|
|
16
|
+
| Module | What it does | Tools it adds |
|
|
17
|
+
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
|
|
18
|
+
| `tasks` | Run shell commands in the background. The agent gets the result posted back into the session when they finish, and a nudge if one blocks on a prompt. | `task_run` `task_status` `task_output` `task_kill` |
|
|
19
|
+
| `sched` | Recurring prompts on a cron expression or an interval (`"5m"`). Survives restarts; an interval on the current session makes a loop. | `schedule_create` `schedule_list` `schedule_delete` |
|
|
20
|
+
| `guard` | Your own quality gates: run a command after the agent edits files, and feed failures back to it once the session goes idle. Built-in recipes & edit recovery. | — |
|
|
21
|
+
| `recovery` | Automatically heal provider errors (missing tool results, thinking block sequencing, context limit) and auto-resume sessions. | — |
|
|
22
|
+
| `truncator` | Context-protecting smart output truncation for high-volume tools (`task_output`, `bash`, `grep`, `glob`, `webfetch`) preserving header & tail diagnostics. | — |
|
|
23
|
+
| `usage` | Per-day and per-session cost and token totals, collected from the event bus (accessible via TUI `/oc-usage`). | — |
|
|
24
|
+
| `buddy` | An ASCII pet next to the prompt that reacts to what the session is doing. Purely cosmetic. | — |
|
|
25
25
|
|
|
26
26
|
On top of the tools, the TUI side adds desktop notifications when a turn finishes or the agent
|
|
27
|
-
needs you, plus `/oc-tasks`, `/oc-usage`, `/oc-schedules` and `/oc-buddy
|
|
27
|
+
needs you, plus `/oc-tasks`, `/oc-usage`, `/oc-schedules`, `/oc-buddy` (pet), `/oc-buddy-switch` (choose species), and `/oc-buddy-cycle` (next species).
|
|
28
28
|
|
|
29
29
|
**Please read this before installing:** overclock gives the agent the ability to run shell
|
|
30
30
|
commands in the background (`task_run`) and to schedule recurring prompts (`schedule_create`).
|
|
@@ -54,142 +54,239 @@ a plugin's `dispose` hook, without which this plugin's timers and watchers are n
|
|
|
54
54
|
|
|
55
55
|
## Configuration
|
|
56
56
|
|
|
57
|
-
Everything is optional. With no
|
|
58
|
-
|
|
59
|
-
gate. A reasonable `.opencode/overclock.json` to start from:
|
|
57
|
+
Everything is optional. With no options configured, overclock runs with sensible defaults: background
|
|
58
|
+
tasks, cron-style scheduling, quality gates, usage telemetry, and the buddy are active immediately.
|
|
60
59
|
|
|
61
|
-
|
|
60
|
+
Configure options directly in your project or global `opencode.json`:
|
|
61
|
+
|
|
62
|
+
```jsonc
|
|
63
|
+
{
|
|
64
|
+
"plugin": [
|
|
65
|
+
[
|
|
66
|
+
"opencode-overclock",
|
|
67
|
+
{
|
|
68
|
+
"tasks": { "killOnExit": true },
|
|
69
|
+
"guard": { "auto": true },
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
],
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
To turn an individual feature off:
|
|
77
|
+
|
|
78
|
+
```jsonc
|
|
79
|
+
{
|
|
80
|
+
"plugin": [
|
|
81
|
+
[
|
|
82
|
+
"opencode-overclock",
|
|
83
|
+
{
|
|
84
|
+
"buddy": false,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
],
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
| Module | Options |
|
|
92
|
+
| ---------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
93
|
+
| `tasks` | `killOnExit` bool · `stallDetection` bool · `stallThresholdMs` num · `stallCheckIntervalMs` num · `tmux` bool |
|
|
94
|
+
| `sched` | `skipIfBusy` bool |
|
|
95
|
+
| `guard` | `hooks` array · `recipes` array (`["tsc", "eslint", "cargo", "ruff", "go"]`) · `auto` bool · `editRecovery` bool |
|
|
96
|
+
| `recovery` | `maxAttempts` num · `cooldownMs` num · `autoResume` bool |
|
|
97
|
+
| `truncator` | `maxChars` num · `tools` array · `headLines` num · `tailLines` num |
|
|
98
|
+
| `usage`, `buddy` | — |
|
|
99
|
+
|
|
100
|
+
### Quality gates (`guard`)
|
|
101
|
+
|
|
102
|
+
Quality gates let you define automated feedback loops. Whenever the agent modifies code with `edit`
|
|
103
|
+
or `write`, the guard runs your project's verification command in the background. If the command fails,
|
|
104
|
+
the error output is automatically fed back into the session once the agent finishes its turn, prompting
|
|
105
|
+
it to self-correct.
|
|
106
|
+
|
|
107
|
+
Because verification commands vary by language and repository, configure hooks in your project's
|
|
108
|
+
local `opencode.json`:
|
|
109
|
+
|
|
110
|
+
#### Stack recipes
|
|
111
|
+
|
|
112
|
+
##### TypeScript / JavaScript
|
|
113
|
+
|
|
114
|
+
```jsonc
|
|
115
|
+
{
|
|
116
|
+
"guard": {
|
|
117
|
+
"hooks": [
|
|
118
|
+
{
|
|
119
|
+
"name": "typecheck",
|
|
120
|
+
"tools": ["edit", "write"],
|
|
121
|
+
"pathFilter": "src/**/*.ts",
|
|
122
|
+
"run": "npm run typecheck",
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
##### Python (Ruff / Pytest)
|
|
130
|
+
|
|
131
|
+
```jsonc
|
|
62
132
|
{
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
},
|
|
74
|
-
"tasks": { "killOnExit": true }
|
|
75
|
-
}
|
|
133
|
+
"guard": {
|
|
134
|
+
"hooks": [
|
|
135
|
+
{
|
|
136
|
+
"name": "lint",
|
|
137
|
+
"tools": ["edit", "write"],
|
|
138
|
+
"pathFilter": "**/*.py",
|
|
139
|
+
"run": "ruff check .",
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
},
|
|
76
143
|
}
|
|
77
144
|
```
|
|
78
145
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
146
|
+
##### Rust (Cargo)
|
|
147
|
+
|
|
148
|
+
```jsonc
|
|
149
|
+
{
|
|
150
|
+
"guard": {
|
|
151
|
+
"hooks": [
|
|
152
|
+
{
|
|
153
|
+
"name": "cargo-check",
|
|
154
|
+
"tools": ["edit", "write"],
|
|
155
|
+
"pathFilter": "**/*.rs",
|
|
156
|
+
"run": "cargo check",
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
```
|
|
82
162
|
|
|
83
|
-
|
|
84
|
-
something off:
|
|
163
|
+
##### Go
|
|
85
164
|
|
|
86
|
-
```
|
|
87
|
-
{
|
|
165
|
+
```jsonc
|
|
166
|
+
{
|
|
167
|
+
"guard": {
|
|
168
|
+
"hooks": [
|
|
169
|
+
{
|
|
170
|
+
"name": "go-test",
|
|
171
|
+
"tools": ["edit", "write"],
|
|
172
|
+
"pathFilter": "**/*.go",
|
|
173
|
+
"run": "go test ./...",
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
}
|
|
88
178
|
```
|
|
89
179
|
|
|
90
|
-
|
|
91
|
-
| ------------------------------- | ----------------------------------------------------------------------------------------------- |
|
|
92
|
-
| `tasks` | `killOnExit` bool · `stallDetection` bool · `stallThresholdMs` num · `stallCheckIntervalMs` num |
|
|
93
|
-
| `sched` | `skipIfBusy` bool |
|
|
94
|
-
| `sandbox` | `net` bool |
|
|
95
|
-
| `guard` | `hooks` array |
|
|
96
|
-
| `usage`, `checkpoints`, `buddy` | — |
|
|
180
|
+
##### Generic / Make
|
|
97
181
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
182
|
+
```jsonc
|
|
183
|
+
{
|
|
184
|
+
"guard": {
|
|
185
|
+
"hooks": [
|
|
186
|
+
{
|
|
187
|
+
"name": "check",
|
|
188
|
+
"tools": ["edit", "write"],
|
|
189
|
+
"run": "make check",
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
```
|
|
101
195
|
|
|
102
|
-
|
|
196
|
+
#### Hook options
|
|
103
197
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
`
|
|
108
|
-
|
|
198
|
+
| Field | Default | Description |
|
|
199
|
+
| ------------ | ----------- | ----------------------------------------------------------------------------- |
|
|
200
|
+
| `name` | _required_ | Identifier displayed in failure reports |
|
|
201
|
+
| `tools` | _required_ | Tools to trigger on, e.g. `["edit", "write"]` |
|
|
202
|
+
| `run` | _required_ | Shell command to execute (receives `$GUARD_TOOL` and `$GUARD_FILE` in env) |
|
|
203
|
+
| `pathFilter` | `undefined` | Optional glob pattern to limit triggers to relevant files (e.g. `**/*.py`) |
|
|
204
|
+
| `mode` | `"inject"` | `"inject"` waits for the session to go idle; `"append"` reports immediately |
|
|
205
|
+
| `debounceMs` | `2000` | Debounce duration for rapid successive edits |
|
|
206
|
+
| `timeoutMs` | `60000` | Execution timeout before killing the command |
|
|
207
|
+
| `maxDeferMs` | `300000` | Maximum time `"inject"` will wait for an idle session before reporting anyway |
|
|
208
|
+
| `onSuccess` | `"silent"` | `"silent"` or `"notify"` |
|
|
109
209
|
|
|
110
|
-
### Restricting tool names
|
|
210
|
+
### Restricting and remapping tool names
|
|
111
211
|
|
|
112
212
|
If your setup only accepts certain tool names, list them in `toolAllowlist`. Any tool whose
|
|
113
|
-
name isn't permitted is withheld from the model rather than offered and refused
|
|
114
|
-
|
|
213
|
+
name isn't permitted is withheld from the model rather than offered and refused:
|
|
214
|
+
|
|
215
|
+
```jsonc
|
|
216
|
+
{
|
|
217
|
+
"plugin": [
|
|
218
|
+
[
|
|
219
|
+
"opencode-overclock",
|
|
220
|
+
{
|
|
221
|
+
"toolAllowlist": ["task_run", "task_status", "schedule_create"],
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
],
|
|
225
|
+
}
|
|
226
|
+
```
|
|
115
227
|
|
|
116
|
-
|
|
228
|
+
`toolNames` maps this plugin's tools onto custom names you want the model to see:
|
|
229
|
+
|
|
230
|
+
```jsonc
|
|
117
231
|
{
|
|
118
|
-
"
|
|
119
|
-
|
|
232
|
+
"plugin": [
|
|
233
|
+
[
|
|
234
|
+
"opencode-overclock",
|
|
235
|
+
{
|
|
236
|
+
"toolNames": { "task_run": "run_background_task" },
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
],
|
|
120
240
|
}
|
|
121
241
|
```
|
|
122
242
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
built-in (`bash`, `task`, …) or differs from one only by capitalisation — the first replaces
|
|
129
|
-
that built-in, the second reads as a duplicate to anything matching case-insensitively.
|
|
130
|
-
Descriptions mentioning a renamed tool are rewritten too, so the agent never gets instructions
|
|
131
|
-
naming a tool it wasn't given. Permission ids keep their declared names, so existing permission
|
|
132
|
-
config still applies.
|
|
133
|
-
|
|
134
|
-
#### Named lists
|
|
135
|
-
|
|
136
|
-
`toolAllowlist` entries can also name a bundled list, which expands to every name it permits.
|
|
137
|
-
Mix and match freely — `["claude-code", "MyExtraTool"]` is a bundled list plus one of your own.
|
|
138
|
-
|
|
139
|
-
`claude-code` is the tool set Claude Code registers. opencode's ids are snake_case and Claude
|
|
140
|
-
Code's are PascalCase, so the two vocabularies don't overlap and these names are free to use.
|
|
141
|
-
A bundled list also supplies default names for tools where it contains the same operation:
|
|
142
|
-
|
|
143
|
-
| Module | Declared | Sent as |
|
|
144
|
-
| ------- | ----------------- | ------------ |
|
|
145
|
-
| `tasks` | `task_run` | `TaskCreate` |
|
|
146
|
-
| `tasks` | `task_status` | `TaskList` |
|
|
147
|
-
| `tasks` | `task_output` | `TaskOutput` |
|
|
148
|
-
| `tasks` | `task_kill` | `TaskStop` |
|
|
149
|
-
| `sched` | `schedule_create` | `CronCreate` |
|
|
150
|
-
| `sched` | `schedule_list` | `CronList` |
|
|
151
|
-
| `sched` | `schedule_delete` | `CronDelete` |
|
|
152
|
-
|
|
153
|
-
That's the whole table, and it stops there on purpose. Nothing in the `claude-code` set means
|
|
154
|
-
"revert a session checkpoint" or "report token spend", so `checkpoints`, `usage` and
|
|
155
|
-
`bash_unsandboxed` get no default name: handing them an unrelated one would tell the model the
|
|
156
|
-
wrong thing about what they do. They're withheld until you choose a name yourself, and startup
|
|
157
|
-
says which names are free:
|
|
158
|
-
|
|
159
|
-
```json
|
|
243
|
+
### Running OpenCode V2 plugins on OpenCode V1
|
|
244
|
+
|
|
245
|
+
OpenCode V1 distributions cannot natively load V2 plugins (which export `{ id, setup }` or `{ id, effect }` instead of a server function). Overclock provides an embedded V2 host engine that runs V2 plugins side-by-side with V1 tools:
|
|
246
|
+
|
|
247
|
+
```jsonc
|
|
160
248
|
{
|
|
161
|
-
"
|
|
162
|
-
|
|
249
|
+
"plugin": [
|
|
250
|
+
[
|
|
251
|
+
"opencode-overclock",
|
|
252
|
+
{
|
|
253
|
+
"plugins": ["./plugins/custom-agent.ts", ["opencode-plugin-review", { "strict": true }]],
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
],
|
|
163
257
|
}
|
|
164
258
|
```
|
|
165
259
|
|
|
166
|
-
`
|
|
167
|
-
|
|
260
|
+
Overclock synthesizes a spec-compliant `PluginContext`, adapting V2 domain transforms (`agent`, `command`, `catalog`, `reference`, `skill`, `aisdk`) to live V1 config and chat hooks while keeping all V1 power tools active.
|
|
261
|
+
|
|
262
|
+
Startup tells you what was renamed, what was withheld, and warns on collisions with opencode
|
|
263
|
+
built-in tools. Descriptions mentioning a renamed tool are rewritten automatically.
|
|
168
264
|
|
|
169
265
|
## Notes on the TUI surface
|
|
170
266
|
|
|
171
267
|
The TUI plugin sends a desktop notification (with sound) when a turn completes or the agent
|
|
172
268
|
needs permission, asks a question, or errors — each individually switchable through plugin
|
|
173
|
-
options
|
|
269
|
+
options (`notifyIdle`, `notifyPermission`, `notifyQuestion`, `notifyError`, `buddy` in `tui.json`).
|
|
270
|
+
Its slash commands read the state files under `.opencode/overclock/`, so they work
|
|
174
271
|
without going through the model.
|
|
175
272
|
|
|
176
273
|
The buddy hatches once per install with a random species, rarity and name, persists in the
|
|
177
274
|
TUI's key-value store, hides itself below 100 columns, and needs `@opentui/solid` resolvable at
|
|
178
275
|
runtime. If it isn't, the buddy quietly sits out and the rest of the TUI plugin still loads.
|
|
276
|
+
Switch buddy on demand via `/oc-buddy-switch` (opens an interactive species picker or rolls a fresh companion)
|
|
277
|
+
or `/oc-buddy-cycle` (advances directly to the next species in rotation).
|
|
179
278
|
|
|
180
279
|
## Contributing
|
|
181
280
|
|
|
182
281
|
```
|
|
183
282
|
src/
|
|
184
|
-
index.ts entry:
|
|
283
|
+
index.ts entry: init enabled modules, merge hooks, hybrid V1/V2 export
|
|
185
284
|
tui.ts TUI plugin (notifications + slash commands), separate export
|
|
186
|
-
types
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
lib/ state dir + json, session inject + toast
|
|
192
|
-
features/ one file per module + registry
|
|
285
|
+
core/ types, lifecycle/hook merging, tool policy, capability summary, bridge
|
|
286
|
+
platform/ host adapters: process (exec/tmux), session (busy/inject/notify), storage (state/store), probe
|
|
287
|
+
buddy/ ASCII companion state, sprites, and TUI slot integration
|
|
288
|
+
v2/ embedded V2 plugin host, synthetic context, and dynamic loader
|
|
289
|
+
features/ feature modules (tasks, sched, guard, recovery, truncator, usage, buddy)
|
|
193
290
|
test/ bun test
|
|
194
291
|
```
|
|
195
292
|
|
|
@@ -197,8 +294,6 @@ Adding a feature:
|
|
|
197
294
|
|
|
198
295
|
1. Write `src/features/<name>.ts` exporting a `FeatureModule`.
|
|
199
296
|
2. Register it in `src/features/index.ts`.
|
|
200
|
-
3. If it adds tools, add each one to the `claude-code` table in `src/tools.ts`. A test fails
|
|
201
|
-
otherwise, since an unmapped tool is invisible behind a whitelisting gateway.
|
|
202
297
|
|
|
203
298
|
Background reading:
|
|
204
299
|
[docs/opencode-plugin-surface.md](docs/opencode-plugin-surface.md) maps opencode's
|
|
@@ -243,7 +338,7 @@ somewhere else.
|
|
|
243
338
|
### Headless end-to-end
|
|
244
339
|
|
|
245
340
|
```sh
|
|
246
|
-
timeout 90 opencode run -m
|
|
341
|
+
timeout 90 opencode run -m <provider>/<model> "Use task_run to run 'echo hi' ..." < /dev/null
|
|
247
342
|
```
|
|
248
343
|
|
|
249
344
|
- `< /dev/null` is required; an open stdin hangs.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-overclock",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Power-ups for opencode: background tasks, scheduling,
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Power-ups for opencode: background tasks, cron scheduling, quality-gate hooks, usage telemetry, and companion. Modular, toggleable.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
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
|
|