Roadmap
Avançado

Text-based MMORPG com JavaScript

Entenda o processo de criação de um jogo MMORPG web com JavaScript

react
nextjs
javascript
prisma
trpc
tailwind

Adicionando itens na interface principal

Depois de todo esse trabalho podemos finalmente terminar o nosso jogo, para isso precisamos tornar os detalhes funcionais na nossa interface principal.

Esse processo é simples mas o trabalho depende de como você prefere que seu design seja, muita gente não gosta de trabalhar com Modal (dialog) que são aquelas janelas que aparecem no meio da tela, e se você gosta de organizar seu código eu recomendo que você evite usar porque acaba dando mais trabalho gerenciar esses componentes.

Eu vou reduzir um pouco o escopo do projeto para que esse tutorial não fique muito grande, mas você pode adicionar mais detalhes se quiser.

Adicionando o ShadCN

Se você nunca ouviu falar o ShadCN é uma biblioteca de componentes que é mantida pela Vercel para ser usada com varios frameworks, como ela não instala dependencias no projeto vamos usar ela pois deixa o nosso projeto mais leve.

Para instalar o ShadCN basta rodar o comando:

Antes recomendo que você faça o backup do arquivo styles/globals.css pois ele vai ser substituido, ou você pode copiar o meu arquivo caso esteja seguindo a risca esse tutorial.

npx shadcn-ui@latest init
Clique aqui para expandir

Caso não saiba como criar os componentes do shadcn basta olhar no site deles que tem a documentação de como criar os componentes.

Depois eu criei um Dialog para ser usado como modal, você pode ver o codigo dele no arquivo components/ui/Dialog.tsx.

npx shadcn-ui@latest add dialog
Clique aqui para expandir
globals.css
@import url("https://fonts.googleapis.com/css2?family=Poor+Story&family=Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap");

body > div {
  height: 100vh;
}

@layer utilities {
  .medieval-font {
    font-family: "Poor Story", system-ui;
  }
  .golden-gradient {
    background: radial-gradient(
        ellipse farthest-corner at right bottom,
        #fedb37 0%,
        #fdb931 8%,
        #9f7928 30%,
        #8a6e2f 40%,
        transparent 80%
      ), radial-gradient(ellipse farthest-corner at left top, #ffffff 0%, #ffffac
          8%, #d1b464 25%, #5d4a1f 62.5%, #5d4a1f 100%);
  }

  .medieval-button {
    padding: 10px 20px;
    background-color: #795649;
    color: #f4e5c3;
    border: 2px solid #c0a080;
    font-family: "Goudy Bookletter 1911", serif;
    font-size: 18px;
    text-shadow: 1px 1px 0px #a67c52;
    box-shadow: 2px 2px 0px #a67c52;
    border-radius: 4px;
    cursor: pointer;
    transition: all 0.3s ease;
  }

  .medieval-button:hover {
    background-color: #8a6e2f;
    box-shadow: 1px 1px 5px 0px rgba(0, 0, 0, 0.3);
  }

  .medieval-button:active {
    box-shadow: inset 1px 1px 2px 0px rgba(0, 0, 0, 0.5);
    transform: translateY(2px);
  }

  .medieval-menu {
    background-color: #162825;
    list-style: none;
  }

  .medieval-menu li {
    margin-bottom: 10px;
    border-radius: 4px;
    transition: background-color 0.3s ease, box-shadow 0.2s ease;
    box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.3);
  }

  .medieval-menu a {
    text-decoration: none;
    color: #af9567;
    display: block;
    padding: 10px 20px;
    transition: color 0.3s ease;
    font-family: "Poor Story", system-ui;
    font-size: 16px;
  }

  .medieval-menu li:hover {
    background-color: #4f4334;
  }

  .medieval-menu li:active {
    box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.2);
  }

  .medieval-menu li:hover a {
    color: #af9567;
  }
}

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 222.2 84% 4.9%;

    --card: 0 0% 100%;
    --card-foreground: 222.2 84% 4.9%;

    --popover: 0 0% 100%;
    --popover-foreground: 222.2 84% 4.9%;

    --primary: 222.2 47.4% 11.2%;
    --primary-foreground: 210 40% 98%;

    --secondary: 210 40% 96.1%;
    --secondary-foreground: 222.2 47.4% 11.2%;

    --muted: 210 40% 96.1%;
    --muted-foreground: 215.4 16.3% 46.9%;

    --accent: 210 40% 96.1%;
    --accent-foreground: 222.2 47.4% 11.2%;

    --destructive: 0 84.2% 60.2%;
    --destructive-foreground: 210 40% 98%;

    --border: 214.3 31.8% 91.4%;
    --input: 214.3 31.8% 91.4%;
    --ring: 222.2 84% 4.9%;

    --radius: 0.5rem;
  }

  .dark {
    --background: 222.2 84% 4.9%;
    --foreground: 210 40% 98%;

    --card: 222.2 84% 4.9%;
    --card-foreground: 210 40% 98%;

    --popover: 222.2 84% 4.9%;
    --popover-foreground: 210 40% 98%;

    --primary: 210 40% 98%;
    --primary-foreground: 222.2 47.4% 11.2%;

    --secondary: 217.2 32.6% 17.5%;
    --secondary-foreground: 210 40% 98%;

    --muted: 217.2 32.6% 17.5%;
    --muted-foreground: 215 20.2% 65.1%;

    --accent: 217.2 32.6% 17.5%;
    --accent-foreground: 210 40% 98%;

    --destructive: 0 62.8% 30.6%;
    --destructive-foreground: 210 40% 98%;

    --border: 217.2 32.6% 17.5%;
    --input: 217.2 32.6% 17.5%;
    --ring: 212.7 26.8% 83.9%;
  }
}

@layer base {
  * {
    @apply border-border;
  }
  body {
    @apply bg-background text-foreground;
  }
}

@tailwind base;
@tailwind components;
@tailwind utilities;
Clique aqui para expandir

Criando um personagem

Primeiro eu adicionei no topo do index.tsx uma interface para o personagem chamada Char.

interface Char {
  id: number;
  name: string;
  level: number;
  user?: unknown;
  class?: {
    id: number;
    name: string;
    emoji: string;
    image: string;
  };
}
Clique aqui para expandir

Depois adicionei o import dos componentes de Dialog que criamos usando o ShadCN.

import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "~/components/ui/dialog";
Clique aqui para expandir

Fiz algumas alterações no componente Home do index.tsx que são as seguintes:

  1. State para os dados que precisam de reatividade
  2. Inicializacao do useQuery e useMutation para buscar os personagens e criar um novo personagem
  3. useEffect para atualizar os personagens
  4. Funcao para recarregar os personagens do banco
  5. Funcao para selecionar um personagem da lista
  6. Funcao para criar um novo personagem
const [collapsedSkills, setCollapsedSkills] = useState(true);
const [chars, setChars] = useState<Char[]>([]);
const [selectedChar, setSelectedChar] = useState<Char | undefined>(undefined);
const [newCharacter, setNewCharacter] = useState(false);
const [selectedClass, setSelectedClass] = useState(-1);
const [charName, setCharName] = useState("");
useSessionGuard();

const characters = api.character.getChars.useQuery();
const createCharMutation = api.character.create.useMutation();

useEffect(() => {
  if (characters?.data) {
    const updatedChars = characters.data as Char[];
    setChars(updatedChars);
    if (!selectedChar && updatedChars.length > 0) {
      setSelectedChar(updatedChars[0]);
    }
  }
}, [characters, selectedChar]);

const refreshChars = () => {
  void characters.refetch();
};

const selectChar = (char: Char) => {
  setSelectedChar(char);
};

const createChar = async () => {
  try {
    await createCharMutation.mutateAsync({
      name: charName,
      classId: selectedClass,
    });
    refreshChars();
  } catch (error) {
    console.error("Error creating character:", error);
  } finally {
    setNewCharacter(false);
  }
};
Clique aqui para expandir

E por ultimo fiz o bind nos elementos que tinhamos na tela claro adicionando o Dialog no topo do dom.

  return (
    <>
      <Dialog open={newCharacter} onOpenChange={setNewCharacter}>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>Criar novo personagem?</DialogTitle>
            <DialogDescription>
              Escolha entre as classes disponivel para o seu personagem
            </DialogDescription>
          </DialogHeader>
          <div className="flex items-center space-x-2">
            <div className="grid flex-1 gap-2">
              {createCharMutation.error && (
                <p>{createCharMutation.error.message}</p>
              )}
              <div>
                <label className="text-white">Nome</label>
                <input
                  type="text"
                  className="input input-bordered w-full"
                  placeholder="Nome do personagem"
                  value={charName}
                  onChange={(e) => setCharName(e.target.value)}
                />
              </div>
              <div
                className={`flex flex-col items-center justify-center rounded-md bg-[#152724] p-2 ${selectedClass === 1 ? "border-2 border-red-500 shadow-lg" : ""}`}
                onClick={() => setSelectedClass(1)}
              >
                <Image
                  src="/warrior.png"
                  alt="Logo"
                  width={0}
                  height={0}
                  sizes="100vw"
                  className="h-32 w-32 object-cover"
                />
                <div className="text-center text-white">Guerreiro</div>
              </div>
              <div
                className={`flex flex-col items-center justify-center rounded-md bg-[#152724] p-2 ${selectedClass === 2 ? "border-2 border-red-500 shadow-lg" : ""}`}
                onClick={() => setSelectedClass(2)}
              >
                <Image
                  src="/rouge.png"
                  alt="Logo"
                  width={0}
                  height={0}
                  sizes="100vw"
                  className="h-32 w-32 object-cover"
                />
                <div className="text-center text-white">Ladino</div>
              </div>
              <div
                className={`flex flex-col items-center justify-center rounded-md bg-[#152724] p-2 ${selectedClass === 3 ? "border-2 border-red-500 shadow-lg" : ""}`}
                onClick={() => setSelectedClass(3)}
              >
                <Image
                  src="/mage.png"
                  alt="Logo"
                  width={0}
                  height={0}
                  sizes="100vw"
                  className="h-32 w-32 object-cover"
                />
                <div className="text-center text-white">Mago</div>
              </div>
              <div
                className={`flex flex-col items-center justify-center rounded-md bg-[#152724] p-2 ${selectedClass === 4 ? "border-2 border-red-500 shadow-lg" : ""}`}
                onClick={() => setSelectedClass(4)}
              >
                <Image
                  src="/druid.png"
                  alt="Logo"
                  width={0}
                  height={0}
                  sizes="100vw"
                  className="h-32 w-32 object-cover"
                />
                <div className="text-center text-white">Druida</div>
              </div>
            </div>
          </div>
          <DialogFooter className="flex h-full w-full items-end justify-end">
            <button
              className="btn btn-primary flex h-fit items-center gap-1 rounded-lg bg-green-500 p-2 text-white"
              onClick={() => createChar()}
            >
              <span role="img" aria-label="Plus">
              </span>
              Criar
            </button>
            <DialogClose asChild>
              <button className="btn btn-secondary h-fit gap-1 rounded-lg bg-red-500 p-2 text-white">
                <span role="img" aria-label="Trash Bin">
                  🗑️
                </span>
                Cancelar
              </button>
            </DialogClose>
          </DialogFooter>
        </DialogContent>
      </Dialog>
      <main className="flex h-full w-full gap-2 bg-[#152724]">
        <div className="flex h-full w-full gap-2 bg-[#152724] ">
          <div className="golden-gradient hidden w-fit p-2 sm:block">
            <Image
              src={selectedChar?.class?.image ?? "/rouge.png"}
              alt="Logo"
              width={0}
              height={0}
              sizes="100vw"
              className="h-full w-full object-cover"
            />
          </div>
          <div className="flex w-full flex-col justify-between bg-[#1D2F2C]">
            <div className="golden-gradient w-full rounded-sm p-1">
              <div className="bg-[#2C534B]">
                <h1
                  className="medieval-font p-2 text-2xl font-bold text-white"
                  onClick={() => setCollapsedSkills(!collapsedSkills)}
                >
                  Personagens
                </h1>
                <ul
                  className={`medieval-menu w-full overflow-y-auto ${collapsedSkills ? "" : "hidden"}`}
                >
                  <li>
                    <a onClick={() => setNewCharacter(true)}>Novo Jogo</a>
                  </li>
                  {chars?.map((char: Char) => (
                    <li key={char.id} onClick={() => selectChar(char)}>
                      <a
                        className={`${
                          selectedChar?.id === char.id
                            ? "bg-[#4f4334] text-white"
                            : ""
                        }`}
                      >
                        {char.class?.emoji} {char.name} Lv: {char.level}
                      </a>
                    </li>
                  ))}
                </ul>
              </div>
            </div>
Clique aqui para expandir
index.tsx
import Image from "next/image";
import { useEffect, useState } from "react";
import { useSessionGuard } from "~/utils/useSessionGuard";

import { api } from "~/utils/api";
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "~/components/ui/dialog";

interface Char {
  id: number;
  name: string;
  level: number;
  user?: unknown;
  class?: {
    id: number;
    name: string;
    emoji: string;
    image: string;
  };
}

export default function Home() {
  const [collapsedSkills, setCollapsedSkills] = useState(true);
  const [chars, setChars] = useState<Char[]>([]);
  const [selectedChar, setSelectedChar] = useState<Char | undefined>(undefined);
  const [newCharacter, setNewCharacter] = useState(false);
  const [selectedClass, setSelectedClass] = useState(-1);
  const [charName, setCharName] = useState("");
  useSessionGuard();

  const characters = api.character.getChars.useQuery();
  const createCharMutation = api.character.create.useMutation();

  useEffect(() => {
    if (characters?.data) {
      const updatedChars = characters.data as Char[];
      setChars(updatedChars);
      if (!selectedChar && updatedChars.length > 0) {
        setSelectedChar(updatedChars[0]);
      }
    }
  }, [characters, selectedChar]);

  const refreshChars = () => {
    void characters.refetch();
  };

  const selectChar = (char: Char) => {
    setSelectedChar(char);
  };

  const createChar = async () => {
    try {
      await createCharMutation.mutateAsync({
        name: charName,
        classId: selectedClass,
      });
      refreshChars();
    } catch (error) {
      console.error("Error creating character:", error);
    } finally {
      setNewCharacter(false);
    }
  };

  return (
    <>
      <Dialog open={newCharacter} onOpenChange={setNewCharacter}>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>Criar novo personagem?</DialogTitle>
            <DialogDescription>
              Escolha entre as classes disponivel para o seu personagem
            </DialogDescription>
          </DialogHeader>
          <div className="flex items-center space-x-2">
            <div className="grid flex-1 gap-2">
              {createCharMutation.error && (
                <p>{createCharMutation.error.message}</p>
              )}
              <div>
                <label className="text-white">Nome</label>
                <input
                  type="text"
                  className="input input-bordered w-full"
                  placeholder="Nome do personagem"
                  value={charName}
                  onChange={(e) => setCharName(e.target.value)}
                />
              </div>
              <div
                className={`flex flex-col items-center justify-center rounded-md bg-[#152724] p-2 ${selectedClass === 1 ? "border-2 border-red-500 shadow-lg" : ""}`}
                onClick={() => setSelectedClass(1)}
              >
                <Image
                  src="/warrior.png"
                  alt="Logo"
                  width={0}
                  height={0}
                  sizes="100vw"
                  className="h-32 w-32 object-cover"
                />
                <div className="text-center text-white">Guerreiro</div>
              </div>
              <div
                className={`flex flex-col items-center justify-center rounded-md bg-[#152724] p-2 ${selectedClass === 2 ? "border-2 border-red-500 shadow-lg" : ""}`}
                onClick={() => setSelectedClass(2)}
              >
                <Image
                  src="/rouge.png"
                  alt="Logo"
                  width={0}
                  height={0}
                  sizes="100vw"
                  className="h-32 w-32 object-cover"
                />
                <div className="text-center text-white">Ladino</div>
              </div>
              <div
                className={`flex flex-col items-center justify-center rounded-md bg-[#152724] p-2 ${selectedClass === 3 ? "border-2 border-red-500 shadow-lg" : ""}`}
                onClick={() => setSelectedClass(3)}
              >
                <Image
                  src="/mage.png"
                  alt="Logo"
                  width={0}
                  height={0}
                  sizes="100vw"
                  className="h-32 w-32 object-cover"
                />
                <div className="text-center text-white">Mago</div>
              </div>
              <div
                className={`flex flex-col items-center justify-center rounded-md bg-[#152724] p-2 ${selectedClass === 4 ? "border-2 border-red-500 shadow-lg" : ""}`}
                onClick={() => setSelectedClass(4)}
              >
                <Image
                  src="/druid.png"
                  alt="Logo"
                  width={0}
                  height={0}
                  sizes="100vw"
                  className="h-32 w-32 object-cover"
                />
                <div className="text-center text-white">Druida</div>
              </div>
            </div>
          </div>
          <DialogFooter className="flex h-full w-full items-end justify-end">
            <button
              className="btn btn-primary flex h-fit items-center gap-1 rounded-lg bg-green-500 p-2 text-white"
              onClick={() => createChar()}
            >
              <span role="img" aria-label="Plus">
              </span>
              Criar
            </button>
            <DialogClose asChild>
              <button className="btn btn-secondary h-fit gap-1 rounded-lg bg-red-500 p-2 text-white">
                <span role="img" aria-label="Trash Bin">
                  🗑️
                </span>
                Cancelar
              </button>
            </DialogClose>
          </DialogFooter>
        </DialogContent>
      </Dialog>
      <main className="flex h-full w-full gap-2 bg-[#152724]">
        <div className="flex h-full w-full gap-2 bg-[#152724] ">
          <div className="golden-gradient hidden w-fit p-2 sm:block">
            <Image
              src={selectedChar?.class?.image ?? "/rouge.png"}
              alt="Logo"
              width={0}
              height={0}
              sizes="100vw"
              className="h-full w-full object-cover"
            />
          </div>
          <div className="flex w-full flex-col justify-between bg-[#1D2F2C]">
            <div className="golden-gradient w-full rounded-sm p-1">
              <div className="bg-[#2C534B]">
                <h1
                  className="medieval-font p-2 text-2xl font-bold text-white"
                  onClick={() => setCollapsedSkills(!collapsedSkills)}
                >
                  Personagens
                </h1>
                <ul
                  className={`medieval-menu w-full overflow-y-auto ${collapsedSkills ? "" : "hidden"}`}
                >
                  <li>
                    <a onClick={() => setNewCharacter(true)}>Novo Jogo</a>
                  </li>
                  {chars?.map((char: Char) => (
                    <li key={char.id} onClick={() => selectChar(char)}>
                      <a
                        className={`${
                          selectedChar?.id === char.id
                            ? "bg-[#4f4334] text-white"
                            : ""
                        }`}
                      >
                        {char.class?.emoji} {char.name} Lv: {char.level}
                      </a>
                    </li>
                  ))}
                </ul>
              </div>
            </div>
            <div className="golden-gradient w-full bg-[#2C534B] p-1">
              <div className="bg-[#2C534B]">
                <h1 className="medieval-font p-2 text-2xl font-bold text-white">
                  Ações
                </h1>
                <ul className="medieval-menu w-full overflow-y-auto">
                  <li>
                    <a>Personagem</a>
                  </li>
                  <li>
                    <a>Inventario</a>
                  </li>
                  <li>
                    <a>
                      <div className="medieval-menu__item__content">Caçar</div>
                    </a>
                  </li>
                  <li>
                    <a>
                      <div className="medieval-menu__item__content">Atacar</div>
                    </a>
                  </li>
                  <li>
                    <a>
                      <div className="medieval-menu__item__content">Viajar</div>
                    </a>
                  </li>
                </ul>
              </div>
            </div>
            <div className="golden-gradient w-full bg-[#2C534B] p-1">
              <div className="bg-[#2C534B]">
                <h1
                  className="medieval-font p-2 text-2xl font-bold text-white"
                  onClick={() => setCollapsedSkills(!collapsedSkills)}
                >
                  Skills
                </h1>
                <ul
                  className={`medieval-menu w-full overflow-y-auto ${collapsedSkills ? "hidden" : ""}`}
                >
                  <li>
                    <a>Apunhalhar 🗡️ [1] </a>
                  </li>
                  <li>
                    <a>Saraivada de flechas 🏹 [2]</a>
                  </li>
                  <li>
                    <a>Punhal venenoso 🗡️ [3]</a>
                  </li>
                  <li>
                    <a>Furtividade 🗡️ [4]</a>
                  </li>
                </ul>
              </div>
            </div>
            <div className="flex w-full grow flex-col justify-end gap-1 bg-[#152724] pb-4">
              <h1 className="medieval-font bg-[#2C534B] p-2 text-2xl font-bold text-white">
                Dados
              </h1>
              <div className="text-md medieval-font rounded-md bg-red-600 text-center text-white">
                100/100
              </div>
              <div className="text-md medieval-font rounded-md bg-blue-600 text-center text-white">
                100/100
              </div>
              <div className="text-md medieval-font rounded-md bg-orange-300 text-center text-white">
                100/100
              </div>
            </div>
          </div>
        </div>
        <div className="flex w-full flex-col gap-2 bg-[#152724]">
          <div className="golden-gradient w-full rounded-lg">
            <Image
              src="/Valdheim.png"
              alt="Logo"
              width={0}
              height={0}
              sizes="100vw"
              className="h-full w-full rounded-xl object-cover p-2"
            />
          </div>
          <div className="flex h-full w-full flex-row rounded-lg">
            <div className="h-full w-full border-r-2 border-[#af9567]">
              <h2 className="medieval-font p-2 text-2xl text-[#af9567]">
                História
              </h2>
              <ul className="medieval-menu w-full overflow-y-auto">
                <li>
                  <a>
                    <div className="flex h-full w-full justify-around">
                      <Image
                        src="/coin.png"
                        alt="Logo"
                        width={0}
                        height={0}
                        sizes="100vw"
                        className="h-10 w-10 object-cover"
                      />
                      <div className="m-auto">
                        Encontre o Lobo cinzento [4h]
                      </div>
                    </div>
                  </a>
                </li>
              </ul>
            </div>
            <div className="h-full w-full">
              <h2 className="medieval-font p-2 text-2xl text-[#af9567]">
                Caçada
              </h2>
              <ul className="medieval-menu w-full overflow-y-auto">
                <li>
                  <a>
                    <div className="flex h-full w-full justify-around">
                      <Image
                        src="/coin-1.png"
                        alt="Logo"
                        width={0}
                        height={0}
                        sizes="100vw"
                        className="h-10 w-10 object-cover"
                      />
                      <div className="m-auto">Mate 20 gremilins</div>
                    </div>
                  </a>
                </li>
              </ul>
            </div>
          </div>
        </div>
      </main>
    </>
  );
}
Clique aqui para expandir

Para fazer isso funcionar você precisa criar algumas coisas no banco de dados antes, como as classes e o Mapa principal, com isso você ja consegue criar um personagem e selecionar ele.

Agora é com você, crie o seu primeiro personagem.

Opa, calma aí!

Parece que você não está logado, caso tenha interesse em salvar seu progresso de estudo faça login agora.