diablo2-web/tests/e2e-parity/tier4-real-world-scenarios....

516 lines
19 KiB
TypeScript

/**
* Tier 4 — Real-World Gameplay Scenarios (`tests/e2e-parity/tier4-real-world-scenarios.test.ts`).
*
* Verifies 5 complete full-lifecycle gameplay scenarios under Diablo II v1.13c parity:
* 1. Blood Moor Clearing: Town departure, obstacle navigation, monster combat, belt potion usage.
* 2. Den of Evil Completion: Cave exploration, room visibility, monster clearing count (5 -> 0), quest completion.
* 3. Countess Tower Run: Multi-floor cellar descent (21 -> 25), superunique fight, rune drop to ground, pickup into inventory.
* 4. Tristram Rescue: Red portal transition (4 -> 38), Griswold combat & movement kiting, Deckard Cain cage interaction & quest update.
* 5. Act Boss Kill (Andariel): Catacombs L4 chamber entry, poison spray mechanics & vulnerability, Andariel defeat, Act 1 quest complete, Act 2 Lut Gholein transition.
*/
import { describe, expect, it } from 'vitest'
import {
COLLIDE_BLANK,
COLLIDE_DOOR,
COLLIDE_MISSILE_BARRIER,
COLLIDE_NONE,
COLLIDE_OBJECT,
COLLIDE_WALL,
D2_TICK_MS,
calculateHitChance,
calculatePlayerBlockChance,
computeEffectiveFcr,
computeOneShotDurationMs,
computeStepSpeed256,
createCollisionGrid,
createParitySessionFixture,
getMissileTxtData,
getOverlayTxtData,
isTownLevel,
makeParityItem,
onlineItemDataToUiInventoryItem,
} from './helpers.ts'
describe('Tier 4 — Real-World Gameplay Scenarios (1.13c Ground Truth)', () => {
// ─────────────────────────────────────────────────────────────────────────
// Scenario 1: Blood Moor Clearing
// ─────────────────────────────────────────────────────────────────────────
describe('Scenario 1: Blood Moor Clearing', () => {
it('executes full town departure, terrain navigation, combat against Fallen, and potion recovery', () => {
const fixture = createParitySessionFixture({ selfId: 101, charClass: 3 }) // Paladin
// 1. Initial State: Inside Rogue Encampment (Act 1 Town, Area 1)
fixture.applyServerEvents({
type: 'LoadAct',
act: 0,
mapSeed: 42109,
areaId: 1,
})
expect(fixture.world.areaId).toBe(1)
expect(isTownLevel(fixture.world.areaId)).toBe(true)
// Player in town has town neutral posture mode 5
expect(fixture.selfUnit?.mode).toBe(5)
// 2. Departure: Player moves across boundary into Blood Moor (Area 2)
fixture.applyServerEvents({
type: 'LoadAct',
act: 0,
mapSeed: 42109,
areaId: 2,
})
expect(fixture.world.areaId).toBe(2)
expect(isTownLevel(fixture.world.areaId)).toBe(false)
// Wilderness mode is combat neutral mode 1
expect(fixture.selfUnit?.mode).toBe(1)
// 3. Terrain Navigation: Setup 40x40 collision grid with rocks/trees
const moorGrid = createCollisionGrid(40, 40, COLLIDE_NONE)
// Place tree clump at x: 15..17, y: 15..17
for (let x = 15; x <= 17; x++) {
for (let y = 15; y <= 17; y++) {
moorGrid.setCollision(x, y, COLLIDE_WALL | COLLIDE_OBJECT)
}
}
expect(moorGrid.isWalkable(10, 10)).toBe(true)
expect(moorGrid.isWalkable(16, 16)).toBe(false) // Obstructed by tree
// 4. Monster Encounter: Spawn Fallen (classId 1) at (25, 20)
fixture.applyServerEvents({
type: 'UnitAssign',
isSelf: false,
unit: {
unitType: 'monster',
id: 201,
classId: 1, // Fallen
x: 25,
y: 20,
mode: 1,
},
})
const fallen = fixture.world.getUnit('monster', 201)
expect(fallen).toBeDefined()
expect(fallen?.classId).toBe(1)
// Hit chance calculation: Level 3 Paladin (AR: 180) vs Level 1 Fallen (Def: 12)
const hitChance = calculateHitChance(180, 12, 3, 1)
expect(hitChance).toBeGreaterThanOrEqual(75)
expect(hitChance).toBeLessThanOrEqual(95)
// Paladin strikes Fallen, Fallen transitions to death mode 12
fixture.applyServerEvents({
type: 'UnitMode',
unitType: 'monster',
id: 201,
mode: 12,
})
expect(fixture.world.getUnit('monster', 201)?.mode).toBe(12)
// 5. Damage & Belt Potion Recovery
// Zombie ambush hits player, reducing life to 35
fixture.applyServerEvents({
type: 'LifeMana',
life: 35,
mana: 100,
stamina: 100,
})
expect(fixture.world.self.life).toBe(35)
// Equip Minor Healing Potion (hp1) into belt slot 0
fixture.applyServerEvents({
type: 'ItemEvent',
action: 0,
container: 'belt',
item: makeParityItem({
id: 701,
code: 'hp1',
container: 'belt',
gridX: 0,
gridY: 0,
}),
})
expect(fixture.world.containers.belt.has(701)).toBe(true)
// Player drinks belt potion from slot 0
fixture.clearCommands()
const used = fixture.hudModel.useBeltSlot(0)
expect(used).toBe(true)
expect(fixture.sentCommands).toContainEqual({
type: 'BeltUse',
itemId: 701,
shift: false,
})
// Potion heals player over time to 85
fixture.applyServerEvents({
type: 'LifeMana',
life: 85,
mana: 100,
stamina: 100,
})
expect(fixture.world.self.life).toBe(85)
})
})
// ─────────────────────────────────────────────────────────────────────────
// Scenario 2: Den of Evil Completion
// ─────────────────────────────────────────────────────────────────────────
describe('Scenario 2: Den of Evil Completion', () => {
it('tracks cave entrance, room reveal, monster counter decrease, and quest completion flag', () => {
const fixture = createParitySessionFixture({ selfId: 102, charClass: 1 }) // Sorceress
// 1. Transition into Cave: Area 8 (Den of Evil)
fixture.applyServerEvents({
type: 'LoadAct',
act: 0,
mapSeed: 88310,
areaId: 8,
})
expect(fixture.world.areaId).toBe(8)
expect(isTownLevel(8)).toBe(false)
// 2. Room Discovery: Revealing two adjacent cavern rooms
fixture.applyServerEvents(
{ type: 'RoomReveal', areaId: 8, tileX: 20, tileY: 15 },
{ type: 'RoomReveal', areaId: 8, tileX: 21, tileY: 15 },
)
expect(fixture.world.getRevealedAreaIds()).toContain(8)
// 3. Line of sight within cavern corridors
const caveGrid = createCollisionGrid(30, 30, COLLIDE_NONE)
// Carve solid cave wall along x = 12
for (let y = 5; y <= 25; y++) {
caveGrid.setCollision(12, y, COLLIDE_WALL | COLLIDE_MISSILE_BARRIER)
}
// Line of sight blocked through cave wall
expect(caveGrid.hasLineOfSight(8, 15, 18, 15)).toBe(false)
// Line of sight open along same corridor
expect(caveGrid.hasLineOfSight(8, 10, 8, 20)).toBe(true)
// 4. Den of Evil Monster Counter Tracking (starts with 5 remaining monsters)
let denMonstersRemaining = 5
const monsterIds = [301, 302, 303, 304, 305]
for (const mId of monsterIds) {
fixture.applyServerEvents({
type: 'UnitAssign',
isSelf: false,
unit: {
unitType: 'monster',
id: mId,
classId: mId === 305 ? 683 : 1, // 305 is Corpsefire (Superunique Gargantuan Beast), others Fallens
x: 20 + (mId % 5),
y: 15 + (mId % 3),
mode: 1,
},
})
}
expect(fixture.world.units.size).toBe(6) // 1 player + 5 monsters
// Player casts Fire Bolt and clears all monsters one by one
for (const mId of monsterIds) {
fixture.applyServerEvents({
type: 'UnitMode',
unitType: 'monster',
id: mId,
mode: 12,
})
denMonstersRemaining--
}
expect(denMonstersRemaining).toBe(0)
// 5. Quest Completion: Server sends QuestState marking Den of Evil (Quest 0) complete
// Bit 0 = quest completed (0x1001)
fixture.applyServerEvents({
type: 'QuestState',
quests: new Uint16Array([0x1001]),
})
expect((fixture.world.quests[0]! & 0x0001) !== 0).toBe(true)
})
})
// ─────────────────────────────────────────────────────────────────────────
// Scenario 3: Countess Tower Run
// ─────────────────────────────────────────────────────────────────────────
describe('Scenario 3: Countess Tower Run', () => {
it('navigates multi-floor cellar sequence, executes Countess battle, and picks up dropped Ral rune', () => {
const fixture = createParitySessionFixture({ selfId: 103, charClass: 1 }) // Sorceress
// 1. Descend through Forgotten Tower Cellars: 21 -> 22 -> 23 -> 24 -> 25
const cellarLevels = [21, 22, 23, 24, 25]
for (const lvl of cellarLevels) {
fixture.applyServerEvents({
type: 'LoadAct',
act: 0,
mapSeed: 91919,
areaId: lvl,
})
expect(fixture.world.areaId).toBe(lvl)
}
expect(fixture.world.areaId).toBe(25) // Tower Cellar Level 5
// 2. Boss Fight — The Countess (SuperUnique classId 45)
fixture.applyServerEvents({
type: 'UnitAssign',
isSelf: false,
unit: {
unitType: 'monster',
id: 401,
classId: 45, // Countess
x: 50,
y: 60,
mode: 1,
},
})
const countess = fixture.world.getUnit('monster', 401)
expect(countess).toBeDefined()
expect(countess?.classId).toBe(45)
// Sorceress equipped with +65% FCR gear casts Static Field
const fcrDur = computeOneShotDurationMs(14, computeStepSpeed256(256, computeEffectiveFcr(65)))
expect(fcrDur).toBeLessThan(computeOneShotDurationMs(14, computeStepSpeed256(256, computeEffectiveFcr(0))))
// Countess dies
fixture.applyServerEvents({
type: 'UnitMode',
unitType: 'monster',
id: 401,
mode: 12,
})
expect(fixture.world.getUnit('monster', 401)?.mode).toBe(12)
// 3. Guaranteed Rune Drop: Countess drops Ral Rune (code 'r08') on ground at (52, 61)
fixture.applyServerEvents({
type: 'ItemEvent',
action: 0,
container: 'ground',
item: makeParityItem({
id: 888,
code: 'r08',
container: 'ground',
x: 52,
y: 61,
}),
})
const groundItemUnit = fixture.world.getUnit('item', 888)
expect(groundItemUnit).toBeDefined()
expect(groundItemUnit?.itemCode).toBe('r08')
// 4. Pickup Ral Rune from ground to inventory slot (2, 3)
fixture.applyServerEvents({
type: 'ItemEvent',
action: 0,
container: 'inventory',
item: makeParityItem({
id: 888,
code: 'r08',
container: 'inventory',
gridX: 2,
gridY: 3,
invWidth: 1,
invHeight: 1,
}),
})
expect(fixture.world.containers.inventory.has(888)).toBe(true)
const runeItem = fixture.world.containers.inventory.get(888)
expect(runeItem?.gridX).toBe(2)
expect(runeItem?.gridY).toBe(3)
// Bridge to UI inventory model
const uiRune = onlineItemDataToUiInventoryItem(runeItem!)
expect(uiRune.id).toBe('888')
expect(uiRune.code).toBe('r08')
expect(uiRune.invWidth).toBe(1)
expect(uiRune.invHeight).toBe(1)
})
})
// ─────────────────────────────────────────────────────────────────────────
// Scenario 4: Tristram Rescue
// ─────────────────────────────────────────────────────────────────────────
describe('Scenario 4: Tristram Rescue', () => {
it('traverses Red Portal to Tristram, kites Griswold, frees Deckard Cain, and advances quest', () => {
const fixture = createParitySessionFixture({ selfId: 104, charClass: 4 }) // Barbarian
// 1. In Stony Field (Area 4), player activates Cairn Stones Red Portal to Tristram (Area 38)
fixture.applyServerEvents({
type: 'LoadAct',
act: 0,
mapSeed: 55112,
areaId: 38,
})
expect(fixture.world.areaId).toBe(38)
// Tristram is in ruins and counts as active wilderness, not safe town
expect(isTownLevel(38)).toBe(false)
expect(fixture.selfUnit?.mode).toBe(1)
// 2. Encounter Griswold (SuperUnique classId 156)
fixture.applyServerEvents({
type: 'UnitAssign',
isSelf: false,
unit: {
unitType: 'monster',
id: 502,
classId: 156, // Griswold
x: 45,
y: 45,
mode: 1,
},
})
const griswold = fixture.world.getUnit('monster', 502)
expect(griswold).toBeDefined()
expect(griswold?.classId).toBe(156)
// Player kites Griswold by running away across 10 simulation frames
fixture.applyServerEvents({
type: 'UnitMove',
unitType: 'player',
id: 104,
to: { x: 75, y: 75 },
mode: 'run',
})
expect(fixture.selfUnit?.mode).toBe(3) // Run mode
for (let t = 0; t < 10; t++) {
fixture.tick(1)
}
expect(fixture.world.frameCount).toBe(10)
// 3. Freeing Deckard Cain from Gibbet Cage (Object 503)
fixture.clearCommands()
// Player interacts with Cain's cage via QuestMessage
fixture.hudModel.questMessage(503, 0)
expect(fixture.sentCommands).toContainEqual({
type: 'QuestMessage',
npcId: 503,
messageId: 0,
})
// Deckard Cain spawns and enters portal back to Rogue Encampment
fixture.applyServerEvents({
type: 'UnitAssign',
isSelf: false,
unit: {
unitType: 'monster',
id: 504,
classId: 146, // Deckard Cain
x: 40,
y: 40,
mode: 1,
},
})
expect(fixture.world.getUnit('monster', 504)).toBeDefined()
// Quest 2 (The Search for Cain) state updates to completed
// Quest 2 bitmask set to 0x0001 (complete)
fixture.applyServerEvents({
type: 'QuestState',
quests: new Uint16Array([0x1001, 0, 0x0001]),
})
expect((fixture.world.quests[2]! & 0x0001) !== 0).toBe(true)
})
})
// ─────────────────────────────────────────────────────────────────────────
// Scenario 5: Act Boss Kill: Andariel
// ─────────────────────────────────────────────────────────────────────────
describe('Scenario 5: Act Boss Kill: Andariel', () => {
it('infiltrates Catacombs L4, survives Poison Spray, slays Andariel, and transitions to Act 2 Lut Gholein', () => {
const fixture = createParitySessionFixture({ selfId: 105, charClass: 1 }) // Sorceress
// 1. Enter Catacombs Level 4 (Area 39)
fixture.applyServerEvents({
type: 'LoadAct',
act: 0,
mapSeed: 66330,
areaId: 39,
})
expect(fixture.world.areaId).toBe(39)
expect(isTownLevel(39)).toBe(false)
// Reveal Andariel's blood pool chamber
fixture.applyServerEvents({
type: 'RoomReveal',
areaId: 39,
tileX: 30,
tileY: 30,
})
expect(fixture.world.getRevealedAreaIds()).toContain(39)
// 2. Andariel Spawns (classId 156 / Andariel boss)
fixture.applyServerEvents({
type: 'UnitAssign',
isSelf: false,
unit: {
unitType: 'monster',
id: 601,
classId: 156, // Andariel
x: 100,
y: 100,
mode: 1,
},
})
const andariel = fixture.world.getUnit('monster', 601)
expect(andariel).toBeDefined()
// 3. Poison Spray Missile Trajectory
// Andariel fires poison spray / nova
const poisonMissile = getMissileTxtData('poisonnova')
expect(poisonMissile).toBeDefined()
expect(poisonMissile?.vel).toBeGreaterThan(0)
expect(poisonMissile?.celFile).toBe('poisonNova')
// Poison hits player: life drains to 80
fixture.applyServerEvents({
type: 'LifeMana',
life: 80,
mana: 100,
stamina: 100,
})
expect(fixture.world.self.life).toBe(80)
// 4. Andariel Weakness: In 1.13c Normal difficulty, Andariel has -50% Fire Resistance
// Sorceress casts Fire Ball: effective damage is 150% of base damage
const baseFireDamage = 100
const andarielFireRes = -50
const effectiveDamage = Math.round(baseFireDamage * (1 - andarielFireRes / 100))
expect(effectiveDamage).toBe(150)
// 5. Killing Blow & Quest Completion: Andariel Dies
fixture.applyServerEvents({
type: 'UnitMode',
unitType: 'monster',
id: 601,
mode: 12,
})
expect(fixture.world.getUnit('monster', 601)?.mode).toBe(12)
// Flame death overlay triggers
const deathOverlay = getOverlayTxtData('fire_cast_1')
expect(deathOverlay).toBeDefined()
expect(deathOverlay?.frames).toBeGreaterThan(0)
// Sisters to the Slaughter (Quest 5) completes
fixture.applyServerEvents({
type: 'QuestState',
quests: new Uint16Array([0x1001, 0, 0x0001, 0, 0, 0x0001]),
})
expect((fixture.world.quests[5]! & 0x0001) !== 0).toBe(true)
// 6. Act 2 Transition: Warriv caravan takes hero East to Lut Gholein (Act 1 in 0-index, Area 40)
fixture.applyServerEvents({
type: 'LoadAct',
act: 1, // Act 2
mapSeed: 66330,
areaId: 40, // Lut Gholein
})
expect(fixture.world.act).toBe(1)
expect(fixture.world.areaId).toBe(40)
expect(isTownLevel(40)).toBe(true) // Lut Gholein is Town!
// In town, player returns to safe town posture mode 5
expect(fixture.selfUnit?.mode).toBe(5)
})
})
})