@umang-boss/claudemon 1.3.0 → 1.4.1
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/cli/doctor.ts +18 -2
- package/cli/index.ts +26 -8
- package/dist/cli/doctor.js +19 -2
- package/dist/cli/index.js +26 -9
- package/dist/src/engine/constants.js +9 -3
- package/dist/src/engine/encounters.js +50 -10
- package/dist/src/engine/mood.js +187 -0
- package/dist/src/engine/types.js +2 -0
- package/dist/src/gamification/achievements.js +3 -3
- package/dist/src/gamification/legendary-quests.js +4 -4
- package/dist/src/hooks/award-xp.js +75 -5
- package/dist/src/server/index.js +8 -0
- package/dist/src/server/instructions.js +23 -0
- package/dist/src/server/tools/catch.js +3 -0
- package/dist/src/server/tools/evolve.js +3 -0
- package/dist/src/server/tools/feed.js +120 -0
- package/dist/src/server/tools/play.js +310 -0
- package/dist/src/server/tools/settings.js +116 -0
- package/dist/src/server/tools/show.js +5 -0
- package/dist/src/server/tools/train.js +144 -0
- package/dist/src/state/schemas.js +18 -1
- package/dist/src/state/state-manager.js +23 -6
- package/package.json +1 -1
- package/skills/buddy/SKILL.md +16 -0
- package/src/engine/constants.ts +12 -3
- package/src/engine/encounters.ts +65 -9
- package/src/engine/mood.ts +220 -0
- package/src/engine/types.ts +24 -0
- package/src/gamification/achievements.ts +3 -3
- package/src/gamification/legendary-quests.ts +4 -4
- package/src/hooks/award-xp.ts +97 -5
- package/src/server/index.ts +8 -0
- package/src/server/instructions.ts +25 -0
- package/src/server/tools/catch.ts +4 -0
- package/src/server/tools/evolve.ts +4 -0
- package/src/server/tools/feed.ts +145 -0
- package/src/server/tools/play.ts +378 -0
- package/src/server/tools/settings.ts +142 -0
- package/src/server/tools/show.ts +7 -0
- package/src/server/tools/train.ts +180 -0
- package/src/state/schemas.ts +20 -0
- package/src/state/state-manager.ts +26 -6
- package/statusline/buddy-status.sh +77 -62
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* buddy_train tool — Train your active Pokemon's coding stats.
|
|
3
|
+
* Has a 30-minute cooldown. Awards +3 to a stat and +5 XP.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import type { CodingStat } from "../../engine/types.js";
|
|
9
|
+
import { CODING_STATS } from "../../engine/types.js";
|
|
10
|
+
import { POKEMON_BY_ID } from "../../engine/pokemon-data.js";
|
|
11
|
+
import { STAT_DISPLAY_NAMES } from "../../engine/constants.js";
|
|
12
|
+
import { addXp } from "../../engine/xp.js";
|
|
13
|
+
import { applyStatBoost, renderStatBar } from "../../engine/stats.js";
|
|
14
|
+
import { StateManager } from "../../state/state-manager.js";
|
|
15
|
+
|
|
16
|
+
/** Cooldown: 30 minutes in milliseconds. */
|
|
17
|
+
const TRAIN_COOLDOWN_MS = 1_800_000;
|
|
18
|
+
|
|
19
|
+
/** Stat boost per training session. */
|
|
20
|
+
const TRAIN_STAT_BOOST = 3;
|
|
21
|
+
|
|
22
|
+
/** XP awarded per training session. */
|
|
23
|
+
const TRAIN_XP_AWARD = 5;
|
|
24
|
+
|
|
25
|
+
/** Pick a random element from a non-empty array. */
|
|
26
|
+
function randomPick<T>(arr: readonly T[]): T {
|
|
27
|
+
const index = Math.floor(Math.random() * arr.length);
|
|
28
|
+
return arr[index] as T;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Format remaining cooldown as "Xm Ys". */
|
|
32
|
+
function formatCooldownRemaining(remainingMs: number): string {
|
|
33
|
+
const totalSeconds = Math.ceil(remainingMs / 1000);
|
|
34
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
35
|
+
const seconds = totalSeconds % 60;
|
|
36
|
+
if (minutes > 0 && seconds > 0) {
|
|
37
|
+
return `${minutes}m ${seconds}s`;
|
|
38
|
+
}
|
|
39
|
+
if (minutes > 0) {
|
|
40
|
+
return `${minutes}m`;
|
|
41
|
+
}
|
|
42
|
+
return `${seconds}s`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Registers the buddy_train tool on the MCP server. */
|
|
46
|
+
export function registerTrainTool(server: McpServer): void {
|
|
47
|
+
server.tool(
|
|
48
|
+
"buddy_train",
|
|
49
|
+
"Train your active Pokemon's coding stats. Optionally specify a stat (debugging, stability, velocity, wisdom, stamina). 30-minute cooldown.",
|
|
50
|
+
{
|
|
51
|
+
stat: z
|
|
52
|
+
.string()
|
|
53
|
+
.optional()
|
|
54
|
+
.describe(
|
|
55
|
+
"Stat to train: debugging, stability, velocity, wisdom, or stamina. Random if omitted.",
|
|
56
|
+
),
|
|
57
|
+
},
|
|
58
|
+
async ({ stat: statInput }) => {
|
|
59
|
+
const stateManager = StateManager.getInstance();
|
|
60
|
+
const state = await stateManager.load();
|
|
61
|
+
|
|
62
|
+
if (!state || state.party.length === 0) {
|
|
63
|
+
return {
|
|
64
|
+
content: [
|
|
65
|
+
{
|
|
66
|
+
type: "text" as const,
|
|
67
|
+
text: "You don't have a Pokemon yet! Use buddy_starter to pick your first companion.",
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const active = stateManager.getActivePokemon();
|
|
74
|
+
if (!active) {
|
|
75
|
+
return {
|
|
76
|
+
content: [
|
|
77
|
+
{
|
|
78
|
+
type: "text" as const,
|
|
79
|
+
text: "No active Pokemon found. Use buddy_party to set one active.",
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const species = POKEMON_BY_ID.get(active.pokemonId);
|
|
86
|
+
if (!species) {
|
|
87
|
+
return {
|
|
88
|
+
content: [
|
|
89
|
+
{
|
|
90
|
+
type: "text" as const,
|
|
91
|
+
text: `Could not find species data for Pokemon ID ${active.pokemonId}.`,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
isError: true,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check cooldown
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
const elapsed = now - state.lastTrainedAt;
|
|
101
|
+
if (elapsed < TRAIN_COOLDOWN_MS) {
|
|
102
|
+
const remaining = TRAIN_COOLDOWN_MS - elapsed;
|
|
103
|
+
return {
|
|
104
|
+
content: [
|
|
105
|
+
{
|
|
106
|
+
type: "text" as const,
|
|
107
|
+
text: `Still resting from training! Try again in ${formatCooldownRemaining(remaining)}.`,
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Determine which stat to train
|
|
114
|
+
let targetStat: CodingStat;
|
|
115
|
+
if (statInput !== undefined && statInput !== "") {
|
|
116
|
+
const normalized = statInput.toLowerCase().trim();
|
|
117
|
+
if (!CODING_STATS.includes(normalized as CodingStat)) {
|
|
118
|
+
const validStats = CODING_STATS.map((s) => STAT_DISPLAY_NAMES[s].toLowerCase()).join(
|
|
119
|
+
", ",
|
|
120
|
+
);
|
|
121
|
+
return {
|
|
122
|
+
content: [
|
|
123
|
+
{
|
|
124
|
+
type: "text" as const,
|
|
125
|
+
text: `Invalid stat "${statInput}". Valid stats: ${validStats}`,
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
targetStat = normalized as CodingStat;
|
|
131
|
+
} else {
|
|
132
|
+
targetStat = randomPick(CODING_STATS);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const displayName = active.nickname ?? species.name;
|
|
136
|
+
|
|
137
|
+
// Record stat before boost for display
|
|
138
|
+
const statBefore = active.codingStats[targetStat];
|
|
139
|
+
|
|
140
|
+
// Apply stat boost
|
|
141
|
+
applyStatBoost(active, targetStat, TRAIN_STAT_BOOST);
|
|
142
|
+
|
|
143
|
+
const statAfter = active.codingStats[targetStat];
|
|
144
|
+
|
|
145
|
+
// Award XP
|
|
146
|
+
const levelUp = addXp(active, TRAIN_XP_AWARD, species);
|
|
147
|
+
|
|
148
|
+
// Set mood to energetic
|
|
149
|
+
state.mood = "energetic";
|
|
150
|
+
state.moodSetAt = now;
|
|
151
|
+
|
|
152
|
+
// Record train timestamp
|
|
153
|
+
state.lastTrainedAt = now;
|
|
154
|
+
|
|
155
|
+
// Save state and update status line
|
|
156
|
+
await stateManager.save();
|
|
157
|
+
await stateManager.writeStatus();
|
|
158
|
+
|
|
159
|
+
// Build response
|
|
160
|
+
const statDisplayName = STAT_DISPLAY_NAMES[targetStat];
|
|
161
|
+
const lines: string[] = [];
|
|
162
|
+
|
|
163
|
+
lines.push(`*${displayName} trains its ${statDisplayName}! +${TRAIN_STAT_BOOST}*`);
|
|
164
|
+
lines.push("");
|
|
165
|
+
lines.push(
|
|
166
|
+
`${statDisplayName}: ${statBefore} \u2192 ${statAfter} ${renderStatBar(statAfter)}`,
|
|
167
|
+
);
|
|
168
|
+
lines.push(`+${TRAIN_XP_AWARD} XP`);
|
|
169
|
+
|
|
170
|
+
if (levelUp) {
|
|
171
|
+
lines.push("");
|
|
172
|
+
lines.push(`*** ${displayName} grew to Lv.${levelUp.newLevel}! ***`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
content: [{ type: "text" as const, text: lines.join("\n") }],
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
);
|
|
180
|
+
}
|
package/src/state/schemas.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
BADGE_TYPES,
|
|
10
10
|
CODING_STATS,
|
|
11
11
|
EVENT_COUNTER_KEYS,
|
|
12
|
+
MOOD_TYPES,
|
|
12
13
|
} from "../engine/types.js";
|
|
13
14
|
|
|
14
15
|
// ---- Shared Primitives ----
|
|
@@ -69,6 +70,8 @@ export const BuddyConfigSchema = z.object({
|
|
|
69
70
|
reactionCooldownMs: z.number().int().min(0).default(30_000),
|
|
70
71
|
statusLineEnabled: z.boolean().default(true),
|
|
71
72
|
bellEnabled: z.boolean().default(true),
|
|
73
|
+
encounterSpeed: z.enum(["fast", "normal", "slow"]).default("normal"),
|
|
74
|
+
xpSharePercent: z.number().min(0).max(100).default(25),
|
|
72
75
|
});
|
|
73
76
|
|
|
74
77
|
// ---- Pokedex ----
|
|
@@ -93,6 +96,15 @@ export const UnlockedAchievementSchema = z.object({
|
|
|
93
96
|
unlockedAt: z.string(),
|
|
94
97
|
});
|
|
95
98
|
|
|
99
|
+
// ---- Pending Quiz ----
|
|
100
|
+
|
|
101
|
+
export const PendingQuizSchema = z.object({
|
|
102
|
+
type: z.enum(["type_matchup", "stat_compare", "evolution", "pokedex_trivia"]),
|
|
103
|
+
question: z.string(),
|
|
104
|
+
options: z.array(z.string()),
|
|
105
|
+
correctAnswer: z.number().int().min(1).max(4),
|
|
106
|
+
});
|
|
107
|
+
|
|
96
108
|
// ---- Catch Condition ----
|
|
97
109
|
|
|
98
110
|
export const CatchConditionSchema = z.object({
|
|
@@ -127,5 +139,13 @@ export const PlayerStateSchema = z.object({
|
|
|
127
139
|
totalSessions: z.number().int().min(0).default(0),
|
|
128
140
|
pendingEncounter: WildEncounterSchema.nullable().default(null),
|
|
129
141
|
xpSinceLastEncounter: z.number().int().min(0).default(0),
|
|
142
|
+
recentToolTypes: z.array(z.string()).default([]),
|
|
143
|
+
lastEncounterTime: z.number().int().min(0).default(0),
|
|
144
|
+
mood: z.enum(MOOD_TYPES).default("neutral"),
|
|
145
|
+
moodSetAt: z.number().default(0),
|
|
146
|
+
lastFedAt: z.number().default(0),
|
|
147
|
+
lastTrainedAt: z.number().default(0),
|
|
148
|
+
lastPlayedAt: z.number().default(0),
|
|
149
|
+
pendingQuiz: PendingQuizSchema.nullable().default(null),
|
|
130
150
|
});
|
|
131
151
|
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
BuddyConfig,
|
|
14
14
|
PokedexState,
|
|
15
15
|
WildEncounter,
|
|
16
|
+
MoodType,
|
|
16
17
|
} from "../engine/types.js";
|
|
17
18
|
import { EVENT_COUNTER_KEYS } from "../engine/types.js";
|
|
18
19
|
import { atomicWrite, safeRead, ensureDir, backupCorrupted, withLock } from "./io.js";
|
|
@@ -25,6 +26,7 @@ interface StatusPayload {
|
|
|
25
26
|
xpPercent: number;
|
|
26
27
|
speciesId: number;
|
|
27
28
|
evolutionReady: boolean;
|
|
29
|
+
mood: MoodType;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
/** Build a zeroed-out EventCounters record */
|
|
@@ -53,6 +55,8 @@ function defaultConfig(): BuddyConfig {
|
|
|
53
55
|
reactionCooldownMs: 30_000,
|
|
54
56
|
statusLineEnabled: true,
|
|
55
57
|
bellEnabled: true,
|
|
58
|
+
encounterSpeed: "normal",
|
|
59
|
+
xpSharePercent: 25,
|
|
56
60
|
};
|
|
57
61
|
}
|
|
58
62
|
|
|
@@ -188,6 +192,14 @@ export class StateManager {
|
|
|
188
192
|
totalSessions: 0,
|
|
189
193
|
pendingEncounter: null,
|
|
190
194
|
xpSinceLastEncounter: 0,
|
|
195
|
+
recentToolTypes: [],
|
|
196
|
+
lastEncounterTime: 0,
|
|
197
|
+
mood: "neutral",
|
|
198
|
+
moodSetAt: 0,
|
|
199
|
+
lastFedAt: 0,
|
|
200
|
+
lastTrainedAt: 0,
|
|
201
|
+
lastPlayedAt: 0,
|
|
202
|
+
pendingQuiz: null,
|
|
191
203
|
};
|
|
192
204
|
|
|
193
205
|
this.state = state;
|
|
@@ -210,19 +222,21 @@ export class StateManager {
|
|
|
210
222
|
await this.save();
|
|
211
223
|
}
|
|
212
224
|
|
|
213
|
-
/**
|
|
225
|
+
/**
|
|
226
|
+
* Update daily streak with weekend grace period.
|
|
227
|
+
* Allows up to 2 days off without breaking the streak (covers weekends).
|
|
228
|
+
* Streak counts "coding days" not "consecutive calendar days".
|
|
229
|
+
*/
|
|
214
230
|
async updateStreak(): Promise<void> {
|
|
215
231
|
const state = this.getState();
|
|
216
232
|
const today = todayDateString();
|
|
217
233
|
const { streak } = state;
|
|
218
234
|
|
|
219
235
|
if (streak.lastActiveDate === today) {
|
|
220
|
-
// Already recorded today, nothing to do
|
|
221
236
|
return;
|
|
222
237
|
}
|
|
223
238
|
|
|
224
239
|
if (streak.lastActiveDate === null) {
|
|
225
|
-
// First ever activity
|
|
226
240
|
streak.currentStreak = 1;
|
|
227
241
|
streak.totalDaysActive = 1;
|
|
228
242
|
} else {
|
|
@@ -231,11 +245,14 @@ export class StateManager {
|
|
|
231
245
|
const diffMs = now.getTime() - last.getTime();
|
|
232
246
|
const diffDays = Math.round(diffMs / (1000 * 60 * 60 * 24));
|
|
233
247
|
|
|
234
|
-
|
|
235
|
-
|
|
248
|
+
// Grace period: up to 2 days off (covers weekends)
|
|
249
|
+
// 1 day gap = next day (consecutive) ✓
|
|
250
|
+
// 2 day gap = skipped 1 day (e.g., Friday → Sunday) ✓
|
|
251
|
+
// 3 day gap = skipped 2 days (e.g., Friday → Monday) ✓
|
|
252
|
+
// 4+ day gap = streak broken
|
|
253
|
+
if (diffDays <= 3) {
|
|
236
254
|
streak.currentStreak += 1;
|
|
237
255
|
} else {
|
|
238
|
-
// Streak broken
|
|
239
256
|
streak.currentStreak = 1;
|
|
240
257
|
}
|
|
241
258
|
streak.totalDaysActive += 1;
|
|
@@ -256,6 +273,8 @@ export class StateManager {
|
|
|
256
273
|
return;
|
|
257
274
|
}
|
|
258
275
|
|
|
276
|
+
const state = this.getState();
|
|
277
|
+
|
|
259
278
|
// XP percent is currentXp as a rough percentage toward next level
|
|
260
279
|
// Exact formula depends on exp group; use a simple ratio for the status line
|
|
261
280
|
const xpPercent = active.level >= 100 ? 100 : Math.min(100, Math.floor((active.currentXp / Math.max(1, active.currentXp + 50)) * 100));
|
|
@@ -274,6 +293,7 @@ export class StateManager {
|
|
|
274
293
|
xpPercent,
|
|
275
294
|
speciesId: active.pokemonId,
|
|
276
295
|
evolutionReady,
|
|
296
|
+
mood: state.mood ?? "neutral",
|
|
277
297
|
};
|
|
278
298
|
|
|
279
299
|
const stateDir = getStateDir();
|
|
@@ -56,6 +56,7 @@ SPECIES_ID=$(echo "$STATUS" | jq -r '.speciesId // 0')
|
|
|
56
56
|
EVOLVING=$(echo "$STATUS" | jq -r '.evolutionReady // false')
|
|
57
57
|
REACTION=$(echo "$STATUS" | jq -r '.reaction // empty')
|
|
58
58
|
ENCOUNTER=$(echo "$STATUS" | jq -r '.encounter // empty')
|
|
59
|
+
MOOD=$(echo "$STATUS" | jq -r '.mood // "neutral"')
|
|
59
60
|
|
|
60
61
|
# ── Colors ──────────────────────────────────────────────────
|
|
61
62
|
NC=$'\033[0m'
|
|
@@ -158,69 +159,83 @@ if [ -n "$ENCOUNTER" ]; then
|
|
|
158
159
|
elif [ -n "$REACTION" ]; then
|
|
159
160
|
SPEECH="$REACTION"
|
|
160
161
|
else
|
|
162
|
+
# Mood-based speeches — pick from mood-specific arrays
|
|
161
163
|
NOW=$(date +%s)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
164
|
+
|
|
165
|
+
case "$MOOD" in
|
|
166
|
+
happy)
|
|
167
|
+
MOOD_SPEECHES=(
|
|
168
|
+
"*${NAME} is beaming with pride!*"
|
|
169
|
+
"*${NAME} does a little victory dance*"
|
|
170
|
+
"*${NAME} radiates positive energy*"
|
|
171
|
+
"*${NAME} bounces happily*"
|
|
172
|
+
"*${NAME} gives you a thumbs up*"
|
|
173
|
+
)
|
|
174
|
+
;;
|
|
175
|
+
worried)
|
|
176
|
+
MOOD_SPEECHES=(
|
|
177
|
+
"*${NAME} looks concerned...*"
|
|
178
|
+
"*${NAME} nervously watches the errors*"
|
|
179
|
+
"*${NAME} hides behind the terminal*"
|
|
180
|
+
"*${NAME} paces back and forth*"
|
|
181
|
+
"*${NAME} offers you a virtual hug*"
|
|
182
|
+
)
|
|
183
|
+
;;
|
|
184
|
+
sleepy)
|
|
185
|
+
MOOD_SPEECHES=(
|
|
186
|
+
"*${NAME} yawns widely*"
|
|
187
|
+
"*${NAME} dozes off... zzz*"
|
|
188
|
+
"*${NAME} rubs its eyes*"
|
|
189
|
+
"*${NAME} curls up near the keyboard*"
|
|
190
|
+
"*${NAME} mumbles in its sleep*"
|
|
191
|
+
)
|
|
192
|
+
;;
|
|
193
|
+
energetic)
|
|
194
|
+
MOOD_SPEECHES=(
|
|
195
|
+
"*${NAME} is fired up! Let's go!*"
|
|
196
|
+
"*${NAME} bounces off the walls*"
|
|
197
|
+
"*${NAME} can't sit still!*"
|
|
198
|
+
"*${NAME} is ready to code all day!*"
|
|
199
|
+
"*${NAME} stretches and flexes*"
|
|
200
|
+
)
|
|
201
|
+
;;
|
|
202
|
+
proud)
|
|
203
|
+
MOOD_SPEECHES=(
|
|
204
|
+
"*${NAME} puffs up with pride*"
|
|
205
|
+
"*${NAME} strikes a victory pose*"
|
|
206
|
+
"*${NAME} shows off to everyone*"
|
|
207
|
+
"*${NAME} earned bragging rights!*"
|
|
208
|
+
"*${NAME} stands tall and proud*"
|
|
209
|
+
)
|
|
210
|
+
;;
|
|
211
|
+
*)
|
|
212
|
+
# neutral / default — use the original idle speeches
|
|
213
|
+
MOOD_SPEECHES=(
|
|
214
|
+
"*${NAME} looks at your code curiously*"
|
|
215
|
+
"*${NAME} nods along as you type*"
|
|
216
|
+
"*${NAME} is watching closely*"
|
|
217
|
+
"*${NAME} hums softly*"
|
|
218
|
+
"*${NAME} waits patiently*"
|
|
219
|
+
"*${NAME} tilts head at the screen*"
|
|
220
|
+
"*${NAME} chirps encouragingly*"
|
|
221
|
+
"*${NAME} peers at a variable name*"
|
|
222
|
+
"*${NAME} sniffs at a function*"
|
|
223
|
+
"*${NAME} sits on the keyboard*"
|
|
224
|
+
"*${NAME} chases the cursor*"
|
|
225
|
+
"*${NAME} judges your indentation*"
|
|
226
|
+
"*${NAME} found a semicolon!*"
|
|
227
|
+
"*${NAME} debugs alongside you*"
|
|
228
|
+
"*${NAME} spots a typo... maybe*"
|
|
229
|
+
"*${NAME} celebrates a clean build*"
|
|
230
|
+
"*${NAME} dreams of evolution*"
|
|
231
|
+
"*${NAME} is ready for action!*"
|
|
232
|
+
)
|
|
233
|
+
;;
|
|
234
|
+
esac
|
|
235
|
+
|
|
236
|
+
MOOD_COUNT=${#MOOD_SPEECHES[@]}
|
|
237
|
+
IDX=$(( (NOW / 30) % MOOD_COUNT ))
|
|
238
|
+
SPEECH="${MOOD_SPEECHES[$IDX]}"
|
|
224
239
|
fi
|
|
225
240
|
if [ -n "$ENCOUNTER" ]; then
|
|
226
241
|
# Bright yellow for encounter alerts
|