Add ink placement and special space animations

This commit is contained in:
Andrio Celos 2023-07-12 23:03:47 +10:00
parent e417b148c6
commit 50529a4eea
4 changed files with 201 additions and 19 deletions

View File

@ -6,17 +6,19 @@ class Board {
playerIndex: number | null = 0;
cardPlaying: Card | null = null;
cardRotation = 0;
specialAttack = false;
_specialAttack = false;
autoHighlight = true;
flip = false;
highlightX = NaN;
highlightY = NaN;
private highlightedCells: [x: number, y: number][] = [ ];
private highlightedCells: Point[] = [ ];
private animatedCells: Point[] = [ ];
private specialAnimatedCells: [point: Point, elements: HTMLElement[]][] = [ ];
private touch: [index: number, x: number, y: number, highlightX: number, highlightY: number] | null = null;
startSpaces: { x: number, y: number }[] = [ ];
startSpaces: Point[] = [ ];
onsubmit: ((x: number, y: number) => void) | null = null;
oncancel: (() => void) | null = null;
@ -100,6 +102,15 @@ class Board {
});
}
get specialAttack() { return this._specialAttack; }
set specialAttack(value: boolean) {
this._specialAttack = value;
if (value)
this.table.classList.add('specialAttackVisual');
else
this.table.classList.remove('specialAttackVisual');
}
moveHighlight(move: (x: number, y: number, r: number) => [number, number, number], clamp: boolean) {
if (this.playerIndex == null) return;
if (this.highlightedCells.length == 0 || isNaN(this.highlightX) || isNaN(this.highlightY) || isNaN(this.cardRotation)) {
@ -185,7 +196,7 @@ class Board {
cell.classList.add('hoverillegal');
if (space == Space.SpecialInactive1)
cell.classList.add('hoverspecial');
this.highlightedCells.push([x2, y2]);
this.highlightedCells.push({ x: x2, y: y2 });
}
}
}
@ -201,18 +212,82 @@ class Board {
}
private internalClearHighlight() {
for (const [x, y] of this.highlightedCells) {
this.cells[x][y].setAttribute('class', Space[this.grid[x][y]] );
for (const s of this.highlightedCells) {
this.cells[s.x][s.y].setAttribute('class', Space[this.grid[s.x][s.y]] );
}
this.highlightedCells.splice(0);
}
enableInkAnimations() {
this.table.classList.add('enableInkAnimation');
}
showInkAnimation(x: number, y: number) {
this.animatedCells.push({ x, y });
this.cells[x][y].classList.add('inkAnimation');
}
clearInkAnimations() {
for (const s of this.animatedCells)
this.cells[s.x][s.y].classList.remove('inkAnimation');
this.animatedCells.splice(0);
this.table.classList.remove('enableInkAnimation');
}
showSubmitAnimation() {
for (const s of this.highlightedCells)
this.cells[s.x][s.y].classList.add('submitted');
}
private showSpecialAnimation(x: number, y: number) {
if ((this.grid[x][y] & Space.SpecialActive1) != Space.SpecialActive1 || this.specialAnimatedCells.find(el => el[0].x == x && el[0].y == y))
return;
const parent = this.cells[x][y];
const els = [ ];
els.push(this.createSpecialAnimationElement(parent));
els.push(this.createSpecialAnimationElement(parent));
els.push(this.createSpecialAnimationElement(parent));
els.push(this.createSpecialAnimationElement(parent));
els.push(this.createSpecialAnimationElement(parent));
els.push(this.createSpecialAnimationElement(parent));
els.push(this.createSpecialAnimationElement(parent));
els.push(this.createSpecialAnimationElement(parent));
els.push(this.createSpecialAnimationElement(parent));
els.push(this.createSpecialAnimationElement(parent));
this.specialAnimatedCells.push([{ x, y }, els]);
}
private createSpecialAnimationElement(parent: Element) {
const el = document.createElement('div');
parent.appendChild(el);
return el;
}
private clearSpecialAnimation(x: number, y: number) {
const i = this.specialAnimatedCells.findIndex(el => el[0].x == x && el[0].y == y);
if (i < 0) return;
for (const el of this.specialAnimatedCells[i][1])
el.parentElement!.removeChild(el);
this.specialAnimatedCells.splice(i, 1);
}
clearAllSpecialAnimations() {
for (const entry of this.specialAnimatedCells) {
for (const el of entry[1])
el.parentElement!.removeChild(el);
}
this.specialAnimatedCells.splice(0);
}
resize(grid?: Space[][]) {
if (grid) this.grid = grid;
clearChildren(this.table);
this.cells.splice(0);
this.highlightedCells.splice(0);
this.specialAnimatedCells.splice(0);
const boardWidth = this.grid.length;
const boardHeight = this.grid[0].length;
@ -229,7 +304,8 @@ class Board {
col.push(td);
td.addEventListener('click', () => {
if (this.autoHighlight && this.cardPlaying != null && this.onsubmit && !isNaN(this.highlightX) && !isNaN(this.highlightY)) {
this.onsubmit(this.highlightX, this.highlightY);
if (this.onsubmit)
this.onsubmit(this.highlightX, this.highlightY);
}
});
td.addEventListener('contextmenu', e => e.preventDefault());
@ -291,15 +367,21 @@ class Board {
refresh() {
this.clearHighlight();
this.clearInkAnimations();
for (let x = 0; x < this.grid.length; x++) {
for (let y = 0; y < this.grid[x].length; y++) {
this.cells[x][y].setAttribute('class', Space[this.grid[x][y]] );
this.setDisplayedSpace(x, y, this.grid[x][y]);
}
}
}
setDisplayedSpace(x: number, y: number, newState: Space) {
this.cells[x][y].setAttribute('class', Space[newState]);
if (this.cells[x][y].childNodes.length > 0) {
if ((newState & Space.SpecialActive1) != Space.SpecialActive1)
this.clearSpecialAnimation(x, y);
} else
this.showSpecialAnimation(x, y);
}
getScores() {

View File

@ -229,7 +229,7 @@ replayNextButton.addEventListener('click', _ => {
replayAnimationAbortController = new AbortController();
(async () => {
await playInkAnimations({ game: { state: GameState.Ongoing, board: null, turnNumber: currentGame.turnNumber, players: currentGame.players }, placements: result.placements, specialSpacesActivated: result.specialSpacesActivated }, anySpecialAttacks, replayAnimationAbortController.signal);
await playInkAnimations({ game: { state: GameState.Ongoing, board: null, turnNumber: currentGame.turnNumber, players: currentGame.players }, moves, placements: result.placements, specialSpacesActivated: result.specialSpacesActivated }, anySpecialAttacks, replayAnimationAbortController.signal);
turnNumberLabel.setTurnNumber(currentGame.turnNumber);
clearPlayContainers();
if (currentGame.turnNumber > 12) {
@ -527,6 +527,7 @@ function lockGamePage() {
async function playInkAnimations(data: {
game: { state: GameState, board: Space[][] | null, turnNumber: number, players: Player[] },
moves: Move[],
placements: Placement[],
specialSpacesActivated: Point[]
}, anySpecialAttacks: boolean, abortSignal?: AbortSignal) {
@ -535,9 +536,16 @@ async function playInkAnimations(data: {
board.clearHighlight();
board.cardPlaying = null;
board.autoHighlight = false;
board.specialAttack = false;
canPlay = false;
timeLabel.faded = true;
await delay(anySpecialAttacks ? 3000 : 1000, abortSignal);
for (let i = 0; i < data.game.players.length; i++) {
if ((data.moves[i] as PlayMove).isSpecialAttack)
playerBars[i].specialPoints -= data.moves[i].card.specialCost;
playerBars[i].highlightSpecialPoints = 0;
}
board.enableInkAnimations();
for (const placement of placements) {
// Skip the delay when cards don't overlap.
if (placement.spacesAffected.find(p => inkPlaced.has(p.space.y * 37 + p.space.x))) {
@ -548,9 +556,12 @@ async function playInkAnimations(data: {
for (const p of placement.spacesAffected) {
inkPlaced.add(p.space.y * 37 + p.space.x);
board.setDisplayedSpace(p.space.x, p.space.y, p.newState);
board.showInkAnimation(p.space.x, p.space.y);
}
}
await delay(1000, abortSignal);
await delay(500, abortSignal);
board.clearInkAnimations();
await delay(500, abortSignal);
// Show special spaces.
if (data.game.board)
@ -679,6 +690,8 @@ function updateHandAndDeck(playerData: PlayerData) {
}
} else {
board.cardPlaying = card;
if (specialButton.checked)
playerBars[currentGame!.me!.playerIndex].highlightSpecialPoints = card.specialCost;
if (isNaN(board.highlightX) || isNaN(board.highlightY)) {
board.highlightX = board.startSpaces[board.playerIndex!].x - (board.flip ? 4 : 3);
board.highlightY = board.startSpaces[board.playerIndex!].y - (board.flip ? 4 : 3);
@ -784,6 +797,7 @@ function passButton_click(e: Event) {
clearPlayHint();
specialButton.checked = false;
board.cardPlaying = null;
playerBars[currentGame!.me!.playerIndex].highlightSpecialPoints = 0;
board.specialAttack = false;
board.clearHighlight();
handButtons.deselect();
@ -809,6 +823,7 @@ function specialButton_click(e: Event) {
specialButton.checked = !specialButton.checked;
board.specialAttack = specialButton.checked;
handButtons.deselect();
playerBars[currentGame!.me!.playerIndex].highlightSpecialPoints = 0;
if (specialButton.checked) {
passHint.hidden = true;
showPlayHint('Unleash it next to <div class="playHintSpecial">&nbsp;</div>!');
@ -857,9 +872,16 @@ board.onsubmit = (x, y) => {
}
if (testMode) {
const move: PlayMove = { card: board.cardPlaying, isPass: false, isTimeout: false, x, y, rotation: board.cardRotation, isSpecialAttack: false };
const r = board.makePlacements([ move ]);
testPlacements.push({ card: board.cardPlaying, placementResults: r });
board.refresh();
const result = board.makePlacements([ move ]);
testPlacements.push({ card: board.cardPlaying, placementResults: result });
board.enableInkAnimations();
for (const p of result.placements[0].spacesAffected) {
board.setDisplayedSpace(p.space.x, p.space.y, p.newState);
board.showInkAnimation(p.space.x, p.space.y);
}
if (result.specialSpacesActivated.length > 0)
setTimeout(() => board.refresh(), 333);
var li = document.createElement('div');
li.innerText = board.cardPlaying.name;
@ -879,6 +901,7 @@ board.onsubmit = (x, y) => {
board.cardPlaying = null;
testUndoButton.enabled = true;
} else if (canPlay) {
board.showSubmitAnimation();
timeLabel.faded = true;
lockGamePage();
let req = new XMLHttpRequest();
@ -903,6 +926,8 @@ board.onsubmit = (x, y) => {
board.oncancel = () => {
board.cardPlaying = null;
if (currentGame?.me)
playerBars[currentGame.me.playerIndex].highlightSpecialPoints = 0;
board.clearHighlight();
for (const button of handButtons.buttons) {
if (button.checked) {

View File

@ -66,6 +66,23 @@ class PlayerBar {
}
}
set highlightSpecialPoints(value: number) {
const points = this.specialPointsContainer.getElementsByClassName('specialPoint');
for (let i = 0; i < points.length; i++) {
if (i < value) {
points[i].classList.add('specialAnimation');
for (let j = 0; j < 10; j++) {
const el = document.createElement('div');
points[i].appendChild(el);
}
} else {
points[i].classList.remove('specialAnimation');
clearChildren(points[i]);
(points[i] as HTMLElement).innerText = `${i + 1}`;
}
}
}
get pointsTo() { return this.pointsToContainer.hidden ? null : parseInt(this.pointsToElement.innerText); }
set pointsTo(value: number | null) {
if (value == null || value == 0) // A player can never actually have 0 points because they start with a permanent special space.

View File

@ -422,14 +422,60 @@ dialog::backdrop {
#gameBoard td.SpecialInactive2 { background: url('assets/SpecialOverlay.png') center/cover, var(--special-colour-2); }
#gameBoard td.SpecialInactive3 { background: url('assets/SpecialOverlay.png') center/cover, var(--special-colour-3); }
#gameBoard td.SpecialInactive4 { background: url('assets/SpecialOverlay.png') center/cover, var(--special-colour-4); }
#gameBoard td.SpecialActive1 { background: url('assets/SpecialOverlay.png') center/contain, radial-gradient(circle, var(--special-accent-colour-1) 25%, var(--special-colour-1) 75%); }
#gameBoard td.SpecialActive1 { background: url('assets/SpecialOverlay.png') center/cover, radial-gradient(circle, var(--special-accent-colour-1) 25%, var(--special-colour-1) 75%); }
#gameBoard td.SpecialActive2 { background: url('assets/SpecialOverlay.png') center/cover, radial-gradient(circle, var(--special-accent-colour-2) 25%, var(--special-colour-2) 75%); }
#gameBoard td.SpecialActive3 { background: url('assets/SpecialOverlay.png') center/cover, radial-gradient(circle, var(--special-accent-colour-3) 25%, var(--special-colour-3) 75%); }
#gameBoard td.SpecialActive4 { background: url('assets/SpecialOverlay.png') center/cover, radial-gradient(circle, var(--special-accent-colour-4) 25%, var(--special-colour-4) 75%); }
#gameBoard.specialAttackVisual td:is(.Ink1, .Ink2, .Ink3, .Ink4) {
opacity: 0.5;
}
#gameBoard.enableInkAnimation td.inkAnimation { animation: 0.333s inkPlaceAnimation; }
#gameBoard td.submitted { animation: 0.333s inkPlaceAnimation; }
@keyframes inkPlaceAnimation {
from { box-shadow: 0 0 0px 0 #ffffffff; }
to { box-shadow: 0 0 50px 10px #ffffff00; }
}
:is(#gameBoard td, .specialAnimation) > div {
position: absolute;
border-radius: var(--anim-size);
filter: blur(calc(var(--anim-size) * 0.1));
animation: 1.5s linear infinite specialSpaceAnimation;
left: calc(var(--x) * 0.01 * var(--anim-size));
top: calc(var(--y) * 0.01 * var(--anim-size));
width: calc(var(--w) * 0.01 * var(--anim-size));
height: calc(var(--w) * 0.01 * var(--anim-size));
z-index: 1;
}
#gameBoard td > div { --anim-size: min(3.5vh, 3.5vw); }
#gameBoard td.SpecialActive1 > div { background: color-mix(in srgb, var(--special-colour-1), var(--special-accent-colour-1) 75%); }
#gameBoard td.SpecialActive2 > div { background: color-mix(in srgb, var(--special-colour-2), var(--special-accent-colour-2) 75%); }
#gameBoard td.SpecialActive3 > div { background: color-mix(in srgb, var(--special-colour-3), var(--special-accent-colour-3) 75%); }
#gameBoard td.SpecialActive4 > div { background: color-mix(in srgb, var(--special-colour-4), var(--special-accent-colour-4) 75%); }
:is(#gameBoard td, .specialAnimation) > div:nth-of-type( 1) { --x: -12; --y: 29; --w: 36; animation-delay: -900ms; }
:is(#gameBoard td, .specialAnimation) > div:nth-of-type( 2) { --x: -10; --y: 20; --w: 52; animation-delay: -300ms; }
:is(#gameBoard td, .specialAnimation) > div:nth-of-type( 3) { --x: 1; --y: 4; --w: 48; animation-delay: -600ms; }
:is(#gameBoard td, .specialAnimation) > div:nth-of-type( 4) { --x: 12; --y: 7; --w: 46; animation-delay: -750ms; }
:is(#gameBoard td, .specialAnimation) > div:nth-of-type( 5) { --x: 20; --y: -1; --w: 52; animation-delay: -1350ms; }
:is(#gameBoard td, .specialAnimation) > div:nth-of-type( 6) { --x: 29; --y: 3; --w: 52; animation-delay: -150ms; }
:is(#gameBoard td, .specialAnimation) > div:nth-of-type( 7) { --x: 44; --y: 11; --w: 42; animation-delay: -450ms; }
:is(#gameBoard td, .specialAnimation) > div:nth-of-type( 8) { --x: 50; --y: 9; --w: 50; animation-delay: -1200ms; }
:is(#gameBoard td, .specialAnimation) > div:nth-of-type( 9) { --x: 66; --y: 20; --w: 36; animation-delay: 0 ; }
:is(#gameBoard td, .specialAnimation) > div:nth-of-type(10) { --x: 72; --y: 16; --w: 46; animation-delay: -1050ms; }
@keyframes specialSpaceAnimation {
from { transform: translateY(0) scale(0); }
20% { transform: translateY(calc(var(--anim-size) * -0.15)) scale(1); }
60% { transform: translateY(calc(var(--anim-size) * -0.45)) scale(1); }
to { transform: translateY(calc(var(--anim-size) * -0.75)) scale(0); }
}
#gameBoard td {
position: relative;
overflow: hidden;
}
#gameBoard td.hover::after {
@ -443,6 +489,7 @@ dialog::backdrop {
background: repeating-linear-gradient(135deg, var(--hover-colour) 0, var(--hover-colour) 0.6ex, transparent 0.6ex, transparent 1.2ex);
background-attachment: fixed;
}
#gameBoard td.hoverspecial { overflow: hidden; }
#gameBoard td.hoverspecial::after {
left: -50%;
top: -50%;
@ -500,21 +547,25 @@ dialog::backdrop {
.playerBar[data-index="0"] {
--colour: var(--primary-colour-1);
--special-colour: var(--special-colour-1);
--special-accent-colour: var(--special-accent-colour-1);
grid-row: player-row-0;
}
.playerBar[data-index="1"] {
--colour: var(--primary-colour-2);
--special-colour: var(--special-colour-2);
--special-accent-colour: var(--special-accent-colour-2);
grid-row: player-row-1;
}
.playerBar[data-index="2"] {
--colour: var(--primary-colour-3);
--special-colour: var(--special-colour-3);
--special-accent-colour: var(--special-accent-colour-3);
grid-row: player-row-2;
}
.playerBar[data-index="3"] {
--colour: var(--primary-colour-4);
--special-colour: var(--special-colour-4);
--special-accent-colour: var(--special-accent-colour-4);
grid-row: player-row-3;
}
@ -847,15 +898,22 @@ dialog::backdrop {
.specialPoints div {
display: inline-block;
width: 1em;
height: 1em;
border: 1px solid;
width: 1.25em;
height: 1.25em;
text-align: center;
}
.playerBar .specialPoint {
position: relative;
color: transparent;
background: var(--special-colour);
background: url('assets/SpecialOverlay.png') center/cover, var(--special-colour);
}
.playerBar .specialPoint.specialAnimation {
background: url('assets/SpecialOverlay.png') center/cover, radial-gradient(circle, var(--special-accent-colour) 25%, var(--special-colour) 75%);
}
.playerBar .specialPoint.specialAnimation > div {
background: color-mix(in srgb, var(--special-colour), var(--special-accent-colour) 75%);
--anim-size: 1.25em;
}
.specialPoints div {