461 lines
19 KiB
TypeScript
461 lines
19 KiB
TypeScript
import { describe, it, expect, beforeAll } from 'vitest'
|
|
import * as fs from 'fs'
|
|
import { MountedArchives } from '../src/mpq/mount.ts'
|
|
import { MpqArchive } from '../src/mpq/archive.ts'
|
|
import { fileSource } from '../src/mpq/file-source.ts'
|
|
import type { D2Table } from '../src/game/acts.ts'
|
|
import { cell, parseTable } from '../src/game/acts.ts'
|
|
import {
|
|
parseItemTypesTable,
|
|
loadItemTypes,
|
|
isA,
|
|
ITEM_TYPES_TABLE_PATH,
|
|
type ItemTypeTable,
|
|
} from '../src/game/item-types.ts'
|
|
|
|
const hasD2 = fs.existsSync('samples/d2/d2data.mpq')
|
|
|
|
describe('ItemTypes loader & Equiv inheritance (Issue #89)', () => {
|
|
let table: ItemTypeTable
|
|
let rawTable: D2Table | undefined
|
|
|
|
beforeAll(async () => {
|
|
if (!hasD2) {
|
|
throw new Error(
|
|
'[Preflight] samples/d2 MPQ files (d2data.mpq, d2exp.mpq, Patch_D2.mpq) are required for ItemTypes table verification. ' +
|
|
'Please symlink samples/d2 to your Diablo II 1.13c installation directory.'
|
|
)
|
|
}
|
|
const archives = new MountedArchives()
|
|
for (const name of ['d2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq']) {
|
|
archives.add(name, await MpqArchive.open(await fileSource(`samples/d2/${name}`)))
|
|
}
|
|
const bytes = await archives.read(ITEM_TYPES_TABLE_PATH)
|
|
rawTable = parseTable(bytes)
|
|
table = parseItemTypesTable(rawTable)
|
|
})
|
|
|
|
it('correctly uses the canonical MPQ table path', () => {
|
|
expect(ITEM_TYPES_TABLE_PATH).toBe('data\\global\\excel\\ItemTypes.txt')
|
|
})
|
|
|
|
it('loads item types using loadItemTypes with mounted archives', async () => {
|
|
if (hasD2) {
|
|
const archives = new MountedArchives()
|
|
for (const name of ['d2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq']) {
|
|
archives.add(name, await MpqArchive.open(await fileSource(`samples/d2/${name}`)))
|
|
}
|
|
const loaded = await loadItemTypes(archives)
|
|
expect(loaded.types.length).toBe(98)
|
|
expect(loaded.byCode.size).toBe(98)
|
|
expect(isA('shie', 'shld', loaded)).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('verifies 104 raw rows in original file and filters exactly 6 rows to yield 98 valid types', () => {
|
|
if (hasD2 && rawTable) {
|
|
expect(rawTable.rows.length).toBe(104)
|
|
|
|
// Filtered indices:
|
|
// Index 0 & 1: None placeholders
|
|
expect(cell(rawTable, rawTable.rows[0]!, 'ItemType')).toBe('None')
|
|
expect(cell(rawTable, rawTable.rows[0]!, 'Code')).toBe('')
|
|
expect(cell(rawTable, rawTable.rows[1]!, 'ItemType')).toBe('None')
|
|
expect(cell(rawTable, rawTable.rows[1]!, 'Code')).toBe('')
|
|
|
|
// Index 14, 17, 23: Not Used placeholders
|
|
expect(cell(rawTable, rawTable.rows[14]!, 'ItemType')).toBe('Not Used')
|
|
expect(cell(rawTable, rawTable.rows[14]!, 'Code')).toBe('')
|
|
expect(cell(rawTable, rawTable.rows[17]!, 'ItemType')).toBe('Not Used')
|
|
expect(cell(rawTable, rawTable.rows[17]!, 'Code')).toBe('')
|
|
expect(cell(rawTable, rawTable.rows[23]!, 'ItemType')).toBe('Not Used')
|
|
expect(cell(rawTable, rawTable.rows[23]!, 'Code')).toBe('')
|
|
|
|
// Index 58: Expansion separator row
|
|
expect(cell(rawTable, rawTable.rows[58]!, 'ItemType')).toBe('Expansion')
|
|
expect(cell(rawTable, rawTable.rows[58]!, 'Code')).toBe('')
|
|
}
|
|
|
|
expect(table.types.length).toBe(98)
|
|
expect(table.byCode.size).toBe(98)
|
|
|
|
// Ensure placeholders and separator are not in parsed types
|
|
expect(table.byCode.has('')).toBe(false)
|
|
expect(table.byCode.has('None')).toBe(false)
|
|
expect(table.byCode.has('Not Used')).toBe(false)
|
|
expect(table.byCode.has('Expansion')).toBe(false)
|
|
})
|
|
|
|
it('indexes rows by code with case-insensitive and trimmed lookup', () => {
|
|
const shie = table.byCode.get('shie')
|
|
expect(shie).toBeDefined()
|
|
expect(shie?.name).toBe('Shield')
|
|
|
|
// Case-insensitive & trimmed lookups
|
|
expect(table.byCode.get('SHIE')).toBe(shie)
|
|
expect(table.byCode.get(' shie ')).toBe(shie)
|
|
expect(table.byCode.get('ShIe')).toBe(shie)
|
|
expect(table.byCode.has('SHIE')).toBe(true)
|
|
expect(table.byCode.has(' shie ')).toBe(true)
|
|
expect(table.byCode.has('nonexistent')).toBe(false)
|
|
})
|
|
|
|
it('asserts exactly 5 rows have treasureClass === 1 (TreasureClass seeds)', () => {
|
|
const tcSeeds = table.types.filter(t => t.treasureClass === 1)
|
|
expect(tcSeeds.length).toBe(5)
|
|
|
|
const seedCodes = tcSeeds.map(t => t.code)
|
|
expect(seedCodes).toEqual(['bow', 'weap', 'mele', 'armo', 'abow'])
|
|
|
|
const seedNames = tcSeeds.map(t => t.name)
|
|
expect(seedNames).toEqual(['Bow', 'Weapon', 'Melee Weapon', 'Any Armor', 'Amazon Bow'])
|
|
|
|
// All remaining 93 types must have treasureClass === 0
|
|
const nonSeeds = table.types.filter(t => t.treasureClass === 0)
|
|
expect(nonSeeds.length).toBe(93)
|
|
})
|
|
|
|
it('maps all ItemTypeDefinition fields accurately', () => {
|
|
const shie = table.byCode.get('shie')!
|
|
expect(shie.code).toBe('shie')
|
|
expect(shie.name).toBe('Shield')
|
|
expect(shie.equiv1).toBe('shld')
|
|
expect(shie.equiv2).toBe('')
|
|
expect(shie.treasureClass).toBe(0)
|
|
expect(shie.rarity).toBe(3)
|
|
expect(shie.maxSock1).toBe(3)
|
|
expect(shie.maxSock25).toBe(3)
|
|
expect(shie.maxSock40).toBe(4)
|
|
expect(shie.magic).toBe(false)
|
|
expect(shie.rare).toBe(true)
|
|
expect(shie.normal).toBe(false)
|
|
expect(shie.charm).toBe(false)
|
|
expect(shie.gem).toBe(false)
|
|
expect(shie.body).toBe(true)
|
|
expect(shie.bodyLoc1).toBe('rarm')
|
|
expect(shie.bodyLoc2).toBe('larm')
|
|
expect(shie.beltable).toBe(false)
|
|
expect(shie.class).toBe('')
|
|
|
|
// StaffMods test (Paladin scepters, Necromancer wands, Sorceress staves)
|
|
const scep = table.byCode.get('scep')!
|
|
expect(scep.staffMods).toBe('pal')
|
|
expect(scep.rarity).toBe(1)
|
|
expect(scep.maxSock1).toBe(3)
|
|
expect(scep.maxSock25).toBe(5)
|
|
expect(scep.maxSock40).toBe(6)
|
|
|
|
const wand = table.byCode.get('wand')!
|
|
expect(wand.staffMods).toBe('nec')
|
|
|
|
const staf = table.byCode.get('staf')!
|
|
expect(staf.staffMods).toBe('sor')
|
|
|
|
// Class-specific items
|
|
const amaz = table.byCode.get('amaz')!
|
|
expect(amaz.class).toBe('ama')
|
|
|
|
const barb = table.byCode.get('barb')!
|
|
expect(barb.class).toBe('bar')
|
|
|
|
// Beltable potions
|
|
const poti = table.byCode.get('poti')!
|
|
expect(poti.beltable).toBe(true)
|
|
expect(poti.body).toBe(false)
|
|
|
|
// Charms & gems
|
|
const char = table.byCode.get('char')!
|
|
expect(char.charm).toBe(true)
|
|
expect(char.magic).toBe(true)
|
|
|
|
const gem = table.byCode.get('gem')!
|
|
expect(gem.gem).toBe(true)
|
|
})
|
|
|
|
describe('Type inheritance check (isA)', () => {
|
|
it('verifies direct inheritance: shie -> shld', () => {
|
|
expect(isA('shie', 'shld', table)).toBe(true)
|
|
})
|
|
|
|
it('verifies melee to weapon inheritance: mele -> weap', () => {
|
|
expect(isA('mele', 'weap', table)).toBe(true)
|
|
})
|
|
|
|
it('verifies wand/rod inheritance: wand -> rod', () => {
|
|
expect(isA('wand', 'rod', table)).toBe(true)
|
|
expect(isA('scep', 'rod', table)).toBe(true)
|
|
expect(isA('staf', 'rod', table)).toBe(true)
|
|
})
|
|
|
|
it('verifies multi-hop inheritance chain: abow -> bow -> miss (and abow -> weap)', () => {
|
|
expect(isA('abow', 'bow', table)).toBe(true)
|
|
expect(isA('bow', 'miss', table)).toBe(true)
|
|
expect(isA('abow', 'miss', table)).toBe(true)
|
|
expect(isA('miss', 'weap', table)).toBe(true)
|
|
expect(isA('abow', 'weap', table)).toBe(true)
|
|
})
|
|
|
|
it('verifies Equiv2 multi-parent inheritance: abow -> amaz -> clas', () => {
|
|
expect(isA('abow', 'amaz', table)).toBe(true)
|
|
expect(isA('amaz', 'clas', table)).toBe(true)
|
|
expect(isA('abow', 'clas', table)).toBe(true)
|
|
})
|
|
|
|
it('verifies diamond inheritance: tkni -> comb -> mele/thro -> weap', () => {
|
|
expect(isA('tkni', 'comb', table)).toBe(true)
|
|
expect(isA('tkni', 'knif', table)).toBe(true)
|
|
expect(isA('tkni', 'mele', table)).toBe(true)
|
|
expect(isA('tkni', 'thro', table)).toBe(true)
|
|
expect(isA('tkni', 'weap', table)).toBe(true)
|
|
})
|
|
|
|
it('verifies reflexive inheritance (typeCode === ancestorCode)', () => {
|
|
expect(isA('shie', 'shie', table)).toBe(true)
|
|
expect(isA('weap', 'weap', table)).toBe(true)
|
|
expect(isA('armo', 'armo', table)).toBe(true)
|
|
})
|
|
|
|
it('correctly rejects non-ancestor relationships', () => {
|
|
expect(isA('shie', 'weap', table)).toBe(false)
|
|
expect(isA('bow', 'armo', table)).toBe(false)
|
|
expect(isA('weap', 'mele', table)).toBe(false) // Parent is not a descendant of child
|
|
expect(isA('shld', 'shie', table)).toBe(false)
|
|
expect(isA('armo', 'shie', table)).toBe(false)
|
|
})
|
|
|
|
it('handles empty or unknown codes gracefully without throwing', () => {
|
|
expect(isA('', 'weap', table)).toBe(false)
|
|
expect(isA('shie', '', table)).toBe(false)
|
|
expect(isA('', '', table)).toBe(false)
|
|
expect(isA('unknown', 'weap', table)).toBe(false)
|
|
expect(isA('shie', 'unknown', table)).toBe(false)
|
|
expect(isA('unknown', 'unknown', table)).toBe(false)
|
|
})
|
|
|
|
it('supports case-insensitive and whitespace-padded arguments', () => {
|
|
expect(isA('SHIE', 'shld', table)).toBe(true)
|
|
expect(isA('shie', 'SHLD', table)).toBe(true)
|
|
expect(isA(' abow ', ' WEAP ', table)).toBe(true)
|
|
expect(isA('WAND', 'ROD', table)).toBe(true)
|
|
})
|
|
|
|
it('memoizes results in table._isACache', () => {
|
|
const cache = table._isACache
|
|
expect(cache).toBeDefined()
|
|
|
|
// Query and check cache entry
|
|
const result = isA('wand', 'rod', table)
|
|
expect(result).toBe(true)
|
|
expect(cache?.get('wand:rod')).toBe(true)
|
|
|
|
// Negative query memoized
|
|
const negResult = isA('wand', 'armo', table)
|
|
expect(negResult).toBe(false)
|
|
expect(cache?.get('wand:armo')).toBe(false)
|
|
})
|
|
|
|
it('safely handles circular inheritance chains without infinite loop', () => {
|
|
const cyclicTable: ItemTypeTable = {
|
|
types: [
|
|
{
|
|
code: 'cycA',
|
|
name: 'Cyclic A',
|
|
equiv1: 'cycB',
|
|
equiv2: '',
|
|
treasureClass: 0,
|
|
rarity: 0,
|
|
maxSock1: 0,
|
|
maxSock25: 0,
|
|
maxSock40: 0,
|
|
magic: false,
|
|
rare: false,
|
|
normal: false,
|
|
charm: false,
|
|
gem: false,
|
|
staffMods: '',
|
|
body: false,
|
|
bodyLoc1: '',
|
|
bodyLoc2: '',
|
|
beltable: false,
|
|
class: '',
|
|
},
|
|
{
|
|
code: 'cycB',
|
|
name: 'Cyclic B',
|
|
equiv1: 'cycA',
|
|
equiv2: '',
|
|
treasureClass: 0,
|
|
rarity: 0,
|
|
maxSock1: 0,
|
|
maxSock25: 0,
|
|
maxSock40: 0,
|
|
magic: false,
|
|
rare: false,
|
|
normal: false,
|
|
charm: false,
|
|
gem: false,
|
|
staffMods: '',
|
|
body: false,
|
|
bodyLoc1: '',
|
|
bodyLoc2: '',
|
|
beltable: false,
|
|
class: '',
|
|
},
|
|
{
|
|
code: 'target',
|
|
name: 'Target',
|
|
equiv1: '',
|
|
equiv2: '',
|
|
treasureClass: 0,
|
|
rarity: 0,
|
|
maxSock1: 0,
|
|
maxSock25: 0,
|
|
maxSock40: 0,
|
|
magic: false,
|
|
rare: false,
|
|
normal: false,
|
|
charm: false,
|
|
gem: false,
|
|
staffMods: '',
|
|
body: false,
|
|
bodyLoc1: '',
|
|
bodyLoc2: '',
|
|
beltable: false,
|
|
class: '',
|
|
},
|
|
],
|
|
byCode: new Map([
|
|
['cyca', {
|
|
code: 'cycA',
|
|
name: 'Cyclic A',
|
|
equiv1: 'cycB',
|
|
equiv2: '',
|
|
treasureClass: 0,
|
|
rarity: 0,
|
|
maxSock1: 0,
|
|
maxSock25: 0,
|
|
maxSock40: 0,
|
|
magic: false,
|
|
rare: false,
|
|
normal: false,
|
|
charm: false,
|
|
gem: false,
|
|
staffMods: '',
|
|
body: false,
|
|
bodyLoc1: '',
|
|
bodyLoc2: '',
|
|
beltable: false,
|
|
class: '',
|
|
}],
|
|
['cycb', {
|
|
code: 'cycB',
|
|
name: 'Cyclic B',
|
|
equiv1: 'cycA',
|
|
equiv2: '',
|
|
treasureClass: 0,
|
|
rarity: 0,
|
|
maxSock1: 0,
|
|
maxSock25: 0,
|
|
maxSock40: 0,
|
|
magic: false,
|
|
rare: false,
|
|
normal: false,
|
|
charm: false,
|
|
gem: false,
|
|
staffMods: '',
|
|
body: false,
|
|
bodyLoc1: '',
|
|
bodyLoc2: '',
|
|
beltable: false,
|
|
class: '',
|
|
}],
|
|
['target', {
|
|
code: 'target',
|
|
name: 'Target',
|
|
equiv1: '',
|
|
equiv2: '',
|
|
treasureClass: 0,
|
|
rarity: 0,
|
|
maxSock1: 0,
|
|
maxSock25: 0,
|
|
maxSock40: 0,
|
|
magic: false,
|
|
rare: false,
|
|
normal: false,
|
|
charm: false,
|
|
gem: false,
|
|
staffMods: '',
|
|
body: false,
|
|
bodyLoc1: '',
|
|
bodyLoc2: '',
|
|
beltable: false,
|
|
class: '',
|
|
}],
|
|
]),
|
|
}
|
|
|
|
// Should find direct parent
|
|
expect(isA('cycA', 'cycB', cyclicTable)).toBe(true)
|
|
expect(isA('cycB', 'cycA', cyclicTable)).toBe(true)
|
|
|
|
// Should safely terminate and return false when target is not reached
|
|
expect(isA('cycA', 'target', cyclicTable)).toBe(false)
|
|
expect(isA('cycB', 'target', cyclicTable)).toBe(false)
|
|
})
|
|
})
|
|
})
|
|
|
|
/**
|
|
* Creates a minimal fallback D2Table for environments where MPQ is missing.
|
|
*/
|
|
function createMinimalFallbackTable(): D2Table {
|
|
const header = [
|
|
'ItemType', 'Code', 'Equiv1', 'Equiv2', 'Repair', 'Body',
|
|
'BodyLoc1', 'BodyLoc2', 'Shoots', 'Quiver', 'Throwable', 'Reload',
|
|
'ReEquip', 'AutoStack', 'Magic', 'Rare', 'Normal', 'Charm',
|
|
'Gem', 'Beltable', 'MaxSock1', 'MaxSock25', 'MaxSock40', 'TreasureClass',
|
|
'Rarity', 'StaffMods', 'CostFormula', 'Class', 'VarInvGfx', 'InvGfx1',
|
|
'InvGfx2', 'InvGfx3', 'InvGfx4', 'InvGfx5', 'InvGfx6', 'StorePage',
|
|
'*eol',
|
|
]
|
|
|
|
// Filtered rows + key types
|
|
const rows: string[][] = [
|
|
// 0, 1: None placeholders
|
|
['None', '', '', '', '0', '0', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '0', '0', '', '', '', '', '', '', '', '', '', '', '', '0'],
|
|
['None', '', '', '', '0', '0', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '0', '0', '', '', '', '', '', '', '', '', '', '', '', '0'],
|
|
['Shield', 'shie', 'shld', '', '1', '1', 'rarm', 'larm', '', '', '', '', '', '', '', '1', '', '', '', '', '3', '3', '4', '0', '3', '', '', '', '', '', '', '', '', '', '', 'armo', '0'],
|
|
['Armor', 'tors', 'armo', '', '1', '1', 'tors', 'tors', '', '', '', '', '', '', '', '1', '', '', '', '', '3', '4', '6', '0', '3', '', '', '', '', '', '', '', '', '', '', 'armo', '0'],
|
|
['Gold', 'gold', 'misc', '', '0', '0', '', '', '', '', '', '', '', '', '', '', '1', '', '', '', '0', '0', '0', '0', '3', '', '', '', '', '', '', '', '', '', '', 'misc', '0'],
|
|
// 14: Not Used placeholder
|
|
['Not Used', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '0', '3', '', '', '', '', '', '', '', '', '', '', '', '0'],
|
|
['Scepter', 'scep', 'rod', '', '1', '1', 'rarm', 'larm', '', '', '', '', '', '', '', '1', '', '', '', '', '3', '5', '6', '0', '1', 'pal', '', '', '', '', '', '', '', '', '', 'weap', '0'],
|
|
['Wand', 'wand', 'rod', '', '1', '1', 'rarm', 'larm', '', '', '', '', '', '', '', '1', '', '', '', '', '2', '2', '2', '0', '1', 'nec', '', '', '', '', '', '', '', '', '', 'weap', '0'],
|
|
['Staff', 'staf', 'rod', '', '1', '1', 'rarm', 'larm', '', '', '', '', '', '', '', '1', '', '', '', '', '5', '6', '6', '0', '1', 'sor', '', '', '', '', '', '', '', '', '', 'weap', '0'],
|
|
['Bow', 'bow', 'miss', '', '1', '1', 'rarm', 'larm', '', '', '', '', '', '', '', '1', '', '', '', '', '3', '4', '6', '1', '3', '', '', '', '', '', '', '', '', '', '', 'weap', '0'],
|
|
['Weapon', 'weap', '', '', '0', '0', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '0', '0', '0', '1', '3', '', '', '', '', '', '', '', '', '', '', 'weap', '0'],
|
|
['Melee Weapon', 'mele', 'weap', '', '0', '0', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '0', '0', '0', '1', '3', '', '', '', '', '', '', '', '', '', '', 'weap', '0'],
|
|
['Missile Weapon', 'miss', 'weap', '', '0', '0', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '0', '0', '0', '0', '3', '', '', '', '', '', '', '', '', '', '', 'weap', '0'],
|
|
['Thrown Weapon', 'thro', 'weap', '', '0', '0', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '0', '0', '0', '0', '3', '', '', '', '', '', '', '', '', '', '', 'weap', '0'],
|
|
['Combo Weapon', 'comb', 'mele', 'thro', '0', '0', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '0', '0', '0', '0', '3', '', '', '', '', '', '', '', '', '', '', 'weap', '0'],
|
|
['Any Armor', 'armo', '', '', '0', '0', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '0', '0', '0', '1', '3', '', '', '', '', '', '', '', '', '', '', 'armo', '0'],
|
|
['Any Shield', 'shld', 'armo', 'seco', '0', '0', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '0', '0', '0', '0', '3', '', '', '', '', '', '', '', '', '', '', 'armo', '0'],
|
|
['Second Hand', 'seco', '', '', '0', '0', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '0', '0', '0', '0', '3', '', '', '', '', '', '', '', '', '', '', 'misc', '0'],
|
|
['Staves And Rods', 'rod', 'blun', '', '0', '0', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '0', '0', '0', '0', '3', '', '', '', '', '', '', '', '', '', '', 'weap', '0'],
|
|
['Blunt', 'blun', 'mele', '', '0', '0', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '0', '0', '0', '0', '3', '', '', '', '', '', '', '', '', '', '', 'weap', '0'],
|
|
// 58: Expansion separator
|
|
['Expansion', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '0'],
|
|
['Class Specific', 'clas', '', '', '0', '0', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '0', '0', '0', '0', '3', '', '', '', '', '', '', '', '', '', '', 'misc', '0'],
|
|
['Amazon Item', 'amaz', 'clas', '', '0', '0', '', '', '', '', '', '', '', '', '', '1', '', '', '', '', '0', '0', '0', '0', '1', '', '', 'ama', '', '', '', '', '', '', '', 'misc', '0'],
|
|
['Barbarian Item', 'barb', 'clas', '', '0', '0', '', '', '', '', '', '', '', '', '', '1', '', '', '', '', '0', '0', '0', '0', '1', '', '', 'bar', '', '', '', '', '', '', '', 'misc', '0'],
|
|
['Potion', 'poti', 'misc', '', '0', '0', '', '', '', '', '', '', '', '', '', '', '', '', '', '1', '0', '0', '0', '0', '3', '', '', '', '', '', '', '', '', '', '', 'misc', '0'],
|
|
['Charm', 'char', 'misc', '', '0', '0', '', '', '', '', '', '', '', '', '1', '', '', '1', '', '', '0', '0', '0', '0', '3', '', '', '', '', '', '', '', '', '', '', 'misc', '0'],
|
|
['Gem', 'gem', 'sock', '', '0', '0', '', '', '', '', '', '', '', '', '', '', '', '', '1', '', '0', '0', '0', '0', '3', '', '', '', '', '', '', '', '', '', '', 'misc', '0'],
|
|
['Amazon Bow', 'abow', 'bow', 'amaz', '1', '1', 'rarm', 'larm', '', '', '', '', '', '', '', '1', '', '', '', '', '3', '4', '5', '1', '1', '', '', 'ama', '', '', '', '', '', '', '', 'weap', '0'],
|
|
['Throwing Knife', 'tkni', 'comb', 'knif', '1', '1', 'rarm', 'larm', '', '', '1', '1', '1', '1', '', '1', '', '', '', '', '0', '0', '0', '0', '3', '', '', '', '', '', '', '', '', '', '', 'weap', '0'],
|
|
['Knife', 'knif', 'mele', '', '1', '1', 'rarm', 'larm', '', '', '', '', '', '', '', '1', '', '', '', '', '2', '2', '3', '0', '3', '', '', '', '', '', '', '', '', '', '', 'weap', '0'],
|
|
]
|
|
|
|
return { header, rows }
|
|
}
|