mirror of
https://github.com/AndrioCelos/TableturfBattleApp.git
synced 2026-08-20 18:04:00 -05:00
Refine game page hints
This commit is contained in:
75
TableturfBattleClient/src/HintLabel.ts
Normal file
75
TableturfBattleClient/src/HintLabel.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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"> </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"> </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) {
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
Reference in New Issue
Block a user