Merge branch 'drawer'
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
node_modules
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
|
||||||
|
**/dist/**/*
|
||||||
|
static/dist
|
42
deno.json
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"lock": false,
|
||||||
|
"tasks": {
|
||||||
|
"dev": "deno run -A tools/dev.ts",
|
||||||
|
"build": "deno run -A tools/build.ts",
|
||||||
|
"build:watch": "deno run --watch=src -A tools/build.ts",
|
||||||
|
"serve": "deno run -A src/server/index.ts",
|
||||||
|
"serve:watch": "deno run --watch=src -A src/server/index.ts"
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"rules": {
|
||||||
|
"tags": [
|
||||||
|
"recommended"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"imports": {
|
||||||
|
"react/": "https://esm.sh/react@18.3.1/",
|
||||||
|
"react-dom/": "https://esm.sh/react-dom@18.3.1/client/",
|
||||||
|
"react": "https://esm.sh/react@18.3.1",
|
||||||
|
"react-dom": "https://esm.sh/react-dom@18.3.1/client",
|
||||||
|
"canvas": "https://esm.sh/canvas@3.0.0"
|
||||||
|
},
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["deno.ns", "DOM"],
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "react",
|
||||||
|
"strict": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"allowUnusedLabels": false
|
||||||
|
},
|
||||||
|
"fmt": {
|
||||||
|
"useTabs": true
|
||||||
|
}
|
||||||
|
}
|
1230
reference.js
Normal file
3
root.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { dirname, fromFileUrl } from "jsr:@std/path";
|
||||||
|
|
||||||
|
export const projectRootDir = dirname(fromFileUrl(import.meta.url));
|
304
src/cards.ts
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
import {
|
||||||
|
DominionCard,
|
||||||
|
TYPE_ACTION,
|
||||||
|
TYPE_DURATION,
|
||||||
|
TYPE_NIGHT,
|
||||||
|
TYPE_TREASURE,
|
||||||
|
TYPE_VICTORY,
|
||||||
|
} from "./types.ts";
|
||||||
|
|
||||||
|
const expansionIcon = "";
|
||||||
|
const author = "Dylan";
|
||||||
|
|
||||||
|
const _sampleCard = {
|
||||||
|
orientation: "card",
|
||||||
|
title: "Sample",
|
||||||
|
description: "",
|
||||||
|
types: [TYPE_ACTION],
|
||||||
|
image: "",
|
||||||
|
artist: "",
|
||||||
|
author,
|
||||||
|
version: "0.1",
|
||||||
|
cost: "$",
|
||||||
|
preview: "",
|
||||||
|
expansionIcon,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cards: DominionCard[] = [
|
||||||
|
{
|
||||||
|
orientation: "card",
|
||||||
|
title: "Flask",
|
||||||
|
description:
|
||||||
|
"+2 Cards\n\nAt the start of your Clean-up phase, you may put a card from your hand onto your deck.",
|
||||||
|
types: [TYPE_TREASURE],
|
||||||
|
image: "",
|
||||||
|
artist: "",
|
||||||
|
author,
|
||||||
|
version: "0.1",
|
||||||
|
cost: "$6",
|
||||||
|
preview: "",
|
||||||
|
expansionIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orientation: "card",
|
||||||
|
title: "Promising Land",
|
||||||
|
description: "Worth 1% per 4 cards you have that cost $4 or $5.",
|
||||||
|
types: [TYPE_VICTORY],
|
||||||
|
image: "",
|
||||||
|
artist: "",
|
||||||
|
author,
|
||||||
|
version: "0.1",
|
||||||
|
cost: "$4",
|
||||||
|
preview: "",
|
||||||
|
expansionIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orientation: "card",
|
||||||
|
title: "Steelworker",
|
||||||
|
description:
|
||||||
|
"If it's your Action phase, +3 Cards.\n\nIf it's your Buy phase, +1 Buy, and +$1.",
|
||||||
|
types: [TYPE_ACTION, TYPE_TREASURE],
|
||||||
|
image: "",
|
||||||
|
artist: "",
|
||||||
|
author,
|
||||||
|
version: "0.2",
|
||||||
|
cost: "$5",
|
||||||
|
preview: "$?",
|
||||||
|
expansionIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orientation: "card",
|
||||||
|
title: "Shovel",
|
||||||
|
description:
|
||||||
|
"Play a Treasure card from your hand. Then trash it from play to gain a Treasure card costing up to $3 more than it.",
|
||||||
|
types: [TYPE_TREASURE],
|
||||||
|
image: "",
|
||||||
|
artist: "",
|
||||||
|
author,
|
||||||
|
version: "0.1",
|
||||||
|
cost: "$6",
|
||||||
|
preview: "",
|
||||||
|
expansionIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orientation: "card",
|
||||||
|
title: "High Council",
|
||||||
|
description:
|
||||||
|
"+2 Cards\n+1 Action\n+1 Buy\n\nEach player (including you) may choose one: +1 Card, or trash a card from their hand.",
|
||||||
|
types: [TYPE_ACTION],
|
||||||
|
image: "",
|
||||||
|
artist: "",
|
||||||
|
author: "Lou + Dylan",
|
||||||
|
version: "0.1",
|
||||||
|
cost: "$7",
|
||||||
|
preview: "",
|
||||||
|
expansionIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orientation: "card",
|
||||||
|
title: "Productive Village",
|
||||||
|
description:
|
||||||
|
"If it's your Action phase, +3 Actions.\n\nIf it's your Buy phase, +$1 per unused Action you have (Action, not Action card). +$1 if you have no Actions.",
|
||||||
|
types: [TYPE_ACTION, TYPE_TREASURE],
|
||||||
|
image: "",
|
||||||
|
artist: "",
|
||||||
|
author: "Dylan",
|
||||||
|
version: "0.2",
|
||||||
|
cost: "$3",
|
||||||
|
preview: "$?",
|
||||||
|
expansionIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orientation: "card",
|
||||||
|
title: "Secret Society",
|
||||||
|
description:
|
||||||
|
"+1 Action\n\nIf you have at least 3 copies of Secret Society in play, trash all of them to gain any number of cards costing at least $2, whose total combined cost is at most $50.\n\n-\n\nThis card cannot be gained other than by buying it. During a player's buy phase, this costs $3 plus $2 per Secret Society they've gained this game.",
|
||||||
|
types: [TYPE_ACTION],
|
||||||
|
image: "",
|
||||||
|
artist: "",
|
||||||
|
author: "Dylan",
|
||||||
|
version: "0.1",
|
||||||
|
cost: "$4*",
|
||||||
|
preview: "",
|
||||||
|
expansionIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orientation: "card",
|
||||||
|
title: "Eclipse",
|
||||||
|
description:
|
||||||
|
"+1 Card\n\nIf you have no Actions, +1 Action. If you have no Buys, +1 Buy. Return to your Action phase.",
|
||||||
|
types: [TYPE_NIGHT],
|
||||||
|
image: "",
|
||||||
|
artist: "",
|
||||||
|
author: "Dylan",
|
||||||
|
version: "0.1",
|
||||||
|
cost: "$5",
|
||||||
|
preview: "",
|
||||||
|
expansionIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orientation: "card",
|
||||||
|
title: "Moonlit Scheme",
|
||||||
|
description: "You may play an Action card from your hand.",
|
||||||
|
types: [TYPE_NIGHT],
|
||||||
|
image: "",
|
||||||
|
artist: "",
|
||||||
|
author: "Dylan",
|
||||||
|
version: "0.1",
|
||||||
|
cost: "$2",
|
||||||
|
preview: "",
|
||||||
|
expansionIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orientation: "card",
|
||||||
|
title: "Beaver",
|
||||||
|
description:
|
||||||
|
"Pay $1. If you did, gain a card costing up to the amount of $ you have.",
|
||||||
|
types: [TYPE_NIGHT],
|
||||||
|
image: "",
|
||||||
|
artist: "",
|
||||||
|
author: "Dylan",
|
||||||
|
version: "0.1",
|
||||||
|
cost: "$3",
|
||||||
|
preview: "",
|
||||||
|
expansionIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orientation: "card",
|
||||||
|
title: "Silk",
|
||||||
|
description: "Choose one: +$2, or gain a Silver.",
|
||||||
|
types: [TYPE_TREASURE],
|
||||||
|
image: "",
|
||||||
|
artist: "",
|
||||||
|
author,
|
||||||
|
version: "0.1",
|
||||||
|
cost: "$4",
|
||||||
|
preview: "$?",
|
||||||
|
expansionIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orientation: "card",
|
||||||
|
title: "Foundry",
|
||||||
|
description:
|
||||||
|
"Choose one: +1 Card, +1 Action and +$1; or trash a card from your hand to gain a card that costs up to $2 more than it.",
|
||||||
|
types: [TYPE_ACTION],
|
||||||
|
image: "",
|
||||||
|
artist: "",
|
||||||
|
author,
|
||||||
|
version: "0.1",
|
||||||
|
cost: "$5",
|
||||||
|
preview: "",
|
||||||
|
expansionIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orientation: "card",
|
||||||
|
title: "Vendor",
|
||||||
|
description:
|
||||||
|
"Choose three different options: +1 Card, +1 Action, +1 Buy, +$1, trash a card from your hand.",
|
||||||
|
types: [TYPE_ACTION],
|
||||||
|
image: "",
|
||||||
|
artist: "",
|
||||||
|
author,
|
||||||
|
version: "0.2",
|
||||||
|
cost: "$5",
|
||||||
|
preview: "",
|
||||||
|
expansionIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orientation: "card",
|
||||||
|
title: "Chateau",
|
||||||
|
description:
|
||||||
|
"1%\n\n-\n\nWhen you gain this, choose one: gain an Estate; or +1 Card, +1 Action, +1 Buy, +$1, and if it's your Buy phase, return to your Action phase.",
|
||||||
|
types: [TYPE_VICTORY],
|
||||||
|
image: "",
|
||||||
|
artist: "",
|
||||||
|
author,
|
||||||
|
version: "0.1",
|
||||||
|
cost: "$3",
|
||||||
|
preview: "",
|
||||||
|
expansionIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orientation: "card",
|
||||||
|
title: "Retainer",
|
||||||
|
description:
|
||||||
|
"Set aside a card from your hand (under this).\n\nAt any time during any of your turns, you may take +1 Action, and add the set aside card to your hand, discarding this from play.\n\nAt the start of each of your Buy phases, if the card is still set aside, +@1.",
|
||||||
|
types: [TYPE_ACTION, TYPE_DURATION],
|
||||||
|
image: "",
|
||||||
|
artist: "",
|
||||||
|
author,
|
||||||
|
version: "0.2",
|
||||||
|
cost: "$2",
|
||||||
|
preview: "",
|
||||||
|
expansionIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orientation: "card",
|
||||||
|
title: "Crop Field",
|
||||||
|
description: "$1\n\n-\n\n1%",
|
||||||
|
types: [TYPE_TREASURE, TYPE_VICTORY],
|
||||||
|
image: "",
|
||||||
|
artist: "",
|
||||||
|
author,
|
||||||
|
version: "0.1",
|
||||||
|
cost: "$3",
|
||||||
|
preview: "",
|
||||||
|
expansionIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orientation: "card",
|
||||||
|
title: "Duet",
|
||||||
|
description:
|
||||||
|
"Play one of the set aside cards, leaving it there\n\n-\n\nSetup: set aside two unused non-Duration Action cards of the same cost. This costs $1 more than the cost of the set aside cards.",
|
||||||
|
types: [TYPE_ACTION],
|
||||||
|
image: "",
|
||||||
|
artist: "",
|
||||||
|
author,
|
||||||
|
version: "0.1",
|
||||||
|
cost: "$?",
|
||||||
|
preview: "",
|
||||||
|
expansionIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orientation: "card",
|
||||||
|
title: "Scraps",
|
||||||
|
description:
|
||||||
|
"If it's your Action phase, trash up to 3 cards from your hand.\n\nIf it's your Buy phase, +$1 per 10 cards in the trash (round down).",
|
||||||
|
types: [TYPE_ACTION, TYPE_TREASURE],
|
||||||
|
image: "",
|
||||||
|
artist: "",
|
||||||
|
author,
|
||||||
|
version: "0.1",
|
||||||
|
cost: "$4",
|
||||||
|
preview: "",
|
||||||
|
expansionIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orientation: "card",
|
||||||
|
title: "Slogger",
|
||||||
|
description:
|
||||||
|
"+1 Card\n+1 Action\n\nReveal the top 5 cards of your deck. Put the Victory cards into your hand, and put the rest back on top in any order.",
|
||||||
|
types: [TYPE_ACTION],
|
||||||
|
image: "",
|
||||||
|
artist: "",
|
||||||
|
author,
|
||||||
|
version: "0.1",
|
||||||
|
cost: "$4",
|
||||||
|
preview: "",
|
||||||
|
expansionIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orientation: "card",
|
||||||
|
title: "Vase",
|
||||||
|
description:
|
||||||
|
"$2\nReturn this to its pile.\n\n-\n\nWhen you gain this, gain another Vase (that doesn't come with another).",
|
||||||
|
types: [TYPE_TREASURE],
|
||||||
|
image: "",
|
||||||
|
artist: "",
|
||||||
|
author,
|
||||||
|
version: "0.1",
|
||||||
|
cost: "$3",
|
||||||
|
preview: "",
|
||||||
|
expansionIcon,
|
||||||
|
},
|
||||||
|
];
|
10
src/client/App.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { cards } from "../cards.ts";
|
||||||
|
import { Card } from "./Card.tsx";
|
||||||
|
|
||||||
|
export const App = () => {
|
||||||
|
return <div>
|
||||||
|
{cards.map((card) => {
|
||||||
|
return <Card key={`${card.title}`} card={card}/>
|
||||||
|
})}
|
||||||
|
</div>;
|
||||||
|
};
|
28
src/client/Card.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { drawCard, loadImages, loadFonts } from "../draw.ts";
|
||||||
|
import { DominionCard } from "../types.ts";
|
||||||
|
|
||||||
|
const sizeMap = {
|
||||||
|
card: {
|
||||||
|
width: 1403,
|
||||||
|
height: 2151,
|
||||||
|
},
|
||||||
|
landscape: {
|
||||||
|
width: 2151,
|
||||||
|
height: 1403,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Card = (props: {card: DominionCard}) => {
|
||||||
|
const {card} = props;
|
||||||
|
const {width, height} = sizeMap[card.orientation];
|
||||||
|
return <canvas style={{width: "2.5in"}} width={width} height={height} ref={async (canvasElement) => {
|
||||||
|
if (canvasElement) {
|
||||||
|
const context = canvasElement.getContext("2d");
|
||||||
|
if (context) {
|
||||||
|
await loadFonts();
|
||||||
|
await loadImages();
|
||||||
|
await drawCard(context, card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}></canvas>
|
||||||
|
}
|
14
src/client/index.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { StrictMode } from "react";
|
||||||
|
import { createRoot } from "react-dom";
|
||||||
|
import { App } from "./App.tsx";
|
||||||
|
|
||||||
|
const rootElement = document.getElementById("root");
|
||||||
|
if (!rootElement) {
|
||||||
|
throw Error("No root element to attach react to.");
|
||||||
|
}
|
||||||
|
|
||||||
|
createRoot(rootElement).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
5
src/colorhelper.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import parseColor1 from "npm:parse-color";
|
||||||
|
|
||||||
|
export const parseColor = (c: string): { rgb: [number, number, number] } => {
|
||||||
|
return parseColor1(c);
|
||||||
|
};
|
480
src/dominiontext.ts
Normal file
@ -0,0 +1,480 @@
|
|||||||
|
import { getImage } from "./draw.ts";
|
||||||
|
import { parseFont, stringifyFont } from "./fonthelper.ts";
|
||||||
|
|
||||||
|
export type Piece =
|
||||||
|
| { type: "text"; text: string; isBold?: boolean; isItalic?: boolean }
|
||||||
|
| { type: "space" }
|
||||||
|
| { type: "break" }
|
||||||
|
| { type: "hr" }
|
||||||
|
| {
|
||||||
|
type: "symbol";
|
||||||
|
symbol: "coin" | "debt" | "potion" | "vp" | "vp-token" | "sun";
|
||||||
|
isBig?: boolean;
|
||||||
|
prefix?: string;
|
||||||
|
text: string;
|
||||||
|
textColor: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PromiseOr<T> = T | Promise<T>;
|
||||||
|
|
||||||
|
type PieceMeasure = {
|
||||||
|
type: "content" | "space" | "break";
|
||||||
|
width: number;
|
||||||
|
ascent: number;
|
||||||
|
descent: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Line = {
|
||||||
|
pieces: {
|
||||||
|
piece: Piece;
|
||||||
|
measure: PieceMeasure;
|
||||||
|
xOffset: number;
|
||||||
|
}[];
|
||||||
|
width: number;
|
||||||
|
ascent: number;
|
||||||
|
descent: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PieceTools = {
|
||||||
|
measurePiece: (
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
piece: Piece
|
||||||
|
) => PromiseOr<PieceMeasure>;
|
||||||
|
renderPiece: (
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
piece: Piece,
|
||||||
|
x: number,
|
||||||
|
y: number
|
||||||
|
) => PromiseOr<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PieceDef<T extends Piece["type"], M extends PieceMeasure> = {
|
||||||
|
type: T;
|
||||||
|
measure(
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
piece: Piece & { type: T },
|
||||||
|
tools: PieceTools
|
||||||
|
): PromiseOr<M>;
|
||||||
|
render(
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
piece: Piece & { type: T },
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
measure: NoInfer<M>,
|
||||||
|
tools: PieceTools
|
||||||
|
): PromiseOr<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pieceDef = <T extends Piece["type"], M extends PieceMeasure>(
|
||||||
|
def: PieceDef<T, M>
|
||||||
|
) => {
|
||||||
|
return def;
|
||||||
|
};
|
||||||
|
|
||||||
|
const textPiece = pieceDef({
|
||||||
|
type: "text",
|
||||||
|
measure(context, piece) {
|
||||||
|
context.save();
|
||||||
|
const fontInfo = parseFont(context.font);
|
||||||
|
if (piece.isBold) {
|
||||||
|
fontInfo.weight = "bold";
|
||||||
|
}
|
||||||
|
if (piece.isItalic) {
|
||||||
|
fontInfo.style = "italic";
|
||||||
|
}
|
||||||
|
const font = stringifyFont(fontInfo);
|
||||||
|
context.font = font;
|
||||||
|
const metrics = context.measureText(piece.text);
|
||||||
|
context.restore();
|
||||||
|
return {
|
||||||
|
type: "content",
|
||||||
|
width: metrics.width,
|
||||||
|
ascent: metrics.fontBoundingBoxAscent,
|
||||||
|
descent: metrics.fontBoundingBoxDescent,
|
||||||
|
font,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
render(context, piece, x, y, measure) {
|
||||||
|
context.save();
|
||||||
|
context.font = measure.font;
|
||||||
|
context.fillText(piece.text, x, y);
|
||||||
|
context.restore();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const spacePiece = pieceDef({
|
||||||
|
type: "space",
|
||||||
|
measure(context, _piece) {
|
||||||
|
const metrics = context.measureText(" ");
|
||||||
|
return {
|
||||||
|
type: "space",
|
||||||
|
width: metrics.width,
|
||||||
|
ascent: metrics.fontBoundingBoxAscent,
|
||||||
|
descent: metrics.fontBoundingBoxDescent,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
render() {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const breakPiece = pieceDef({
|
||||||
|
type: "break",
|
||||||
|
measure(context, _piece) {
|
||||||
|
const metrics = context.measureText(" ");
|
||||||
|
return {
|
||||||
|
type: "break",
|
||||||
|
width: 0,
|
||||||
|
ascent: metrics.fontBoundingBoxAscent / 3,
|
||||||
|
descent: metrics.fontBoundingBoxDescent / 3,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
render() {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const hrPiece = pieceDef({
|
||||||
|
type: "hr",
|
||||||
|
measure(context, _piece) {
|
||||||
|
const metrics = context.measureText(" ");
|
||||||
|
const h =
|
||||||
|
(metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent) /
|
||||||
|
3;
|
||||||
|
return {
|
||||||
|
type: "content",
|
||||||
|
width: 750,
|
||||||
|
ascent: h / 2,
|
||||||
|
descent: h / 2,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
render(context, _piece, x, y, measure) {
|
||||||
|
context.save();
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(x, y);
|
||||||
|
context.lineTo(x + measure.width, y);
|
||||||
|
context.lineWidth = 8;
|
||||||
|
context.stroke();
|
||||||
|
context.restore();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const symbolPiece = pieceDef({
|
||||||
|
type: "symbol",
|
||||||
|
measure(context, piece) {
|
||||||
|
context.save();
|
||||||
|
const metrics = context.measureText(" ");
|
||||||
|
const height =
|
||||||
|
metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;
|
||||||
|
const prefixMetrics = context.measureText(piece.prefix ?? "");
|
||||||
|
const coinImage = getImage(piece.symbol);
|
||||||
|
context.restore();
|
||||||
|
const { isBig } = piece;
|
||||||
|
const scale = isBig ? 2.5 : 1;
|
||||||
|
return {
|
||||||
|
type: "content",
|
||||||
|
width:
|
||||||
|
scale *
|
||||||
|
(prefixMetrics.width +
|
||||||
|
coinImage.width * (height / coinImage.height)),
|
||||||
|
ascent: scale * metrics.fontBoundingBoxAscent,
|
||||||
|
descent: scale * metrics.fontBoundingBoxDescent,
|
||||||
|
prefixWidth: scale * prefixMetrics.width,
|
||||||
|
scale,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
render(context, piece, x, y, measure) {
|
||||||
|
if (piece.isBig) {
|
||||||
|
console.log("big", piece, measure);
|
||||||
|
}
|
||||||
|
context.save();
|
||||||
|
// context.fillStyle = "yellow";
|
||||||
|
const height = measure.ascent + measure.descent;
|
||||||
|
// context.fillRect(x, y - measure.ascent, measure.width, height);
|
||||||
|
context.drawImage(
|
||||||
|
getImage(piece.symbol),
|
||||||
|
x + measure.prefixWidth,
|
||||||
|
y - measure.ascent,
|
||||||
|
measure.width - measure.prefixWidth,
|
||||||
|
height
|
||||||
|
);
|
||||||
|
|
||||||
|
context.save();
|
||||||
|
const prefixFontInfo = parseFont(context.font);
|
||||||
|
prefixFontInfo.weight = "bold";
|
||||||
|
prefixFontInfo.size =
|
||||||
|
parseInt(prefixFontInfo.size.toString()) * measure.scale;
|
||||||
|
const prefixFont = stringifyFont(prefixFontInfo);
|
||||||
|
context.font = prefixFont;
|
||||||
|
context.fillText(piece.prefix ?? "", x, y);
|
||||||
|
context.restore();
|
||||||
|
|
||||||
|
const fontInfo = parseFont(context.font);
|
||||||
|
fontInfo.family = ["DominionSpecial"];
|
||||||
|
fontInfo.weight = "bold";
|
||||||
|
fontInfo.size =
|
||||||
|
parseInt(fontInfo.size.toString()) * 1.2 * measure.scale;
|
||||||
|
const font = stringifyFont(fontInfo);
|
||||||
|
context.font = font;
|
||||||
|
context.fillStyle = piece.textColor;
|
||||||
|
context.textAlign = "center";
|
||||||
|
context.fillText(
|
||||||
|
piece.text,
|
||||||
|
x + measure.prefixWidth + (measure.width - measure.prefixWidth) / 2,
|
||||||
|
y
|
||||||
|
);
|
||||||
|
context.restore();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const pieceDefs = [textPiece, spacePiece, breakPiece, symbolPiece, hrPiece];
|
||||||
|
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
const tools: PieceTools = {} as any;
|
||||||
|
|
||||||
|
const measurePiece = (context: CanvasRenderingContext2D, piece: Piece) => {
|
||||||
|
const def = pieceDefs.find((def) => def.type === piece.type)!;
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
return def.measure(context, piece as any, tools);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderPiece = (
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
piece: Piece,
|
||||||
|
x: number,
|
||||||
|
y: number
|
||||||
|
) => {
|
||||||
|
const def = pieceDefs.find((def) => def.type === piece.type)!;
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
const measure = def.measure(context, piece as any, tools);
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
return def.render(context, piece as any, x, y, measure as any, tools);
|
||||||
|
};
|
||||||
|
|
||||||
|
tools.measurePiece = measurePiece;
|
||||||
|
tools.renderPiece = renderPiece;
|
||||||
|
|
||||||
|
type DominionFont = {
|
||||||
|
font: "text" | "title";
|
||||||
|
size: number;
|
||||||
|
isBold: boolean;
|
||||||
|
isItalic: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PieceWithInfo = {
|
||||||
|
piece: Piece;
|
||||||
|
measure: PieceMeasure;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const measureDominionText = async (
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
pieces: Piece[],
|
||||||
|
maxWidth = Infinity
|
||||||
|
) => {
|
||||||
|
const data: PieceWithInfo[] = await Promise.all(
|
||||||
|
pieces.map(async (piece) => ({
|
||||||
|
piece,
|
||||||
|
measure: await measurePiece(context, piece),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
let lines: Line[] = [{ pieces: [], width: 0, ascent: 0, descent: 0 }];
|
||||||
|
for (const pieceInfo of data) {
|
||||||
|
const line = lines[lines.length - 1]!;
|
||||||
|
if (pieceInfo.measure.type === "break") {
|
||||||
|
line.pieces.push({ ...pieceInfo, xOffset: line.width });
|
||||||
|
line.width += pieceInfo.measure.width;
|
||||||
|
line.ascent = Math.max(line.ascent, pieceInfo.measure.ascent);
|
||||||
|
line.descent = Math.max(line.descent, pieceInfo.measure.descent);
|
||||||
|
lines.push({ pieces: [], width: 0, ascent: 0, descent: 0 });
|
||||||
|
} else {
|
||||||
|
if (line.width + pieceInfo.measure.width > maxWidth) {
|
||||||
|
lines.push({
|
||||||
|
pieces: [{ ...pieceInfo, xOffset: 0 }],
|
||||||
|
width: pieceInfo.measure.width,
|
||||||
|
ascent: pieceInfo.measure.ascent,
|
||||||
|
descent: pieceInfo.measure.descent,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
line.pieces.push({
|
||||||
|
...pieceInfo,
|
||||||
|
xOffset: line.width,
|
||||||
|
});
|
||||||
|
line.width += pieceInfo.measure.width;
|
||||||
|
line.ascent = Math.max(line.ascent, pieceInfo.measure.ascent);
|
||||||
|
line.descent = Math.max(
|
||||||
|
line.descent,
|
||||||
|
pieceInfo.measure.descent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines = lines.map((line) => {
|
||||||
|
while (
|
||||||
|
line.pieces[line.pieces.length - 1] &&
|
||||||
|
line.pieces[line.pieces.length - 1]!.measure.type === "space"
|
||||||
|
) {
|
||||||
|
line.pieces = line.pieces.slice(0, -1);
|
||||||
|
}
|
||||||
|
line.width = line.pieces
|
||||||
|
.map((piece) => piece.measure.width)
|
||||||
|
.reduce((a, b) => a + b, 0);
|
||||||
|
return line;
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
lines,
|
||||||
|
width: Math.max(...lines.map((line) => line.width)),
|
||||||
|
height: lines
|
||||||
|
.map((line) => line.ascent + line.descent)
|
||||||
|
.reduce((a, b) => a + b, 0),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const debug = false;
|
||||||
|
|
||||||
|
export const renderDominionText = async (
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
pieces: Piece[],
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
maxWidth = Infinity
|
||||||
|
) => {
|
||||||
|
const { lines, height } = await measureDominionText(
|
||||||
|
context,
|
||||||
|
pieces,
|
||||||
|
maxWidth
|
||||||
|
);
|
||||||
|
let yOffset = 0;
|
||||||
|
for (const line of lines) {
|
||||||
|
yOffset += line.ascent;
|
||||||
|
for (const { piece, xOffset } of line.pieces) {
|
||||||
|
await renderPiece(
|
||||||
|
context,
|
||||||
|
piece,
|
||||||
|
x - line.width / 2 + xOffset,
|
||||||
|
y - height / 2 + yOffset
|
||||||
|
);
|
||||||
|
if (debug) {
|
||||||
|
context.save();
|
||||||
|
context.strokeStyle = "blue";
|
||||||
|
context.lineWidth = 5;
|
||||||
|
const pieceMeasure = await measurePiece(context, piece);
|
||||||
|
context.strokeRect(
|
||||||
|
x - line.width / 2 + xOffset,
|
||||||
|
y - height / 2 - line.ascent + yOffset,
|
||||||
|
pieceMeasure.width,
|
||||||
|
pieceMeasure.ascent + pieceMeasure.descent
|
||||||
|
);
|
||||||
|
context.strokeStyle = "red";
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(
|
||||||
|
x - line.width / 2 + xOffset - 5,
|
||||||
|
y - height / 2 + yOffset
|
||||||
|
);
|
||||||
|
context.lineTo(
|
||||||
|
x - line.width / 2 + xOffset + 5,
|
||||||
|
y - height / 2 + yOffset
|
||||||
|
);
|
||||||
|
context.stroke();
|
||||||
|
context.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
yOffset += line.descent;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parse = (
|
||||||
|
text: string,
|
||||||
|
options?: { isDescription: boolean }
|
||||||
|
): Piece[] => {
|
||||||
|
const { isDescription = false } = options ?? {};
|
||||||
|
const pieces: Piece[] = [];
|
||||||
|
const symbolMap = {
|
||||||
|
"$": { symbol: "coin", textColor: "black" },
|
||||||
|
"@": { symbol: "debt", textColor: "white" },
|
||||||
|
"^": { symbol: "potion", textColor: "white" },
|
||||||
|
"%": { symbol: "vp", textColor: "white" },
|
||||||
|
"#": { symbol: "vp-token", textColor: "black" },
|
||||||
|
"*": { symbol: "sun", textColor: "black" },
|
||||||
|
} as const;
|
||||||
|
for (let i = 0; i < text.length; i++) {
|
||||||
|
const char = text[i]!;
|
||||||
|
if (char === " ") {
|
||||||
|
pieces.push({ type: "space" });
|
||||||
|
} else if (char === "\n") {
|
||||||
|
pieces.push({ type: "break" });
|
||||||
|
} else if (char in symbolMap) {
|
||||||
|
const c = char as keyof typeof symbolMap;
|
||||||
|
const end = text.slice(i).match(new RegExp(`\\${c}[^ \n.,;]*`))![0]
|
||||||
|
.length;
|
||||||
|
const isBig =
|
||||||
|
isDescription &&
|
||||||
|
["\n", undefined].includes(text[i - 1]) &&
|
||||||
|
["\n", undefined].includes(text[i + end]);
|
||||||
|
pieces.push({
|
||||||
|
type: "symbol",
|
||||||
|
...symbolMap[c],
|
||||||
|
text: text.slice(i + 1, i + end),
|
||||||
|
isBig,
|
||||||
|
});
|
||||||
|
i += end - 1;
|
||||||
|
} else if (char === "+") {
|
||||||
|
const match = text.slice(i).match(/\+\d*( \w+)?/);
|
||||||
|
if (match) {
|
||||||
|
const end = match[0].length;
|
||||||
|
pieces.push({
|
||||||
|
type: "text",
|
||||||
|
isBold: true,
|
||||||
|
text: text.slice(i, i + end),
|
||||||
|
});
|
||||||
|
i += end - 1;
|
||||||
|
} else {
|
||||||
|
pieces.push({
|
||||||
|
type: "text",
|
||||||
|
isBold: true,
|
||||||
|
text: "+",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
char === "-" &&
|
||||||
|
text[i - 1] === "\n" &&
|
||||||
|
text[i + 1] === "\n"
|
||||||
|
) {
|
||||||
|
pieces.push({ type: "hr" });
|
||||||
|
} else if (/\d/.test(char)) {
|
||||||
|
const match = text.slice(i).match(
|
||||||
|
new RegExp(
|
||||||
|
`\\d+(${Object.keys(symbolMap)
|
||||||
|
.map((s) => `\\${s}`)
|
||||||
|
.join("|")})`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (match) {
|
||||||
|
const end = match[0].length;
|
||||||
|
const symbolChar = match[1] as keyof typeof symbolMap;
|
||||||
|
const isBig =
|
||||||
|
isDescription &&
|
||||||
|
["\n", undefined].includes(text[i - 1]) &&
|
||||||
|
["\n", undefined].includes(text[i + end]);
|
||||||
|
pieces.push({
|
||||||
|
type: "symbol",
|
||||||
|
...symbolMap[symbolChar],
|
||||||
|
prefix: text.slice(i, i + end - 1),
|
||||||
|
text: "",
|
||||||
|
isBig,
|
||||||
|
});
|
||||||
|
i += end - 1;
|
||||||
|
} else {
|
||||||
|
const end = text.slice(i).match(/\d+/)![0].length;
|
||||||
|
pieces.push({ type: "text", text: text.slice(i, i + end) });
|
||||||
|
i += end - 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const end = text.slice(i).match(
|
||||||
|
new RegExp(
|
||||||
|
`[^${Object.keys(symbolMap)
|
||||||
|
.map((s) => `\\${s}`)
|
||||||
|
.join("")} \n]+`
|
||||||
|
)
|
||||||
|
)![0].length;
|
||||||
|
pieces.push({ type: "text", text: text.slice(i, i + end) });
|
||||||
|
i += end - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pieces;
|
||||||
|
};
|
405
src/draw.ts
Normal file
@ -0,0 +1,405 @@
|
|||||||
|
import { parseColor } from "./colorhelper.ts";
|
||||||
|
import {
|
||||||
|
measureDominionText,
|
||||||
|
parse,
|
||||||
|
renderDominionText,
|
||||||
|
} from "./dominiontext.ts";
|
||||||
|
import { DominionCardType, TYPE_ACTION } from "./types.ts";
|
||||||
|
import { DominionCard } from "./types.ts";
|
||||||
|
|
||||||
|
const imageCache: Record<string, HTMLImageElement> = {};
|
||||||
|
export const loadImage = (
|
||||||
|
src: string,
|
||||||
|
key?: string
|
||||||
|
): Promise<HTMLImageElement | null> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (key && key in imageCache && imageCache[key]) {
|
||||||
|
resolve(imageCache[key]);
|
||||||
|
}
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
if (key) {
|
||||||
|
imageCache[key] = img;
|
||||||
|
}
|
||||||
|
resolve(img);
|
||||||
|
};
|
||||||
|
img.onerror = (e) => {
|
||||||
|
console.log("err", e);
|
||||||
|
resolve(null);
|
||||||
|
};
|
||||||
|
img.src = src;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const imageList = [
|
||||||
|
{
|
||||||
|
key: "card-color-1",
|
||||||
|
src: "/static/assets/CardColorOne.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "card-color-2",
|
||||||
|
src: "/static/assets/CardColorTwo.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "card-color-2-night",
|
||||||
|
src: "/static/assets/CardColorTwoNight.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "card-brown",
|
||||||
|
src: "/static/assets/CardBrown.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "card-gray",
|
||||||
|
src: "/static/assets/CardGray.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "card-description-focus",
|
||||||
|
src: "/static/assets/DescriptionFocus.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "coin",
|
||||||
|
src: "/static/assets/Coin.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "debt",
|
||||||
|
src: "/static/assets/Debt.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "potion",
|
||||||
|
src: "/static/assets/Potion.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "vp",
|
||||||
|
src: "/static/assets/VP.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "vp-token",
|
||||||
|
src: "/static/assets/VP-Token.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "sun",
|
||||||
|
src: "/static/assets/Sun.png",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const loadImages = async () => {
|
||||||
|
for (const imageInfo of imageList) {
|
||||||
|
const { key, src } = imageInfo;
|
||||||
|
await loadImage(src, key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getImage = (key: string) => {
|
||||||
|
const image = imageCache[key];
|
||||||
|
if (!image) {
|
||||||
|
throw Error(`Tried to get an invalid image ${key}`);
|
||||||
|
}
|
||||||
|
return image;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadFonts = async () => {
|
||||||
|
const titleFont = new FontFace(
|
||||||
|
"DominionTitle",
|
||||||
|
`local("Trajan Pro Bold"), local("TrajanPro-Bold"), local('Trajan Pro'),
|
||||||
|
url('https://fonts.cdnfonts.com/s/14928/TrajanPro-Bold.woff') format('woff'),
|
||||||
|
url('https://shemitz.net/static/dominion3/Trajan%20Pro%20Bold.ttf') format('truetype'),
|
||||||
|
url('https://dominion.games/fonts/TrajanPro-Bold.otf') format('opentype'),
|
||||||
|
local("Trajan"),
|
||||||
|
local("Optimus Princeps"),
|
||||||
|
url(https://fonts.gstatic.com/s/cinzel/v8/8vIJ7ww63mVu7gt79mT7PkRXMw.woff2) format('woff2')`
|
||||||
|
);
|
||||||
|
|
||||||
|
const specialFont = new FontFace(
|
||||||
|
"DominionSpecial",
|
||||||
|
`local("Minion Std Black"), local("MinionStd-Black"), local("Minion Std"), local('Minion Pro'),
|
||||||
|
url('https://fonts.cdnfonts.com/s/13260/MinionPro-Regular.woff') format('woff'),
|
||||||
|
url('https://shemitz.net/static/dominion3/MinionStd-Black.otf') format('opentype'),
|
||||||
|
local("Optimus Princeps"),
|
||||||
|
url(https://fonts.gstatic.com/s/cinzel/v8/8vIJ7ww63mVu7gt79mT7PkRXMw.woff2) format('woff2')`
|
||||||
|
);
|
||||||
|
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
(document.fonts as any).add(titleFont);
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
(document.fonts as any).add(specialFont);
|
||||||
|
|
||||||
|
await Promise.all([titleFont.load(), specialFont.load()]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const colorImage = (
|
||||||
|
image: HTMLImageElement,
|
||||||
|
color?: string
|
||||||
|
): HTMLCanvasElement => {
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = image.width;
|
||||||
|
canvas.height = image.height;
|
||||||
|
const context = canvas.getContext("2d")!;
|
||||||
|
context.save();
|
||||||
|
context.drawImage(image, 0, 0);
|
||||||
|
context.globalCompositeOperation = "multiply";
|
||||||
|
context.fillStyle = color ?? "white";
|
||||||
|
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
context.globalCompositeOperation = "destination-atop"; // restore transparency
|
||||||
|
context.drawImage(image, 0, 0);
|
||||||
|
context.restore();
|
||||||
|
return canvas;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const drawCard = (
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
card: DominionCard
|
||||||
|
): Promise<void> => {
|
||||||
|
if (card.orientation === "card") {
|
||||||
|
return drawStandardCard(context, card);
|
||||||
|
} else {
|
||||||
|
return drawLandscapeCard(context, card);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const _rgbCache: Record<string, { r: number; g: number; b: number }> = {};
|
||||||
|
const getColorRgb = (c: string): { r: number; g: number; b: number } => {
|
||||||
|
const { rgb } = parseColor(c);
|
||||||
|
const [r, g, b] = rgb;
|
||||||
|
return { r, g, b };
|
||||||
|
// if (c in _rgbCache) {
|
||||||
|
// return _rgbCache[c]!;
|
||||||
|
// }
|
||||||
|
// const canvas = document.createElement("canvas");
|
||||||
|
// canvas.width = 10;
|
||||||
|
// canvas.height = 10;
|
||||||
|
// const context = canvas.getContext("2d")!;
|
||||||
|
// context.fillRect(0, 0, 10, 10);
|
||||||
|
// const data = context.getImageData(5, 5, 1, 1).data;
|
||||||
|
// console.log(data);
|
||||||
|
// const [r, g, b] = data;
|
||||||
|
// const rgb = { r: r!, g: g!, b: b! };
|
||||||
|
// _rgbCache[c] = rgb;
|
||||||
|
// return rgb;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTextColorForBackground = (c: string): string => {
|
||||||
|
// return "black";
|
||||||
|
const { r, g, b } = getColorRgb(c);
|
||||||
|
const avg = (r + g + b) / 3 / 255;
|
||||||
|
console.log([r, g, b], avg);
|
||||||
|
return avg > 0.5 ? "black" : "white";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getColors = (
|
||||||
|
types: DominionCardType[]
|
||||||
|
): {
|
||||||
|
primary: string;
|
||||||
|
secondary: string | null;
|
||||||
|
description: string | null;
|
||||||
|
descriptionText: string;
|
||||||
|
titleText: string;
|
||||||
|
} => {
|
||||||
|
const descriptionType =
|
||||||
|
types.find((t) => t.color?.onConflictDescriptionOnly) ?? null;
|
||||||
|
const byPriority = [...types]
|
||||||
|
.filter((type) => type.color && type !== descriptionType)
|
||||||
|
.sort((a, b) => b.color!.priority - a.color!.priority);
|
||||||
|
const priority1 = byPriority[0]!;
|
||||||
|
let primaryType: DominionCardType | null = priority1 ?? null;
|
||||||
|
let secondaryType = byPriority[1] ?? null;
|
||||||
|
if (priority1 === TYPE_ACTION) {
|
||||||
|
const overriders = byPriority.filter((t) => t.color!.overridesAction);
|
||||||
|
if (overriders.length) {
|
||||||
|
primaryType = overriders[0] ?? null;
|
||||||
|
}
|
||||||
|
if (primaryType === secondaryType) {
|
||||||
|
secondaryType = byPriority[2] ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
primaryType = primaryType ?? descriptionType;
|
||||||
|
const primary = primaryType?.color?.value ?? "white";
|
||||||
|
const secondary = secondaryType?.color?.value ?? null;
|
||||||
|
const description = descriptionType?.color?.value ?? null;
|
||||||
|
const descriptionText = getTextColorForBackground(description ?? primary);
|
||||||
|
const titleText = getTextColorForBackground(primary);
|
||||||
|
return {
|
||||||
|
primary,
|
||||||
|
secondary,
|
||||||
|
description,
|
||||||
|
descriptionText,
|
||||||
|
titleText,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawStandardCard = async (
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
card: DominionCard & { orientation: "card" }
|
||||||
|
): Promise<void> => {
|
||||||
|
const w = context.canvas.width;
|
||||||
|
// const h = context.canvas.height;
|
||||||
|
let size;
|
||||||
|
context.save();
|
||||||
|
// Draw the image
|
||||||
|
const image = await loadImage(card.image);
|
||||||
|
if (image) {
|
||||||
|
const cx = w / 2;
|
||||||
|
const cy = 704;
|
||||||
|
const windowHeight = 830;
|
||||||
|
const windowWidth = 1194;
|
||||||
|
const scale = Math.max(
|
||||||
|
windowHeight / image.height,
|
||||||
|
windowWidth / image.width
|
||||||
|
);
|
||||||
|
context.drawImage(
|
||||||
|
image,
|
||||||
|
cx - (scale * image.width) / 2,
|
||||||
|
cy - (scale * image.height) / 2,
|
||||||
|
scale * image.width,
|
||||||
|
scale * image.height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Draw the card base
|
||||||
|
const colors = getColors(card.types); // "#ffbc55";
|
||||||
|
|
||||||
|
if (colors.secondary) {
|
||||||
|
context.drawImage(
|
||||||
|
colorImage(getImage("card-color-1"), colors.secondary),
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
context.drawImage(
|
||||||
|
colorImage(getImage("card-color-2"), colors.primary),
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
} else if (colors.description) {
|
||||||
|
context.drawImage(
|
||||||
|
colorImage(getImage("card-color-1"), colors.description),
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
context.drawImage(
|
||||||
|
colorImage(getImage("card-color-2-night"), colors.primary),
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
context.drawImage(
|
||||||
|
colorImage(getImage("card-color-1"), colors.primary),
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
context.drawImage(getImage("card-description-focus"), 44, 1094);
|
||||||
|
}
|
||||||
|
context.drawImage(getImage("card-gray"), 0, 0);
|
||||||
|
context.drawImage(colorImage(getImage("card-brown"), "#ff9911"), 0, 0);
|
||||||
|
// Draw the name
|
||||||
|
context.fillStyle = colors.titleText;
|
||||||
|
context.font = "90pt DominionText";
|
||||||
|
const previewMeasure = await measureDominionText(
|
||||||
|
context,
|
||||||
|
parse(card.preview ?? "")
|
||||||
|
);
|
||||||
|
size = 78;
|
||||||
|
context.font = `${size}pt DominionTitle`;
|
||||||
|
while (
|
||||||
|
(await measureDominionText(context, parse(card.title))).width >
|
||||||
|
1050 - previewMeasure.width * 1.5
|
||||||
|
) {
|
||||||
|
size -= 1;
|
||||||
|
context.font = `${size}pt DominionTitle`;
|
||||||
|
}
|
||||||
|
await renderDominionText(context, parse(card.title), w / 2, 220);
|
||||||
|
// Draw the description
|
||||||
|
context.fillStyle = colors.descriptionText;
|
||||||
|
size = 60;
|
||||||
|
context.font = `${size}pt DominionText`;
|
||||||
|
while (
|
||||||
|
(
|
||||||
|
await measureDominionText(
|
||||||
|
context,
|
||||||
|
parse(card.description, { isDescription: true }),
|
||||||
|
1000
|
||||||
|
)
|
||||||
|
).height > 650
|
||||||
|
) {
|
||||||
|
size -= 1;
|
||||||
|
context.font = `${size}pt DominionText`;
|
||||||
|
}
|
||||||
|
await renderDominionText(
|
||||||
|
context,
|
||||||
|
parse(card.description, { isDescription: true }),
|
||||||
|
w / 2,
|
||||||
|
1490,
|
||||||
|
1000
|
||||||
|
);
|
||||||
|
// Draw the types
|
||||||
|
context.fillStyle = colors.titleText;
|
||||||
|
size = 65;
|
||||||
|
context.font = `${size}pt DominionTitle`;
|
||||||
|
while (
|
||||||
|
(
|
||||||
|
await measureDominionText(
|
||||||
|
context,
|
||||||
|
parse(card.types.map((t) => t.name).join(" - "))
|
||||||
|
)
|
||||||
|
).width > 800
|
||||||
|
) {
|
||||||
|
size -= 1;
|
||||||
|
context.font = `${size}pt DominionTitle`;
|
||||||
|
}
|
||||||
|
await renderDominionText(
|
||||||
|
context,
|
||||||
|
parse(card.types.map((t) => t.name).join(" - ")),
|
||||||
|
w / 2,
|
||||||
|
1930,
|
||||||
|
800
|
||||||
|
);
|
||||||
|
// Draw the cost
|
||||||
|
context.fillStyle = colors.titleText;
|
||||||
|
context.font = "90pt DominionText";
|
||||||
|
const costMeasure = await measureDominionText(context, parse(card.cost));
|
||||||
|
await renderDominionText(
|
||||||
|
context,
|
||||||
|
parse(card.cost),
|
||||||
|
130 + costMeasure.width / 2,
|
||||||
|
1940
|
||||||
|
);
|
||||||
|
// Draw the preview
|
||||||
|
context.fillStyle = colors.titleText;
|
||||||
|
if (card.preview) {
|
||||||
|
context.font = "90pt DominionText";
|
||||||
|
await renderDominionText(context, parse(card.preview), 200, 210);
|
||||||
|
await renderDominionText(context, parse(card.preview), w - 200, 210);
|
||||||
|
}
|
||||||
|
// Draw the expansion icon
|
||||||
|
// Draw the author credit
|
||||||
|
context.fillStyle = "white";
|
||||||
|
context.font = "31pt DominionText";
|
||||||
|
const authorMeasure = await measureDominionText(
|
||||||
|
context,
|
||||||
|
parse(card.author)
|
||||||
|
);
|
||||||
|
await renderDominionText(
|
||||||
|
context,
|
||||||
|
parse(card.author),
|
||||||
|
w - 150 - authorMeasure.width / 2,
|
||||||
|
2035
|
||||||
|
);
|
||||||
|
// Draw the artist credit
|
||||||
|
context.fillStyle = "white";
|
||||||
|
const artistMeasure = await measureDominionText(
|
||||||
|
context,
|
||||||
|
parse(card.artist)
|
||||||
|
);
|
||||||
|
await renderDominionText(
|
||||||
|
context,
|
||||||
|
parse(card.artist),
|
||||||
|
155 + artistMeasure.width / 2,
|
||||||
|
2035
|
||||||
|
);
|
||||||
|
// Restore the context
|
||||||
|
context.restore();
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawLandscapeCard = async (
|
||||||
|
_context: CanvasRenderingContext2D,
|
||||||
|
_card: DominionCard & { orientation: "landscape" }
|
||||||
|
): Promise<void> => {
|
||||||
|
// TODO: everything
|
||||||
|
};
|
41
src/fonthelper.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import font from "npm:css-font";
|
||||||
|
|
||||||
|
export type FontInfo = {
|
||||||
|
style: "normal" | "italic" | "oblique";
|
||||||
|
variant: "normal" | "small-caps";
|
||||||
|
weight:
|
||||||
|
| "normal"
|
||||||
|
| "bold"
|
||||||
|
| "lighter"
|
||||||
|
| "bolder"
|
||||||
|
| "100"
|
||||||
|
| "200"
|
||||||
|
| "300"
|
||||||
|
| "400"
|
||||||
|
| "500"
|
||||||
|
| "600"
|
||||||
|
| "700"
|
||||||
|
| "800"
|
||||||
|
| "900";
|
||||||
|
stretch:
|
||||||
|
| "normal"
|
||||||
|
| "condensed"
|
||||||
|
| "semi-condensed"
|
||||||
|
| "extra-condensed"
|
||||||
|
| "ultra-condensed"
|
||||||
|
| "expanded"
|
||||||
|
| "semi-expanded"
|
||||||
|
| "extra-expanded"
|
||||||
|
| "ultra-expanded";
|
||||||
|
lineHeight: "normal" | number | string;
|
||||||
|
size: number | string;
|
||||||
|
family: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseFont = (fontString: string): FontInfo => {
|
||||||
|
return { ...font.parse(fontString) };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const stringifyFont = (fontInfo: FontInfo): string => {
|
||||||
|
return font.stringify(fontInfo);
|
||||||
|
};
|
0
src/isocanvas.ts
Normal file
25
src/richtext.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// type RichnessNodeDefinition<N extends {type: string}> = {
|
||||||
|
// type: N["type"]
|
||||||
|
// measure(context: CanvasRenderingContext2D, node: N): Promise<TextMetrics>;
|
||||||
|
// render(
|
||||||
|
// context: CanvasRenderingContext2D,
|
||||||
|
// node: N,
|
||||||
|
// x: number,
|
||||||
|
// y: number
|
||||||
|
// ): Promise<void>;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// type Richness<N extends {type: string}> = {[K in N["type"]]: RichnessNodeDefinition<N & {type: K}>}
|
||||||
|
|
||||||
|
// const drawRichText = <N extends {type: string}>(
|
||||||
|
// context: CanvasRenderingContext2D,
|
||||||
|
// richness: Richness<N>,
|
||||||
|
// richText: N[],
|
||||||
|
// x: number,
|
||||||
|
// y: number,
|
||||||
|
// maxWidth: number,
|
||||||
|
// ) => {
|
||||||
|
// context.save();
|
||||||
|
// const
|
||||||
|
// context.restore();
|
||||||
|
// };
|
78
src/sampleData.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import {
|
||||||
|
DominionCard,
|
||||||
|
TYPE_ACTION,
|
||||||
|
TYPE_DURATION,
|
||||||
|
TYPE_REACTION,
|
||||||
|
TYPE_TREASURE,
|
||||||
|
TYPE_VICTORY,
|
||||||
|
} from "./types.ts";
|
||||||
|
|
||||||
|
export const sampleCards: DominionCard[] = [
|
||||||
|
{
|
||||||
|
orientation: "card",
|
||||||
|
title: "Title",
|
||||||
|
description:
|
||||||
|
"+*\n\nReveal the top card of your deck. If it's an Action card, +1 Action. If it has ^ in its cost, +1 Card.",
|
||||||
|
types: [TYPE_ACTION, TYPE_DURATION, TYPE_REACTION],
|
||||||
|
image: "https://wiki.dominionstrategy.com/images/7/76/AdventurerArt.jpg",
|
||||||
|
expansionIcon: "",
|
||||||
|
artist: "Dall-E",
|
||||||
|
author: "John Doe",
|
||||||
|
version: "",
|
||||||
|
cost: "@8",
|
||||||
|
preview: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orientation: "card",
|
||||||
|
title: "Market",
|
||||||
|
description: "+1 Card\n+1 Action\n+1 Buy\n+$1",
|
||||||
|
types: [TYPE_ACTION],
|
||||||
|
image: "",
|
||||||
|
expansionIcon: "",
|
||||||
|
artist: "Leonardo DaVinci",
|
||||||
|
author: "Jane Smith",
|
||||||
|
version: "",
|
||||||
|
cost: "$4",
|
||||||
|
preview: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orientation: "card",
|
||||||
|
title: "Flask",
|
||||||
|
description:
|
||||||
|
"+2 Cards\n\nAt the start of your Clean-up phase, you may put a card from your hand onto your deck.",
|
||||||
|
types: [TYPE_TREASURE],
|
||||||
|
image: "",
|
||||||
|
expansionIcon: "",
|
||||||
|
artist: "",
|
||||||
|
author: "",
|
||||||
|
version: "",
|
||||||
|
cost: "$6",
|
||||||
|
preview: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orientation: "card",
|
||||||
|
title: "VP Card",
|
||||||
|
description: "Worth 1% per 3 cards you have that cost $4 or $5.",
|
||||||
|
types: [TYPE_VICTORY],
|
||||||
|
image: "",
|
||||||
|
expansionIcon: "",
|
||||||
|
artist: "",
|
||||||
|
author: "",
|
||||||
|
version: "",
|
||||||
|
cost: "$3",
|
||||||
|
preview: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orientation: "card",
|
||||||
|
title: "Nobles",
|
||||||
|
description: "Choose one: +3 Cards, or +2 Actions.\n\n\n-\n\n\n2%",
|
||||||
|
types: [TYPE_ACTION, TYPE_VICTORY],
|
||||||
|
image: "",
|
||||||
|
expansionIcon: "",
|
||||||
|
artist: "",
|
||||||
|
author: "",
|
||||||
|
version: "",
|
||||||
|
cost: "$6",
|
||||||
|
preview: "",
|
||||||
|
},
|
||||||
|
];
|
14
src/server/index.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { serveDir, serveFile } from "jsr:@std/http/file-server";
|
||||||
|
|
||||||
|
Deno.serve((req: Request) => {
|
||||||
|
const pathname = new URL(req.url).pathname;
|
||||||
|
|
||||||
|
if (pathname.startsWith("/static")) {
|
||||||
|
return serveDir(req, {
|
||||||
|
fsRoot: "static",
|
||||||
|
urlRoot: "static",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return serveFile(req, "static/index.html");
|
||||||
|
}
|
||||||
|
});
|
171
src/types.ts
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
export type DominionText = string;
|
||||||
|
|
||||||
|
export type DominionColor = {
|
||||||
|
value: string;
|
||||||
|
priority: number; // highest priority is "primary", second highest is "secondary".
|
||||||
|
overridesAction?: boolean;
|
||||||
|
onConflictDescriptionOnly?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DominionBasicCardType = {
|
||||||
|
typeType: "basic";
|
||||||
|
name:
|
||||||
|
| "Action"
|
||||||
|
| "Treasure"
|
||||||
|
| "Victory"
|
||||||
|
| "Curse"
|
||||||
|
| "Reaction"
|
||||||
|
| "Duration"
|
||||||
|
| "Reserve"
|
||||||
|
| "Night"
|
||||||
|
| "Attack"
|
||||||
|
| "Command";
|
||||||
|
color: null | DominionColor;
|
||||||
|
};
|
||||||
|
export type DominionBasicLandscapeType = {
|
||||||
|
typeType: "basic";
|
||||||
|
name: "Event" | "Landmark" | "Project" | "Way" | "Trait";
|
||||||
|
color: null | DominionColor;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DominionCardType = DominionBasicCardType | DominionCustomCardType;
|
||||||
|
export type DominionLandscapeType =
|
||||||
|
| DominionBasicLandscapeType
|
||||||
|
| DominionCustomLandscapeType;
|
||||||
|
|
||||||
|
export type DominionCard =
|
||||||
|
| {
|
||||||
|
orientation: "card";
|
||||||
|
title: string;
|
||||||
|
description: DominionText;
|
||||||
|
types: Array<DominionCardType>;
|
||||||
|
image: string;
|
||||||
|
artist: string;
|
||||||
|
author: string;
|
||||||
|
version: string;
|
||||||
|
cost: DominionText;
|
||||||
|
expansionIcon: string;
|
||||||
|
preview?: DominionText;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
orientation: "landscape";
|
||||||
|
title: string;
|
||||||
|
description: DominionText;
|
||||||
|
types: Array<DominionLandscapeType>;
|
||||||
|
image: string;
|
||||||
|
artist: string;
|
||||||
|
author: string;
|
||||||
|
version: string;
|
||||||
|
cost: DominionText;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DominionCustomSymbol = {
|
||||||
|
image: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DominionCustomCardType = {
|
||||||
|
typeType: "custom";
|
||||||
|
name: string;
|
||||||
|
color: DominionColor;
|
||||||
|
};
|
||||||
|
export type DominionCustomLandscapeType = {
|
||||||
|
typeType: "custom";
|
||||||
|
name: string;
|
||||||
|
color: DominionColor;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DominionExpansion = {
|
||||||
|
cards: Array<DominionCard>;
|
||||||
|
icon: string;
|
||||||
|
customSymbols: Array<DominionCustomSymbol>;
|
||||||
|
customCardTypes: Array<DominionCustomCardType>;
|
||||||
|
customLandscapeTypes: Array<DominionCustomLandscapeType>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TYPE_ACTION: DominionBasicCardType = {
|
||||||
|
typeType: "basic",
|
||||||
|
name: "Action",
|
||||||
|
color: {
|
||||||
|
value: "white",
|
||||||
|
priority: 6,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TYPE_TREASURE: DominionBasicCardType = {
|
||||||
|
typeType: "basic",
|
||||||
|
name: "Treasure",
|
||||||
|
color: {
|
||||||
|
value: "#ffe076",
|
||||||
|
priority: 5,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TYPE_VICTORY: DominionBasicCardType = {
|
||||||
|
typeType: "basic",
|
||||||
|
name: "Victory",
|
||||||
|
color: {
|
||||||
|
value: "#b3e5ad",
|
||||||
|
priority: 4,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TYPE_CURSE: DominionBasicCardType = {
|
||||||
|
typeType: "basic",
|
||||||
|
name: "Curse",
|
||||||
|
color: {
|
||||||
|
value: "#d285ff",
|
||||||
|
priority: 4,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TYPE_REACTION: DominionBasicCardType = {
|
||||||
|
typeType: "basic",
|
||||||
|
name: "Reaction",
|
||||||
|
color: {
|
||||||
|
value: "#81adff",
|
||||||
|
priority: 1,
|
||||||
|
overridesAction: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TYPE_DURATION: DominionBasicCardType = {
|
||||||
|
typeType: "basic",
|
||||||
|
name: "Duration",
|
||||||
|
color: {
|
||||||
|
value: "#ffbc55",
|
||||||
|
priority: 3,
|
||||||
|
overridesAction: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TYPE_RESERVE: DominionBasicCardType = {
|
||||||
|
typeType: "basic",
|
||||||
|
name: "Reserve",
|
||||||
|
color: {
|
||||||
|
value: "#e5c28b",
|
||||||
|
priority: 2, // unknown whether this should be above or below reaction/duration?
|
||||||
|
overridesAction: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TYPE_NIGHT: DominionBasicCardType = {
|
||||||
|
typeType: "basic",
|
||||||
|
name: "Night",
|
||||||
|
color: {
|
||||||
|
value: "#485058",
|
||||||
|
priority: 6,
|
||||||
|
onConflictDescriptionOnly: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TYPE_ATTACK: DominionBasicCardType = {
|
||||||
|
typeType: "basic",
|
||||||
|
name: "Attack",
|
||||||
|
color: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TYPE_COMMAND: DominionBasicCardType = {
|
||||||
|
typeType: "basic",
|
||||||
|
name: "Command",
|
||||||
|
color: null,
|
||||||
|
};
|
BIN
static/assets/BaseCardBrown.png
Normal file
After Width: | Height: | Size: 624 KiB |
BIN
static/assets/BaseCardColorOne.png
Normal file
After Width: | Height: | Size: 484 KiB |
BIN
static/assets/BaseCardGray.png
Normal file
After Width: | Height: | Size: 80 KiB |
BIN
static/assets/BaseCardIcon.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
static/assets/CardBrown.png
Normal file
After Width: | Height: | Size: 142 KiB |
BIN
static/assets/CardColorOne.png
Normal file
After Width: | Height: | Size: 1.6 MiB |
BIN
static/assets/CardColorThree.png
Normal file
After Width: | Height: | Size: 1.0 MiB |
BIN
static/assets/CardColorTwo.png
Normal file
After Width: | Height: | Size: 1.0 MiB |
BIN
static/assets/CardColorTwoBig.png
Normal file
After Width: | Height: | Size: 1.2 MiB |
BIN
static/assets/CardColorTwoNight.png
Normal file
After Width: | Height: | Size: 1.3 MiB |
BIN
static/assets/CardColorTwoSmall.png
Normal file
After Width: | Height: | Size: 1.0 MiB |
BIN
static/assets/CardGray.png
Normal file
After Width: | Height: | Size: 117 KiB |
BIN
static/assets/CardPortraitIcon.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
static/assets/Coin.png
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
static/assets/Debt.png
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
static/assets/DescriptionFocus.png
Normal file
After Width: | Height: | Size: 1.2 MiB |
BIN
static/assets/DoubleColorOne.png
Normal file
After Width: | Height: | Size: 960 KiB |
BIN
static/assets/DoubleUncoloredDetails.png
Normal file
After Width: | Height: | Size: 466 KiB |
BIN
static/assets/EventBrown.png
Normal file
After Width: | Height: | Size: 265 KiB |
BIN
static/assets/EventBrown2.png
Normal file
After Width: | Height: | Size: 90 KiB |
BIN
static/assets/EventColorOne.png
Normal file
After Width: | Height: | Size: 353 KiB |
BIN
static/assets/EventColorTwo.png
Normal file
After Width: | Height: | Size: 291 KiB |
BIN
static/assets/EventHeirloom.png
Normal file
After Width: | Height: | Size: 200 KiB |
BIN
static/assets/Heirloom.png
Normal file
After Width: | Height: | Size: 175 KiB |
BIN
static/assets/MatBannerBottom.png
Normal file
After Width: | Height: | Size: 157 KiB |
BIN
static/assets/MatBannerTop.png
Normal file
After Width: | Height: | Size: 113 KiB |
BIN
static/assets/MatIcon.png
Normal file
After Width: | Height: | Size: 131 KiB |
BIN
static/assets/PileMarkerColorOne.png
Normal file
After Width: | Height: | Size: 916 KiB |
BIN
static/assets/PileMarkerGrey.png
Normal file
After Width: | Height: | Size: 735 KiB |
BIN
static/assets/PileMarkerIcon.png
Normal file
After Width: | Height: | Size: 80 KiB |
BIN
static/assets/Potion.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
static/assets/Sun.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
static/assets/TraitBrown.png
Normal file
After Width: | Height: | Size: 316 KiB |
BIN
static/assets/TraitBrownSide.png
Normal file
After Width: | Height: | Size: 88 KiB |
BIN
static/assets/TraitColorOne.png
Normal file
After Width: | Height: | Size: 384 KiB |
BIN
static/assets/TraitColorOneSide.png
Normal file
After Width: | Height: | Size: 140 KiB |
BIN
static/assets/Traveller.png
Normal file
After Width: | Height: | Size: 88 KiB |
BIN
static/assets/VP-Token.png
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
static/assets/VP.png
Normal file
After Width: | Height: | Size: 38 KiB |
27
static/fonts.css
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/* @font-face {
|
||||||
|
font-family: 'DominionTitle';
|
||||||
|
font-display: block;
|
||||||
|
src: local("Trajan Pro Bold"), local("TrajanPro-Bold"), local('Trajan Pro'),
|
||||||
|
url('https://fonts.cdnfonts.com/s/14928/TrajanPro-Bold.woff') format('woff'),
|
||||||
|
url('https://shemitz.net/static/dominion3/Trajan%20Pro%20Bold.ttf') format('truetype'),
|
||||||
|
url('https://dominion.games/fonts/TrajanPro-Bold.otf') format('opentype'),
|
||||||
|
local("Trajan"),
|
||||||
|
local("Optimus Princeps"),
|
||||||
|
url(https://fonts.gstatic.com/s/cinzel/v8/8vIJ7ww63mVu7gt79mT7PkRXMw.woff2) format('woff2');
|
||||||
|
} */
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'DominionText';
|
||||||
|
font-display: block;
|
||||||
|
src: local("Times New Roman"), serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* @font-face {
|
||||||
|
font-family: 'DominionSpecial';
|
||||||
|
font-display: block;
|
||||||
|
src: local("Minion Std Black"), local("MinionStd-Black"), local("Minion Std"), local('Minion Pro'),
|
||||||
|
url('https://fonts.cdnfonts.com/s/13260/MinionPro-Regular.woff') format('woff'),
|
||||||
|
url('https://shemitz.net/static/dominion3/MinionStd-Black.otf') format('opentype'),
|
||||||
|
local("Optimus Princeps"),
|
||||||
|
url(https://fonts.gstatic.com/s/cinzel/v8/8vIJ7ww63mVu7gt79mT7PkRXMw.woff2) format('woff2');
|
||||||
|
} */
|
13
static/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Dominionator</title>
|
||||||
|
<link rel="stylesheet" href="/static/fonts.css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script src="/static/dist/bundle.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
28
tools/build.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import * as esbuild from "npm:esbuild";
|
||||||
|
import { denoPlugins } from "jsr:@luca/esbuild-deno-loader";
|
||||||
|
import browserslist from "npm:browserslist";
|
||||||
|
import { projectRootDir } from "../root.ts";
|
||||||
|
|
||||||
|
const browsers = browserslist([
|
||||||
|
"last 4 Chrome versions",
|
||||||
|
"last 4 Edge versions",
|
||||||
|
"last 4 Opera versions",
|
||||||
|
"last 4 Firefox versions",
|
||||||
|
"last 4 Safari versions",
|
||||||
|
]).map((browser: string) => browser.replace(" ", ""));
|
||||||
|
|
||||||
|
// esbuild target is fine-grained: https://esbuild.github.io/api/#target
|
||||||
|
const target = [...browsers, "ios18", "ios17", "ios16", "ios14"];
|
||||||
|
await esbuild.build({
|
||||||
|
plugins: [...denoPlugins()],
|
||||||
|
absWorkingDir: projectRootDir,
|
||||||
|
entryPoints: ["src/client/index.tsx"],
|
||||||
|
outfile: "static/dist/bundle.js",
|
||||||
|
bundle: true,
|
||||||
|
format: "esm",
|
||||||
|
target,
|
||||||
|
jsx: "automatic",
|
||||||
|
jsxImportSource: "react",
|
||||||
|
});
|
||||||
|
|
||||||
|
esbuild.stop();
|
37
tools/dev.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
runConcurrentTasks().catch((error) => {
|
||||||
|
console.error("Error running tasks:", error);
|
||||||
|
Deno.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function runConcurrentTasks() {
|
||||||
|
const tasks = [
|
||||||
|
runCommand("deno task build:watch"),
|
||||||
|
runCommand("deno task serve:watch"),
|
||||||
|
];
|
||||||
|
|
||||||
|
const results = await Promise.all(tasks);
|
||||||
|
if (results.includes(false)) {
|
||||||
|
console.error("One or more tasks failed.");
|
||||||
|
Deno.exit(1);
|
||||||
|
} else {
|
||||||
|
console.log("All tasks completed successfully.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCommand(fullCommand: string) {
|
||||||
|
const [command, ...args] = fullCommand.split(" ");
|
||||||
|
|
||||||
|
const cmd = new Deno.Command(command!, {
|
||||||
|
args,
|
||||||
|
stdout: "piped",
|
||||||
|
});
|
||||||
|
const process = cmd.spawn();
|
||||||
|
|
||||||
|
const status = await process.status;
|
||||||
|
if (status.code !== 0) {
|
||||||
|
console.error(`Command failed: ${command}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|