Compare commits
	
		
			9 Commits
		
	
	
		
			drawer
			...
			3c3ee0565a
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 3c3ee0565a | ||
|  | 98a2ce93fe | ||
|  | 84bc6d79f5 | ||
|  | c698ac3499 | ||
|  | e4484b6873 | ||
|  | f330dbde33 | ||
|  | bb2403adad | ||
|  | f7e4116b42 | ||
|  | 50f27010f0 | 
							
								
								
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,7 +0,0 @@ | |||||||
| node_modules |  | ||||||
|  |  | ||||||
| # dotenv environment variable files |  | ||||||
| .env |  | ||||||
|  |  | ||||||
| **/dist/**/* |  | ||||||
| static/dist |  | ||||||
							
								
								
									
										16
									
								
								cards.js
									
									
									
									
									
								
							
							
						
						| @@ -1,6 +1,16 @@ | |||||||
| const cards = [ | const cards = [ | ||||||
| 	"Discover", | 	"Chateau", | ||||||
| 	"Prospector", | 	"Consul", | ||||||
| 	"Scientist", | 	"Eclipse", | ||||||
|  | 	"Flask", | ||||||
|  | 	"Foundry", | ||||||
|  | 	"Moonlit_Scheme", | ||||||
|  | 	"Productive_Village", | ||||||
|  | 	"Retainer", | ||||||
|  | 	"Secret_Society", | ||||||
|  | 	"Shovel", | ||||||
|  | 	"Silk", | ||||||
|  | 	"Steelworker", | ||||||
| 	"Vase", | 	"Vase", | ||||||
|  | 	"Vendor", | ||||||
| ] | ] | ||||||
							
								
								
									
										
											BIN
										
									
								
								cards/Beaver_v0.1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.7 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/Eclipse_v0.1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.6 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/Merchant’s_Favor_v0.1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.7 MiB | 
							
								
								
									
										
											BIN
										
									
								
								cards/Moonlit_Scheme_v0.1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.6 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/Retainer_v0.1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.1 MiB | 
							
								
								
									
										
											BIN
										
									
								
								cards/Secret_Society_v0.1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.0 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/Vendor_v0.1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.9 MiB | 
							
								
								
									
										42
									
								
								deno.json
									
									
									
									
									
								
							
							
						
						| @@ -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 |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -30,10 +30,10 @@ | |||||||
| 			const addCard = (card) => { | 			const addCard = (card) => { | ||||||
| 				cardsDiv.innerHTML += `<img class="card" src="./cards/${card}_v0.1.png"/>` | 				cardsDiv.innerHTML += `<img class="card" src="./cards/${card}_v0.1.png"/>` | ||||||
| 			} | 			} | ||||||
| 			// randomizers | 			// // randomizers | ||||||
| 			for (const card of cards) { | 			// for (const card of cards) { | ||||||
| 				addCard(card); | 			// 	addCard(card); | ||||||
| 			} | 			// } | ||||||
| 			// cards | 			// cards | ||||||
| 			for (const card of cards) { | 			for (const card of cards) { | ||||||
| 				for (let i = 0; i < 10; i++) { | 				for (let i = 0; i < 10; i++) { | ||||||
|   | |||||||
							
								
								
									
										1230
									
								
								reference.js
									
									
									
									
									
								
							
							
						
						
							
								
								
									
										3
									
								
								root.ts
									
									
									
									
									
								
							
							
						
						| @@ -1,3 +0,0 @@ | |||||||
| import { dirname, fromFileUrl } from "jsr:@std/path"; |  | ||||||
|  |  | ||||||
| export const projectRootDir = dirname(fromFileUrl(import.meta.url)); |  | ||||||
							
								
								
									
										304
									
								
								src/cards.ts
									
									
									
									
									
								
							
							
						
						| @@ -1,304 +0,0 @@ | |||||||
| 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, |  | ||||||
| 	}, |  | ||||||
| ]; |  | ||||||
| @@ -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>; |  | ||||||
| }; |  | ||||||
| @@ -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> |  | ||||||
| } |  | ||||||
| @@ -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>, |  | ||||||
| ); |  | ||||||
| @@ -1,5 +0,0 @@ | |||||||
| import parseColor1 from "npm:parse-color"; |  | ||||||
|  |  | ||||||
| export const parseColor = (c: string): { rgb: [number, number, number] } => { |  | ||||||
| 	return parseColor1(c); |  | ||||||
| }; |  | ||||||
| @@ -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; |  | ||||||
| }; |  | ||||||
							
								
								
									
										405
									
								
								src/draw.ts
									
									
									
									
									
								
							
							
						
						| @@ -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 |  | ||||||
| }; |  | ||||||
| @@ -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); |  | ||||||
| }; |  | ||||||
| @@ -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(); |  | ||||||
| // }; |  | ||||||
| @@ -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: "", |  | ||||||
| 	}, |  | ||||||
| ]; |  | ||||||
| @@ -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"); |  | ||||||
| 	} |  | ||||||
| }); |  | ||||||
							
								
								
									
										171
									
								
								src/types.ts
									
									
									
									
									
								
							
							
						
						| @@ -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, |  | ||||||
| }; |  | ||||||
| Before Width: | Height: | Size: 624 KiB | 
| Before Width: | Height: | Size: 484 KiB | 
| Before Width: | Height: | Size: 80 KiB | 
| Before Width: | Height: | Size: 36 KiB | 
| Before Width: | Height: | Size: 142 KiB | 
| Before Width: | Height: | Size: 1.6 MiB | 
| Before Width: | Height: | Size: 1.0 MiB | 
| Before Width: | Height: | Size: 1.0 MiB | 
| Before Width: | Height: | Size: 1.2 MiB | 
| Before Width: | Height: | Size: 1.3 MiB | 
| Before Width: | Height: | Size: 1.0 MiB | 
| Before Width: | Height: | Size: 117 KiB | 
| Before Width: | Height: | Size: 48 KiB | 
| Before Width: | Height: | Size: 49 KiB | 
| Before Width: | Height: | Size: 41 KiB | 
| Before Width: | Height: | Size: 1.2 MiB | 
| Before Width: | Height: | Size: 960 KiB | 
| Before Width: | Height: | Size: 466 KiB | 
| Before Width: | Height: | Size: 265 KiB | 
| Before Width: | Height: | Size: 90 KiB | 
| Before Width: | Height: | Size: 353 KiB | 
| Before Width: | Height: | Size: 291 KiB | 
| Before Width: | Height: | Size: 200 KiB | 
| Before Width: | Height: | Size: 175 KiB | 
| Before Width: | Height: | Size: 157 KiB | 
| Before Width: | Height: | Size: 113 KiB | 
| Before Width: | Height: | Size: 131 KiB | 
| Before Width: | Height: | Size: 916 KiB | 
| Before Width: | Height: | Size: 735 KiB | 
| Before Width: | Height: | Size: 80 KiB | 
| Before Width: | Height: | Size: 23 KiB | 
| Before Width: | Height: | Size: 32 KiB | 
| Before Width: | Height: | Size: 316 KiB | 
| Before Width: | Height: | Size: 88 KiB | 
| Before Width: | Height: | Size: 384 KiB | 
| Before Width: | Height: | Size: 140 KiB | 
| Before Width: | Height: | Size: 88 KiB | 
| Before Width: | Height: | Size: 50 KiB | 
| Before Width: | Height: | Size: 38 KiB | 
| @@ -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'); |  | ||||||
| } */ |  | ||||||
| @@ -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> |  | ||||||
| @@ -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(); |  | ||||||
							
								
								
									
										37
									
								
								tools/dev.ts
									
									
									
									
									
								
							
							
						
						| @@ -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; |  | ||||||
| } |  | ||||||