import assert from 'node:assert/strict'; import test from 'node:test'; import { createTestPlayerCharacter } from '../../testFixtures/runtimeCharacter.js'; import { craftForgeRecipe, equipInventoryItem, useInventoryItem, type RuntimeGameState, type RuntimeInventoryItem, } from './inventoryMutationService.js'; const TEST_WORLD = 'WUXIA' as RuntimeGameState['worldType']; const TEST_IDLE_ANIMATION = 'idle' as RuntimeGameState['animationState']; function requireCharacter() { return createTestPlayerCharacter< NonNullable >(); } function buildItem( overrides: Partial & Pick, ): RuntimeInventoryItem { return { quantity: 1, rarity: 'common', tags: [], ...overrides, }; } function createState(overrides: Partial = {}): RuntimeGameState { return { worldType: TEST_WORLD, customWorldProfile: null, playerCharacter: requireCharacter(), runtimeStats: { playTimeMs: 0, lastPlayTickAt: null, hostileNpcsDefeated: 0, questsAccepted: 0, itemsUsed: 0, scenesTraveled: 0, }, currentScene: 'test-scene', storyHistory: [], characterChats: {}, animationState: TEST_IDLE_ANIMATION, currentEncounter: null, npcInteractionActive: false, currentScenePreset: null, sceneHostileNpcs: [], playerX: 0, playerOffsetY: 0, playerFacing: 'right', playerActionMode: 'melee', scrollWorld: false, inBattle: false, playerHp: 64, playerMaxHp: 100, playerMana: 18, playerMaxMana: 60, playerSkillCooldowns: { slash: 2, }, activeBuildBuffs: [], activeCombatEffects: [], playerCurrency: 120, playerInventory: [], playerEquipment: { weapon: null, armor: null, relic: null, }, npcStates: {}, quests: [], roster: [], companions: [], currentBattleNpcId: null, currentNpcBattleMode: null, currentNpcBattleOutcome: null, sparReturnEncounter: null, sparPlayerHpBefore: null, sparPlayerMaxHpBefore: null, sparStoryHistoryBefore: null, ...overrides, } satisfies RuntimeGameState; } test('useInventoryItem applies recovery, cooldown推进 and buff mutation', () => { const state = createState({ playerInventory: [ buildItem({ id: 'focus-tonic', category: '消耗品', name: '凝神灵液', rarity: 'rare', tags: ['healing', 'mana'], useProfile: { hpRestore: 22, manaRestore: 16, cooldownReduction: 1, buildBuffs: [ { id: 'focus-tonic:buff', sourceType: 'item', sourceId: 'focus-tonic', name: '凝神增益', tags: ['快剑'], durationTurns: 2, }, ], }, }), ], }); const result = useInventoryItem(state, 'focus-tonic'); assert.equal(result.ok, true); if (!result.ok) { return; } assert.equal(result.mutation, 'use'); assert.equal(result.nextState.playerHp, 86); assert.equal(result.nextState.playerMana, 34); assert.equal(result.nextState.playerSkillCooldowns.slash, 1); assert.equal(result.nextState.playerInventory.length, 0); assert.equal(result.nextState.runtimeStats.itemsUsed, 1); assert.equal(result.nextState.activeBuildBuffs[0]?.id, 'focus-tonic:buff'); }); test('equipInventoryItem swaps loadout and returns replaced gear to inventory', () => { const oldWeapon = buildItem({ id: 'starter-blade', category: '武器', name: '旧佩剑', rarity: 'common', tags: ['weapon', '快剑'], equipmentSlotId: 'weapon', statProfile: { outgoingDamageBonus: 0.04, }, buildProfile: { role: '快剑', tags: ['快剑'], synergy: ['快剑'], forgeRank: 0, }, }); const nextWeapon = buildItem({ id: 'storm-blade', category: '武器', name: '逐风短剑', rarity: 'rare', tags: ['weapon', '快剑', '突进'], equipmentSlotId: 'weapon', statProfile: { outgoingDamageBonus: 0.12, }, buildProfile: { role: '快剑', tags: ['快剑', '突进'], synergy: ['快剑', '突进'], forgeRank: 0, }, }); const state = createState({ playerInventory: [nextWeapon], playerEquipment: { weapon: oldWeapon, armor: null, relic: null, }, }); const result = equipInventoryItem(state, 'storm-blade'); assert.equal(result.ok, true); if (!result.ok) { return; } assert.equal(result.mutation, 'equip'); assert.equal(result.slot, 'weapon'); assert.equal(result.nextState.playerEquipment.weapon?.name, '逐风短剑'); assert.equal( result.nextState.playerInventory.some((item) => item.id === 'starter-blade'), true, ); assert.equal( result.nextState.playerInventory.some((item) => item.id === 'storm-blade'), false, ); }); test('craftForgeRecipe consumes materials and produces forged output on the server side', () => { const state = createState({ playerCurrency: 40, playerInventory: [ buildItem({ id: 'scrap-iron', category: '材料', name: '残铁碎片', quantity: 3, rarity: 'common', tags: ['material'], }), ], }); const result = craftForgeRecipe(state, 'synthesis-refined-ingot'); assert.equal(result.ok, true); if (!result.ok) { return; } assert.equal(result.mutation, 'craft'); assert.equal(result.nextState.playerCurrency, 22); assert.equal(result.createdItem?.name, '精炼锭材'); assert.equal( result.nextState.playerInventory.some((item) => item.name === '精炼锭材'), true, ); assert.equal( result.nextState.playerInventory.some((item) => item.id === 'scrap-iron'), false, ); });