For the complete documentation index, see llms.txt. This page is also available as Markdown.

Ch 3: Game UI Components

In this chapter, you'll build the game interface — the clown sprite with punch animations, action buttons, and the entry screen. Estimated time: ~15 minutes

ShowClown: Animated clown sprite

The clown sprite changes appearance based on how many times it's been hit. Create src/components/game/ShowClown.tsx:

import { motion, useAnimation } from 'framer-motion'
import { useEffect, useMemo } from 'react'

import { Box } from '@mui/material'

type ClownProps = {
  isKO: boolean
  isShakingAnimation: boolean
  isHittingAnimation: boolean
  punchCount: number
}

const ShowClown: React.FC<ClownProps> = ({
  isKO,
  isShakingAnimation,
  isHittingAnimation,
  punchCount,
}) => {
  const controls = useAnimation()

  useEffect(() => {
    if (isShakingAnimation) {
      controls.start({
        rotate: [0, -5, 5, -5, 5, 0],
        transition: { duration: 0.5 },
      })
    } else if (isHittingAnimation) {
      controls.start({
        scale: [1, 0.9, 1.1, 1],
        transition: { duration: 0.3 },
      })
    }
  }, [isShakingAnimation, isHittingAnimation, controls])

  // Select the appropriate clown image based on punch count and KO state
  // Using useMemo to prevent recalculating on every render
  const clownImage = useMemo(() => {
    // If shaking, show the shaking clown image
    if (isShakingAnimation) {
      return '/clown_shaking.png'
    }

    if (isKO) {
      return '/clownko.png'
    }

    let imagePath
    switch (punchCount) {
      case 0:
        imagePath = '/clown1.png'
        break
      case 1:
        imagePath = '/clown2.png'
        break
      case 2:
      case 3:
        imagePath = '/clown3.png'
        break
      default:
        imagePath = '/clown1.png'
    }

    return imagePath
  }, [isKO, isShakingAnimation, punchCount])

  return (
    <Box
      sx={{
        width: { xs: '70%', sm: '70%', md: '80%', lg: '80%', xl: '100%' },
        height: { xs: '100%', sm: '70%', md: '80%', lg: '80%', xl: '100%' },
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
      }}
    >
      <div className="relative">
        <motion.div animate={controls} className="relative">
          <img
            src={clownImage}
            alt="Clown"
            style={{
              maxWidth: '100%',
              height: 'auto',
              objectFit: 'contain',
            }}
          />
        </motion.div>
      </div>
    </Box>
  )
}

export default ShowClown

The sprite progression creates visual feedback as the clown takes damage:

  • 0 hitsclown1.png (full health)

  • 1 hitclown2.png (damaged)

  • 2–3 hitsclown3.png (heavily damaged)

  • KOclownko.png (knocked out)

  • Shakingclown_shaking.png (mid-animation)

Framer Motion's useAnimation hook controls two animations: a shake (rotation) when the clown gets hit, and a scale punch effect for impact feedback.

ButtonContainer: Action buttons

The button container renders the game's action buttons — hit, rob, and reset. Create src/components/game/ButtonContainer.tsx:

The button layout adapts based on game state and screen size:

  • Desktop — rob button on the left, hit/reset on the right

  • Mobile — both buttons side by side below the clown

  • Clown standing — show Hit button (right)

  • Clown KO — show Reset button (right), Rob button now callable (left)

ClownPuncher: Main game component

This component ties everything together. Create src/components/game/ClownPuncher.tsx:

EntryScreen: Wallet connection

The entry screen prompts the user to connect their wallet before playing. Create src/components/game/EntryScreen.tsx:

Clicking the logo opens the RainbowKit wallet connection modal. Once authenticated, the onEnter callback fires and the ClownPuncher component takes over.

WalletConnectButton: Auth context

Create the auth context and wallet button used throughout the app. Create src/components/chain/WalletConnectButton.tsx:

Running the frontend

Start the frontend dev server:

Make sure sanvil is running and the contract is deployed (see Deploying). Open http://localhost:5173 in your browser, connect your wallet, and start punching the clown!

Game flow recap

  1. Connect wallet — RainbowKit modal, ShieldedWalletProvider derives shielded keys

  2. Hit the clowntwrite.hit() sends a shielded transaction, stamina decrements

  3. Clown KO — stamina reaches 0, sprite changes to clownko.png

  4. Rob a secretread.rob() performs a signed read, secret is decrypted and displayed

  5. Resettwrite.reset() restores stamina and picks a new random secret for the next round

Congratulations! You've built a complete Seismic dApp — from smart contract to CLI to web frontend.

Last updated