135 lines
5.2 KiB
TypeScript
135 lines
5.2 KiB
TypeScript
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)
|
|
}
|
|
})
|
|
})
|
|
|