👁️
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

at dev 187 lines 5.4 kB view raw
1import { useCallback, useEffect, useMemo, useState } from "react"; 2import type { DeckCard } from "@/lib/deck-types"; 3import type { Card, ScryfallId } from "@/lib/scryfall-types"; 4import { useSeededRandom } from "@/lib/useSeededRandom"; 5import * as engine from "./engine"; 6import type { GameState, Zone } from "./types"; 7 8export interface GoldfishActions { 9 draw: () => void; 10 mulligan: () => void; 11 reset: () => void; 12 tap: (instanceId: number) => void; 13 untap: (instanceId: number) => void; 14 toggleTap: (instanceId: number) => void; 15 untapAll: () => void; 16 cycleFace: (instanceId: number, maxFaces: number) => void; 17 toggleFaceDown: (instanceId: number) => void; 18 flipCard: (instanceId: number, maxFaces: number) => void; 19 moveCard: ( 20 instanceId: number, 21 toZone: Zone, 22 options?: { position?: { x: number; y: number }; faceDown?: boolean }, 23 ) => void; 24 setHoveredCard: (instanceId: number | null) => void; 25 addCounter: ( 26 instanceId: number, 27 counterType: string, 28 amount?: number, 29 ) => void; 30 removeCounter: ( 31 instanceId: number, 32 counterType: string, 33 amount?: number, 34 ) => void; 35 setCounter: (instanceId: number, counterType: string, value: number) => void; 36 clearCounters: (instanceId: number) => void; 37 adjustLife: (amount: number) => void; 38 adjustPoison: (amount: number) => void; 39} 40 41export interface UseGoldfishGameOptions { 42 startingLife?: number; 43 cardLookup?: (id: ScryfallId) => Card | undefined; 44} 45 46export interface UseGoldfishGameResult { 47 state: GameState; 48 actions: GoldfishActions; 49 SeedEmbed: () => React.ReactElement; 50} 51 52export function useGoldfishGame( 53 deck: DeckCard[], 54 options: UseGoldfishGameOptions = {}, 55): UseGoldfishGameResult { 56 const { startingLife = 20, cardLookup } = options; 57 const { rng, SeedEmbed } = useSeededRandom(); 58 59 const [state, setState] = useState<GameState>(() => 60 engine.createInitialState(deck, rng, startingLife), 61 ); 62 63 const actions: GoldfishActions = useMemo( 64 () => ({ 65 draw: () => setState((s) => engine.draw(s)), 66 mulligan: () => setState((s) => engine.mulligan(s, rng, startingLife)), 67 reset: () => setState(engine.createInitialState(deck, rng, startingLife)), 68 tap: (id) => setState((s) => engine.tap(s, id)), 69 untap: (id) => setState((s) => engine.untap(s, id)), 70 toggleTap: (id) => setState((s) => engine.toggleTap(s, id)), 71 untapAll: () => setState((s) => engine.untapAll(s)), 72 cycleFace: (id, maxFaces) => 73 setState((s) => engine.cycleFace(s, id, maxFaces)), 74 toggleFaceDown: (id) => setState((s) => engine.toggleFaceDown(s, id)), 75 flipCard: (id, maxFaces) => 76 setState((s) => engine.flipCard(s, id, maxFaces)), 77 moveCard: (id, zone, opts) => 78 setState((s) => engine.moveCard(s, id, zone, opts)), 79 setHoveredCard: (id) => setState((s) => engine.setHoveredCard(s, id)), 80 addCounter: (id, type, amount) => 81 setState((s) => engine.addCounter(s, id, type, amount)), 82 removeCounter: (id, type, amount) => 83 setState((s) => engine.removeCounter(s, id, type, amount)), 84 setCounter: (id, type, value) => 85 setState((s) => engine.setCounter(s, id, type, value)), 86 clearCounters: (id) => setState((s) => engine.clearCounters(s, id)), 87 adjustLife: (amount) => setState((s) => engine.adjustLife(s, amount)), 88 adjustPoison: (amount) => setState((s) => engine.adjustPoison(s, amount)), 89 }), 90 [deck, rng, startingLife], 91 ); 92 93 const getFaceCount = useCallback( 94 (cardId: ScryfallId): number => { 95 if (!cardLookup) return 1; 96 const card = cardLookup(cardId); 97 if (!card) return 1; 98 return card.card_faces?.length ?? 1; 99 }, 100 [cardLookup], 101 ); 102 103 useEffect(() => { 104 function handleKeyDown(e: KeyboardEvent) { 105 if ( 106 e.target instanceof HTMLInputElement || 107 e.target instanceof HTMLTextAreaElement 108 ) { 109 return; 110 } 111 112 const hoveredId = state.hoveredId; 113 114 switch (e.key.toLowerCase()) { 115 case "d": 116 e.preventDefault(); 117 actions.draw(); 118 break; 119 case "u": 120 e.preventDefault(); 121 actions.untapAll(); 122 break; 123 case "t": 124 case " ": 125 if (hoveredId !== null) { 126 e.preventDefault(); 127 actions.toggleTap(hoveredId); 128 } 129 break; 130 case "f": 131 if (hoveredId !== null) { 132 e.preventDefault(); 133 // Include library top so it can be revealed 134 const card = [ 135 ...state.hand, 136 ...state.battlefield, 137 ...state.graveyard, 138 ...state.exile, 139 ...state.library.slice(0, 1), 140 ].find((c) => c.instanceId === hoveredId); 141 if (card) { 142 actions.flipCard(hoveredId, getFaceCount(card.cardId)); 143 } 144 } 145 break; 146 case "g": 147 if (hoveredId !== null) { 148 e.preventDefault(); 149 actions.moveCard(hoveredId, "graveyard"); 150 } 151 break; 152 case "e": 153 if (hoveredId !== null) { 154 e.preventDefault(); 155 actions.moveCard(hoveredId, "exile"); 156 } 157 break; 158 case "h": 159 if (hoveredId !== null) { 160 e.preventDefault(); 161 actions.moveCard(hoveredId, "hand"); 162 } 163 break; 164 case "b": 165 if (hoveredId !== null) { 166 e.preventDefault(); 167 actions.moveCard(hoveredId, "battlefield"); 168 } 169 break; 170 } 171 } 172 173 window.addEventListener("keydown", handleKeyDown); 174 return () => window.removeEventListener("keydown", handleKeyDown); 175 }, [ 176 state.hoveredId, 177 state.hand, 178 state.library, 179 state.battlefield, 180 state.graveyard, 181 state.exile, 182 actions, 183 getFaceCount, 184 ]); 185 186 return { state, actions, SeedEmbed }; 187}