Refine game page hints

This commit is contained in:
Andrio Celos
2023-07-13 10:31:34 +10:00
parent 812474f38a
commit 86ca6feb66
3 changed files with 161 additions and 73 deletions

View File

@@ -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);
}
}

View File

@@ -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 <div class="playHintSpecial">&nbsp;</div>!', 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 <div class="playHintSpecial">&nbsp;</div>!');
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) {

View File

@@ -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]) {