diff --git a/src/__tests__/observer.test.ts b/src/__tests__/observer.test.ts index 98d28e9..62bdf50 100644 --- a/src/__tests__/observer.test.ts +++ b/src/__tests__/observer.test.ts @@ -27,6 +27,7 @@ const mockCompanion: Companion = { level: 1, xp: 0, mood: 'happy', + availablePoints: 0, hatchedAt: Date.now(), }; diff --git a/src/__tests__/reasoning/observer-integration.test.ts b/src/__tests__/reasoning/observer-integration.test.ts index 2c3352a..9577204 100644 --- a/src/__tests__/reasoning/observer-integration.test.ts +++ b/src/__tests__/reasoning/observer-integration.test.ts @@ -38,6 +38,7 @@ const mockCompanion: Companion = { level: 1, xp: 0, mood: 'happy', + availablePoints: 0, hatchedAt: Date.now(), }; diff --git a/src/__tests__/reasoning/scrub-wiring.test.ts b/src/__tests__/reasoning/scrub-wiring.test.ts index c718ef7..0feea71 100644 --- a/src/__tests__/reasoning/scrub-wiring.test.ts +++ b/src/__tests__/reasoning/scrub-wiring.test.ts @@ -21,6 +21,7 @@ const mock: Companion = { level: 1, xp: 0, mood: 'happy', + availablePoints: 0, hatchedAt: Date.now(), }; diff --git a/src/db/schema.ts b/src/db/schema.ts index 4c1fa62..e1466ed 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -22,6 +22,12 @@ export function initDb() { mood TEXT DEFAULT 'happy', personality_bio TEXT DEFAULT '', user_id TEXT, + stat_debugging INTEGER, + stat_patience INTEGER, + stat_chaos INTEGER, + stat_wisdom INTEGER, + stat_snark INTEGER, + stat_points_available INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); @@ -79,6 +85,14 @@ export function initDb() { db.exec(`ALTER TABLE companions ADD COLUMN cc_rescue INTEGER DEFAULT 0`); } catch { /* column already exists */ } + // Migration: add stat columns for growth + try { db.exec(`ALTER TABLE companions ADD COLUMN stat_debugging INTEGER`); } catch {} + try { db.exec(`ALTER TABLE companions ADD COLUMN stat_patience INTEGER`); } catch {} + try { db.exec(`ALTER TABLE companions ADD COLUMN stat_chaos INTEGER`); } catch {} + try { db.exec(`ALTER TABLE companions ADD COLUMN stat_wisdom INTEGER`); } catch {} + try { db.exec(`ALTER TABLE companions ADD COLUMN stat_snark INTEGER`); } catch {} + try { db.exec(`ALTER TABLE companions ADD COLUMN stat_points_available INTEGER DEFAULT 0`); } catch {} + // Reasoning-layer migration (claims/edges tables + guard_mode column). initReasoningSchema(db); } diff --git a/src/lib/companion.ts b/src/lib/companion.ts index 22396a3..b3835b1 100644 --- a/src/lib/companion.ts +++ b/src/lib/companion.ts @@ -39,6 +39,15 @@ export function loadCompanion(row: any, userIdOverride?: string): Companion | nu const xp = row.xp || 0; const derivedLevel = levelFromXp(xp); + // Stats priority: DB values > bones fallback + const stats = { + DEBUGGING: row.stat_debugging ?? bones.stats.DEBUGGING, + PATIENCE: row.stat_patience ?? bones.stats.PATIENCE, + CHAOS: row.stat_chaos ?? bones.stats.CHAOS, + WISDOM: row.stat_wisdom ?? bones.stats.WISDOM, + SNARK: row.stat_snark ?? bones.stats.SNARK, + }; + // Self-healing: if DB level drifted from XP-derived level, fix it if (row.id && row.level !== derivedLevel) { db.prepare('UPDATE companions SET level = ? WHERE id = ?').run(derivedLevel, row.id); @@ -46,12 +55,14 @@ export function loadCompanion(row: any, userIdOverride?: string): Companion | nu return { ...bones, + stats, species: row.species, name: row.name, personalityBio: row.personality_bio || '', level: derivedLevel, xp, mood: row.mood, + availablePoints: row.stat_points_available || 0, hatchedAt: new Date(row.created_at).getTime(), }; } @@ -76,6 +87,7 @@ export function writeBuddyStatus(companion: Companion, reaction?: { state: strin eye: companion.eye, hat: companion.hat, stats: companion.stats, + available_points: companion.availablePoints, rarity_stars: RARITY_STARS[companion.rarity], personality_bio: companion.personalityBio, ...(reaction ? { @@ -113,8 +125,14 @@ export function createCompanion(opts: { const bio = generateBio({ ...bones, species: finalSpecies }); db.prepare( - 'INSERT INTO companions (id, name, species, user_id, personality_bio) VALUES (?, ?, ?, ?, ?)' - ).run(id, finalName, finalSpecies, userId, bio); + `INSERT INTO companions ( + id, name, species, user_id, personality_bio, + stat_debugging, stat_patience, stat_chaos, stat_wisdom, stat_snark + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + ).run( + id, finalName, finalSpecies, userId, bio, + bones.stats.DEBUGGING, bones.stats.PATIENCE, bones.stats.CHAOS, bones.stats.WISDOM, bones.stats.SNARK + ); const companion: Companion = { ...bones, @@ -124,6 +142,7 @@ export function createCompanion(opts: { level: 1, xp: 0, mood: 'happy', + availablePoints: 0, hatchedAt: Date.now(), }; @@ -196,6 +215,7 @@ export function rescueCompanion(importResult: { level: 1, xp: 0, mood: 'happy', + availablePoints: 0, hatchedAt, }; diff --git a/src/lib/types.ts b/src/lib/types.ts index ec97015..399641a 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -32,6 +32,7 @@ export type Companion = CompanionBones & CompanionSoul & { level: number; xp: number; mood: string; + availablePoints: number; hatchedAt: number; }; diff --git a/src/server/index.ts b/src/server/index.ts index 611f470..f24c065 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -65,12 +65,19 @@ function awardXp(companionId: string, eventType: string): { newXp: number; newLe db.prepare("INSERT INTO xp_events (id, companion_id, event_type, xp_gained) VALUES (?, ?, ?, ?)").run(id, companionId, eventType, xp); // Get current total XP - const row = db.prepare("SELECT xp, level FROM companions WHERE id = ?").get(companionId) as any; + const row = db.prepare("SELECT xp, level, stat_points_available FROM companions WHERE id = ?").get(companionId) as any; const newXp = (row?.xp || 0) + xp; const newLevel = levelFromXp(newXp); const leveledUp = newLevel > (row?.level || 1); - db.prepare("UPDATE companions SET xp = ?, level = ? WHERE id = ?").run(newXp, newLevel, companionId); + // Award +2 stat points per level gained + if (leveledUp) { + const levelsGained = newLevel - (row?.level || 1); + const newPoints = (row?.stat_points_available || 0) + (levelsGained * 2); + db.prepare("UPDATE companions SET xp = ?, level = ?, stat_points_available = ? WHERE id = ?").run(newXp, newLevel, newPoints, companionId); + } else { + db.prepare("UPDATE companions SET xp = ?, level = ? WHERE id = ?").run(newXp, newLevel, companionId); + } return { newXp, newLevel, leveledUp }; }