diff --git a/.prettierrc b/.prettierrc
index 8b137891..9e26dfee 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1 +1 @@
-
+{}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 36a5875b..68eb7208 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,6 +18,7 @@
"pg": "^8.5.1",
"react": "17.0.1",
"react-dom": "17.0.1",
+ "react-textfit": "^1.1.1",
"short-uuid": "^4.1.0",
"sqlite3": "^5.0.0"
},
@@ -30,6 +31,7 @@
"@types/puppeteer": "^5.4.2",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
+ "@types/react-textfit": "^1.1.0",
"@types/sqlite3": "^3.1.6",
"@typescript-eslint/eslint-plugin": "^4.11.0",
"@typescript-eslint/parser": "^4.11.0",
@@ -39,7 +41,7 @@
"typescript": "^4.1.3"
},
"engines": {
- "node": "12.x"
+ "node": ">=12.x"
}
},
"node_modules/@ampproject/toolbox-core": {
@@ -629,6 +631,15 @@
"@types/react": "*"
}
},
+ "node_modules/@types/react-textfit": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@types/react-textfit/-/react-textfit-1.1.0.tgz",
+ "integrity": "sha512-iF49wuf4TMUKxcQjKyZcaJRN8rKFrXUBIAo0KQ0O/orHLQpwCyxpiD8NlPRqmcRN3FOAxSiRAOENbEQF1bhqiQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
"node_modules/@types/sinonjs__fake-timers": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.2.tgz",
@@ -8228,6 +8239,19 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-textfit": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/react-textfit/-/react-textfit-1.1.1.tgz",
+ "integrity": "sha512-UDSQRo5yBEGueLTE5SgNV9fSmr5CWJkE0E0R0YbcbCO69iuJGfcT6wspKhX2sIwdsDyT9qXOwMC80cnRolir7Q==",
+ "dependencies": {
+ "process": "^0.11.10",
+ "prop-types": "^15.7.2"
+ },
+ "peerDependencies": {
+ "react": "^15.0.0 || ^16.0.0",
+ "react-dom": "^15.0.0 || ^16.0.0"
+ }
+ },
"node_modules/readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
@@ -11771,6 +11795,15 @@
"@types/react": "*"
}
},
+ "@types/react-textfit": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@types/react-textfit/-/react-textfit-1.1.0.tgz",
+ "integrity": "sha512-iF49wuf4TMUKxcQjKyZcaJRN8rKFrXUBIAo0KQ0O/orHLQpwCyxpiD8NlPRqmcRN3FOAxSiRAOENbEQF1bhqiQ==",
+ "dev": true,
+ "requires": {
+ "@types/react": "*"
+ }
+ },
"@types/sinonjs__fake-timers": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.2.tgz",
@@ -17825,6 +17858,15 @@
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz",
"integrity": "sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg=="
},
+ "react-textfit": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/react-textfit/-/react-textfit-1.1.1.tgz",
+ "integrity": "sha512-UDSQRo5yBEGueLTE5SgNV9fSmr5CWJkE0E0R0YbcbCO69iuJGfcT6wspKhX2sIwdsDyT9qXOwMC80cnRolir7Q==",
+ "requires": {
+ "process": "^0.11.10",
+ "prop-types": "^15.7.2"
+ }
+ },
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
diff --git a/package.json b/package.json
index 4a3490c9..42671bf2 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "everdell",
"version": "0.1.0",
"engines": {
- "node": "12.x"
+ "node": ">=12.x"
},
"private": true,
"scripts": {
@@ -32,6 +32,7 @@
"pg": "^8.5.1",
"react": "17.0.1",
"react-dom": "17.0.1",
+ "react-textfit": "^1.1.1",
"short-uuid": "^4.1.0",
"sqlite3": "^5.0.0"
},
@@ -44,6 +45,7 @@
"@types/puppeteer": "^5.4.2",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
+ "@types/react-textfit": "^1.1.0",
"@types/sqlite3": "^3.1.6",
"@typescript-eslint/eslint-plugin": "^4.11.0",
"@typescript-eslint/parser": "^4.11.0",
diff --git a/public/images/legendary_card.png b/public/images/legendary_card.png
new file mode 100644
index 00000000..17e1b850
Binary files /dev/null and b/public/images/legendary_card.png differ
diff --git a/src/components/Card.tsx b/src/components/Card.tsx
index 50e0ac7f..628bedc0 100644
--- a/src/components/Card.tsx
+++ b/src/components/Card.tsx
@@ -1,7 +1,13 @@
import * as React from "react";
+import { Textfit } from "react-textfit";
import { Card as CardModel } from "../model/card";
import styles from "../styles/card.module.css";
-import { ResourceType, CardName, PlayedCardInfo } from "../model/types";
+import {
+ ResourceType,
+ CardName,
+ PlayedCardInfo,
+ ExpansionType,
+} from "../model/types";
import { Player } from "../model/player";
import { resourceMapToGameText, toGameText } from "../model/gameText";
import {
@@ -19,6 +25,7 @@ const colorClassMap = {
PRODUCTION: styles.color_production,
DESTINATION: styles.color_destination,
TRAVELER: styles.color_traveler,
+ LEGEND: styles.color_legend,
};
function romanize(num: number): string {
@@ -65,7 +72,12 @@ function romanize(num: number): string {
// determine rarity label, which is unique vs. common and
// critter vs. construction
const getRarityLabel = (card: CardModel) => {
- const rarity = card.isUnique ? "Unique" : "Common";
+ const rarity =
+ card.expansion === ExpansionType.LEGENDS
+ ? "Legendary"
+ : card.isUnique
+ ? "Unique"
+ : "Common";
const category = card.isCritter ? "Critter" : "Construction";
return rarity + " " + category;
};
@@ -75,14 +87,12 @@ const getAssociatedCard = (card: CardModel) => {
if (card.associatedCard) {
return card.associatedCard;
} else {
- if (card.name == CardName.FARM) {
+ if (card.name == CardName.FARM || card.name == CardName.MCGREGORS_MARKET) {
return "Husband / Wife";
} else if (card.name == CardName.EVERTREE) {
return "Any";
} else {
- throw new Error(
- "Associated card is null and card is not Farm or Evertree"
- );
+ <> >;
}
}
};
@@ -156,6 +166,18 @@ const AssociatedCard = ({
);
};
+const UpgradeableCard = ({ card }: { card: CardModel }) => {
+ if (!card.upgradeableCard) {
+ return <> >;
+ }
+
+ return (
+
+ {card.upgradeableCard}
+
+ );
+};
+
const Card: React.FC<{ name: CardName; usedForCritter?: boolean }> = ({
name,
usedForCritter = false,
@@ -166,25 +188,29 @@ const Card: React.FC<{ name: CardName; usedForCritter?: boolean }> = ({
return (
<>
-
-
+
+
-
- {card.baseVP}
-
-
-
{name}
+
+
+
+ {name}
+
+
{card.expansion && (
{card.expansion}
)}
+
+ {card.baseVP}
+
+
+
+ {rarityLabel}
+ · {romanize(card.numInDeck)}
@@ -221,10 +247,7 @@ const Card: React.FC<{ name: CardName; usedForCritter?: boolean }> = ({
-
- {rarityLabel}
- · {romanize(card.numInDeck)}
-
+
diff --git a/src/components/CardPayment.tsx b/src/components/CardPayment.tsx
index 95f32d52..79eaa633 100644
--- a/src/components/CardPayment.tsx
+++ b/src/components/CardPayment.tsx
@@ -3,7 +3,12 @@ import { Field, useField } from "formik";
import { Card as CardModel } from "../model/card";
import { Player } from "../model/player";
-import { ResourceType, CardName, GameInputPlayCard } from "../model/types";
+import {
+ ResourceType,
+ CardName,
+ GameInputPlayCard,
+ ExpansionType,
+} from "../model/types";
import { ResourceTypeIcon, Description } from "./common";
import styles from "../styles/CardPayment.module.css";
@@ -95,6 +100,7 @@ const OptionToUseAssociatedCard: React.FC<{
const [_field, meta, helpers] = useField(name);
const card = CardModel.fromName(cardName);
const hasUnusedAssociatedCard =
+ card.expansion !== ExpansionType.LEGENDS &&
card.associatedCard &&
viewingPlayer.hasUnusedByCritterConstruction(card.associatedCard);
const hasUnusedEvertree = viewingPlayer.hasUnusedByCritterConstruction(
@@ -102,8 +108,9 @@ const OptionToUseAssociatedCard: React.FC<{
);
const canUseAssociatedCard =
card.isCritter && (hasUnusedAssociatedCard || hasUnusedEvertree);
+
if (!canUseAssociatedCard) {
- return <>>;
+ return null;
}
const isChecked = !!meta.value;
@@ -231,6 +238,32 @@ const CardToDungeonForm: React.FC<{
) : null;
};
+const CardToUpgradeForm: React.FC<{
+ name: string;
+ cardName: CardName;
+ viewingPlayer: Player;
+}> = ({ name, cardName }) => {
+ const card = CardModel.fromName(cardName);
+ if (card.expansion !== ExpansionType.LEGENDS || !card.upgradeableCard) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+};
+
const CardPayment: React.FC<{
name: string;
resetPaymentOptions: (state: "DEFAULT" | "COST" | "ZERO") => void;
@@ -247,7 +280,9 @@ const CardPayment: React.FC<{
resetPaymentOptions={resetPaymentOptions}
/>
)}
-
+ {clientOptions.paymentOptions.resources && (
+
+ )}
{clientOptions.card && (
+ {clientOptions.card && (
+
+ )}
);
};
diff --git a/src/components/GameBuilder.tsx b/src/components/GameBuilder.tsx
index bd65912c..0f2e172d 100644
--- a/src/components/GameBuilder.tsx
+++ b/src/components/GameBuilder.tsx
@@ -110,6 +110,10 @@ const GameBuilder: React.FC = () => {
{"Pearlbrook"}
+
Settings
diff --git a/src/components/GameInputPlayCard.tsx b/src/components/GameInputPlayCard.tsx
index c6cd7ca1..29863eba 100644
--- a/src/components/GameInputPlayCard.tsx
+++ b/src/components/GameInputPlayCard.tsx
@@ -3,7 +3,7 @@ import { useField } from "formik";
import styles from "../styles/gameBoard.module.css";
-import { CardName, ResourceType } from "../model/types";
+import { CardName, ExpansionType, ResourceType } from "../model/types";
import { Player } from "../model/player";
import { Card as CardModel } from "../model/card";
@@ -22,6 +22,9 @@ const GameInputPlayCard: React.FC<{
overrides: any = {}
) => {
const card = CardModel.fromName(cardName);
+ const canUpgradeCard =
+ card.expansion == ExpansionType.LEGENDS &&
+ viewingPlayer.hasCardInCity(card.upgradeableCard!);
const canUseAssociatedCard =
card.isCritter &&
card.associatedCard &&
@@ -34,16 +37,20 @@ const GameInputPlayCard: React.FC<{
paymentOptions: {
cardToUse: null,
cardToDungeon: null,
+ cardToUpgrade: canUpgradeCard ? card.upgradeableCard : null,
useAssociatedCard: state === "DEFAULT" ? canUseAssociatedCard : false,
- resources: {
- [ResourceType.BERRY]: 0,
- [ResourceType.TWIG]: 0,
- [ResourceType.RESIN]: 0,
- [ResourceType.PEBBLE]: 0,
- ...((state === "DEFAULT" && !canUseAssociatedCard) || state === "COST"
- ? card.baseCost
- : {}),
- },
+ resources: canUpgradeCard
+ ? null
+ : {
+ [ResourceType.BERRY]: 0,
+ [ResourceType.TWIG]: 0,
+ [ResourceType.RESIN]: 0,
+ [ResourceType.PEBBLE]: 0,
+ ...((state === "DEFAULT" && !canUseAssociatedCard) ||
+ state === "COST"
+ ? card.baseCost
+ : {}),
+ },
},
});
};
diff --git a/src/components/Players.tsx b/src/components/Players.tsx
index a294e298..6e47b6d5 100644
--- a/src/components/Players.tsx
+++ b/src/components/Players.tsx
@@ -11,7 +11,7 @@ import {
import { PlayerJSON, GameStateJSON } from "../model/jsonTypes";
import { Player } from "../model/player";
import { GameState } from "../model/gameState";
-import { GameBlock } from "./common";
+import { GameBlock, LegendaryCardIcon } from "./common";
import { PlayerCity } from "./gameBoard";
import {
InfoIconSvg,
@@ -237,6 +237,19 @@ const PlayerStatus: React.FC<{
)}
+ {gameStateJSON.gameOptions.legends && (
+
+
+
+
+
+ {player.numLegendsInHand}
+
+
+ )}
{"WORKERS"}
diff --git a/src/components/ViewerUI.tsx b/src/components/ViewerUI.tsx
index b5337fd3..1bd724ba 100644
--- a/src/components/ViewerUI.tsx
+++ b/src/components/ViewerUI.tsx
@@ -22,6 +22,11 @@ const ViewerUI: React.FC<{
{player.adornmentsInHand.map((name, idx) => (
))}
+ {player.legendsInHand.map((name, idx) => (
+
+
+
+ ))}
diff --git a/src/components/common.tsx b/src/components/common.tsx
index bc48b5fd..7766eda8 100644
--- a/src/components/common.tsx
+++ b/src/components/common.tsx
@@ -86,6 +86,16 @@ export const AdornmentCardIcon = () => {
);
};
+export const LegendaryCardIcon = () => {
+ return (
+
+ );
+};
+
export const WorkerSpotIcon = ({ locked = false }: { locked?: boolean }) => {
if (locked) {
return (
@@ -290,7 +300,6 @@ export const Description = ({ textParts }: { textParts: GameText }) => {
);
}
assertUnreachable(part, `Unexpected part: ${JSON.stringify(part)}`);
- break;
default:
assertUnreachable(part, `Unexpected part: ${JSON.stringify(part)}`);
}
diff --git a/src/model/card.ts b/src/model/card.ts
index 3f480ac2..3fe980cd 100644
--- a/src/model/card.ts
+++ b/src/model/card.ts
@@ -44,6 +44,19 @@ import {
} from "./gameText";
import { assertUnreachable } from "../utils";
+import {
+ amillaGlistendew,
+ bridgeOfTheSky,
+ cirrusWindfall,
+ foresight,
+ fynnNobletail,
+ mcgregorsMarket,
+ oleandersOperaHouse,
+ poe,
+ silverScaleSpring,
+ theGreenAcorn,
+} from "./cards/legends";
+
type NumWorkersInnerFn = (cardOwner: Player) => number;
type ProductionInnerFn = (
gameState: GameState,
@@ -83,6 +96,7 @@ export class Card
readonly expansion: ExpansionType | null;
readonly isConstruction: boolean;
readonly associatedCard: CardName | null;
+ readonly upgradeableCard: CardName | null;
readonly isOpenDestination: boolean;
readonly productionInner: ProductionInnerFn | undefined;
@@ -102,6 +116,7 @@ export class Card
isUnique,
isConstruction,
associatedCard,
+ upgradeableCard,
resourcesToGain,
productionInner,
productionWillActivateInner,
@@ -122,7 +137,8 @@ export class Card
isUnique: boolean;
numInDeck: number;
isConstruction: boolean;
- associatedCard: CardName | null;
+ associatedCard?: CardName | null;
+ upgradeableCard?: CardName | null;
isOpenDestination?: boolean;
expansion?: ExpansionType | null;
playInner?: GameStatePlayFn;
@@ -157,7 +173,8 @@ export class Card
this.isUnique = isUnique;
this.isCritter = !isConstruction;
this.isConstruction = isConstruction;
- this.associatedCard = associatedCard;
+ this.associatedCard = associatedCard || null;
+ this.upgradeableCard = upgradeableCard || null;
this.isOpenDestination = isOpenDestination;
this.playInner = playInner;
this.canPlayCheckInner = canPlayCheckInner;
@@ -254,7 +271,8 @@ export class Card
}
if (
!gameInput.clientOptions.fromMeadow &&
- player.cardsInHand.indexOf(this.name) === -1
+ player.cardsInHand.indexOf(this.name) === -1 &&
+ player.legendsInHand.indexOf(this.name) === -1
) {
return `Card ${
this.name
@@ -601,7 +619,11 @@ const CARD_REGISTRY: Record = {
" in your city.",
]),
// 1 point per common construction
- pointsInner: getPointsPerRarityLabel({ isCritter: false, isUnique: false }),
+ pointsInner: getPointsPerRarityLabel({
+ isCritter: false,
+ isUnique: false,
+ pointsEach: 1,
+ }),
}),
[CardName.CEMETARY]: new Card({
name: CardName.CEMETARY,
@@ -2016,7 +2038,11 @@ const CARD_REGISTRY: Record = {
" in your city",
]),
// 1 point per unique construction
- pointsInner: getPointsPerRarityLabel({ isCritter: false, isUnique: true }),
+ pointsInner: getPointsPerRarityLabel({
+ isCritter: false,
+ isUnique: true,
+ pointsEach: 1,
+ }),
}),
[CardName.PEDDLER]: new Card({
name: CardName.PEDDLER,
@@ -2708,7 +2734,11 @@ const CARD_REGISTRY: Record = {
" in your city.",
]),
// 1 point per common critter
- pointsInner: getPointsPerRarityLabel({ isCritter: true, isUnique: false }),
+ pointsInner: getPointsPerRarityLabel({
+ isCritter: true,
+ isUnique: false,
+ pointsEach: 1,
+ }),
}),
[CardName.SHEPHERD]: new Card({
name: CardName.SHEPHERD,
@@ -3128,7 +3158,11 @@ const CARD_REGISTRY: Record = {
" in your city.",
]),
// 1 point per unique critter
- pointsInner: getPointsPerRarityLabel({ isCritter: true, isUnique: true }),
+ pointsInner: getPointsPerRarityLabel({
+ isCritter: true,
+ isUnique: true,
+ pointsEach: 1,
+ }),
}),
[CardName.TWIG_BARGE]: new Card({
name: CardName.TWIG_BARGE,
@@ -3952,25 +3986,41 @@ const CARD_REGISTRY: Record = {
}
},
}),
+
+ /**
+ * WIP: Legends Cards
+ */
+ [CardName.AMILLA_GLISTENDEW]: new Card(amillaGlistendew),
+ [CardName.BRIDGE_OF_THE_SKY]: new Card(bridgeOfTheSky),
+ [CardName.CIRRUS_WINDFALL]: new Card(cirrusWindfall),
+ [CardName.FORESIGHT]: new Card(foresight),
+ [CardName.FYNN_NOBLETAIL]: new Card(fynnNobletail),
+ [CardName.MCGREGORS_MARKET]: new Card(mcgregorsMarket),
+ [CardName.OLEANDERS_OPERA_HOUSE]: new Card(oleandersOperaHouse),
+ [CardName.POE]: new Card(poe),
+ [CardName.SILVER_SCALE_SPRING]: new Card(silverScaleSpring),
+ [CardName.THE_GREEN_ACORN]: new Card(theGreenAcorn),
};
-function getPointsPerRarityLabel({
+export function getPointsPerRarityLabel({
isCritter,
isUnique,
+ pointsEach,
}: {
isCritter: boolean;
isUnique: boolean;
+ pointsEach: number;
}): GameStateCountPointsFn {
return (gameState: GameState, playerId: string) => {
const player = gameState.getPlayer(playerId);
- let numCardsToCount = 0;
+ let numPoints = 0;
player.forEachPlayedCard(({ cardName }) => {
const card = Card.fromName(cardName as CardName);
if (card.isCritter === isCritter && card.isUnique === isUnique) {
- numCardsToCount++;
+ numPoints += pointsEach;
}
});
- return numCardsToCount;
+ return numPoints;
};
}
diff --git a/src/model/cards/legends/amillaGlistendew.ts b/src/model/cards/legends/amillaGlistendew.ts
new file mode 100644
index 00000000..cde835c1
--- /dev/null
+++ b/src/model/cards/legends/amillaGlistendew.ts
@@ -0,0 +1,54 @@
+import { Card } from "../../card";
+import { GameState } from "../../gameState";
+import { toGameText } from "../../gameText";
+import {
+ CardName,
+ CardType,
+ ExpansionType,
+ GameInput,
+ GameInputType,
+ ResourceType,
+} from "../../types";
+
+export const amillaGlistendew: ConstructorParameters[0] = {
+ expansion: ExpansionType.LEGENDS,
+ name: CardName.AMILLA_GLISTENDEW,
+ upgradeableCard: CardName.QUEEN,
+ cardType: CardType.DESTINATION,
+ cardDescription: toGameText(
+ "Achieve an Event, even if you don't meet the listed requirements."
+ ),
+ isConstruction: false,
+ isUnique: false,
+ baseVP: 5,
+ numInDeck: 1,
+ resourcesToGain: {},
+ baseCost: {
+ [ResourceType.BERRY]: 6,
+ },
+ canPlayCheckInner: (gameState: GameState, gameInput: GameInput) => {
+ if (gameInput.inputType === GameInputType.VISIT_DESTINATION_CARD) {
+ if (
+ Object.entries(gameState.eventsMap).every(
+ ([eventName, playerId]) => playerId !== null
+ )
+ ) {
+ return "No playable events";
+ }
+ }
+ return null;
+ },
+ playInner: (gameState: GameState, gameInput: GameInput) => {
+ if (gameInput.inputType === GameInputType.VISIT_DESTINATION_CARD) {
+ gameState.pendingGameInputs.push({
+ inputType: GameInputType.CLAIM_EVENT,
+ prevInputType: gameInput.inputType,
+ label: "Select an EVENT to play for free",
+ cardContext: CardName.AMILLA_GLISTENDEW,
+ clientOptions: {
+ event: null,
+ },
+ });
+ }
+ },
+};
diff --git a/src/model/cards/legends/bridgeOfTheSky.ts b/src/model/cards/legends/bridgeOfTheSky.ts
new file mode 100644
index 00000000..cedaba88
--- /dev/null
+++ b/src/model/cards/legends/bridgeOfTheSky.ts
@@ -0,0 +1,40 @@
+import { Card } from "../../card";
+import { GameState } from "../../gameState";
+import { toGameText } from "../../gameText";
+import {
+ CardName,
+ CardType,
+ ExpansionType,
+ GameInput,
+ ResourceType,
+} from "../../types";
+
+export const bridgeOfTheSky: ConstructorParameters[0] = {
+ expansion: ExpansionType.LEGENDS,
+ name: CardName.BRIDGE_OF_THE_SKY,
+ associatedCard: CardName.ARCHITECT,
+ upgradeableCard: CardName.CRANE,
+ cardType: CardType.GOVERNANCE,
+ cardDescription: toGameText([
+ "You may play 1 ",
+ { type: "em", text: "Construction " },
+ "for -3 ANY, then place it on top of this card. ",
+ { type: "HR" },
+ "PLUS is equal to the ",
+ { type: "points", value: 0 },
+ " value of the ",
+ { type: "em", text: "Construction" },
+ " on top of this card.",
+ ]),
+ isConstruction: true,
+ isUnique: false,
+ baseVP: 0,
+ numInDeck: 1,
+ resourcesToGain: {},
+ baseCost: {
+ [ResourceType.PEBBLE]: 2,
+ },
+ playInner: (gameState: GameState, gameInput: GameInput) => {
+ // TODO: Implement this
+ },
+};
diff --git a/src/model/cards/legends/cirrusWindfall.ts b/src/model/cards/legends/cirrusWindfall.ts
new file mode 100644
index 00000000..a2c5e712
--- /dev/null
+++ b/src/model/cards/legends/cirrusWindfall.ts
@@ -0,0 +1,37 @@
+import { Card } from "../../card";
+import { GameState } from "../../gameState";
+import { toGameText } from "../../gameText";
+import {
+ CardName,
+ CardType,
+ ExpansionType,
+ GameInput,
+ ResourceType,
+} from "../../types";
+
+export const cirrusWindfall: ConstructorParameters[0] = {
+ expansion: ExpansionType.LEGENDS,
+ name: CardName.CIRRUS_WINDFALL,
+ upgradeableCard: CardName.POSTAL_PIGEON,
+ cardType: CardType.TRAVELER,
+ cardDescription: toGameText([
+ "You may play 1 CARD worth up to ",
+ { type: "points", value: 3 },
+ " for free.",
+ ]),
+ isConstruction: false,
+ isUnique: false,
+ baseVP: 4,
+ numInDeck: 1,
+ resourcesToGain: {},
+ baseCost: {
+ [ResourceType.BERRY]: 4,
+ },
+ canPlayCheckInner: (gameState: GameState, gameInput: GameInput) => {
+ // TODO: Implement this
+ return null;
+ },
+ playInner: (gameState: GameState, gameInput: GameInput) => {
+ // TODO: Implement this
+ },
+};
diff --git a/src/model/cards/legends/foresight.test.ts b/src/model/cards/legends/foresight.test.ts
new file mode 100644
index 00000000..8f3d73d7
--- /dev/null
+++ b/src/model/cards/legends/foresight.test.ts
@@ -0,0 +1,76 @@
+import expect from "expect.js";
+import { Card } from "../../card";
+import { GameState } from "../../gameState";
+import { Player } from "../../player";
+import {
+ multiStepGameInputTest,
+ playCardInput,
+ testInitialGameState,
+} from "../../testHelpers";
+import { CardName, GameInputType, ResourceType } from "../../types";
+
+describe(CardName.FORESIGHT, () => {
+ let gameState: GameState;
+ let player: Player;
+
+ beforeEach(() => {
+ gameState = testInitialGameState();
+ player = gameState.getActivePlayer();
+ });
+
+ it("should be worth 4 VP", () => {
+ expect(Card.fromName(CardName.FORESIGHT).baseVP).to.equal(4);
+ });
+
+ it("should draw 2 cards if player plays a critter", () => {
+ player.addToCity(gameState, CardName.FORESIGHT);
+
+ const cardToPlay = Card.fromName(CardName.MINER_MOLE);
+ player.cardsInHand.push(cardToPlay.name);
+ player.gainResources(gameState, cardToPlay.baseCost);
+
+ gameState.deck.addToStack(CardName.QUEEN);
+ gameState.deck.addToStack(CardName.KING);
+
+ [player, gameState] = multiStepGameInputTest(gameState, [
+ playCardInput(cardToPlay.name),
+ ]);
+
+ expect(player.cardsInHand).to.eql([CardName.KING, CardName.QUEEN]);
+ });
+
+ it("should gain a resource if player plays a construction", () => {
+ player.addToCity(gameState, CardName.FORESIGHT);
+
+ const cardToPlay = Card.fromName(CardName.MINE);
+ player.cardsInHand = [cardToPlay.name];
+ player.gainResources(gameState, cardToPlay.baseCost);
+
+ [player, gameState] = multiStepGameInputTest(gameState, [
+ playCardInput(cardToPlay.name),
+ {
+ inputType: GameInputType.SELECT_OPTION_GENERIC,
+ prevInputType: GameInputType.PLAY_CARD,
+ cardContext: CardName.FORESIGHT,
+ options: ["TWIG", "RESIN", "PEBBLE", "BERRY"],
+ clientOptions: {
+ selectedOption: "BERRY",
+ },
+ },
+ ]);
+
+ expect(player.getNumResourcesByType(ResourceType.PEBBLE)).to.be(1);
+ expect(player.getNumResourcesByType(ResourceType.BERRY)).to.be(1);
+ });
+
+ it("should not draw cards when the player plays the foresight", () => {
+ const cardToPlay = Card.fromName(CardName.FORESIGHT);
+ player.cardsInHand = [cardToPlay.name];
+ player.gainResources(gameState, cardToPlay.baseCost);
+
+ [player, gameState] = multiStepGameInputTest(gameState, [
+ playCardInput(cardToPlay.name),
+ ]);
+ expect(player.cardsInHand).to.eql([]);
+ });
+});
diff --git a/src/model/cards/legends/foresight.ts b/src/model/cards/legends/foresight.ts
new file mode 100644
index 00000000..3db0512e
--- /dev/null
+++ b/src/model/cards/legends/foresight.ts
@@ -0,0 +1,76 @@
+import { Card } from "../../card";
+import { GameState } from "../../gameState";
+import { toGameText } from "../../gameText";
+import {
+ CardName,
+ CardType,
+ ExpansionType,
+ GameInput,
+ GameInputType,
+ ResourceType,
+} from "../../types";
+
+export const foresight: ConstructorParameters[0] = {
+ expansion: ExpansionType.LEGENDS,
+ name: CardName.FORESIGHT,
+ upgradeableCard: CardName.HISTORIAN,
+ cardType: CardType.GOVERNANCE,
+ cardDescription: toGameText([
+ "Draw 2 CARD after you play a ",
+ { type: "em", text: "Critter" },
+ ". Gain 1 ANY after you play a ",
+ { type: "em", text: "Construction" },
+ ]),
+ isConstruction: false,
+ isUnique: false,
+ baseVP: 4,
+ numInDeck: 1,
+ resourcesToGain: {},
+ baseCost: {
+ [ResourceType.BERRY]: 4,
+ },
+ playInner: (gameState: GameState, gameInput: GameInput) => {
+ const player = gameState.getActivePlayer();
+ if (
+ gameInput.inputType === GameInputType.PLAY_CARD &&
+ gameInput.clientOptions.card !== CardName.FORESIGHT &&
+ gameInput.clientOptions.card
+ ) {
+ const card = Card.fromName(gameInput.clientOptions.card);
+ if (card.isCritter) {
+ player.drawCards(gameState, 2);
+ gameState.addGameLogFromCard(CardName.HISTORIAN, [
+ player,
+ ` drew 2 CARDS.`,
+ ]);
+ } else {
+ gameState.pendingGameInputs.push({
+ inputType: GameInputType.SELECT_OPTION_GENERIC,
+ label: "Select TWIG / RESIN / PEBBLE / BERRY",
+ prevInputType: gameInput.inputType,
+ cardContext: CardName.FORESIGHT,
+ options: ["TWIG", "RESIN", "PEBBLE", "BERRY"],
+ clientOptions: {
+ selectedOption: null,
+ },
+ });
+ }
+ } else if (
+ gameInput.inputType === GameInputType.SELECT_OPTION_GENERIC &&
+ gameInput.cardContext === CardName.FORESIGHT
+ ) {
+ const selectedOption = gameInput.clientOptions?.selectedOption || "";
+ if (["TWIG", "RESIN", "PEBBLE", "BERRY"].indexOf(selectedOption) === -1) {
+ throw new Error("Invalid input");
+ }
+
+ player.gainResources(gameState, {
+ [selectedOption]: 1,
+ });
+ gameState.addGameLogFromCard(CardName.FORESIGHT, [
+ player,
+ ` gained ${selectedOption} for playing a Construction.`,
+ ]);
+ }
+ },
+};
diff --git a/src/model/cards/legends/fynnNobletail.test.ts b/src/model/cards/legends/fynnNobletail.test.ts
new file mode 100644
index 00000000..a2a1f20b
--- /dev/null
+++ b/src/model/cards/legends/fynnNobletail.test.ts
@@ -0,0 +1,30 @@
+import expect from "expect.js";
+import { Card } from "../../card";
+import { GameState } from "../../gameState";
+import { Player } from "../../player";
+import { testInitialGameState } from "../../testHelpers";
+import { CardName, EventName } from "../../types";
+
+describe(CardName.FYNN_NOBLETAIL, () => {
+ let gameState: GameState;
+ let player: Player;
+
+ beforeEach(() => {
+ gameState = testInitialGameState();
+ player = gameState.getActivePlayer();
+ });
+
+ it("should be worth 4 VP", () => {
+ expect(Card.fromName(CardName.FYNN_NOBLETAIL).baseVP).to.equal(5);
+ });
+
+ it("should be worth 2 extra VP per basic event critter", () => {
+ const card = Card.fromName(CardName.FYNN_NOBLETAIL);
+
+ player.placeWorkerOnEvent(EventName.BASIC_FOUR_PRODUCTION);
+ expect(card.getPoints(gameState, player.playerId)).to.be(5 + 2);
+
+ player.placeWorkerOnEvent(EventName.SPECIAL_GRADUATION_OF_SCHOLARS);
+ expect(card.getPoints(gameState, player.playerId)).to.be(5 + 2 + 3);
+ });
+});
diff --git a/src/model/cards/legends/fynnNobletail.ts b/src/model/cards/legends/fynnNobletail.ts
new file mode 100644
index 00000000..bbe38639
--- /dev/null
+++ b/src/model/cards/legends/fynnNobletail.ts
@@ -0,0 +1,47 @@
+import { Card } from "../../card";
+import { Event } from "../../event";
+import { GameState } from "../../gameState";
+import { toGameText } from "../../gameText";
+import {
+ CardName,
+ CardType,
+ EventName,
+ EventType,
+ ExpansionType,
+ ResourceType,
+} from "../../types";
+
+export const fynnNobletail: ConstructorParameters[0] = {
+ expansion: ExpansionType.LEGENDS,
+ name: CardName.FYNN_NOBLETAIL,
+ upgradeableCard: CardName.KING,
+ cardType: CardType.PROSPERITY,
+ cardDescription: toGameText([
+ { type: "points", value: 2 },
+ " for each basic Event you achieved.",
+ { type: "BR" },
+ { type: "points", value: 3 },
+ " for each special Event you achieved.",
+ ]),
+ isConstruction: false,
+ isUnique: false,
+ baseVP: 5,
+ numInDeck: 1,
+ resourcesToGain: {},
+ baseCost: {
+ [ResourceType.BERRY]: 7,
+ },
+ pointsInner: (gameState: GameState, playerId: string) => {
+ let numPoints = 0;
+ const player = gameState.getPlayer(playerId);
+ Object.keys(player.claimedEvents).forEach((eventName) => {
+ const event = Event.fromName(eventName as EventName);
+ if (event.type === EventType.BASIC) {
+ numPoints += 2;
+ } else if (event.type === EventType.SPECIAL) {
+ numPoints += 3;
+ }
+ });
+ return numPoints;
+ },
+};
diff --git a/src/model/cards/legends/index.ts b/src/model/cards/legends/index.ts
new file mode 100644
index 00000000..db1f32fc
--- /dev/null
+++ b/src/model/cards/legends/index.ts
@@ -0,0 +1,10 @@
+export { amillaGlistendew } from "./amillaGlistendew";
+export { bridgeOfTheSky } from "./bridgeOfTheSky";
+export { cirrusWindfall } from "./cirrusWindfall";
+export { foresight } from "./foresight";
+export { fynnNobletail } from "./fynnNobletail";
+export { mcgregorsMarket } from "./mcgregorsMarket";
+export { oleandersOperaHouse } from "./oleandersOperaHouse";
+export { poe } from "./poe";
+export { silverScaleSpring } from "./silverScaleSpring";
+export { theGreenAcorn } from "./theGreenAcorn";
diff --git a/src/model/cards/legends/mcgregorsMarket.ts b/src/model/cards/legends/mcgregorsMarket.ts
new file mode 100644
index 00000000..ff108700
--- /dev/null
+++ b/src/model/cards/legends/mcgregorsMarket.ts
@@ -0,0 +1,37 @@
+import { Card } from "../../card";
+import { GameState } from "../../gameState";
+import { GainMoreThan1AnyResource } from "../../gameStatePlayHelpers";
+import { toGameText } from "../../gameText";
+import {
+ CardName,
+ CardType,
+ ExpansionType,
+ GameInput,
+ ResourceType,
+} from "../../types";
+
+export const mcgregorsMarket: ConstructorParameters[0] = {
+ expansion: ExpansionType.LEGENDS,
+ name: CardName.MCGREGORS_MARKET,
+ associatedCard: null,
+ upgradeableCard: CardName.FARM,
+ cardType: CardType.PRODUCTION,
+ isConstruction: true,
+ isUnique: false,
+ baseVP: 5,
+ numInDeck: 1,
+ cardDescription: toGameText("Gain 2 ANY."),
+ baseCost: {
+ [ResourceType.TWIG]: 2,
+ [ResourceType.RESIN]: 2,
+ [ResourceType.PEBBLE]: 1,
+ },
+ playInner: (gameState: GameState, gameInput: GameInput) => {
+ const gainAnyHelper = new GainMoreThan1AnyResource({
+ cardContext: CardName.MCGREGORS_MARKET,
+ });
+ if (gainAnyHelper.matchesGameInput(gameInput)) {
+ gainAnyHelper.play(gameState, gameInput);
+ }
+ },
+};
diff --git a/src/model/cards/legends/oleandersOperaHouse.test.ts b/src/model/cards/legends/oleandersOperaHouse.test.ts
new file mode 100644
index 00000000..a3bd4f9b
--- /dev/null
+++ b/src/model/cards/legends/oleandersOperaHouse.test.ts
@@ -0,0 +1,43 @@
+import expect from "expect.js";
+import { Card } from "../../card";
+import { GameState } from "../../gameState";
+import { Player } from "../../player";
+import { testInitialGameState } from "../../testHelpers";
+import { CardName } from "../../types";
+
+describe(CardName.OLEANDERS_OPERA_HOUSE, () => {
+ let gameState: GameState;
+ let player: Player;
+
+ beforeEach(() => {
+ gameState = testInitialGameState();
+ player = gameState.getActivePlayer();
+ });
+
+ it("should be worth 4 VP", () => {
+ expect(Card.fromName(CardName.OLEANDERS_OPERA_HOUSE).baseVP).to.equal(4);
+ });
+
+ it("should be worth 2 extra VP per unique critter", () => {
+ const card = Card.fromName(CardName.OLEANDERS_OPERA_HOUSE);
+ player.addToCity(gameState, card.name);
+ const playerId = player.playerId;
+
+ expect(card.getPoints(gameState, playerId)).to.be(4 + 0);
+
+ player.addToCity(gameState, CardName.DUNGEON);
+ expect(card.getPoints(gameState, playerId)).to.be(4 + 0);
+
+ player.addToCity(gameState, CardName.RANGER);
+ expect(card.getPoints(gameState, playerId)).to.be(4 + 2);
+
+ player.addToCity(gameState, CardName.HUSBAND);
+ expect(card.getPoints(gameState, playerId)).to.be(4 + 2);
+
+ player.addToCity(gameState, CardName.WANDERER);
+ expect(card.getPoints(gameState, playerId)).to.be(4 + 2);
+
+ player.addToCity(gameState, CardName.BARD);
+ expect(card.getPoints(gameState, playerId)).to.be(4 + 4);
+ });
+});
diff --git a/src/model/cards/legends/oleandersOperaHouse.ts b/src/model/cards/legends/oleandersOperaHouse.ts
new file mode 100644
index 00000000..b96a4828
--- /dev/null
+++ b/src/model/cards/legends/oleandersOperaHouse.ts
@@ -0,0 +1,31 @@
+import { Card, getPointsPerRarityLabel } from "../../card";
+import { toGameText } from "../../gameText";
+import { CardName, CardType, ExpansionType, ResourceType } from "../../types";
+
+export const oleandersOperaHouse: ConstructorParameters[0] = {
+ expansion: ExpansionType.LEGENDS,
+ name: CardName.OLEANDERS_OPERA_HOUSE,
+ associatedCard: CardName.BARD,
+ upgradeableCard: CardName.THEATRE,
+ cardType: CardType.PROSPERITY,
+ isConstruction: true,
+ isUnique: false,
+ baseVP: 4,
+ numInDeck: 1,
+ cardDescription: toGameText([
+ { type: "points", value: 2 },
+ " for each ",
+ { type: "em", text: "Unique Critter" },
+ " in your city.",
+ ]),
+ baseCost: {
+ [ResourceType.TWIG]: 3,
+ [ResourceType.RESIN]: 2,
+ [ResourceType.PEBBLE]: 2,
+ },
+ pointsInner: getPointsPerRarityLabel({
+ isCritter: true,
+ isUnique: true,
+ pointsEach: 2,
+ }),
+};
diff --git a/src/model/cards/legends/poe.ts b/src/model/cards/legends/poe.ts
new file mode 100644
index 00000000..fdebb9af
--- /dev/null
+++ b/src/model/cards/legends/poe.ts
@@ -0,0 +1,34 @@
+import { Card } from "../../card";
+import { GameState } from "../../gameState";
+import { toGameText } from "../../gameText";
+import {
+ CardName,
+ CardType,
+ ExpansionType,
+ GameInput,
+ ResourceType,
+} from "../../types";
+
+export const poe: ConstructorParameters[0] = {
+ expansion: ExpansionType.LEGENDS,
+ name: CardName.POE,
+ upgradeableCard: CardName.TEACHER,
+ cardType: CardType.PRODUCTION,
+ cardDescription: toGameText(
+ "Discard any number of CARD, then draw up to your hand limit."
+ ),
+ isConstruction: false,
+ isUnique: false,
+ baseVP: 4,
+ numInDeck: 1,
+ baseCost: {
+ [ResourceType.BERRY]: 4,
+ },
+ canPlayCheckInner: (gameState: GameState, gameInput: GameInput) => {
+ // TODO: Implement this
+ return null;
+ },
+ playInner: (gameState: GameState, gameInput: GameInput) => {
+ // TODO: Implement this
+ },
+};
diff --git a/src/model/cards/legends/silverScaleSpring.ts b/src/model/cards/legends/silverScaleSpring.ts
new file mode 100644
index 00000000..f166736a
--- /dev/null
+++ b/src/model/cards/legends/silverScaleSpring.ts
@@ -0,0 +1,40 @@
+import { Card } from "../../card";
+import { GameState } from "../../gameState";
+import { toGameText } from "../../gameText";
+import {
+ CardName,
+ CardType,
+ ExpansionType,
+ GameInput,
+ ResourceType,
+} from "../../types";
+
+export const silverScaleSpring: ConstructorParameters[0] = {
+ expansion: ExpansionType.LEGENDS,
+ name: CardName.SILVER_SCALE_SPRING,
+ associatedCard: CardName.PEDDLER,
+ upgradeableCard: CardName.RUINS,
+ cardType: CardType.TRAVELER,
+ isConstruction: true,
+ isUnique: false,
+ baseVP: 2,
+ numInDeck: 1,
+ cardDescription: toGameText([
+ "Play this card under a ",
+ { type: "em", text: "Construction " },
+ "in your city. ",
+ "Gain that ",
+ { type: "em", text: "Construction" },
+ "'s resources and draw 2 CARD",
+ ]),
+ baseCost: {
+ [ResourceType.PEBBLE]: 1,
+ },
+ canPlayCheckInner: (gameState: GameState, gameInput: GameInput) => {
+ // TODO: Implement this
+ return null;
+ },
+ playInner: (gameState: GameState, gameInput: GameInput) => {
+ // TODO: Implement this
+ },
+};
diff --git a/src/model/cards/legends/theGreenAcorn.ts b/src/model/cards/legends/theGreenAcorn.ts
new file mode 100644
index 00000000..5532782f
--- /dev/null
+++ b/src/model/cards/legends/theGreenAcorn.ts
@@ -0,0 +1,160 @@
+import { Card } from "../../card";
+import { GameState } from "../../gameState";
+import { sumResources } from "../../gameStatePlayHelpers";
+import { toGameText } from "../../gameText";
+import { Player } from "../../player";
+import {
+ CardName,
+ CardType,
+ ExpansionType,
+ GameInput,
+ GameInputType,
+ ResourceType,
+} from "../../types";
+
+function availableCardsToPlay(
+ gameState: GameState,
+ player: Player
+): CardName[] {
+ const resources = player.getResources();
+ const availableCards = gameState.meadowCards.concat(player.cardsInHand);
+ return availableCards.filter((cardName) => {
+ const card = Card.fromName(cardName);
+ return (
+ card.canPlayIgnoreCostAndSource(gameState) &&
+ player.isPaidResourcesValid(resources, card.baseCost, "ANY 4", false)
+ );
+ });
+}
+
+export const theGreenAcorn: ConstructorParameters[0] = {
+ expansion: ExpansionType.LEGENDS,
+ name: CardName.THE_GREEN_ACORN,
+ associatedCard: CardName.INNKEEPER,
+ upgradeableCard: CardName.INN,
+ cardType: CardType.DESTINATION,
+ isOpenDestination: true,
+ isConstruction: true,
+ isUnique: false,
+ baseVP: 4,
+ numInDeck: 1,
+ cardDescription: toGameText([
+ "Play a ",
+ { type: "em", text: "Critter " },
+ "or ",
+ { type: "em", text: "Construction " },
+ "for 4 fewer ANY",
+ ]),
+ baseCost: {
+ [ResourceType.TWIG]: 3,
+ [ResourceType.RESIN]: 3,
+ },
+ canPlayCheckInner: (gameState: GameState, gameInput: GameInput) => {
+ const player = gameState.getActivePlayer();
+ if (gameInput.inputType === GameInputType.VISIT_DESTINATION_CARD) {
+ const canPlayCards = availableCardsToPlay(gameState, player).length > 0;
+ if (!canPlayCards) {
+ return `You don't have any cards you can play`;
+ }
+ }
+ return null;
+ },
+ // Play any card for 4 less resources
+ playInner: (gameState: GameState, gameInput: GameInput) => {
+ const player = gameState.getActivePlayer();
+ if (gameInput.inputType === GameInputType.VISIT_DESTINATION_CARD) {
+ // add pending input to select a card to play
+ gameState.pendingGameInputs.push({
+ inputType: GameInputType.SELECT_CARDS,
+ prevInputType: gameInput.inputType,
+ label: "Select 1 CARD from to play for 4 fewer ANY",
+ cardOptions: availableCardsToPlay(gameState, player),
+ maxToSelect: 1,
+ minToSelect: 1,
+ cardContext: CardName.THE_GREEN_ACORN,
+ clientOptions: {
+ selectedCards: [],
+ },
+ });
+ } else if (
+ gameInput.inputType === GameInputType.SELECT_CARDS &&
+ gameInput.cardContext === CardName.THE_GREEN_ACORN
+ ) {
+ const selectedCards = gameInput.clientOptions.selectedCards;
+ if (!selectedCards) {
+ throw new Error("Must select card to play.");
+ }
+ if (selectedCards.length !== 1) {
+ throw new Error("Can only play 1 card.");
+ }
+ const selectedCardName = selectedCards[0];
+ if (
+ gameState.meadowCards.indexOf(selectedCardName) < 0 ||
+ player.cardsInHand.indexOf(selectedCardName) < 0
+ ) {
+ throw new Error("Cannot find selected card.");
+ }
+ const selectedCard = Card.fromName(selectedCardName);
+ if (!selectedCard.canPlayIgnoreCostAndSource(gameState)) {
+ throw new Error(`Unable to play ${selectedCardName}`);
+ }
+
+ gameState.addGameLogFromCard(CardName.THE_GREEN_ACORN, [
+ player,
+ " selected ",
+ selectedCard,
+ " to play.",
+ ]);
+
+ if (sumResources(selectedCard.baseCost) <= 4) {
+ gameState.removeCardFromMeadow(selectedCard.name);
+ selectedCard.addToCityAndPlay(gameState, gameInput);
+ gameState.addGameLogFromCard(CardName.THE_GREEN_ACORN, [
+ player,
+ " played ",
+ selectedCard,
+ ]);
+ } else {
+ gameState.pendingGameInputs.push({
+ inputType: GameInputType.SELECT_PAYMENT_FOR_CARD,
+ prevInputType: gameInput.inputType,
+ cardContext: CardName.THE_GREEN_ACORN,
+ card: selectedCardName,
+ clientOptions: {
+ card: selectedCardName,
+ paymentOptions: { resources: {} },
+ },
+ });
+ }
+ } else if (
+ gameInput.inputType === GameInputType.SELECT_PAYMENT_FOR_CARD &&
+ gameInput.cardContext === CardName.INN
+ ) {
+ if (!gameInput.clientOptions?.paymentOptions?.resources) {
+ throw new Error(
+ "Invalid input: clientOptions.paymentOptions.resources missing"
+ );
+ }
+ const card = Card.fromName(gameInput.card);
+ const paymentError = player.validatePaidResources(
+ gameInput.clientOptions.paymentOptions.resources,
+ card.baseCost,
+ "ANY 3"
+ );
+ if (paymentError) {
+ throw new Error(paymentError);
+ }
+ player.payForCard(gameState, gameInput);
+ card.addToCityAndPlay(gameState, gameInput);
+
+ gameState.removeCardFromMeadow(card.name);
+
+ gameState.addGameLogFromCard(CardName.INN, [
+ player,
+ " played ",
+ card,
+ " from the Meadow.",
+ ]);
+ }
+ },
+};
diff --git a/src/model/deck.ts b/src/model/deck.ts
index 92a1496f..dbcf7f27 100644
--- a/src/model/deck.ts
+++ b/src/model/deck.ts
@@ -87,3 +87,29 @@ export const initialDeck = ({
return cardStack;
};
+
+export const legendaryCritters = (): CardStack => {
+ return new CardStack({
+ name: "Legendary Critters",
+ cards: [
+ CardName.AMILLA_GLISTENDEW,
+ CardName.CIRRUS_WINDFALL,
+ CardName.FORESIGHT,
+ CardName.FYNN_NOBLETAIL,
+ CardName.POE,
+ ],
+ });
+};
+
+export const legendaryConstructions = (): CardStack => {
+ return new CardStack({
+ name: "Legendary Constructions",
+ cards: [
+ CardName.BRIDGE_OF_THE_SKY,
+ CardName.MCGREGORS_MARKET,
+ CardName.OLEANDERS_OPERA_HOUSE,
+ CardName.SILVER_SCALE_SPRING,
+ CardName.THE_GREEN_ACORN,
+ ],
+ });
+};
diff --git a/src/model/gameState.ts b/src/model/gameState.ts
index b3a93c83..425aca36 100644
--- a/src/model/gameState.ts
+++ b/src/model/gameState.ts
@@ -44,7 +44,7 @@ import {
RiverDestinationSpot,
} from "./riverDestination";
import { Event, initialEventMap } from "./event";
-import { initialDeck } from "./deck";
+import { initialDeck, legendaryCritters, legendaryConstructions } from "./deck";
import { assertUnreachable } from "../utils";
import {
toGameText,
@@ -109,7 +109,6 @@ export const gameTextToDebugStr = (gameText: GameText): string => {
);
}
assertUnreachable(part, `Unexpected part: ${JSON.stringify(part)}`);
- break;
default:
assertUnreachable(part, `Unexpected part: ${JSON.stringify(part)}`);
}
@@ -121,6 +120,7 @@ const defaultGameOptions = (gameOptions: Partial): GameOptions => {
return {
realtimePoints: false,
pearlbrook: false,
+ legends: false,
...gameOptions,
};
};
@@ -151,6 +151,9 @@ export class GameState {
readonly adornmentsPile: CardStack | null;
readonly riverDestinationMap: RiverDestinationMap | null;
+ readonly legendaryCritters: CardStack | null;
+ readonly legendaryConstructions: CardStack | null;
+
constructor({
gameStateId,
activePlayerId,
@@ -166,6 +169,8 @@ export class GameState {
playedGameInputs = [],
adornmentsPile = null,
riverDestinationMap = null,
+ legendaryCritters = null,
+ legendaryConstructions = null,
}: {
gameStateId: number;
activePlayerId?: Player["playerId"];
@@ -177,6 +182,8 @@ export class GameState {
eventsMap: EventNameToPlayerId;
adornmentsPile?: CardStack | null;
riverDestinationMap?: RiverDestinationMap | null;
+ legendaryCritters?: CardStack | null;
+ legendaryConstructions?: CardStack | null;
pendingGameInputs: GameInputMultiStep[];
playedGameInputs?: GameInput[];
gameLog: GameLogEntry[];
@@ -193,8 +200,15 @@ export class GameState {
this.pendingGameInputs = pendingGameInputs;
this.playedGameInputs = playedGameInputs;
this.gameLog = gameLog;
+
+ // Pearlbrook
this.adornmentsPile = adornmentsPile;
this.riverDestinationMap = riverDestinationMap;
+
+ // Legends
+ this.legendaryCritters = legendaryCritters;
+ this.legendaryConstructions = legendaryConstructions;
+
this.gameOptions = defaultGameOptions(gameOptions);
}
@@ -308,6 +322,12 @@ export class GameState {
adornmentsPile: this.adornmentsPile
? this.adornmentsPile.toJSON(includePrivate)
: null,
+ legendaryCritters: this.legendaryCritters
+ ? this.legendaryCritters.toJSON(includePrivate)
+ : null,
+ legendaryConstructions: this.legendaryConstructions
+ ? this.legendaryConstructions.toJSON(includePrivate)
+ : null,
},
...(includePrivate
? {
@@ -1279,6 +1299,12 @@ export class GameState {
riverDestinationMap: gameStateJSON.riverDestinationMap
? RiverDestinationMap.fromJSON(gameStateJSON.riverDestinationMap)
: null,
+ legendaryCritters: gameStateJSON.legendaryCritters
+ ? CardStack.fromJSON(gameStateJSON.legendaryCritters)
+ : null,
+ legendaryConstructions: gameStateJSON.legendaryConstructions
+ ? CardStack.fromJSON(gameStateJSON.legendaryConstructions)
+ : null,
});
}
@@ -1297,6 +1323,7 @@ export class GameState {
const gameOptionsWithDefaults = defaultGameOptions(gameOptions);
const withPearlbrook = gameOptionsWithDefaults.pearlbrook;
+ const withLegends = gameOptionsWithDefaults.legends;
const gameState = new GameState({
gameStateId: 1,
players,
@@ -1309,6 +1336,8 @@ export class GameState {
),
adornmentsPile: withPearlbrook ? allAdornments() : null,
riverDestinationMap: withPearlbrook ? initialRiverDestinationMap() : null,
+ legendaryCritters: withLegends ? legendaryCritters() : null,
+ legendaryConstructions: withLegends ? legendaryConstructions() : null,
eventsMap: initialEventMap(gameOptionsWithDefaults),
gameOptions: gameOptionsWithDefaults,
gameLog: [],
@@ -1323,11 +1352,22 @@ export class GameState {
` expansion.`,
]);
}
+ if (withLegends) {
+ gameState.addGameLog([
+ `Playing with the `,
+ { type: "em", text: "Legends" },
+ ` expansion.`,
+ ]);
+ }
if (shuffleDeck) {
if (withPearlbrook) {
gameState.adornmentsPile!.shuffle();
}
+ if (withLegends) {
+ gameState.legendaryCritters!.shuffle();
+ gameState.legendaryConstructions!.shuffle();
+ }
gameState.deck.shuffle();
}
@@ -1340,6 +1380,12 @@ export class GameState {
gameState.adornmentsPile!.drawInner()
);
}
+ if (withLegends) {
+ p.legendsInHand.push(
+ gameState.legendaryCritters!.drawInner(),
+ gameState.legendaryConstructions!.drawInner()
+ );
+ }
p.drawCards(gameState, STARTING_PLAYER_HAND_SIZE + idx);
});
@@ -1349,6 +1395,11 @@ export class GameState {
if (withPearlbrook) {
gameState.addGameLog(`Dealing 2 adornment cards to each player.`);
}
+ if (withLegends) {
+ gameState.addGameLog(
+ `Dealing 1 legendary critter and 1 legendary construction to each player.`
+ );
+ }
return gameState;
}
@@ -1496,6 +1547,15 @@ export class GameState {
ret.push({ card: cardName, fromMeadow: false });
}
});
+ player.legendsInHand.forEach((cardName) => {
+ const card = Card.fromName(cardName);
+ if (
+ player.canAffordCard(card.name, false) &&
+ card.canPlayIgnoreCostAndSource(this, false /* strict */)
+ ) {
+ ret.push({ card: cardName, fromMeadow: false });
+ }
+ });
return ret;
}
diff --git a/src/model/jsonTypes.ts b/src/model/jsonTypes.ts
index 53af4eef..d20a6b7e 100644
--- a/src/model/jsonTypes.ts
+++ b/src/model/jsonTypes.ts
@@ -38,6 +38,8 @@ export type GameStateJSON = {
gameLog: GameLogEntry[];
riverDestinationMap: RiverDestinationMapJSON | null;
adornmentsPile: CardStackJSON | null;
+ legendaryCritters: CardStackJSON | null;
+ legendaryConstructions: CardStackJSON | null;
};
export type CardStackJSON = {
@@ -63,6 +65,8 @@ export type PlayerJSON = {
adornmentsInHand: AdornmentName[];
playedAdornments: AdornmentName[];
numAmbassadors: number;
+ legendsInHand: CardName[];
+ numLegendsInHand: number;
};
export type RiverDestinationMapJSON = {
diff --git a/src/model/player.ts b/src/model/player.ts
index 9da851bf..0275037c 100644
--- a/src/model/player.ts
+++ b/src/model/player.ts
@@ -21,6 +21,7 @@ import {
LocationType,
PlayerStatus,
IGameTextEntity,
+ ExpansionType,
} from "./types";
import { PlayerJSON } from "./jsonTypes";
import { GameState } from "./gameState";
@@ -58,6 +59,8 @@ export class Player implements IGameTextEntity {
readonly adornmentsInHand: AdornmentName[];
readonly playedAdornments: AdornmentName[];
+ readonly legendsInHand: CardName[];
+
constructor({
name,
playerSecret = uuid(),
@@ -81,6 +84,7 @@ export class Player implements IGameTextEntity {
playerStatus = PlayerStatus.DURING_SEASON,
adornmentsInHand = [],
playedAdornments = [],
+ legendsInHand = [],
}: {
name: string;
playerSecret?: string;
@@ -97,6 +101,7 @@ export class Player implements IGameTextEntity {
playerStatus?: PlayerStatus;
adornmentsInHand?: AdornmentName[];
playedAdornments?: AdornmentName[];
+ legendsInHand?: CardName[];
}) {
this.playerId = playerId;
this.playerSecret = playerSecret;
@@ -115,6 +120,9 @@ export class Player implements IGameTextEntity {
this.adornmentsInHand = adornmentsInHand;
this.playedAdornments = playedAdornments;
+ // legends only
+ this.legendsInHand = legendsInHand;
+
this._numCardsInHand = numCardsInHand;
}
@@ -332,26 +340,29 @@ export class Player implements IGameTextEntity {
while (pendingPlayCardGameInput.length !== 0) {
const gameInput = pendingPlayCardGameInput.pop() as GameInputPlayCard;
pendingCardNames.pop();
- [CardName.HISTORIAN, CardName.SHOPKEEPER, CardName.COURTHOUSE].forEach(
- (cardName) => {
- // Don't trigger if we just played this card and we haven't gotten to it yet.
- // Eg. We played POSTAL_PIGEON -> SHOPKEEPER. We shouldn't activate SHOPKEEPER
- // on the POSTAL_PIGEON.
- if (pendingCardNames.indexOf(cardName) !== -1) {
- return;
- }
+ [
+ CardName.HISTORIAN,
+ CardName.SHOPKEEPER,
+ CardName.COURTHOUSE,
+ CardName.FORESIGHT,
+ ].forEach((cardName) => {
+ // Don't trigger if we just played this card and we haven't gotten to it yet.
+ // Eg. We played POSTAL_PIGEON -> SHOPKEEPER. We shouldn't activate SHOPKEEPER
+ // on the POSTAL_PIGEON.
+ if (pendingCardNames.indexOf(cardName) !== -1) {
+ return;
+ }
- if (this.hasCardInCity(cardName)) {
- const card = Card.fromName(cardName);
- card.activateCard(
- gameState,
- gameInput,
- this,
- this.getFirstPlayedCard(cardName)
- );
- }
+ if (this.hasCardInCity(cardName)) {
+ const card = Card.fromName(cardName);
+ card.activateCard(
+ gameState,
+ gameInput,
+ this,
+ this.getFirstPlayedCard(cardName)
+ );
}
- );
+ });
}
}
@@ -996,6 +1007,13 @@ export class Player implements IGameTextEntity {
}
}
+ // Check if you have the associated card if card is legendary
+ if (card.expansion === ExpansionType.LEGENDS) {
+ if (this.hasCardInCity(card.upgradeableCard!)) {
+ return true;
+ }
+ }
+
// Queen (below 3 vp free)
if (
card.baseVP <= 3 &&
@@ -1037,7 +1055,7 @@ export class Player implements IGameTextEntity {
paidResources: CardCost,
cardCost: CardCost,
// Discounts are exclusive so we use a single argument to represent them
- discount: ResourceType.BERRY | "ANY 3" | "ANY 1" | null = null,
+ discount: ResourceType.BERRY | "ANY 4" | "ANY 3" | "ANY 1" | null = null,
errorIfOverpay = true
): string | null {
const needToPay = {
@@ -1086,6 +1104,17 @@ export class Player implements IGameTextEntity {
const payingWithRemainerSum = sumResources(payingWith);
// With wild discount, should have outstandingOwedSum left
+ if (discount === "ANY 4" && outstandingOwedSum <= 4) {
+ if (
+ errorIfOverpay &&
+ payingWithSum !== 0 &&
+ payingWithSum + 4 > needToPaySum
+ ) {
+ return "Cannot overpay for cards";
+ }
+ return null;
+ }
+
if (discount === "ANY 3" && outstandingOwedSum <= 3) {
if (
errorIfOverpay &&
@@ -1138,7 +1167,7 @@ export class Player implements IGameTextEntity {
paidResources: CardCost,
cardCost: CardCost,
// Discounts are exclusive so we use a single argument to represent them
- discount: ResourceType.BERRY | "ANY 3" | "ANY 1" | null = null,
+ discount: ResourceType.BERRY | "ANY 4" | "ANY 3" | "ANY 1" | null = null,
errorIfOverpay = true
): boolean {
return !this.validatePaidResources(
@@ -1231,6 +1260,8 @@ export class Player implements IGameTextEntity {
`Unexpected card: ${paymentOptions.cardToUse}`
);
}
+ } else if (paymentOptions.cardToUpgrade) {
+ // Do something?
}
}
@@ -1654,12 +1685,15 @@ export class Player implements IGameTextEntity {
numAdornmentsInHand: this.adornmentsInHand.length,
cardsInHand: null,
adornmentsInHand: [],
+ legendsInHand: [],
playedAdornments: this.playedAdornments,
+ numLegendsInHand: this.legendsInHand.length,
...(includePrivate
? {
playerSecret: this.playerSecret,
cardsInHand: this.cardsInHand,
adornmentsInHand: this.adornmentsInHand,
+ legendsInHand: this.legendsInHand,
}
: {}),
});
diff --git a/src/model/types.ts b/src/model/types.ts
index cb42cb10..f1640738 100644
--- a/src/model/types.ts
+++ b/src/model/types.ts
@@ -74,6 +74,7 @@ export type GameInputPlayCard = {
export type GameInputClaimEvent = {
inputType: GameInputType.CLAIM_EVENT;
+ prevInputType?: GameInputType;
clientOptions: {
event: EventName | null;
};
@@ -262,6 +263,7 @@ export type GameInputMultiStep = (
| GameInputSelectOptionGeneric
| GameInputSelectPlayedAdornment
| GameInputSelectRiverDestination
+ | GameInputClaimEvent
) &
GameInputMultiStepContext & {
prevInput?: GameInput;
@@ -515,6 +517,18 @@ export enum CardName {
PIRATE = "Pirate",
PIRATE_SHIP = "Pirate Ship",
SHIPWRIGHT = "Shipwright",
+
+ // Legends
+ AMILLA_GLISTENDEW = "Amilla Glistendew",
+ BRIDGE_OF_THE_SKY = "Bridge of the Sky",
+ CIRRUS_WINDFALL = "Cirrus Windfall",
+ FORESIGHT = "Foresight",
+ FYNN_NOBLETAIL = "Fynn Nobletail",
+ MCGREGORS_MARKET = "McGregor's Market",
+ OLEANDERS_OPERA_HOUSE = "Oleander's Opera House",
+ POE = "Poe",
+ SILVER_SCALE_SPRING = "Silver Scale Spring",
+ THE_GREEN_ACORN = "The Green Acorn",
}
export enum ResourceType {
@@ -572,6 +586,19 @@ export type CardPaymentOptions = {
| CardName.INN
| null;
+ // Eg. farm, ruins, queen
+ cardToUpgrade?:
+ | CardName.QUEEN
+ | CardName.CRANE
+ | CardName.POSTAL_PIGEON
+ | CardName.HISTORIAN
+ | CardName.KING
+ | CardName.FARM
+ | CardName.THEATRE
+ | CardName.TEACHER
+ | CardName.RUINS
+ | CardName.INN;
+
resources: {
[ResourceType.TWIG]?: number;
[ResourceType.BERRY]?: number;
@@ -641,6 +668,7 @@ export interface IGameTextEntity {
export type GameOptions = {
realtimePoints: boolean;
pearlbrook: boolean;
+ legends: boolean;
};
export enum RiverDestinationType {
@@ -667,6 +695,7 @@ export enum RiverDestinationName {
export enum ExpansionType {
PEARLBROOK = "PEARLBROOK",
+ LEGENDS = "LEGENDS",
}
export enum AdornmentName {
diff --git a/src/pages/api/create-game-from-state.ts b/src/pages/api/create-game-from-state.ts
new file mode 100644
index 00000000..b854050a
--- /dev/null
+++ b/src/pages/api/create-game-from-state.ts
@@ -0,0 +1,20 @@
+import type { NextApiRequest, NextApiResponse } from "next";
+import { createGameFromGameState } from "../../model/game";
+import { GameState } from "../../model/gameState";
+
+export default async (
+ req: NextApiRequest,
+ res: NextApiResponse
+): Promise => {
+ if (req.method !== "POST") {
+ res.setHeader("Allow", ["POST"]);
+ res.status(405).end(`Method ${req.method} Not Allowed`);
+ return;
+ }
+
+ const game = await createGameFromGameState(GameState.fromJSON(req.body));
+ res.json({
+ success: "ok",
+ gameUrl: `/game/${game.gameId}?gameSecret=${game.gameSecretUNSAFE}`,
+ });
+};
diff --git a/src/pages/api/create-game.ts b/src/pages/api/create-game.ts
index 3738a61e..3e7f3491 100644
--- a/src/pages/api/create-game.ts
+++ b/src/pages/api/create-game.ts
@@ -33,9 +33,10 @@ export default async (
: [...body.players];
const realtimePoints = !!body.realtimePoints;
const pearlbrook = !!body.pearlbrook;
+ const legends = !!body.legends;
const game = await createGame(
players.map((p: any) => p.name),
- { realtimePoints, pearlbrook }
+ { realtimePoints, pearlbrook, legends }
);
res.json({
success: "ok",
diff --git a/src/pages/test/inputs.tsx b/src/pages/test/inputs.tsx
index 957f0f58..59153f47 100644
--- a/src/pages/test/inputs.tsx
+++ b/src/pages/test/inputs.tsx
@@ -73,6 +73,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
player.addToCity(gameState, CardName.LOOKOUT);
player.addToCity(gameState, CardName.CHIP_SWEEP);
player.addToCity(gameState, CardName.MINER_MOLE);
+ player.addToCity(gameState, CardName.AMILLA_GLISTENDEW);
player.placeWorkerOnCard(
gameState,
diff --git a/src/pages/test/legends.tsx b/src/pages/test/legends.tsx
new file mode 100644
index 00000000..f41c9601
--- /dev/null
+++ b/src/pages/test/legends.tsx
@@ -0,0 +1,76 @@
+import { GetServerSideProps, NextPage } from "next";
+import GameInputBox from "../../components/GameInputBox";
+import { testInitialGameState } from "../../model/testHelpers";
+import { CardName, EventName, GameInputType } from "../../model/types";
+import { Game as GameModel } from "../../model/game";
+import { GameJSON } from "../../model/jsonTypes";
+import { GameState } from "../../model/gameState";
+import { FunctionComponent, useEffect } from "react";
+import Game from "../../components/Game";
+import { NextRouter, useRouter } from "next/router";
+
+export const getServerSideProps: GetServerSideProps = async (context) => {
+ const playerNames = ["Michael", "Elynn", "Chris", "Vanessa"];
+ const numPlayers = playerNames.length;
+ const gameState = testInitialGameState({
+ numPlayers,
+ playerNames,
+ specialEvents: [
+ EventName.SPECIAL_GRADUATION_OF_SCHOLARS,
+ EventName.SPECIAL_A_BRILLIANT_MARKETING_PLAN,
+ EventName.SPECIAL_PERFORMER_IN_RESIDENCE,
+ EventName.SPECIAL_CAPTURE_OF_THE_ACORN_THIEVES,
+ ],
+ shuffleDeck: true,
+ gameOptions: {
+ legends: true,
+ },
+ });
+
+ gameState.players.forEach((player, idx) => {
+ player.nextSeason();
+
+ player.drawCards(gameState, idx);
+ });
+
+ const game = new GameModel({
+ gameId: "testGameId",
+ gameSecret: "testGameSecret",
+ gameState,
+ });
+
+ return {
+ props: {
+ game: game.toJSON(true /* includePrivate */),
+ },
+ };
+};
+
+async function createGame(router: NextRouter, game: GameJSON) {
+ const response = await fetch("/api/create-game-from-state", {
+ method: "POST",
+ cache: "no-cache",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(game.gameState),
+ });
+ const json = await response.json();
+ if (json.success && json.gameUrl) {
+ router.push(json.gameUrl);
+ } else {
+ alert(json.error);
+ }
+}
+
+const AmillaGlistendew: FunctionComponent<{ game: GameJSON }> = (props) => {
+ const router = useRouter();
+
+ useEffect(() => {
+ createGame(router, props.game);
+ }, []);
+
+ return null;
+};
+
+export default AmillaGlistendew;
diff --git a/src/pages/test/ui.tsx b/src/pages/test/ui.tsx
index 3b420f41..a3946c5c 100644
--- a/src/pages/test/ui.tsx
+++ b/src/pages/test/ui.tsx
@@ -64,6 +64,7 @@ export default function TestUIPage() {
) as LocationName[];
const [showPearlbrookOnly, setShowPearlbrookOnly] = useState(false);
+ const [showLegendsOnly, setShowLegendsOnly] = useState(false);
const [showCards, setShowCards] = useState(true);
const [showAdornments, setShowAdornments] = useState(true);
const [showRiver, setShowRiver] = useState(true);
@@ -111,6 +112,16 @@ export default function TestUIPage() {
/>
Pearlbrook only
+
{allCards
@@ -127,6 +138,11 @@ export default function TestUIPage() {
return false;
}
}
+ if (showLegendsOnly) {
+ if (CardModel.fromName(x).expansion !== ExpansionType.LEGENDS) {
+ return false;
+ }
+ }
return true;
})
.map((card) => {
diff --git a/src/styles/card.module.css b/src/styles/card.module.css
index b94f8da1..366648eb 100644
--- a/src/styles/card.module.css
+++ b/src/styles/card.module.css
@@ -1,5 +1,5 @@
.card {
- min-height: 180px;
+ min-height: 200px;
width: 200px;
border-radius: 5px;
position: relative;
@@ -12,6 +12,14 @@
justify-content: space-between;
}
+.card_header_row {
+ display: flex;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ width: 100%;
+ overflow: hidden;
+}
+
.card_header {
height: 35px;
font-weight: var(--font-bold);
@@ -22,6 +30,8 @@
display: flex;
flex-direction: column;
justify-content: center;
+ flex: 1 1 auto;
+ overflow: hidden;
}
.expansion {
@@ -38,7 +48,11 @@
display: flex;
color: var(--text-color);
text-transform: uppercase;
- text-align: right;
+}
+
+.upgradeable_card {
+ display: flex;
+ text-transform: uppercase;
}
.associated_card_used {
@@ -46,7 +60,8 @@
opacity: 0.3;
}
-.associated_card > span {
+.associated_card > span,
+.upgradeable_card > span {
border-radius: 5px 0 0 0;
font-weight: var(--font-bold);
color: var(--text-color-inv);
@@ -55,6 +70,11 @@
align-self: flex-end;
}
+.upgradeable_card > span {
+ background: var(--color-legend);
+ border-radius: 0 5px 0 0;
+}
+
.info_row {
display: flex;
flex-direction: column;
@@ -90,6 +110,7 @@
font-size: 10px;
font-weight: var(--font-bold);
color: var(--text-color-light);
+ text-align: center;
}
.circle {
@@ -97,16 +118,10 @@
width: 24px;
height: 24px;
margin: 6px;
-
text-align: center;
font-size: 27px;
-}
-
-.card_header_vp {
- position: absolute;
- top: 0;
- right: 0;
- z-index: 10;
+ position: relative;
+ flex: 0 0 auto;
}
.card_header_vp_number {
@@ -116,12 +131,7 @@
margin-top: -14px;
vertical-align: middle;
color: var(--color-vp-text);
-}
-
-.card_header_symbol {
- position: absolute;
- top: 0;
- z-index: 10;
+ flex: 0 0 auto;
}
.color_victory_point {
diff --git a/src/styles/globals.css b/src/styles/globals.css
index 562be452..e90070d4 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -24,12 +24,13 @@
--color-vp: #e3b117;
--color-vp-text: #3c1346;
- --color-dest: #943032;
+ --color-dest: #79191d;
--color-prod: #88a55e;
--color-pros: #534279;
--color-trav: #796a53;
--color-gov: #476f80;
--color-loc: #13463c;
+ --color-legend: #b94147;
--color-loc-basic: #88a55e;
--color-loc-forest: #4e5627;
diff --git a/src/styles/location.module.css b/src/styles/location.module.css
index 7f4fd205..3df160e6 100644
--- a/src/styles/location.module.css
+++ b/src/styles/location.module.css
@@ -1,5 +1,5 @@
.location {
- height: 100px;
+ height: 120px;
width: 200px;
border-radius: 5px;
color: var(--color-loc);