From 5d0c1dba7e2396a1a4d9b002d284a30d6b26fdd1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 9 Nov 2025 22:51:53 +0000 Subject: [PATCH 1/2] feat: Add PuppetMasterConsole component Adds a new component for debugging and controlling Unity interactions. Co-authored-by: edd --- src/App.tsx | 4 +- src/components/PuppetMasterConsole.tsx | 1018 ++++++++++++++++++++++++ 2 files changed, 1021 insertions(+), 1 deletion(-) create mode 100644 src/components/PuppetMasterConsole.tsx diff --git a/src/App.tsx b/src/App.tsx index b0dfecf..5734846 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,6 +22,7 @@ import DisclaimerModal from "./components/DisclaimerModal"; import { useContractWrapper } from "./context/contractWrapperContext"; import FightingWatersWeak from "./components/FightingWatersWeak"; import FightingWatersNonLethal from "./components/FightingWatersNonLethal"; +import PuppetMasterConsole from "./components/PuppetMasterConsole"; type RenderProps = { hours: any; @@ -56,11 +57,12 @@ const App = () => { {/* } /> */} } /> - } /> + } /> }> {/* } /> */} {/* } /> */} + } /> diff --git a/src/components/PuppetMasterConsole.tsx b/src/components/PuppetMasterConsole.tsx new file mode 100644 index 0000000..525771a --- /dev/null +++ b/src/components/PuppetMasterConsole.tsx @@ -0,0 +1,1018 @@ +import { useEffect, useMemo, useState } from "react"; +import styled from "styled-components"; + +import { + BaseContainer, + BaseText, + BaseTitle, + BaseButtonStyle, + UIContainer, +} from "./BaseStyles"; +import { useUnity } from "../context/unityContext"; +import { useFishPool } from "../context/fishPoolContext"; +import { Fish } from "../utils/fish"; +import { Fight } from "../utils/fight"; + +type PuppetCommand = { + functionName: string; + sends: string[]; + notes?: string; +}; + +const defaultFightPayload = JSON.stringify( + { + typeOfFight: 0, + fishChallenger: 0, + fishOpponent: 0, + timeOfFight: Math.floor(Date.now() / 1000), + round1: { value: 0, description: "Strength" }, + round2: { value: 1, description: "Intelligence" }, + round3: { value: 2, description: "Agility" }, + winner: 0, + playerResult: 0, + }, + null, + 2 +); + +const commandReference: PuppetCommand[] = [ + { + functionName: "fishCaught(fish)", + sends: ['CanvasUserInterface.FishCaught(JSON.stringify(fish))'], + notes: "Show caught fish results UI", + }, + { + functionName: "showFishingLocation()", + sends: [ + 'Camera.SetAnimState("ShowFishing")', + 'CanvasUserInterface.SetAnimState("ShowFishing")', + ], + }, + { + functionName: "showFightingLocation()", + sends: ['Camera.SetAnimState("ShowFighting")'], + notes: "Canvas UI fight animation currently commented out", + }, + { + functionName: "showBreedingLocation()", + sends: ['Camera.SetAnimState("ShowBreeding")'], + }, + { + functionName: "showOceanLocation()", + sends: [ + 'Camera.SetAnimState("ShowOcean")', + 'CanvasUserInterface.SetAnimState("ShowOcean")', + ], + }, + { + functionName: "showHome()", + sends: [ + 'Camera.SetAnimState("ShowHome")', + 'CanvasUserInterface.SetAnimState("ShowHome")', + ], + }, + { + functionName: "showTank()", + sends: [ + 'Camera.SetAnimState("ShowOcean")', + 'CanvasUserInterface.SetAnimState("ShowOcean")', + ], + }, + { + functionName: "showFishUI()", + sends: ['CanvasUserInterface.SetAnimState("ShowFish")'], + }, + { + functionName: "showFishingUI()", + sends: ['CanvasUserInterface.SetAnimState("ShowFishing")'], + }, + { + functionName: "showFightingUI()", + sends: ['CanvasUserInterface.SetAnimState("ShowFighting")'], + }, + { + functionName: "showBreedingUI()", + sends: ['CanvasUserInterface.SetAnimState("ShowBreeding")'], + }, + { + functionName: "clearUIFish()", + sends: ["FishPool.ClearUIFish()"], + }, + { + functionName: "hideUI()", + sends: ['CanvasUserInterface.SetAnimState("Hide")'], + }, + { + functionName: "clearFishPool(pool)", + sends: ["FishPool.ClearPool(pool)"], + notes: 'Known pools: "Ocean", "Fighting", "Breeding", "Fishing", "Fish", "ShowFighting", "showOceanLocation"', + }, + { + functionName: "addFishOcean(fish)", + sends: ['FishPool.AddFish_OceanView(JSON.stringify(fish))'], + }, + { + functionName: "addFishTank(fish)", + sends: ['FishPool.AddFish_TankView(JSON.stringify(fish))'], + }, + { + functionName: "addFishFightingPool(fish)", + sends: ['FishPool.AddFish_FightingView(JSON.stringify(fish))'], + }, + { + functionName: "addFishBreedingPool(fish)", + sends: ['FishPool.AddFish_BreedingView(JSON.stringify(fish))'], + }, + { + functionName: "addFishFight1(fish)", + sends: [ + 'FishPool.AddFish1_FightingView(JSON.stringify(fish))', + 'FishPool.AddFish1_FishView(JSON.stringify(fish))', + 'CanvasUserInterface.FightingUI_SetFish1(JSON.stringify(fish))', + ], + }, + { + functionName: "addFishFight2(fish)", + sends: [ + 'FishPool.AddFish2_FightingView(JSON.stringify(fish))', + 'FishPool.AddFish2_FishView(JSON.stringify(fish))', + 'CanvasUserInterface.FightingUI_SetFish2(JSON.stringify(fish))', + ], + }, + { + functionName: "addFishBreed1(fish)", + sends: [ + 'FishPool.AddFish1_FishView(JSON.stringify(fish))', + 'CanvasUserInterface.BreedingUI_SetFish1(JSON.stringify(fish))', + ], + }, + { + functionName: "addFishBreed2(fish)", + sends: [ + 'FishPool.AddFish2_FishView(JSON.stringify(fish))', + 'CanvasUserInterface.BreedingUI_SetFish2(JSON.stringify(fish))', + ], + }, + { + functionName: "addBreedOffspring(fish)", + sends: [ + 'CanvasUserInterface.SetAnimState("ShowBreedingResultsSuccess")', + 'CanvasUserInterface.BreedingResultsUI_SetFish1(JSON.stringify(fish))', + 'FishPool.AddFish2_FishView(JSON.stringify(fish.parentAFish))', + 'FishPool.AddFish3_FishView(JSON.stringify(fish.parentBFish))', + 'FishPool.AddFish1_FishView(JSON.stringify(fish))', + ], + notes: "Parents only sent if available", + }, + { + functionName: "addFishFishing(fish)", + sends: [ + 'FishPool.AddFish_FishingView(JSON.stringify(fish))', + 'FishPool.AddFish1_FishView(JSON.stringify(fish))', + 'CanvasUserInterface.SetAnimState("ShowFishingResultsSuccess")', + 'CanvasUserInterface.FishingResultsUI_SetFish1(JSON.stringify(fish))', + ], + }, + { + functionName: "showFish(fish)", + sends: [ + 'CanvasUserInterface.FishUI_SetFish1(JSON.stringify(fish))', + 'FishPool.AddFish2_FishView(JSON.stringify(fish.parentAFish))', + 'FishPool.AddFish3_FishView(JSON.stringify(fish.parentBFish))', + 'FishPool.AddFish1_FishView(JSON.stringify(fish))', + ], + notes: "Parent fish only sent when available", + }, + { + functionName: "sendRound(round, stat)", + sends: [ + "FishPool.SetRound1Stat(roundStat)", + "FishPool.SetRound2Stat(roundStat)", + "FishPool.SetRound3Stat(roundStat)", + ], + notes: "Uses SetRound{1,2,3}Stat based on round argument", + }, + { + functionName: "sendFightResult(fight, fish1, fish2)", + sends: [ + "FishPool.SetFightResults(JSON.stringify(fight))", + 'CanvasUserInterface.SetAnimState("ShowFightResultsSuccess")', + 'CanvasUserInterface.FightingResultsUI_SetFish1(JSON.stringify(fish1))', + 'CanvasUserInterface.FightingResultsUI_SetFish2(JSON.stringify(fish2))', + ], + }, + { + functionName: "sendTie()", + sends: ["FishPool.SetTie()"], + }, +]; + +const knownPools = [ + "Ocean", + "Fighting", + "Breeding", + "Fishing", + "Fish", + "ShowFighting", + "showOceanLocation", + "ShowBreeding", +]; + +const PuppetMasterConsole = () => { + const unity = useUnity(); + const { + oceanFish, + userFish, + fightingFish, + fightingFishWeak, + fightingFishNonLethal, + breedingFish, + } = useFishPool(); + + const [primaryFishId, setPrimaryFishId] = useState(); + const [secondaryFishId, setSecondaryFishId] = useState(); + const [tertiaryFishId, setTertiaryFishId] = useState(); + const [poolName, setPoolName] = useState(knownPools[0]); + const [roundNumber, setRoundNumber] = useState(1); + const [roundStat, setRoundStat] = useState(0); + const [fightPayload, setFightPayload] = useState(defaultFightPayload); + const [consoleLog, setConsoleLog] = useState([]); + + const fishOptions = useMemo(() => { + const dedup = new Map(); + [ + ...oceanFish, + ...userFish, + ...fightingFish, + ...fightingFishWeak, + ...fightingFishNonLethal, + ...breedingFish, + ].forEach((fish) => { + if (!dedup.has(fish.tokenId)) { + dedup.set(fish.tokenId, fish); + } + }); + return Array.from(dedup.values()).sort((a, b) => a.tokenId - b.tokenId); + }, [ + oceanFish, + userFish, + fightingFish, + fightingFishWeak, + fightingFishNonLethal, + breedingFish, + ]); + + useEffect(() => { + if (fishOptions.length === 0) return; + setPrimaryFishId((prev) => prev ?? fishOptions[0].tokenId); + setSecondaryFishId((prev) => + prev ?? fishOptions[Math.min(1, fishOptions.length - 1)].tokenId + ); + setTertiaryFishId((prev) => + prev ?? fishOptions[Math.min(2, fishOptions.length - 1)].tokenId + ); + }, [fishOptions]); + + const getFishById = (tokenId?: number) => + fishOptions.find((fish) => fish.tokenId === tokenId); + + const primaryFish = getFishById(primaryFishId); + const secondaryFish = getFishById(secondaryFishId); + const tertiaryFish = getFishById(tertiaryFishId); + + const pushLog = (message: string) => { + setConsoleLog((prev) => [ + `${new Date().toLocaleTimeString()} • ${message}`, + ...prev, + ]); + }; + + const guardUnityReady = (actionLabel: string, checkPoolReady = true) => { + if (!unity.isLoaded) { + pushLog(`${actionLabel}: Unity not loaded yet`); + return false; + } + if (checkPoolReady && !unity.isFishPoolReady) { + pushLog(`${actionLabel}: Fish pool not ready`); + return false; + } + return true; + }; + + const handleFishAction = ( + label: string, + fish: Fish | undefined, + handler: (fish: Fish) => void, + requiresPoolReady = true + ) => { + if (!guardUnityReady(label, requiresPoolReady)) return; + if (!fish) { + pushLog(`${label}: Select a fish first`); + return; + } + try { + handler(fish); + pushLog(`${label}: Sent for #${fish.tokenId}`); + } catch (error) { + pushLog(`${label}: ${String(error)}`); + } + }; + + const handleFightResult = () => { + if (!guardUnityReady("sendFightResult")) return; + if (!primaryFish || !secondaryFish) { + pushLog("sendFightResult: Select two fish first"); + return; + } + try { + const parsed = JSON.parse(fightPayload) as Fight; + unity.sendFightResult(parsed, primaryFish, secondaryFish); + pushLog( + `sendFightResult: Sent for challenger #${primaryFish.tokenId} vs opponent #${secondaryFish.tokenId}` + ); + } catch (error) { + pushLog(`sendFightResult: Invalid JSON payload (${String(error)})`); + } + }; + + const handleRoundStat = () => { + if (!guardUnityReady(`sendRound(${roundNumber})`)) return; + unity.sendRound(roundNumber, roundStat); + pushLog(`sendRound: Round ${roundNumber} updated to ${roundStat}`); + }; + + const handleClearPool = () => { + if (!guardUnityReady("clearFishPool")) return; + unity.clearFishPool(poolName); + pushLog(`clearFishPool: Requested clear for "${poolName}"`); + }; + + const poolPlaceholder = + 'Try "Ocean", "Fighting", "Breeding", "Fishing", "Fish"...'; + + return ( + + +
+ Puppetmaster Console + + + Unity Loaded + + {unity.isLoaded ? "Yes" : "No"} + + + + Fish Pool Ready + + {unity.isFishPoolReady ? "Yes" : "No"} + + + + Mounted + + {unity.isUnityMounted ? "Yes" : "No"} + + + + Progress + + {(unity.progression * 100).toFixed(0)}% + + + + { + unity.toggleIsUnityMounted(); + pushLog("toggleIsUnityMounted: Toggled Unity mount state"); + }} + > + Toggle Mount + +
+ +
+ Fish Selection + + + + + + + + + + + + + + + + The dropdown aggregates fish from ocean, user, fighting, breeding, + and fishing pools. Update the pools with the refresh button in the + main menu if your fish list looks stale. + +
+ +
+ Camera & Locations + + { + if (!guardUnityReady("showHome", false)) return; + unity.showHome(); + pushLog("showHome: Requested Home camera view"); + }} + > + Show Home + + { + if (!guardUnityReady("showOceanLocation")) return; + unity.showOceanLocation(); + pushLog("showOceanLocation: Requested Ocean view"); + }} + > + Show Ocean + + { + if (!guardUnityReady("showFishingLocation")) return; + unity.showFishingLocation(); + pushLog("showFishingLocation: Requested Fishing view"); + }} + > + Show Fishing + + { + if (!guardUnityReady("showFightingLocation")) return; + unity.showFightingLocation(); + pushLog("showFightingLocation: Requested Fighting view"); + }} + > + Show Fighting + + { + if (!guardUnityReady("showBreedingLocation")) return; + unity.showBreedingLocation(); + pushLog("showBreedingLocation: Requested Breeding view"); + }} + > + Show Breeding + + { + if (!guardUnityReady("showTank", false)) return; + unity.showTank(); + pushLog("showTank: Requested Tank view"); + }} + > + Show Tank + + + + Camera.SetAnimState("Show*") — see reference chart for exact + strings. + +
+ +
+ UI State Toggles + + { + if (!guardUnityReady("showFishUI", false)) return; + unity.showFishUI(); + pushLog("showFishUI: Showing Fish UI overlay"); + }} + > + Fish UI + + { + if (!guardUnityReady("showFishingUI", false)) return; + unity.showFishingUI(); + pushLog("showFishingUI: Showing Fishing UI overlay"); + }} + > + Fishing UI + + { + if (!guardUnityReady("showFightingUI", false)) return; + unity.showFightingUI(); + pushLog("showFightingUI: Showing Fighting UI overlay"); + }} + > + Fighting UI + + { + if (!guardUnityReady("showBreedingUI", false)) return; + unity.showBreedingUI(); + pushLog("showBreedingUI: Showing Breeding UI overlay"); + }} + > + Breeding UI + + { + if (!guardUnityReady("hideUI", false)) return; + unity.hideUI(); + pushLog("hideUI: Hiding Canvas UI"); + }} + > + Hide UI + + { + if (!guardUnityReady("clearUIFish")) return; + unity.clearUIFish(); + pushLog("clearUIFish: Cleared UI fish slots"); + }} + > + Clear UI Fish + + + CanvasUserInterface.SetAnimState("*") & FishPool.ClearUIFish +
+ +
+ Fish Pool Management + + + + + setPoolName(event.target.value)} + placeholder={poolPlaceholder} + /> + + + + + + Clear Fish Pool + + + handleFishAction( + "addFishOcean", + primaryFish, + unity.addFishOcean + ) + } + > + Add to Ocean View + + + handleFishAction( + "addFishTank", + primaryFish, + unity.addFishTank + ) + } + > + Add to Tank View + + + handleFishAction( + "addFishFightingPool", + primaryFish, + unity.addFishFightingPool + ) + } + > + Add to Fighting Pool + + + handleFishAction( + "addFishBreedingPool", + primaryFish, + unity.addFishBreedingPool + ) + } + > + Add to Breeding Pool + + + + + FishPool.AddFish_* and FishPool.ClearPool(pool) +
+ +
+ Fishing Results & Showcase + + + handleFishAction("fishCaught", primaryFish, unity.fishCaught) + } + > + Trigger Fish Caught + + + handleFishAction("addFishFishing", primaryFish, unity.addFishFishing) + } + > + Add Fishing Result + + + handleFishAction("showFish", primaryFish, unity.showFish) + } + > + Show Fish Detail + + { + if (!guardUnityReady("refreshFishUnity")) return; + if (!primaryFish) { + pushLog("refreshFishUnity: Select a fish first"); + return; + } + unity.refreshFishUnity(primaryFish); + pushLog( + `refreshFishUnity: Refreshed fish #${primaryFish.tokenId}` + ); + }} + > + Refresh Fish Unity + + + + Fish caught & showcase actions rely on CanvasUserInterface.Fish* + setters. + +
+ +
+ Fight Controls + + + handleFishAction("addFishFight1", primaryFish, unity.addFishFight1) + } + > + Set Fighter 1 + + + handleFishAction("addFishFight2", secondaryFish, unity.addFishFight2) + } + > + Set Fighter 2 + + + Send Fight Result + + { + if (!guardUnityReady("sendTie")) return; + unity.sendTie(); + pushLog("sendTie: Declared a tie"); + }} + > + Send Tie + + + + + + + setRoundStat(Number(event.target.value))} + /> + + Update Round Stat + + + + +