1
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
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<RuntimeGameState['playerCharacter']>
|
||||
>();
|
||||
}
|
||||
|
||||
function buildItem(
|
||||
overrides: Partial<RuntimeInventoryItem> &
|
||||
Pick<RuntimeInventoryItem, 'id' | 'category' | 'name'>,
|
||||
): RuntimeInventoryItem {
|
||||
return {
|
||||
quantity: 1,
|
||||
rarity: 'common',
|
||||
tags: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createState(overrides: Partial<RuntimeGameState> = {}): 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,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user