jasonlooked #1

Merged
eros merged 21 commits from jasonlooked into main 2026-07-11 10:13:09 -07:00
2 changed files with 89 additions and 49 deletions
Showing only changes of commit 56a7d5abba - Show all commits

View File

@ -13,6 +13,10 @@ export function startGame(): void {
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
@ -31,30 +35,17 @@ export function startGame(): void {
const shareLink = `${window.location.origin}/?room=${room}`
hud.setShareLink(shareLink)
const picker = document.createElement('div')
picker.id = 'team-picker'
picker.innerHTML = `
<div class="team-picker-box">
<h2>Choose your team</h2>
<div class="team-picker-buttons">
<button class="team-btn team-red" data-team="red">Play as Red</button>
<button class="team-btn team-yellow" data-team="yellow">Play as Yellow</button>
</div>
</div>
`
app.appendChild(picker)
const model = new GameModel()
picker.querySelectorAll<HTMLButtonElement>('.team-btn').forEach((btn) => {
btn.addEventListener('click', () => {
const team = btn.dataset.team as Team
picker.remove()
connect(room, team, callbacks)
})
})
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)
@ -63,6 +54,11 @@ export function startGame(): void {
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())
@ -85,8 +81,7 @@ export function startGame(): void {
}
const callbacks: NetCallbacks = {
onJoined: (_roomId, team) => {
model.setMyTeam(team)
onJoined: () => {
updateControls()
},
onWaiting: () => {
@ -97,8 +92,8 @@ export function startGame(): void {
model.updateGameState(msg)
updateControls()
},
onTrajectory: (path) => {
model.startTrajectory(path, model.state.turnTeam)
onTrajectory: (paths) => {
model.startTrajectory(paths)
updateControls()
},
onEndScored: (end, points, scoringTeam) => {
@ -142,22 +137,51 @@ export function startGame(): void {
return world
}
const isInHouse = (world: { x: number; y: number }) => {
const dx = world.x - HOUSE_CENTER.x
const dy = world.y - HOUSE_CENTER.y
return Math.sqrt(dx * dx + dy * dy) <= HOUSE_RADIUS
}
let lastPanScreenY = 0
const handleStart = (e: Event) => {
if (!model.isMyTurn) return
model.isDragging = true
const pos = getPos(e as TouchEvent)
model.broom = constrainBroom(renderer.screenToWorld(pos.x, pos.y))
const world = renderer.screenToWorld(pos.x, pos.y)
if (model.isMyTurn && isInHouse(world)) {
model.isDragging = true
model.broom = constrainBroom(world)
} else {
model.isPanning = true
lastPanScreenY = pos.y
}
}
const handleMove = (e: Event) => {
if (!model.isDragging || !model.isMyTurn) return
if (model.isDragging && model.isMyTurn) {
e.preventDefault()
const pos = getPos(e as TouchEvent)
model.broom = constrainBroom(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)
Outdated
Review

physics parameters (curl, friction). should be a shared game state.

physics parameters (curl, friction). should be a shared game state.
// 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 })
@ -170,9 +194,17 @@ export function startGame(): void {
throwBtn.addEventListener('click', () => {
if (!model.isMyTurn) return
sendThrow(model.broom.x, model.broom.y, velocity.getWeight(), curls.getSelected(), friction.getFriction())
sendThrow(
hud.teamSelect.value as Team,
model.broom.x,
model.broom.y,
velocity.getWeight(),
curls.getSelected(),
friction.getFriction(),
)
})
connect(room, callbacks)
updateControls()
render()
}

View File

@ -3,17 +3,18 @@ import { velocityToWeight, weightToVelocity } from './game-helpers'
export interface Hud {
root: HTMLDivElement
teamSelect: HTMLSelectElement
velocityControl: HTMLDivElement
curlSelector: HTMLDivElement
frictionControl: HTMLDivElement
throwButton: HTMLButtonElement
setTeam: (team: Team) => void
update: (state: {
phase: Phase
end: number
scores: number[]
hammer: Team
turnTeam: Team
myTeam: Team | null
animating: boolean
}) => void
showToast: (message: string) => void
@ -48,12 +49,20 @@ export function createHud(): Hud {
const root = document.createElement('div')
root.id = 'hud'
root.innerHTML = `
<div id="hud-top-group">
<div class="hud-row" id="share-row">
<div id="share"><button>Copy share link</button></div>
</div>
<div class="hud-row">
<div id="score">Red 0 - Yellow 0</div>
<div id="end-info">End 1 · Waiting</div>
<div id="team">You: -</div>
<select id="team-select" aria-label="Team">
<option value="red">Red</option>
<option value="yellow">Yellow</option>
</select>
<div id="hammer">Hammer: -</div>
</div>
</div>
<div class="hud-row" style="align-items:flex-end;">
<div id="velocity-control"></div>
<div id="curl-selector"></div>
@ -66,34 +75,32 @@ export function createHud(): Hud {
<button id="throw-btn" disabled>THROW</button>
</div>
</div>
<div id="waiting">Waiting for other player</div>
<div id="share"><button>Copy share link</button></div>
<div id="waiting">Waiting</div>
`
const scoreEl = root.querySelector<HTMLDivElement>('#score')!
const endInfoEl = root.querySelector<HTMLDivElement>('#end-info')!
const teamEl = root.querySelector<HTMLDivElement>('#team')!
const teamSelect = root.querySelector<HTMLSelectElement>('#team-select')!
const hammerEl = root.querySelector<HTMLDivElement>('#hammer')!
const waitingEl = root.querySelector<HTMLDivElement>('#waiting')!
return {
root,
teamSelect,
velocityControl: root.querySelector<HTMLDivElement>('#velocity-control')!,
curlSelector: root.querySelector<HTMLDivElement>('#curl-selector')!,
frictionControl: root.querySelector<HTMLDivElement>('#friction-control')!,
throwButton: root.querySelector<HTMLButtonElement>('#throw-btn')!,
setTeam: (team) => {
teamSelect.value = team
},
update: (state) => {
scoreEl.textContent = `Red ${state.scores[0] ?? 0} - Yellow ${state.scores[1] ?? 0}`
const teamNames: Record<Team, string> = { red: 'Red', yellow: 'Yellow' }
const phaseText = state.phase === 'playing' ? `${teamNames[state.turnTeam]}'s turn` : state.phase.replace(/_/g, ' ')
endInfoEl.textContent = `End ${state.end} · ${phaseText}`
teamEl.textContent = state.myTeam ? `You: ${teamNames[state.myTeam]}` : 'You: -'
hammerEl.textContent = `Hammer: ${teamNames[state.hammer]}`
const waiting =
state.phase === 'waiting' ||
(state.phase === 'playing' && state.myTeam !== null && state.turnTeam !== state.myTeam) ||
state.animating
waitingEl.classList.toggle('visible', state.phase !== 'game_complete' && waiting)
waitingEl.classList.toggle('visible', state.phase !== 'game_complete' && state.animating)
},
showToast: (message: string) => {
const toast = document.createElement('div')
@ -179,14 +186,15 @@ export function createCurlSelector(
): { getSelected: () => number; setEnabled: (enabled: boolean) => void } {
const state = { selected: 1, enabled: true }
const options = [
{ value: -1, label: '↶ Left' },
{ value: 1, label: 'Right ↷' },
{ value: -1, label: '↷', ariaLabel: 'Left curl' },
{ value: 1, label: '↶', ariaLabel: 'Right curl' },
]
container.innerHTML = ''
for (const opt of options) {
const btn = document.createElement('button')
btn.className = 'curl-btn'
btn.textContent = opt.label
btn.ariaLabel = opt.ariaLabel
btn.dataset.curl = String(opt.value)
btn.addEventListener('click', () => {
if (!state.enabled) return