diff --git a/TableturfBattleClient/index.html b/TableturfBattleClient/index.html
index 0c4350e..1c34925 100644
--- a/TableturfBattleClient/index.html
+++ b/TableturfBattleClient/index.html
@@ -241,6 +241,7 @@
+
Select a card to discard.
diff --git a/TableturfBattleClient/src/Board.ts b/TableturfBattleClient/src/Board.ts
index 347782d..803ef8d 100644
--- a/TableturfBattleClient/src/Board.ts
+++ b/TableturfBattleClient/src/Board.ts
@@ -124,14 +124,14 @@ class Board {
var x2 = x + dx;
var y2 = y + dy;
if (x2 < 0 || x2 >= this.grid.length || y2 < 0 || y2 >= this.grid[x2].length)
- return "It cannot overlap a wall or out of bounds."; // Out of bounds.
+ return 'It cannot be over a wall.'; // Out of bounds.
const space = this.grid[x2][y2];
if (space == Space.Wall || space == Space.OutOfBounds)
- return "It cannot overlap a wall or out of bounds.";
+ return 'It cannot be over a wall.';
if (space >= Space.SpecialInactive1)
- return "It cannot overlap a special space."
+ return `It cannot be over${'
'.repeat(Math.max(currentGame?.players?.length ?? 0, 2))}.`;
if (space != Space.Empty && !isSpecialAttack)
- return "It cannot overlap existing ink without a special attack.";
+ return 'It cannot be over inked spaces.';
if (!isAnchored) {
// A normal play must be adjacent to ink of the player's colour.
// A special attack must be adjacent to a special space of theirs.
@@ -152,7 +152,7 @@ class Board {
}
}
}
- return isAnchored ? null : (isSpecialAttack ? "It must be next to one of your special spaces." : "It must be next to your turf.");
+ return isAnchored ? null : (isSpecialAttack ? 'It\'s not next to
.' : 'It\'s not touching your colour of ink.');
}
refreshHighlight() {
diff --git a/TableturfBattleClient/src/Pages/GamePage.ts b/TableturfBattleClient/src/Pages/GamePage.ts
index d82d88e..0d76646 100644
--- a/TableturfBattleClient/src/Pages/GamePage.ts
+++ b/TableturfBattleClient/src/Pages/GamePage.ts
@@ -6,6 +6,7 @@ const handButtons: CardButton[] = [ ];
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;
@@ -22,6 +23,9 @@ 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 shareReplayLinkButton = document.getElementById('shareReplayLinkButton') as HTMLButtonElement;
let canShareReplay = false;
@@ -449,6 +453,30 @@ 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');
@@ -499,6 +527,8 @@ function lockGamePage() {
for (const el of handButtons) el.enabled = false;
passButton.enabled = false;
specialButton.enabled = false;
+ passHint.hidden = true;
+ clearPlayHint();
}
async function playInkAnimations(data: {
@@ -756,7 +786,9 @@ function redrawButton_click(e: MouseEvent) {
function passButton_input() {
board.autoHighlight = !passButton.checked;
+ passHint.hidden = !passButton.checked;
if (passButton.checked) {
+ clearPlayHint();
specialButton.checked = false;
board.cardPlaying = null;
board.specialAttack = false;
@@ -776,6 +808,8 @@ passButton.input.addEventListener('input', passButton_input);
function specialButton_input() {
board.specialAttack = specialButton.checked;
if (specialButton.checked) {
+ passHint.hidden = true;
+ showPlayHint('Unleash it next to
!');
passButton.checked = false;
board.autoHighlight = true;
for (let i = 0; i < 4; i++) {
@@ -784,6 +818,7 @@ function specialButton_input() {
handButtons[i].checked = false;
}
} else {
+ clearPlayHint();
for (let i = 0; i < 4; i++) {
handButtons[i].enabled = canPlayCard[i];
if (!canPlayCard[i])
@@ -817,7 +852,7 @@ board.onsubmit = (x, y) => {
return;
const message = board.checkMoveLegality(currentGame.me.playerIndex, board.cardPlaying, x, y, board.cardRotation, board.specialAttack);
if (message != null) {
- alert(message);
+ showPlayHintError(message);
return;
}
if (testMode) {
diff --git a/TableturfBattleClient/src/TimeLabel.ts b/TableturfBattleClient/src/TimeLabel.ts
index 310c2d3..15827dc 100644
--- a/TableturfBattleClient/src/TimeLabel.ts
+++ b/TableturfBattleClient/src/TimeLabel.ts
@@ -76,14 +76,8 @@ class TimeLabel {
private updateWarningState() {
if (!this.faded && this.timeLeft != null && this.timeLeft <= 10) {
this.contentElement.classList.add('warning');
- this.resetAnimation();
+ resetAnimation(this.contentElement);
} else
this.contentElement.classList.remove('warning');
}
-
- private resetAnimation() {
- this.contentElement.style.animation = 'none';
- this.contentElement.offsetHeight; // Trigger a reflow.
- this.contentElement.style.animation = '';
- }
}
diff --git a/TableturfBattleClient/src/app.ts b/TableturfBattleClient/src/app.ts
index d3e02e1..cd39891 100644
--- a/TableturfBattleClient/src/app.ts
+++ b/TableturfBattleClient/src/app.ts
@@ -628,6 +628,12 @@ function clearChildren(el: Element) {
while (el2 = el.firstChild)
el.removeChild(el2);
}
+function resetAnimation(el: HTMLElement) {
+ el.style.animation = 'none';
+ el.offsetHeight; // Trigger a reflow.
+ el.style.animation = '';
+}
+
document.getElementById('noJSPage')!.innerText = 'Loading client...';
diff --git a/TableturfBattleClient/tableturf.css b/TableturfBattleClient/tableturf.css
index 40aeb0c..58fa65b 100644
--- a/TableturfBattleClient/tableturf.css
+++ b/TableturfBattleClient/tableturf.css
@@ -353,14 +353,25 @@ dialog::backdrop {
row-gap: 4px;
}
-.cardSpecialPoint {
+.cardSpecialPoint, .playHintSpecial {
display: inline-block;
color: transparent;
background: var(--player-special-colour);
width: 1ch;
height: 1ch;
+ vertical-align: middle;
+}
+.cardSpecialPoint {
margin-right: 0.3em;
}
+.playHintSpecial {
+ width: 1.5ch;
+ height: 1.5ch;
+}
+.playHintSpecial:nth-of-type(1):not(:last-of-type) { background: var(--special-colour-1); }
+.playHintSpecial:nth-of-type(2) { background: var(--special-colour-2); }
+.playHintSpecial:nth-of-type(3) { background: var(--special-colour-3); }
+.playHintSpecial:nth-of-type(4) { background: var(--special-colour-4); }
.card::before {
/* If it exists */
@@ -553,6 +564,14 @@ 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;
@@ -706,7 +725,7 @@ label[for="flipBox"] {
bottom: 0;
}
-.playContainer { text-align: center; }
+.playContainer { text-align: center; margin: 8em 0; }
.playContainer[data-index="2"] { grid-column: play-column-2; grid-row: 2 / -1; align-self: end; }
.playContainer[data-index="1"] { grid-column: play-column-2; grid-row: 1 / 4; align-self: start; }
@@ -722,6 +741,40 @@ label[for="flipBox"] {
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;
+}
+#playHint.playError {
+ animation: 1s infinite playHintError;
+}
+#playHint.playError.repeated {
+ animation: 1s infinite playHintError, 0.5s linear 1 playHintShake;
+}
+@keyframes playHintError {
+ from { background: #ff0000ff; }
+ 50% { background: #ff000080; }
+ to { background: #ff0000ff; }
+}
+@keyframes playHintShake {
+ 0% { transform: translate(1px, 1px); }
+ 10% { transform: translate(-1px, -2px); }
+ 20% { transform: translate(-3px, 0px); }
+ 30% { transform: translate(3px, 2px); }
+ 40% { transform: translate(1px, -1px); }
+ 50% { transform: translate(-1px, 2px); }
+ 60% { transform: translate(-3px, 1px); }
+ 70% { transform: translate(3px, 1px); }
+ 80% { transform: translate(-1px, -1px); }
+ 90% { transform: translate(1px, 2px); }
+ 100% { transform: translate(0px, 0px); }
+}
+
#testPlacementList {
display: none;
}
@@ -1331,6 +1384,10 @@ label[for="flipBox"] {
min-height: 12em;
}
+ #passHint:not([hidden]) {
+ margin: 0;
+ }
+
#scoreSection:not([hidden]) {
grid-column: 1;
grid-row: 2;
@@ -1338,7 +1395,7 @@ label[for="flipBox"] {
grid-template-columns: 1fr;
}
- #turnNumberContainer {
+ #timeSection {
grid-column: 1;
grid-row: 2;
margin: 0;
@@ -1404,6 +1461,12 @@ label[for="flipBox"] {
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]) {