mirror of
https://github.com/AndrioCelos/TableturfBattleApp.git
synced 2026-07-13 06:30:57 -05:00
Add ink placement and special space animations
This commit is contained in:
parent
e417b148c6
commit
50529a4eea
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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"> </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) {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user