8000 fix(character): persist xp across create and update by Mnehmos · Pull Request #81 · Mnehmos/mnehmos.rpg.mcp · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/server/consolidated/character-manage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ async function handleAddXp(args: z.infer<typeof AddXpSchema>): Promise<object> {
throw new Error(`Character ${args.characterId} not found`);
}

const currentXp = (char as any).xp || 0;
const currentXp = char.xp ?? 0;
const newXp = currentXp + args.amount;
const currentLevel = char.level;
const nextLevelXp = XP_TABLE[currentLevel + 1];
Expand Down
10 changes: 7 additions & 3 deletions src/storage/repos/character.repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class CharacterRepository {
const validChar = isNPC ? NPCSchema.parse(character) : CharacterSchema.parse(character);

const stmt = this.db.prepare(`
INSERT INTO characters (id, name, stats, hp, max_hp, ac, level, faction_id, behavior, character_type,
INSERT INTO characters (id, name, stats, hp, max_hp, ac, level, xp, faction_id, behavior, character_type,
character_class, race, spell_slots, pact_magic_slots, known_spells, prepared_spells,
cantrips_known, max_spell_level, concentrating_on, conditions,
currency,
Expand All @@ -22,7 +22,7 @@ export class CharacterRepository {
armor_proficiencies, weapon_proficiencies, tool_proficiencies, languages,
background, alignment, origin,
created_at, updated_at)
VALUES (@id, @name, @stats, @hp, @maxHp, @ac, @level, @factionId, @behavior, @characterType,
VALUES (@id, @name, @stats, @hp, @maxHp, @ac, @level, @xp, @factionId, @behavior, @characterType,
@characterClass, @race, @spellSlots, @pactMagicSlots, @knownSpells, @preparedSpells,
@cantripsKnown, @maxSpellLevel, @concentratingOn, @conditions,
@currency,
Expand All @@ -43,6 +43,7 @@ export class CharacterRepository {
maxHp: validChar.maxHp,
ac: validChar.ac,
level: validChar.level,
xp: validChar.xp ?? 0,
factionId: (validChar as NPC).factionId || null,
behavior: (validChar as NPC).behavior || null,
characterType: validChar.characterType || 'pc',
Expand Down Expand Up @@ -134,7 +135,7 @@ export class CharacterRepository {

const stmt = this.db.prepare(`
UPDATE characters
SET name = ?, stats = ?, hp = ?, max_hp = ?, ac = ?, level = ?,
SET name = ?, stats = ?, hp = ?, max_hp = ?, ac = ?, level = ?, xp = ?,
faction_id = ?, behavior = ?, character_type = ?,
character_class = ?, race = ?, spell_slots = ?, pact_magic_slots = ?,
known_spells = ?, prepared_spells = ?, cantrips_known = ?,
Expand All @@ -159,6 +160,7 @@ export class CharacterRepository {
validChar.maxHp,
validChar.ac,
validChar.level,
validChar.xp ?? 0,
(validChar as NPC).factionId || null,
(validChar as NPC).behavior || null,
validChar.characterType || 'pc',
Expand Down Expand Up @@ -223,6 +225,7 @@ export class CharacterRepository {
maxHp: row.max_hp,
ac: row.ac,
level: row.level,
xp: row.xp ?? 0,
characterType: (row.character_type as CharacterType) || 'pc',
// CRIT-002/006: Spellcasting fields
characterClass: row.character_class || 'fighter',
Expand Down Expand Up @@ -286,6 +289,7 @@ interface CharacterRow {
max_hp: number;
ac: number;
level: number;
xp: number | null;
faction_id: string | null;
behavior: string | null;
character_type: string | null;
Expand Down
50 changes: 50 additions & 0 deletions tests/storage/character.repo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,54 @@ describe('CharacterRepository', () => {
const retrieved = repo.findById('edge-max');
expect(retrieved?.name).toBe(maxName);
});

// The characters table has had an `xp` column since the XP-system migration,
// but the repository never read or wrote it: create/update omitted the column
// and rowToCharacter never mapped it, so CharacterSchema's `.default(0)`
// silently re-synthesized 0 on every read. update() still *returned* the
// merged in-memory object, so callers saw their own value echoed back and
// only noticed on the next get.
describe('xp persistence', () => {
const baseCharacter = (id: string, xp?: number): Character => ({
id,
name: 'Adventurer',
stats: { str: 10, dex: 10, con: 10, int: 10, wis: 10, cha: 10 },
hp: 20, maxHp: 20, ac: 15, level: 1,
...(xp === undefined ? {} : { xp }),
createdAt: FIXED_TIMESTAMP, updatedAt: FIXED_TIMESTAMP
} as Character);

it('persists xp supplied at create time', () => {
repo.create(baseCharacter('xp-create', 250));

expect(repo.findById('xp-create')?.xp).toBe(250);
});

it('defaults xp to 0 when omitted at create time', () => {
repo.create(baseCharacter('xp-default'));

expect(repo.findById('xp-default')?.xp).toBe(0);
});

it('persists xp through update instead of resetting it to 0', () => {
repo.create(baseCharacter('xp-update', 0));

const returned = repo.update('xp-update', { xp: 500 });

// The echoed response and the stored row must agree — the original
// bug returned 500 here while writing nothing.
expect(returned?.xp).toBe(500);
expect(repo.findById('xp-update')?.xp).toBe(500);
});

it('leaves xp untouched when an unrelated field is updated', () => {
repo.create(baseCharacter('xp-untouched', 1200));

repo.update('xp-untouched', { hp: 12 });

const retrieved = repo.findById('xp-untouched');
expect(retrieved?.hp).toBe(12);
expect(retrieved?.xp).toBe(1200);
});
});
});
Loading
0