forked from eros/curltastic
fix(frontend): lock multi-stone parallel trajectory animation
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
2e11f005dc
commit
20c592d8ac
104
frontend/src/game-model.test.ts
Normal file
104
frontend/src/game-model.test.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { GameModel } from './game-model'
|
||||
import type { ServerStoneTrajectory, StoneState } from './protocol'
|
||||
|
||||
function stone(partial: Partial<StoneState> & Pick<StoneState, 'id' | 'team'>): StoneState {
|
||||
return {
|
||||
x: 0,
|
||||
y: 30,
|
||||
rotation: 0,
|
||||
active: true,
|
||||
...partial,
|
||||
}
|
||||
}
|
||||
|
||||
describe('GameModel multi-path trajectory animation', () => {
|
||||
it('returns two DrawableStones in parallel mid-trajectory', () => {
|
||||
const model = new GameModel()
|
||||
model.state.stones = [stone({ id: 1, team: 'red' }), stone({ id: 2, team: 'yellow' })]
|
||||
model.state.turnTeam = 'red'
|
||||
|
||||
const paths: ServerStoneTrajectory[] = [
|
||||
{
|
||||
stone_id: 1,
|
||||
path: [
|
||||
[0, 10, 0],
|
||||
[0, 11, 0.5],
|
||||
[0, 12, 1.0],
|
||||
],
|
||||
},
|
||||
{
|
||||
stone_id: 2,
|
||||
path: [
|
||||
[1, 10, 0],
|
||||
[1, 11, 0.5],
|
||||
[1, 12, 1.0],
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
model.startTrajectory(paths)
|
||||
expect(model.state.animating).toBe(true)
|
||||
|
||||
const mid = performance.now() + 500
|
||||
const drawn = model.tick(mid)
|
||||
|
||||
expect(drawn).toHaveLength(2)
|
||||
expect(drawn.map((d) => d.team).sort()).toEqual(['red', 'yellow'])
|
||||
// Mid-sample y ≈ 11 for both paths (y never reaches hog line → no trim shift)
|
||||
for (const d of drawn) {
|
||||
expect(d.y).toBeCloseTo(11, 0)
|
||||
}
|
||||
})
|
||||
|
||||
it('clears animating when elapsed reaches maxTotal on a short path', () => {
|
||||
const model = new GameModel()
|
||||
model.state.stones = [stone({ id: 1, team: 'red' })]
|
||||
model.state.turnTeam = 'red'
|
||||
|
||||
model.startTrajectory([
|
||||
{
|
||||
stone_id: 1,
|
||||
path: [
|
||||
[0, 10, 0],
|
||||
[0, 10.5, 0.2],
|
||||
[0, 11, 0.4],
|
||||
],
|
||||
},
|
||||
])
|
||||
expect(model.state.animating).toBe(true)
|
||||
|
||||
const afterEnd = performance.now() + 500
|
||||
const drawn = model.tick(afterEnd)
|
||||
|
||||
expect(drawn).toEqual([])
|
||||
expect(model.state.animating).toBe(false)
|
||||
})
|
||||
|
||||
it('trims thrown stone path to hog line when id is not in existing stones', () => {
|
||||
const model = new GameModel()
|
||||
// Only stone 1 is already on the sheet; stone 2 is the newly thrown rock.
|
||||
model.state.stones = [stone({ id: 1, team: 'yellow', x: 0.5, y: 35 })]
|
||||
model.state.turnTeam = 'red'
|
||||
|
||||
const thrownPath: [number, number, number][] = [
|
||||
[0, 2, 0],
|
||||
[0, 20, 1],
|
||||
[0, 21.5, 2],
|
||||
[0, 30, 3],
|
||||
]
|
||||
|
||||
model.startTrajectory([{ stone_id: 2, path: thrownPath }])
|
||||
expect(model.state.animating).toBe(true)
|
||||
|
||||
// Immediately after start: hog-trimmed path begins at y=20 (sample before hog), t=0
|
||||
const atStart = performance.now()
|
||||
const drawn = model.tick(atStart)
|
||||
|
||||
expect(drawn).toHaveLength(1)
|
||||
expect(drawn[0].team).toBe('red') // turnTeam fallback for unknown id
|
||||
expect(drawn[0].y).toBeCloseTo(20, 0)
|
||||
// Must not still be at the hack (y=2)
|
||||
expect(drawn[0].y).toBeGreaterThan(15)
|
||||
})
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user