This is a data registry for game cards, holding definitions for all card types used in the simulation. It provides static factory methods to define cards and access them by ID.
using System;
using System.Collections.Generic;
using System.Linq;
namespace CitySim.Cards;
/// <summary>GDD v0.12 card catalog. Forest is Sapling's grown state, not a draw.</summary>
public static class CardLibrary
{
static CardEffect E( EffectHook hook, string rule, int amount = 0, int range = 0, int cap = 0, int divisor = 1 ) => new( hook, rule, amount, range, cap, divisor );
static CardDef C( string id, string name, CardType type, Tag tags, Rarity rarity, bool road,
string icon, string rules, int weight, CardEffect[] effects, bool tray = false, int limit = 0, int park = 0 ) =>
new( id, name, type, tags, rarity, road, icon, rules, weight, Array.AsReadOnly( effects ), tray, limit, park );
static IReadOnlyList<CardDef> Create() => Array.AsReadOnly( new[]
{
C("street", "#cards.street.name", CardType.Road, Tag.None, Rarity.Common, false, "🛣️", "#cards.street.rules", 14, [E(EffectHook.OwnerPlacedCard,"street-building-vp",1)]),
C("house", "#cards.house.name", CardType.Residential, Tag.Home, Rarity.Common, false, "🏠", "#cards.house.rules", 10,
[E(EffectHook.Place,"beside-road-vp",1), E(EffectHook.Payout,"shop-condition",1,2), E(EffectHook.Place,"adjacent-home-tag-vp",1)]),
C("cottage", "#cards.cottage.name", CardType.Residential, Tag.Home, Rarity.Common, false, "🏡", "#cards.cottage.rules", 4,
[E(EffectHook.Payout,"beside-park-coins",1), E(EffectHook.Place,"adjacent-park-vp",1)]),
C("shop", "#cards.shop.name", CardType.Commerce, Tag.Trade, Rarity.Common, true, "🏪", "#cards.shop.rules", 6,
[E(EffectHook.Payout,"nearby-any-homes",1,2,3)]),
C("farm", "#cards.farm.name", CardType.Industry, Tag.Nature, Rarity.Common, false, "🌾", "#cards.farm.rules", 6,
[E(EffectHook.Payout,"adjacent-farm-condition",1)]),
C("park", "#cards.park.name", CardType.Green, Tag.Nature, Rarity.Common, false, "🌳", "#cards.park.rules", 8, [E(EffectHook.Place,"park-score",1), E(EffectHook.Place,"beside-nature-vp",1)], park:1),
C("wild-garden", "#cards.wild_garden.name", CardType.Green, Tag.Nature, Rarity.Common, false, "🌿", "#cards.wild_garden.rules", 3, [E(EffectHook.Place,"park-score",1), E(EffectHook.Place,"beside-nature-vp",1)], park:1),
C("grant", "#cards.grant.name", CardType.Event, Tag.None, Rarity.Common, false, "🪙", "#cards.grant.rules", 2, [E(EffectHook.Place,"coins",4)]),
C("avenue", "#cards.avenue.name", CardType.Road, Tag.None, Rarity.Uncommon, false, "🛣️", "#cards.avenue.rules", 3, [E(EffectHook.Payout,"avenue-buildings",1,0,2)]),
C("roundabout", "#cards.roundabout.name", CardType.Road, Tag.None, Rarity.Uncommon, false, "🔄", "#cards.roundabout.rules", 2, [E(EffectHook.OwnerPlacedCard,"roundabout-vp",1)]),
C("orchard", "#cards.orchard.name", CardType.Industry, Tag.Nature, Rarity.Uncommon, false, "🍎", "#cards.orchard.rules", 2, [E(EffectHook.Payout,"adjacent-farm-condition",1), E(EffectHook.Place,"park-score",1), E(EffectHook.Place,"beside-nature-vp",1)], park:1),
C("lake", "#cards.lake.name", CardType.Green, Tag.Nature, Rarity.Uncommon, false, "🌊", "#cards.lake.rules", 2, [E(EffectHook.Place,"park-score",1), E(EffectHook.Place,"beside-nature-vp",1), E(EffectHook.Payout,"lake-houses",1,1)], park:1),
C("vineyard", "#cards.vineyard.name", CardType.Growth, Tag.Nature | Tag.Trade, Rarity.Uncommon, false, "🍇", "#cards.vineyard.rules", 2, [E(EffectHook.Payout,"delayed-coins",3,0,2)]),
C("startup", "#cards.startup.name", CardType.Growth, Tag.Trade, Rarity.Rare, false, "💡", "#cards.startup.rules", 2, [E(EffectHook.Payout,"ramping-coins",1,0,3)]),
C("market", "#cards.market.name", CardType.Commerce, Tag.Trade, Rarity.Rare, true, "🏬", "#cards.market.rules", 3, [E(EffectHook.Payout,"nearby-shops",1,2,4)], true),
C("contractor", "#cards.contractor.name", CardType.Civic, Tag.Industry | Tag.Civic, Rarity.Rare, false, "👷", "#cards.contractor.rules", 2, [E(EffectHook.OwnerPlacedCard,"contractor-coins",1)], true,1),
C("apartment", "#cards.apartment.name", CardType.Residential, Tag.Home, Rarity.Uncommon, true, "🏢", "#cards.apartment.rules", 3,
[E(EffectHook.Payout,"shop-condition",2,2), E(EffectHook.Place,"adjacent-home-tag-vp",2)]),
C("cafe", "#cards.cafe.name", CardType.Commerce, Tag.Trade | Tag.Culture, Rarity.Uncommon, false, "☕", "#cards.cafe.rules", 4,
[E(EffectHook.Place,"beside-road-vp",1), E(EffectHook.Payout,"cafe-neighbours",1,1,3), E(EffectHook.EndGame,"beside-park-vp",2)]),
C("fountain", "#cards.fountain.name", CardType.Green, Tag.Nature | Tag.Culture, Rarity.Uncommon, false, "⛲", "#cards.fountain.rules", 3, [E(EffectHook.Place,"park-score",2), E(EffectHook.Place,"beside-nature-vp",1)], park:1),
C("factory", "#cards.factory.name", CardType.Industry, Tag.Industry, Rarity.Uncommon, true, "🏭", "#cards.factory.rules", 3,
[E(EffectHook.Payout,"coins",2), E(EffectHook.EndGame,"pollution",1,1)]),
C("sapling", "#cards.sapling.name", CardType.Growth, Tag.Nature, Rarity.Uncommon, false, "🌱", "#cards.sapling.rules", 3,
[E(EffectHook.Growth,"forest",2,0,2), E(EffectHook.Place,"park-score",1), E(EffectHook.Place,"beside-nature-vp",1)], park:1),
C("bank", "#cards.bank.name", CardType.Civic, Tag.Trade, Rarity.Rare, false, "🏦", "#cards.bank.rules", 4, [E(EffectHook.Payout,"coins",2)], true,3),
C("town-hall", "#cards.town_hall.name", CardType.Civic, Tag.Civic, Rarity.Rare, true, "🏛️", "#cards.town_hall.rules", 2,
[E(EffectHook.Payout,"civic-threshold",2,0,3)], true,1),
C("museum", "#cards.museum.name", CardType.Civic, Tag.Culture | Tag.Civic, Rarity.Rare, false, "🖼️", "#cards.museum.rules", 2,
[E(EffectHook.Place,"beside-road-vp",1), E(EffectHook.EndGame,"other-culture-vp",1,0,8)], true,1),
C("chamber-of-commerce", "#cards.chamber_of_commerce.name", CardType.Civic, Tag.Trade | Tag.Civic, Rarity.Rare, true, "🤝", "#cards.chamber_of_commerce.rules", 2,
[E(EffectHook.Payout,"trade-pairs",1,0,4,2)], true,1),
C("warehouse", "#cards.warehouse.name", CardType.Civic, Tag.Industry, Rarity.Uncommon, true, "📦", "#cards.warehouse.rules", 2, [E(EffectHook.RoundStart,"builder-street",1), E(EffectHook.OwnerPlacedCard,"warehouse-street-coins",1,0,1)], true,1),
C("builders-yard", "#cards.builders_yard.name", CardType.Civic, Tag.Industry, Rarity.Rare, true, "🏗️", "#cards.builders_yard.rules", 2, [E(EffectHook.RoundStart,"builder-choice",1)], true,1),
C("hospital", "#cards.hospital.name", CardType.Civic, Tag.Civic, Rarity.Rare, false, "🏥", "#cards.hospital.rules", 2, [E(EffectHook.Place,"beside-road-vp",1), E(EffectHook.OwnerPlacedCard,"hospital-emergency",1,4)], true,1),
C("tourist-office", "#cards.tourist_office.name", CardType.Civic, Tag.Culture | Tag.Civic, Rarity.Rare, false, "🗺️", "#cards.tourist_office.rules", 2, [E(EffectHook.Place,"beside-road-vp",1), E(EffectHook.Payout,"culture-count",1,0,3)], true,1),
C("tax-office", "#cards.tax_office.name", CardType.Civic, Tag.Civic, Rarity.Uncommon, false, "🧾", "#cards.tax_office.rules", 2, [E(EffectHook.Payout,"house-groups",1,0,0,4)], true,1),
C("depot", "#cards.depot.name", CardType.Civic, Tag.Industry, Rarity.Uncommon, true, "🚉", "#cards.depot.rules", 2, [E(EffectHook.Payout,"road-groups",1,0,4,5)], true,1),
C("school", "#cards.school.name", CardType.Civic, Tag.Civic | Tag.Culture, Rarity.Uncommon, false, "🏫", "#cards.school.rules", 2, [E(EffectHook.Place,"beside-road-vp",1), E(EffectHook.Payout,"nearby-homes",1,2,4)], true,1),
C("granary", "#cards.granary.name", CardType.Civic, Tag.Civic, Rarity.Uncommon, false, "🌾", "#cards.granary.rules", 2, [E(EffectHook.Payout,"owned-farms",1,0,3)], true,1),
C("stadium", "#cards.stadium.name", CardType.Landmark, Tag.Culture | Tag.Civic, Rarity.Rare, true, "🏟️", "#cards.stadium.rules", 2, [E(EffectHook.Payout,"stadium-neighbours",1,1), E(EffectHook.EndGame,"stadium-house-vp",1,1)]),
C("airport", "#cards.airport.name", CardType.Landmark, Tag.Industry | Tag.Civic, Rarity.Rare, true, "✈️", "#cards.airport.rules", 2, [E(EffectHook.Payout,"coins",3), E(EffectHook.EndGame,"airport-noise",1,2,3)]),
C("skyscraper", "#cards.skyscraper.name", CardType.Landmark, Tag.Trade | Tag.Civic, Rarity.Rare, true, "🏙️", "#cards.skyscraper.rules", 2, [E(EffectHook.Place,"minimum-buildings",0,0,2), E(EffectHook.EndGame,"nearby-building-vp",1,2,6)]),
C("industrial-estate", "#cards.industrial_estate.name", CardType.Civic, Tag.Industry | Tag.Civic, Rarity.Rare, true, "🏭", "#cards.industrial_estate.rules", 2,
[E(EffectHook.Payout,"industry-count",1,0,5)], true,1),
C("housing-authority", "#cards.housing_authority.name", CardType.Civic, Tag.Home | Tag.Civic, Rarity.Rare, false, "🏘️", "#cards.housing_authority.rules", 2,
[E(EffectHook.EndGame,"home-thirds-vp",1,0,6,3)], true,1),
C("eco-council", "#cards.eco_council.name", CardType.Civic, Tag.Nature | Tag.Civic, Rarity.Rare, false, "🌿", "#cards.eco_council.rules", 2,
[E(EffectHook.EndGame,"eco-council-vp",1,2,8,2)], true,1),
C("gardeners-guild", "#cards.gardeners_guild.name", CardType.Civic, Tag.Nature | Tag.Civic, Rarity.Uncommon, false, "🧑🌾", "#cards.gardeners_guild.rules", 2,
[E(EffectHook.OwnerPlacedCard,"park-coins",1)], true,1),
C("architects-studio", "#cards.architects_studio.name", CardType.Civic, Tag.Culture | Tag.Civic, Rarity.Rare, false, "📐", "#cards.architects_studio.rules", 2,
[E(EffectHook.OwnerPlacedCard,"first-building-beside-own",2)], true,1),
C("renovation", "#cards.renovation.name", CardType.Event, Tag.None, Rarity.Uncommon, false, "🔨", "#cards.renovation.rules", 2, [E(EffectHook.Place,"renovation")]),
C("ribbon-cutting", "#cards.ribbon_cutting.name", CardType.Event, Tag.None, Rarity.Common, false, "🎀", "#cards.ribbon_cutting.rules", 1, [E(EffectHook.Place,"round-building-vp",1,0,4)]),
C("rush-hour", "#cards.rush_hour.name", CardType.Event, Tag.None, Rarity.Common, false, "🚦", "#cards.rush_hour.rules", 1, [E(EffectHook.Place,"rush-hour",1,0,4)]),
C("land-claim", "#cards.land_claim.name", CardType.Event, Tag.None, Rarity.Uncommon, false, "🚩", "#cards.land_claim.rules", 2, [E(EffectHook.Reserve,"land-claim",1)]),
C("central-station", "#cards.central_station.name", CardType.Landmark, Tag.Trade | Tag.Civic, Rarity.Legendary, true, "🚉", "#cards.central_station.rules", 1, [E(EffectHook.Payout,"owned-shops-markets",1,0,4)], true),
C("senate", "#cards.senate.name", CardType.Landmark, Tag.Civic, Rarity.Legendary, true, "🏛️", "#cards.senate.rules", 1, [E(EffectHook.EndGame,"tag-majorities-vp",2,0,12,3)], true),
C("monument", "#cards.monument.name", CardType.Landmark, Tag.Culture, Rarity.Legendary, false, "🗿", "#cards.monument.rules", 1, [E(EffectHook.EndGame,"vp",6)], true)
} );
// Increment when card data changes: the editor preserves static fields across play restarts/hotloads.
const int DataVersion = 20;
static int loadedVersion;
static IReadOnlyList<CardDef> catalog;
static IReadOnlyDictionary<string,CardDef> byId;
static void Ensure()
{
if (catalog != null && loadedVersion == DataVersion) return;
catalog = Create(); byId = catalog.ToDictionary(card => card.Id,StringComparer.Ordinal); loadedVersion = DataVersion;
}
public static IReadOnlyList<CardDef> All { get { Ensure(); return catalog; } }
public static CardDef Get( string id ) { Ensure(); return byId.TryGetValue( id ?? "", out var card ) ? card : throw new ArgumentException( $"Unknown card: {id}", nameof(id) ); }
public static bool TryGet( string id, out CardDef card ) { Ensure(); return byId.TryGetValue( id ?? "", out card ); }
}