@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
package/cli/doctor.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { access, stat, unlink, readdir, readFile } from "node:fs/promises";
|
|
9
|
-
import { constants as fsConstants } from "node:fs";
|
|
9
|
+
import { constants as fsConstants, existsSync } from "node:fs";
|
|
10
10
|
import { resolve, dirname } from "node:path";
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
import { spawnSync } from "node:child_process";
|
|
@@ -32,7 +32,20 @@ const LOCK_MAX_AGE_MS = 5000;
|
|
|
32
32
|
const EXPECTED_SPRITE_COUNT = 151;
|
|
33
33
|
const __filename = fileURLToPath(import.meta.url);
|
|
34
34
|
const __dirname = dirname(__filename);
|
|
35
|
-
|
|
35
|
+
// Sprite dir: check multiple candidate paths (works from source, dist, and npm global)
|
|
36
|
+
function findColorscriptDir(): string | null {
|
|
37
|
+
const candidates = [
|
|
38
|
+
resolve(dirname(__dirname), "sprites", "colorscripts", "small"),
|
|
39
|
+
resolve(dirname(__dirname), "..", "sprites", "colorscripts", "small"),
|
|
40
|
+
resolve(__dirname, "..", "sprites", "colorscripts", "small"),
|
|
41
|
+
resolve(__dirname, "..", "..", "sprites", "colorscripts", "small"),
|
|
42
|
+
];
|
|
43
|
+
for (const c of candidates) {
|
|
44
|
+
if (existsSync(c)) return c;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const COLORSCRIPT_DIR = findColorscriptDir();
|
|
36
49
|
|
|
37
50
|
// ── Helpers ──────────────────────────────────────────────────
|
|
38
51
|
|
|
@@ -236,6 +249,9 @@ async function checkStaleLock(): Promise<CheckResult> {
|
|
|
236
249
|
|
|
237
250
|
async function checkSpriteCount(): Promise<CheckResult> {
|
|
238
251
|
try {
|
|
252
|
+
if (!COLORSCRIPT_DIR) {
|
|
253
|
+
return { label: "Sprites", passed: false, detail: "colorscripts directory not found" };
|
|
254
|
+
}
|
|
239
255
|
await access(COLORSCRIPT_DIR, fsConstants.F_OK);
|
|
240
256
|
const entries = await readdir(COLORSCRIPT_DIR);
|
|
241
257
|
const spriteFiles = entries.filter((f) => f.endsWith(".txt"));
|
package/cli/index.ts
CHANGED
|
@@ -1,19 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
3
|
* Claudemon CLI entry point.
|
|
4
|
-
* Routes to install, uninstall, update,
|
|
5
|
-
*
|
|
6
|
-
* Usage:
|
|
7
|
-
* npx claudemon install
|
|
8
|
-
* npx claudemon uninstall
|
|
9
|
-
* npx claudemon update
|
|
10
|
-
* npx claudemon doctor
|
|
4
|
+
* Routes to install, uninstall, update, doctor, or --version.
|
|
11
5
|
*/
|
|
12
6
|
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { resolve, dirname } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
13
11
|
export {};
|
|
14
12
|
const command = process.argv[2];
|
|
15
13
|
|
|
16
14
|
switch (command) {
|
|
15
|
+
case "--version":
|
|
16
|
+
case "-v":
|
|
17
|
+
case "version": {
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const pkgPaths = [
|
|
20
|
+
resolve(__dirname, "..", "package.json"),
|
|
21
|
+
resolve(__dirname, "..", "..", "package.json"),
|
|
22
|
+
];
|
|
23
|
+
for (const p of pkgPaths) {
|
|
24
|
+
try {
|
|
25
|
+
const pkg = JSON.parse(readFileSync(p, "utf-8")) as { version: string };
|
|
26
|
+
console.log(`claudemon v${pkg.version}`);
|
|
27
|
+
break;
|
|
28
|
+
} catch {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
17
34
|
case "install":
|
|
18
35
|
await import("./install.js");
|
|
19
36
|
break;
|
|
@@ -28,13 +45,14 @@ switch (command) {
|
|
|
28
45
|
break;
|
|
29
46
|
default:
|
|
30
47
|
console.log(`
|
|
31
|
-
Claudemon — Pokemon
|
|
48
|
+
Claudemon — Pokemon coding companion for Claude Code
|
|
32
49
|
|
|
33
50
|
Usage:
|
|
34
51
|
claudemon install Set up Claudemon (MCP server, hooks, skill, status line)
|
|
35
52
|
claudemon uninstall Remove Claudemon from Claude Code
|
|
36
53
|
claudemon update Re-register everything (preserves save data)
|
|
37
54
|
claudemon doctor Run diagnostics
|
|
55
|
+
claudemon --version Show version
|
|
38
56
|
|
|
39
57
|
After install, start a new Claude Code session and type /buddy
|
|
40
58
|
`);
|
package/dist/cli/doctor.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Usage: bun run cli/doctor.ts
|
|
6
6
|
*/
|
|
7
7
|
import { access, stat, unlink, readdir, readFile } from "node:fs/promises";
|
|
8
|
-
import { constants as fsConstants } from "node:fs";
|
|
8
|
+
import { constants as fsConstants, existsSync } from "node:fs";
|
|
9
9
|
import { resolve, dirname } from "node:path";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
@@ -19,7 +19,21 @@ const LOCK_MAX_AGE_MS = 5000;
|
|
|
19
19
|
const EXPECTED_SPRITE_COUNT = 151;
|
|
20
20
|
const __filename = fileURLToPath(import.meta.url);
|
|
21
21
|
const __dirname = dirname(__filename);
|
|
22
|
-
|
|
22
|
+
// Sprite dir: check multiple candidate paths (works from source, dist, and npm global)
|
|
23
|
+
function findColorscriptDir() {
|
|
24
|
+
const candidates = [
|
|
25
|
+
resolve(dirname(__dirname), "sprites", "colorscripts", "small"),
|
|
26
|
+
resolve(dirname(__dirname), "..", "sprites", "colorscripts", "small"),
|
|
27
|
+
resolve(__dirname, "..", "sprites", "colorscripts", "small"),
|
|
28
|
+
resolve(__dirname, "..", "..", "sprites", "colorscripts", "small"),
|
|
29
|
+
];
|
|
30
|
+
for (const c of candidates) {
|
|
31
|
+
if (existsSync(c))
|
|
32
|
+
return c;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const COLORSCRIPT_DIR = findColorscriptDir();
|
|
23
37
|
function formatCheck(result) {
|
|
24
38
|
const icon = result.passed ? "\u2713" : "\u2717";
|
|
25
39
|
return `[${icon}] ${result.label}: ${result.detail}`;
|
|
@@ -188,6 +202,9 @@ async function checkStaleLock() {
|
|
|
188
202
|
// ── Check 10: Sprite Count ──────────────────────────────────
|
|
189
203
|
async function checkSpriteCount() {
|
|
190
204
|
try {
|
|
205
|
+
if (!COLORSCRIPT_DIR) {
|
|
206
|
+
return { label: "Sprites", passed: false, detail: "colorscripts directory not found" };
|
|
207
|
+
}
|
|
191
208
|
await access(COLORSCRIPT_DIR, fsConstants.F_OK);
|
|
192
209
|
const entries = await readdir(COLORSCRIPT_DIR);
|
|
193
210
|
const spriteFiles = entries.filter((f) => f.endsWith(".txt"));
|
package/dist/cli/index.js
CHANGED
|
@@ -1,16 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
3
|
* Claudemon CLI entry point.
|
|
4
|
-
* Routes to install, uninstall, update,
|
|
5
|
-
*
|
|
6
|
-
* Usage:
|
|
7
|
-
* npx claudemon install
|
|
8
|
-
* npx claudemon uninstall
|
|
9
|
-
* npx claudemon update
|
|
10
|
-
* npx claudemon doctor
|
|
4
|
+
* Routes to install, uninstall, update, doctor, or --version.
|
|
11
5
|
*/
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { resolve, dirname } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
12
9
|
const command = process.argv[2];
|
|
13
10
|
switch (command) {
|
|
11
|
+
case "--version":
|
|
12
|
+
case "-v":
|
|
13
|
+
case "version": {
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const pkgPaths = [
|
|
16
|
+
resolve(__dirname, "..", "package.json"),
|
|
17
|
+
resolve(__dirname, "..", "..", "package.json"),
|
|
18
|
+
];
|
|
19
|
+
for (const p of pkgPaths) {
|
|
20
|
+
try {
|
|
21
|
+
const pkg = JSON.parse(readFileSync(p, "utf-8"));
|
|
22
|
+
console.log(`claudemon v${pkg.version}`);
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
14
31
|
case "install":
|
|
15
32
|
await import("./install.js");
|
|
16
33
|
break;
|
|
@@ -25,16 +42,16 @@ switch (command) {
|
|
|
25
42
|
break;
|
|
26
43
|
default:
|
|
27
44
|
console.log(`
|
|
28
|
-
Claudemon — Pokemon
|
|
45
|
+
Claudemon — Pokemon coding companion for Claude Code
|
|
29
46
|
|
|
30
47
|
Usage:
|
|
31
48
|
claudemon install Set up Claudemon (MCP server, hooks, skill, status line)
|
|
32
49
|
claudemon uninstall Remove Claudemon from Claude Code
|
|
33
50
|
claudemon update Re-register everything (preserves save data)
|
|
34
51
|
claudemon doctor Run diagnostics
|
|
52
|
+
claudemon --version Show version
|
|
35
53
|
|
|
36
54
|
After install, start a new Claude Code session and type /buddy
|
|
37
55
|
`);
|
|
38
56
|
break;
|
|
39
57
|
}
|
|
40
|
-
export {};
|
|
@@ -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
|
],
|