@umang-boss/claudemon 1.3.0 → 1.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/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 +60 -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 +80 -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 +17 -1
- package/dist/src/state/state-manager.js +22 -6
- package/package.json +1 -1
- package/skills/buddy/SKILL.md +15 -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 +23 -0
- package/src/gamification/achievements.ts +3 -3
- package/src/gamification/legendary-quests.ts +4 -4
- package/src/hooks/award-xp.ts +82 -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 +101 -0
- package/src/server/tools/show.ts +7 -0
- package/src/server/tools/train.ts +180 -0
- package/src/state/schemas.ts +19 -0
- package/src/state/state-manager.ts +25 -6
- package/statusline/buddy-status.sh +77 -62
package/src/hooks/award-xp.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Loads state, awards XP to the active Pokemon, applies stat boosts,
|
|
6
6
|
* increments the event counter, updates the streak, and saves.
|
|
7
|
+
* Supports enhanced encounter triggers: streak bonuses, time-of-day bias,
|
|
8
|
+
* bonus encounters (10%), and tool diversity encounters.
|
|
7
9
|
* Emits a terminal bell on level-up.
|
|
8
10
|
*/
|
|
9
11
|
|
|
@@ -14,7 +16,17 @@ import { addXp, createXpEvent } from "../engine/xp.js";
|
|
|
14
16
|
import { applyStatBoost } from "../engine/stats.js";
|
|
15
17
|
import { POKEMON_BY_ID } from "../engine/pokemon-data.js";
|
|
16
18
|
import { checkEvolution, getNewlyEarnedBadges } from "../engine/evolution.js";
|
|
17
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
shouldTriggerEncounter,
|
|
21
|
+
generateEncounter,
|
|
22
|
+
shouldBonusEncounter,
|
|
23
|
+
shouldDiversityBonus,
|
|
24
|
+
getTimeOfDayBias,
|
|
25
|
+
} from "../engine/encounters.js";
|
|
26
|
+
import type { EncounterContext } from "../engine/encounters.js";
|
|
27
|
+
import { calculateMood } from "../engine/mood.js";
|
|
28
|
+
|
|
29
|
+
const MAX_RECENT_TOOL_TYPES = 20;
|
|
18
30
|
|
|
19
31
|
const eventType = process.argv[2] as XpEventType;
|
|
20
32
|
const counterKey = process.argv[3] as EventCounterKey | undefined;
|
|
@@ -60,6 +72,14 @@ if (counterKey) {
|
|
|
60
72
|
state.counters[counterKey] += 1;
|
|
61
73
|
}
|
|
62
74
|
|
|
75
|
+
// Track tool type for diversity bonus (keep last MAX_RECENT_TOOL_TYPES entries)
|
|
76
|
+
const recentToolTypes = state.recentToolTypes ?? [];
|
|
77
|
+
recentToolTypes.push(eventType);
|
|
78
|
+
if (recentToolTypes.length > MAX_RECENT_TOOL_TYPES) {
|
|
79
|
+
recentToolTypes.splice(0, recentToolTypes.length - MAX_RECENT_TOOL_TYPES);
|
|
80
|
+
}
|
|
81
|
+
state.recentToolTypes = recentToolTypes;
|
|
82
|
+
|
|
63
83
|
// Update the daily coding streak
|
|
64
84
|
// Inline the streak logic to avoid the extra save from updateStreak()
|
|
65
85
|
const today = new Date();
|
|
@@ -100,19 +120,76 @@ for (const badge of newBadges) {
|
|
|
100
120
|
// Check if evolution is available (sets flag in status for status line indicator)
|
|
101
121
|
const evolutionReady = checkEvolution(pokemon, state) !== null;
|
|
102
122
|
|
|
103
|
-
//
|
|
104
|
-
|
|
123
|
+
// Build encounter context for the enhanced trigger system
|
|
124
|
+
const encounterSpeed = state.config.encounterSpeed ?? "normal";
|
|
125
|
+
const currentHour = new Date().getHours();
|
|
126
|
+
const timeOfDayTypes = getTimeOfDayBias(currentHour);
|
|
127
|
+
|
|
128
|
+
const encounterCtx: EncounterContext = {
|
|
129
|
+
xpSinceLastEncounter: (state.xpSinceLastEncounter ?? 0) + xpEvent.xp,
|
|
130
|
+
encounterSpeed,
|
|
131
|
+
currentStreak: streak.currentStreak,
|
|
132
|
+
recentToolTypes: state.recentToolTypes,
|
|
133
|
+
currentHour,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Track XP toward next encounter
|
|
137
|
+
state.xpSinceLastEncounter = encounterCtx.xpSinceLastEncounter;
|
|
105
138
|
let encounterTriggered = false;
|
|
106
139
|
|
|
107
|
-
if (shouldTriggerEncounter(
|
|
108
|
-
const encounter = generateEncounter(eventType, state);
|
|
140
|
+
if (shouldTriggerEncounter(encounterCtx) && !state.pendingEncounter) {
|
|
141
|
+
const encounter = generateEncounter(eventType, state, timeOfDayTypes);
|
|
109
142
|
if (encounter) {
|
|
110
143
|
state.pendingEncounter = encounter;
|
|
111
144
|
state.xpSinceLastEncounter = 0;
|
|
145
|
+
state.lastEncounterTime = Date.now();
|
|
112
146
|
encounterTriggered = true;
|
|
147
|
+
|
|
148
|
+
// 10% chance for a bonus encounter after a regular one
|
|
149
|
+
// (bonus replaces the pending encounter with a second roll)
|
|
150
|
+
if (shouldBonusEncounter()) {
|
|
151
|
+
const bonusEncounter = generateEncounter(eventType, state, timeOfDayTypes);
|
|
152
|
+
if (bonusEncounter) {
|
|
153
|
+
// The bonus encounter replaces the first; first is already set as pending
|
|
154
|
+
// In practice the player still sees one encounter per trigger,
|
|
155
|
+
// but the bonus gives them a fresh roll (potentially rarer Pokemon)
|
|
156
|
+
state.pendingEncounter = bonusEncounter;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
113
159
|
}
|
|
114
160
|
}
|
|
115
161
|
|
|
162
|
+
// Tool diversity bonus: if 3+ unique tool types used recently and no pending encounter,
|
|
163
|
+
// grant an extra encounter opportunity
|
|
164
|
+
if (!encounterTriggered && !state.pendingEncounter && shouldDiversityBonus(state.recentToolTypes)) {
|
|
165
|
+
const diversityEncounter = generateEncounter(eventType, state, timeOfDayTypes);
|
|
166
|
+
if (diversityEncounter) {
|
|
167
|
+
state.pendingEncounter = diversityEncounter;
|
|
168
|
+
state.xpSinceLastEncounter = 0;
|
|
169
|
+
state.lastEncounterTime = Date.now();
|
|
170
|
+
// Clear recent tool types after diversity bonus triggers
|
|
171
|
+
state.recentToolTypes = [];
|
|
172
|
+
encounterTriggered = true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Calculate mood based on the event that just happened
|
|
177
|
+
const newMood = calculateMood(
|
|
178
|
+
eventType,
|
|
179
|
+
state.counters,
|
|
180
|
+
currentHour,
|
|
181
|
+
state.mood ?? "neutral",
|
|
182
|
+
state.moodSetAt ?? 0,
|
|
183
|
+
evolutionReady, // evolution ready counts as a proud trigger
|
|
184
|
+
false, // achievements are checked in catch/evolve tools
|
|
185
|
+
false, // catches are handled in the catch tool
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
if (newMood !== (state.mood ?? "neutral")) {
|
|
189
|
+
state.mood = newMood;
|
|
190
|
+
state.moodSetAt = Date.now();
|
|
191
|
+
}
|
|
192
|
+
|
|
116
193
|
// Single atomic save for all mutations
|
|
117
194
|
await stateManager.save();
|
|
118
195
|
|
package/src/server/index.ts
CHANGED
|
@@ -19,6 +19,10 @@ import { registerAchievementsTool } from "./tools/achievements.js";
|
|
|
19
19
|
import { registerLegendaryTool } from "./tools/legendary.js";
|
|
20
20
|
import { registerHideTool, registerUnhideTool } from "./tools/visibility.js";
|
|
21
21
|
import { registerRenameTool } from "./tools/rename.js";
|
|
22
|
+
import { registerSettingsTool } from "./tools/settings.js";
|
|
23
|
+
import { registerFeedTool } from "./tools/feed.js";
|
|
24
|
+
import { registerTrainTool } from "./tools/train.js";
|
|
25
|
+
import { registerPlayTool } from "./tools/play.js";
|
|
22
26
|
import { buildInstructions } from "./instructions.js";
|
|
23
27
|
|
|
24
28
|
/** Safely register a tool, logging to stderr on failure instead of crashing. */
|
|
@@ -65,6 +69,10 @@ async function main(): Promise<void> {
|
|
|
65
69
|
safeRegister("buddy_hide", registerHideTool, server);
|
|
66
70
|
safeRegister("buddy_unhide", registerUnhideTool, server);
|
|
67
71
|
safeRegister("buddy_rename", registerRenameTool, server);
|
|
72
|
+
safeRegister("buddy_settings", registerSettingsTool, server);
|
|
73
|
+
safeRegister("buddy_feed", registerFeedTool, server);
|
|
74
|
+
safeRegister("buddy_train", registerTrainTool, server);
|
|
75
|
+
safeRegister("buddy_play", registerPlayTool, server);
|
|
68
76
|
|
|
69
77
|
// Connect via stdio transport
|
|
70
78
|
const transport = new StdioServerTransport();
|
|
@@ -9,6 +9,7 @@ import { POKEMON_BY_ID } from "../engine/pokemon-data.js";
|
|
|
9
9
|
import { cumulativeXpForLevel } from "../engine/xp.js";
|
|
10
10
|
import { getEvolutionLinks } from "../engine/evolution.js";
|
|
11
11
|
import { getTypePersonality } from "../engine/reactions.js";
|
|
12
|
+
import { getMoodDescription } from "../engine/mood.js";
|
|
12
13
|
|
|
13
14
|
// ── Public API ──────────────────────────────────────────────
|
|
14
15
|
|
|
@@ -60,11 +61,17 @@ function buildActiveInstructions(
|
|
|
60
61
|
const evolutionNote = buildEvolutionNote(active, species);
|
|
61
62
|
const encounterNote = buildEncounterNote(state);
|
|
62
63
|
|
|
64
|
+
const currentMood = state.mood ?? "neutral";
|
|
65
|
+
const moodDesc = getMoodDescription(currentMood);
|
|
66
|
+
const moodHint = buildMoodHint(currentMood);
|
|
67
|
+
|
|
63
68
|
const lines: string[] = [
|
|
64
69
|
"You have a Claudemon Pokemon companion.",
|
|
65
70
|
"",
|
|
66
71
|
`Active Pokemon: ${displayName}, Level ${active.level}, ${typeStr} type.`,
|
|
67
72
|
`Personality: ${personality}`,
|
|
73
|
+
`Current mood: ${moodDesc.toLowerCase()}${moodHint}`,
|
|
74
|
+
"Adjust your buddy references to match the mood.",
|
|
68
75
|
"",
|
|
69
76
|
`Occasionally (not every message), naturally reference ${displayName}:`,
|
|
70
77
|
`- When an error occurs: ${displayName} reacts (use ${primaryType} type personality)`,
|
|
@@ -95,6 +102,24 @@ function buildActiveInstructions(
|
|
|
95
102
|
|
|
96
103
|
// ── Helper Functions ────────────────────────────────────────
|
|
97
104
|
|
|
105
|
+
/** Build a short contextual hint for the current mood. */
|
|
106
|
+
function buildMoodHint(mood: string): string {
|
|
107
|
+
switch (mood) {
|
|
108
|
+
case "happy":
|
|
109
|
+
return " (tests passing, feeling good)";
|
|
110
|
+
case "worried":
|
|
111
|
+
return " (errors detected, feeling anxious)";
|
|
112
|
+
case "sleepy":
|
|
113
|
+
return " (late night coding, very drowsy)";
|
|
114
|
+
case "energetic":
|
|
115
|
+
return " (morning energy, fired up!)";
|
|
116
|
+
case "proud":
|
|
117
|
+
return " (just accomplished something big!)";
|
|
118
|
+
default:
|
|
119
|
+
return " (calm, waiting for action)";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
98
123
|
/** Find the active Pokemon in the player's party. */
|
|
99
124
|
function findActivePokemon(state: PlayerState): OwnedPokemon | null {
|
|
100
125
|
return state.party.find((p) => p.isActive) ?? null;
|
|
@@ -248,6 +248,10 @@ export function registerCatchTool(server: McpServer): void {
|
|
|
248
248
|
state.achievements.push(unlockAchievement(achievement.id));
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
// Set mood to proud after a successful catch
|
|
252
|
+
state.mood = "proud";
|
|
253
|
+
state.moodSetAt = Date.now();
|
|
254
|
+
|
|
251
255
|
// Save state
|
|
252
256
|
await stateManager.save();
|
|
253
257
|
await stateManager.writeStatus();
|
|
@@ -197,6 +197,10 @@ export function registerEvolveTool(server: McpServer): void {
|
|
|
197
197
|
// Confirm mode: apply evolution
|
|
198
198
|
const { newName, newTypes } = applyEvolution(active, eligibleLink.to);
|
|
199
199
|
|
|
200
|
+
// Set mood to proud after evolution
|
|
201
|
+
state.mood = "proud";
|
|
202
|
+
state.moodSetAt = Date.now();
|
|
203
|
+
|
|
200
204
|
// Save state
|
|
201
205
|
await stateManager.save();
|
|
202
206
|
await stateManager.writeStatus();
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* buddy_feed tool — Feed your active Pokemon to boost happiness.
|
|
3
|
+
* Has a 1-hour cooldown. Returns type-flavored food reactions.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import type { PokemonType, Pokemon } from "../../engine/types.js";
|
|
8
|
+
import { POKEMON_BY_ID } from "../../engine/pokemon-data.js";
|
|
9
|
+
import { MAX_HAPPINESS } from "../../engine/constants.js";
|
|
10
|
+
import { StateManager } from "../../state/state-manager.js";
|
|
11
|
+
|
|
12
|
+
/** Cooldown: 1 hour in milliseconds. */
|
|
13
|
+
const FEED_COOLDOWN_MS = 3_600_000;
|
|
14
|
+
|
|
15
|
+
/** Happiness increase per feed. */
|
|
16
|
+
const FEED_HAPPINESS_BOOST = 10;
|
|
17
|
+
|
|
18
|
+
/** Food reactions keyed by primary Pokemon type. */
|
|
19
|
+
const FEED_REACTIONS: Record<PokemonType, (name: string) => string> = {
|
|
20
|
+
Normal: (n) => `*${n} enjoys a PokePuff!* \u{1F9C1}`,
|
|
21
|
+
Fire: (n) => `*${n} gobbles down a Spicy Berry!* \u{1F336}\u{FE0F}`,
|
|
22
|
+
Water: (n) => `*${n} slurps an Oran Berry!* \u{1F4A7}`,
|
|
23
|
+
Electric: (n) => `*${n} munches a Cheri Berry!* \u{26A1}`,
|
|
24
|
+
Grass: (n) => `*${n} nibbles a fresh Leaf Salad!* \u{1F33F}`,
|
|
25
|
+
Ice: (n) => `*${n} crunches a Frozen Berry!* \u{2744}\u{FE0F}`,
|
|
26
|
+
Fighting: (n) => `*${n} devours a Protein Shake!* \u{1F4AA}`,
|
|
27
|
+
Poison: (n) => `*${n} sips a Pecha Smoothie!* \u{1F49C}`,
|
|
28
|
+
Ground: (n) => `*${n} chews a Rawst Root!* \u{1F33E}`,
|
|
29
|
+
Flying: (n) => `*${n} pecks at Skyberry Seeds!* \u{1F426}`,
|
|
30
|
+
Psychic: (n) => `*${n} absorbs a Mind Melon!* \u{1F52E}`,
|
|
31
|
+
Bug: (n) => `*${n} nibbles on Sweet Honey!* \u{1F36F}`,
|
|
32
|
+
Rock: (n) => `*${n} crunches a Mineral Cookie!* \u{1FAA8}`,
|
|
33
|
+
Ghost: (n) => `*${n} absorbs a Shadow Treat!* \u{1F47B}`,
|
|
34
|
+
Dragon: (n) => `*${n} feasts on a Dragon Scale Fruit!* \u{1F409}`,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Get a feed reaction based on the Pokemon's primary type. */
|
|
38
|
+
function getFeedReaction(species: Pokemon, displayName: string): string {
|
|
39
|
+
const primaryType = species.types[0];
|
|
40
|
+
return FEED_REACTIONS[primaryType](displayName);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Format remaining cooldown as "Xm Ys". */
|
|
44
|
+
function formatCooldownRemaining(remainingMs: number): string {
|
|
45
|
+
const totalSeconds = Math.ceil(remainingMs / 1000);
|
|
46
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
47
|
+
const seconds = totalSeconds % 60;
|
|
48
|
+
if (minutes > 0 && seconds > 0) {
|
|
49
|
+
return `${minutes}m ${seconds}s`;
|
|
50
|
+
}
|
|
51
|
+
if (minutes > 0) {
|
|
52
|
+
return `${minutes}m`;
|
|
53
|
+
}
|
|
54
|
+
return `${seconds}s`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Registers the buddy_feed tool on the MCP server. */
|
|
58
|
+
export function registerFeedTool(server: McpServer): void {
|
|
59
|
+
server.tool(
|
|
60
|
+
"buddy_feed",
|
|
61
|
+
"Feed your active Pokemon to boost happiness. 1-hour cooldown.",
|
|
62
|
+
{},
|
|
63
|
+
async () => {
|
|
64
|
+
const stateManager = StateManager.getInstance();
|
|
65
|
+
const state = await stateManager.load();
|
|
66
|
+
|
|
67
|
+
if (!state || state.party.length === 0) {
|
|
68
|
+
return {
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: "text" as const,
|
|
72
|
+
text: "You don't have a Pokemon yet! Use buddy_starter to pick your first companion.",
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const active = stateManager.getActivePokemon();
|
|
79
|
+
if (!active) {
|
|
80
|
+
return {
|
|
81
|
+
content: [
|
|
82
|
+
{
|
|
83
|
+
type: "text" as const,
|
|
84
|
+
text: "No active Pokemon found. Use buddy_party to set one active.",
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const species = POKEMON_BY_ID.get(active.pokemonId);
|
|
91
|
+
if (!species) {
|
|
92
|
+
return {
|
|
93
|
+
content: [
|
|
94
|
+
{
|
|
95
|
+
type: "text" as const,
|
|
96
|
+
text: `Could not find species data for Pokemon ID ${active.pokemonId}.`,
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
isError: true,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check cooldown
|
|
104
|
+
const now = Date.now();
|
|
105
|
+
const elapsed = now - state.lastFedAt;
|
|
106
|
+
if (elapsed < FEED_COOLDOWN_MS) {
|
|
107
|
+
const remaining = FEED_COOLDOWN_MS - elapsed;
|
|
108
|
+
return {
|
|
109
|
+
content: [
|
|
110
|
+
{
|
|
111
|
+
type: "text" as const,
|
|
112
|
+
text: `Already fed! Try again in ${formatCooldownRemaining(remaining)}.`,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const displayName = active.nickname ?? species.name;
|
|
119
|
+
|
|
120
|
+
// Increase happiness (cap at MAX_HAPPINESS)
|
|
121
|
+
const previousHappiness = active.happiness;
|
|
122
|
+
active.happiness = Math.min(MAX_HAPPINESS, active.happiness + FEED_HAPPINESS_BOOST);
|
|
123
|
+
|
|
124
|
+
// Set mood to happy
|
|
125
|
+
state.mood = "happy";
|
|
126
|
+
state.moodSetAt = now;
|
|
127
|
+
|
|
128
|
+
// Record feed timestamp
|
|
129
|
+
state.lastFedAt = now;
|
|
130
|
+
|
|
131
|
+
// Save state and update status line
|
|
132
|
+
await stateManager.save();
|
|
133
|
+
await stateManager.writeStatus();
|
|
134
|
+
|
|
135
|
+
// Build response
|
|
136
|
+
const reaction = getFeedReaction(species, displayName);
|
|
137
|
+
const lines: string[] = [reaction, ""];
|
|
138
|
+
lines.push(`Happiness: ${previousHappiness} \u2192 ${active.happiness}`);
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
content: [{ type: "text" as const, text: lines.join("\n") }],
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
}
|