Compare commits
44 Commits
app
...
83b42776d1
Author | SHA1 | Date | |
---|---|---|---|
83b42776d1 | |||
54a405b4b9 | |||
6c3194c256 | |||
dc49eb961f | |||
055e42ecf7 | |||
4d318d20ee | |||
7a752a92cb | |||
03fff8576e | |||
6827b966fd | |||
68c4222b3b | |||
1e90062f6d | |||
ff5c543147 | |||
52db63d395 | |||
e786943d16 | |||
3adf3bf76d | |||
6213eda240 | |||
7f4268f960 | |||
a5979647fc | |||
0e9661f146 | |||
b0249f8baf | |||
9cef34b976 | |||
28f214fb51 | |||
0099624165 | |||
4e79fd38a1 | |||
8e7bcc185c | |||
1e6e336f73 | |||
f497057dae | |||
3bb0308949 | |||
0da7c3ab23 | |||
c196646955 | |||
3c3ee0565a | |||
98a2ce93fe | |||
7236e9f04e | |||
b81144153b | |||
84bc6d79f5 | |||
c698ac3499 | |||
e4484b6873 | |||
f330dbde33 | |||
bb2403adad | |||
f7e4116b42 | |||
50f27010f0 | |||
5ec05e3db7 | |||
5a837cc373 | |||
317d2e6de3 |
26
.gitignore
vendored
@ -1,23 +1,7 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
node_modules
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
|
||||
# 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*
|
||||
**/dist/**/*
|
||||
static/dist
|
||||
|
46
README.md
@ -1,46 +1,2 @@
|
||||
# Getting Started with Create React App
|
||||
# dominionator
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## 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 can’t go back!**
|
||||
|
||||
If you aren’t 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 you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t 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/).
|
||||
|
16
cards.js
Normal file
@ -0,0 +1,16 @@
|
||||
const cards = [
|
||||
"Chateau",
|
||||
"Consul",
|
||||
"Eclipse",
|
||||
"Flask",
|
||||
"Foundry",
|
||||
"Moonlit_Scheme",
|
||||
"Productive_Village",
|
||||
"Retainer",
|
||||
"Secret_Society",
|
||||
"Shovel",
|
||||
"Silk",
|
||||
"Steelworker",
|
||||
"Vase",
|
||||
"Vendor",
|
||||
]
|
BIN
cards/Beaver_v0.1.png
Normal file
After Width: | Height: | Size: 1.7 MiB |
BIN
cards/Birdly_v0.1.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
cards/Catastrophe_v0.1.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
cards/Chateau_v0.1.png
Normal file
After Width: | Height: | Size: 2.1 MiB |
BIN
cards/Consul_v0.1.png
Normal file
After Width: | Height: | Size: 1.8 MiB |
BIN
cards/Discover_v0.1.png
Normal file
After Width: | Height: | Size: 3.3 MiB |
BIN
cards/Eclipse_v0.1.png
Normal file
After Width: | Height: | Size: 1.6 MiB |
BIN
cards/Flask_v0.1.png
Normal file
After Width: | Height: | Size: 2.8 MiB |
BIN
cards/Foundry_v0.1.png
Normal file
After Width: | Height: | Size: 2.0 MiB |
BIN
cards/High_Council_v0.1.png
Normal file
After Width: | Height: | Size: 3.6 MiB |
BIN
cards/Leasing_v0.1.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
cards/Merchant’s_Favor_v0.1.png
Normal file
After Width: | Height: | Size: 3.7 MiB |
BIN
cards/Moneypress_v0.1.png
Normal file
After Width: | Height: | Size: 3.4 MiB |
BIN
cards/Moonlit_Scheme_v0.1.png
Normal file
After Width: | Height: | Size: 1.6 MiB |
BIN
cards/Mountains_v0.1.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
cards/Occult_v0.1.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
cards/Penny_Pincher_v0.1.png
Normal file
After Width: | Height: | Size: 2.1 MiB |
BIN
cards/Productive_Village_v0.1.png
Normal file
After Width: | Height: | Size: 2.0 MiB |
BIN
cards/Prospector_v0.1.png
Normal file
After Width: | Height: | Size: 3.0 MiB |
BIN
cards/Prospering_v0.1.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
cards/Retainer_v0.1.png
Normal file
After Width: | Height: | Size: 2.1 MiB |
BIN
cards/Scientist_v0.1.png
Normal file
After Width: | Height: | Size: 3.4 MiB |
BIN
cards/Secret_Society_v0.1.png
Normal file
After Width: | Height: | Size: 2.0 MiB |
BIN
cards/Service_v0.1.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
cards/Shovel_v0.1.png
Normal file
After Width: | Height: | Size: 2.1 MiB |
BIN
cards/Silk_v0.1.png
Normal file
After Width: | Height: | Size: 2.1 MiB |
BIN
cards/Steelworker_v0.1.png
Normal file
After Width: | Height: | Size: 2.0 MiB |
BIN
cards/Surplus_v0.1.png
Normal file
After Width: | Height: | Size: 1.2 MiB |
BIN
cards/Timekeeping_v0.1.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
cards/Toll_v0.1.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
cards/Trash_Collector_v0.1.png
Normal file
After Width: | Height: | Size: 3.6 MiB |
BIN
cards/Vase_v0.1.png
Normal file
After Width: | Height: | Size: 3.9 MiB |
BIN
cards/Vendor_v0.1.png
Normal file
After Width: | Height: | Size: 1.9 MiB |
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
|
||||
}
|
||||
}
|
45
index.html
Normal file
@ -0,0 +1,45 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Dominionator</title>
|
||||
<style>
|
||||
body {
|
||||
display: block;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
img {
|
||||
display: inline-block;
|
||||
}
|
||||
.break {
|
||||
page-break-after: always;
|
||||
}
|
||||
.card {
|
||||
width: 2.25in;
|
||||
break-inside: avoid;
|
||||
}
|
||||
.card.horizontal {
|
||||
width: auto;
|
||||
height: 2.25in;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="cards"></div>
|
||||
<script src="cards.js"></script>
|
||||
<script>
|
||||
const cardsDiv = document.getElementById("cards");
|
||||
const addCard = (card) => {
|
||||
cardsDiv.innerHTML += `<img class="card" src="./cards/${card}_v0.1.png"/>`
|
||||
}
|
||||
// // randomizers
|
||||
// for (const card of cards) {
|
||||
// addCard(card);
|
||||
// }
|
||||
// cards
|
||||
for (const card of cards) {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
addCard(card);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
29401
package-lock.json
generated
48
package.json
@ -1,48 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 3.8 KiB |
@ -1,43 +0,0 @@
|
||||
<!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>
|
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 9.4 KiB |
@ -1,25 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
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));
|
38
src/App.css
@ -1,38 +0,0 @@
|
||||
.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);
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
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
@ -1,23 +0,0 @@
|
||||
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
@ -1,43 +0,0 @@
|
||||
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>
|
||||
}
|
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);
|
||||
};
|
@ -1,46 +0,0 @@
|
||||
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;
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
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();
|
0
src/isocanvas.ts
Normal file
@ -1 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 2.6 KiB |
1
src/react-app-env.d.ts
vendored
@ -1 +0,0 @@
|
||||
/// <reference types="react-scripts" />
|
@ -1,15 +0,0 @@
|
||||
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;
|
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");
|
||||
}
|
||||
});
|
@ -1,5 +0,0 @@
|
||||
// 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';
|
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 |