import { connect, sendThrow, type NetCallbacks } from './net' import { createRenderer } from './renderer' import { createHud, createVelocitySelector, createCurlSelector, createFrictionSlider } from './hud' import { type Team } from './protocol' import { GameModel } from './game-model' export function startGame(): void { const app = document.querySelector('#app')! app.innerHTML = '' const canvas = app.querySelector('#game-canvas')! const renderer = createRenderer(canvas) const hud = createHud() app.appendChild(hud.root) if (import.meta.env.DEV) { ;(window as unknown as { __renderer: typeof renderer }).__renderer = renderer } const velocityContainer = hud.velocityControl const curlContainer = hud.curlSelector const frictionContainer = hud.frictionControl const throwBtn = hud.throwButton const velocity = createVelocitySelector(velocityContainer, () => {}) const curls = createCurlSelector(curlContainer, () => {}) const friction = createFrictionSlider(frictionContainer) const params = new URLSearchParams(window.location.search) let room = params.get('room') if (!room) { room = generateRoomCode() window.history.replaceState({}, '', `/?room=${room}`) } const shareLink = `${window.location.origin}/?room=${room}` hud.setShareLink(shareLink) const model = new GameModel() const initialTeam: Team = localStorage.getItem('curltastic-team') === 'yellow' ? 'yellow' : 'red' model.setMyTeam(initialTeam) hud.setTeam(initialTeam) // connect() is invoked after callbacks is defined below. const updateControls = () => { const activeTeam = hud.teamSelect.value as Team model.setMyTeam(activeTeam) const myTurn = model.isMyTurn velocity.setEnabled(myTurn) curls.setEnabled(myTurn) friction.setEnabled(myTurn) throwBtn.disabled = !myTurn hud.update(model.state) } hud.teamSelect.addEventListener('change', () => { localStorage.setItem('curltastic-team', hud.teamSelect.value) updateControls() }) const render = () => { const wasAnimating = model.state.animating const activeStonePos = model.tick(performance.now()) if (wasAnimating && !model.state.animating) { updateControls() } renderer.draw({ stones: model.state.stones, broom: model.state.phase === 'playing' && !model.state.animating && model.state.myTeam === model.state.turnTeam ? model.broom : null, animating: model.state.animating, activeStonePos, }) requestAnimationFrame(render) } const callbacks: NetCallbacks = { onJoined: () => { updateControls() }, onWaiting: () => { model.setWaiting() updateControls() }, onGameState: (msg) => { model.updateGameState(msg) updateControls() }, onTrajectory: (paths) => { model.startTrajectory(paths) updateControls() }, onEndScored: (end, points, scoringTeam) => { if (points > 0 && scoringTeam) { hud.showToast(`${scoringTeam.toUpperCase()} scores ${points} in end ${end}`) } else { hud.showToast(`End ${end} scored: blank end`) } }, onGameOver: (scores, winner) => { const msg = winner ? `${winner.toUpperCase()} wins!` : 'Tie game!' hud.showToast(`Game over: ${msg} (${scores[0]}-${scores[1]})`) }, onError: (message) => { hud.showToast(message) }, onClose: () => { // Disconnect => do nothing; keep UI as-is. }, } const getPos = (e: TouchEvent | MouseEvent) => { if ('touches' in e) { const t = e.touches[0] ?? e.changedTouches[0] return { x: t.clientX, y: t.clientY } } return { x: e.clientX, y: e.clientY } } let lastPanScreenY = 0 const handleStart = (e: Event) => { const pos = getPos(e as TouchEvent) const world = renderer.screenToWorld(pos.x, pos.y) if (model.isMyTurn) { model.isDragging = true model.broom = world } else { model.isPanning = true lastPanScreenY = pos.y } } const handleMove = (e: Event) => { if (model.isDragging && model.isMyTurn) { e.preventDefault() const pos = getPos(e as TouchEvent) model.broom = renderer.screenToWorld(pos.x, pos.y) return } if (model.isPanning) { e.preventDefault() const pos = getPos(e as TouchEvent) const prevWorld = renderer.screenToWorld(pos.x, lastPanScreenY) const nowWorld = renderer.screenToWorld(pos.x, pos.y) // Direct-manipulation pan: dragging the sheet down reveals the hog line above. const deltaY = nowWorld.y - prevWorld.y const sensitivity = 1.5 renderer.setViewYOffset(renderer.clampViewYOffset() + deltaY * sensitivity) lastPanScreenY = pos.y } } const handleEnd = () => { model.isDragging = false model.isPanning = false } canvas.addEventListener('touchstart', handleStart, { passive: false }) canvas.addEventListener('touchmove', handleMove, { passive: false }) canvas.addEventListener('touchend', handleEnd) canvas.addEventListener('mousedown', handleStart) canvas.addEventListener('mousemove', handleMove) canvas.addEventListener('mouseup', handleEnd) canvas.addEventListener('mouseleave', handleEnd) throwBtn.addEventListener('click', () => { if (!model.isMyTurn) return sendThrow( hud.teamSelect.value as Team, model.broom.x, model.broom.y, velocity.getWeight(), curls.getSelected(), friction.getFriction(), ) }) connect(room, callbacks) updateControls() render() } function generateRoomCode(): string { const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' let s = '' for (let i = 0; i < 6; i++) { s += chars.charAt(Math.floor(Math.random() * chars.length)) } return s }