import { describe, it, expect } from 'vitest' import { createNetSimulation } from '../src/scene/net-scene.ts' import { createWorld, addPlayer } from '../src/game/combat.ts' import { LockstepSession } from '../src/net/lockstep.ts' describe('net-scene lockstep integration', () => { it('detects inventory desyncs when one peer picks up an item', () => { // Setup identical honest and tampered simulations const worldA = createWorld(0, 0) addPlayer(worldA, 10, 0) // Create worldB identical to worldA const worldB = createWorld(0, 0) addPlayer(worldB, 10, 0) const options = { playerSpeed: 100, playerReach: 20, playerCooldownTicks: 10, playerDamage: 10, playerManaPerAttack: 0, respawnTicks: 10, } const terrain = { overlap: () => 0 } const simA = createNetSimulation(worldA, options, terrain, [0, 100], 2) const simB = createNetSimulation(worldB, options, terrain, [0, 100], 2) // BOTH have an item at the same coordinate. // However, A has Gold, B has a Potion. simA.ground.push({ x: 0, y: 0, item: { name: 'Gold', stack: 100, value: 100, base: {} as unknown as import('../src/game/items.ts').ItemBase, prefix: null, suffix: null, level: 1, invWidth: 1, invHeight: 1, stats: {} } }) simB.ground.push({ x: 0, y: 0, item: { name: 'Potion', stack: 1, value: 5, base: {} as unknown as import('../src/game/items.ts').ItemBase, prefix: null, suffix: null, level: 1, invWidth: 1, invHeight: 1, stats: {} } }) const sessionA = new LockstepSession({ peers: 2, inputDelayTicks: 0 }, simA.simulation) const sessionB = new LockstepSession({ peers: 2, inputDelayTicks: 0 }, simB.simulation) // Tick 0: BOTH pick up! // At the end of the tick, both grounds are empty. But inventories differ. sessionA.submit(0, { tick: 0, movement: {x:0, y:0}, attack: false, pickup: true, talk: false, skill: 0 }) sessionA.submit(1, { tick: 0, movement: {x:0, y:0}, attack: false, pickup: false, talk: false, skill: 0 }) sessionB.submit(0, { tick: 0, movement: {x:0, y:0}, attack: false, pickup: true, talk: false, skill: 0 }) sessionB.submit(1, { tick: 0, movement: {x:0, y:0}, attack: false, pickup: false, talk: false, skill: 0 }) const outA = sessionA.step() const outB = sessionB.step() expect(outA.kind).toBe('stepped') expect(outB.kind).toBe('stepped') if (outA.kind === 'stepped' && outB.kind === 'stepped') { expect(outA.hash).not.toBe(outB.hash) } }) it('produces identical ground drops and lockstep hashes across peers when monsters die (Issue #427)', () => { const worldA = createWorld(0, 0) addPlayer(worldA, 10, 0) const worldB = createWorld(0, 0) addPlayer(worldB, 10, 0) const sharedMonsterSeed = 0x1337beef const zombieStats = { id: 'zombie', name: 'Zombie', hp: 5, damage: 1, speed: 0, reach: 10, aggroRadius: 100, cooldownTicks: 20, xp: 50, level: 5, } worldA.monsters.push({ index: 0, stats: zombieStats, x: 5, y: 0, hp: 5, cooldown: 0, state: 'idle', facing: 0, hitFlash: 0, corpseTicks: 0, dwInitSeed: sharedMonsterSeed, }) worldB.monsters.push({ index: 0, stats: zombieStats, x: 5, y: 0, hp: 5, cooldown: 0, state: 'idle', facing: 0, hitFlash: 0, corpseTicks: 0, dwInitSeed: sharedMonsterSeed, }) const options = { playerSpeed: 100, playerReach: 30, playerCooldownTicks: 5, playerDamage: 25, playerManaPerAttack: 0, respawnTicks: 100, } const terrain = { overlap: () => 0 } const simA = createNetSimulation(worldA, options, terrain, [0, 100], 2, 0xc0ffee) const simB = createNetSimulation(worldB, options, terrain, [0, 100], 2, 0xc0ffee) const sessionA = new LockstepSession({ peers: 2, inputDelayTicks: 0 }, simA.simulation) const sessionB = new LockstepSession({ peers: 2, inputDelayTicks: 0 }, simB.simulation) // Peer 0 attacks and kills the zombie on both peers sessionA.submit(0, { tick: 0, movement: { x: 0, y: 0 }, attack: true, pickup: false, talk: false, skill: 0 }) sessionA.submit(1, { tick: 0, movement: { x: 0, y: 0 }, attack: false, pickup: false, talk: false, skill: 0 }) sessionB.submit(0, { tick: 0, movement: { x: 0, y: 0 }, attack: true, pickup: false, talk: false, skill: 0 }) sessionB.submit(1, { tick: 0, movement: { x: 0, y: 0 }, attack: false, pickup: false, talk: false, skill: 0 }) const stepA = sessionA.step() const stepB = sessionB.step() expect(stepA.kind).toBe('stepped') expect(stepB.kind).toBe('stepped') if (stepA.kind === 'stepped' && stepB.kind === 'stepped') { expect(stepA.hash).toBe(stepB.hash) } expect(simA.ground.length).toBe(simB.ground.length) for (let i = 0; i < simA.ground.length; i++) { expect(simA.ground[i]!.x).toBe(simB.ground[i]!.x) expect(simA.ground[i]!.y).toBe(simB.ground[i]!.y) expect(simA.ground[i]!.item.name).toBe(simB.ground[i]!.item.name) expect(simA.ground[i]!.item.quality).toBe(simB.ground[i]!.item.quality) expect((simA.ground[i]!.item as any).uniqueId).toBe((simB.ground[i]!.item as any).uniqueId) } }) })