pr2-features #2

Merged
eros merged 9 commits from pr2-features into main 2026-07-11 13:15:51 -07:00
3 changed files with 216 additions and 0 deletions
Showing only changes of commit 95d04f8d9e - Show all commits

View File

@ -36,6 +36,7 @@ export function startGame(): void {
hud.setShareLink(shareLink)
const model = new GameModel()
let lastScoreboardLen = model.state.scoreboard.length
const stored = localStorage.getItem('curltastic-team')
const initialTeam: Team = stored === 'team2' || stored === 'yellow' ? 'team2' : 'team1'
@ -55,6 +56,24 @@ export function startGame(): void {
hud.update(model.state)
}
const maybeShowEndModal = () => {
const board = model.state.scoreboard
if (board.length <= lastScoreboardLen) {
lastScoreboardLen = board.length
return
}
const last = board[board.length - 1]
lastScoreboardLen = board.length
if (!last) return
hud.showEndModal({
end: last.end,
team1: last.team1,
team2: last.team2,
nextHammer: model.state.hammer,
scoreboard: board,
})
}
hud.teamSelect.addEventListener('change', () => {
localStorage.setItem('curltastic-team', hud.teamSelect.value)
updateControls()
@ -91,6 +110,7 @@ export function startGame(): void {
},
onGameState: (msg) => {
model.updateGameState(msg)
maybeShowEndModal()
updateControls()
},
onTrajectories: (stones) => {

View File

@ -26,6 +26,13 @@ export interface Hud {
stonesRemaining: number[]
scoreboard: EndScore[]
}) => void
showEndModal: (payload: {
end: number
team1: number
team2: number
nextHammer: Team
scoreboard: EndScore[]
}) => void
showToast: (message: string) => void
setShareLink: (link: string) => void
}
@ -66,6 +73,21 @@ function buildStoneChipsHtml(team: Team): string {
return `<div class="stones-row stones-row--${team}" data-team="${team}" role="img" aria-label="${TEAM_LABELS[team]} stones remaining">${chips}</div>`
}
function renderScoreboardTable(scoreboard: EndScore[]): string {
if (scoreboard.length === 0) {
return '<p class="end-modal-empty">No ends scored yet</p>'
}
const rows = scoreboard
.map(
(e) =>
`<tr><td>${e.end}</td><td>${e.team1}</td><td>${e.team2}</td><td>${TEAM_LABELS[e.hammer]}</td></tr>`,
)
.join('')
return `<table class="scoreboard-table" aria-label="Scoreboard">
<thead><tr><th>End</th><th>Team 1</th><th>Team 2</th><th>Hammer</th></tr></thead>
<tbody>${rows}</tbody>
</table>`
}
export function createHud(): Hud {
const root = document.createElement('div')
@ -113,6 +135,20 @@ export function createHud(): Hud {
const stonesHud = root.querySelector<HTMLDivElement>('#stones-hud')!
const scoreboardStrip = root.querySelector<HTMLDivElement>('#scoreboard-strip')!
let endModalEl: HTMLDivElement | null = null
let endModalTimer = 0
const dismissEndModal = () => {
if (endModalTimer) {
window.clearTimeout(endModalTimer)
endModalTimer = 0
}
if (endModalEl) {
endModalEl.remove()
endModalEl = null
}
}
const updateStonesRemaining = (remaining: number[]) => {
const teams: Team[] = ['team1', 'team2']
for (let t = 0; t < teams.length; t++) {
@ -169,6 +205,35 @@ export function createHud(): Hud {
updateStonesRemaining(state.stonesRemaining)
updateScoreboardStrip(state.scoreboard, state.scores)
},
showEndModal: (payload) => {
dismissEndModal()
const modal = document.createElement('div')
modal.id = 'end-modal'
modal.setAttribute('role', 'dialog')
modal.setAttribute('aria-modal', 'true')
modal.setAttribute('aria-labelledby', 'end-modal-title')
modal.innerHTML = `
<div class="end-modal-backdrop" data-dismiss="1"></div>
<div class="end-modal-card">
<h2 id="end-modal-title">End ${payload.end} complete</h2>
<p class="end-modal-points">
<span class="end-modal-team end-modal-team--team1">Team 1 <strong>${payload.team1}</strong></span>
<span class="end-modal-vs">vs</span>
<span class="end-modal-team end-modal-team--team2">Team 2 <strong>${payload.team2}</strong></span>
</p>
<p class="end-modal-hammer">Next hammer: <strong>${TEAM_LABELS[payload.nextHammer]}</strong></p>
<div class="end-modal-board">${renderScoreboardTable(payload.scoreboard)}</div>
<button type="button" class="end-modal-dismiss" data-dismiss="1">Dismiss</button>
</div>
`
modal.addEventListener('click', (e) => {
const t = e.target as HTMLElement
if (t.closest('[data-dismiss]')) dismissEndModal()
})
document.body.appendChild(modal)
endModalEl = modal
endModalTimer = window.setTimeout(dismissEndModal, 5000)
},
showToast: (message: string) => {
const toast = document.createElement('div')
toast.id = 'toast'

View File

@ -167,6 +167,130 @@ html, body {
padding: 2px 6px;
}
/* —— End-of-end modal —— */
#end-modal {
position: fixed;
inset: 0;
z-index: 50;
display: flex;
align-items: center;
justify-content: center;
pointer-events: auto;
}
.end-modal-backdrop {
position: absolute;
inset: 0;
background: rgba(5, 14, 28, 0.72);
backdrop-filter: blur(3px);
}
.end-modal-card {
position: relative;
z-index: 1;
width: min(360px, calc(100vw - 32px));
max-height: min(80vh, 520px);
overflow: auto;
padding: 20px 18px 16px;
border-radius: 18px;
background: linear-gradient(160deg, rgba(28, 74, 122, 0.96), rgba(11, 31, 58, 0.98));
border: 1px solid rgba(255, 255, 255, 0.22);
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.12);
text-align: center;
}
.end-modal-card h2 {
margin: 0 0 12px;
font-size: 20px;
font-weight: 700;
letter-spacing: 0.02em;
}
.end-modal-points {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin: 0 0 8px;
font-size: 15px;
}
.end-modal-team strong {
font-size: 22px;
margin-left: 4px;
}
.end-modal-team--team1 strong {
color: #ff6b5c;
}
.end-modal-team--team2 strong {
color: #ffd666;
}
.end-modal-vs {
opacity: 0.55;
font-size: 12px;
text-transform: uppercase;
}
.end-modal-hammer {
margin: 0 0 14px;
font-size: 13px;
opacity: 0.95;
}
.end-modal-board {
margin-bottom: 14px;
}
.end-modal-empty {
margin: 0;
font-size: 13px;
opacity: 0.7;
}
.scoreboard-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.scoreboard-table th,
.scoreboard-table td {
padding: 6px 4px;
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
}
.scoreboard-table th {
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 10px;
opacity: 0.75;
}
.end-modal-dismiss {
appearance: none;
border: 1px solid rgba(255, 255, 255, 0.35);
background: rgba(255, 255, 255, 0.14);
color: #fff;
font-weight: 700;
font-size: 13px;
padding: 8px 18px;
border-radius: 12px;
cursor: pointer;
pointer-events: auto;
}
.end-modal-dismiss:hover {
background: rgba(255, 255, 255, 0.22);
}
.end-modal-dismiss:focus-visible {
outline: 2px solid #00aaff;
outline-offset: 2px;
}
#share {
display: flex;
@ -351,6 +475,13 @@ html, body {
gap: 4px;
}
.end-modal-card {
padding: 16px 14px 12px;
}
.end-modal-card h2 {
font-size: 17px;
}
#velocity-control {
min-width: 0;