pr2-features #2
@ -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) => {
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user