commit 6ee8432c26b6290746c1475f7428ecdbc53ed992 Author: grovedruid Date: Tue May 12 04:56:41 2026 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..deed335 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +.env diff --git a/assets/DSC05189.JPG b/assets/DSC05189.JPG new file mode 100755 index 0000000..0930d1f Binary files /dev/null and b/assets/DSC05189.JPG differ diff --git a/assets/card-background.png b/assets/card-background.png new file mode 100644 index 0000000..aa8a437 Binary files /dev/null and b/assets/card-background.png differ diff --git a/assets/cards/1-gift-of-water-action.png b/assets/cards/1-gift-of-water-action.png new file mode 100644 index 0000000..2823d96 Binary files /dev/null and b/assets/cards/1-gift-of-water-action.png differ diff --git a/assets/cards/10-tree-planter.png b/assets/cards/10-tree-planter.png new file mode 100644 index 0000000..f6c668c Binary files /dev/null and b/assets/cards/10-tree-planter.png differ diff --git a/assets/cards/11-seed-scout.png b/assets/cards/11-seed-scout.png new file mode 100644 index 0000000..e4efc21 Binary files /dev/null and b/assets/cards/11-seed-scout.png differ diff --git a/assets/cards/12-natural-spritz.png b/assets/cards/12-natural-spritz.png new file mode 100644 index 0000000..2b22fd6 Binary files /dev/null and b/assets/cards/12-natural-spritz.png differ diff --git a/assets/cards/13-dashboard-designer.png b/assets/cards/13-dashboard-designer.png new file mode 100644 index 0000000..288df48 Binary files /dev/null and b/assets/cards/13-dashboard-designer.png differ diff --git a/assets/cards/14-onion-bodyguard.png b/assets/cards/14-onion-bodyguard.png new file mode 100644 index 0000000..0429765 Binary files /dev/null and b/assets/cards/14-onion-bodyguard.png differ diff --git a/assets/cards/15-local-investor.png b/assets/cards/15-local-investor.png new file mode 100644 index 0000000..71cc200 Binary files /dev/null and b/assets/cards/15-local-investor.png differ diff --git a/assets/cards/16-poster-designer.png b/assets/cards/16-poster-designer.png new file mode 100644 index 0000000..da50a72 Binary files /dev/null and b/assets/cards/16-poster-designer.png differ diff --git a/assets/cards/17-change-maker.png b/assets/cards/17-change-maker.png new file mode 100644 index 0000000..1aebb0e Binary files /dev/null and b/assets/cards/17-change-maker.png differ diff --git a/assets/cards/18-waste-remover.png b/assets/cards/18-waste-remover.png new file mode 100644 index 0000000..00b4cf4 Binary files /dev/null and b/assets/cards/18-waste-remover.png differ diff --git a/assets/cards/19-manure-maker.png b/assets/cards/19-manure-maker.png new file mode 100644 index 0000000..51ed796 Binary files /dev/null and b/assets/cards/19-manure-maker.png differ diff --git a/assets/cards/2-grow-greens-action.png b/assets/cards/2-grow-greens-action.png new file mode 100644 index 0000000..d0a0438 Binary files /dev/null and b/assets/cards/2-grow-greens-action.png differ diff --git a/assets/cards/20-mixed-garden.png b/assets/cards/20-mixed-garden.png new file mode 100644 index 0000000..aa28675 Binary files /dev/null and b/assets/cards/20-mixed-garden.png differ diff --git a/assets/cards/21-ash-bug-blaster.png b/assets/cards/21-ash-bug-blaster.png new file mode 100644 index 0000000..9cd8e53 Binary files /dev/null and b/assets/cards/21-ash-bug-blaster.png differ diff --git a/assets/cards/22-dry-and-store.png b/assets/cards/22-dry-and-store.png new file mode 100644 index 0000000..94d9cbb Binary files /dev/null and b/assets/cards/22-dry-and-store.png differ diff --git a/assets/cards/23-seed-distributor.png b/assets/cards/23-seed-distributor.png new file mode 100644 index 0000000..705729a Binary files /dev/null and b/assets/cards/23-seed-distributor.png differ diff --git a/assets/cards/24-river-cleaners.png b/assets/cards/24-river-cleaners.png new file mode 100644 index 0000000..8505c86 Binary files /dev/null and b/assets/cards/24-river-cleaners.png differ diff --git a/assets/cards/25-garden-documenter.png b/assets/cards/25-garden-documenter.png new file mode 100644 index 0000000..b0b2aba Binary files /dev/null and b/assets/cards/25-garden-documenter.png differ diff --git a/assets/cards/26-local-variety-guardian.png b/assets/cards/26-local-variety-guardian.png new file mode 100644 index 0000000..c96c45e Binary files /dev/null and b/assets/cards/26-local-variety-guardian.png differ diff --git a/assets/cards/27-nature-regenerator.png b/assets/cards/27-nature-regenerator.png new file mode 100644 index 0000000..379044b Binary files /dev/null and b/assets/cards/27-nature-regenerator.png differ diff --git a/assets/cards/28-biodiversity-steward.png b/assets/cards/28-biodiversity-steward.png new file mode 100644 index 0000000..f61823e Binary files /dev/null and b/assets/cards/28-biodiversity-steward.png differ diff --git a/assets/cards/29-land-negotiator.png b/assets/cards/29-land-negotiator.png new file mode 100644 index 0000000..1d3e5f7 Binary files /dev/null and b/assets/cards/29-land-negotiator.png differ diff --git a/assets/cards/3-fine-dining-compost-dish-action.png b/assets/cards/3-fine-dining-compost-dish-action.png new file mode 100644 index 0000000..6619c59 Binary files /dev/null and b/assets/cards/3-fine-dining-compost-dish-action.png differ diff --git a/assets/cards/30-tjd-booster-action.png b/assets/cards/30-tjd-booster-action.png new file mode 100644 index 0000000..8e8b661 Binary files /dev/null and b/assets/cards/30-tjd-booster-action.png differ diff --git a/assets/cards/31-kitchen-garden.png b/assets/cards/31-kitchen-garden.png new file mode 100644 index 0000000..0b8a37c Binary files /dev/null and b/assets/cards/31-kitchen-garden.png differ diff --git a/assets/cards/32-balanced-plate-builder.png b/assets/cards/32-balanced-plate-builder.png new file mode 100644 index 0000000..c72b498 Binary files /dev/null and b/assets/cards/32-balanced-plate-builder.png differ diff --git a/assets/cards/33-indigenous-tree-lover.png b/assets/cards/33-indigenous-tree-lover.png new file mode 100644 index 0000000..a869f0c Binary files /dev/null and b/assets/cards/33-indigenous-tree-lover.png differ diff --git a/assets/cards/34-cover-crop-protector.png b/assets/cards/34-cover-crop-protector.png new file mode 100644 index 0000000..7c8b152 Binary files /dev/null and b/assets/cards/34-cover-crop-protector.png differ diff --git a/assets/cards/35-local-food-producer.png b/assets/cards/35-local-food-producer.png new file mode 100644 index 0000000..b8ea254 Binary files /dev/null and b/assets/cards/35-local-food-producer.png differ diff --git a/assets/cards/36-market-price-checker.png b/assets/cards/36-market-price-checker.png new file mode 100644 index 0000000..c1b9916 Binary files /dev/null and b/assets/cards/36-market-price-checker.png differ diff --git a/assets/cards/37-community-meeting.png b/assets/cards/37-community-meeting.png new file mode 100644 index 0000000..b71ad12 Binary files /dev/null and b/assets/cards/37-community-meeting.png differ diff --git a/assets/cards/38-jar-farm-product-marketer.png b/assets/cards/38-jar-farm-product-marketer.png new file mode 100644 index 0000000..a14b406 Binary files /dev/null and b/assets/cards/38-jar-farm-product-marketer.png differ diff --git a/assets/cards/39-food-donation-hero.png b/assets/cards/39-food-donation-hero.png new file mode 100644 index 0000000..b1b5b38 Binary files /dev/null and b/assets/cards/39-food-donation-hero.png differ diff --git a/assets/cards/4-buzzing-with-wisdom-action.png b/assets/cards/4-buzzing-with-wisdom-action.png new file mode 100644 index 0000000..c2746de Binary files /dev/null and b/assets/cards/4-buzzing-with-wisdom-action.png differ diff --git a/assets/cards/40-windbreak-warrior.png b/assets/cards/40-windbreak-warrior.png new file mode 100644 index 0000000..f524f47 Binary files /dev/null and b/assets/cards/40-windbreak-warrior.png differ diff --git a/assets/cards/41-seed-champion.png b/assets/cards/41-seed-champion.png new file mode 100644 index 0000000..48d565b Binary files /dev/null and b/assets/cards/41-seed-champion.png differ diff --git a/assets/cards/42-fruit-and-tree-team.png b/assets/cards/42-fruit-and-tree-team.png new file mode 100644 index 0000000..640f5a5 Binary files /dev/null and b/assets/cards/42-fruit-and-tree-team.png differ diff --git a/assets/cards/43-manure-cycle-drawer.png b/assets/cards/43-manure-cycle-drawer.png new file mode 100644 index 0000000..ee7faa4 Binary files /dev/null and b/assets/cards/43-manure-cycle-drawer.png differ diff --git a/assets/cards/44-animal-hero.png b/assets/cards/44-animal-hero.png new file mode 100644 index 0000000..77af5a0 Binary files /dev/null and b/assets/cards/44-animal-hero.png differ diff --git a/assets/cards/45-land-magic.png b/assets/cards/45-land-magic.png new file mode 100644 index 0000000..c6ff021 Binary files /dev/null and b/assets/cards/45-land-magic.png differ diff --git a/assets/cards/46-local-chicken-lover.png b/assets/cards/46-local-chicken-lover.png new file mode 100644 index 0000000..c6182a5 Binary files /dev/null and b/assets/cards/46-local-chicken-lover.png differ diff --git a/assets/cards/47-food-dryer-explorer.png b/assets/cards/47-food-dryer-explorer.png new file mode 100644 index 0000000..7caebc2 Binary files /dev/null and b/assets/cards/47-food-dryer-explorer.png differ diff --git a/assets/cards/48-scrap-tastic-animal-feeder.png b/assets/cards/48-scrap-tastic-animal-feeder.png new file mode 100644 index 0000000..cc10081 Binary files /dev/null and b/assets/cards/48-scrap-tastic-animal-feeder.png differ diff --git a/assets/cards/49-leftover-chef.png b/assets/cards/49-leftover-chef.png new file mode 100644 index 0000000..8a41e9d Binary files /dev/null and b/assets/cards/49-leftover-chef.png differ diff --git a/assets/cards/5-baking-flatbread.png b/assets/cards/5-baking-flatbread.png new file mode 100644 index 0000000..274e2c4 Binary files /dev/null and b/assets/cards/5-baking-flatbread.png differ diff --git a/assets/cards/50-weather-tracker-checker.png b/assets/cards/50-weather-tracker-checker.png new file mode 100644 index 0000000..05c5568 Binary files /dev/null and b/assets/cards/50-weather-tracker-checker.png differ diff --git a/assets/cards/51-farm-record-keeper.png b/assets/cards/51-farm-record-keeper.png new file mode 100644 index 0000000..10ccd13 Binary files /dev/null and b/assets/cards/51-farm-record-keeper.png differ diff --git a/assets/cards/52-the-water-fertilizers.png b/assets/cards/52-the-water-fertilizers.png new file mode 100644 index 0000000..589e698 Binary files /dev/null and b/assets/cards/52-the-water-fertilizers.png differ diff --git a/assets/cards/53-small-farm-big-voice.png b/assets/cards/53-small-farm-big-voice.png new file mode 100644 index 0000000..7e7e193 Binary files /dev/null and b/assets/cards/53-small-farm-big-voice.png differ diff --git a/assets/cards/54-nature-educator.png b/assets/cards/54-nature-educator.png new file mode 100644 index 0000000..d7780e2 Binary files /dev/null and b/assets/cards/54-nature-educator.png differ diff --git a/assets/cards/55-teamwork-harvest.png b/assets/cards/55-teamwork-harvest.png new file mode 100644 index 0000000..e48331c Binary files /dev/null and b/assets/cards/55-teamwork-harvest.png differ diff --git a/assets/cards/56-sky-garden.png b/assets/cards/56-sky-garden.png new file mode 100644 index 0000000..2b11e14 Binary files /dev/null and b/assets/cards/56-sky-garden.png differ diff --git a/assets/cards/57-be-the-change-action.png b/assets/cards/57-be-the-change-action.png new file mode 100644 index 0000000..8f1b57d Binary files /dev/null and b/assets/cards/57-be-the-change-action.png differ diff --git a/assets/cards/58-ancient-wisdom-defender-action.png b/assets/cards/58-ancient-wisdom-defender-action.png new file mode 100644 index 0000000..6a98efd Binary files /dev/null and b/assets/cards/58-ancient-wisdom-defender-action.png differ diff --git a/assets/cards/59-seedling-protector-action.png b/assets/cards/59-seedling-protector-action.png new file mode 100644 index 0000000..2bd3985 Binary files /dev/null and b/assets/cards/59-seedling-protector-action.png differ diff --git a/assets/cards/6-mixed-wastemonster-action.png b/assets/cards/6-mixed-wastemonster-action.png new file mode 100644 index 0000000..e2667ce Binary files /dev/null and b/assets/cards/6-mixed-wastemonster-action.png differ diff --git a/assets/cards/60-cooperative-builder-action.png b/assets/cards/60-cooperative-builder-action.png new file mode 100644 index 0000000..73ea174 Binary files /dev/null and b/assets/cards/60-cooperative-builder-action.png differ diff --git a/assets/cards/7-ingredient-detective-action.png b/assets/cards/7-ingredient-detective-action.png new file mode 100644 index 0000000..53513a5 Binary files /dev/null and b/assets/cards/7-ingredient-detective-action.png differ diff --git a/assets/cards/8-mindful-eating-action.png b/assets/cards/8-mindful-eating-action.png new file mode 100644 index 0000000..803f61e Binary files /dev/null and b/assets/cards/8-mindful-eating-action.png differ diff --git a/assets/cards/9-fermentastic-action.png b/assets/cards/9-fermentastic-action.png new file mode 100644 index 0000000..eafea24 Binary files /dev/null and b/assets/cards/9-fermentastic-action.png differ diff --git a/assets/cgiar-mfl-logo.png b/assets/cgiar-mfl-logo.png new file mode 100644 index 0000000..8bafd59 Binary files /dev/null and b/assets/cgiar-mfl-logo.png differ diff --git a/assets/fsys.png b/assets/fsys.png new file mode 100644 index 0000000..916b09c Binary files /dev/null and b/assets/fsys.png differ diff --git a/assets/game-background.png b/assets/game-background.png new file mode 100644 index 0000000..ee08237 Binary files /dev/null and b/assets/game-background.png differ diff --git a/assets/icons/MFL/MFL-1.png b/assets/icons/MFL/MFL-1.png new file mode 100644 index 0000000..5b3031b Binary files /dev/null and b/assets/icons/MFL/MFL-1.png differ diff --git a/assets/icons/MFL/MFL-10.png b/assets/icons/MFL/MFL-10.png new file mode 100644 index 0000000..ad6e3f7 Binary files /dev/null and b/assets/icons/MFL/MFL-10.png differ diff --git a/assets/icons/MFL/MFL-11.png b/assets/icons/MFL/MFL-11.png new file mode 100644 index 0000000..4179c1b Binary files /dev/null and b/assets/icons/MFL/MFL-11.png differ diff --git a/assets/icons/MFL/MFL-12.png b/assets/icons/MFL/MFL-12.png new file mode 100644 index 0000000..9a1c1e1 Binary files /dev/null and b/assets/icons/MFL/MFL-12.png differ diff --git a/assets/icons/MFL/MFL-2.png b/assets/icons/MFL/MFL-2.png new file mode 100644 index 0000000..b24c6b5 Binary files /dev/null and b/assets/icons/MFL/MFL-2.png differ diff --git a/assets/icons/MFL/MFL-3.png b/assets/icons/MFL/MFL-3.png new file mode 100644 index 0000000..dabbb62 Binary files /dev/null and b/assets/icons/MFL/MFL-3.png differ diff --git a/assets/icons/MFL/MFL-4.png b/assets/icons/MFL/MFL-4.png new file mode 100644 index 0000000..689bec6 Binary files /dev/null and b/assets/icons/MFL/MFL-4.png differ diff --git a/assets/icons/MFL/MFL-5.png b/assets/icons/MFL/MFL-5.png new file mode 100644 index 0000000..e2fdc8a Binary files /dev/null and b/assets/icons/MFL/MFL-5.png differ diff --git a/assets/icons/MFL/MFL-6.png b/assets/icons/MFL/MFL-6.png new file mode 100644 index 0000000..c4f7f2c Binary files /dev/null and b/assets/icons/MFL/MFL-6.png differ diff --git a/assets/icons/MFL/MFL-7.png b/assets/icons/MFL/MFL-7.png new file mode 100644 index 0000000..9f0e1af Binary files /dev/null and b/assets/icons/MFL/MFL-7.png differ diff --git a/assets/icons/MFL/MFL-8.png b/assets/icons/MFL/MFL-8.png new file mode 100644 index 0000000..85a5167 Binary files /dev/null and b/assets/icons/MFL/MFL-8.png differ diff --git a/assets/icons/MFL/MFL-9.png b/assets/icons/MFL/MFL-9.png new file mode 100644 index 0000000..2408d8d Binary files /dev/null and b/assets/icons/MFL/MFL-9.png differ diff --git a/assets/icons/SDG/SDG-1.png b/assets/icons/SDG/SDG-1.png new file mode 100644 index 0000000..1b1f680 Binary files /dev/null and b/assets/icons/SDG/SDG-1.png differ diff --git a/assets/icons/SDG/SDG-10.png b/assets/icons/SDG/SDG-10.png new file mode 100644 index 0000000..4020715 Binary files /dev/null and b/assets/icons/SDG/SDG-10.png differ diff --git a/assets/icons/SDG/SDG-11.png b/assets/icons/SDG/SDG-11.png new file mode 100644 index 0000000..afb9b44 Binary files /dev/null and b/assets/icons/SDG/SDG-11.png differ diff --git a/assets/icons/SDG/SDG-12.png b/assets/icons/SDG/SDG-12.png new file mode 100644 index 0000000..23a3a0d Binary files /dev/null and b/assets/icons/SDG/SDG-12.png differ diff --git a/assets/icons/SDG/SDG-13.png b/assets/icons/SDG/SDG-13.png new file mode 100644 index 0000000..444afa1 Binary files /dev/null and b/assets/icons/SDG/SDG-13.png differ diff --git a/assets/icons/SDG/SDG-14.png b/assets/icons/SDG/SDG-14.png new file mode 100644 index 0000000..659db4e Binary files /dev/null and b/assets/icons/SDG/SDG-14.png differ diff --git a/assets/icons/SDG/SDG-15.png b/assets/icons/SDG/SDG-15.png new file mode 100644 index 0000000..df2f341 Binary files /dev/null and b/assets/icons/SDG/SDG-15.png differ diff --git a/assets/icons/SDG/SDG-16.png b/assets/icons/SDG/SDG-16.png new file mode 100644 index 0000000..de777b0 Binary files /dev/null and b/assets/icons/SDG/SDG-16.png differ diff --git a/assets/icons/SDG/SDG-17.png b/assets/icons/SDG/SDG-17.png new file mode 100644 index 0000000..4672420 Binary files /dev/null and b/assets/icons/SDG/SDG-17.png differ diff --git a/assets/icons/SDG/SDG-2.png b/assets/icons/SDG/SDG-2.png new file mode 100644 index 0000000..af193aa Binary files /dev/null and b/assets/icons/SDG/SDG-2.png differ diff --git a/assets/icons/SDG/SDG-3.png b/assets/icons/SDG/SDG-3.png new file mode 100644 index 0000000..42eb43d Binary files /dev/null and b/assets/icons/SDG/SDG-3.png differ diff --git a/assets/icons/SDG/SDG-4.png b/assets/icons/SDG/SDG-4.png new file mode 100644 index 0000000..2ab8d39 Binary files /dev/null and b/assets/icons/SDG/SDG-4.png differ diff --git a/assets/icons/SDG/SDG-5.png b/assets/icons/SDG/SDG-5.png new file mode 100644 index 0000000..19d1156 Binary files /dev/null and b/assets/icons/SDG/SDG-5.png differ diff --git a/assets/icons/SDG/SDG-6.png b/assets/icons/SDG/SDG-6.png new file mode 100644 index 0000000..40c1c2b Binary files /dev/null and b/assets/icons/SDG/SDG-6.png differ diff --git a/assets/icons/SDG/SDG-7.png b/assets/icons/SDG/SDG-7.png new file mode 100644 index 0000000..2caaa94 Binary files /dev/null and b/assets/icons/SDG/SDG-7.png differ diff --git a/assets/icons/SDG/SDG-8.png b/assets/icons/SDG/SDG-8.png new file mode 100644 index 0000000..e2245d1 Binary files /dev/null and b/assets/icons/SDG/SDG-8.png differ diff --git a/assets/icons/SDG/SDG-9.png b/assets/icons/SDG/SDG-9.png new file mode 100644 index 0000000..0e10540 Binary files /dev/null and b/assets/icons/SDG/SDG-9.png differ diff --git a/assets/icons/ae/AE-1.svg b/assets/icons/ae/AE-1.svg new file mode 100644 index 0000000..600e7ad --- /dev/null +++ b/assets/icons/ae/AE-1.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/assets/icons/ae/AE-10.svg b/assets/icons/ae/AE-10.svg new file mode 100644 index 0000000..1619738 --- /dev/null +++ b/assets/icons/ae/AE-10.svg @@ -0,0 +1,337 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ae/AE-11.svg b/assets/icons/ae/AE-11.svg new file mode 100644 index 0000000..c050d7d --- /dev/null +++ b/assets/icons/ae/AE-11.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ae/AE-12.svg b/assets/icons/ae/AE-12.svg new file mode 100644 index 0000000..7460dd2 --- /dev/null +++ b/assets/icons/ae/AE-12.svg @@ -0,0 +1,261 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ae/AE-13.svg b/assets/icons/ae/AE-13.svg new file mode 100644 index 0000000..9b04306 --- /dev/null +++ b/assets/icons/ae/AE-13.svg @@ -0,0 +1,649 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/icons/ae/AE-2.svg b/assets/icons/ae/AE-2.svg new file mode 100644 index 0000000..eda7ef7 --- /dev/null +++ b/assets/icons/ae/AE-2.svg @@ -0,0 +1,585 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ae/AE-3.svg b/assets/icons/ae/AE-3.svg new file mode 100644 index 0000000..a48051b --- /dev/null +++ b/assets/icons/ae/AE-3.svg @@ -0,0 +1,865 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/icons/ae/AE-4.svg b/assets/icons/ae/AE-4.svg new file mode 100644 index 0000000..44664a0 --- /dev/null +++ b/assets/icons/ae/AE-4.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ae/AE-5.svg b/assets/icons/ae/AE-5.svg new file mode 100644 index 0000000..09ba99c --- /dev/null +++ b/assets/icons/ae/AE-5.svg @@ -0,0 +1,956 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ae/AE-6.svg b/assets/icons/ae/AE-6.svg new file mode 100644 index 0000000..b2fec57 --- /dev/null +++ b/assets/icons/ae/AE-6.svg @@ -0,0 +1,750 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ae/AE-7.svg b/assets/icons/ae/AE-7.svg new file mode 100644 index 0000000..432e646 --- /dev/null +++ b/assets/icons/ae/AE-7.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/assets/icons/ae/AE-8.svg b/assets/icons/ae/AE-8.svg new file mode 100644 index 0000000..db62a8b --- /dev/null +++ b/assets/icons/ae/AE-8.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/assets/icons/ae/AE-9.svg b/assets/icons/ae/AE-9.svg new file mode 100644 index 0000000..4138b56 --- /dev/null +++ b/assets/icons/ae/AE-9.svg @@ -0,0 +1,596 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..917e4ae --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + +fsys CCG — Coopetitive Match + + + +
+ + + diff --git a/layout.png b/layout.png new file mode 100644 index 0000000..52aee76 Binary files /dev/null and b/layout.png differ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3c66f75 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1615 @@ +{ + "name": "fsys-ccg-react", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "fsys-ccg-react", + "version": "1.0.0", + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "typescript": "^5.2.2", + "vite": "^5.0.8" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", + "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001792", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.353", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz", + "integrity": "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d037a3b --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "fsys-ccg-react", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "typescript": "^5.2.2", + "vite": "^5.0.8" + } +} diff --git a/public/assets/card-background.png b/public/assets/card-background.png new file mode 100644 index 0000000..aa8a437 Binary files /dev/null and b/public/assets/card-background.png differ diff --git a/public/assets/cards/1-gift-of-water-action.png b/public/assets/cards/1-gift-of-water-action.png new file mode 100644 index 0000000..2823d96 Binary files /dev/null and b/public/assets/cards/1-gift-of-water-action.png differ diff --git a/public/assets/cards/10-tree-planter.png b/public/assets/cards/10-tree-planter.png new file mode 100644 index 0000000..f6c668c Binary files /dev/null and b/public/assets/cards/10-tree-planter.png differ diff --git a/public/assets/cards/11-seed-scout.png b/public/assets/cards/11-seed-scout.png new file mode 100644 index 0000000..e4efc21 Binary files /dev/null and b/public/assets/cards/11-seed-scout.png differ diff --git a/public/assets/cards/12-natural-spritz.png b/public/assets/cards/12-natural-spritz.png new file mode 100644 index 0000000..2b22fd6 Binary files /dev/null and b/public/assets/cards/12-natural-spritz.png differ diff --git a/public/assets/cards/13-dashboard-designer.png b/public/assets/cards/13-dashboard-designer.png new file mode 100644 index 0000000..288df48 Binary files /dev/null and b/public/assets/cards/13-dashboard-designer.png differ diff --git a/public/assets/cards/14-onion-bodyguard.png b/public/assets/cards/14-onion-bodyguard.png new file mode 100644 index 0000000..0429765 Binary files /dev/null and b/public/assets/cards/14-onion-bodyguard.png differ diff --git a/public/assets/cards/15-local-investor.png b/public/assets/cards/15-local-investor.png new file mode 100644 index 0000000..71cc200 Binary files /dev/null and b/public/assets/cards/15-local-investor.png differ diff --git a/public/assets/cards/16-poster-designer.png b/public/assets/cards/16-poster-designer.png new file mode 100644 index 0000000..da50a72 Binary files /dev/null and b/public/assets/cards/16-poster-designer.png differ diff --git a/public/assets/cards/17-change-maker.png b/public/assets/cards/17-change-maker.png new file mode 100644 index 0000000..1aebb0e Binary files /dev/null and b/public/assets/cards/17-change-maker.png differ diff --git a/public/assets/cards/18-waste-remover.png b/public/assets/cards/18-waste-remover.png new file mode 100644 index 0000000..00b4cf4 Binary files /dev/null and b/public/assets/cards/18-waste-remover.png differ diff --git a/public/assets/cards/19-manure-maker.png b/public/assets/cards/19-manure-maker.png new file mode 100644 index 0000000..51ed796 Binary files /dev/null and b/public/assets/cards/19-manure-maker.png differ diff --git a/public/assets/cards/2-grow-greens-action.png b/public/assets/cards/2-grow-greens-action.png new file mode 100644 index 0000000..d0a0438 Binary files /dev/null and b/public/assets/cards/2-grow-greens-action.png differ diff --git a/public/assets/cards/20-mixed-garden.png b/public/assets/cards/20-mixed-garden.png new file mode 100644 index 0000000..aa28675 Binary files /dev/null and b/public/assets/cards/20-mixed-garden.png differ diff --git a/public/assets/cards/21-ash-bug-blaster.png b/public/assets/cards/21-ash-bug-blaster.png new file mode 100644 index 0000000..9cd8e53 Binary files /dev/null and b/public/assets/cards/21-ash-bug-blaster.png differ diff --git a/public/assets/cards/22-dry-and-store.png b/public/assets/cards/22-dry-and-store.png new file mode 100644 index 0000000..94d9cbb Binary files /dev/null and b/public/assets/cards/22-dry-and-store.png differ diff --git a/public/assets/cards/23-seed-distributor.png b/public/assets/cards/23-seed-distributor.png new file mode 100644 index 0000000..705729a Binary files /dev/null and b/public/assets/cards/23-seed-distributor.png differ diff --git a/public/assets/cards/24-river-cleaners.png b/public/assets/cards/24-river-cleaners.png new file mode 100644 index 0000000..8505c86 Binary files /dev/null and b/public/assets/cards/24-river-cleaners.png differ diff --git a/public/assets/cards/25-garden-documenter.png b/public/assets/cards/25-garden-documenter.png new file mode 100644 index 0000000..b0b2aba Binary files /dev/null and b/public/assets/cards/25-garden-documenter.png differ diff --git a/public/assets/cards/26-local-variety-guardian.png b/public/assets/cards/26-local-variety-guardian.png new file mode 100644 index 0000000..c96c45e Binary files /dev/null and b/public/assets/cards/26-local-variety-guardian.png differ diff --git a/public/assets/cards/27-nature-regenerator.png b/public/assets/cards/27-nature-regenerator.png new file mode 100644 index 0000000..379044b Binary files /dev/null and b/public/assets/cards/27-nature-regenerator.png differ diff --git a/public/assets/cards/28-biodiversity-steward.png b/public/assets/cards/28-biodiversity-steward.png new file mode 100644 index 0000000..f61823e Binary files /dev/null and b/public/assets/cards/28-biodiversity-steward.png differ diff --git a/public/assets/cards/29-land-negotiator.png b/public/assets/cards/29-land-negotiator.png new file mode 100644 index 0000000..1d3e5f7 Binary files /dev/null and b/public/assets/cards/29-land-negotiator.png differ diff --git a/public/assets/cards/3-fine-dining-compost-dish-action.png b/public/assets/cards/3-fine-dining-compost-dish-action.png new file mode 100644 index 0000000..6619c59 Binary files /dev/null and b/public/assets/cards/3-fine-dining-compost-dish-action.png differ diff --git a/public/assets/cards/30-tjd-booster-action.png b/public/assets/cards/30-tjd-booster-action.png new file mode 100644 index 0000000..8e8b661 Binary files /dev/null and b/public/assets/cards/30-tjd-booster-action.png differ diff --git a/public/assets/cards/31-kitchen-garden.png b/public/assets/cards/31-kitchen-garden.png new file mode 100644 index 0000000..0b8a37c Binary files /dev/null and b/public/assets/cards/31-kitchen-garden.png differ diff --git a/public/assets/cards/32-balanced-plate-builder.png b/public/assets/cards/32-balanced-plate-builder.png new file mode 100644 index 0000000..c72b498 Binary files /dev/null and b/public/assets/cards/32-balanced-plate-builder.png differ diff --git a/public/assets/cards/33-indigenous-tree-lover.png b/public/assets/cards/33-indigenous-tree-lover.png new file mode 100644 index 0000000..a869f0c Binary files /dev/null and b/public/assets/cards/33-indigenous-tree-lover.png differ diff --git a/public/assets/cards/34-cover-crop-protector.png b/public/assets/cards/34-cover-crop-protector.png new file mode 100644 index 0000000..7c8b152 Binary files /dev/null and b/public/assets/cards/34-cover-crop-protector.png differ diff --git a/public/assets/cards/35-local-food-producer.png b/public/assets/cards/35-local-food-producer.png new file mode 100644 index 0000000..b8ea254 Binary files /dev/null and b/public/assets/cards/35-local-food-producer.png differ diff --git a/public/assets/cards/36-market-price-checker.png b/public/assets/cards/36-market-price-checker.png new file mode 100644 index 0000000..c1b9916 Binary files /dev/null and b/public/assets/cards/36-market-price-checker.png differ diff --git a/public/assets/cards/37-community-meeting.png b/public/assets/cards/37-community-meeting.png new file mode 100644 index 0000000..b71ad12 Binary files /dev/null and b/public/assets/cards/37-community-meeting.png differ diff --git a/public/assets/cards/38-jar-farm-product-marketer.png b/public/assets/cards/38-jar-farm-product-marketer.png new file mode 100644 index 0000000..a14b406 Binary files /dev/null and b/public/assets/cards/38-jar-farm-product-marketer.png differ diff --git a/public/assets/cards/39-food-donation-hero.png b/public/assets/cards/39-food-donation-hero.png new file mode 100644 index 0000000..b1b5b38 Binary files /dev/null and b/public/assets/cards/39-food-donation-hero.png differ diff --git a/public/assets/cards/4-buzzing-with-wisdom-action.png b/public/assets/cards/4-buzzing-with-wisdom-action.png new file mode 100644 index 0000000..c2746de Binary files /dev/null and b/public/assets/cards/4-buzzing-with-wisdom-action.png differ diff --git a/public/assets/cards/40-windbreak-warrior.png b/public/assets/cards/40-windbreak-warrior.png new file mode 100644 index 0000000..f524f47 Binary files /dev/null and b/public/assets/cards/40-windbreak-warrior.png differ diff --git a/public/assets/cards/41-seed-champion.png b/public/assets/cards/41-seed-champion.png new file mode 100644 index 0000000..48d565b Binary files /dev/null and b/public/assets/cards/41-seed-champion.png differ diff --git a/public/assets/cards/42-fruit-and-tree-team.png b/public/assets/cards/42-fruit-and-tree-team.png new file mode 100644 index 0000000..640f5a5 Binary files /dev/null and b/public/assets/cards/42-fruit-and-tree-team.png differ diff --git a/public/assets/cards/43-manure-cycle-drawer.png b/public/assets/cards/43-manure-cycle-drawer.png new file mode 100644 index 0000000..ee7faa4 Binary files /dev/null and b/public/assets/cards/43-manure-cycle-drawer.png differ diff --git a/public/assets/cards/44-animal-hero.png b/public/assets/cards/44-animal-hero.png new file mode 100644 index 0000000..77af5a0 Binary files /dev/null and b/public/assets/cards/44-animal-hero.png differ diff --git a/public/assets/cards/45-land-magic.png b/public/assets/cards/45-land-magic.png new file mode 100644 index 0000000..c6ff021 Binary files /dev/null and b/public/assets/cards/45-land-magic.png differ diff --git a/public/assets/cards/46-local-chicken-lover.png b/public/assets/cards/46-local-chicken-lover.png new file mode 100644 index 0000000..c6182a5 Binary files /dev/null and b/public/assets/cards/46-local-chicken-lover.png differ diff --git a/public/assets/cards/47-food-dryer-explorer.png b/public/assets/cards/47-food-dryer-explorer.png new file mode 100644 index 0000000..7caebc2 Binary files /dev/null and b/public/assets/cards/47-food-dryer-explorer.png differ diff --git a/public/assets/cards/48-scrap-tastic-animal-feeder.png b/public/assets/cards/48-scrap-tastic-animal-feeder.png new file mode 100644 index 0000000..cc10081 Binary files /dev/null and b/public/assets/cards/48-scrap-tastic-animal-feeder.png differ diff --git a/public/assets/cards/49-leftover-chef.png b/public/assets/cards/49-leftover-chef.png new file mode 100644 index 0000000..8a41e9d Binary files /dev/null and b/public/assets/cards/49-leftover-chef.png differ diff --git a/public/assets/cards/5-baking-flatbread.png b/public/assets/cards/5-baking-flatbread.png new file mode 100644 index 0000000..274e2c4 Binary files /dev/null and b/public/assets/cards/5-baking-flatbread.png differ diff --git a/public/assets/cards/50-weather-tracker-checker.png b/public/assets/cards/50-weather-tracker-checker.png new file mode 100644 index 0000000..05c5568 Binary files /dev/null and b/public/assets/cards/50-weather-tracker-checker.png differ diff --git a/public/assets/cards/51-farm-record-keeper.png b/public/assets/cards/51-farm-record-keeper.png new file mode 100644 index 0000000..10ccd13 Binary files /dev/null and b/public/assets/cards/51-farm-record-keeper.png differ diff --git a/public/assets/cards/52-the-water-fertilizers.png b/public/assets/cards/52-the-water-fertilizers.png new file mode 100644 index 0000000..589e698 Binary files /dev/null and b/public/assets/cards/52-the-water-fertilizers.png differ diff --git a/public/assets/cards/53-small-farm-big-voice.png b/public/assets/cards/53-small-farm-big-voice.png new file mode 100644 index 0000000..7e7e193 Binary files /dev/null and b/public/assets/cards/53-small-farm-big-voice.png differ diff --git a/public/assets/cards/54-nature-educator.png b/public/assets/cards/54-nature-educator.png new file mode 100644 index 0000000..d7780e2 Binary files /dev/null and b/public/assets/cards/54-nature-educator.png differ diff --git a/public/assets/cards/55-teamwork-harvest.png b/public/assets/cards/55-teamwork-harvest.png new file mode 100644 index 0000000..e48331c Binary files /dev/null and b/public/assets/cards/55-teamwork-harvest.png differ diff --git a/public/assets/cards/56-sky-garden.png b/public/assets/cards/56-sky-garden.png new file mode 100644 index 0000000..2b11e14 Binary files /dev/null and b/public/assets/cards/56-sky-garden.png differ diff --git a/public/assets/cards/57-be-the-change-action.png b/public/assets/cards/57-be-the-change-action.png new file mode 100644 index 0000000..8f1b57d Binary files /dev/null and b/public/assets/cards/57-be-the-change-action.png differ diff --git a/public/assets/cards/58-ancient-wisdom-defender-action.png b/public/assets/cards/58-ancient-wisdom-defender-action.png new file mode 100644 index 0000000..6a98efd Binary files /dev/null and b/public/assets/cards/58-ancient-wisdom-defender-action.png differ diff --git a/public/assets/cards/59-seedling-protector-action.png b/public/assets/cards/59-seedling-protector-action.png new file mode 100644 index 0000000..2bd3985 Binary files /dev/null and b/public/assets/cards/59-seedling-protector-action.png differ diff --git a/public/assets/cards/6-mixed-wastemonster-action.png b/public/assets/cards/6-mixed-wastemonster-action.png new file mode 100644 index 0000000..e2667ce Binary files /dev/null and b/public/assets/cards/6-mixed-wastemonster-action.png differ diff --git a/public/assets/cards/60-cooperative-builder-action.png b/public/assets/cards/60-cooperative-builder-action.png new file mode 100644 index 0000000..73ea174 Binary files /dev/null and b/public/assets/cards/60-cooperative-builder-action.png differ diff --git a/public/assets/cards/7-ingredient-detective-action.png b/public/assets/cards/7-ingredient-detective-action.png new file mode 100644 index 0000000..53513a5 Binary files /dev/null and b/public/assets/cards/7-ingredient-detective-action.png differ diff --git a/public/assets/cards/8-mindful-eating-action.png b/public/assets/cards/8-mindful-eating-action.png new file mode 100644 index 0000000..803f61e Binary files /dev/null and b/public/assets/cards/8-mindful-eating-action.png differ diff --git a/public/assets/cards/9-fermentastic-action.png b/public/assets/cards/9-fermentastic-action.png new file mode 100644 index 0000000..eafea24 Binary files /dev/null and b/public/assets/cards/9-fermentastic-action.png differ diff --git a/public/assets/cgiar.png b/public/assets/cgiar.png new file mode 100644 index 0000000..8bafd59 Binary files /dev/null and b/public/assets/cgiar.png differ diff --git a/public/assets/diy.jpg b/public/assets/diy.jpg new file mode 100755 index 0000000..0930d1f Binary files /dev/null and b/public/assets/diy.jpg differ diff --git a/public/assets/fsys.png b/public/assets/fsys.png new file mode 100644 index 0000000..916b09c Binary files /dev/null and b/public/assets/fsys.png differ diff --git a/public/assets/game-background.png b/public/assets/game-background.png new file mode 100644 index 0000000..ee08237 Binary files /dev/null and b/public/assets/game-background.png differ diff --git a/public/assets/icons/MFL/MFL-1.png b/public/assets/icons/MFL/MFL-1.png new file mode 100644 index 0000000..5b3031b Binary files /dev/null and b/public/assets/icons/MFL/MFL-1.png differ diff --git a/public/assets/icons/MFL/MFL-10.png b/public/assets/icons/MFL/MFL-10.png new file mode 100644 index 0000000..ad6e3f7 Binary files /dev/null and b/public/assets/icons/MFL/MFL-10.png differ diff --git a/public/assets/icons/MFL/MFL-11.png b/public/assets/icons/MFL/MFL-11.png new file mode 100644 index 0000000..4179c1b Binary files /dev/null and b/public/assets/icons/MFL/MFL-11.png differ diff --git a/public/assets/icons/MFL/MFL-12.png b/public/assets/icons/MFL/MFL-12.png new file mode 100644 index 0000000..9a1c1e1 Binary files /dev/null and b/public/assets/icons/MFL/MFL-12.png differ diff --git a/public/assets/icons/MFL/MFL-2.png b/public/assets/icons/MFL/MFL-2.png new file mode 100644 index 0000000..b24c6b5 Binary files /dev/null and b/public/assets/icons/MFL/MFL-2.png differ diff --git a/public/assets/icons/MFL/MFL-3.png b/public/assets/icons/MFL/MFL-3.png new file mode 100644 index 0000000..dabbb62 Binary files /dev/null and b/public/assets/icons/MFL/MFL-3.png differ diff --git a/public/assets/icons/MFL/MFL-4.png b/public/assets/icons/MFL/MFL-4.png new file mode 100644 index 0000000..689bec6 Binary files /dev/null and b/public/assets/icons/MFL/MFL-4.png differ diff --git a/public/assets/icons/MFL/MFL-5.png b/public/assets/icons/MFL/MFL-5.png new file mode 100644 index 0000000..e2fdc8a Binary files /dev/null and b/public/assets/icons/MFL/MFL-5.png differ diff --git a/public/assets/icons/MFL/MFL-6.png b/public/assets/icons/MFL/MFL-6.png new file mode 100644 index 0000000..c4f7f2c Binary files /dev/null and b/public/assets/icons/MFL/MFL-6.png differ diff --git a/public/assets/icons/MFL/MFL-7.png b/public/assets/icons/MFL/MFL-7.png new file mode 100644 index 0000000..9f0e1af Binary files /dev/null and b/public/assets/icons/MFL/MFL-7.png differ diff --git a/public/assets/icons/MFL/MFL-8.png b/public/assets/icons/MFL/MFL-8.png new file mode 100644 index 0000000..85a5167 Binary files /dev/null and b/public/assets/icons/MFL/MFL-8.png differ diff --git a/public/assets/icons/MFL/MFL-9.png b/public/assets/icons/MFL/MFL-9.png new file mode 100644 index 0000000..2408d8d Binary files /dev/null and b/public/assets/icons/MFL/MFL-9.png differ diff --git a/public/assets/icons/SDG/SDG-1.png b/public/assets/icons/SDG/SDG-1.png new file mode 100644 index 0000000..1b1f680 Binary files /dev/null and b/public/assets/icons/SDG/SDG-1.png differ diff --git a/public/assets/icons/SDG/SDG-10.png b/public/assets/icons/SDG/SDG-10.png new file mode 100644 index 0000000..4020715 Binary files /dev/null and b/public/assets/icons/SDG/SDG-10.png differ diff --git a/public/assets/icons/SDG/SDG-11.png b/public/assets/icons/SDG/SDG-11.png new file mode 100644 index 0000000..afb9b44 Binary files /dev/null and b/public/assets/icons/SDG/SDG-11.png differ diff --git a/public/assets/icons/SDG/SDG-12.png b/public/assets/icons/SDG/SDG-12.png new file mode 100644 index 0000000..23a3a0d Binary files /dev/null and b/public/assets/icons/SDG/SDG-12.png differ diff --git a/public/assets/icons/SDG/SDG-13.png b/public/assets/icons/SDG/SDG-13.png new file mode 100644 index 0000000..444afa1 Binary files /dev/null and b/public/assets/icons/SDG/SDG-13.png differ diff --git a/public/assets/icons/SDG/SDG-14.png b/public/assets/icons/SDG/SDG-14.png new file mode 100644 index 0000000..659db4e Binary files /dev/null and b/public/assets/icons/SDG/SDG-14.png differ diff --git a/public/assets/icons/SDG/SDG-15.png b/public/assets/icons/SDG/SDG-15.png new file mode 100644 index 0000000..df2f341 Binary files /dev/null and b/public/assets/icons/SDG/SDG-15.png differ diff --git a/public/assets/icons/SDG/SDG-16.png b/public/assets/icons/SDG/SDG-16.png new file mode 100644 index 0000000..de777b0 Binary files /dev/null and b/public/assets/icons/SDG/SDG-16.png differ diff --git a/public/assets/icons/SDG/SDG-17.png b/public/assets/icons/SDG/SDG-17.png new file mode 100644 index 0000000..4672420 Binary files /dev/null and b/public/assets/icons/SDG/SDG-17.png differ diff --git a/public/assets/icons/SDG/SDG-2.png b/public/assets/icons/SDG/SDG-2.png new file mode 100644 index 0000000..af193aa Binary files /dev/null and b/public/assets/icons/SDG/SDG-2.png differ diff --git a/public/assets/icons/SDG/SDG-3.png b/public/assets/icons/SDG/SDG-3.png new file mode 100644 index 0000000..42eb43d Binary files /dev/null and b/public/assets/icons/SDG/SDG-3.png differ diff --git a/public/assets/icons/SDG/SDG-4.png b/public/assets/icons/SDG/SDG-4.png new file mode 100644 index 0000000..2ab8d39 Binary files /dev/null and b/public/assets/icons/SDG/SDG-4.png differ diff --git a/public/assets/icons/SDG/SDG-5.png b/public/assets/icons/SDG/SDG-5.png new file mode 100644 index 0000000..19d1156 Binary files /dev/null and b/public/assets/icons/SDG/SDG-5.png differ diff --git a/public/assets/icons/SDG/SDG-6.png b/public/assets/icons/SDG/SDG-6.png new file mode 100644 index 0000000..40c1c2b Binary files /dev/null and b/public/assets/icons/SDG/SDG-6.png differ diff --git a/public/assets/icons/SDG/SDG-7.png b/public/assets/icons/SDG/SDG-7.png new file mode 100644 index 0000000..2caaa94 Binary files /dev/null and b/public/assets/icons/SDG/SDG-7.png differ diff --git a/public/assets/icons/SDG/SDG-8.png b/public/assets/icons/SDG/SDG-8.png new file mode 100644 index 0000000..e2245d1 Binary files /dev/null and b/public/assets/icons/SDG/SDG-8.png differ diff --git a/public/assets/icons/SDG/SDG-9.png b/public/assets/icons/SDG/SDG-9.png new file mode 100644 index 0000000..0e10540 Binary files /dev/null and b/public/assets/icons/SDG/SDG-9.png differ diff --git a/public/assets/icons/ae/AE-1.svg b/public/assets/icons/ae/AE-1.svg new file mode 100644 index 0000000..600e7ad --- /dev/null +++ b/public/assets/icons/ae/AE-1.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/public/assets/icons/ae/AE-10.svg b/public/assets/icons/ae/AE-10.svg new file mode 100644 index 0000000..1619738 --- /dev/null +++ b/public/assets/icons/ae/AE-10.svg @@ -0,0 +1,337 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/icons/ae/AE-11.svg b/public/assets/icons/ae/AE-11.svg new file mode 100644 index 0000000..c050d7d --- /dev/null +++ b/public/assets/icons/ae/AE-11.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/icons/ae/AE-12.svg b/public/assets/icons/ae/AE-12.svg new file mode 100644 index 0000000..7460dd2 --- /dev/null +++ b/public/assets/icons/ae/AE-12.svg @@ -0,0 +1,261 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/icons/ae/AE-13.svg b/public/assets/icons/ae/AE-13.svg new file mode 100644 index 0000000..9b04306 --- /dev/null +++ b/public/assets/icons/ae/AE-13.svg @@ -0,0 +1,649 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/assets/icons/ae/AE-2.svg b/public/assets/icons/ae/AE-2.svg new file mode 100644 index 0000000..eda7ef7 --- /dev/null +++ b/public/assets/icons/ae/AE-2.svg @@ -0,0 +1,585 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/icons/ae/AE-3.svg b/public/assets/icons/ae/AE-3.svg new file mode 100644 index 0000000..a48051b --- /dev/null +++ b/public/assets/icons/ae/AE-3.svg @@ -0,0 +1,865 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/assets/icons/ae/AE-4.svg b/public/assets/icons/ae/AE-4.svg new file mode 100644 index 0000000..44664a0 --- /dev/null +++ b/public/assets/icons/ae/AE-4.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/icons/ae/AE-5.svg b/public/assets/icons/ae/AE-5.svg new file mode 100644 index 0000000..09ba99c --- /dev/null +++ b/public/assets/icons/ae/AE-5.svg @@ -0,0 +1,956 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/icons/ae/AE-6.svg b/public/assets/icons/ae/AE-6.svg new file mode 100644 index 0000000..b2fec57 --- /dev/null +++ b/public/assets/icons/ae/AE-6.svg @@ -0,0 +1,750 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/icons/ae/AE-7.svg b/public/assets/icons/ae/AE-7.svg new file mode 100644 index 0000000..432e646 --- /dev/null +++ b/public/assets/icons/ae/AE-7.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/public/assets/icons/ae/AE-8.svg b/public/assets/icons/ae/AE-8.svg new file mode 100644 index 0000000..db62a8b --- /dev/null +++ b/public/assets/icons/ae/AE-8.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/public/assets/icons/ae/AE-9.svg b/public/assets/icons/ae/AE-9.svg new file mode 100644 index 0000000..4138b56 --- /dev/null +++ b/public/assets/icons/ae/AE-9.svg @@ -0,0 +1,596 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..1e4e97e --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,13 @@ +import { useGameState } from './hooks/useGameState'; +import StartScreen from './components/StartScreen'; +import GameScreen from './components/GameScreen'; + +export default function App() { + const game = useGameState(); + + if (game.phase === 'menu') { + return game.startGame(1, mode)} />; + } + + return ; +} diff --git a/src/components/AIPlayers.tsx b/src/components/AIPlayers.tsx new file mode 100644 index 0000000..eb4ff45 --- /dev/null +++ b/src/components/AIPlayers.tsx @@ -0,0 +1,30 @@ +import type { Player } from '../types'; + +interface Props { + players: Player[]; +} + +export default function AIPlayers({ players }: Props) { + return ( +
+ {players.map((p, i) => { + const isWinner = p.matchInfo && p.matchInfo.matched === p.matchInfo.total && p.matchInfo.total > 0; + return ( +
+
{p.name}
+
Hand: {p.hand.length} | Deck: N/A
+
Projects: {p.projectZone.length}/3
+ {p.matchInfo && ( +
+ Match: {p.matchInfo.matched}/{p.matchInfo.total} +
+ )} +
+ {p.backupCard ? '[Backup: hidden]' : '[No backup]'} +
+
+ ); + })} +
+ ); +} diff --git a/src/components/ActionBar.tsx b/src/components/ActionBar.tsx new file mode 100644 index 0000000..bda3a84 --- /dev/null +++ b/src/components/ActionBar.tsx @@ -0,0 +1,40 @@ +import type { GameAPI } from '../hooks/useGameState'; + +interface Props { + game: GameAPI; +} + +export default function ActionBar({ game }: Props) { + const human = game.players[0]; + const canPlay = game.selectedCard !== null && + game.selectedCard < human.hand.length && + !human.hasActed && + game.phase === 'play_turn'; + + const canEndTurn = human.hasActed && game.phase === 'end_turn_prompt'; + + return ( +
+ {game.phase === 'replace' && ( + <> + + + + )} + {(game.phase === 'play_turn' || game.phase === 'end_turn_prompt') && ( + <> + + + + )} +
+ ); +} diff --git a/src/components/BackupChoicePanel.tsx b/src/components/BackupChoicePanel.tsx new file mode 100644 index 0000000..a8ad42f --- /dev/null +++ b/src/components/BackupChoicePanel.tsx @@ -0,0 +1,46 @@ +import type { GameAPI } from '../hooks/useGameState'; +import type { Player } from '../types'; +import { getCardImage } from '../data/cards'; + +interface Props { + game: GameAPI; + player: Player; +} + +export default function BackupChoicePanel({ game, player }: Props) { + const backup = player.backupCard; + if (!backup) return null; + + const slots = player.projectZone; + + return ( +
+
Backup Project Card Available
+

+ You can replace one of your project cards with your backup card to improve your score. +

+
+
+
+
+ {backup.name} +
+
+
{backup.name}
+
+

Replace which slot?

+
+ {slots.map((c, i) => ( + + ))} +
+
+ +
+
+ ); +} diff --git a/src/components/ChallengeRevealOverlay.tsx b/src/components/ChallengeRevealOverlay.tsx new file mode 100644 index 0000000..6e8bcc2 --- /dev/null +++ b/src/components/ChallengeRevealOverlay.tsx @@ -0,0 +1,27 @@ +interface Props { + step: string | null; +} + +export default function ChallengeRevealOverlay({ step }: Props) { + if (!step) return null; + + const isNumber = step === '3' || step === '2' || step === '1'; + + return ( +
+
+ {step === 'intro' && ( +
+ The Challenge is... + ... +
+ )} + {isNumber && ( +
+ {step} +
+ )} +
+
+ ); +} diff --git a/src/components/ChallengeZone.tsx b/src/components/ChallengeZone.tsx new file mode 100644 index 0000000..8adada9 --- /dev/null +++ b/src/components/ChallengeZone.tsx @@ -0,0 +1,84 @@ +import type { Card, GamePhase } from '../types'; +import { iconPath, AE_LABELS, MFL_LABELS, SDG_LABELS } from '../data/labels'; +import { getCardImage } from '../data/cards'; + +interface Props { + challenge: Card | null; + phase: GamePhase; + animatingCardId: number | null; +} + +function iconLabel(type: 'ae' | 'mfl' | 'sdg', n: number): string { + if (type === 'ae') return AE_LABELS[n - 1] || `AE${n}`; + if (type === 'mfl') return MFL_LABELS[n - 1] || `MFL${n}`; + return SDG_LABELS[n - 1] || `SDG${n}`; +} + +export default function ChallengeZone({ challenge, phase, animatingCardId }: Props) { + if (!challenge) { + return ( +
+
Global Challenge
+
+
+
+
+
+
+ ); + } + + const revealed = phase !== 'menu' && phase !== 'setup'; + + return ( +
+
Global Challenge
+
+
+ {revealed ? ( + {challenge.name} + ) : ( +
+ )} +
+
+
+ {revealed && ( + <> + {challenge.ae.map(v => ( + {`AE${v}`} + ))} + {challenge.mfl.map(v => ( + {`MFL${v}`} + ))} + {challenge.sdgs.map(v => ( + {`SDG${v}`} + ))} + + )} +
+ {revealed && ( +
+ This Challenge is about: +
+ {challenge.ae.map(v => ( + {iconLabel('ae', v)} + ))} + {challenge.mfl.map(v => ( + {iconLabel('mfl', v)} + ))} + {challenge.sdgs.map(v => ( + {iconLabel('sdg', v)} + ))} +
+
+ )} +
+
+
+ ); +} diff --git a/src/components/EndScreen.tsx b/src/components/EndScreen.tsx new file mode 100644 index 0000000..6023e11 --- /dev/null +++ b/src/components/EndScreen.tsx @@ -0,0 +1,126 @@ +import { useState, useEffect } from 'react'; +import type { Player, MatchRecord, MatchMode } from '../types'; + +interface Props { + players: Player[]; + collaborativeWin: boolean; + matchMode: MatchMode; + matchIndex: number; + matchHistory: MatchRecord[]; + seriesWinner: number | null; + onBack: () => void; + onNextMatch: () => void; +} + +export default function EndScreen({ players, collaborativeWin, matchMode, matchIndex, matchHistory, seriesWinner, onBack, onNextMatch }: Props) { + const [celebrate, setCelebrate] = useState(true); + const isMulti = matchMode !== 'single'; + const sorted = [...players].sort((a, b) => b.points - a.points); + const maxPoints = sorted[0]?.points ?? 0; + + useEffect(() => { + if (!collaborativeWin) return; + const t = setTimeout(() => setCelebrate(false), 4000); + return () => clearTimeout(t); + }, [collaborativeWin]); + + const cumulativeScores = isMulti ? players.map((_, pi) => + matchHistory.reduce((sum, m) => sum + (m.scores[pi] || 0), 0) + ) : []; + + const seriesOver = seriesWinner !== null || (matchMode === 'bo2' && matchHistory.length >= 2) || (matchMode === 'bo3' && matchHistory.length >= 3); + + return ( +
+ {collaborativeWin && celebrate && ( +
+
+ {Array.from({ length: 40 }).map((_, i) => ( +
+ ))} +
+
+

Collaboration!

+

+ Congratulations — you collaborated and put your projects together to solve the challenge! +

+
+
+ )} +

+ {seriesOver ? 'Series Over' : isMulti ? `Match ${matchIndex + 1} Complete` : 'Game Over'} +

+ + {isMulti && matchHistory.length > 0 && ( +
+
Score Tracker
+
+
+ Match + {players.map((p, i) => ( + {p.name} + ))} +
+ {matchHistory.map((m, mi) => ( +
+ #{mi + 1} + {m.scores.map((s, si) => ( + 0 ? 'tracking-points' : ''}`}>{s} + ))} +
+ ))} +
+ Total + {cumulativeScores.map((s, i) => ( + 0 ? 'tracking-points' : ''}`}>{s} + ))} +
+
+
+ )} + + {seriesOver && ( +
+ {seriesWinner !== null ? `${players[seriesWinner].name} wins the series!` : 'The series ended in a draw.'} +
+ )} + +
+ {sorted.map((p, i) => { + const isWinner = p.points === maxPoints && p.points > 0; + const mi = p.matchInfo || { matched: 0, total: 0 }; + const details = mi.matched === mi.total && mi.total > 0 + ? `Full match! ${mi.matched}/${mi.total} icons` + : `${mi.matched}/${mi.total} icons matched`; + + return ( +
+
{i === 0 && maxPoints > 0 ? '🏆' : ''}
+
{p.name}
+
{p.points} point{p.points !== 1 ? 's' : ''}
+
{details}
+
+ Cards: {p.projectZone.map(c => c.name).join(', ')} +
+
+ ); + })} +
+
+ {isMulti && !seriesOver && ( + + )} + +
+
+ ); +} diff --git a/src/components/GameScreen.tsx b/src/components/GameScreen.tsx new file mode 100644 index 0000000..ce72d43 --- /dev/null +++ b/src/components/GameScreen.tsx @@ -0,0 +1,96 @@ +import type { GameAPI } from '../hooks/useGameState'; +import ChallengeZone from './ChallengeZone'; +import ProjectZone from './ProjectZone'; +import AIPlayers from './AIPlayers'; +import HandZone from './HandZone'; +import ActionBar from './ActionBar'; +import TurnBanner from './TurnBanner'; +import ChallengeRevealOverlay from './ChallengeRevealOverlay'; +import EndScreen from './EndScreen'; +import SuddenSolvePanel from './SuddenSolvePanel'; +import BackupChoicePanel from './BackupChoicePanel'; +import HowToPlay from './HowToPlay'; + +interface Props { + game: GameAPI; +} + +export default function GameScreen({ game }: Props) { + if (game.phase === 'done' && game.showResults) { + return ( + + ); + } + + return ( +
+ + + +
+
+ + {game.round >= 1 ? `Year ${game.round} / 3` : 'Setup'} + + Deck: {game.deck.length} +
+
+ + + +
+ + +
+ +
+ + + +
Your Hand
+ {game.phase === 'sudden_solve' ? ( + + ) : game.phase === 'backup_choice' ? ( + + ) : ( + + )} +
+ + + +
+ {game.statusMessage} +
+
+ ); +} diff --git a/src/components/HandZone.tsx b/src/components/HandZone.tsx new file mode 100644 index 0000000..ad90234 --- /dev/null +++ b/src/components/HandZone.tsx @@ -0,0 +1,64 @@ +import { useState, useRef, useEffect } from 'react'; +import type { Card, GamePhase } from '../types'; +import { getCardImage } from '../data/cards'; + +interface Props { + cards: Card[]; + selectedCard: number | null; + hasActed: boolean; + phase: GamePhase; + onSelect: (index: number | null) => void; +} + +export default function HandZone({ cards, selectedCard, hasActed, phase, onSelect }: Props) { + const [previewCard, setPreviewCard] = useState(null); + const [tapPreview, setTapPreview] = useState(null); + const isCoarse = useRef(false); + + useEffect(() => { + isCoarse.current = window.matchMedia('(pointer: coarse)').matches; + }, []); + + const canSelect = (phase === 'play_turn' && !hasActed) || phase === 'sudden_solve'; + const isDimmed = hasActed && phase !== 'replace'; + + if (phase === 'done' || phase === 'scoring') { + return Game over — see results; + } + + return ( + <> +
+ {cards.map((card, i) => ( +
{ + if (!canSelect) return; + onSelect(selectedCard === i ? null : i); + }} + onMouseEnter={() => setPreviewCard(card)} + onMouseLeave={() => setPreviewCard(null)} + onTouchStart={() => { + if (isCoarse.current) setTapPreview(i); + }} + onTouchEnd={() => { + if (isCoarse.current) { + setTimeout(() => setTapPreview(null), 800); + } + }} + > + {card.name} + {selectedCard === i &&
} +
+ ))} +
+ {(previewCard || (tapPreview !== null && cards[tapPreview])) && ( +
+ {(tapPreview !== null ? cards[tapPreview] : previewCard)!.name} + {(tapPreview !== null ? cards[tapPreview] : previewCard)!.action_text} +
+ )} + + ); +} diff --git a/src/components/HowToPlay.tsx b/src/components/HowToPlay.tsx new file mode 100644 index 0000000..efb2821 --- /dev/null +++ b/src/components/HowToPlay.tsx @@ -0,0 +1,33 @@ +import { useState } from 'react'; +import type { GamePhase } from '../types'; + +interface Props { + phase: GamePhase; +} + +const GUIDES: Record = { + replace: "Look at the Challenge icons on the top. If just a few or no icons in your hand cards match the challenge icons, you can draw 5 new cards. Click Replace. If you are happy with your hand click Keep hand.", + play_turn: "Each year, you play out 1 project card. Try to match as many icons with the challenge icons as possible. Choose a card wisely, then click Play Card, then End turn (this is the same for all 3 years)", + end_turn_prompt: "Each year, you play out 1 project card. Try to match as many icons with the challenge icons as possible. Choose a card wisely, then click Play Card, then End turn (this is the same for all 3 years)", +}; + +export default function HowToPlay({ phase }: Props) { + const [open, setOpen] = useState(false); + + if (phase === 'menu' || phase === 'setup' || phase === 'done' || phase === 'scoring') return null; + + const text = GUIDES[phase] || GUIDES.play_turn; + + return ( +
+ + {open && ( +
+

{text}

+
+ )} +
+ ); +} diff --git a/src/components/ProjectZone.tsx b/src/components/ProjectZone.tsx new file mode 100644 index 0000000..d1c20ac --- /dev/null +++ b/src/components/ProjectZone.tsx @@ -0,0 +1,56 @@ +import type { Player, GamePhase } from '../types'; +import { getCardImage } from '../data/cards'; + +interface Props { + player: Player; + phase: GamePhase; + animatingCardId: number | null; +} + +export default function ProjectZone({ player, phase, animatingCardId }: Props) { + const revealed = phase === 'sudden_solve' || phase === 'scoring' || phase === 'done' || phase === 'backup_choice'; + const hideFaces = !revealed; + + return ( +
+
Your Projects
+
+ {[0, 1, 2].map(i => ( +
c.id === animatingCardId && player.projectZone.indexOf(c) === i) ? 'animating-in' : ''}`} + > + {i < player.projectZone.length ? ( +
+
+
+
+
+ {player.projectZone[i].name} +
+
+ ) : ( + Project {i + 1} + )} +
+ ))} +
+
+ {player.backupCard ? ( +
+
+
+
+
+ {player.backupCard.name} +
+
+ ) : ( + backup + )} +
+
+
+ ); +} diff --git a/src/components/StartScreen.tsx b/src/components/StartScreen.tsx new file mode 100644 index 0000000..4525e4f --- /dev/null +++ b/src/components/StartScreen.tsx @@ -0,0 +1,55 @@ +import { useState } from 'react'; +import type { MatchMode } from '../types'; + +interface Props { + onStart: (mode: MatchMode) => void; +} + +const MODES: { key: MatchMode; label: string }[] = [ + { key: 'single', label: '1 Match' }, + { key: 'bo2', label: 'Best of 2' }, + { key: 'bo3', label: 'Best of 3' }, +]; + +export default function StartScreen({ onStart }: Props) { + const [mode, setMode] = useState('single'); + + return ( +
+
+ fsys +

Collectible Card Game

+

Coopetitive Match — You vs AI

+

+ You are
an international Organization.
+ You have 3 years and 3 projects to solve a global challenge. +

+
+
+ Series: +
+ {MODES.map(m => ( + + ))} +
+
+ +
+
+
+

Want to print and play real cards?

+ + DIY card kit + +
+
+ ); +} diff --git a/src/components/SuddenSolvePanel.tsx b/src/components/SuddenSolvePanel.tsx new file mode 100644 index 0000000..9dc3ec8 --- /dev/null +++ b/src/components/SuddenSolvePanel.tsx @@ -0,0 +1,108 @@ +import { useState } from 'react'; +import type { GameAPI } from '../hooks/useGameState'; +import type { Player } from '../types'; +import { getCardImage } from '../data/cards'; + +interface Props { + game: GameAPI; + player: Player; +} + +export default function SuddenSolvePanel({ game, player }: Props) { + const [step, setStep] = useState<'choose' | 'backup' | 'backup-slot' | 'done'>('choose'); + + const handlePlayCard = (index: number) => { + game.suddenSolvePlayCard(index); + if (player.backupCard) { + setStep('backup'); + } else { + setStep('done'); + } + }; + + const handleSkipPlay = () => { + if (player.backupCard) { + setStep('backup'); + } else { + game.suddenSolveSkipBackup(); + setStep('done'); + } + }; + + if (game.animatingCardId) { + return
Playing card...
; + } + + const noCardsInHand = player.hand.length === 0; + + if (step === 'choose' && !noCardsInHand) { + return ( +
+
⚡ Sudden Solve! Final Chance ⚡
+
Select a card to play:
+
+ {player.hand.map((card, i) => ( +
handlePlayCard(i)} + > + {card.name} +
+ ))} +
+ +
+ ); + } + + const showBackup = player.backupCard && (step === 'backup' || (noCardsInHand && step === 'choose')); + + if (showBackup) { + return ( +
+
⚡ Sudden Solve! Final Chance ⚡
+
+

Use your Backup Project Card?

+
+ + +
+
+
+ ); + } + + if (step === 'backup-slot' && player.backupCard) { + if (player.projectZone.length === 0) { + game.suddenSolveUseBackup(-1); + return
Adding backup...
; + } + return ( +
+
⚡ Sudden Solve! Final Chance ⚡
+
+

Replace which project slot?

+
+ {player.projectZone.map((c, i) => ( + + ))} + +
+
+
+ ); + } + + return
Processing...
; +} diff --git a/src/components/TurnBanner.tsx b/src/components/TurnBanner.tsx new file mode 100644 index 0000000..aadc494 --- /dev/null +++ b/src/components/TurnBanner.tsx @@ -0,0 +1,15 @@ +interface Props { + message: string; +} + +export default function TurnBanner({ message }: Props) { + if (!message) return null; + + return ( +
+
+ {message} +
+
+ ); +} diff --git a/src/data/cards.ts b/src/data/cards.ts new file mode 100644 index 0000000..6a1e684 --- /dev/null +++ b/src/data/cards.ts @@ -0,0 +1,93 @@ +import type { Card } from '../types'; + +export const CARDS: Card[] = [ + {id:1,name:"Gift of Water",action_text:"Make someone happy and bring fresh them water from the well, tank, or tap today.",description:"Sharing water reminds us that clean water is a treasure that everyone in our community should have.",ae:[9],mfl:[5],sdgs:[3,6]}, + {id:2,name:"Grow Greens",action_text:"Tasty sprouts & herbs. Get seeds, plant in soil, water, care. Grow and snip fresh greens.",description:"Growing your own food is a superpower that makes you more independent and gives you the freshest snacks.",ae:[1,9],mfl:[2,3],sdgs:[2,3]}, + {id:3,name:"Fine Dining Compost Dish",action_text:"Style food scraps for composting like a fine dining dish and offer them to Mother Earth.",description:"When we compost, we are showing respect to 'Mother Earth' by giving back the energy we didn't use.",ae:[1,2],mfl:[1,9],sdgs:[12,13]}, + {id:4,name:"Buzzing with Wisdom",action_text:"Learn about bees – their lives, their rhythms, and their ancient connection to plants and ecosystems. Memorize one fact and share it with others.",description:"Understanding bees is important because they are the 'connectors' that help plants grow and keep our world alive.",ae:[4,5],mfl:[5,7],sdgs:[13,15]}, + {id:5,name:"Baking Traditional Bread",action_text:"Traditional bread or flatbread is full of life force and taste. Try using flour of indigenous or other nutritious crops to bake your favorite bread this week.",description:"Using local crops keeps our traditions alive and fills our bodies with the 'life force' of our own land.",ae:[8,9],mfl:[3,6],sdgs:[2,3]}, + {id:6,name:"Mixed Waste-monster",action_text:"Food waste all mixed together is useless. Identify the different waste types, sort them, and find ways to reduce & recycle this week.",description:"Sorting trash stops it from becoming a 'monster' and turns it into useful things we can use again.",ae:[1,3],"mfl":[5,9],sdgs:[12,13]}, + {id:7,name:"Ingredient Detective",action_text:"Flip the package, read every ingredient, learn what it is and where it comes from.",description:"Being a detective helps you choose food that is good for your body and fair to the people who made it.",ae:[8,13],mfl:[3,10],sdgs:[3,12]}, + {id:8,name:"Mindful Eating",action_text:"Smell, touch, feel, savor each bite consciously. Actively appreciate how food nourishes and sustains us.",description:"Slowing down to enjoy food helps us appreciate all the hard work nature and farmers did to feed us.",ae:[9,11],mfl:[3],sdgs:[3,12]}, + {id:9,name:"Fermentastic",action_text:"Discover the art of fermentation – a natural and cheap way to preserve and enrich ingredients. Learn the basics and set up your first jar.",description:"Fermenting is a clever, low-cost way to make food last longer and become even better for your tummy.",ae:[1,7],mfl:[3,8],sdgs:[3,12]}, + {id:10,name:"Tree Planter",action_text:"Plant a tree in your compound, school, or in degraded spaces. Join hands with others to learn more and have fun.",description:"Planting a tree is a gift to the future; it helps fix the land and gives the Earth a chance to breathe.",ae:[5,8],mfl:[4,5],sdgs:[7,13]}, + {id:11,name:"Seed Scout",action_text:"Scout for seeds in your surroundings. Where can you get them? Shops, farms, schools, neighbors? Then obtain some and start saving seeds.",description:"Learning where to find and save seeds is the first step to making sure your community always has enough food to grow.",ae:[5,12],mfl:[6],sdgs:[1,2]}, + {id:12,name:"Natural Spritz",action_text:"Spraying chemicals kills pests but harms the environment, beneficial insects, and health. Prepare and use a natural biopesticide and see how it works wonders and protects people and the environment.",description:"Using natural sprays proves we can protect our gardens without hurting ourselves or the helpful bugs.",ae:[2,4],mfl:[1,2],sdgs:[9,12]}, + {id:13,name:"Dashboard Designer",action_text:"Design and paint a 'data dashboard' which shows information about something that influences how well your family's crops grow (which seed, which management, which inputs, etc.).",description:"Tracking your 'data' helps you become a smarter farmer because you can see exactly what makes your plants happy.",ae:[8],mfl:[10],sdgs:[8,9]}, + {id:14,name:"Onion Bodyguard",action_text:"Ever heard of the superpower of onions? Plant some onions between other plants and observe their protection pester powers.",description:"This shows how different plants can look out for each other, using 'bodyguard' plants to keep pests away naturally.",ae:[2,6],mfl:[1,5],sdgs:[13,15]}, + {id:15,name:"Local Investor",action_text:"Buy food from a local farm or market today instead of going to a supermarket!",description:"Buying from neighbors keeps your community's money close to home and supports local families.",ae:[7,9],mfl:[3,8],sdgs:[1,11]}, + {id:16,name:"Poster Designer",action_text:"Create posters about why everyone has the right to be included in farming, owning land, and attending community meetings.",description:"Using art to speak up about land rights helps everyone understand that they have a fair place in the world of farming",ae:[12,13],mfl:[7,11],sdgs:[5,10]}, + {id:17,name:"Change-Maker",action_text:"Find a topic you are passionate about, join a group that cares about the same, and contribute to making a difference!",description:"Joining a group shows that when we work together on things we care about, our small actions turn into big changes.",ae:[8,13],mfl:[11,12],sdgs:[16,17]}, + {id:18,name:"Waste Remover",action_text:"Clean up, sort, and recycle the waste you find in your surroundings to help keep animals, plants, land, and water healthy.",description:"Cleaning up trash keeps our land and water healthy for the animals and plants we share our home with.",ae:[1,12],mfl:[5],sdgs:[6,13]}, + {id:19,name:"Manure Maker",action_text:"Learn how to 'cool down' animal waste before adding it to your compost pile to create super-food for your plants!",description:"Turning waste into 'super-food' for plants is the best way to recycle energy back into the soil.",ae:[3,6],mfl:[1,9],sdgs:[12,15]}, + {id:20,name:"Mixed Garden",action_text:"Plant maize, beans, leafy greens, forages, and more. Learn about companion planting and that a mixed garden means more food, more soil nutrients, and a healthier family.",description:"A garden with many types of plants is stronger, healthier, and provides a diversity of vitamins for your family.",ae:[4,5],mfl:[2,5],sdgs:[2,15]}, + {id:21,name:"Ash Bug Blaster",action_text:"Use wood ash to fight bugs naturally. No chemicals needed — ash is free, safe, and it works!",description:"It's important that we can protect our food using simple, free things from our own homes, like wood ash.",ae:[2,6],mfl:[1,5],sdgs:[12,15]}, + {id:22,name:"Dry & Store",action_text:"Dry your seeds and store them safely. Dry seeds last longer and grow better — patience now means abundance later.",description:"Proper drying and storage is a smart way to protect your seeds so they stay healthy and ready to sprout when the next season comes.",ae:[5,9],mfl:[6],sdgs:[2,15]}, + {id:23,name:"Seed Distributor",action_text:"Collect ripe fruits and share the seeds with local farmers to spread the harvest further.",description:"Sharing seeds with other farmers is a great way to spread the harvest and make sure everyone in the village can grow good food.",ae:[10,11],mfl:[6,8],sdgs:[12,15]}, + {id:24,name:"River Cleaners",action_text:"Organize a river clean-up day. A clean river means clean water for animals, plants, and the whole community.",description:"A clean river is like a healthy heartbeat for a community, providing safe water for everyone",ae:[8,12],mfl:[11,12],sdgs:[6,14]}, + {id:25,name:"Garden Documenter",action_text:"Use a smartphone or tablet camera. Take pictures of all the plants growing in the school garden in a month. Create a slideshow and present to your classmates.",description:"Using cameras and technology to track your garden helps you see the 'big picture' of how nature changes over time.",ae:[8],mfl:[10],sdgs:[4,9]}, + {id:26,name:"Local Variety Guardian",action_text:"Save seeds from traditional crops. Local varieties are often stronger, more nutritious, and part of your community's food heritage — protect them.",description:"Saving seeds from traditional crops protects your community's history and keeps local plants strong and healthy.",ae:[5,9],mfl:[6],sdgs:[2,3]}, + {id:27,name:"Nature Regenerator",action_text:"Let nature grow back. Protect young trees and shrubs that appear naturally — farmer-managed regeneration works with nature, not against it.",description:"Sometimes the best way to help nature is to simply protect what is already trying to grow back on its own.",ae:[5,6],mfl:[4,5],sdgs:[7,13]}, + {id:28,name:"Biodiversity Steward",action_text:"Protect wild places. Some land should stay free from human activity — forests, wetlands, and riverbanks are home to animals and plants that we all and the earth need.",description:"Protecting wild places ensures that nature has its own safe space to thrive without being disturbed.",ae:[4,5],mfl:[5,7],sdgs:[13,14]}, + {id:29,name:"Land Negotiator",action_text:"When different people want to use the same land for different things, think together about strategies that will help develop a plan that works for everyone.",description:"Learning how to talk through disagreements about land helps create a plan that is fair and peaceful for everyone.",ae:[12,13],mfl:[7,11],sdgs:[5,16]}, + {id:30,name:"TJD Booster",action_text:"If you played out 5 cards and action missions this week, reward yourself the World Class Changemaker achievement",description:"Celebrating your hard work as a 'Changemaker' gives you the energy and pride to keep helping your community and the planet.",ae:[13],mfl:[11],sdgs:[17]}, + {id:31,name:"Kitchen Garden",action_text:"Plant a fruit, vegetable, or herb outside your home. Water every day and watch it grow. Fresh food right at your door—save money and eat well!",description:"Having a garden right outside your door means you always have healthy food that costs nothing but a little care.",ae:[2,3],mfl:[2,3],sdgs:[1,11]}, + {id:32,name:"Balanced Plate Builder",action_text:"Look at what you eat today and sort your food into groups: Carbs, proteins, dairy, fruits & vegetables, and fats. Find out what each group does for your body. Tomorrow, try to build a more balanced plate, to help your body grow strong and stay healthy!",description:"Learning how to balance your plate is the key to building a body that is strong enough to change the world.",ae:[9],mfl:[3],sdgs:[2,3]}, + {id:33,name:"Indigenous Tree Lover",action_text:"Ask a family or community member about an indigenous tree in your area and all its benefits. Ask about methods to protect them, find an indigenous tree and out your new skills into practice.",description:"Protecting local trees using wisdom from your family keeps the balance of nature in your area healthy.",ae:[5,6],mfl:[4,5],sdgs:[13,15]}, + {id:34,name:"Cover Crop Protector",action_text:"Cover crops are natural protectors. They protect from the sun, preserve moisture, and keep the soil alive. Plant a cover crop in your school or home garden, such as pumpkins, 9 or cowpeas.",description:"These plants act like a 'living blanket' for the soil, keeping it cool, wet, and full of life.",ae:[3,6],mfl:[1,2],sdgs:[13,15]}, + {id:35,name:"Local Food Producer",action_text:"Find one food your community usually buys from far away (e.g., flour, fruits, oil, jam). Make a plan: How could your school or family produce this locally? What would you need? Who would you sell to?",description:"Making things locally reduces the need for big trucks and long trips, which is better for the planet.",ae:[7,11],mfl:[8,11],sdgs:[8,12]}, + {id:36,name:"Market Price Checker",action_text:"Visit a local market and check the price of 3 vegetables. Compare prices at the market vs. at a supermarket. Discuss with your friend where you and the producers get a fairer deal, and why.",description:"Checking prices helps you understand how to get a fair deal for both the person growing the food and the person buying it.",ae:[10,11],mfl:[8,10],sdgs:[1,8]}, + {id:37,name:"Community Meeting",action_text:"When people learn and decide together, everyone grows stronger. Plan a class or community meeting about a food or farming topic. Ensure everyone has a chance to speak. Write down the key ideas and decisions.",description:"Making sure everyone gets a chance to speak at meetings ensures that big decisions are fair and include everyone's best ideas.",ae:[8,13],mfl:[11],sdgs:[5,10]}, + {id:38,name:"Jar Farm Product Marketer",action_text:"Grow sprouts in a jar for a week, invent a product name, design a label and 'market' your healthy food ingredient to family, friends and neighbors.",description:"Learning how to label and 'sell' healthy food teaches you the business skills needed to be a successful green entrepreneur.",ae:[2,7],mfl:[2,8],sdgs:[1,9]}, + {id:39,name:"Food Donation Hero",action_text:"Everybody should have access to healthy and yummy food. If you can, share some food with someone who needs it today. It can be small, every act matters.",description:"Sharing food ensures that 'healthy and yummy' meals aren't a luxury, but something everyone can enjoy.",ae:[10],mfl:[3],sdgs:[2,10]}, + {id:40,name:"Windbreak Warrior",action_text:"Trees are powerful protectors for people and farms. Stand under a tree and feel what it does. Think about how it blocks wind, gives shade, and protects both you and your crops?",description:"Trees are like giant shields that protect our homes and crops from being damaged by the wind and sun.",ae:[5,6],mfl:[4,7],sdgs:[7,15]}, + {id:41,name:"Seed Champion",action_text:"Every person who saves seeds makes the whole community stronger, and more independent. Help someone understand why saving and sharing seeds matters.",description:"When you teach others to save seeds, you help your whole community become more independent and powerful.",ae:[5,10],mfl:[6,12],sdgs:[1,10]}, + {id:42,name:"Fruit & Tree Team",action_text:"Nature works together. Try planting or finding a climbing fruit next to a tree. Watch how the tree supports the fruit, and how they live together happily.",description:"This shows how nature loves teamwork; trees can provide the 'ladder' for fruits to climb and grow better.",ae:[5,6],mfl:[4,7],sdgs:[13,15]}, + {id:43,name:"Manure Cycle Drawer",action_text:"Animals make manure. The nutrients in the manure feed crops. Crops feed people. Draw this farm nutrient cycle and show how everything is connected!",description:"Drawing this cycle helps us see how animals, plants, and people are all part of one big, connected family.",ae:[4,6],mfl:[1,7],sdgs:[13,15]}, + {id:44,name:"Animal Hero",action_text:"Think about a farm with animals, crops, trees and people together. How do animals help the farm? Tell a friend about all the ways!",description:"Sharing the story of how animals help the farm helps others see that every creature has an important job to do in nature.",ae:[4,6],mfl:[1,7],sdgs:[15]}, + {id:45,name:"Land Magic",action_text:"One piece of land can do so many things! Choose a piece of land (home, school, or a nearby farm), and list everything it already provides for people and nature. Now think about 3 more things it could do. Draw a map and share it with your friends.",description:"Mapping all the things one piece of land can do helps us discover new ways to grow food and protect nature at the same time.",ae:[5,6],mfl:[7],sdgs:[13,15]}, + {id:46,name:"Local Chicken Lover",action_text:"Find a member of the local community who raises and sells local chickens. Ask them what they love about them!",description:"Supporting local chicken farmers helps keep your community's economy strong and respects animals that belong in your environment.",ae:[6,7],mfl:[1,8],sdgs:[1,8]}, + {id:47,name:"Food Dryer Explorer",action_text:"Choose a fruit or vegetable that your home sometimes has too much of (e.g. mangoes, tomatoes). Find out how to dry and preserve it, then try it out!",description:"Drying food is a smart way to make sure that 'too much' food today becomes a great snack for tomorrow.",ae:[1,7],mfl:[3,9],sdgs:[9,12]}, + {id:48,name:"Scrap-tastic Animal Feeder",action_text:"Turn waste into feeds — nothing goes to waste! Collect food scraps and give them to animals (like chickens). What scraps can they eat? What should not be fed? Write down a scrap-tastic menu for them.",description:"Feeding animals the right scraps means nothing is ever wasted on a smart, circular farm.",ae:[1,4],mfl:[1,9],sdgs:[11,15]}, + {id:49,name:"Leftover Chef",action_text:"Say no to food waste! Use yesterday's leftovers to make a meal. How can you make it tasty again?",description:"Being a leftover chef is a creative way to say 'no' to waste and 'yes' to delicious, recycled meals.",ae:[1,9],mfl:[3,9],sdgs:[2,12]}, + {id:50,name:"Weather Tracker Checker",action_text:"Check the weather each day for a week using a phone or computer. Tip: Planting at the right time saves crops and makes your harvest bigger!",description:"Checking the weather helps you become a 'science-based' farmer, so you can plant at the perfect time to get a big harvest.",ae:[8,12],mfl:[10],sdgs:[4,9]}, + {id:51,name:"Farm Record Keeper",action_text:"Good records help you understand your farm better. Record information about 3 plants in your farm (at home or at school): planting time, management, pests, changes in color, growth etc. Think about the data you need to make good farming decision.",description:"Writing down facts about your plants helps you understand what they need to grow best, making you a much smarter farmer.",ae:[8,12],mfl:[10],sdgs:[4,9]}, + {id:52,name:"The Water Fertilizers",action_text:"Did you know that fish are little wizards? They make for yummy meals, but also fertilize the water they live in that can be reused to water your crops. Discuss with your friends if you can set up a fish pond at school.",description:"This shows how we can use nature's own 'magic' (like fish) to help our food grow without needing chemicals",ae:[6,7],mfl:[1,7],sdgs:[1,14]}, + {id:53,name:"Small Farm, Big Voice",action_text:"Even small farms matter! Everything is connected. Challenge: Roleplay a farmer at a community meeting and speak up for sustainable land use. What would you say?",description:"Even if a farm is small, speaking up for sustainable land use is a powerful way to protect the future of food.",ae:[12,13],mfl:[7,11],sdgs:[5,10]}, + {id:54,name:"Nature Educator",action_text:"When people understand, they act. Teach someone in your community why and how to protect nature, save forests, animals, and rivers!",description:"Teaching others how to protect forests and rivers spreads a 'green message' that can save the environment for everyone.",ae:[8,13],mfl:[5,12],sdgs:[10,15]}, + {id:55,name:"Teamwork Harvest",action_text:"Plant and grow something with 1 or 2 friends. Teamwork makes your work easier and your harvest bigger. Many hands, full baskets — and full hearts!",description:"Doing things with friends makes the work lighter and the celebration of the harvest much bigger!",ae:[8,13],mfl:[2,11],sdgs:[8,15]}, + {id:56,name:"Sky Garden",action_text:"Food can grow almost anywhere. Try growing upwards by planting in a hanging basket! More food with less space. Your garden can reach the sky!",description:"Growing upwards proves that you don't need a huge farm to be a successful gardener—you just need a little creativity!",ae:[7,9],mfl:[2,7],sdgs:[9,10]}, + {id:57,name:"Write for Change",action_text:"Write to a local leader about a food system issue you care about. Be precise, present evidence, make suggestions for change, offer support.",description:"Writing to leaders with real evidence shows them that young people have the power and the facts to help change the world.",ae:[12,13],mfl:[11,12],sdgs:[10,16]}, + {id:58,name:"Ancient Wisdom Defender",action_text:"Old knowledge is gold! Talk to an elder about farming and food. Learn from their stories!",description:"Talking to elders is like finding a treasure chest of secrets that can help us farm better today.",ae:[8,9],mfl:[11,12],sdgs:[5,10]}, + {id:59,name:"Seedling Protector",action_text:"Visit a tree nursery. Learn how you can join hands to grow a tree step-by-step from selecting a good seed to planting, and helping it grow.",description:"Learning how to care for tiny trees in a nursery ensures they grow up strong enough to protect the environment.",ae:[6,12],mfl:[4,11],sdgs:[7,15]}, + {id:60,name:"Cooperative Builder",action_text:"Every voice counts — but there is power in numbers. When farmers work together and sell as a group, they earn more and waste less. Find ways to improve access to healthy food in your area.",description:"When we work together as a group, we have a much louder voice and can solve bigger problems.",ae:[10,11],mfl:[8,9],sdgs:[10,11]} +]; + +export const CARD_IMG: (string | null)[] = [ + null, + "1-gift-of-water-action.png","2-grow-greens-action.png","3-fine-dining-compost-dish-action.png", + "4-buzzing-with-wisdom-action.png","5-baking-flatbread.png","6-mixed-wastemonster-action.png", + "7-ingredient-detective-action.png","8-mindful-eating-action.png","9-fermentastic-action.png", + "10-tree-planter.png","11-seed-scout.png","12-natural-spritz.png", + "13-dashboard-designer.png","14-onion-bodyguard.png","15-local-investor.png", + "16-poster-designer.png","17-change-maker.png","18-waste-remover.png", + "19-manure-maker.png","20-mixed-garden.png","21-ash-bug-blaster.png", + "22-dry-and-store.png","23-seed-distributor.png","24-river-cleaners.png", + "25-garden-documenter.png","26-local-variety-guardian.png","27-nature-regenerator.png", + "28-biodiversity-steward.png","29-land-negotiator.png","30-tjd-booster-action.png", + "31-kitchen-garden.png","32-balanced-plate-builder.png","33-indigenous-tree-lover.png", + "34-cover-crop-protector.png","35-local-food-producer.png","36-market-price-checker.png", + "37-community-meeting.png","38-jar-farm-product-marketer.png","39-food-donation-hero.png", + "40-windbreak-warrior.png","41-seed-champion.png","42-fruit-and-tree-team.png", + "43-manure-cycle-drawer.png","44-animal-hero.png","45-land-magic.png", + "46-local-chicken-lover.png","47-food-dryer-explorer.png","48-scrap-tastic-animal-feeder.png", + "49-leftover-chef.png","50-weather-tracker-checker.png","51-farm-record-keeper.png", + "52-the-water-fertilizers.png","53-small-farm-big-voice.png","54-nature-educator.png", + "55-teamwork-harvest.png","56-sky-garden.png","57-be-the-change-action.png", + "58-ancient-wisdom-defender-action.png","59-seedling-protector-action.png","60-cooperative-builder-action.png" +]; + +export function getCardImage(card: Card): string { + const f = CARD_IMG[card.id]; + return f ? `/assets/cards/${f}` : ''; +} diff --git a/src/data/labels.ts b/src/data/labels.ts new file mode 100644 index 0000000..d943fbe --- /dev/null +++ b/src/data/labels.ts @@ -0,0 +1,27 @@ +export const AE_LABELS = [ + "AE1: Recycling","AE2: Input reduction","AE3: Soil health","AE4: Animal health", + "AE5: Biodiversity","AE6: Synergy","AE7: Economic diversification", + "AE8: Co-creation of knowledge","AE9: Social values & diets", + "AE10: Fairness","AE11: Connectivity","AE12: Land & natural governance", + "AE13: Participation" +]; + +export const MFL_LABELS = [ + "MFL1: Conservation","MFL2: Production","MFL3: Consumption","MFL4: Trees", + "MFL5: Water","MFL6: Seeds","MFL7: Ecosystems","MFL8: Markets", + "MFL9: Waste","MFL10: Data","MFL11: Governance","MFL12: Education" +]; + +export const SDG_LABELS = [ + "SDG1: No poverty","SDG2: Zero hunger","SDG3: Good health","SDG4: Quality education", + "SDG5: Gender equality","SDG6: Clean water","SDG7: Clean energy","SDG8: Decent work", + "SDG9: Industry & innovation","SDG10: Reduced inequalities","SDG11: Sustainable cities", + "SDG12: Responsible consumption","SDG13: Climate action","SDG14: Life below water", + "SDG15: Life on land","SDG16: Peace & justice","SDG17: Partnerships" +]; + +export function iconPath(type: 'ae' | 'mfl' | 'sdg', n: number): string { + const folderMap = { ae: 'ae', mfl: 'MFL', sdg: 'SDG' } as const; + const extMap = { ae: 'svg', mfl: 'png', sdg: 'png' } as const; + return `/assets/icons/${folderMap[type]}/${type.toUpperCase()}-${n}.${extMap[type]}`; +} diff --git a/src/hooks/useGameState.ts b/src/hooks/useGameState.ts new file mode 100644 index 0000000..673461b --- /dev/null +++ b/src/hooks/useGameState.ts @@ -0,0 +1,566 @@ +import { useState, useCallback, useRef } from 'react'; +import type { Card, Player, GamePhase, MatchInfo, MatchRecord, MatchMode } from '../types'; +import { CARDS } from '../data/cards'; + +function shuffle(a: T[]): T[] { + const arr = [...a]; + for (let i = arr.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [arr[i], arr[j]] = [arr[j], arr[i]]; + } + return arr; +} + +function matchCount(playerCards: Card[], challenge: Card): MatchInfo { + const ae = new Set(), mfl = new Set(), sdgs = new Set(); + for (const c of playerCards) { + c.ae.forEach(v => ae.add(v)); + c.mfl.forEach(v => mfl.add(v)); + c.sdgs.forEach(v => sdgs.add(v)); + } + const cae = new Set(challenge.ae), cmfl = new Set(challenge.mfl), csdg = new Set(challenge.sdgs); + let m = 0, t = 0; + cae.forEach(v => { t++; if (ae.has(v)) m++; }); + cmfl.forEach(v => { t++; if (mfl.has(v)) m++; }); + csdg.forEach(v => { t++; if (sdgs.has(v)) m++; }); + return { matched: m, total: t }; +} + +function isCompleteMatch(playerCards: Card[], challenge: Card) { + const r = matchCount(playerCards, challenge); + return r.matched === r.total && r.total > 0; +} + +function aiChooseCard(p: Player, challenge: Card): number { + let bestIdx = 0, bestScore = -1; + for (let i = 0; i < p.hand.length; i++) { + const test = [...p.projectZone, p.hand[i]]; + const r = matchCount(test, challenge); + if (r.matched > bestScore || (r.matched === bestScore && Math.random() > 0.5)) { + bestScore = r.matched; + bestIdx = i; + } + } + return bestIdx; +} + +function calculateScores(players: Player[], challenge: Card): { players: Player[]; collaborativeWin: boolean } { + const p2 = JSON.parse(JSON.stringify(players)) as Player[]; + let anyFullMatch = false; + for (const p of p2) { + const info = matchCount(p.projectZone, challenge); + p.matchInfo = info; + if (info.matched === info.total && info.total > 0) anyFullMatch = true; + } + if (anyFullMatch) { + let bestIdx = 0, bestMatch = 0; + p2.forEach((p, i) => { + if (p.matchInfo!.matched > bestMatch) { bestMatch = p.matchInfo!.matched; bestIdx = i; } + }); + p2[bestIdx].points += 2; + p2.forEach((p, i) => { + if (i !== bestIdx && p.matchInfo!.matched === p.matchInfo!.total) p.points += 1; + }); + return { players: p2, collaborativeWin: false }; + } + const combined = { ae: new Set(), mfl: new Set(), sdgs: new Set() }; + p2.forEach(p => p.projectZone.forEach(c => { + c.ae.forEach(v => combined.ae.add(v)); + c.mfl.forEach(v => combined.mfl.add(v)); + c.sdgs.forEach(v => combined.sdgs.add(v)); + })); + const cae = new Set(challenge.ae), cmfl = new Set(challenge.mfl), csdg = new Set(challenge.sdgs); + let allCovered = true; + cae.forEach(v => { if (!combined.ae.has(v)) allCovered = false; }); + cmfl.forEach(v => { if (!combined.mfl.has(v)) allCovered = false; }); + csdg.forEach(v => { if (!combined.sdgs.has(v)) allCovered = false; }); + if (allCovered) { + p2.forEach(p => p.points += 1); + return { players: p2, collaborativeWin: true }; + } + return { players: p2, collaborativeWin: false }; +} + +const delay = (ms: number) => new Promise(r => setTimeout(r, ms)); + +export interface GameAPI { + phase: GamePhase; + deck: Card[]; + challenge: Card | null; + players: Player[]; + currentPlayer: number; + round: number; + selectedCard: number | null; + statusMessage: string; + statusClass: string; + showResults: boolean; + animatingCardId: number | null; + turnBanner: string; + challengeReveal: string | null; + collaborativeWin: boolean; + matchMode: MatchMode; + matchIndex: number; + matchHistory: MatchRecord[]; + seriesWinner: number | null; + startGame: (aiCount: number, mode: MatchMode) => void; + nextMatch: () => void; + selectCard: (index: number | null) => void; + playCard: () => void; + endTurn: () => void; + handleReplace: (doReplace: boolean) => void; + backToMenu: () => void; + suddenSolvePlayCard: (index: number) => void; + suddenSolveUseBackup: (slotIndex?: number) => void; + suddenSolveSkipBackup: () => void; + applyBackupChoice: (slotIndex?: number) => void; +} + +export function useGameState(): GameAPI { + const [phase, setPhase] = useState('menu'); + const [deck, setDeck] = useState([]); + const [challenge, setChallenge] = useState(null); + const [players, setPlayers] = useState([]); + const [currentPlayer, setCurrentPlayer] = useState(0); + const [round, setRound] = useState(0); + const [selectedCard, setSelectedCard] = useState(null); + const [statusMessage, setStatusMessage] = useState(''); + const [statusClass, setStatusClass] = useState(''); + const [showResults, setShowResults] = useState(false); + const [animatingCardId, setAnimatingCardId] = useState(null); + const [turnBanner, setTurnBanner] = useState(''); + const [challengeReveal, setChallengeReveal] = useState(null); + const [collaborativeWin, setCollaborativeWin] = useState(false); + const [matchMode, setMatchMode] = useState('single'); + const [matchIndex, setMatchIndex] = useState(0); + const [matchHistory, setMatchHistory] = useState([]); + + const cancelRef = useRef(false); + + const setStatus = useCallback((msg: string, cls = '') => { + setStatusMessage(msg); + setStatusClass(cls); + }, []); + + const dealMatch = useCallback(async () => { + const d = shuffle([...CARDS]); + const ch = d.pop()!; + const p: Player[] = [ + { name: 'You', isHuman: true, hand: [], projectZone: [], backupCard: null, usedReplace: false, points: 0, hasActed: false, matchInfo: null } + ]; + const aiCount = 1; + for (let i = 1; i <= aiCount; i++) { + p.push({ name: `AI ${i}`, isHuman: false, hand: [], projectZone: [], backupCard: null, usedReplace: false, points: 0, hasActed: false, matchInfo: null }); + } + p.forEach(pl => { + pl.hand = d.splice(0, 5); + pl.backupCard = d.pop() || null; + }); + + setDeck(d); + setChallenge(ch); + setPlayers(p); + setCurrentPlayer(0); + setRound(1); + setSelectedCard(null); + setCollaborativeWin(false); + setShowResults(false); + setAnimatingCardId(null); + setTurnBanner(''); + setStatus('', ''); + + setPhase('setup'); + setChallengeReveal('intro'); + await delay(1500); + setChallengeReveal('3'); + await delay(1000); + setChallengeReveal('2'); + await delay(1000); + setChallengeReveal('1'); + await delay(1000); + setChallengeReveal(null); + setPhase('replace'); + setStatus('Global challenge revealed! Review the icons, then decide if you want to Replace! your hand.', 'highlight'); + }, []); + + const startGame = useCallback(async (aiCount: number, mode: MatchMode) => { + cancelRef.current = false; + setMatchMode(mode); + setMatchIndex(0); + setMatchHistory([]); + await dealMatch(); + }, [dealMatch]); + + const nextMatch = useCallback(async () => { + cancelRef.current = false; + setMatchIndex(i => i + 1); + await dealMatch(); + }, [dealMatch]); + + const handleReplace = useCallback(async (doReplace: boolean) => { + const p = JSON.parse(JSON.stringify(players)) as Player[]; + let d = [...deck]; + + if (doReplace) { + d.push(...p[0].hand); + const shuffled = shuffle(d); + p[0].hand = shuffled.splice(0, 5); + p[0].usedReplace = true; + d = shuffled; + } + + for (let i = 1; i < p.length; i++) { + const iconCount = p[i].hand.reduce((s, c) => s + c.ae.length + c.mfl.length + c.sdgs.length, 0); + if (iconCount < 5) { + d.push(...p[i].hand); + const shuffled = shuffle(d); + p[i].hand = shuffled.splice(0, 5); + p[i].usedReplace = true; + d = shuffled; + } + } + setDeck(d); + setPlayers(p); + + setPhase('play_turn'); + setCurrentPlayer(0); + setSelectedCard(null); + setStatus('Year 1 — Project 1 — Select a card to play.', ''); + }, [players, deck]); + + const selectCard = useCallback((index: number | null) => { + if (phase !== 'play_turn' && phase !== 'sudden_solve') return; + setSelectedCard(index); + }, [phase]); + + const playCard = useCallback(() => { + if (selectedCard === null || phase !== 'play_turn') return; + const p = JSON.parse(JSON.stringify(players)) as Player[]; + const human = p[0]; + if (selectedCard >= human.hand.length || human.hasActed) return; + + const card = human.hand.splice(selectedCard, 1)[0]; + human.projectZone.push(card); + human.hasActed = true; + setSelectedCard(null); + setPlayers(p); + setAnimatingCardId(card.id); + + setTimeout(async () => { + setAnimatingCardId(null); + if (cancelRef.current) return; + + if (isCompleteMatch(human.projectZone, challenge!)) { + setStatus('SUDDEN SOLVE! You matched all challenge icons!', 'sudden'); + await delay(1200); + await runSuddenSolve(0, JSON.parse(JSON.stringify(p)), challenge!); + return; + } + + if (round < 3) { + setPhase('end_turn_prompt'); + setStatus('Card played! Click End Turn.', ''); + } else { + setPhase('end_turn_prompt'); + setStatus('Card played.', ''); + } + }, 500); + }, [selectedCard, phase, players, round, challenge]); + + const runSuddenSolve = async (triggerIdx: number, p: Player[], ch: Card) => { + setPhase('sudden_solve'); + p.forEach(pl => pl.hasActed = false); + setPlayers([...p]); + + for (let i = 0; i < p.length; i++) { + if (i === triggerIdx) continue; + const pl = p[i]; + if (cancelRef.current) return; + + if (pl.isHuman) { + setStatus('Sudden Solve! Play a card from your hand if you wish.', 'highlight'); + setSelectedCard(null); + return; + } else { + if (pl.hand.length > 0) { + const ci = aiChooseCard(pl, ch); + const played = pl.hand.splice(ci, 1)[0]; + pl.projectZone.push(played); + setAnimatingCardId(played.id); + setPlayers([...p]); + await delay(600); + setAnimatingCardId(null); + } + if (pl.backupCard && !cancelRef.current) { + await aiUseBackup(pl, ch); + setPlayers([...p]); + await delay(400); + } + } + } + await finishGame(p, ch); + }; + + const aiUseBackup = async (pl: Player, ch: Card) => { + const testCards = [...pl.projectZone, pl.backupCard!]; + if (isCompleteMatch(testCards, ch)) { + pl.projectZone.push(pl.backupCard!); + pl.backupCard = null; + } else { + let bestGain = 0, bestSlot = -1; + for (let s = 0; s < pl.projectZone.length; s++) { + const alt = [...pl.projectZone]; + alt[s] = pl.backupCard!; + const curR = matchCount(pl.projectZone, ch); + const newR = matchCount(alt, ch); + const gain = newR.matched - curR.matched; + if (gain > bestGain) { bestGain = gain; bestSlot = s; } + } + if (bestSlot >= 0 && bestGain > 0) { + pl.hand.push(pl.projectZone[bestSlot]); + pl.projectZone[bestSlot] = pl.backupCard!; + pl.backupCard = null; + } + } + }; + + const suddenSolvePlayCard = useCallback((index: number) => { + if (phase !== 'sudden_solve') return; + const p = JSON.parse(JSON.stringify(players)) as Player[]; + if (index >= 0 && index < p[0].hand.length) { + const card = p[0].hand.splice(index, 1)[0]; + p[0].projectZone.push(card); + setPlayers(p); + setAnimatingCardId(card.id); + setTimeout(async () => { + setAnimatingCardId(null); + if (cancelRef.current) return; + if (p[0].backupCard) { + setStatus('Do you want to use your Backup Project Card?', 'highlight'); + return; + } + await continueAfterHuman(p); + }, 500); + } + }, [phase, players]); + + const suddenSolveUseBackup = useCallback((slotIndex?: number) => { + if (phase !== 'sudden_solve') return; + const p = JSON.parse(JSON.stringify(players)) as Player[]; + if (p[0].backupCard) { + if (slotIndex !== undefined && slotIndex >= 0 && slotIndex < p[0].projectZone.length) { + p[0].hand.push(p[0].projectZone[slotIndex]); + p[0].projectZone[slotIndex] = p[0].backupCard; + p[0].backupCard = null; + } else if (slotIndex === -1) { + p[0].projectZone.push(p[0].backupCard); + p[0].backupCard = null; + } + setPlayers(p); + } + continueAfterHuman(p); + }, [phase, players]); + + const suddenSolveSkipBackup = useCallback(() => { + if (phase !== 'sudden_solve') return; + const p = JSON.parse(JSON.stringify(players)) as Player[]; + continueAfterHuman(p); + }, [phase, players]); + + const continueAfterHuman = async (p: Player[]) => { + if (cancelRef.current) return; + const ch = challenge!; + + for (let i = 1; i < p.length; i++) { + const pl = p[i]; + if (cancelRef.current) return; + if (pl.hand.length === 0 && !pl.backupCard) continue; + + if (pl.hand.length > 0) { + const ci = aiChooseCard(pl, ch); + const played = pl.hand.splice(ci, 1)[0]; + pl.projectZone.push(played); + setAnimatingCardId(played.id); + setPlayers([...p]); + await delay(600); + setAnimatingCardId(null); + } + if (pl.backupCard && !cancelRef.current) { + await aiUseBackup(pl, ch); + setPlayers([...p]); + await delay(400); + } + } + await finishGame(p, ch); + }; + + const completeScoring = async (p: Player[], ch: Card) => { + p[0].matchInfo = matchCount(p[0].projectZone, ch); + setPlayers(p); + const { players: scored, collaborativeWin } = calculateScores(p, ch); + setPlayers(scored); + setCollaborativeWin(collaborativeWin); + await delay(600); + const matchScores = scored.map(pl => pl.points); + const maxScore = Math.max(...matchScores); + const mWinner = maxScore > 0 ? matchScores.indexOf(maxScore) : null; + setMatchHistory(h => [...h, { scores: matchScores, winnerIndex: mWinner, collaborativeWin }]); + setPhase('done'); + setShowResults(true); + setStatus('', ''); + setTurnBanner(''); + }; + + const applyBackupChoice = useCallback((slotIndex?: number) => { + if (phase !== 'backup_choice') return; + const p = JSON.parse(JSON.stringify(players)) as Player[]; + const ch = challenge!; + if (slotIndex !== undefined && slotIndex >= 0 && slotIndex < p[0].projectZone.length) { + p[0].hand.push(p[0].projectZone[slotIndex]); + p[0].projectZone[slotIndex] = p[0].backupCard!; + p[0].backupCard = null; + } + completeScoring(p, ch); + }, [phase, players, challenge]); + + const finishGame = async (p: Player[], ch: Card) => { + setPhase('scoring'); + setStatus('Game Over! Calculating results...', ''); + // AI backup optimization + for (let i = 1; i < p.length; i++) { + const pl = p[i]; + if (pl.backupCard) { + const curR = matchCount(pl.projectZone, ch); + let bestR = curR.matched, bestSlot = -1; + for (let s = 0; s < pl.projectZone.length; s++) { + const alt = [...pl.projectZone]; + alt[s] = pl.backupCard!; + const r = matchCount(alt, ch); + if (r.matched > bestR) { bestR = r.matched; bestSlot = s; } + } + if (bestSlot >= 0 && bestR > curR.matched) { + pl.projectZone[bestSlot] = pl.backupCard!; + pl.backupCard = null; + } + } + pl.matchInfo = matchCount(pl.projectZone, ch); + } + // Human backup choice — pause and ask + if (p[0].backupCard) { + setPlayers(p); + setPhase('backup_choice'); + setStatus('Do you want to replace a project card with your Backup Project Card?', 'highlight'); + return; + } + await completeScoring(p, ch); + }; + + const endTurn = useCallback(() => { + if (phase !== 'end_turn_prompt') return; + const p = JSON.parse(JSON.stringify(players)) as Player[]; + const d = [...deck]; + let cp = currentPlayer + 1; + let r = round; + + const advanceRound = async () => { + r++; + setRound(r); + setTurnBanner(`Year ${r}`); + setStatus(`Year ${r} begins!`, 'highlight'); + await delay(1200); + setTurnBanner(''); + + for (let i = 0; i < p.length; i++) { + if (p[i].hand.length < 5 && d.length > 0) { + p[i].hand.push(d.pop()!); + } + p[i].hasActed = false; + } + setDeck([...d]); + setPlayers([...p]); + setPhase('play_turn'); + setCurrentPlayer(0); + setSelectedCard(null); + setStatus(`Year ${r} — Project ${r} — Select a card to play.`, ''); + }; + + const processAITurns = async () => { + while (cp < p.length) { + const ai = p[cp]; + if (cancelRef.current) return; + setCurrentPlayer(cp); + setStatus(`${ai.name} is thinking...`, ''); + await delay(800 + Math.random() * 600); + + if (cancelRef.current) return; + const ci = aiChooseCard(ai, challenge!); + const played = ai.hand.splice(ci, 1)[0]; + ai.projectZone.push(played); + ai.hasActed = true; + setPlayers([...p]); + setAnimatingCardId(played.id); + await delay(500); + setAnimatingCardId(null); + + if (isCompleteMatch(ai.projectZone, challenge!)) { + setStatus(`${ai.name} completed the challenge! Sudden Solve!`, 'sudden'); + await delay(1200); + await runSuddenSolve(cp, JSON.parse(JSON.stringify(p)), challenge!); + return; + } + cp++; + } + + if (r < 3) { + await advanceRound(); + } else { + await finishGame(p, challenge!); + } + }; + + processAITurns(); + }, [phase, currentPlayer, round, players, deck, challenge]); + + let seriesWinner: number | null = null; + if (matchMode !== 'single' && matchHistory.length > 0) { + const wins = [0, 0]; + const needed = matchMode === 'bo2' ? 2 : 2; + for (const m of matchHistory) { + if (m.winnerIndex !== null) wins[m.winnerIndex]++; + } + const totalMatches = matchHistory.length; + if (matchMode === 'bo2' && totalMatches >= 2) { + if (wins[0] > wins[1]) seriesWinner = 0; + else if (wins[1] > wins[0]) seriesWinner = 1; + } else if (matchMode === 'bo3' && (wins[0] >= 2 || wins[1] >= 2 || totalMatches >= 3)) { + if (wins[0] > wins[1]) seriesWinner = 0; + else if (wins[1] > wins[0]) seriesWinner = 1; + } + } + + const backToMenu = useCallback(() => { + cancelRef.current = true; + setPhase('menu'); + setShowResults(false); + setChallenge(null); + setPlayers([]); + setStatusMessage(''); + setStatusClass(''); + setSelectedCard(null); + setTurnBanner(''); + setAnimatingCardId(null); + setCollaborativeWin(false); + setMatchMode('single'); + setMatchIndex(0); + setMatchHistory([]); + }, []); + + return { + phase, deck, challenge, players, currentPlayer, round, + selectedCard, statusMessage, statusClass, showResults, + animatingCardId, turnBanner, challengeReveal, collaborativeWin, + matchMode, matchIndex, matchHistory, seriesWinner, + startGame, nextMatch, selectCard, playCard, endTurn, + handleReplace, backToMenu, + suddenSolvePlayCard, suddenSolveUseBackup, suddenSolveSkipBackup, applyBackupChoice, + }; +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..b934846 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './styles/index.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/src/styles/index.css b/src/styles/index.css new file mode 100644 index 0000000..3727fa1 --- /dev/null +++ b/src/styles/index.css @@ -0,0 +1,1441 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --bg: #1a2a1a; + --bg-gradient: linear-gradient(135deg, #1a2a1a 0%, #0d1a0d 50%, #1a2a1a 100%); + --surface: #2d3d2d; + --surface2: #3a4f3a; + --surface-glass: rgba(45, 61, 45, 0.45); + --surface-glass2: rgba(58, 79, 58, 0.3); + --accent: #8bc34a; + --accent2: #cddc39; + --gold: #ffd54f; + --text: #e8f5e9; + --text-dim: #a5d6a7; + --danger: #ef5350; + --card-w: 130px; + --card-h: 182px; + --radius: 10px; +} + +html, body, #root { + height: 100%; + overflow: hidden; +} + +body { + font-family: "Segoe UI", system-ui, -apple-system, sans-serif; + background: var(--bg); + color: var(--text); +} + +body::before { + content: ''; + position: fixed; + inset: 0; + background: url('/assets/game-background.png') center/cover no-repeat; + z-index: -1; +} + +/* ─── Start Screen ─── */ +.start-screen { + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 20px; + animation: fadeIn 0.6s ease-out; + overflow-y: auto; +} + +.start-card { + background: var(--surface-glass); + border: 2px solid var(--surface2); + border-radius: 20px; + padding: 48px 40px; + text-align: center; + max-width: 380px; + width: 100%; + backdrop-filter: blur(12px); + box-shadow: 0 0 60px rgba(139, 195, 74, 0.15); + animation: slideUp 0.6s ease-out; +} + +.start-logo { + width: 180px; + margin-bottom: 16px; + animation: float 3s ease-in-out infinite; +} + +.start-card h1 { + font-size: 1.6rem; + color: var(--gold); + margin-bottom: 6px; + line-height: 1.2; +} + +.subtitle { + color: var(--text-dim); + font-size: 0.95rem; + margin-bottom: 6px; +} + +.intro-text { + color: var(--text-dim); + font-size: 1.3rem; + line-height: 1.6; + margin-bottom: 28px; + opacity: 0.85; +} + +.start-form { + display: flex; + flex-direction: column; + gap: 16px; + align-items: center; +} + +.diy-section { + margin-top: 24px; + text-align: center; + animation: slideUp 0.5s ease-out 0.3s both; + max-width: 380px; + width: 100%; +} + +.diy-text { + font-size: 1.3rem; + color: var(--gold); + margin-bottom: 12px; + font-weight: 800; +} + +.diy-image { + width: 100%; + border-radius: 12px; + border: 2px solid var(--surface2); + transition: transform 0.3s ease, border-color 0.3s ease; + cursor: pointer; +} + +@media (hover: hover) { + .diy-image:hover { + transform: scale(1.03); + border-color: var(--accent); + } +} + +.mode-selector { + width: 100%; +} + +.mode-label { + font-size: 0.8rem; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 8px; + display: block; +} + +.mode-buttons { + display: flex; + gap: 6px; + justify-content: center; +} + +.mode-buttons .btn { + flex: 1; + padding: 8px 10px; + font-size: 0.8rem; +} + +/* ─── Buttons ─── */ +.btn { + padding: 10px 28px; + font-size: 1rem; + border: none; + border-radius: 8px; + background: var(--surface2); + color: var(--text); + font-weight: 700; + cursor: pointer; + transition: all 0.2s ease; + position: relative; + overflow: hidden; +} + +@media (hover: hover) { + .btn:hover { + background: var(--accent); + color: #1a2a1a; + transform: translateY(-2px); + box-shadow: 0 4px 14px rgba(139, 195, 74, 0.3); + } +} + +.btn:disabled { + opacity: 0.3; + cursor: not-allowed; + transform: none !important; + box-shadow: none !important; +} + +.btn-accent { + background: var(--accent); + color: #1a2a1a; +} + +@media (hover: hover) { + .btn-accent:hover { + background: var(--accent2); + } +} + +.btn-glow { + animation: glowPulse 2s ease-in-out infinite; +} + +.btn-play { + background: var(--accent); + color: #1a2a1a; +} + +@media (hover: hover) { + .btn-play:hover:not(:disabled) { + background: var(--accent2); + } +} + +.btn-end { + background: var(--gold); + color: #1a2a1a; +} + +@media (hover: hover) { + .btn-end:hover:not(:disabled) { + background: #ffe082; + } +} + +.btn-small { + padding: 6px 14px; + font-size: 0.8rem; +} + +/* ─── Game Screen: FULL SCREEN ─── */ +.game-screen { + height: 100vh; + display: flex; + flex-direction: column; + padding: 8px 12px; + gap: 6px; + animation: fadeIn 0.3s ease-out; + overflow: hidden; +} + +.game-header { + display: flex; + justify-content: space-between; + align-items: center; + flex-shrink: 0; +} + +.header-left { + display: flex; + gap: 8px; + align-items: center; +} + +.round-badge { + background: var(--surface); + border: 1px solid var(--surface2); + border-radius: 20px; + padding: 4px 14px; + font-size: 0.75rem; + color: var(--gold); + font-weight: 600; +} + +.deck-count { + background: var(--surface); + border-radius: 20px; + padding: 4px 14px; + font-size: 0.75rem; + color: var(--text-dim); +} + +/* ─── Challenge Zone ─── */ +.challenge-zone { + padding: 8px 12px; + background: var(--surface-glass); + border-radius: var(--radius); + border: 1px solid var(--surface2); + backdrop-filter: blur(8px); + flex-shrink: 0; +} + +.challenge-row { + display: flex; + gap: 12px; + justify-content: center; + align-items: flex-start; +} + +.challenge-row .card-slot { + flex-shrink: 0; +} + +.challenge-right { + display: flex; + flex-direction: column; + gap: 8px; + flex: 1; + min-width: 0; +} + +/* ─── Card Slots ─── */ +.card-slot { + width: var(--card-w); + height: var(--card-h); + border: 2px dashed var(--surface2); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; + transition: all 0.3s ease; + flex-shrink: 0; +} + +.card-slot.filled { + border-style: solid; + border-color: var(--accent); +} + +.card-slot.selected { + border-color: var(--gold); + box-shadow: 0 0 20px rgba(255, 213, 79, 0.5); + transform: translateY(-8px) scale(1.05); +} + +/* ─── Project Slots ─── */ +.project-slot { + width: calc(var(--card-w) * 0.78); + height: calc(var(--card-h) * 0.78); + cursor: default; + overflow: visible; +} + +/* Flip card container */ +.flip-card { + perspective: 800px; +} + +.flip-inner { + width: 100%; + height: 100%; + position: relative; + transform-style: preserve-3d; + transition: transform 0.5s ease; + z-index: 1; +} + +@media (hover: hover) { + .flip-inner.flip-enabled:hover { + transform: rotateY(180deg) scale(2); + z-index: 50; + } +} + +@media (hover: none) and (pointer: coarse) { + .flip-inner.flip-enabled:active { + transform: rotateY(180deg) scale(2); + z-index: 50; + transition: 0.15s; + } +} + +@media (hover: hover) { + .flip-card .flip-inner.preview-scale:hover, + .backup-card-display .flip-inner.preview-scale:hover { + transform: rotateY(180deg) scale(2); + z-index: 50; + } +} + +@media (hover: none) and (pointer: coarse) { + .flip-card .flip-inner.preview-scale:active, + .backup-card-display .flip-inner.preview-scale:active { + transform: rotateY(180deg) scale(2); + z-index: 50; + transition: 0.15s; + } +} + +.flip-inner.flip-revealed { + transform: rotateY(180deg); +} + +.flip-front, .flip-back { + position: absolute; + inset: 0; + backface-visibility: hidden; + border-radius: 6px; + overflow: hidden; +} + +.flip-back { + transform: rotateY(180deg); +} + +.flip-back .card-face { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} + +/* Card back overlay inside flip front */ +.flip-front .card-back-overlay { + width: 100%; + height: 100%; + background: url('/assets/card-background.png') center/cover no-repeat; + display: flex; + align-items: center; + justify-content: center; +} + +.flip-front .card-back-overlay::after { + content: "?"; + font-size: 1.6rem; + font-weight: 700; + color: rgba(255, 255, 255, 0.3); + text-shadow: 0 2px 8px rgba(0, 0, 0, 0.6); +} + +.project-slot.filled.animating-in { + animation: slotFill 0.5s ease-out; +} + +/* ─── Card Face ─── */ +.card-face { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 6px; + display: block; + pointer-events: none; +} + +.card-back { + width: 100%; + height: 100%; + border-radius: 6px; + background: linear-gradient(135deg, #3a5a3a 0%, #2d4a2d 50%, #1a3a1a 100%); + background-image: repeating-linear-gradient( + 45deg, transparent, transparent 8px, + rgba(139, 195, 74, 0.08) 8px, rgba(139, 195, 74, 0.08) 16px + ); + display: flex; + align-items: center; + justify-content: center; + border: 2px solid var(--surface2); +} + +.card-back::after { + content: "fsys"; + font-size: 1.2rem; + font-weight: 700; + color: rgba(139, 195, 74, 0.3); + letter-spacing: 3px; + text-transform: uppercase; +} + +.slot-label { + color: var(--text-dim); + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 1px; + opacity: 0.6; +} + +/* ─── Icon Panel ─── */ +.icon-panel { + display: flex; + flex-direction: row; + gap: 8px; + justify-content: center; + align-items: center; + flex-wrap: wrap; + flex: 1; + min-width: 100px; +} + +.icon-img { + width: 128px; + height: 128px; + object-fit: contain; + border-radius: 4px; + transition: transform 0.3s ease; + cursor: help; +} + +@media (hover: hover) { + .icon-img:hover { + transform: scale(1.15); + filter: brightness(1.2); + } +} + +.icon-pulse { + animation: iconAppear 0.4s ease-out both; +} + +.icon-pulse:nth-child(1) { animation-delay: 0.05s; } +.icon-pulse:nth-child(2) { animation-delay: 0.1s; } +.icon-pulse:nth-child(3) { animation-delay: 0.15s; } +.icon-pulse:nth-child(4) { animation-delay: 0.2s; } +.icon-pulse:nth-child(5) { animation-delay: 0.25s; } +.icon-pulse:nth-child(6) { animation-delay: 0.3s; } + +/* ─── Challenge Description ─── */ +.challenge-description { + text-align: left; +} + +.challenge-desc-label { + font-size: 0.9rem; + color: var(--gold); + font-weight: 600; + display: block; + margin-bottom: 6px; +} + +.challenge-desc-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.challenge-tag { + font-size: 0.8rem; + padding: 4px 10px; + border-radius: 5px; + font-weight: 600; +} + +.tag-ae { + background: rgba(139, 195, 74, 0.2); + color: #8bc34a; + border: 1px solid rgba(139, 195, 74, 0.3); +} + +.tag-mfl { + background: rgba(100, 181, 246, 0.2); + color: #64b5f6; + border: 1px solid rgba(100, 181, 246, 0.3); +} + +.tag-sdg { + background: rgba(255, 213, 79, 0.2); + color: #ffd54f; + border: 1px solid rgba(255, 213, 79, 0.3); +} + +/* ─── Mid Row ─── */ +.mid-row { + display: flex; + gap: 8px; + align-items: stretch; + flex-shrink: 0; +} + +.project-zone { + flex: 1; + min-width: 200px; + background: var(--surface-glass); + border-radius: var(--radius); + padding: 20px 12px; + text-align: center; + border: 1px solid var(--surface2); + backdrop-filter: blur(8px); +} + +.project-row { + display: flex; + gap: 10px; + justify-content: center; + align-items: center; + flex-wrap: wrap; + padding: 8px 0; +} + +.project-row-sep { + width: 2px; + height: 50px; + background: var(--surface2); + border-radius: 2px; + flex-shrink: 0; +} + +/* ─── AI Panel ─── */ +.ai-players { + display: flex; + flex-direction: column; + gap: 6px; + min-width: 110px; + flex: 0 0 auto; + justify-content: center; +} + +.ai-player { + background: var(--surface-glass); + border-radius: 8px; + padding: 6px 10px; + text-align: center; + border: 1px solid var(--surface2); + backdrop-filter: blur(8px); + transition: all 0.3s ease; +} + +.ai-player.ai-winner { + border-color: var(--gold); + box-shadow: 0 0 12px rgba(255, 213, 79, 0.2); +} + +.player-name { + font-weight: 700; + color: var(--accent); + font-size: 0.85rem; +} + +.player-name.winner-text { + color: var(--gold) !important; +} + +.player-stats { + font-size: 0.65rem; + color: var(--text-dim); + margin-top: 2px; +} + +.match-info { + color: var(--gold); + font-weight: 600; +} + +.backup-info { + font-size: 0.6rem; + font-style: italic; +} + +/* ─── Hand Zone ─── */ +.hand-zone { + flex: 1; + min-height: 0; + background: var(--surface-glass); + border-radius: var(--radius); + padding: 20px 12px 10px; + text-align: center; + position: relative; + z-index: 2; + border: 1px solid var(--surface2); + backdrop-filter: blur(8px); + overflow: visible; + display: flex; + flex-direction: column; +} + +/* ─── How to Play ─── */ +.how-to-play { + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + z-index: 20; + display: flex; + align-items: center; + pointer-events: none; +} + +.how-to-play-toggle { + pointer-events: all; + writing-mode: vertical-rl; + text-orientation: mixed; + background: var(--surface2); + border: 1px solid var(--surface); + border-left: none; + border-radius: 0 8px 8px 0; + padding: 12px 6px; + cursor: pointer; + font-size: 0.7rem; + font-weight: 700; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 1px; + transition: all 0.2s ease; + user-select: none; +} + +.how-to-play-toggle:hover { + background: var(--accent); + color: #1a2a1a; +} + +.how-to-play-open .how-to-play-toggle { + background: var(--accent); + color: #1a2a1a; +} + +.how-to-play-panel { + pointer-events: all; + background: var(--surface-glass); + backdrop-filter: blur(12px); + border: 1px solid var(--surface2); + border-radius: 0 12px 12px 0; + padding: 16px 18px; + max-width: 320px; + margin-left: -1px; + animation: slideRight 0.25s ease-out; +} + +.how-to-play-text { + font-size: 0.85rem; + color: var(--text); + line-height: 1.6; + margin: 0; +} + +@keyframes slideRight { + from { opacity: 0; transform: translateX(-12px); } + to { opacity: 1; transform: translateX(0); } +} + +.hand-zone-logo { + position: absolute; + bottom: 8px; + height: 48px; + opacity: 0.5; + pointer-events: none; + z-index: 1; +} + +.hand-zone-logo-left { + left: 10px; +} + +.hand-zone-logo-right { + right: 10px; +} + +/* ─── Backup Choice Panel ─── */ +.backup-choice-panel { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; + padding: 8px; + animation: fadeIn 0.3s ease-out; +} + +.backup-header { + font-size: 1.2rem; + font-weight: 700; + color: var(--gold); +} + +.backup-hint { + font-size: 0.85rem; + color: var(--text-dim); + text-align: center; + max-width: 320px; +} + +.backup-card-display { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; +} + +.backup-card-display .flip-inner { + width: var(--card-w); + height: var(--card-h); + position: relative; + transform-style: preserve-3d; +} + +.backup-card-name { + font-size: 0.8rem; + color: var(--text); + font-weight: 600; +} + +.backup-slot-grid { + display: flex; + gap: 6px; + flex-wrap: wrap; + justify-content: center; +} + +.backup-slot-grid .btn { + font-size: 0.75rem; + padding: 6px 12px; +} + +.backup-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; + justify-content: center; +} + +.zone-label { + font-size: 0.75rem; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 1px; + text-align: center; + position: absolute; + top: 4px; + left: 50%; + transform: translateX(-50%); +} + +.hand-row { + display: flex; + gap: 8px; + justify-content: center; + align-items: center; + flex: 1; + min-height: 0; + padding-top: 4px; + position: relative; +} + +.hand-card { + cursor: pointer; + position: relative; + z-index: 1; +} + +.hand-card:not(.disabled) { + transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.hand-card.selected { + border-color: var(--gold); + box-shadow: 0 0 24px rgba(255, 213, 79, 0.5); + transform: translateY(-8px) scale(1.05); +} + +@media (hover: hover) { + .hand-card:not(.disabled):hover { + transform: scale(2); + box-shadow: 0 16px 48px rgba(0, 0, 0, 0.7); + z-index: 100; + border-color: var(--accent2); + border-width: 3px; + } + .hand-card.selected:hover { + transform: translateY(-8px) scale(2); + } +} + +@media (hover: none) and (pointer: coarse) { + .hand-card:not(.disabled):active { + transform: scale(1.6); + z-index: 100; + border-color: var(--accent2); + border-width: 3px; + transition: 0.1s; + } + .hand-card.tapped-preview { + transform: scale(2); + z-index: 100; + border-color: var(--accent2); + border-width: 3px; + } +} + +.hand-card.disabled { + cursor: default; + opacity: 0.6; +} + +.card-glow { + position: absolute; + inset: -4px; + border-radius: 10px; + background: radial-gradient(circle at center, rgba(255, 213, 79, 0.25), transparent 70%); + pointer-events: none; + animation: glowPulse 1.5s ease-in-out infinite; +} + +.hand-placeholder { + color: var(--text-dim); + font-size: 0.9rem; + flex: 1; + display: flex; + align-items: center; + justify-content: center; +} + +/* ─── Card Preview Bar ─── */ +.card-preview-bar { + flex-shrink: 0; + margin-top: auto; + padding: 8px 14px; + background: var(--surface); + border: 1px solid var(--surface2); + border-radius: 8px; + text-align: center; + animation: fadeIn 0.2s ease-out; + min-height: 36px; +} + +.preview-name { + font-weight: 700; + color: var(--gold); + font-size: 0.85rem; + display: block; + margin-bottom: 2px; +} + +.preview-desc { + font-size: 0.75rem; + color: var(--text-dim); + line-height: 1.3; + display: block; +} + +/* ─── Action Bar ─── */ +.action-bar { + display: flex; + gap: 8px; + justify-content: center; + flex-shrink: 0; + padding: 4px 0; +} + +/* ─── Status Bar ─── */ +.status-bar { + text-align: center; + font-size: 0.8rem; + color: var(--text-dim); + min-height: 1.2em; + padding: 6px 12px; + background: var(--surface-glass); + border-radius: 8px; + border: 1px solid var(--surface2); + backdrop-filter: blur(8px); + transition: all 0.3s ease; + flex-shrink: 0; +} + +.status-bar.highlight { + color: var(--gold); + font-weight: 700; +} + +.status-bar.sudden { + color: var(--danger); + font-weight: 700; + animation: pulse 0.6s ease-in-out 3; + border-color: var(--danger); +} + +/* ─── Turn Banner ─── */ +.turn-banner { + position: fixed; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: 200; + background: rgba(0, 0, 0, 0.6); + animation: fadeIn 0.3s ease-out; + pointer-events: none; +} + +.turn-banner-inner { + font-size: 3rem; + font-weight: 800; + color: var(--gold); + text-shadow: 0 0 40px rgba(255, 213, 79, 0.4); + animation: bannerIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); + background: rgba(26, 42, 26, 0.9); + padding: 24px 48px; + border-radius: 16px; + border: 2px solid var(--gold); + letter-spacing: 4px; +} + +/* ─── Challenge Reveal Overlay ─── */ +.challenge-reveal-overlay { + position: fixed; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: 250; + background: rgba(0, 0, 0, 0.7); + animation: fadeIn 0.4s ease-out; + pointer-events: none; +} + +.challenge-reveal-inner { + text-align: center; +} + +.reveal-intro { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; +} + +.reveal-label { + font-size: 3.5rem; + font-weight: 800; + color: var(--gold); + text-shadow: 0 0 60px rgba(255, 213, 79, 0.5); + animation: revealScaleIn 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); + letter-spacing: 6px; +} + +.reveal-dots { + display: flex; + gap: 8px; +} + +.reveal-dots span { + font-size: 4rem; + font-weight: 800; + color: var(--gold); + animation: dotBounce 1.2s ease-in-out infinite; +} + +.reveal-dots span:nth-child(2) { animation-delay: 0.4s; } +.reveal-dots span:nth-child(3) { animation-delay: 0.8s; } + +.reveal-count { + font-size: 8rem; + font-weight: 900; + color: var(--accent); + text-shadow: 0 0 80px rgba(139, 195, 74, 0.6); + animation: countPop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +@keyframes revealScaleIn { + 0% { opacity: 0; transform: scale(0.5); } + 100% { opacity: 1; transform: scale(1); } +} + +@keyframes dotBounce { + 0%, 100% { opacity: 0.3; transform: translateY(0); } + 50% { opacity: 1; transform: translateY(-12px); } +} + +@keyframes countPop { + 0% { opacity: 0; transform: scale(0.3) rotate(-10deg); } + 60% { transform: scale(1.2) rotate(3deg); } + 100% { opacity: 1; transform: scale(1) rotate(0deg); } +} + +/* ─── Sudden Solve Panel ─── */ +.sudden-solve-panel { + padding: 8px; + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; +} + +.ss-header { + font-size: 1.1rem; + font-weight: 700; + color: var(--danger); + animation: pulse 0.6s ease-in-out 4; +} + +.ss-animating { + color: var(--text-dim); + padding: 16px; +} + +.ss-backup-choice p, .ss-backup-slot-choice p { + margin-bottom: 10px; + color: var(--gold); +} + +.ss-buttons, .ss-slot-grid { + display: flex; + gap: 8px; + justify-content: center; + flex-wrap: wrap; +} + +.ss-hint { + font-size: 0.85rem; + color: var(--text-dim); + margin-bottom: 8px; +} + +.ss-card-row { + display: flex; + gap: 8px; + justify-content: center; + flex-wrap: wrap; + margin-bottom: 8px; +} + +/* ─── End Screen ─── */ +.end-screen { + height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 20px; + animation: fadeIn 0.5s ease-out; +} + +.end-screen.celebrating { + position: relative; +} + +.celebration-overlay { + position: fixed; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 300; + background: rgba(0, 0, 0, 0.55); + animation: fadeIn 0.5s ease-out; + pointer-events: none; +} + +.confetti-container { + position: absolute; + inset: 0; + overflow: hidden; + pointer-events: none; +} + +.confetti-piece { + position: absolute; + top: -10px; + width: 10px; + height: 10px; + border-radius: 2px; + animation: confettiFall linear forwards; +} + +@keyframes confettiFall { + 0% { transform: translateY(-10px) rotate(0deg) scale(1); opacity: 1; } + 100% { transform: translateY(100vh) rotate(720deg) scale(0.5); opacity: 0; } +} + +.celebration-text { + text-align: center; + animation: bannerIn 0.8s cubic-bezier(0.34, 1.56, 0.64, 1); + background: rgba(26, 42, 26, 0.92); + padding: 32px 48px; + border-radius: 20px; + border: 2px solid var(--gold); + max-width: 520px; +} + +.celebration-title { + font-size: 2.5rem; + font-weight: 800; + color: var(--gold); + text-shadow: 0 0 40px rgba(255, 213, 79, 0.4); + margin-bottom: 12px; + letter-spacing: 4px; +} + +.celebration-message { + font-size: 1.1rem; + color: var(--text); + line-height: 1.6; +} + +.end-title { + font-size: 2.2rem; + color: var(--gold); + margin-bottom: 24px; + animation: slideUp 0.5s ease-out; +} + +.results-container { + max-width: 480px; + width: 100%; + margin-bottom: 20px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.result-card { + background: var(--surface-glass); + border: 1px solid var(--surface2); + border-radius: 12px; + padding: 14px 18px; + text-align: center; + backdrop-filter: blur(8px); + animation: slideUp 0.4s ease-out both; + position: relative; + transition: all 0.3s ease; +} + +.result-card:nth-child(1) { animation-delay: 0.1s; } +.result-card:nth-child(2) { animation-delay: 0.2s; } + +.result-card.winner { + border-color: var(--gold); + box-shadow: 0 0 30px rgba(255, 213, 79, 0.15); +} + +.result-rank { + font-size: 1.5rem; + position: absolute; + top: -12px; + right: -8px; +} + +.result-name { + font-size: 1.1rem; + font-weight: 700; + color: var(--accent); +} + +.winner .result-name { + color: var(--gold); +} + +.result-score { + font-size: 1.5rem; + color: var(--gold); + margin: 2px 0; + font-weight: 700; +} + +.result-detail { + font-size: 0.85rem; + color: var(--text-dim); +} + +.result-cards { + font-size: 0.75rem; + color: var(--text-dim); + margin-top: 4px; + opacity: 0.7; +} + +/* ─── Tracking Sheet ─── */ +.tracking-sheet { + background: var(--surface-glass); + border: 1px solid var(--surface2); + border-radius: 12px; + padding: 12px 16px; + margin-bottom: 12px; + backdrop-filter: blur(8px); + max-width: 480px; + width: 100%; + animation: slideUp 0.4s ease-out; +} + +.tracking-title { + font-size: 0.8rem; + color: var(--gold); + text-transform: uppercase; + letter-spacing: 1px; + text-align: center; + margin-bottom: 8px; + font-weight: 600; +} + +.tracking-grid { + display: flex; + flex-direction: column; + gap: 2px; +} + +.tracking-header, .tracking-row { + display: grid; + grid-template-columns: 60px 1fr 1fr; + text-align: center; + gap: 4px; +} + +.tracking-cell { + font-size: 0.8rem; + padding: 4px 6px; + color: var(--text-dim); +} + +.tracking-header .tracking-cell { + font-weight: 700; + color: var(--text); + font-size: 0.75rem; + text-transform: uppercase; +} + +.tracking-row.tracking-current { + background: rgba(139, 195, 74, 0.1); + border-radius: 4px; +} + +.tracking-row.tracking-total { + border-top: 1px solid var(--surface2); + margin-top: 2px; + padding-top: 2px; +} + +.tracking-row.tracking-total .tracking-cell { + font-weight: 700; + color: var(--gold); +} + +.tracking-cell.tracking-points { + color: var(--accent); + font-weight: 700; +} + +.series-result { + font-size: 1.2rem; + font-weight: 800; + color: var(--gold); + text-align: center; + margin-bottom: 12px; + text-shadow: 0 0 30px rgba(255, 213, 79, 0.3); + animation: slideUp 0.5s ease-out; +} + +.end-buttons { + display: flex; + gap: 10px; + justify-content: center; + margin-top: 4px; +} + +/* ─── Animations ─── */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slideUp { + from { opacity: 0; transform: translateY(24px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes float { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-6px); } +} + +@keyframes glowPulse { + 0%, 100% { box-shadow: 0 0 8px rgba(139, 195, 74, 0.3); } + 50% { box-shadow: 0 0 20px rgba(139, 195, 74, 0.5); } +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} + +@keyframes iconAppear { + from { opacity: 0; transform: scale(0.5) rotate(-10deg); } + to { opacity: 1; transform: scale(1) rotate(0deg); } +} + +@keyframes cardReveal { + from { opacity: 0; transform: rotateY(90deg) scale(0.8); } + to { opacity: 1; transform: rotateY(0) scale(1); } +} + +@keyframes cardSlideIn { + from { opacity: 0; transform: translateY(-60px) scale(0.7); } + to { opacity: 1; transform: translateY(0) scale(1); } +} + +@keyframes slotFill { + from { transform: scale(0.7); opacity: 0; } + to { transform: scale(1); opacity: 1; } +} + +@keyframes bannerIn { + from { transform: scale(0.5); opacity: 0; } + to { transform: scale(1); opacity: 1; } +} + +.card-reveal { + animation: cardReveal 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) both; +} + +.card-flip { + animation: cardReveal 0.4s ease-out both; +} + +.card-slide-in { + animation: cardSlideIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both; +} + +/* ─── Responsive ─── */ +@media (max-width: 1000px) { + .icon-img { width: 96px; height: 96px; } + .reveal-label { font-size: 2.8rem; } + .reveal-count { font-size: 6rem; } +} + +@media (max-width: 900px) { + :root { + --card-w: 110px; + --card-h: 154px; + } + .icon-img { width: 80px; height: 80px; } + .game-screen { padding: 6px 8px; gap: 4px; } +} + +@media (max-width: 600px) { + :root { + --card-w: 90px; + --card-h: 126px; + } + .icon-img { width: 64px; height: 64px; } + .icon-panel { min-width: 70px; gap: 4px; } + .btn { padding: 8px 16px; font-size: 0.85rem; } + .mid-row { flex-direction: column; } + .ai-players { flex-direction: row; justify-content: center; } + .ai-player { flex: 1; min-width: 100px; } + .turn-banner-inner { font-size: 1.8rem; padding: 14px 28px; } + .reveal-label { font-size: 2rem; } + .reveal-count { font-size: 4.5rem; } +} + +@media (max-width: 400px) { + :root { + --card-w: 72px; + --card-h: 100px; + } + .icon-img { width: 32px; height: 32px; } + .hand-card.clickable:active { transform: scale(1.5); } + .reveal-label { font-size: 1.5rem; letter-spacing: 3px; } + .reveal-count { font-size: 3.5rem; } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..5eeaaf4 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,85 @@ +export interface Card { + id: number; + name: string; + action_text: string; + description: string; + ae: number[]; + mfl: number[]; + sdgs: number[]; +} + +export interface Player { + name: string; + isHuman: boolean; + hand: Card[]; + projectZone: Card[]; + backupCard: Card | null; + usedReplace: boolean; + points: number; + hasActed: boolean; + matchInfo: MatchInfo | null; +} + +export interface MatchInfo { + matched: number; + total: number; +} + +export interface MatchRecord { + scores: number[]; + winnerIndex: number | null; + collaborativeWin: boolean; +} + +export type MatchMode = 'single' | 'bo2' | 'bo3'; + +export type GamePhase = + | 'menu' + | 'setup' + | 'replace' + | 'play_turn' + | 'end_turn_prompt' + | 'turn_transition' + | 'sudden_solve' + | 'scoring' + | 'backup_choice' + | 'done'; + +export interface ModalConfig { + title: string; + body: string; + buttons: { text: string; value: string; accent?: boolean }[]; +} + +export interface GameState { + phase: GamePhase; + deck: Card[]; + challenge: Card | null; + players: Player[]; + currentPlayer: number; + round: number; + selectedCard: number | null; + suddenSolveActive: boolean; + gameOver: boolean; + modal: ModalConfig | null; + modalResolve: ((value: string) => void) | null; + statusMessage: string; + statusClass: string; + animatingCard: { card: Card; from: 'hand' | 'deck'; to: 'project' | 'challenge' } | null; +} + +export type GameAction = + | { type: 'START_GAME'; aiCount: number } + | { type: 'REPLACE_HAND' } + | { type: 'KEEP_HAND' } + | { type: 'SELECT_CARD'; index: number | null } + | { type: 'PLAY_CARD' } + | { type: 'END_TURN' } + | { type: 'AI_PLAY_CARD'; playerIndex: number; cardIndex: number } + | { type: 'NEXT_PHASE' } + | { type: 'SET_STATUS'; message: string; cls?: string } + | { type: 'SUDDEN_SOLVE_PLAY'; cardIndex: number } + | { type: 'SUDDEN_SOLVE_SKIP' } + | { type: 'USE_BACKUP'; slotIndex?: number } + | { type: 'SKIP_BACKUP' } + | { type: 'DISMISS_MODAL' }; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..17f43b1 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..42872c5 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..0466183 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], +});