jasonlooked #1
@ -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
|
||||
e.preventDefault()
|
||||
const pos = getPos(e as TouchEvent)
|
||||
model.broom = constrainBroom(renderer.screenToWorld(pos.x, pos.y))
|
||||
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)
|
||||
// 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()
|
||||
}
|
||||
|
||||
@ -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,11 +49,19 @@ export function createHud(): Hud {
|
||||
const root = document.createElement('div')
|
||||
root.id = 'hud'
|
||||
root.innerHTML = `
|
||||
<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>
|
||||
<div id="hammer">Hammer: -</div>
|
||||
<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>
|
||||
<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>
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user