263 lines
8.4 KiB
TypeScript
263 lines
8.4 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { getEmbeddedDropTables } from '../src/game/embedded-drop-tables.ts'
|
|
import {
|
|
TransactionType,
|
|
calculateTransactionCost,
|
|
calculateGambleCost,
|
|
isItemRepairable,
|
|
isClassSpecificItemBase,
|
|
hasStaffModsItemType,
|
|
} from '../src/game/item-cost.ts'
|
|
import { VendorNpcId } from '../src/game/npc-table.ts'
|
|
import { createDroppedItem } from '../src/game/drop-pipeline.ts'
|
|
import { D2Rng } from '../src/game/d2-rng.ts'
|
|
import { ItemFlag, ItemQuality, decodeItemFlags, type Item } from '../src/game/items.ts'
|
|
|
|
describe('1.13c Item Transaction & Acquisition Cost Engine (M19-P3)', () => {
|
|
const tables = getEmbeddedDropTables()
|
|
|
|
it('prices Starter Items at 1 gold for Buy and Sell and 0/1 for Repair', () => {
|
|
const haxBase = tables.getBase('hax')!
|
|
const normalItem = createDroppedItem(haxBase, 'normal', {
|
|
itemTypes: tables.itemTypes,
|
|
ilvl: 1,
|
|
dwInitSeed: 100,
|
|
itemRng: new D2Rng(100),
|
|
})
|
|
const starterItem: Item = {
|
|
...normalItem,
|
|
rawFlags: (normalItem.rawFlags ?? 0) | ItemFlag.STARTER_ITEM,
|
|
flags: decodeItemFlags((normalItem.rawFlags ?? 0) | ItemFlag.STARTER_ITEM),
|
|
}
|
|
|
|
expect(
|
|
calculateTransactionCost({
|
|
item: starterItem,
|
|
transactionType: TransactionType.BUY,
|
|
vendorId: VendorNpcId.CHARSI,
|
|
tables,
|
|
}),
|
|
).toBe(1)
|
|
|
|
expect(
|
|
calculateTransactionCost({
|
|
item: starterItem,
|
|
transactionType: TransactionType.SELL,
|
|
vendorId: VendorNpcId.CHARSI,
|
|
tables,
|
|
}),
|
|
).toBe(1)
|
|
})
|
|
|
|
it('prices Tomes (tbk / ibk) dynamically by scroll charge count per Books.txt', () => {
|
|
const tbkBase = tables.getBase('tbk')!
|
|
const baseTome = createDroppedItem(tbkBase, 'normal', {
|
|
itemTypes: tables.itemTypes,
|
|
ilvl: 1,
|
|
dwInitSeed: 200,
|
|
itemRng: new D2Rng(200),
|
|
})
|
|
|
|
// Tome with 3 scrolls vs Tome with 20 scrolls
|
|
const tome3: Item = { ...baseTome, stack: 3, quantity: 3 }
|
|
const tome20: Item = { ...baseTome, stack: 20, quantity: 20 }
|
|
|
|
const buy3 = calculateTransactionCost({
|
|
item: tome3,
|
|
transactionType: TransactionType.BUY,
|
|
vendorId: VendorNpcId.AKARA,
|
|
tables,
|
|
})
|
|
const buy20 = calculateTransactionCost({
|
|
item: tome20,
|
|
transactionType: TransactionType.BUY,
|
|
vendorId: VendorNpcId.AKARA,
|
|
tables,
|
|
})
|
|
|
|
// Books.txt: BaseCost=250, CostPerCharge=50 -> 3 scrolls = 400, 20 scrolls = 1250
|
|
expect(buy20).toBeGreaterThan(buy3)
|
|
expect(buy20 - buy3).toBe(17 * 50)
|
|
|
|
const sell3 = calculateTransactionCost({
|
|
item: tome3,
|
|
transactionType: TransactionType.SELL,
|
|
vendorId: VendorNpcId.AKARA,
|
|
tables,
|
|
})
|
|
const sell20 = calculateTransactionCost({
|
|
item: tome20,
|
|
transactionType: TransactionType.SELL,
|
|
vendorId: VendorNpcId.AKARA,
|
|
tables,
|
|
})
|
|
expect(sell20).toBeGreaterThan(sell3)
|
|
})
|
|
|
|
it('applies 1/4 acquisition divisor to class-specific items (e.g. Sorceress Orb / Necromancer Wand)', () => {
|
|
const ob1 = tables.getBase('ob1')! // Jared's Stone (Orb, class='sor')
|
|
expect(isClassSpecificItemBase(ob1, tables.itemTypes)).toBe(true)
|
|
|
|
const wnd = tables.getBase('wnd')! // Wand (StaffMods='nec', non-class-specific)
|
|
expect(hasStaffModsItemType(wnd, tables.itemTypes)).toBe(true)
|
|
})
|
|
|
|
it('adds affix / stat bonus cost and staffMod single-skill cost to Magic/Rare items', () => {
|
|
const wndBase = tables.getBase('wnd')!
|
|
const plainWand = createDroppedItem(wndBase, 'normal', {
|
|
itemTypes: tables.itemTypes,
|
|
ilvl: 12,
|
|
dwInitSeed: 301,
|
|
itemRng: new D2Rng(301),
|
|
})
|
|
|
|
const staffModWand: Item = {
|
|
...plainWand,
|
|
staffMods: [{ code: 'skill', param: 67, min: 3, max: 3, value: 3 }], // +3 Teeth (skillId 67)
|
|
}
|
|
|
|
const plainBuy = calculateTransactionCost({
|
|
item: plainWand,
|
|
transactionType: TransactionType.BUY,
|
|
vendorId: VendorNpcId.AKARA,
|
|
tables,
|
|
})
|
|
const staffModBuy = calculateTransactionCost({
|
|
item: staffModWand,
|
|
transactionType: TransactionType.BUY,
|
|
vendorId: VendorNpcId.AKARA,
|
|
tables,
|
|
})
|
|
expect(staffModBuy).toBeGreaterThan(plainBuy)
|
|
|
|
const plainSell = calculateTransactionCost({
|
|
item: plainWand,
|
|
transactionType: TransactionType.SELL,
|
|
vendorId: VendorNpcId.AKARA,
|
|
tables,
|
|
})
|
|
const staffModSell = calculateTransactionCost({
|
|
item: staffModWand,
|
|
transactionType: TransactionType.SELL,
|
|
vendorId: VendorNpcId.AKARA,
|
|
tables,
|
|
})
|
|
expect(staffModSell).toBeGreaterThan(plainSell)
|
|
})
|
|
|
|
it('clamps acquisition (SELL) price to Npc.txt MaxBuy cap per Act and Difficulty (5000 / 30000 / 35000)', () => {
|
|
const aarBase = tables.getBase('uar')! // Sacred Armor (high cost elite armor)
|
|
const baseArmor = createDroppedItem(aarBase, 'magic', {
|
|
itemTypes: tables.itemTypes,
|
|
ilvl: 85,
|
|
dwInitSeed: 401,
|
|
itemRng: new D2Rng(401),
|
|
})
|
|
// Give it high-cost skills/stats so raw sell value exceeds 35,000
|
|
const expensiveArmor: Item = {
|
|
...baseArmor,
|
|
statList: [
|
|
{ statId: 127, param: 0, value: 5 }, // +5 All Skills
|
|
{ statId: 7, param: 0, value: 100 }, // +100 Life
|
|
],
|
|
}
|
|
|
|
const sellAct1Norm = calculateTransactionCost({
|
|
item: expensiveArmor,
|
|
transactionType: TransactionType.SELL,
|
|
difficulty: 0,
|
|
vendorId: VendorNpcId.AKARA,
|
|
actIndex: 0,
|
|
tables,
|
|
})
|
|
expect(sellAct1Norm).toBe(5000)
|
|
|
|
const sellNightmare = calculateTransactionCost({
|
|
item: expensiveArmor,
|
|
transactionType: TransactionType.SELL,
|
|
difficulty: 1,
|
|
vendorId: VendorNpcId.AKARA,
|
|
actIndex: 0,
|
|
tables,
|
|
})
|
|
expect(sellNightmare).toBe(30000)
|
|
|
|
const sellHell = calculateTransactionCost({
|
|
item: expensiveArmor,
|
|
transactionType: TransactionType.SELL,
|
|
difficulty: 2,
|
|
vendorId: VendorNpcId.AKARA,
|
|
actIndex: 0,
|
|
tables,
|
|
})
|
|
expect(sellHell).toBe(35000)
|
|
})
|
|
|
|
it('halves acquisition (SELL) price for Ethereal items and forbids repairing Ethereal items', () => {
|
|
const capBase = tables.getBase('plt')! // Plate Mail (below 35k cap so halving is observable)
|
|
const normalArmor = createDroppedItem(capBase, 'normal', {
|
|
itemTypes: tables.itemTypes,
|
|
ilvl: 25,
|
|
ethereal: false,
|
|
dwInitSeed: 501,
|
|
itemRng: new D2Rng(501),
|
|
})
|
|
const ethArmor = createDroppedItem(capBase, 'normal', {
|
|
itemTypes: tables.itemTypes,
|
|
ilvl: 25,
|
|
ethereal: true,
|
|
dwInitSeed: 501,
|
|
itemRng: new D2Rng(501),
|
|
})
|
|
|
|
const normSell = calculateTransactionCost({
|
|
item: normalArmor,
|
|
transactionType: TransactionType.SELL,
|
|
difficulty: 2,
|
|
vendorId: VendorNpcId.CHARSI,
|
|
tables,
|
|
})
|
|
const ethSell = calculateTransactionCost({
|
|
item: ethArmor,
|
|
transactionType: TransactionType.SELL,
|
|
difficulty: 2,
|
|
vendorId: VendorNpcId.CHARSI,
|
|
tables,
|
|
})
|
|
expect(ethSell).toBeLessThan(normSell)
|
|
|
|
const damagedEth: Item = { ...ethArmor, durability: 5, maxDurability: 30 }
|
|
expect(isItemRepairable(damagedEth)).toBe(false)
|
|
expect(
|
|
calculateTransactionCost({
|
|
item: damagedEth,
|
|
transactionType: TransactionType.REPAIR,
|
|
vendorId: VendorNpcId.CHARSI,
|
|
tables,
|
|
}),
|
|
).toBe(0)
|
|
})
|
|
|
|
it('computes Gamble costs (fixed 50k Ring / 63k Amulet, cLvl-scaled equipment) and applies vendor discounts', () => {
|
|
const rinBase = tables.getBase('rin')!
|
|
const amuBase = tables.getBase('amu')!
|
|
const capBase = tables.getBase('cap')!
|
|
|
|
const rinItem = createDroppedItem(rinBase, 'magic', { itemTypes: tables.itemTypes, ilvl: 20, dwInitSeed: 601 })
|
|
const amuItem = createDroppedItem(amuBase, 'magic', { itemTypes: tables.itemTypes, ilvl: 20, dwInitSeed: 602 })
|
|
const capItem = createDroppedItem(capBase, 'magic', { itemTypes: tables.itemTypes, ilvl: 20, dwInitSeed: 603 })
|
|
|
|
expect(calculateGambleCost({ item: rinItem, cLvl: 30, tables })).toBe(50000)
|
|
expect(calculateGambleCost({ item: amuItem, cLvl: 30, tables })).toBe(63000)
|
|
|
|
// 15% Gheed's Fortune + 15% Edge = 30% reduced vendor prices
|
|
expect(
|
|
calculateGambleCost({ item: rinItem, cLvl: 30, reducedPricesPercent: 30, tables }),
|
|
).toBe(35000)
|
|
|
|
const capGambleLvl10 = calculateGambleCost({ item: capItem, cLvl: 10, tables })
|
|
const capGambleLvl80 = calculateGambleCost({ item: capItem, cLvl: 80, tables })
|
|
expect(capGambleLvl80).toBeGreaterThan(capGambleLvl10)
|
|
})
|
|
})
|