layout blackjack table
This commit is contained in:
88
client/src/Animation.ts
Normal file
88
client/src/Animation.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import type { Sprite, Ticker, TickerCallback } from "pixi.js";
|
||||
import { CardStatus } from "./Hand";
|
||||
import { getSpriteSheet } from "./spritesheet";
|
||||
|
||||
const spritesheet = await getSpriteSheet();
|
||||
|
||||
export abstract class Animation {
|
||||
protected ticker: Ticker;
|
||||
protected cb: TickerCallback<any>;
|
||||
#next: Animation | null;
|
||||
|
||||
constructor(ticker: Ticker) {
|
||||
console.log("this got called", ticker);
|
||||
if (ticker === undefined) throw new Error("got nothing");
|
||||
this.ticker = ticker;
|
||||
this.cb = () => {};
|
||||
this.#next = null;
|
||||
}
|
||||
|
||||
abstract getCallback(): TickerCallback<any>;
|
||||
|
||||
next(animation: Animation) {
|
||||
this.#next = animation;
|
||||
}
|
||||
|
||||
done() {
|
||||
console.log("removing", this.ticker);
|
||||
this.ticker.remove(this.cb);
|
||||
}
|
||||
}
|
||||
|
||||
export class FlipCard extends Animation {
|
||||
card: Sprite;
|
||||
cardWidth: number;
|
||||
status: CardStatus;
|
||||
flipped: boolean;
|
||||
|
||||
constructor(ticker: Ticker, card: Sprite, status: CardStatus) {
|
||||
super(ticker);
|
||||
|
||||
this.cb = () => {};
|
||||
this.card = card;
|
||||
this.status = status;
|
||||
this.flipped = false;
|
||||
this.cardWidth = card.width;
|
||||
}
|
||||
|
||||
getCallback() {
|
||||
const callback = (ticker: Ticker) => {
|
||||
const to = this.status.isFaceDown ? this.status.face : "unknown";
|
||||
this.status.isFaceDown = !this.status.isFaceDown;
|
||||
|
||||
console.log(this);
|
||||
|
||||
this.card.pivot.set(this.cardWidth / 2, 0);
|
||||
|
||||
// the card has just fipped
|
||||
if (!this.flipped && this.card.width <= 10) {
|
||||
this.flipped = true;
|
||||
this.card.texture = spritesheet.textures[to];
|
||||
}
|
||||
|
||||
// the other side of the card is how showing
|
||||
if (this.flipped) {
|
||||
const change = 20 * ticker.deltaTime;
|
||||
if (this.card.width + change >= this.cardWidth)
|
||||
this.card.width = this.cardWidth;
|
||||
else this.card.width += change;
|
||||
}
|
||||
|
||||
// the original side is still showing
|
||||
else {
|
||||
const change = 20 * ticker.deltaTime;
|
||||
if (this.card.width <= change) this.card.width = 0;
|
||||
else this.card.width -= change;
|
||||
}
|
||||
|
||||
if (this.flipped && this.card.width === this.cardWidth) {
|
||||
console.log("remove", this.flipped, this.card.width, this.cardWidth);
|
||||
super.done();
|
||||
}
|
||||
};
|
||||
|
||||
this.cb = callback.bind(this);
|
||||
|
||||
return this.cb;
|
||||
}
|
||||
}
|
||||
@ -1,25 +1,53 @@
|
||||
import { Assets, Container, Sprite } from "pixi.js";
|
||||
import {
|
||||
Assets,
|
||||
Container,
|
||||
Sprite,
|
||||
Spritesheet,
|
||||
Ticker,
|
||||
type TickerCallback,
|
||||
} from "pixi.js";
|
||||
import { Card } from "./Card";
|
||||
import { CARD_HOVER_DIST, CARD_WIDTH } from "./constants";
|
||||
import { Animation, FlipCard } from "./Animation";
|
||||
|
||||
type Pivot = "left" | "right" | "center";
|
||||
type Layout = "horizontal" | "ascending" | "stacked";
|
||||
|
||||
const spritesheet = await Assets.load("/public/assets/cards.json");
|
||||
const spritesheet = await Assets.load<Spritesheet>("/public/assets/cards.json");
|
||||
export type CardStatus = {
|
||||
face: Card;
|
||||
isAnimating: boolean;
|
||||
isFaceDown: boolean;
|
||||
};
|
||||
|
||||
export class Hand {
|
||||
#cards: Sprite[];
|
||||
#animations: Animation[];
|
||||
#cardSprites: Sprite[];
|
||||
#statuses: CardStatus[];
|
||||
#container: Container;
|
||||
#hover: boolean;
|
||||
#layout: Layout;
|
||||
#maxWidth: number;
|
||||
#pivot: Pivot;
|
||||
#ticker: Ticker | null;
|
||||
|
||||
constructor(maxWidth: number, cards: Card[] = []) {
|
||||
if (maxWidth < CARD_WIDTH) {
|
||||
throw new Error("hand cannot be narrower than a single card");
|
||||
}
|
||||
|
||||
this.#maxWidth = maxWidth;
|
||||
this.#animations = [];
|
||||
this.#container = new Container();
|
||||
this.#hover = false;
|
||||
this.#maxWidth = maxWidth;
|
||||
this.#pivot = "left";
|
||||
this.#layout = "horizontal";
|
||||
this.#statuses = cards.map((card) => ({
|
||||
face: card,
|
||||
isAnimating: false,
|
||||
isFaceDown: false,
|
||||
}));
|
||||
this.#ticker = null;
|
||||
|
||||
if (cards.length > 0) {
|
||||
const sprites: Sprite[] = [];
|
||||
@ -27,33 +55,90 @@ export class Hand {
|
||||
this.add(card);
|
||||
}
|
||||
|
||||
this.#cards = sprites;
|
||||
this.#cardSprites = sprites;
|
||||
this.#container.addChild(...sprites);
|
||||
this.fanCards();
|
||||
} else {
|
||||
this.#cards = [];
|
||||
this.#cardSprites = [];
|
||||
}
|
||||
}
|
||||
|
||||
add(card: Card) {
|
||||
add(card: Card, faceDown = false) {
|
||||
const sprite = new Sprite(spritesheet.textures[card]);
|
||||
sprite.eventMode = "dynamic";
|
||||
sprite.onmouseenter = () => {
|
||||
sprite.y -= CARD_HOVER_DIST;
|
||||
};
|
||||
sprite.onmouseleave = () => {
|
||||
sprite.y += CARD_HOVER_DIST;
|
||||
};
|
||||
this.#cards.push(sprite);
|
||||
|
||||
this.#cardSprites.push(sprite);
|
||||
this.#statuses.push({
|
||||
face: card,
|
||||
isAnimating: false,
|
||||
isFaceDown: faceDown,
|
||||
});
|
||||
this.#container.addChild(sprite);
|
||||
this.#setCardHover(sprite);
|
||||
this.fanCards();
|
||||
this.setPivot(this.#pivot);
|
||||
}
|
||||
|
||||
animationTicker(ticker: Ticker) {
|
||||
this.#ticker = ticker;
|
||||
|
||||
ticker.add((ticker) => {
|
||||
if (this.#animations.length) {
|
||||
const animation = this.#animations.shift()!;
|
||||
ticker.add(animation.getCallback());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
flip(cardIdx: number) {
|
||||
if (cardIdx >= this.#cardSprites.length)
|
||||
throw new Error(`card index out of range: ${cardIdx}`);
|
||||
|
||||
if (!this.#ticker)
|
||||
throw new Error("can't animate hand before passing it a ticker");
|
||||
|
||||
const card = this.#cardSprites[cardIdx];
|
||||
const status = this.#statuses[cardIdx];
|
||||
const ticker = this.#ticker;
|
||||
|
||||
this.#animations.push(new FlipCard(ticker, card, status));
|
||||
}
|
||||
|
||||
#setCardHover(sprite: Sprite) {
|
||||
if (this.#hover) {
|
||||
const { y } = sprite;
|
||||
|
||||
sprite.eventMode = "dynamic";
|
||||
|
||||
sprite.onmouseenter = () => {
|
||||
sprite.y -= CARD_HOVER_DIST;
|
||||
};
|
||||
|
||||
sprite.onmouseleave = () => {
|
||||
sprite.y = y;
|
||||
};
|
||||
} else {
|
||||
sprite.eventMode = "static";
|
||||
}
|
||||
}
|
||||
|
||||
hover(hasHover: boolean) {
|
||||
if (this.#hover === hasHover) return;
|
||||
|
||||
this.#hover = hasHover;
|
||||
|
||||
for (const sprite of this.#cardSprites) {
|
||||
this.#setCardHover(sprite);
|
||||
}
|
||||
}
|
||||
|
||||
setPosition(x: number, y: number) {
|
||||
this.#container.position.set(x, y);
|
||||
}
|
||||
|
||||
setCardLayout(layout: Layout) {
|
||||
this.#layout = layout;
|
||||
}
|
||||
|
||||
setPivot(pivot: "left" | "right" | "center") {
|
||||
switch (pivot) {
|
||||
case "left":
|
||||
@ -73,13 +158,30 @@ export class Hand {
|
||||
}
|
||||
|
||||
fanCards() {
|
||||
const count = this.#cards.length;
|
||||
const count = this.#cardSprites.length;
|
||||
const max = this.#maxWidth;
|
||||
const offset = count * CARD_WIDTH > max ? max / count : CARD_WIDTH;
|
||||
let offset = CARD_WIDTH / 3;
|
||||
|
||||
if (this.#layout === "horizontal") {
|
||||
offset = count * CARD_WIDTH > max ? max / count : CARD_WIDTH;
|
||||
}
|
||||
let x = 0;
|
||||
for (const card of this.#cards) {
|
||||
let y = 0;
|
||||
for (const card of this.#cardSprites) {
|
||||
card.x = x;
|
||||
x += offset;
|
||||
card.y = y;
|
||||
switch (this.#layout) {
|
||||
case "ascending":
|
||||
x += offset;
|
||||
y -= offset;
|
||||
break;
|
||||
case "horizontal":
|
||||
x += offset;
|
||||
break;
|
||||
case "stacked":
|
||||
x += CARD_WIDTH / 20;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,18 +1,19 @@
|
||||
import { Application } from "pixi.js";
|
||||
import {
|
||||
Application,
|
||||
Bounds,
|
||||
FillGradient,
|
||||
Graphics,
|
||||
Text,
|
||||
TextStyle,
|
||||
TextStyleOptions,
|
||||
} from "pixi.js";
|
||||
import { Hand } from "./Hand";
|
||||
import { Card } from "./Card";
|
||||
import { CARD_HEIGHT, CARD_HOVER_DIST } from "./constants";
|
||||
import { CARD_HEIGHT, CARD_HOVER_DIST, CARD_WIDTH } from "./constants";
|
||||
|
||||
const playerCards: Card[] = [
|
||||
"threeOfClubs",
|
||||
"twoOfDiamonds",
|
||||
"aceOfClubs",
|
||||
"eightOfDiamonds",
|
||||
"nineOfSpades",
|
||||
"sixOfClubs",
|
||||
];
|
||||
const playerCards: Card[] = ["nineOfSpades", "sixOfClubs"];
|
||||
|
||||
const dealerCards: Card[] = ["aceOfHearts"];
|
||||
const dealerCards: Card[] = ["unknown", "aceOfHearts"];
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
@ -27,25 +28,108 @@ const dealerCards: Card[] = ["aceOfHearts"];
|
||||
// Append the application canvas to the document body
|
||||
document.getElementById("pixi-container")!.appendChild(app.canvas);
|
||||
|
||||
const payoutStyle: TextStyleOptions = {
|
||||
fontFamily: "Fira Sans",
|
||||
fontWeight: "bold",
|
||||
fontSize: CARD_HEIGHT / 5,
|
||||
fill: "#ff9900",
|
||||
padding: 10,
|
||||
letterSpacing: 10,
|
||||
};
|
||||
|
||||
const blackjackPayout = new Text({
|
||||
text: "BLACKJACK PAYS 3 TO 2",
|
||||
style: payoutStyle,
|
||||
});
|
||||
|
||||
const tableMarkings = new Graphics();
|
||||
|
||||
const markTable = () => {
|
||||
tableMarkings.clear();
|
||||
|
||||
// dark green dealer area
|
||||
tableMarkings.rect(0, 0, app.screen.width, CARD_HEIGHT * 1.8);
|
||||
tableMarkings.fill("#055010");
|
||||
|
||||
// white insurance info box
|
||||
tableMarkings.rect(
|
||||
CARD_WIDTH / 2,
|
||||
CARD_HEIGHT * 2.15,
|
||||
app.screen.width - CARD_WIDTH,
|
||||
CARD_HEIGHT / 2.5,
|
||||
);
|
||||
tableMarkings.stroke({
|
||||
color: "#ffffff",
|
||||
width: 8,
|
||||
});
|
||||
};
|
||||
|
||||
const positionPayoutInfo = () => {
|
||||
blackjackPayout.pivot.set(blackjackPayout.width / 2, 0);
|
||||
blackjackPayout.position.set(app.screen.width / 2, CARD_HEIGHT * 1.5);
|
||||
};
|
||||
|
||||
const dealerRulesStyle: TextStyleOptions = {
|
||||
fontFamily: "Fira Sans",
|
||||
fontStyle: "italic",
|
||||
fontSize: CARD_HEIGHT / 7,
|
||||
fill: "#ffffff",
|
||||
};
|
||||
|
||||
const dealerRules = new Text({
|
||||
text: "Dealer must stand on 17 and must draw to 16",
|
||||
style: dealerRulesStyle,
|
||||
});
|
||||
|
||||
const positionDealerInfo = () => {
|
||||
dealerRules.pivot.set(dealerRules.width / 2, 0);
|
||||
dealerRules.position.set(app.screen.width / 2, CARD_HEIGHT * 1.85);
|
||||
};
|
||||
|
||||
const insuranceStyle: TextStyleOptions = {
|
||||
fontFamily: "Fira Sans",
|
||||
fontWeight: "bold",
|
||||
fontSize: CARD_HEIGHT / 5,
|
||||
fill: "#ffffff",
|
||||
letterSpacing: 10,
|
||||
};
|
||||
|
||||
const insurancePayout = new Text({
|
||||
text: "INSURANCE PAYS 2 TO 1",
|
||||
style: insuranceStyle,
|
||||
});
|
||||
|
||||
const positionInsurancePayout = () => {
|
||||
insurancePayout.pivot.set(insurancePayout.width / 2, 0);
|
||||
insurancePayout.position.set(app.screen.width / 2, CARD_HEIGHT * 2.2);
|
||||
};
|
||||
|
||||
const playerHand = new Hand(350, []);
|
||||
const dealerHand = new Hand(350, []);
|
||||
|
||||
const positionPlayer = () => {
|
||||
playerHand.setPivot("center");
|
||||
playerHand.setPosition(
|
||||
app.screen.width / 2,
|
||||
app.screen.height - CARD_HEIGHT + CARD_HOVER_DIST,
|
||||
);
|
||||
playerHand.setPosition(app.screen.width / 2, CARD_HEIGHT * 3);
|
||||
playerHand.setCardLayout("ascending");
|
||||
};
|
||||
|
||||
const positionDealer = () => {
|
||||
dealerHand.setPivot("center");
|
||||
dealerHand.setPosition(app.screen.width / 2, 0 + CARD_HOVER_DIST);
|
||||
dealerHand.setCardLayout("stacked");
|
||||
};
|
||||
|
||||
markTable();
|
||||
positionPayoutInfo();
|
||||
positionDealerInfo();
|
||||
positionInsurancePayout();
|
||||
positionPlayer();
|
||||
positionDealer();
|
||||
|
||||
app.stage.addChild(tableMarkings);
|
||||
app.stage.addChild(blackjackPayout);
|
||||
app.stage.addChild(dealerRules);
|
||||
app.stage.addChild(insurancePayout);
|
||||
app.stage.addChild(playerHand.getContainer());
|
||||
app.stage.addChild(dealerHand.getContainer());
|
||||
|
||||
@ -60,6 +144,10 @@ const dealerCards: Card[] = ["aceOfHearts"];
|
||||
// https://pixijs.com/8.x/guides/concepts/scene-graph#local-vs-global-coordinates
|
||||
window.addEventListener?.("resize", () => {
|
||||
app.renderer.resize(window.innerWidth, window.innerHeight);
|
||||
markTable();
|
||||
positionPayoutInfo();
|
||||
positionDealerInfo();
|
||||
positionInsurancePayout();
|
||||
positionPlayer();
|
||||
positionDealer();
|
||||
});
|
||||
@ -71,11 +159,11 @@ const dealerCards: Card[] = ["aceOfHearts"];
|
||||
if (elapsed >= ms) {
|
||||
ms += 300;
|
||||
if (playerCards.length) {
|
||||
playerHand.add(playerCards.pop()!);
|
||||
playerHand.add(playerCards.shift()!);
|
||||
}
|
||||
|
||||
if (dealerCards.length) {
|
||||
dealerHand.add(dealerCards.pop()!);
|
||||
dealerHand.add(dealerCards.shift()!);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
11
client/src/spritesheet.ts
Normal file
11
client/src/spritesheet.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Assets, Spritesheet } from "pixi.js";
|
||||
|
||||
let spriteSheet: Spritesheet | null = null;
|
||||
|
||||
export async function getSpriteSheet(): Promise<Spritesheet> {
|
||||
if (spriteSheet === null) {
|
||||
spriteSheet = await Assets.load<Spritesheet>("public/assets/cards.json");
|
||||
}
|
||||
|
||||
return spriteSheet;
|
||||
}
|
||||
Reference in New Issue
Block a user