From 86ca6feb66784d8a2856b1c3320b839a326c458f Mon Sep 17 00:00:00 2001 From: Andrio Celos Date: Thu, 13 Jul 2023 10:31:34 +1000 Subject: [PATCH] Refine game page hints --- TableturfBattleClient/src/HintLabel.ts | 75 +++++++++++++++++ TableturfBattleClient/src/Pages/GamePage.ts | 66 ++++++--------- TableturfBattleClient/tableturf.css | 93 ++++++++++++++------- 3 files changed, 161 insertions(+), 73 deletions(-) create mode 100644 TableturfBattleClient/src/HintLabel.ts diff --git a/TableturfBattleClient/src/HintLabel.ts b/TableturfBattleClient/src/HintLabel.ts new file mode 100644 index 0000000..b061b7e --- /dev/null +++ b/TableturfBattleClient/src/HintLabel.ts @@ -0,0 +1,75 @@ +class HintLabel { + private static readonly CLASS_NAME_ERROR = 'hintError'; + private static readonly CLASS_NAME_SPECIAL = 'hintSpecial'; + private static readonly CLASS_NAME_REPEATED = 'repeated'; + + readonly element: HTMLElement; + + private _isSpecial = false; + private _errorTimeout: number | null = null; + private _baseHtml: string | null = null; + + constructor(element: HTMLElement) { + this.element = element; + } + + static fromId(id: string) { return new HintLabel(document.getElementById(id)!); } + + private clearError() { + if (this._errorTimeout) { + clearTimeout(this._errorTimeout); + this._errorTimeout = null; + } + } + + show(html: string, isSpecial: boolean) { + this._baseHtml = html; + this._isSpecial = isSpecial; + this.clearError(); + this.element.classList.remove(HintLabel.CLASS_NAME_ERROR); + this.element.classList.remove(HintLabel.CLASS_NAME_REPEATED); + if (isSpecial) + this.element.classList.add(HintLabel.CLASS_NAME_SPECIAL); + else + this.element.classList.remove(HintLabel.CLASS_NAME_SPECIAL); + this.element.innerHTML = html; + this.element.hidden = false; + } + + clear() { + this._baseHtml = null; + this._isSpecial = false; + clearChildren(this.element); + this.clearError(); + this.element.hidden = true; + this.element.classList.remove(HintLabel.CLASS_NAME_ERROR); + this.element.classList.remove(HintLabel.CLASS_NAME_REPEATED); + this.element.classList.remove(HintLabel.CLASS_NAME_SPECIAL); + } + + showError(html: string) { + if (this._errorTimeout != null && html == this.element.innerHTML) { + this.element.classList.add(HintLabel.CLASS_NAME_REPEATED); + resetAnimation(this.element); + } else + this.element.classList.remove(HintLabel.CLASS_NAME_REPEATED); + this.clearError(); + this.element.classList.remove(HintLabel.CLASS_NAME_SPECIAL); + this.element.classList.add(HintLabel.CLASS_NAME_ERROR); + this.element.innerHTML = html; + this.element.hidden = false; + this._errorTimeout = setTimeout((() => { + this._errorTimeout = null; + this.element.classList.remove(HintLabel.CLASS_NAME_ERROR); + this.element.classList.remove(HintLabel.CLASS_NAME_REPEATED); + if (this._baseHtml) { + if (this._isSpecial) + this.element.classList.add(HintLabel.CLASS_NAME_SPECIAL); + this.element.innerHTML = this._baseHtml; + } else { + this.element.hidden = true; + clearChildren(this.element); + } + }).bind(this), 5000); + } +} diff --git a/TableturfBattleClient/src/Pages/GamePage.ts b/TableturfBattleClient/src/Pages/GamePage.ts index 5ef03e4..b1cef6f 100644 --- a/TableturfBattleClient/src/Pages/GamePage.ts +++ b/TableturfBattleClient/src/Pages/GamePage.ts @@ -7,7 +7,6 @@ const timeLabel = new TimeLabel(document.getElementById('timeLabel')!); const passButton = CheckButton.fromId('passButton'); const specialButton = CheckButton.fromId('specialButton'); -const passHint = document.getElementById('passHint')!; const gameButtonsContainer = document.getElementById('gameButtonsContainer')!; const rotateLeftButton = document.getElementById('rotateLeftButton') as HTMLButtonElement; const rotateRightButton = document.getElementById('rotateRightButton') as HTMLButtonElement; @@ -25,8 +24,8 @@ const replayPreviousButton = document.getElementById('replayPreviousButton')!; const flipButton = document.getElementById('flipButton') as HTMLButtonElement; let replayAnimationAbortController: AbortController | null = null; -const playHint = document.getElementById('playHint')!; -let playHintTimeout: number | null = null; +const cardHint = HintLabel.fromId('cardHint'); +const playHint = HintLabel.fromId('playHint'); const shareReplayLinkButton = document.getElementById('shareReplayLinkButton') as HTMLButtonElement; let canShareReplay = false; @@ -57,6 +56,8 @@ const testDeckCardButtons: CardButton[] = [ ]; const testPlacements: { card: Card, placementResults: PlacementResults }[] = [ ]; const testCardListBackdrop = document.getElementById('testCardListBackdrop')!; +let playHintHtml: string | null = null; + let testMode = false; let canPlay = false; @@ -362,7 +363,7 @@ function addTestCard(card: Card) { function testCardButton_click(button: CardButton) { if (!button.enabled) { - showPlayHintError('This card has already been played.'); + cardHint.showError('This card has already been played.'); return; } board.autoHighlight = true; @@ -447,30 +448,6 @@ function updateStats(playerIndex: number, scores: number[]) { playerBars[playerIndex].statPassesElement.innerText = currentGame.players[playerIndex].passes.toString(); } -function clearPlayHint() { - playHint.hidden = true; - if (playHintTimeout) { - clearTimeout(playHintTimeout); - playHintTimeout = null; - } -} -function showPlayHint(html: string) { - clearPlayHint(); - playHint.innerHTML = html; - playHint.className = ''; - playHint.hidden = false; -} -function showPlayHintError(html: string) { - playHint.className = !playHint.hidden && html == playHint.innerHTML ? 'playError repeated' : 'playError'; - if (playHintTimeout) { - clearTimeout(playHintTimeout!); - resetAnimation(playHint); - } - playHint.innerHTML = html; - playHint.hidden = false; - playHintTimeout = setTimeout(() => playHint.hidden = true, 5000); -} - /** Shows the ready indication for the specified player. */ function showReady(playerIndex: number) { const el = document.createElement('div'); @@ -521,8 +498,8 @@ function lockGamePage() { for (const button of handButtons.buttons) button.enabled = false; passButton.enabled = false; specialButton.enabled = false; - passHint.hidden = true; - clearPlayHint(); + cardHint.clear(); + playHint.clear(); } async function playInkAnimations(data: { @@ -550,7 +527,10 @@ async function playInkAnimations(data: { // Skip the delay when cards don't overlap. if (placement.spacesAffected.find(p => inkPlaced.has(p.space.y * 37 + p.space.x))) { inkPlaced.clear(); - await delay(1000, abortSignal); + await delay(500, abortSignal); + board.clearInkAnimations(); + await delay(500, abortSignal); + board.enableInkAnimations(); } for (const p of placement.spacesAffected) { @@ -676,12 +656,13 @@ function updateHandAndDeck(playerData: PlayerData) { button.buttonElement.addEventListener('click', e => { if (!button.enabled) { if (specialButton.checked && currentGame!.players[currentGame!.me!.playerIndex].specialPoints < card.specialCost) - showPlayHintError('Not enough special points.'); + cardHint.showError('Not enough special points.'); else if (!(specialButton.checked ? canPlayCardAsSpecialAttack : canPlayCard)[i]) - showPlayHintError('No place to play this card.'); + cardHint.showError('No place to play this card.'); e.preventDefault(); return; } + cardHint.clear(); if (passButton.checked) { if (canPlay) { timeLabel.faded = true; @@ -696,6 +677,8 @@ function updateHandAndDeck(playerData: PlayerData) { board.highlightX = board.startSpaces[board.playerIndex!].x - (board.flip ? 4 : 3); board.highlightY = board.startSpaces[board.playerIndex!].y - (board.flip ? 4 : 3); } + if (specialButton.checked) + playHint.show('Unleash it next to
 
!', true); board.cardRotation = board.flip ? 2 : 0; board.refreshHighlight(); board.table.focus(); @@ -792,9 +775,9 @@ function passButton_click(e: Event) { } passButton.checked = !passButton.checked; board.autoHighlight = !passButton.checked; - passHint.hidden = !passButton.checked; if (passButton.checked) { - clearPlayHint(); + cardHint.show('Pick a card to discard.', false); + playHint.clear(); specialButton.checked = false; board.cardPlaying = null; playerBars[currentGame!.me!.playerIndex].highlightSpecialPoints = 0; @@ -804,6 +787,7 @@ function passButton_click(e: Event) { for (const button of handButtons.buttons) button.enabled = true; } else { + cardHint.clear(); for (let i = 0; i < 4; i++) { handButtons.entries[i].button.enabled = canPlayCard[i]; } @@ -814,9 +798,9 @@ passButton.buttonElement.addEventListener('click', passButton_click); function specialButton_click(e: Event) { if (!specialButton.enabled) { if (currentGame!.me!.hand!.every(c => currentGame!.players[currentGame!.me!.playerIndex].specialPoints < c.specialCost)) - showPlayHintError('Not enough special points.'); + cardHint.showError('Not enough special points.'); else if (!canPlayCardAsSpecialAttack.includes(true)) - showPlayHintError('No place to play a special attack.'); + cardHint.showError('No place to play a special attack.'); e.preventDefault(); return; } @@ -824,9 +808,9 @@ function specialButton_click(e: Event) { board.specialAttack = specialButton.checked; handButtons.deselect(); playerBars[currentGame!.me!.playerIndex].highlightSpecialPoints = 0; + playHint.clear(); if (specialButton.checked) { - passHint.hidden = true; - showPlayHint('Unleash it next to
 
!'); + cardHint.show('Pick a card to do a Special Attack!', true); passButton.checked = false; board.autoHighlight = true; board.cardPlaying = null; @@ -834,7 +818,7 @@ function specialButton_click(e: Event) { for (let i = 0; i < 4; i++) handButtons.entries[i].button.enabled = canPlayCardAsSpecialAttack[i]; } else { - clearPlayHint(); + cardHint.clear(); for (let i = 0; i < 4; i++) { handButtons.entries[i].button.enabled = canPlayCard[i]; } @@ -867,7 +851,7 @@ board.onsubmit = (x, y) => { return; const message = board.checkMoveLegality(currentGame.me.playerIndex, board.cardPlaying, x, y, board.cardRotation, board.specialAttack); if (message != null) { - showPlayHintError(message); + playHint.showError(message); return; } if (testMode) { diff --git a/TableturfBattleClient/tableturf.css b/TableturfBattleClient/tableturf.css index 1c6c90f..9429734 100644 --- a/TableturfBattleClient/tableturf.css +++ b/TableturfBattleClient/tableturf.css @@ -621,14 +621,6 @@ dialog::backdrop { text-align: center; } -#passHint:not([hidden]) { - position: absolute; - margin: 0 0 35em 1em; - padding: 0.5em 1em; - background: #80808080; - border-radius: 0.5em; -} - #playRow:not([hidden]) { display: grid; grid-template-columns: 1fr 1fr; @@ -790,27 +782,18 @@ dialog::backdrop { margin: 0; } -#playHint { - grid-column: -3 / -1; grid-row: -2 / -1; align-self: end; - justify-self: end; - margin: 0 3em 1.5em 0; - font-size: larger; - padding: 0 0.5em; - background: #00000080; - border-radius: 0.5em; +.hintError { + animation: 1s infinite hintError; } -#playHint.playError { - animation: 1s infinite playHintError; +.hintError.repeated { + animation: 1s infinite hintError, 0.5s linear 1 hintShake; } -#playHint.playError.repeated { - animation: 1s infinite playHintError, 0.5s linear 1 playHintShake; -} -@keyframes playHintError { +@keyframes hintError { from { background: #ff0000ff; } 50% { background: #ff000080; } to { background: #ff0000ff; } } -@keyframes playHintShake { +@keyframes hintShake { 0% { transform: translate(1px, 1px); } 10% { transform: translate(-1px, -2px); } 20% { transform: translate(-3px, 0px); } @@ -824,6 +807,49 @@ dialog::backdrop { 100% { transform: translate(0px, 0px); } } +.hintSpecial:before { + content: ''; + position: absolute; + left: -24em; + top: -31em; + right: -24em; + bottom: -31em; + transform: scaleY(0.05) rotateZ(0deg); + z-index: -1; + background: radial-gradient(grey, transparent 50%), conic-gradient(hsl(0, 100%, 75%), hsl(45, 100%, 75%), hsl(90, 100%, 75%), hsl(135, 100%, 75%), hsl(180, 100%, 75%), hsl(225, 100%, 75%), hsl(270, 100%, 75%), hsl(315, 100%, 75%), hsl(360, 100%, 75%)); + animation: 3s linear infinite hintSpecial; +} + +@keyframes hintSpecial { + to { transform: scaleY(0.05) rotateZ(360deg); } +} + +#cardHint:not([hidden]) { + position: absolute; + left: -1em; + width: 16em; + margin: 0 0 30em 0em; + padding: 0 1em 0 3em; + font-size: larger; + background: #80808080; + border-radius: 0.5em; + z-index: 0; + overflow: hidden; +} +#playHint:not([hidden]) { + position: relative; + grid-column: -3 / -1; grid-row: -2 / -1; align-self: end; + justify-self: end; + width: 16em; + margin: 0 -1em 1.5em 0; + padding: 0 3em 0 1.5em; + font-size: larger; + background: #00000080; + border-radius: 0.5em; + z-index: 1; + overflow: hidden; +} + #testPlacementList { display: none; } @@ -1138,7 +1164,7 @@ dialog::backdrop { text-align: center; font-size: larger; border-radius: 0.5em; - animation: 3s specialAttackAnimation; + animation: 3s forwards specialAttackAnimation; } @keyframes specialAttackAnimation { @@ -1443,9 +1469,18 @@ dialog::backdrop { min-height: 12em; } - #passHint:not([hidden]) { - margin: 0 0 16em 0; + #cardHint:not([hidden]) { + left: auto; align-self: center; + margin: 0 0 16em 0; + padding: 0; + text-align: center; + } + + #playHint:not([hidden]) { + grid-column: 1 / -1; + grid-row: 2; + margin-bottom: 0; } #scoreSection:not([hidden]) { @@ -1520,12 +1555,6 @@ dialog::backdrop { margin-top: 5em; } - #playHint { - grid-column: 1 / -1; - grid-row: 2; - margin: 0 1em 0 0; - } - /* Deck editor - Mobile layout */ :is(#deckListPage, #deckEditPage):not([hidden]) {