2 Commits
main ... app

Author SHA1 Message Date
3ede3b6986 stuff from a while ago 2023-12-18 19:59:22 -08:00
a18a2c23ef Clean slate 2023-07-12 21:07:55 -07:00
80 changed files with 29811 additions and 2686 deletions

26
.gitignore vendored
View File

@ -1,7 +1,23 @@
node_modules
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dotenv environment variable files
.env
# dependencies
/node_modules
/.pnp
.pnp.js
**/dist/**/*
static/dist
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@ -1,5 +1,46 @@
# Dominionator
# Getting Started with Create React App
A web-app for making [Dominion](https://www.riograndegames.com/games/dominion/) fan-cards. Inspired by [shardofhonor's dominion-card-generator](https://github.com/shardofhonor/dominion-card-generator), but trying to be more modern and maintainable with React and Typescript. Currently it's not much of a web-app since there are no inputs to edit the cards, but you can mess with the hardcoded array in `src/cards.ts` to render arbitrary cards.
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
To run it, clone the repo and run `deno task dev`.
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).

View File

@ -1,42 +0,0 @@
{
"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
}
}

29401
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

48
package.json Normal file
View File

@ -0,0 +1,48 @@
{
"name": "dominionator",
"version": "0.1.0",
"private": true,
"license": "UNLICENSED",
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"repository": {
"type": "git",
"url": "https://git.playbox.link/dylan/dominionator.git"
},
"dependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.38",
"@types/react": "^18.2.14",
"@types/react-dom": "^18.2.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

43
public/index.html Normal file
View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

BIN
public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

25
public/manifest.json Normal file
View File

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
public/robots.txt Normal file
View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +0,0 @@
import { dirname, fromFileUrl } from "jsr:@std/path";
export const projectRootDir = dirname(fromFileUrl(import.meta.url));

38
src/App.css Normal file
View File

@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

9
src/App.test.tsx Normal file
View File

@ -0,0 +1,9 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

23
src/App.tsx Normal file
View File

@ -0,0 +1,23 @@
import React from 'react';
import logo from './logo.svg';
import './App.css';
import { Card } from './Card';
function App() {
return (
<div className="App">
<Card card={{
name: "My Card",
v: 1,
types: ["Action"],
cost: "5",
preview: "",
text: "+3 Cards",
art: "",
artCredit: "",
}} />
</div>
);
}
export default App;

43
src/Card.tsx Normal file
View File

@ -0,0 +1,43 @@
import React from "react";
type CardType =
| "Action"
| "Treasure"
| "Victory"
| "Curse"
| "Reaction"
| "Duration"
| "Reserve"
| "Night"
type CardData = {
name: string,
text: string,
cost: string,
preview: string,
types: Array<CardType>,
art: string,
artCredit: string,
v: number,
}
const drawCard = (canvas: HTMLCanvasElement, card: CardData) => {
const context = canvas.getContext("2d");
if (!context) {
throw new Error("Could not get the context of the canvas");
}
context.clearRect(0, 0, canvas.width, canvas.height);
context.font = "88px myTitle"
context.fillText(card.name, 100, 100);
}
export const Card = (props: {card: CardData}) => {
const {
card
} = props;
return <canvas className="card" width={1403} height={2151} ref={(el) => {
if (el) {
drawCard(el, card);
}
}}></canvas>
}

View File

@ -1,27 +0,0 @@
import {
DominionCard,
TYPE_ACTION,
// TYPE_DURATION,
// TYPE_NIGHT,
// TYPE_TREASURE,
// TYPE_VICTORY,
} from "./types.ts";
const expansionIcon = "";
const author = "Dylan";
const _sampleCard: DominionCard = {
orientation: "card",
title: "Sample",
description: "",
types: [TYPE_ACTION],
image: "",
artist: "",
author,
version: "0.1",
cost: "$",
preview: "",
expansionIcon,
};
export const cards: DominionCard[] = [_sampleCard, _sampleCard];

View File

@ -1,10 +0,0 @@
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>;
};

View File

@ -1,28 +0,0 @@
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>
}

View File

@ -1,14 +0,0 @@
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>,
);

View File

@ -1,5 +0,0 @@
import parseColor1 from "npm:parse-color";
export const parseColor = (c: string): { rgb: [number, number, number] } => {
return parseColor1(c);
};

View File

@ -1,480 +0,0 @@
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;
};

View File

@ -1,405 +0,0 @@
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
};

View File

@ -1,41 +0,0 @@
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);
};

46
src/index.css Normal file
View File

@ -0,0 +1,46 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
@font-face {
font-family: 'myTitle';
font-display: auto;
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: 'myText';
font-display: auto;
src: local("Times New Roman"), serif;
}
@font-face {
font-family: 'mySpecials';
font-display: auto;
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');
}
.card {
width: 2in;
border: 1px solid black;
}

19
src/index.tsx Normal file
View File

@ -0,0 +1,19 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View File

1
src/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

1
src/react-app-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="react-scripts" />

15
src/reportWebVitals.ts Normal file
View File

@ -0,0 +1,15 @@
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@ -1,25 +0,0 @@
// 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();
// };

View File

@ -1,78 +0,0 @@
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: "",
},
];

View File

@ -1,14 +0,0 @@
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");
}
});

5
src/setupTests.ts Normal file
View File

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

View File

@ -1,171 +0,0 @@
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,
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 624 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 484 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 960 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 466 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 916 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 735 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

View File

@ -1,27 +0,0 @@
/* @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');
} */

View File

@ -1,13 +0,0 @@
<!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>

View File

@ -1,28 +0,0 @@
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();

View File

@ -1,37 +0,0 @@
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;
}

26
tsconfig.json Normal file
View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}