@mingxy/opencode-mascot 0.5.8 → 0.6.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 +137 -115
- package/package.json +1 -1
- package/src/builtins/cat/frames.ts +48 -0
- package/src/builtins/cat/index.ts +167 -0
- package/src/components/sidebar-mascot.tsx +5 -1
- package/src/core/ascii-renderer.tsx +2 -1
- package/src/core/mascot-loader.ts +2 -0
package/README.md
CHANGED
|
@@ -1,123 +1,147 @@
|
|
|
1
1
|
# 🐱 opencode-mascot
|
|
2
2
|
|
|
3
|
-
> OpenCode TUI
|
|
3
|
+
> OpenCode TUI mascot plugin framework — bring your terminal to life
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Customizable ASCII mascots that breathe, walk, sleep, get launched across the screen, blown up by falling bombs, then quietly reassemble themselves.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
[English](./README.md) | [简体中文](./README_zh-CN.md)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## 🏠 Home Page
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|------|------|------|
|
|
13
|
-
| **月儿** (yueer) | 紫发呆毛女孩,傲娇风格,默认形象 | `#8B7EB8` 淡紫 |
|
|
14
|
-
| **包子** (baozi) | 热气腾腾的包子,温暖治愈 | `#D4885A` 暖橙 |
|
|
11
|
+

|
|
15
12
|
|
|
16
|
-
|
|
13
|
+
## 💼 Work Page
|
|
17
14
|
|
|
18
|
-
|
|
15
|
+

|
|
19
16
|
|
|
20
|
-
|
|
17
|
+
---
|
|
21
18
|
|
|
22
|
-
|
|
19
|
+
## ✨ Features
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
|---|------|------|------|
|
|
26
|
-
| 1 | 眨眼 | 随机(30%概率/2.5s) | 切换 blink 帧 150ms |
|
|
27
|
-
| 2 | 随机表情 | 每 8s | idle 时随机切换表情 |
|
|
28
|
-
| 3 | 呼吸 | 每 3s | 行向上偏移一格,模拟呼吸起伏 |
|
|
29
|
-
| 4 | 走路 | 每 20-40s | 左右晃动(14步路径) |
|
|
30
|
-
| 5 | 跳跃 | 每 20-40s | 弹跳 -2→-1→0 |
|
|
31
|
-
| 6 | 睡眠 | idle 90-120s | 自动闭眼 + 火星文 Zzz |
|
|
21
|
+
### 🎭 Built-in Characters (3)
|
|
32
22
|
|
|
33
|
-
|
|
23
|
+
| Character | Description | Color |
|
|
24
|
+
|-----------|-------------|-------|
|
|
25
|
+
| **yueer** | Purple-haired girl with an ahoge, tsundere style, default mascot | `#8B7EB8` lavender |
|
|
26
|
+
| **baozi** | A steaming hot bun, warm and cozy | `#D4885A` warm orange |
|
|
27
|
+
| **cat** | Orange tabby cat — purring, kneading, tail-swishing | `#FFA500` orange |
|
|
34
28
|
|
|
35
|
-
|
|
36
|
-
|---|------|------|------|
|
|
37
|
-
| 7 | 呆毛闪烁 | 随机(25%/1.5s) | ☆ ↔ ★ |
|
|
38
|
-
| 8 | 开心晃头 | happy 状态 | 脸左右摆 + 呆毛同步偏移 |
|
|
39
|
-
| 9 | 思考跺脚 | thinking 状态 | 左脚固定 ║,右脚 ║↔_ 单脚跺 |
|
|
40
|
-
| 10 | 思考变脸 | thinking 状态 | 6种表情轮换(o_o / O_O / >_o / o_< / ⊙_⊙ / ◔_◔) |
|
|
41
|
-
| 11 | 火星文气泡 | busy/thinking | 12条上标火星文轮换 |
|
|
42
|
-
| 12 | 拖拽扇手 | 拖拽中 | 手臂 ┃███┃ ↔ ╱███╲ |
|
|
43
|
-
| 13 | 跳跃扇手 | 跳跃中 | 同上 |
|
|
29
|
+
Each character includes **5 expression frames**: default / blink / happy / thinking / sleeping
|
|
44
30
|
|
|
45
|
-
|
|
31
|
+
---
|
|
46
32
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
|
33
|
+
### 🎬 Automatic Animations (16)
|
|
34
|
+
|
|
35
|
+
**Built-in (shared by all characters):**
|
|
36
|
+
|
|
37
|
+
| # | Animation | Trigger | Effect |
|
|
38
|
+
|---|-----------|---------|--------|
|
|
39
|
+
| 1 | Blink | Random (30% / 2.5s) | Switch to blink frame for 150ms |
|
|
40
|
+
| 2 | Random expression | Every 8s | Cycles expressions while idle |
|
|
41
|
+
| 3 | Breathing | Every 3s | Lines shift up one row, simulating breathing |
|
|
42
|
+
| 4 | Walking | Every 20-40s | Sways left and right (14-step path) |
|
|
43
|
+
| 5 | Jumping | Every 20-40s | Bounces -2 → -1 → 0 |
|
|
44
|
+
| 6 | Sleep | Idle 90-120s | Auto-closed eyes + alien-text Zzz |
|
|
45
|
+
|
|
46
|
+
**yueer exclusive:**
|
|
47
|
+
|
|
48
|
+
| # | Animation | Trigger | Effect |
|
|
49
|
+
|---|-----------|---------|--------|
|
|
50
|
+
| 7 | Ahoge sparkle | Random (25% / 1.5s) | ☆ ↔ ★ |
|
|
51
|
+
| 8 | Happy head sway | happy state | Face sways left/right + ahoge synced |
|
|
52
|
+
| 9 | Thinking foot stomp | thinking state | Left foot fixed, right foot stomps ║↔_ |
|
|
53
|
+
| 10 | Thinking face shift | thinking state | 6 expressions rotate (o_o / O_O / >_o / o_< / ⊙_⊙ / ◔_◔) |
|
|
54
|
+
| 11 | Alien text bubble | busy/thinking | 12 alien-text phrases rotate |
|
|
55
|
+
| 12 | Drag arm flail | While dragging | Arms ┃███┃ ↔ ╱███╲ |
|
|
56
|
+
| 13 | Jump arm flail | While jumping | Same as above |
|
|
57
|
+
|
|
58
|
+
**baozi exclusive:**
|
|
59
|
+
|
|
60
|
+
| # | Animation | Trigger | Effect |
|
|
61
|
+
|---|-----------|---------|--------|
|
|
62
|
+
| 14 | Steam | Continuous | 4 steam patterns rotate |
|
|
63
|
+
| 15 | Alien text bubble | busy/thinking | 12 alien-text phrases rotate |
|
|
64
|
+
| 16 | Drag panic | While dragging | `( °□° )` |
|
|
65
|
+
|
|
66
|
+
**cat exclusive:**
|
|
67
|
+
|
|
68
|
+
| # | Animation | Trigger | Effect |
|
|
69
|
+
|---|-----------|---------|--------|
|
|
70
|
+
| 17 | Tail swish | Every 0.6s | Tail `|_)` ↔ `|~)` |
|
|
71
|
+
| 18 | Ear twitch | Random (20% / 2.5s) | Ear `/\` → `/╲` |
|
|
72
|
+
| 19 | Pupil dilate | Random (15% / 4s, idle) | Eyes `o.o` → `@.@` |
|
|
73
|
+
| 20 | Kneading | idle state | Paws alternate `/|| |\` ↔ `/| ||\` |
|
|
74
|
+
| 21 | Purr bubble | busy/thinking | 12 purr phrases rotate (purrr~/mrrrow~/nyaa~) |
|
|
75
|
+
| 22 | Drag bristle | While dragging | Ears bristle `/╲╱╲` + shocked face `>.<` |
|
|
52
76
|
|
|
53
77
|
---
|
|
54
78
|
|
|
55
|
-
### 🖱️
|
|
79
|
+
### 🖱️ Interactions (5)
|
|
56
80
|
|
|
57
|
-
| # |
|
|
58
|
-
|
|
59
|
-
| 1 | **Alt +
|
|
60
|
-
| 2 |
|
|
61
|
-
| 3 |
|
|
62
|
-
| 4 |
|
|
63
|
-
| 5 |
|
|
81
|
+
| # | Action | Effect |
|
|
82
|
+
|---|--------|--------|
|
|
83
|
+
| 1 | **Alt + drag** | Freely move the mascot anywhere |
|
|
84
|
+
| 2 | **Double-click** | Cycle through characters (within 300ms) |
|
|
85
|
+
| 3 | **Drag color flash** | Body flashes through 8 highlight colors at 100ms, locks on release |
|
|
86
|
+
| 4 | **Drag alien text** | Pink "let go of me" alien text appears above head while dragging |
|
|
87
|
+
| 5 | **Drag panic** | `( °□° )` face + arms flailing while dragging |
|
|
64
88
|
|
|
65
89
|
---
|
|
66
90
|
|
|
67
|
-
### 🫣
|
|
91
|
+
### 🫣 Peek-a-Boo System (Work Page)
|
|
68
92
|
|
|
69
|
-
| # |
|
|
70
|
-
|
|
71
|
-
| 1 |
|
|
72
|
-
| 2 |
|
|
73
|
-
| 3 |
|
|
93
|
+
| # | Action | Effect |
|
|
94
|
+
|---|--------|--------|
|
|
95
|
+
| 1 | Drag to left edge | Mascot hides, only 2 rows visible |
|
|
96
|
+
| 2 | Release at edge | Auto peek cycle (peeks 2 more rows every 1.2s, then retreats) |
|
|
97
|
+
| 3 | Click / start working | Slides back from edge + bounce |
|
|
74
98
|
|
|
75
99
|
---
|
|
76
100
|
|
|
77
|
-
### 💥
|
|
101
|
+
### 💥 Random Events (3)
|
|
78
102
|
|
|
79
|
-
| # |
|
|
80
|
-
|
|
81
|
-
| 1 |
|
|
82
|
-
| 2 |
|
|
83
|
-
| 3 |
|
|
103
|
+
| # | Event | Trigger | Effect |
|
|
104
|
+
|---|-------|---------|--------|
|
|
105
|
+
| 1 | **Fall apart** | Jump landing 40% / bounce 50% | Lines scatter → reassemble after 1.5s |
|
|
106
|
+
| 2 | **Bomb drop** | 10% chance replacing walk (idle) | Fuse burns ✦/◌ + countdown ³·→²·→¹· → white flash explosion + `ᵇᵒᵒᵐ~` → reassemble |
|
|
107
|
+
| 3 | **Scatter & assemble** | Startup / switch to work page | Lines start from random positions, 15-frame linear interpolation to home |
|
|
84
108
|
|
|
85
109
|
---
|
|
86
110
|
|
|
87
|
-
### 🔄
|
|
111
|
+
### 🔄 Auto-Update
|
|
88
112
|
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
-
|
|
92
|
-
-
|
|
113
|
+
- Checks npm latest version on startup → semver compare → `npm pack` download → `tar` extract overwrite
|
|
114
|
+
- File lock prevents concurrency (30s expiry auto-cleanup)
|
|
115
|
+
- Syncs opencode plugin manifest version to prevent rollback on restart
|
|
116
|
+
- On successful update: mascot jumps to celebrate + alien-text version number `ᵘᵖ→⁰·⁵·¹`
|
|
93
117
|
|
|
94
118
|
---
|
|
95
119
|
|
|
96
|
-
### 🎵
|
|
120
|
+
### 🎵 State Sync
|
|
97
121
|
|
|
98
|
-
|
|
|
99
|
-
|
|
100
|
-
| session busy |
|
|
101
|
-
| session thinking |
|
|
102
|
-
| session happy |
|
|
103
|
-
| session idle
|
|
104
|
-
|
|
|
122
|
+
| Trigger | Effect |
|
|
123
|
+
|---------|--------|
|
|
124
|
+
| session busy | Alien text bubble + 8-color highlight flash |
|
|
125
|
+
| session thinking | Foot stomp + face shift + alien text |
|
|
126
|
+
| session happy | Head sway celebration 3s |
|
|
127
|
+
| session idle timeout | Auto-sleep + alien-text Zzz (`zᶻ...` → `zᶻᶻ...` → `zᶻᶻᶻ...`) |
|
|
128
|
+
| Drag while sleeping | Startles awake to idle + arm flail panic |
|
|
105
129
|
|
|
106
|
-
>
|
|
130
|
+
> All states default to yueer. Double-click to manually switch to baozi.
|
|
107
131
|
|
|
108
132
|
---
|
|
109
133
|
|
|
110
|
-
### 🚀
|
|
134
|
+
### 🚀 Startup Effects
|
|
111
135
|
|
|
112
|
-
-
|
|
113
|
-
-
|
|
114
|
-
-
|
|
136
|
+
- Version number shown 2s after startup as alien-text `ᵛ⁰·⁵·¹` (3s duration)
|
|
137
|
+
- Home page mascot appears at random horizontal position
|
|
138
|
+
- Work page scatter-assemble animation on first message / `opencode -c`
|
|
115
139
|
|
|
116
140
|
---
|
|
117
141
|
|
|
118
|
-
## 📦
|
|
142
|
+
## 📦 Installation
|
|
119
143
|
|
|
120
|
-
|
|
144
|
+
Add to `~/.config/opencode/tui.json`:
|
|
121
145
|
|
|
122
146
|
```json
|
|
123
147
|
{
|
|
@@ -125,51 +149,48 @@
|
|
|
125
149
|
}
|
|
126
150
|
```
|
|
127
151
|
|
|
128
|
-
|
|
152
|
+
Restart opencode. The plugin auto-updates to the latest version.
|
|
129
153
|
|
|
130
|
-
## 🛠️
|
|
154
|
+
## 🛠️ Tech Stack
|
|
131
155
|
|
|
132
156
|
- **TypeScript** ESM
|
|
133
|
-
- **@opentui/solid** — SolidJS
|
|
134
|
-
- **@opencode-ai/plugin** — OpenCode
|
|
135
|
-
-
|
|
136
|
-
- TypeScript
|
|
157
|
+
- **@opentui/solid** — SolidJS reactive TUI rendering
|
|
158
|
+
- **@opencode-ai/plugin** — OpenCode plugin API
|
|
159
|
+
- Zero runtime dependencies (peer dep only)
|
|
160
|
+
- TypeScript type-checked
|
|
137
161
|
|
|
138
|
-
## 📂
|
|
162
|
+
## 📂 Project Structure
|
|
139
163
|
|
|
140
164
|
```
|
|
141
165
|
opencode-mascot/
|
|
142
|
-
├── tui.tsx #
|
|
166
|
+
├── tui.tsx # Plugin entry, registers slots + startup
|
|
143
167
|
├── src/
|
|
144
168
|
│ ├── core/
|
|
145
|
-
│ │ ├── types.ts # MascotPack / MascotState / Effect
|
|
146
|
-
│ │ ├── ascii-renderer.tsx #
|
|
147
|
-
│ │ ├── mascot-loader.ts #
|
|
148
|
-
│ │ ├── celebration-bus.ts #
|
|
149
|
-
│ │ ├── updater.ts # npm
|
|
150
|
-
│ │ └── logger.ts #
|
|
169
|
+
│ │ ├── types.ts # MascotPack / MascotState / Effect types
|
|
170
|
+
│ │ ├── ascii-renderer.tsx # Core rendering engine (574 lines, 16+ animations)
|
|
171
|
+
│ │ ├── mascot-loader.ts # Built-in character loader
|
|
172
|
+
│ │ ├── celebration-bus.ts # Module-level event bus
|
|
173
|
+
│ │ ├── updater.ts # npm auto-update
|
|
174
|
+
│ │ └── logger.ts # File logger
|
|
151
175
|
│ ├── components/
|
|
152
|
-
│ │ ├── home-mascot.tsx #
|
|
153
|
-
│ │ └── sidebar-mascot.tsx #
|
|
176
|
+
│ │ ├── home-mascot.tsx # Home page mascot
|
|
177
|
+
│ │ └── sidebar-mascot.tsx # Work page mascot (peek-a-boo)
|
|
154
178
|
│ └── builtins/
|
|
155
|
-
│ ├── yueer/ #
|
|
156
|
-
│
|
|
157
|
-
│
|
|
158
|
-
│ └── baozi/ # 包子形象(frames + effects)
|
|
159
|
-
│ ├── frames.ts # 5 帧 ASCII art
|
|
160
|
-
│ └── index.ts # 专属动画(蒸汽/气泡/惊恐)
|
|
179
|
+
│ ├── yueer/ # yueer (frames + effects)
|
|
180
|
+
│ ├── baozi/ # baozi (frames + effects)
|
|
181
|
+
│ └── cat/ # cat (frames + effects)
|
|
161
182
|
```
|
|
162
183
|
|
|
163
|
-
## 🎨
|
|
184
|
+
## 🎨 Custom Characters
|
|
164
185
|
|
|
165
|
-
|
|
186
|
+
Define a `MascotPack`:
|
|
166
187
|
|
|
167
188
|
```typescript
|
|
168
189
|
import type { MascotPack } from "@mingxy/opencode-mascot/types";
|
|
169
190
|
|
|
170
191
|
const myMascot: MascotPack = {
|
|
171
192
|
name: "@mingxy/mascot-custom",
|
|
172
|
-
displayName: "
|
|
193
|
+
displayName: "Kitty",
|
|
173
194
|
version: "0.1.0",
|
|
174
195
|
author: "you",
|
|
175
196
|
description: "My custom mascot",
|
|
@@ -191,22 +212,22 @@ const myMascot: MascotPack = {
|
|
|
191
212
|
};
|
|
192
213
|
```
|
|
193
214
|
|
|
194
|
-
|
|
215
|
+
All built-in animations (blink/breath/walk/jump/sleep/drag/color-flash/bomb/fall-apart/reassemble) **work automatically**.
|
|
195
216
|
|
|
196
|
-
## 📊
|
|
217
|
+
## 📊 Capabilities
|
|
197
218
|
|
|
198
|
-
|
|
|
199
|
-
|
|
200
|
-
|
|
|
201
|
-
|
|
|
202
|
-
|
|
|
203
|
-
|
|
|
204
|
-
|
|
|
205
|
-
|
|
|
206
|
-
|
|
|
207
|
-
|
|
|
208
|
-
|
|
|
209
|
-
|
|
|
219
|
+
| Category | Count |
|
|
220
|
+
|----------|-------|
|
|
221
|
+
| Built-in characters | 3 |
|
|
222
|
+
| Expression frames | 5 / character |
|
|
223
|
+
| Auto animations | 22 |
|
|
224
|
+
| Interactions | 5 |
|
|
225
|
+
| Peek-a-boo behaviors | 3 |
|
|
226
|
+
| Random events | 3 |
|
|
227
|
+
| Alien text phrases | 24 (12/character) |
|
|
228
|
+
| Flash colors | 8 |
|
|
229
|
+
| Drag alien text | 6 |
|
|
230
|
+
| **Total** | **39+** |
|
|
210
231
|
|
|
211
232
|
## 📄 License
|
|
212
233
|
|
|
@@ -216,3 +237,4 @@ MIT © [mingxy](https://github.com/mengfanbo123)
|
|
|
216
237
|
|
|
217
238
|
- [GitHub](https://github.com/mengfanbo123/opencode-mascot)
|
|
218
239
|
- [npm](https://www.npmjs.com/package/@mingxy/opencode-mascot)
|
|
240
|
+
- [中文文档](./README_zh-CN.md)
|
package/package.json
CHANGED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// 0123456789
|
|
2
|
+
const defaultFrame = [
|
|
3
|
+
" /\\_/\\ ",
|
|
4
|
+
" ( o.o ) ",
|
|
5
|
+
" > ^ < ",
|
|
6
|
+
" /| |\\ ",
|
|
7
|
+
"(_| |_) ",
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
const blinkFrame = [
|
|
11
|
+
" /\\_/\\ ",
|
|
12
|
+
" ( -.- ) ",
|
|
13
|
+
" > ^ < ",
|
|
14
|
+
" /| |\\ ",
|
|
15
|
+
"(_| |_) ",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const happyFrame = [
|
|
19
|
+
" /\\_/\\ ",
|
|
20
|
+
" ( ^ω^ ) ",
|
|
21
|
+
" > ω < ",
|
|
22
|
+
" /| |\\ ",
|
|
23
|
+
"(_| |_) ",
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const thinkingFrame = [
|
|
27
|
+
" /\\_/\\ ",
|
|
28
|
+
" ( o_o ) ",
|
|
29
|
+
" > ? < ",
|
|
30
|
+
" /| |\\ ",
|
|
31
|
+
"(_| |_) ",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const sleepingFrame = [
|
|
35
|
+
" /\\_/\\ ",
|
|
36
|
+
" ( -.- ) ",
|
|
37
|
+
" > z < ",
|
|
38
|
+
" /| |\\ ",
|
|
39
|
+
"(_| |_) ",
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
export const frames = {
|
|
43
|
+
default: defaultFrame,
|
|
44
|
+
blink: blinkFrame,
|
|
45
|
+
happy: happyFrame,
|
|
46
|
+
thinking: thinkingFrame,
|
|
47
|
+
sleeping: sleepingFrame,
|
|
48
|
+
};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { MascotPack, EffectRenderCtx } from "../../core/types";
|
|
2
|
+
import { frames } from "./frames";
|
|
3
|
+
|
|
4
|
+
const PURR_TEXTS = [
|
|
5
|
+
"ᵖᵘʳʳʳ~", "ᵐʳʳᵒʷ~", "ᵐʷᵃᵃ~", "ⁿʸᵃᵃ~",
|
|
6
|
+
"ᵖᵘʳʳ..", "ᵐʸᵃ~..", "ᶠᵘʳʳ~", "ᵐⁱᵃᵒ~",
|
|
7
|
+
"ᶜʰᵘʳʳ..", "ᵉᵘⁿ~..", "ˢʰᵖᵘʳʳ~", "ⁿʸᵒ~",
|
|
8
|
+
];
|
|
9
|
+
const THINKING_FACES = ["o_o", "O_O", ">_o", "o_<", "⊙_⊙", "◑_◑"];
|
|
10
|
+
|
|
11
|
+
const catEffects: MascotPack["effects"] = {
|
|
12
|
+
signals: [
|
|
13
|
+
{ name: "tailAlt", initial: false },
|
|
14
|
+
{ name: "earTwitch", initial: false },
|
|
15
|
+
{ name: "pupilWide", initial: false },
|
|
16
|
+
{ name: "kneadAlt", initial: false },
|
|
17
|
+
{ name: "purrIdx", initial: 0 },
|
|
18
|
+
{ name: "thinkingFaceIdx", initial: 0 },
|
|
19
|
+
{ name: "thinkingCountdown", initial: 0 },
|
|
20
|
+
],
|
|
21
|
+
|
|
22
|
+
timers: [
|
|
23
|
+
{
|
|
24
|
+
interval: 600,
|
|
25
|
+
update(ctx) {
|
|
26
|
+
ctx.set("tailAlt", !(ctx.get("tailAlt") as boolean));
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
interval: 2500,
|
|
31
|
+
update(ctx) {
|
|
32
|
+
if (Math.random() < 0.2) {
|
|
33
|
+
ctx.set("earTwitch", true);
|
|
34
|
+
setTimeout(() => ctx.set("earTwitch", false), 250);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
interval: 4000,
|
|
40
|
+
update(ctx) {
|
|
41
|
+
if (ctx.state === "idle" && Math.random() < 0.15) {
|
|
42
|
+
ctx.set("pupilWide", true);
|
|
43
|
+
setTimeout(() => ctx.set("pupilWide", false), 1000);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
interval: 400,
|
|
49
|
+
update(ctx) {
|
|
50
|
+
if (ctx.state === "idle") {
|
|
51
|
+
ctx.set("kneadAlt", !(ctx.get("kneadAlt") as boolean));
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
interval: 2000,
|
|
57
|
+
update(ctx) {
|
|
58
|
+
if (ctx.state === "busy" || ctx.state === "thinking") {
|
|
59
|
+
ctx.set("purrIdx", ((ctx.get("purrIdx") as number) + 1) % PURR_TEXTS.length);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
interval: 1000,
|
|
65
|
+
update(ctx) {
|
|
66
|
+
if (ctx.state === "thinking" || ctx.state === "busy") {
|
|
67
|
+
let cd = ctx.get("thinkingCountdown") as number;
|
|
68
|
+
if (!cd || cd <= 0) cd = 5 + Math.floor(Math.random() * 11);
|
|
69
|
+
cd--;
|
|
70
|
+
if (cd <= 0) {
|
|
71
|
+
ctx.set("thinkingFaceIdx", ((ctx.get("thinkingFaceIdx") as number) + 1) % THINKING_FACES.length);
|
|
72
|
+
}
|
|
73
|
+
ctx.set("thinkingCountdown", cd);
|
|
74
|
+
} else {
|
|
75
|
+
ctx.set("thinkingFaceIdx", 0);
|
|
76
|
+
ctx.set("thinkingCountdown", 0);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
|
|
82
|
+
render(lines: string[], ctx: EffectRenderCtx): string[] {
|
|
83
|
+
const { state, breathPhase, dragging, get } = ctx;
|
|
84
|
+
let result = [...lines];
|
|
85
|
+
|
|
86
|
+
const tailAlt = get("tailAlt") as boolean;
|
|
87
|
+
const earTwitch = get("earTwitch") as boolean;
|
|
88
|
+
const pupilWide = get("pupilWide") as boolean;
|
|
89
|
+
const kneadAlt = get("kneadAlt") as boolean;
|
|
90
|
+
|
|
91
|
+
if (dragging) {
|
|
92
|
+
result[0] = " /╲╱╲\\ ";
|
|
93
|
+
result[1] = " ( >.< ) ";
|
|
94
|
+
result[2] = " > ! < ";
|
|
95
|
+
result[3] = " /| |\\ ";
|
|
96
|
+
result[4] = "(_| |_) ";
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!breathPhase) {
|
|
101
|
+
result = result.map((l) => l.replace(/\^/g, "'"));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (earTwitch) {
|
|
105
|
+
result[0] = result[0].replace("/\\_/\\", "/╲_/\\");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (tailAlt) {
|
|
109
|
+
result[4] = "(_| |~) ";
|
|
110
|
+
} else {
|
|
111
|
+
result[4] = "(_| |_) ";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (pupilWide) {
|
|
115
|
+
const faceLine = result.findIndex((l) => /\(.*\)/.test(l));
|
|
116
|
+
if (faceLine >= 0) {
|
|
117
|
+
result[faceLine] = result[faceLine].replace("o.o", "@.@");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (state === "idle") {
|
|
122
|
+
const armLine = result.findIndex((l) => l.includes("/| |\\"));
|
|
123
|
+
if (armLine >= 0) {
|
|
124
|
+
result[armLine] = kneadAlt ? " /|| |\\ " : " /| ||\\ ";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (state === "thinking" || state === "busy") {
|
|
129
|
+
const faceIdx = get("thinkingFaceIdx") as number;
|
|
130
|
+
const faceLine = result.findIndex((l) => /\(.*\)/.test(l));
|
|
131
|
+
if (faceLine >= 0) {
|
|
132
|
+
result[faceLine] = result[faceLine].replace(/\(.*?\)/, `( ${THINKING_FACES[faceIdx]} )`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (state === "busy" || state === "thinking") {
|
|
137
|
+
const idx = get("purrIdx") as number;
|
|
138
|
+
const earLine = 0;
|
|
139
|
+
result[earLine] = result[earLine].trimEnd() + " " + PURR_TEXTS[idx];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return result;
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const catPack: MascotPack = {
|
|
147
|
+
name: "@mingxy/mascot-cat",
|
|
148
|
+
displayName: "小猫",
|
|
149
|
+
version: "0.1.0",
|
|
150
|
+
author: "mingxy",
|
|
151
|
+
description: "Orange tabby cat — purring, kneading, and knocking things off your terminal.",
|
|
152
|
+
|
|
153
|
+
frames,
|
|
154
|
+
|
|
155
|
+
colors: {
|
|
156
|
+
defaultFg: "#FFA500",
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
animations: {
|
|
160
|
+
blinkInterval: 2500,
|
|
161
|
+
blinkChance: 0.3,
|
|
162
|
+
expressionInterval: 8000,
|
|
163
|
+
idleTimeout: 90000,
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
effects: catEffects,
|
|
167
|
+
};
|
|
@@ -117,6 +117,7 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
|
117
117
|
const returnToView = () => {
|
|
118
118
|
if (!hideSide) return;
|
|
119
119
|
stopPeek();
|
|
120
|
+
stopReturn();
|
|
120
121
|
const cw = getCw();
|
|
121
122
|
const cur = posX();
|
|
122
123
|
const targetX = hideSide === "left" ? 0 : Math.max(0, cw - MASCOT_WIDTH);
|
|
@@ -128,7 +129,10 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
|
128
129
|
setPosX(targetX);
|
|
129
130
|
stopReturn();
|
|
130
131
|
hideSide = null;
|
|
131
|
-
renderers[currentName()]
|
|
132
|
+
const r = renderers[currentName()];
|
|
133
|
+
if (r.getState() === "idle") {
|
|
134
|
+
r.bounce();
|
|
135
|
+
}
|
|
132
136
|
return;
|
|
133
137
|
}
|
|
134
138
|
setPosX(now + step);
|
|
@@ -46,6 +46,7 @@ function getFrameLines(pack: MascotPack, frameName: string): string[] {
|
|
|
46
46
|
|
|
47
47
|
export function createAnimatedRenderer(pack: MascotPack): {
|
|
48
48
|
element: () => JSX.Element;
|
|
49
|
+
getState: () => MascotState;
|
|
49
50
|
setState: (s: MascotState) => void;
|
|
50
51
|
toggleWalk: () => void;
|
|
51
52
|
setDragging: (v: boolean) => void;
|
|
@@ -573,5 +574,5 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
573
574
|
}, 700);
|
|
574
575
|
};
|
|
575
576
|
|
|
576
|
-
return { element, setState, toggleWalk, setDragging, celebrateUpdate, bounce, showVersion, scatterIn, explode };
|
|
577
|
+
return { element, getState: currentState, setState, toggleWalk, setDragging, celebrateUpdate, bounce, showVersion, scatterIn, explode };
|
|
577
578
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { MascotPack } from "./types"
|
|
2
2
|
import { yueerPack } from "../builtins/yueer"
|
|
3
3
|
import { baoziPack } from "../builtins/baozi"
|
|
4
|
+
import { catPack } from "../builtins/cat"
|
|
4
5
|
|
|
5
6
|
const BUILTINS: Record<string, MascotPack> = {
|
|
6
7
|
yueer: yueerPack,
|
|
7
8
|
baozi: baoziPack,
|
|
9
|
+
cat: catPack,
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
const ALL_NAMES = Object.keys(BUILTINS)
|