From f25e1c2dcd628d8f3ca02c8059347953366879d9 Mon Sep 17 00:00:00 2001 From: Aaron Roberts Date: Sun, 27 Feb 2022 20:22:49 -0500 Subject: [PATCH 1/8] add amilla glistendew and test page --- .prettierrc | 1 - src/components/Card.tsx | 4 +- src/model/card.ts | 16 ++++ src/model/cards/legends/amilla-glistendew.ts | 42 +++++++++++ src/model/gameState.ts | 2 +- src/model/types.ts | 16 ++++ src/pages/api/create-game-from-state.ts | 20 +++++ src/pages/test/inputs.tsx | 1 + src/pages/test/legends.tsx | 77 ++++++++++++++++++++ 9 files changed, 175 insertions(+), 4 deletions(-) create mode 100644 src/model/cards/legends/amilla-glistendew.ts create mode 100644 src/pages/api/create-game-from-state.ts create mode 100644 src/pages/test/legends.tsx diff --git a/.prettierrc b/.prettierrc index 8b137891..e69de29b 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1 +0,0 @@ - diff --git a/src/components/Card.tsx b/src/components/Card.tsx index 50e0ac7f..7e1b98c6 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -1,7 +1,7 @@ import * as React from "react"; 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 { @@ -65,7 +65,7 @@ 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; }; diff --git a/src/model/card.ts b/src/model/card.ts index 3f480ac2..19ba20de 100644 --- a/src/model/card.ts +++ b/src/model/card.ts @@ -44,6 +44,8 @@ import { } from "./gameText"; import { assertUnreachable } from "../utils"; +import amilla from "./cards/legends/amilla-glistendew"; + type NumWorkersInnerFn = (cardOwner: Player) => number; type ProductionInnerFn = ( gameState: GameState, @@ -3952,6 +3954,20 @@ const CARD_REGISTRY: Record = { } }, }), + + /** + * WIP: Legends Cards + */ + [CardName.AMILLA_GLISTENDEW]: amilla(), + [CardName.BRIDGE_OF_THE_SKY]: amilla(), + [CardName.CIRRUS_WINDFALL]: amilla(), + [CardName.FORESIGHT]: amilla(), + [CardName.FYNN_NOBLETAIL]: amilla(), + [CardName.MCGREGORS_MARKET]: amilla(), + [CardName.OLEANDERS_OPERA_HOUSE]: amilla(), + [CardName.POE]: amilla(), + [CardName.SILVER_SCALE_SPRING]: amilla(), + [CardName.THE_GREEN_ACORN]: amilla(), }; function getPointsPerRarityLabel({ diff --git a/src/model/cards/legends/amilla-glistendew.ts b/src/model/cards/legends/amilla-glistendew.ts new file mode 100644 index 00000000..fef8bae3 --- /dev/null +++ b/src/model/cards/legends/amilla-glistendew.ts @@ -0,0 +1,42 @@ +import { Card } from "../../card"; +import { GameState } from "../../gameState"; +import { toGameText } from "../../gameText"; +import { Player } from "../../player"; +import { CardName, CardType, ExpansionType, GameInput, GameInputType, ResourceType } from "../../types"; + +export default (): Card => new Card({ + expansion: ExpansionType.LEGENDS, + name: CardName.AMILLA_GLISTENDEW, + associatedCard: CardName.QUEEN, + cardType: CardType.DESTINATION, + cardDescription: toGameText("Achieve an Event, even if you don't meet the listed requirements."), + isConstruction: false, + isUnique: true, + 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, + }, + }); + } + } +}); \ No newline at end of file diff --git a/src/model/gameState.ts b/src/model/gameState.ts index b3a93c83..8195461b 100644 --- a/src/model/gameState.ts +++ b/src/model/gameState.ts @@ -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, }; }; diff --git a/src/model/types.ts b/src/model/types.ts index cb42cb10..33bf5f60 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 = "Oleadner's Opera House", + POE = "Poe", + SILVER_SCALE_SPRING = "Silver Scale Spring", + THE_GREEN_ACORN = "The Green Acorn", } export enum ResourceType { @@ -641,6 +655,7 @@ export interface IGameTextEntity { export type GameOptions = { realtimePoints: boolean; pearlbrook: boolean; + legends: boolean; }; export enum RiverDestinationType { @@ -667,6 +682,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/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..08804084 --- /dev/null +++ b/src/pages/test/legends.tsx @@ -0,0 +1,77 @@ +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); + player.addToCity(gameState, CardName.AMILLA_GLISTENDEW); + }); + + 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 From 0c050576777d965bd13ff0c6ba00cea416e148de Mon Sep 17 00:00:00 2001 From: Aaron Roberts Date: Mon, 28 Feb 2022 23:17:08 -0500 Subject: [PATCH 2/8] add groundwork for legends expansion and cards --- .prettierrc | 1 + public/images/legendary_card.png | Bin 0 -> 1222 bytes src/components/Card.tsx | 18 +++++-- src/components/GameBuilder.tsx | 4 ++ src/components/Players.tsx | 15 +++++- src/components/ViewerUI.tsx | 5 ++ src/components/common.tsx | 11 +++- src/model/card.ts | 33 ++++++++---- ...lla-glistendew.ts => amilla_glistendew.ts} | 28 ++++++++--- src/model/cards/legends/bridge_of_the_sky.ts | 36 ++++++++++++++ src/model/cards/legends/cirrus_windfall.ts | 36 ++++++++++++++ src/model/cards/legends/foresight.ts | 38 ++++++++++++++ src/model/cards/legends/fynn_nobletail.ts | 36 ++++++++++++++ src/model/cards/legends/index.ts | 10 ++++ src/model/cards/legends/mcgregors_market.ts | 34 +++++++++++++ .../cards/legends/oleanders_opera_house.ts | 39 +++++++++++++++ src/model/cards/legends/poe.ts | 34 +++++++++++++ .../cards/legends/silver_scale_spring.ts | 39 +++++++++++++++ src/model/cards/legends/the_green_acorn.ts | 39 +++++++++++++++ src/model/deck.ts | 26 ++++++++++ src/model/gameState.ts | 47 +++++++++++++++++- src/model/jsonTypes.ts | 4 ++ src/model/player.ts | 10 ++++ src/pages/api/create-game.ts | 3 +- src/pages/test/legends.tsx | 11 ++-- src/pages/test/ui.tsx | 16 ++++++ 26 files changed, 540 insertions(+), 33 deletions(-) create mode 100644 public/images/legendary_card.png rename src/model/cards/legends/{amilla-glistendew.ts => amilla_glistendew.ts} (69%) create mode 100644 src/model/cards/legends/bridge_of_the_sky.ts create mode 100644 src/model/cards/legends/cirrus_windfall.ts create mode 100644 src/model/cards/legends/foresight.ts create mode 100644 src/model/cards/legends/fynn_nobletail.ts create mode 100644 src/model/cards/legends/index.ts create mode 100644 src/model/cards/legends/mcgregors_market.ts create mode 100644 src/model/cards/legends/oleanders_opera_house.ts create mode 100644 src/model/cards/legends/poe.ts create mode 100644 src/model/cards/legends/silver_scale_spring.ts create mode 100644 src/model/cards/legends/the_green_acorn.ts diff --git a/.prettierrc b/.prettierrc index e69de29b..9e26dfee 100644 --- a/.prettierrc +++ b/.prettierrc @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/public/images/legendary_card.png b/public/images/legendary_card.png new file mode 100644 index 0000000000000000000000000000000000000000..17e1b850ecbeab36787f05a6db04114bf8d91c23 GIT binary patch literal 1222 zcmV;%1UdVOP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1Y1c&K~!i%?U~DJ z6hRb*t9o)H5u;IDC>X`XM0dLLF?;|;BJKnc6jw%!D?tcec8Uaj0UtqMKzEWXL=yx- zO-!N_CMMJ6In}2+)zvjKReed%h#!PL)sq?K?{oUEzZ3Sa!ReTraHjr2Fg^(%I3xsX zz$gE#%`Tjzm-mc91hrC8X#trmTd_I-{edz z1+$B1=u{~Lbj*afTYqPY2`^rwDhd7`=j=4RCxnaeiB1q39sB@Y!sHwCcM~U zY`e*VS5N8HG}twG|6-Bjm{|~Z2M%YW6@0l%*BKi>Kr4;SO%V2n1uq}bi8Mm+9l_X_ zB1|N+pw^wKA8@lDq9qHbZ!$JEMk@>0H4w&H+nu_W+eGqoD1?K2zr1e_#M-*kXn?pa z5cY&lq{(U`nFQfY4YPlOmfgG44(!?%+-Due5fC75$#opF3QC&6)!U4XjL<3r1)7Mr zAZIhkikpdqU?R)5u6YY8ygTi1G?CYD&re)nM zV;x>eA+^H~me3g+XgK2s0ECI@8?=a^@8>QEEVE8!#NZGW6%m5XTW}VXa5kLdGMvo^FbNHz z_9iDmnA@m~7^raE5O6)+$|3)KrP56hgvALet1$zUo!HbQwfhUlsO&C?ta{fB1}eUH zV8hi@1jZpMB`zI0LdS@z#|_hi24*`dB^?E!750=FV0Y%{Xd#KsK+#bUwDS~|5yKTb z8zyoM2&qd{#&i?}-PuoNWCm4RH9TJE1eUbjR#0WRtsOXSx>XClL1lurg7n-bsnw{c z2zav9f$#T`$^=$HW^M~FLvE_t3=FGo_ta_?)N{Gb*ht|@f1@&mRZve^HDcs8n;9rl zf_gr;3BF3Dm6V{K&uy~{hpDuZ5Y%tE&4$^bzRGPjVnO|w+ib*wpq+h}+ib*wpgVn+ z+iXZd{hHfss17B~pigp}4J9a6Q2Drd zl&%?O0J-h6ue&L{1u0ONfP~SpYpx~&TZA5UvY3G)^BWbpO(bmw8(;?GwDcyoARVu9 zR6L(!fk { - const rarity = card.expansion === ExpansionType.LEGENDS ? "Legendary" : 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,13 +85,13 @@ 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" + "Associated card is null and card is not Farm, MgGregors Market, or Evertree" ); } } 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/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 ( + Legendary Card Icon + ); +}; + 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 19ba20de..736565d6 100644 --- a/src/model/card.ts +++ b/src/model/card.ts @@ -44,7 +44,18 @@ import { } from "./gameText"; import { assertUnreachable } from "../utils"; -import amilla from "./cards/legends/amilla-glistendew"; +import { + amilla_glistendew, + bridge_of_the_sky, + cirrus_windfall, + foresight, + fynn_nobletail, + mcgregors_market, + oleanders_opera_house, + poe, + silver_scale_spring, + the_green_acorn, +} from "./cards/legends"; type NumWorkersInnerFn = (cardOwner: Player) => number; type ProductionInnerFn = ( @@ -3958,16 +3969,16 @@ const CARD_REGISTRY: Record = { /** * WIP: Legends Cards */ - [CardName.AMILLA_GLISTENDEW]: amilla(), - [CardName.BRIDGE_OF_THE_SKY]: amilla(), - [CardName.CIRRUS_WINDFALL]: amilla(), - [CardName.FORESIGHT]: amilla(), - [CardName.FYNN_NOBLETAIL]: amilla(), - [CardName.MCGREGORS_MARKET]: amilla(), - [CardName.OLEANDERS_OPERA_HOUSE]: amilla(), - [CardName.POE]: amilla(), - [CardName.SILVER_SCALE_SPRING]: amilla(), - [CardName.THE_GREEN_ACORN]: amilla(), + [CardName.AMILLA_GLISTENDEW]: new Card(amilla_glistendew), + [CardName.BRIDGE_OF_THE_SKY]: new Card(bridge_of_the_sky), + [CardName.CIRRUS_WINDFALL]: new Card(cirrus_windfall), + [CardName.FORESIGHT]: new Card(foresight), + [CardName.FYNN_NOBLETAIL]: new Card(fynn_nobletail), + [CardName.MCGREGORS_MARKET]: new Card(mcgregors_market), + [CardName.OLEANDERS_OPERA_HOUSE]: new Card(oleanders_opera_house), + [CardName.POE]: new Card(poe), + [CardName.SILVER_SCALE_SPRING]: new Card(silver_scale_spring), + [CardName.THE_GREEN_ACORN]: new Card(the_green_acorn), }; function getPointsPerRarityLabel({ diff --git a/src/model/cards/legends/amilla-glistendew.ts b/src/model/cards/legends/amilla_glistendew.ts similarity index 69% rename from src/model/cards/legends/amilla-glistendew.ts rename to src/model/cards/legends/amilla_glistendew.ts index fef8bae3..9fc63141 100644 --- a/src/model/cards/legends/amilla-glistendew.ts +++ b/src/model/cards/legends/amilla_glistendew.ts @@ -1,17 +1,25 @@ import { Card } from "../../card"; import { GameState } from "../../gameState"; import { toGameText } from "../../gameText"; -import { Player } from "../../player"; -import { CardName, CardType, ExpansionType, GameInput, GameInputType, ResourceType } from "../../types"; +import { + CardName, + CardType, + ExpansionType, + GameInput, + GameInputType, + ResourceType, +} from "../../types"; -export default (): Card => new Card({ +export const amilla_glistendew: ConstructorParameters[0] = { expansion: ExpansionType.LEGENDS, name: CardName.AMILLA_GLISTENDEW, associatedCard: CardName.QUEEN, cardType: CardType.DESTINATION, - cardDescription: toGameText("Achieve an Event, even if you don't meet the listed requirements."), + cardDescription: toGameText( + "Achieve an Event, even if you don't meet the listed requirements." + ), isConstruction: false, - isUnique: true, + isUnique: false, baseVP: 5, numInDeck: 1, resourcesToGain: {}, @@ -20,7 +28,11 @@ export default (): Card => new Card({ }, canPlayCheckInner: (gameState: GameState, gameInput: GameInput) => { if (gameInput.inputType === GameInputType.VISIT_DESTINATION_CARD) { - if (Object.entries(gameState.eventsMap).every(([eventName, playerId]) => playerId !== null)) { + if ( + Object.entries(gameState.eventsMap).every( + ([eventName, playerId]) => playerId !== null + ) + ) { return "No playable events"; } } @@ -38,5 +50,5 @@ export default (): Card => new Card({ }, }); } - } -}); \ No newline at end of file + }, +}; diff --git a/src/model/cards/legends/bridge_of_the_sky.ts b/src/model/cards/legends/bridge_of_the_sky.ts new file mode 100644 index 00000000..7c12a3b6 --- /dev/null +++ b/src/model/cards/legends/bridge_of_the_sky.ts @@ -0,0 +1,36 @@ +import { Card } from "../../card"; +import { GameState } from "../../gameState"; +import { toGameText } from "../../gameText"; +import { + CardName, + CardType, + ExpansionType, + GameInput, + ResourceType, +} from "../../types"; + +export const bridge_of_the_sky: ConstructorParameters[0] = { + expansion: ExpansionType.LEGENDS, + name: CardName.BRIDGE_OF_THE_SKY, + associatedCard: CardName.ARCHITECT, + cardType: CardType.GOVERNANCE, + cardDescription: toGameText([ + "You may play 1 ", + { type: "em", text: "Construction" }, + "for -3 ANY, then place it on top of this card.", + "PLUS is equal to the POINT 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/cirrus_windfall.ts b/src/model/cards/legends/cirrus_windfall.ts new file mode 100644 index 00000000..adb43369 --- /dev/null +++ b/src/model/cards/legends/cirrus_windfall.ts @@ -0,0 +1,36 @@ +import { Card } from "../../card"; +import { GameState } from "../../gameState"; +import { toGameText } from "../../gameText"; +import { + CardName, + CardType, + ExpansionType, + GameInput, + GameInputType, + ResourceType, +} from "../../types"; + +export const cirrus_windfall: ConstructorParameters[0] = { + expansion: ExpansionType.LEGENDS, + name: CardName.CIRRUS_WINDFALL, + associatedCard: CardName.POSTAL_PIGEON, + cardType: CardType.TRAVELER, + cardDescription: toGameText( + "You may play 1 CARD worth up to 3 POINT 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.ts b/src/model/cards/legends/foresight.ts new file mode 100644 index 00000000..fbfcfceb --- /dev/null +++ b/src/model/cards/legends/foresight.ts @@ -0,0 +1,38 @@ +import { Card } from "../../card"; +import { GameState } from "../../gameState"; +import { toGameText } from "../../gameText"; +import { + CardName, + CardType, + ExpansionType, + GameInput, + ResourceType, +} from "../../types"; + +export const foresight: ConstructorParameters[0] = { + expansion: ExpansionType.LEGENDS, + name: CardName.FORESIGHT, + associatedCard: CardName.HISTORIAN, + cardType: CardType.TRAVELER, + 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, + }, + 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/fynn_nobletail.ts b/src/model/cards/legends/fynn_nobletail.ts new file mode 100644 index 00000000..c6a75aee --- /dev/null +++ b/src/model/cards/legends/fynn_nobletail.ts @@ -0,0 +1,36 @@ +import { Card } from "../../card"; +import { GameState } from "../../gameState"; +import { toGameText } from "../../gameText"; +import { + CardName, + CardType, + ExpansionType, + GameInput, + ResourceType, +} from "../../types"; + +export const fynn_nobletail: ConstructorParameters[0] = { + expansion: ExpansionType.LEGENDS, + name: CardName.FYNN_NOBLETAIL, + associatedCard: CardName.KING, + cardType: CardType.PROSPERITY, + cardDescription: toGameText([ + "2 POINTS for each basic Event you achieved.", + "3 POINTS for each special Event you achieved.", + ]), + isConstruction: false, + isUnique: false, + baseVP: 5, + numInDeck: 1, + resourcesToGain: {}, + baseCost: { + [ResourceType.BERRY]: 7, + }, + 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/index.ts b/src/model/cards/legends/index.ts new file mode 100644 index 00000000..5b97aab0 --- /dev/null +++ b/src/model/cards/legends/index.ts @@ -0,0 +1,10 @@ +export { amilla_glistendew } from "./amilla_glistendew"; +export { bridge_of_the_sky } from "./bridge_of_the_sky"; +export { cirrus_windfall } from "./cirrus_windfall"; +export { foresight } from "./foresight"; +export { fynn_nobletail } from "./fynn_nobletail"; +export { mcgregors_market } from "./mcgregors_market"; +export { oleanders_opera_house } from "./oleanders_opera_house"; +export { poe } from "./poe"; +export { silver_scale_spring } from "./silver_scale_spring"; +export { the_green_acorn } from "./the_green_acorn"; diff --git a/src/model/cards/legends/mcgregors_market.ts b/src/model/cards/legends/mcgregors_market.ts new file mode 100644 index 00000000..7859c6e5 --- /dev/null +++ b/src/model/cards/legends/mcgregors_market.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 mcgregors_market: ConstructorParameters[0] = { + expansion: ExpansionType.LEGENDS, + name: CardName.MCGREGORS_MARKET, + associatedCard: null, + 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, + }, + 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/oleanders_opera_house.ts b/src/model/cards/legends/oleanders_opera_house.ts new file mode 100644 index 00000000..b9f5f925 --- /dev/null +++ b/src/model/cards/legends/oleanders_opera_house.ts @@ -0,0 +1,39 @@ +import { Card } from "../../card"; +import { GameState } from "../../gameState"; +import { toGameText } from "../../gameText"; +import { + CardName, + CardType, + ExpansionType, + GameInput, + ResourceType, +} from "../../types"; + +export const oleanders_opera_house: ConstructorParameters[0] = { + expansion: ExpansionType.LEGENDS, + name: CardName.OLEANDERS_OPERA_HOUSE, + associatedCard: CardName.BARD, + 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, + }, + 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/poe.ts b/src/model/cards/legends/poe.ts new file mode 100644 index 00000000..ebcc50a8 --- /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, + associatedCard: 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/silver_scale_spring.ts b/src/model/cards/legends/silver_scale_spring.ts new file mode 100644 index 00000000..3465fc7c --- /dev/null +++ b/src/model/cards/legends/silver_scale_spring.ts @@ -0,0 +1,39 @@ +import { Card } from "../../card"; +import { GameState } from "../../gameState"; +import { toGameText } from "../../gameText"; +import { + CardName, + CardType, + ExpansionType, + GameInput, + ResourceType, +} from "../../types"; + +export const silver_scale_spring: ConstructorParameters[0] = { + expansion: ExpansionType.LEGENDS, + name: CardName.OLEANDERS_OPERA_HOUSE, + associatedCard: CardName.PEDDLER, + 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/the_green_acorn.ts b/src/model/cards/legends/the_green_acorn.ts new file mode 100644 index 00000000..ce1f5ac5 --- /dev/null +++ b/src/model/cards/legends/the_green_acorn.ts @@ -0,0 +1,39 @@ +import { Card } from "../../card"; +import { GameState } from "../../gameState"; +import { toGameText } from "../../gameText"; +import { + CardName, + CardType, + ExpansionType, + GameInput, + ResourceType, +} from "../../types"; + +export const the_green_acorn: ConstructorParameters[0] = { + expansion: ExpansionType.LEGENDS, + name: CardName.THE_GREEN_ACORN, + associatedCard: CardName.INNKEEPER, + cardType: CardType.DESTINATION, + 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) => { + // TODO: Implement this + return null; + }, + playInner: (gameState: GameState, gameInput: GameInput) => { + // TODO: Implement this + }, +}; 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 8195461b..79f1c9d7 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, @@ -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 ? { @@ -1297,6 +1317,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 +1330,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 +1346,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 +1374,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 +1389,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; } 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..33aee3ae 100644 --- a/src/model/player.ts +++ b/src/model/player.ts @@ -58,6 +58,8 @@ export class Player implements IGameTextEntity { readonly adornmentsInHand: AdornmentName[]; readonly playedAdornments: AdornmentName[]; + readonly legendsInHand: CardName[]; + constructor({ name, playerSecret = uuid(), @@ -81,6 +83,7 @@ export class Player implements IGameTextEntity { playerStatus = PlayerStatus.DURING_SEASON, adornmentsInHand = [], playedAdornments = [], + legendsInHand = [], }: { name: string; playerSecret?: string; @@ -97,6 +100,7 @@ export class Player implements IGameTextEntity { playerStatus?: PlayerStatus; adornmentsInHand?: AdornmentName[]; playedAdornments?: AdornmentName[]; + legendsInHand?: CardName[]; }) { this.playerId = playerId; this.playerSecret = playerSecret; @@ -115,6 +119,9 @@ export class Player implements IGameTextEntity { this.adornmentsInHand = adornmentsInHand; this.playedAdornments = playedAdornments; + // legends only + this.legendsInHand = legendsInHand; + this._numCardsInHand = numCardsInHand; } @@ -1654,12 +1661,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/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/legends.tsx b/src/pages/test/legends.tsx index 08804084..f41c9601 100644 --- a/src/pages/test/legends.tsx +++ b/src/pages/test/legends.tsx @@ -31,7 +31,6 @@ export const getServerSideProps: GetServerSideProps = async (context) => { player.nextSeason(); player.drawCards(gameState, idx); - player.addToCity(gameState, CardName.AMILLA_GLISTENDEW); }); const game = new GameModel({ @@ -64,14 +63,14 @@ async function createGame(router: NextRouter, game: GameJSON) { } } -const AmillaGlistendew: FunctionComponent<{ game: GameJSON }> = (props)=> { +const AmillaGlistendew: FunctionComponent<{ game: GameJSON }> = (props) => { const router = useRouter(); useEffect(() => { createGame(router, props.game); - }, []) + }, []); - return null -} + return null; +}; -export default AmillaGlistendew +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) => { From 34ffed4aaabb784948d6ca96626af8baf66810eb Mon Sep 17 00:00:00 2001 From: Aaron Roberts Date: Tue, 1 Mar 2022 17:23:16 -0500 Subject: [PATCH 3/8] add upgradeableCard enhancement for card visual / payment --- src/components/Card.tsx | 26 +++++++--- src/components/CardPayment.tsx | 50 +++++++++++++++++-- src/components/GameInputPlayCard.tsx | 27 ++++++---- src/model/card.ts | 11 ++-- src/model/cards/legends/amilla_glistendew.ts | 2 +- src/model/cards/legends/bridge_of_the_sky.ts | 1 + src/model/cards/legends/cirrus_windfall.ts | 3 +- src/model/cards/legends/foresight.ts | 2 +- src/model/cards/legends/fynn_nobletail.ts | 2 +- src/model/cards/legends/mcgregors_market.ts | 13 +++-- .../cards/legends/oleanders_opera_house.ts | 1 + src/model/cards/legends/poe.ts | 2 +- .../cards/legends/silver_scale_spring.ts | 1 + src/model/cards/legends/the_green_acorn.ts | 1 + src/model/gameState.ts | 15 ++++++ src/model/player.ts | 10 ++++ src/model/types.ts | 13 +++++ src/styles/card.module.css | 15 +++++- src/styles/globals.css | 4 +- 19 files changed, 161 insertions(+), 38 deletions(-) diff --git a/src/components/Card.tsx b/src/components/Card.tsx index 9999f00f..a06e853a 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -24,6 +24,7 @@ const colorClassMap = { PRODUCTION: styles.color_production, DESTINATION: styles.color_destination, TRAVELER: styles.color_traveler, + LEGEND: styles.color_legend, }; function romanize(num: number): string { @@ -90,9 +91,7 @@ const getAssociatedCard = (card: CardModel) => { } else if (card.name == CardName.EVERTREE) { return "Any"; } else { - throw new Error( - "Associated card is null and card is not Farm, MgGregors Market, or Evertree" - ); + <> ; } } }; @@ -166,6 +165,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, @@ -196,6 +207,10 @@ const Card: React.FC<{ name: CardName; usedForCritter?: boolean }> = ({ )} +
+ {rarityLabel} +  · {romanize(card.numInDeck)} +
{Object.entries(card.baseCost).map(([resourceType, count], idx) => { @@ -231,10 +246,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..a61c5041 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"; @@ -97,13 +102,17 @@ const OptionToUseAssociatedCard: React.FC<{ const hasUnusedAssociatedCard = card.associatedCard && viewingPlayer.hasUnusedByCritterConstruction(card.associatedCard); + const canUseCardForLegendary = + card.associatedCard && + card.expansion == ExpansionType.LEGENDS && + viewingPlayer.hasCardInCity(card.associatedCard!); const hasUnusedEvertree = viewingPlayer.hasUnusedByCritterConstruction( CardName.EVERTREE ); const canUseAssociatedCard = card.isCritter && (hasUnusedAssociatedCard || hasUnusedEvertree); - if (!canUseAssociatedCard) { - return <>; + if (!canUseAssociatedCard && !canUseCardForLegendary) { + return null; } const isChecked = !!meta.value; @@ -231,6 +240,32 @@ const CardToDungeonForm: React.FC<{ ) : null; }; +const CardToUpgradeForm: React.FC<{ + name: string; + cardName: CardName; + viewingPlayer: Player; +}> = ({ name, cardName, viewingPlayer }) => { + const card = CardModel.fromName(cardName); + if (!card.upgradeableCard) { + return null; + } + + return ( +
+ +
+ ); +}; + const CardPayment: React.FC<{ name: string; resetPaymentOptions: (state: "DEFAULT" | "COST" | "ZERO") => void; @@ -247,7 +282,9 @@ const CardPayment: React.FC<{ resetPaymentOptions={resetPaymentOptions} /> )} - + {clientOptions.paymentOptions.resources && ( + + )} {clientOptions.card && ( + ); }; 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/model/card.ts b/src/model/card.ts index 736565d6..91a17636 100644 --- a/src/model/card.ts +++ b/src/model/card.ts @@ -96,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; @@ -115,6 +116,7 @@ export class Card isUnique, isConstruction, associatedCard, + upgradeableCard, resourcesToGain, productionInner, productionWillActivateInner, @@ -135,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; @@ -170,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; @@ -267,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 diff --git a/src/model/cards/legends/amilla_glistendew.ts b/src/model/cards/legends/amilla_glistendew.ts index 9fc63141..3d036cc4 100644 --- a/src/model/cards/legends/amilla_glistendew.ts +++ b/src/model/cards/legends/amilla_glistendew.ts @@ -13,7 +13,7 @@ import { export const amilla_glistendew: ConstructorParameters[0] = { expansion: ExpansionType.LEGENDS, name: CardName.AMILLA_GLISTENDEW, - associatedCard: CardName.QUEEN, + upgradeableCard: CardName.QUEEN, cardType: CardType.DESTINATION, cardDescription: toGameText( "Achieve an Event, even if you don't meet the listed requirements." diff --git a/src/model/cards/legends/bridge_of_the_sky.ts b/src/model/cards/legends/bridge_of_the_sky.ts index 7c12a3b6..47102697 100644 --- a/src/model/cards/legends/bridge_of_the_sky.ts +++ b/src/model/cards/legends/bridge_of_the_sky.ts @@ -13,6 +13,7 @@ export const bridge_of_the_sky: 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 ", diff --git a/src/model/cards/legends/cirrus_windfall.ts b/src/model/cards/legends/cirrus_windfall.ts index adb43369..80d42690 100644 --- a/src/model/cards/legends/cirrus_windfall.ts +++ b/src/model/cards/legends/cirrus_windfall.ts @@ -6,14 +6,13 @@ import { CardType, ExpansionType, GameInput, - GameInputType, ResourceType, } from "../../types"; export const cirrus_windfall: ConstructorParameters[0] = { expansion: ExpansionType.LEGENDS, name: CardName.CIRRUS_WINDFALL, - associatedCard: CardName.POSTAL_PIGEON, + upgradeableCard: CardName.POSTAL_PIGEON, cardType: CardType.TRAVELER, cardDescription: toGameText( "You may play 1 CARD worth up to 3 POINT for free." diff --git a/src/model/cards/legends/foresight.ts b/src/model/cards/legends/foresight.ts index fbfcfceb..0019d77f 100644 --- a/src/model/cards/legends/foresight.ts +++ b/src/model/cards/legends/foresight.ts @@ -12,7 +12,7 @@ import { export const foresight: ConstructorParameters[0] = { expansion: ExpansionType.LEGENDS, name: CardName.FORESIGHT, - associatedCard: CardName.HISTORIAN, + upgradeableCard: CardName.HISTORIAN, cardType: CardType.TRAVELER, cardDescription: toGameText([ "Draw 2 CARD after you play a ", diff --git a/src/model/cards/legends/fynn_nobletail.ts b/src/model/cards/legends/fynn_nobletail.ts index c6a75aee..fbe1523f 100644 --- a/src/model/cards/legends/fynn_nobletail.ts +++ b/src/model/cards/legends/fynn_nobletail.ts @@ -12,7 +12,7 @@ import { export const fynn_nobletail: ConstructorParameters[0] = { expansion: ExpansionType.LEGENDS, name: CardName.FYNN_NOBLETAIL, - associatedCard: CardName.KING, + upgradeableCard: CardName.KING, cardType: CardType.PROSPERITY, cardDescription: toGameText([ "2 POINTS for each basic Event you achieved.", diff --git a/src/model/cards/legends/mcgregors_market.ts b/src/model/cards/legends/mcgregors_market.ts index 7859c6e5..b7edca29 100644 --- a/src/model/cards/legends/mcgregors_market.ts +++ b/src/model/cards/legends/mcgregors_market.ts @@ -1,5 +1,6 @@ import { Card } from "../../card"; import { GameState } from "../../gameState"; +import { GainMoreThan1AnyResource } from "../../gameStatePlayHelpers"; import { toGameText } from "../../gameText"; import { CardName, @@ -13,6 +14,7 @@ export const mcgregors_market: ConstructorParameters[0] = { expansion: ExpansionType.LEGENDS, name: CardName.MCGREGORS_MARKET, associatedCard: null, + upgradeableCard: CardName.FARM, cardType: CardType.PRODUCTION, isConstruction: true, isUnique: false, @@ -24,11 +26,12 @@ export const mcgregors_market: ConstructorParameters[0] = { [ResourceType.RESIN]: 2, [ResourceType.PEBBLE]: 1, }, - canPlayCheckInner: (gameState: GameState, gameInput: GameInput) => { - // TODO: Implement this - return null; - }, playInner: (gameState: GameState, gameInput: GameInput) => { - // TODO: Implement this + const gainAnyHelper = new GainMoreThan1AnyResource({ + cardContext: CardName.MCGREGORS_MARKET, + }); + if (gainAnyHelper.matchesGameInput(gameInput)) { + gainAnyHelper.play(gameState, gameInput); + } }, }; diff --git a/src/model/cards/legends/oleanders_opera_house.ts b/src/model/cards/legends/oleanders_opera_house.ts index b9f5f925..629a5430 100644 --- a/src/model/cards/legends/oleanders_opera_house.ts +++ b/src/model/cards/legends/oleanders_opera_house.ts @@ -13,6 +13,7 @@ export const oleanders_opera_house: ConstructorParameters[0] = { expansion: ExpansionType.LEGENDS, name: CardName.OLEANDERS_OPERA_HOUSE, associatedCard: CardName.BARD, + upgradeableCard: CardName.THEATRE, cardType: CardType.PROSPERITY, isConstruction: true, isUnique: false, diff --git a/src/model/cards/legends/poe.ts b/src/model/cards/legends/poe.ts index ebcc50a8..fdebb9af 100644 --- a/src/model/cards/legends/poe.ts +++ b/src/model/cards/legends/poe.ts @@ -12,7 +12,7 @@ import { export const poe: ConstructorParameters[0] = { expansion: ExpansionType.LEGENDS, name: CardName.POE, - associatedCard: CardName.TEACHER, + upgradeableCard: CardName.TEACHER, cardType: CardType.PRODUCTION, cardDescription: toGameText( "Discard any number of CARD, then draw up to your hand limit." diff --git a/src/model/cards/legends/silver_scale_spring.ts b/src/model/cards/legends/silver_scale_spring.ts index 3465fc7c..09e2a600 100644 --- a/src/model/cards/legends/silver_scale_spring.ts +++ b/src/model/cards/legends/silver_scale_spring.ts @@ -13,6 +13,7 @@ export const silver_scale_spring: ConstructorParameters[0] = { expansion: ExpansionType.LEGENDS, name: CardName.OLEANDERS_OPERA_HOUSE, associatedCard: CardName.PEDDLER, + upgradeableCard: CardName.RUINS, cardType: CardType.TRAVELER, isConstruction: true, isUnique: false, diff --git a/src/model/cards/legends/the_green_acorn.ts b/src/model/cards/legends/the_green_acorn.ts index ce1f5ac5..3f33ff64 100644 --- a/src/model/cards/legends/the_green_acorn.ts +++ b/src/model/cards/legends/the_green_acorn.ts @@ -13,6 +13,7 @@ export const the_green_acorn: ConstructorParameters[0] = { expansion: ExpansionType.LEGENDS, name: CardName.THE_GREEN_ACORN, associatedCard: CardName.INNKEEPER, + upgradeableCard: CardName.INN, cardType: CardType.DESTINATION, isConstruction: true, isUnique: false, diff --git a/src/model/gameState.ts b/src/model/gameState.ts index 79f1c9d7..425aca36 100644 --- a/src/model/gameState.ts +++ b/src/model/gameState.ts @@ -1299,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, }); } @@ -1541,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/player.ts b/src/model/player.ts index 33aee3ae..1c199ad9 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"; @@ -1003,6 +1004,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 && @@ -1238,6 +1246,8 @@ export class Player implements IGameTextEntity { `Unexpected card: ${paymentOptions.cardToUse}` ); } + } else if (paymentOptions.cardToUpgrade) { + // Do something? } } diff --git a/src/model/types.ts b/src/model/types.ts index 33bf5f60..3929f522 100644 --- a/src/model/types.ts +++ b/src/model/types.ts @@ -586,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; diff --git a/src/styles/card.module.css b/src/styles/card.module.css index b94f8da1..a570e1b1 100644 --- a/src/styles/card.module.css +++ b/src/styles/card.module.css @@ -38,7 +38,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 +50,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 +60,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 +100,7 @@ font-size: 10px; font-weight: var(--font-bold); color: var(--text-color-light); + text-align: center; } .circle { diff --git a/src/styles/globals.css b/src/styles/globals.css index 562be452..d83f9031 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -24,12 +24,14 @@ --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-sign: sienna; + --color-legend: #b94147; --color-loc-basic: #88a55e; --color-loc-forest: #4e5627; From f54a869eb81cff81b961c782e61b928312bdc479 Mon Sep 17 00:00:00 2001 From: Aaron Roberts Date: Mon, 7 Mar 2022 17:44:55 -0500 Subject: [PATCH 4/8] little bit of cleanup --- src/components/CardPayment.tsx | 26 +++++++++++++------------- src/styles/globals.css | 1 - 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/components/CardPayment.tsx b/src/components/CardPayment.tsx index a61c5041..79eaa633 100644 --- a/src/components/CardPayment.tsx +++ b/src/components/CardPayment.tsx @@ -100,18 +100,16 @@ 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 canUseCardForLegendary = - card.associatedCard && - card.expansion == ExpansionType.LEGENDS && - viewingPlayer.hasCardInCity(card.associatedCard!); const hasUnusedEvertree = viewingPlayer.hasUnusedByCritterConstruction( CardName.EVERTREE ); const canUseAssociatedCard = card.isCritter && (hasUnusedAssociatedCard || hasUnusedEvertree); - if (!canUseAssociatedCard && !canUseCardForLegendary) { + + if (!canUseAssociatedCard) { return null; } @@ -244,9 +242,9 @@ const CardToUpgradeForm: React.FC<{ name: string; cardName: CardName; viewingPlayer: Player; -}> = ({ name, cardName, viewingPlayer }) => { +}> = ({ name, cardName }) => { const card = CardModel.fromName(cardName); - if (!card.upgradeableCard) { + if (card.expansion !== ExpansionType.LEGENDS || !card.upgradeableCard) { return null; } @@ -255,7 +253,7 @@ const CardToUpgradeForm: React.FC<{