jasonlooked #1

Merged
eros merged 21 commits from jasonlooked into main 2026-07-11 10:13:09 -07:00
Showing only changes of commit c28bf32eb8 - Show all commits

View File

@ -1,4 +1,4 @@
import { HOUSE_CENTER, type DrawableStone, type Phase, type ServerGameStateMessage, type StoneState, type Team } from './protocol' import { HOG_LINE_Y, HOUSE_CENTER, type DrawableStone, type Phase, type ServerGameStateMessage, type ServerStoneTrajectory, type StoneState, type Team } from './protocol'
import { trimPathToStartAtHogLine } from './game-helpers' import { trimPathToStartAtHogLine } from './game-helpers'
export interface GameModelState { export interface GameModelState {
@ -27,10 +27,10 @@ export class GameModel {
pendingStones: StoneState[] = [] pendingStones: StoneState[] = []
broom: { x: number; y: number } = { x: 0, y: HOUSE_CENTER.y } broom: { x: number; y: number } = { x: 0, y: HOUSE_CENTER.y }
isDragging = false isDragging = false
isPanning = false
private activePath: [number, number, number][] = [] private activePaths = new Map<number, [number, number, number][]>()
private animationStartTime = 0 private animationStartTime = 0
private animationTeam: Team = 'red'
setMyTeam(team: Team): void { setMyTeam(team: Team): void {
this.state.myTeam = team this.state.myTeam = team
@ -63,36 +63,104 @@ export class GameModel {
} }
} }
startTrajectory(path: [number, number, number][], team: Team): void { startTrajectory(paths: ServerStoneTrajectory[]): void {
this.activePath = trimPathToStartAtHogLine(path) const existingIds = new Set(this.state.stones.map((s) => s.id))
this.state.animating = this.activePath.length > 1
let thrownId: number | null = null
for (const { stone_id } of paths) {
if (!existingIds.has(stone_id)) {
thrownId = stone_id
break
}
}
Outdated
Review

again, all stones should be animating at the synced time step

again, all stones should be animating at the synced time step
if (thrownId === null && paths.length > 0) {
thrownId = paths[0].stone_id
}
let tRef = 0
if (thrownId !== null) {
const thrownPath = paths.find((p) => p.stone_id === thrownId)?.path ?? []
if (thrownPath.length >= 2) {
const idx = thrownPath.findIndex(([, y]) => y >= HOG_LINE_Y)
if (idx >= 0) {
const start = Math.max(0, idx - 1)
tRef = thrownPath[start][2]
}
}
}
const pathMap = new Map<number, [number, number, number][]>()
for (const { stone_id, path } of paths) {
if (stone_id === thrownId) {
pathMap.set(stone_id, trimPathToStartAtHogLine(path))
} else {
const shifted = path
.map(([x, y, t]) => [x, y, t - tRef] as [number, number, number])
.filter(([, , t]) => t >= 0)
pathMap.set(stone_id, shifted)
}
}
Outdated
Review

the rotation (theta) should come from the physics engine

the rotation (theta) should come from the physics engine
this.activePaths = pathMap
this.state.animating = Array.from(pathMap.values()).some((p) => p.length > 1)
this.animationStartTime = performance.now() this.animationStartTime = performance.now()
this.animationTeam = team
this.pendingStones = [] this.pendingStones = []
} }
tick(now: number): DrawableStone | null { tick(now: number): DrawableStone[] {
if (!this.state.animating || this.activePath.length <= 1) { if (!this.state.animating) {
return null return []
} }
const elapsed = (now - this.animationStartTime) / 1000 const elapsed = (now - this.animationStartTime) / 1000
const total = this.activePath[this.activePath.length - 1][2] const maxTotal = Math.max(
if (elapsed >= total) { 0,
...Array.from(this.activePaths.values()).map((p) => (p.length > 0 ? p[p.length - 1][2] : 0)),
)
if (elapsed >= maxTotal) {
this.state.animating = false this.state.animating = false
if (this.pendingStones.length > 0) { if (this.pendingStones.length > 0) {
this.state.stones = this.pendingStones this.state.stones = this.pendingStones
this.pendingStones = [] this.pendingStones = []
} }
return null return []
}
const result: DrawableStone[] = []
for (const [stoneId, path] of this.activePaths) {
const pos = this.interpolatePath(path, elapsed)
if (!pos) continue
const team = this.state.stones.find((s) => s.id === stoneId)?.team ?? this.state.turnTeam
result.push({ ...pos, team })
}
return result
}
private interpolatePath(
path: [number, number, number][],
elapsed: number,
): { x: number; y: number; rotation: number } | null {
if (path.length === 0) return null
if (path.length === 1) {
const [x, y] = path[0]
return { x, y, rotation: 0 }
}
if (elapsed >= path[path.length - 1][2]) {
const last = path[path.length - 1]
const prev = path[path.length - 2]
const dx = last[0] - prev[0]
const dy = last[1] - prev[1]
return { x: last[0], y: last[1], rotation: Math.atan2(dy, dx) * 2 }
} }
let i = 0 let i = 0
while (i + 1 < this.activePath.length && this.activePath[i + 1][2] < elapsed) i++ while (i + 1 < path.length && path[i + 1][2] < elapsed) i++
const p0 = this.activePath[i] const p0 = path[i]
const p1 = this.activePath[i + 1] ?? p0 const p1 = path[i + 1] ?? p0
const t0 = this.activePath[Math.max(i - 1, 0)] const t0 = path[Math.max(i - 1, 0)]
const t2 = this.activePath[Math.min(i + 2, this.activePath.length - 1)] const t2 = path[Math.min(i + 2, path.length - 1)]
const dt = p1[2] - p0[2] const dt = p1[2] - p0[2]
const t = dt > 0 ? (elapsed - p0[2]) / dt : 0 const t = dt > 0 ? (elapsed - p0[2]) / dt : 0
const x = p0[0] + (p1[0] - p0[0]) * t const x = p0[0] + (p1[0] - p0[0]) * t
@ -100,7 +168,7 @@ export class GameModel {
const dx = t2[0] - t0[0] const dx = t2[0] - t0[0]
const dy = t2[1] - t0[1] const dy = t2[1] - t0[1]
const rotation = Math.atan2(dy, dx) * 2 const rotation = Math.atan2(dy, dx) * 2
return { x, y, rotation, team: this.animationTeam } return { x, y, rotation }
} }
get isMyTurn(): boolean { get isMyTurn(): boolean {