jasonlooked #1
@ -13,6 +13,10 @@ export function startGame(): void {
|
|||||||
const hud = createHud()
|
const hud = createHud()
|
||||||
app.appendChild(hud.root)
|
app.appendChild(hud.root)
|
||||||
|
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
;(window as unknown as { __renderer: typeof renderer }).__renderer = renderer
|
||||||
|
}
|
||||||
|
|
||||||
const velocityContainer = hud.velocityControl
|
const velocityContainer = hud.velocityControl
|
||||||
const curlContainer = hud.curlSelector
|
const curlContainer = hud.curlSelector
|
||||||
const frictionContainer = hud.frictionControl
|
const frictionContainer = hud.frictionControl
|
||||||
@ -31,30 +35,17 @@ export function startGame(): void {
|
|||||||
const shareLink = `${window.location.origin}/?room=${room}`
|
const shareLink = `${window.location.origin}/?room=${room}`
|
||||||
hud.setShareLink(shareLink)
|
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()
|
const model = new GameModel()
|
||||||
|
|
||||||
picker.querySelectorAll<HTMLButtonElement>('.team-btn').forEach((btn) => {
|
const initialTeam: Team = localStorage.getItem('curltastic-team') === 'yellow' ? 'yellow' : 'red'
|
||||||
btn.addEventListener('click', () => {
|
model.setMyTeam(initialTeam)
|
||||||
const team = btn.dataset.team as Team
|
hud.setTeam(initialTeam)
|
||||||
picker.remove()
|
|
||||||
connect(room, team, callbacks)
|
// connect() is invoked after callbacks is defined below.
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const updateControls = () => {
|
const updateControls = () => {
|
||||||
|
const activeTeam = hud.teamSelect.value as Team
|
||||||
|
model.setMyTeam(activeTeam)
|
||||||
const myTurn = model.isMyTurn
|
const myTurn = model.isMyTurn
|
||||||
velocity.setEnabled(myTurn)
|
velocity.setEnabled(myTurn)
|
||||||
curls.setEnabled(myTurn)
|
curls.setEnabled(myTurn)
|
||||||
@ -63,6 +54,11 @@ export function startGame(): void {
|
|||||||
hud.update(model.state)
|
hud.update(model.state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hud.teamSelect.addEventListener('change', () => {
|
||||||
|
localStorage.setItem('curltastic-team', hud.teamSelect.value)
|
||||||
|
updateControls()
|
||||||
|
})
|
||||||
|
|
||||||
const render = () => {
|
const render = () => {
|
||||||
const wasAnimating = model.state.animating
|
const wasAnimating = model.state.animating
|
||||||
const activeStonePos = model.tick(performance.now())
|
const activeStonePos = model.tick(performance.now())
|
||||||
@ -85,8 +81,7 @@ export function startGame(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const callbacks: NetCallbacks = {
|
const callbacks: NetCallbacks = {
|
||||||
onJoined: (_roomId, team) => {
|
onJoined: () => {
|
||||||
model.setMyTeam(team)
|
|
||||||
updateControls()
|
updateControls()
|
||||||
},
|
},
|
||||||
onWaiting: () => {
|
onWaiting: () => {
|
||||||
@ -97,8 +92,8 @@ export function startGame(): void {
|
|||||||
model.updateGameState(msg)
|
model.updateGameState(msg)
|
||||||
updateControls()
|
updateControls()
|
||||||
},
|
},
|
||||||
onTrajectory: (path) => {
|
onTrajectory: (paths) => {
|
||||||
model.startTrajectory(path, model.state.turnTeam)
|
model.startTrajectory(paths)
|
||||||
updateControls()
|
updateControls()
|
||||||
},
|
},
|
||||||
onEndScored: (end, points, scoringTeam) => {
|
onEndScored: (end, points, scoringTeam) => {
|
||||||
@ -142,22 +137,51 @@ export function startGame(): void {
|
|||||||
return world
|
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) => {
|
const handleStart = (e: Event) => {
|
||||||
if (!model.isMyTurn) return
|
|
||||||
model.isDragging = true
|
|
||||||
const pos = getPos(e as TouchEvent)
|
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) => {
|
const handleMove = (e: Event) => {
|
||||||
if (!model.isDragging || !model.isMyTurn) return
|
if (model.isDragging && model.isMyTurn) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const pos = getPos(e as TouchEvent)
|
const pos = getPos(e as TouchEvent)
|
||||||
model.broom = constrainBroom(renderer.screenToWorld(pos.x, pos.y))
|
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 = () => {
|
const handleEnd = () => {
|
||||||
model.isDragging = false
|
model.isDragging = false
|
||||||
|
model.isPanning = false
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas.addEventListener('touchstart', handleStart, { passive: false })
|
canvas.addEventListener('touchstart', handleStart, { passive: false })
|
||||||
@ -170,9 +194,17 @@ export function startGame(): void {
|
|||||||
|
|
||||||
throwBtn.addEventListener('click', () => {
|
throwBtn.addEventListener('click', () => {
|
||||||
if (!model.isMyTurn) return
|
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()
|
updateControls()
|
||||||
render()
|
render()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,17 +3,18 @@ import { velocityToWeight, weightToVelocity } from './game-helpers'
|
|||||||
|
|
||||||
export interface Hud {
|
export interface Hud {
|
||||||
root: HTMLDivElement
|
root: HTMLDivElement
|
||||||
|
teamSelect: HTMLSelectElement
|
||||||
velocityControl: HTMLDivElement
|
velocityControl: HTMLDivElement
|
||||||
curlSelector: HTMLDivElement
|
curlSelector: HTMLDivElement
|
||||||
frictionControl: HTMLDivElement
|
frictionControl: HTMLDivElement
|
||||||
throwButton: HTMLButtonElement
|
throwButton: HTMLButtonElement
|
||||||
|
setTeam: (team: Team) => void
|
||||||
update: (state: {
|
update: (state: {
|
||||||
phase: Phase
|
phase: Phase
|
||||||
end: number
|
end: number
|
||||||
scores: number[]
|
scores: number[]
|
||||||
hammer: Team
|
hammer: Team
|
||||||
turnTeam: Team
|
turnTeam: Team
|
||||||
myTeam: Team | null
|
|
||||||
animating: boolean
|
animating: boolean
|
||||||
}) => void
|
}) => void
|
||||||
showToast: (message: string) => void
|
showToast: (message: string) => void
|
||||||
@ -48,12 +49,20 @@ export function createHud(): Hud {
|
|||||||
const root = document.createElement('div')
|
const root = document.createElement('div')
|
||||||
root.id = 'hud'
|
root.id = 'hud'
|
||||||
root.innerHTML = `
|
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 class="hud-row">
|
||||||
<div id="score">Red 0 - Yellow 0</div>
|
<div id="score">Red 0 - Yellow 0</div>
|
||||||
<div id="end-info">End 1 · Waiting</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 id="hammer">Hammer: -</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="hud-row" style="align-items:flex-end;">
|
<div class="hud-row" style="align-items:flex-end;">
|
||||||
<div id="velocity-control"></div>
|
<div id="velocity-control"></div>
|
||||||
<div id="curl-selector"></div>
|
<div id="curl-selector"></div>
|
||||||
@ -66,34 +75,32 @@ export function createHud(): Hud {
|
|||||||
<button id="throw-btn" disabled>THROW</button>
|
<button id="throw-btn" disabled>THROW</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="waiting">Waiting for other player</div>
|
<div id="waiting">Waiting</div>
|
||||||
<div id="share"><button>Copy share link</button></div>
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const scoreEl = root.querySelector<HTMLDivElement>('#score')!
|
const scoreEl = root.querySelector<HTMLDivElement>('#score')!
|
||||||
const endInfoEl = root.querySelector<HTMLDivElement>('#end-info')!
|
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 hammerEl = root.querySelector<HTMLDivElement>('#hammer')!
|
||||||
const waitingEl = root.querySelector<HTMLDivElement>('#waiting')!
|
const waitingEl = root.querySelector<HTMLDivElement>('#waiting')!
|
||||||
|
|
||||||
return {
|
return {
|
||||||
root,
|
root,
|
||||||
|
teamSelect,
|
||||||
velocityControl: root.querySelector<HTMLDivElement>('#velocity-control')!,
|
velocityControl: root.querySelector<HTMLDivElement>('#velocity-control')!,
|
||||||
curlSelector: root.querySelector<HTMLDivElement>('#curl-selector')!,
|
curlSelector: root.querySelector<HTMLDivElement>('#curl-selector')!,
|
||||||
frictionControl: root.querySelector<HTMLDivElement>('#friction-control')!,
|
frictionControl: root.querySelector<HTMLDivElement>('#friction-control')!,
|
||||||
throwButton: root.querySelector<HTMLButtonElement>('#throw-btn')!,
|
throwButton: root.querySelector<HTMLButtonElement>('#throw-btn')!,
|
||||||
|
setTeam: (team) => {
|
||||||
|
teamSelect.value = team
|
||||||
|
},
|
||||||
update: (state) => {
|
update: (state) => {
|
||||||
scoreEl.textContent = `Red ${state.scores[0] ?? 0} - Yellow ${state.scores[1] ?? 0}`
|
scoreEl.textContent = `Red ${state.scores[0] ?? 0} - Yellow ${state.scores[1] ?? 0}`
|
||||||
const teamNames: Record<Team, string> = { red: 'Red', yellow: 'Yellow' }
|
const teamNames: Record<Team, string> = { red: 'Red', yellow: 'Yellow' }
|
||||||
const phaseText = state.phase === 'playing' ? `${teamNames[state.turnTeam]}'s turn` : state.phase.replace(/_/g, ' ')
|
const phaseText = state.phase === 'playing' ? `${teamNames[state.turnTeam]}'s turn` : state.phase.replace(/_/g, ' ')
|
||||||
endInfoEl.textContent = `End ${state.end} · ${phaseText}`
|
endInfoEl.textContent = `End ${state.end} · ${phaseText}`
|
||||||
teamEl.textContent = state.myTeam ? `You: ${teamNames[state.myTeam]}` : 'You: -'
|
|
||||||
hammerEl.textContent = `Hammer: ${teamNames[state.hammer]}`
|
hammerEl.textContent = `Hammer: ${teamNames[state.hammer]}`
|
||||||
const waiting =
|
waitingEl.classList.toggle('visible', state.phase !== 'game_complete' && state.animating)
|
||||||
state.phase === 'waiting' ||
|
|
||||||
(state.phase === 'playing' && state.myTeam !== null && state.turnTeam !== state.myTeam) ||
|
|
||||||
state.animating
|
|
||||||
waitingEl.classList.toggle('visible', state.phase !== 'game_complete' && waiting)
|
|
||||||
},
|
},
|
||||||
showToast: (message: string) => {
|
showToast: (message: string) => {
|
||||||
const toast = document.createElement('div')
|
const toast = document.createElement('div')
|
||||||
@ -179,14 +186,15 @@ export function createCurlSelector(
|
|||||||
): { getSelected: () => number; setEnabled: (enabled: boolean) => void } {
|
): { getSelected: () => number; setEnabled: (enabled: boolean) => void } {
|
||||||
const state = { selected: 1, enabled: true }
|
const state = { selected: 1, enabled: true }
|
||||||
const options = [
|
const options = [
|
||||||
{ value: -1, label: '↶ Left' },
|
{ value: -1, label: '↷', ariaLabel: 'Left curl' },
|
||||||
{ value: 1, label: 'Right ↷' },
|
{ value: 1, label: '↶', ariaLabel: 'Right curl' },
|
||||||
]
|
]
|
||||||
container.innerHTML = ''
|
container.innerHTML = ''
|
||||||
for (const opt of options) {
|
for (const opt of options) {
|
||||||
const btn = document.createElement('button')
|
const btn = document.createElement('button')
|
||||||
btn.className = 'curl-btn'
|
btn.className = 'curl-btn'
|
||||||
btn.textContent = opt.label
|
btn.textContent = opt.label
|
||||||
|
btn.ariaLabel = opt.ariaLabel
|
||||||
btn.dataset.curl = String(opt.value)
|
btn.dataset.curl = String(opt.value)
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
if (!state.enabled) return
|
if (!state.enabled) return
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user