@umang-boss/claudemon 1.2.1 → 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/README.md +13 -4
- 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/starter.js +24 -2
- 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 +2 -3
- 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/starter.ts +21 -2
- 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/README.md
CHANGED
|
@@ -24,22 +24,31 @@ fill your Pokedex -- all while you code.
|
|
|
24
24
|
|
|
25
25
|
## Install
|
|
26
26
|
|
|
27
|
+
**Recommended (global install — persistent):**
|
|
28
|
+
```bash
|
|
29
|
+
npm install -g @umang-boss/claudemon
|
|
30
|
+
claudemon install
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Or quick try (npx — may need reinstall after cache clear):**
|
|
27
34
|
```bash
|
|
28
35
|
npx @umang-boss/claudemon install
|
|
29
36
|
```
|
|
30
37
|
|
|
31
|
-
|
|
38
|
+
Start a new Claude Code session and type `/buddy` to begin!
|
|
32
39
|
|
|
33
40
|
**Requirements:** Node.js 18+ (Bun optional, auto-detected for faster startup)
|
|
34
41
|
|
|
35
42
|
### Other CLI Commands
|
|
36
43
|
|
|
37
44
|
```bash
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
claudemon doctor # Check installation health
|
|
46
|
+
claudemon update # Re-register after updates
|
|
47
|
+
claudemon uninstall # Remove (preserves save data)
|
|
41
48
|
```
|
|
42
49
|
|
|
50
|
+
> **Note:** `npm install -g` is recommended over `npx` because the MCP server path needs to persist. With `npx`, the path points to a temporary cache that may be cleaned up.
|
|
51
|
+
|
|
43
52
|
## Commands
|
|
44
53
|
|
|
45
54
|
Once installed, use `/buddy` in Claude Code:
|
|
@@ -33,8 +33,14 @@ export const STAT_DISPLAY_NAMES = {
|
|
|
33
33
|
wisdom: "WISDOM",
|
|
34
34
|
};
|
|
35
35
|
// ── Encounter Rate ─────────────────────────────────────────
|
|
36
|
-
/** XP
|
|
37
|
-
export const
|
|
36
|
+
/** XP thresholds for encounter triggers by speed setting */
|
|
37
|
+
export const ENCOUNTER_THRESHOLDS = {
|
|
38
|
+
fast: 100,
|
|
39
|
+
normal: 250,
|
|
40
|
+
slow: 500,
|
|
41
|
+
};
|
|
42
|
+
/** Default encounter speed */
|
|
43
|
+
export const DEFAULT_ENCOUNTER_SPEED = "normal";
|
|
38
44
|
// ── Reaction Cooldown ──────────────────────────────────────
|
|
39
45
|
export const DEFAULT_REACTION_COOLDOWN_MS = 30_000;
|
|
40
46
|
// ── Trainer Titles (by highest Pokemon level) ──────────────
|
|
@@ -73,7 +79,7 @@ export const BADGES = [
|
|
|
73
79
|
{
|
|
74
80
|
type: "lunar",
|
|
75
81
|
name: "Lunar Badge",
|
|
76
|
-
description: "
|
|
82
|
+
description: "Code for 30 days (weekends off OK) — unlocks Moon Stone evolutions",
|
|
77
83
|
condition: { type: "streak", minDays: 30 },
|
|
78
84
|
},
|
|
79
85
|
{
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Wild encounter system.
|
|
3
|
-
* Pokemon appear based on coding activity type
|
|
4
|
-
*
|
|
3
|
+
* Pokemon appear based on coding activity type, streak bonuses,
|
|
4
|
+
* tool diversity, and time-of-day biases. Catch eligibility is
|
|
5
|
+
* determined by the active Pokemon's stats and level.
|
|
5
6
|
*/
|
|
6
|
-
import {
|
|
7
|
+
import { ENCOUNTER_THRESHOLDS } from "./constants.js";
|
|
7
8
|
import { POKEMON_BY_ID } from "./pokemon-data.js";
|
|
8
9
|
import { TYPE_POOLS } from "./encounter-pool.js";
|
|
9
10
|
// ── Activity to Pokemon Type Mapping ──────────────────────────
|
|
@@ -28,11 +29,40 @@ export function getEncounterTypes(eventType) {
|
|
|
28
29
|
}
|
|
29
30
|
// ── Encounter Trigger ─────────────────────────────────────────
|
|
30
31
|
/**
|
|
31
|
-
* Check if a wild encounter should trigger based on XP earned
|
|
32
|
-
*
|
|
32
|
+
* Check if a wild encounter should trigger based on XP earned,
|
|
33
|
+
* encounter speed setting, and streak bonus.
|
|
34
|
+
* Streak bonus: 7+ day streak halves the threshold.
|
|
33
35
|
*/
|
|
34
|
-
export function shouldTriggerEncounter(
|
|
35
|
-
|
|
36
|
+
export function shouldTriggerEncounter(ctx) {
|
|
37
|
+
const threshold = ENCOUNTER_THRESHOLDS[ctx.encounterSpeed];
|
|
38
|
+
// Streak bonus: 7+ day streak = halve the threshold
|
|
39
|
+
const streakMultiplier = ctx.currentStreak >= 7 ? 0.5 : 1;
|
|
40
|
+
const effectiveThreshold = Math.floor(threshold * streakMultiplier);
|
|
41
|
+
if (ctx.xpSinceLastEncounter < effectiveThreshold)
|
|
42
|
+
return false;
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
/** Check for bonus encounter (10% chance after a regular encounter). */
|
|
46
|
+
export function shouldBonusEncounter() {
|
|
47
|
+
return Math.random() < 0.1;
|
|
48
|
+
}
|
|
49
|
+
/** Check for activity diversity bonus (3+ unique tool types in recent history). */
|
|
50
|
+
export function shouldDiversityBonus(recentToolTypes) {
|
|
51
|
+
const uniqueTypes = new Set(recentToolTypes);
|
|
52
|
+
return uniqueTypes.size >= 3;
|
|
53
|
+
}
|
|
54
|
+
// ── Time-of-Day Bias ──────────────────────────────────────────
|
|
55
|
+
/** Get time-of-day type biases for encounter generation. */
|
|
56
|
+
export function getTimeOfDayBias(hour) {
|
|
57
|
+
if (hour >= 22 || hour < 5)
|
|
58
|
+
return ["Ghost", "Poison"]; // Night: Ghost types
|
|
59
|
+
if (hour >= 5 && hour < 9)
|
|
60
|
+
return ["Grass", "Bug"]; // Morning: Grass types
|
|
61
|
+
if (hour >= 12 && hour < 14)
|
|
62
|
+
return ["Fire", "Rock"]; // Midday: Fire types
|
|
63
|
+
if (hour >= 17 && hour < 20)
|
|
64
|
+
return ["Water", "Flying"]; // Evening: Water types
|
|
65
|
+
return []; // No bias
|
|
36
66
|
}
|
|
37
67
|
// ── Rarity Weights ────────────────────────────────────────────
|
|
38
68
|
/** Relative weights for rarity-based selection. Higher = more likely to appear. */
|
|
@@ -186,11 +216,21 @@ function determineEncounterLevel(state, seed) {
|
|
|
186
216
|
/**
|
|
187
217
|
* Generate a wild encounter based on the activity type.
|
|
188
218
|
* Picks a Pokemon from the matching type pool, weighted by rarity.
|
|
189
|
-
*
|
|
219
|
+
* If time-of-day bias types are provided, there is a 40% chance to
|
|
220
|
+
* use those types instead of the activity-based types.
|
|
221
|
+
* Excludes Pokemon already in the player's party/box (unless common tier).
|
|
190
222
|
* Returns null if no eligible Pokemon found.
|
|
191
223
|
*/
|
|
192
|
-
export function generateEncounter(eventType, state) {
|
|
193
|
-
|
|
224
|
+
export function generateEncounter(eventType, state, timeOfDayTypes) {
|
|
225
|
+
let types = getEncounterTypes(eventType);
|
|
226
|
+
// 40% chance to use time-of-day biased types if available
|
|
227
|
+
if (timeOfDayTypes && timeOfDayTypes.length > 0) {
|
|
228
|
+
const seed = Math.floor(Date.now() / 1000);
|
|
229
|
+
const biasRoll = seededRandom(seed + 42);
|
|
230
|
+
if (biasRoll < 0.4) {
|
|
231
|
+
types = timeOfDayTypes;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
194
234
|
const candidates = buildCandidatePool(types, state);
|
|
195
235
|
if (candidates.length === 0)
|
|
196
236
|
return null;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mood engine for Claudemon.
|
|
3
|
+
* Pure functions that calculate mood based on recent events, time of day,
|
|
4
|
+
* and special triggers (evolution, achievements, catches).
|
|
5
|
+
*/
|
|
6
|
+
// ── Mood Decay Durations (milliseconds) ───────────────────
|
|
7
|
+
/** How long each mood lasts before decaying back to neutral */
|
|
8
|
+
const MOOD_DECAY_MS = {
|
|
9
|
+
happy: 600_000, // 10 minutes
|
|
10
|
+
worried: 300_000, // 5 minutes
|
|
11
|
+
sleepy: Infinity, // Resets based on time-of-day, not duration
|
|
12
|
+
energetic: 900_000, // 15 minutes
|
|
13
|
+
proud: 600_000, // 10 minutes
|
|
14
|
+
neutral: Infinity, // Never decays (it IS the default)
|
|
15
|
+
};
|
|
16
|
+
// ── XP Event Types That Trigger Moods ─────────────────────
|
|
17
|
+
const POSITIVE_EVENTS = new Set(["test_pass", "build_success", "commit"]);
|
|
18
|
+
const NEGATIVE_EVENTS = new Set(["test_fail", "build_fail", "error"]);
|
|
19
|
+
// ── Mood Calculation ──────────────────────────────────────
|
|
20
|
+
/**
|
|
21
|
+
* Calculate the current mood based on recent events, time, and special triggers.
|
|
22
|
+
*
|
|
23
|
+
* Priority order:
|
|
24
|
+
* 1. Sleepy (midnight to 5 AM)
|
|
25
|
+
* 2. Proud (just evolved/achieved/caught)
|
|
26
|
+
* 3. Worried (recent negative event)
|
|
27
|
+
* 4. Happy (recent positive event)
|
|
28
|
+
* 5. Energetic (morning + active streak)
|
|
29
|
+
* 6. Keep current mood if it hasn't decayed
|
|
30
|
+
* 7. Neutral (default fallback)
|
|
31
|
+
*
|
|
32
|
+
* @param recentEvent - The last XP event type, or null
|
|
33
|
+
* @param counters - Current event counters (for context)
|
|
34
|
+
* @param currentHour - Hour of day (0-23)
|
|
35
|
+
* @param lastMood - The previous mood
|
|
36
|
+
* @param moodSetAt - Timestamp when last mood was set
|
|
37
|
+
* @param hadEvolution - Whether the Pokemon just evolved
|
|
38
|
+
* @param hadAchievement - Whether the player just unlocked an achievement
|
|
39
|
+
* @param hadCatch - Whether the player just caught a Pokemon
|
|
40
|
+
* @returns The calculated mood
|
|
41
|
+
*/
|
|
42
|
+
export function calculateMood(recentEvent, counters, currentHour, lastMood, moodSetAt, hadEvolution, hadAchievement, hadCatch) {
|
|
43
|
+
// 1. Sleepy: midnight to 5 AM (hours 0-4)
|
|
44
|
+
if (currentHour >= 0 && currentHour < 5) {
|
|
45
|
+
return "sleepy";
|
|
46
|
+
}
|
|
47
|
+
// 2. Proud: just evolved, achieved, or caught a Pokemon
|
|
48
|
+
if (hadEvolution || hadAchievement || hadCatch) {
|
|
49
|
+
return "proud";
|
|
50
|
+
}
|
|
51
|
+
// 3. Worried: recent negative event
|
|
52
|
+
if (recentEvent !== null && NEGATIVE_EVENTS.has(recentEvent)) {
|
|
53
|
+
return "worried";
|
|
54
|
+
}
|
|
55
|
+
// 4. Happy: recent positive event
|
|
56
|
+
if (recentEvent !== null && POSITIVE_EVENTS.has(recentEvent)) {
|
|
57
|
+
return "happy";
|
|
58
|
+
}
|
|
59
|
+
// 5. Energetic: morning coding (5 AM - 10 AM) with an active streak
|
|
60
|
+
if (currentHour >= 5 && currentHour < 10) {
|
|
61
|
+
// Use a simple heuristic: if there have been any sessions, consider it an active streak
|
|
62
|
+
const hasStreak = counters.sessions > 0;
|
|
63
|
+
if (hasStreak) {
|
|
64
|
+
return "energetic";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// 6. Keep current mood if it hasn't decayed
|
|
68
|
+
if (!hasMoodDecayed(lastMood, moodSetAt, Date.now())) {
|
|
69
|
+
return lastMood;
|
|
70
|
+
}
|
|
71
|
+
// 7. Default fallback
|
|
72
|
+
return "neutral";
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Check whether a mood has expired based on its decay duration.
|
|
76
|
+
*
|
|
77
|
+
* @param mood - The mood to check
|
|
78
|
+
* @param setAt - Timestamp when the mood was set
|
|
79
|
+
* @param now - Current timestamp
|
|
80
|
+
* @returns true if the mood has decayed (expired)
|
|
81
|
+
*/
|
|
82
|
+
export function hasMoodDecayed(mood, setAt, now) {
|
|
83
|
+
const duration = MOOD_DECAY_MS[mood];
|
|
84
|
+
if (duration === Infinity) {
|
|
85
|
+
// Sleepy decays when it's no longer midnight-5 AM (handled in calculateMood)
|
|
86
|
+
// Neutral never decays
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
return now - setAt >= duration;
|
|
90
|
+
}
|
|
91
|
+
// ── Mood Speeches ─────────────────────────────────────────
|
|
92
|
+
/** Mood-specific speech lines for the status line. Name placeholder {name} is replaced at call time. */
|
|
93
|
+
const MOOD_SPEECHES = {
|
|
94
|
+
happy: [
|
|
95
|
+
"*{name} is beaming with pride!*",
|
|
96
|
+
"*{name} does a little victory dance*",
|
|
97
|
+
"*{name} radiates positive energy*",
|
|
98
|
+
"*{name} bounces happily*",
|
|
99
|
+
"*{name} gives you a thumbs up*",
|
|
100
|
+
],
|
|
101
|
+
worried: [
|
|
102
|
+
"*{name} looks concerned...*",
|
|
103
|
+
"*{name} nervously watches the errors*",
|
|
104
|
+
"*{name} hides behind the terminal*",
|
|
105
|
+
"*{name} paces back and forth*",
|
|
106
|
+
"*{name} offers you a virtual hug*",
|
|
107
|
+
],
|
|
108
|
+
sleepy: [
|
|
109
|
+
"*{name} yawns widely*",
|
|
110
|
+
"*{name} dozes off... zzz*",
|
|
111
|
+
"*{name} rubs its eyes*",
|
|
112
|
+
"*{name} curls up near the keyboard*",
|
|
113
|
+
"*{name} mumbles in its sleep*",
|
|
114
|
+
],
|
|
115
|
+
energetic: [
|
|
116
|
+
"*{name} is fired up! Let's go!*",
|
|
117
|
+
"*{name} bounces off the walls*",
|
|
118
|
+
"*{name} can't sit still!*",
|
|
119
|
+
"*{name} is ready to code all day!*",
|
|
120
|
+
"*{name} stretches and flexes*",
|
|
121
|
+
],
|
|
122
|
+
proud: [
|
|
123
|
+
"*{name} puffs up with pride*",
|
|
124
|
+
"*{name} strikes a victory pose*",
|
|
125
|
+
"*{name} shows off to everyone*",
|
|
126
|
+
"*{name} earned bragging rights!*",
|
|
127
|
+
"*{name} stands tall and proud*",
|
|
128
|
+
],
|
|
129
|
+
neutral: [
|
|
130
|
+
"*{name} looks at your code curiously*",
|
|
131
|
+
"*{name} nods along as you type*",
|
|
132
|
+
"*{name} is watching closely*",
|
|
133
|
+
"*{name} hums softly*",
|
|
134
|
+
"*{name} waits patiently*",
|
|
135
|
+
"*{name} tilts head at the screen*",
|
|
136
|
+
"*{name} chirps encouragingly*",
|
|
137
|
+
"*{name} peers at a variable name*",
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Get mood-specific speech messages with the Pokemon's name filled in.
|
|
142
|
+
*
|
|
143
|
+
* @param name - The Pokemon's display name
|
|
144
|
+
* @param mood - The current mood
|
|
145
|
+
* @returns Array of speech strings with the name interpolated
|
|
146
|
+
*/
|
|
147
|
+
export function getMoodSpeeches(name, mood) {
|
|
148
|
+
const templates = MOOD_SPEECHES[mood];
|
|
149
|
+
return templates.map((t) => t.replace("{name}", name));
|
|
150
|
+
}
|
|
151
|
+
// ── Mood Display Helpers ──────────────────────────────────
|
|
152
|
+
/** Emoji representation for each mood */
|
|
153
|
+
const MOOD_EMOJIS = {
|
|
154
|
+
happy: "\u{1F60A}", // 😊
|
|
155
|
+
worried: "\u{1F61F}", // 😟
|
|
156
|
+
sleepy: "\u{1F634}", // 😴
|
|
157
|
+
energetic: "\u{26A1}", // ⚡
|
|
158
|
+
proud: "\u{1F451}", // 👑
|
|
159
|
+
neutral: "\u{1F610}", // 😐
|
|
160
|
+
};
|
|
161
|
+
/** Human-readable mood descriptions */
|
|
162
|
+
const MOOD_DESCRIPTIONS = {
|
|
163
|
+
happy: "Happy",
|
|
164
|
+
worried: "Worried",
|
|
165
|
+
sleepy: "Sleepy",
|
|
166
|
+
energetic: "Energetic",
|
|
167
|
+
proud: "Proud",
|
|
168
|
+
neutral: "Neutral",
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* Get the emoji for a mood.
|
|
172
|
+
*
|
|
173
|
+
* @param mood - The mood type
|
|
174
|
+
* @returns The emoji string
|
|
175
|
+
*/
|
|
176
|
+
export function getMoodEmoji(mood) {
|
|
177
|
+
return MOOD_EMOJIS[mood];
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Get a human-readable description for a mood.
|
|
181
|
+
*
|
|
182
|
+
* @param mood - The mood type
|
|
183
|
+
* @returns The description string (e.g. "Happy")
|
|
184
|
+
*/
|
|
185
|
+
export function getMoodDescription(mood) {
|
|
186
|
+
return MOOD_DESCRIPTIONS[mood];
|
|
187
|
+
}
|
package/dist/src/engine/types.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Core type definitions for Claudemon.
|
|
3
3
|
* Single source of truth for all shared interfaces and types.
|
|
4
4
|
*/
|
|
5
|
+
// ── Mood Types ────────────────────────────────────────────
|
|
6
|
+
export const MOOD_TYPES = ["happy", "worried", "sleepy", "energetic", "proud", "neutral"];
|
|
5
7
|
// ── Pokemon Types ──────────────────────────────────────────
|
|
6
8
|
export const POKEMON_TYPES = [
|
|
7
9
|
"Normal",
|
|
@@ -93,21 +93,21 @@ export const ACHIEVEMENTS = [
|
|
|
93
93
|
{
|
|
94
94
|
id: "iron_coder",
|
|
95
95
|
name: "Iron Coder",
|
|
96
|
-
description: "
|
|
96
|
+
description: "Code for 7 days (weekends off OK)",
|
|
97
97
|
category: "coding",
|
|
98
98
|
condition: { type: "streak", minDays: 7 },
|
|
99
99
|
},
|
|
100
100
|
{
|
|
101
101
|
id: "marathon",
|
|
102
102
|
name: "Marathon",
|
|
103
|
-
description: "
|
|
103
|
+
description: "Code for 30 days (weekends off OK)",
|
|
104
104
|
category: "coding",
|
|
105
105
|
condition: { type: "streak", minDays: 30 },
|
|
106
106
|
},
|
|
107
107
|
{
|
|
108
108
|
id: "centurion",
|
|
109
109
|
name: "Centurion",
|
|
110
|
-
description: "
|
|
110
|
+
description: "Code for 100 days (weekends off OK)",
|
|
111
111
|
category: "coding",
|
|
112
112
|
condition: { type: "streak", minDays: 100 },
|
|
113
113
|
},
|
|
@@ -12,7 +12,7 @@ export const LEGENDARY_QUESTS = [
|
|
|
12
12
|
name: "The Ice Bird of Endurance",
|
|
13
13
|
steps: [
|
|
14
14
|
{
|
|
15
|
-
description: "
|
|
15
|
+
description: "Code for 30 days (weekends off OK)",
|
|
16
16
|
condition: { type: "streak", minDays: 30 },
|
|
17
17
|
},
|
|
18
18
|
{
|
|
@@ -21,7 +21,7 @@ export const LEGENDARY_QUESTS = [
|
|
|
21
21
|
condition: { type: "pokedex", minCaught: 3 },
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
|
-
description: "
|
|
24
|
+
description: "Code for 100 days (weekends off OK)",
|
|
25
25
|
condition: { type: "streak", minDays: 100 },
|
|
26
26
|
},
|
|
27
27
|
{
|
|
@@ -105,7 +105,7 @@ export const LEGENDARY_QUESTS = [
|
|
|
105
105
|
name: "The Myth",
|
|
106
106
|
steps: [
|
|
107
107
|
{
|
|
108
|
-
description: "
|
|
108
|
+
description: "Code for 100 days (weekends off OK)",
|
|
109
109
|
condition: { type: "streak", minDays: 100 },
|
|
110
110
|
},
|
|
111
111
|
{
|
|
@@ -121,7 +121,7 @@ export const LEGENDARY_QUESTS = [
|
|
|
121
121
|
condition: { type: "pokedex", minCaught: 149 },
|
|
122
122
|
},
|
|
123
123
|
{
|
|
124
|
-
description: "
|
|
124
|
+
description: "Code for 365 days (weekends off OK)",
|
|
125
125
|
condition: { type: "streak", minDays: 365 },
|
|
126
126
|
},
|
|
127
127
|
],
|
|
@@ -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
|
import { BELL } from "../engine/constants.js";
|
|
@@ -12,7 +14,9 @@ import { addXp, createXpEvent } from "../engine/xp.js";
|
|
|
12
14
|
import { applyStatBoost } from "../engine/stats.js";
|
|
13
15
|
import { POKEMON_BY_ID } from "../engine/pokemon-data.js";
|
|
14
16
|
import { checkEvolution, getNewlyEarnedBadges } from "../engine/evolution.js";
|
|
15
|
-
import { shouldTriggerEncounter, generateEncounter } from "../engine/encounters.js";
|
|
17
|
+
import { shouldTriggerEncounter, generateEncounter, shouldBonusEncounter, shouldDiversityBonus, getTimeOfDayBias, } from "../engine/encounters.js";
|
|
18
|
+
import { calculateMood } from "../engine/mood.js";
|
|
19
|
+
const MAX_RECENT_TOOL_TYPES = 20;
|
|
16
20
|
const eventType = process.argv[2];
|
|
17
21
|
const counterKey = process.argv[3];
|
|
18
22
|
if (!eventType) {
|
|
@@ -46,6 +50,13 @@ state.totalXpEarned += xpEvent.xp;
|
|
|
46
50
|
if (counterKey) {
|
|
47
51
|
state.counters[counterKey] += 1;
|
|
48
52
|
}
|
|
53
|
+
// Track tool type for diversity bonus (keep last MAX_RECENT_TOOL_TYPES entries)
|
|
54
|
+
const recentToolTypes = state.recentToolTypes ?? [];
|
|
55
|
+
recentToolTypes.push(eventType);
|
|
56
|
+
if (recentToolTypes.length > MAX_RECENT_TOOL_TYPES) {
|
|
57
|
+
recentToolTypes.splice(0, recentToolTypes.length - MAX_RECENT_TOOL_TYPES);
|
|
58
|
+
}
|
|
59
|
+
state.recentToolTypes = recentToolTypes;
|
|
49
60
|
// Update the daily coding streak
|
|
50
61
|
// Inline the streak logic to avoid the extra save from updateStreak()
|
|
51
62
|
const today = new Date();
|
|
@@ -81,17 +92,61 @@ for (const badge of newBadges) {
|
|
|
81
92
|
}
|
|
82
93
|
// Check if evolution is available (sets flag in status for status line indicator)
|
|
83
94
|
const evolutionReady = checkEvolution(pokemon, state) !== null;
|
|
84
|
-
//
|
|
85
|
-
|
|
95
|
+
// Build encounter context for the enhanced trigger system
|
|
96
|
+
const encounterSpeed = state.config.encounterSpeed ?? "normal";
|
|
97
|
+
const currentHour = new Date().getHours();
|
|
98
|
+
const timeOfDayTypes = getTimeOfDayBias(currentHour);
|
|
99
|
+
const encounterCtx = {
|
|
100
|
+
xpSinceLastEncounter: (state.xpSinceLastEncounter ?? 0) + xpEvent.xp,
|
|
101
|
+
encounterSpeed,
|
|
102
|
+
currentStreak: streak.currentStreak,
|
|
103
|
+
recentToolTypes: state.recentToolTypes,
|
|
104
|
+
currentHour,
|
|
105
|
+
};
|
|
106
|
+
// Track XP toward next encounter
|
|
107
|
+
state.xpSinceLastEncounter = encounterCtx.xpSinceLastEncounter;
|
|
86
108
|
let encounterTriggered = false;
|
|
87
|
-
if (shouldTriggerEncounter(
|
|
88
|
-
const encounter = generateEncounter(eventType, state);
|
|
109
|
+
if (shouldTriggerEncounter(encounterCtx) && !state.pendingEncounter) {
|
|
110
|
+
const encounter = generateEncounter(eventType, state, timeOfDayTypes);
|
|
89
111
|
if (encounter) {
|
|
90
112
|
state.pendingEncounter = encounter;
|
|
91
113
|
state.xpSinceLastEncounter = 0;
|
|
114
|
+
state.lastEncounterTime = Date.now();
|
|
92
115
|
encounterTriggered = true;
|
|
116
|
+
// 10% chance for a bonus encounter after a regular one
|
|
117
|
+
// (bonus replaces the pending encounter with a second roll)
|
|
118
|
+
if (shouldBonusEncounter()) {
|
|
119
|
+
const bonusEncounter = generateEncounter(eventType, state, timeOfDayTypes);
|
|
120
|
+
if (bonusEncounter) {
|
|
121
|
+
// The bonus encounter replaces the first; first is already set as pending
|
|
122
|
+
// In practice the player still sees one encounter per trigger,
|
|
123
|
+
// but the bonus gives them a fresh roll (potentially rarer Pokemon)
|
|
124
|
+
state.pendingEncounter = bonusEncounter;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
93
127
|
}
|
|
94
128
|
}
|
|
129
|
+
// Tool diversity bonus: if 3+ unique tool types used recently and no pending encounter,
|
|
130
|
+
// grant an extra encounter opportunity
|
|
131
|
+
if (!encounterTriggered && !state.pendingEncounter && shouldDiversityBonus(state.recentToolTypes)) {
|
|
132
|
+
const diversityEncounter = generateEncounter(eventType, state, timeOfDayTypes);
|
|
133
|
+
if (diversityEncounter) {
|
|
134
|
+
state.pendingEncounter = diversityEncounter;
|
|
135
|
+
state.xpSinceLastEncounter = 0;
|
|
136
|
+
state.lastEncounterTime = Date.now();
|
|
137
|
+
// Clear recent tool types after diversity bonus triggers
|
|
138
|
+
state.recentToolTypes = [];
|
|
139
|
+
encounterTriggered = true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Calculate mood based on the event that just happened
|
|
143
|
+
const newMood = calculateMood(eventType, state.counters, currentHour, state.mood ?? "neutral", state.moodSetAt ?? 0, evolutionReady, // evolution ready counts as a proud trigger
|
|
144
|
+
false, // achievements are checked in catch/evolve tools
|
|
145
|
+
false);
|
|
146
|
+
if (newMood !== (state.mood ?? "neutral")) {
|
|
147
|
+
state.mood = newMood;
|
|
148
|
+
state.moodSetAt = Date.now();
|
|
149
|
+
}
|
|
95
150
|
// Single atomic save for all mutations
|
|
96
151
|
await stateManager.save();
|
|
97
152
|
// Write status for the status line (includes evolution flag and encounter notification)
|
package/dist/src/server/index.js
CHANGED
|
@@ -17,6 +17,10 @@ import { registerAchievementsTool } from "./tools/achievements.js";
|
|
|
17
17
|
import { registerLegendaryTool } from "./tools/legendary.js";
|
|
18
18
|
import { registerHideTool, registerUnhideTool } from "./tools/visibility.js";
|
|
19
19
|
import { registerRenameTool } from "./tools/rename.js";
|
|
20
|
+
import { registerSettingsTool } from "./tools/settings.js";
|
|
21
|
+
import { registerFeedTool } from "./tools/feed.js";
|
|
22
|
+
import { registerTrainTool } from "./tools/train.js";
|
|
23
|
+
import { registerPlayTool } from "./tools/play.js";
|
|
20
24
|
import { buildInstructions } from "./instructions.js";
|
|
21
25
|
/** Safely register a tool, logging to stderr on failure instead of crashing. */
|
|
22
26
|
function safeRegister(name, register, server) {
|
|
@@ -56,6 +60,10 @@ async function main() {
|
|
|
56
60
|
safeRegister("buddy_hide", registerHideTool, server);
|
|
57
61
|
safeRegister("buddy_unhide", registerUnhideTool, server);
|
|
58
62
|
safeRegister("buddy_rename", registerRenameTool, server);
|
|
63
|
+
safeRegister("buddy_settings", registerSettingsTool, server);
|
|
64
|
+
safeRegister("buddy_feed", registerFeedTool, server);
|
|
65
|
+
safeRegister("buddy_train", registerTrainTool, server);
|
|
66
|
+
safeRegister("buddy_play", registerPlayTool, server);
|
|
59
67
|
// Connect via stdio transport
|
|
60
68
|
const transport = new StdioServerTransport();
|
|
61
69
|
await server.connect(transport);
|
|
@@ -7,6 +7,7 @@ import { POKEMON_BY_ID } from "../engine/pokemon-data.js";
|
|
|
7
7
|
import { cumulativeXpForLevel } from "../engine/xp.js";
|
|
8
8
|
import { getEvolutionLinks } from "../engine/evolution.js";
|
|
9
9
|
import { getTypePersonality } from "../engine/reactions.js";
|
|
10
|
+
import { getMoodDescription } from "../engine/mood.js";
|
|
10
11
|
// ── Public API ──────────────────────────────────────────────
|
|
11
12
|
/**
|
|
12
13
|
* Build dynamic instructions based on current player state.
|
|
@@ -45,11 +46,16 @@ function buildActiveInstructions(state, active, species) {
|
|
|
45
46
|
const personality = getTypePersonality(primaryType);
|
|
46
47
|
const evolutionNote = buildEvolutionNote(active, species);
|
|
47
48
|
const encounterNote = buildEncounterNote(state);
|
|
49
|
+
const currentMood = state.mood ?? "neutral";
|
|
50
|
+
const moodDesc = getMoodDescription(currentMood);
|
|
51
|
+
const moodHint = buildMoodHint(currentMood);
|
|
48
52
|
const lines = [
|
|
49
53
|
"You have a Claudemon Pokemon companion.",
|
|
50
54
|
"",
|
|
51
55
|
`Active Pokemon: ${displayName}, Level ${active.level}, ${typeStr} type.`,
|
|
52
56
|
`Personality: ${personality}`,
|
|
57
|
+
`Current mood: ${moodDesc.toLowerCase()}${moodHint}`,
|
|
58
|
+
"Adjust your buddy references to match the mood.",
|
|
53
59
|
"",
|
|
54
60
|
`Occasionally (not every message), naturally reference ${displayName}:`,
|
|
55
61
|
`- When an error occurs: ${displayName} reacts (use ${primaryType} type personality)`,
|
|
@@ -75,6 +81,23 @@ function buildActiveInstructions(state, active, species) {
|
|
|
75
81
|
return lines.join("\n");
|
|
76
82
|
}
|
|
77
83
|
// ── Helper Functions ────────────────────────────────────────
|
|
84
|
+
/** Build a short contextual hint for the current mood. */
|
|
85
|
+
function buildMoodHint(mood) {
|
|
86
|
+
switch (mood) {
|
|
87
|
+
case "happy":
|
|
88
|
+
return " (tests passing, feeling good)";
|
|
89
|
+
case "worried":
|
|
90
|
+
return " (errors detected, feeling anxious)";
|
|
91
|
+
case "sleepy":
|
|
92
|
+
return " (late night coding, very drowsy)";
|
|
93
|
+
case "energetic":
|
|
94
|
+
return " (morning energy, fired up!)";
|
|
95
|
+
case "proud":
|
|
96
|
+
return " (just accomplished something big!)";
|
|
97
|
+
default:
|
|
98
|
+
return " (calm, waiting for action)";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
78
101
|
/** Find the active Pokemon in the player's party. */
|
|
79
102
|
function findActivePokemon(state) {
|
|
80
103
|
return state.party.find((p) => p.isActive) ?? null;
|
|
@@ -210,6 +210,9 @@ export function registerCatchTool(server) {
|
|
|
210
210
|
for (const achievement of newAchievements) {
|
|
211
211
|
state.achievements.push(unlockAchievement(achievement.id));
|
|
212
212
|
}
|
|
213
|
+
// Set mood to proud after a successful catch
|
|
214
|
+
state.mood = "proud";
|
|
215
|
+
state.moodSetAt = Date.now();
|
|
213
216
|
// Save state
|
|
214
217
|
await stateManager.save();
|
|
215
218
|
await stateManager.writeStatus();
|
|
@@ -157,6 +157,9 @@ export function registerEvolveTool(server) {
|
|
|
157
157
|
}
|
|
158
158
|
// Confirm mode: apply evolution
|
|
159
159
|
const { newName, newTypes } = applyEvolution(active, eligibleLink.to);
|
|
160
|
+
// Set mood to proud after evolution
|
|
161
|
+
state.mood = "proud";
|
|
162
|
+
state.moodSetAt = Date.now();
|
|
160
163
|
// Save state
|
|
161
164
|
await stateManager.save();
|
|
162
165
|
await stateManager.writeStatus();
|