diff --git a/frontend/src/game.ts b/frontend/src/game.ts
index 831a8cf..ed3044f 100644
--- a/frontend/src/game.ts
+++ b/frontend/src/game.ts
@@ -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) => {
diff --git a/frontend/src/hud.ts b/frontend/src/hud.ts
index ae52990..e85aa9b 100644
--- a/frontend/src/hud.ts
+++ b/frontend/src/hud.ts
@@ -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 `
${chips}
`
}
+function renderScoreboardTable(scoreboard: EndScore[]): string {
+ if (scoreboard.length === 0) {
+ return 'No ends scored yet
'
+ }
+ const rows = scoreboard
+ .map(
+ (e) =>
+ `| ${e.end} | ${e.team1} | ${e.team2} | ${TEAM_LABELS[e.hammer]} |
`,
+ )
+ .join('')
+ return `
+ | End | Team 1 | Team 2 | Hammer |
+ ${rows}
+
`
+}
export function createHud(): Hud {
const root = document.createElement('div')
@@ -113,6 +135,20 @@ export function createHud(): Hud {
const stonesHud = root.querySelector('#stones-hud')!
const scoreboardStrip = root.querySelector('#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 = `
+
+
+
End ${payload.end} complete
+
+ Team 1 ${payload.team1}
+ vs
+ Team 2 ${payload.team2}
+
+
Next hammer: ${TEAM_LABELS[payload.nextHammer]}
+
${renderScoreboardTable(payload.scoreboard)}
+
+
+ `
+ 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'
diff --git a/frontend/src/style.css b/frontend/src/style.css
index 4b5033c..58c9034 100644
--- a/frontend/src/style.css
+++ b/frontend/src/style.css
@@ -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;