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

Escrevendo o primeiro schema do banco

Se abrir o arquivo prisma/schema.prisma você vai ver que ele tem algumas coisas já ali, vamos fazer algumas alterações pontuais, mas não se preocupe, vou explicar o que cada coisa faz.

Vamos remover a parte de post que vem por padrão e vamos alterar os Ids para serem do tipo Int e não String como vem por padrão.

Vai ficar assim

Voce pode remover a tabela Post não vamos precisar dela

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
    provider = "prisma-client-js"
}

datasource db {
    provider = "sqlite"
    url      = env("DATABASE_URL")
}

// Necessary for Next auth
model Account {
    id                Int      @id @default(autoincrement())
    userId            Int
    type              String
    provider          String
    providerAccountId String
    refresh_token     String? // @db.Text
    access_token      String? // @db.Text
    expires_at        Int?
    token_type        String?
    scope             String?
    id_token          String? // @db.Text
    session_state     String?
    user              User    @relation(fields: [userId], references: [id], onDelete: Cascade)

    @@unique([provider, providerAccountId])
}

model Session {
    id           Int      @id @default(autoincrement())
    sessionToken String   @unique
    userId       Int
    expires      DateTime
    user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model User {
    id            Int      @id @default(autoincrement())
    name          String?
    email         String?   @unique
    emailVerified DateTime?
    image         String?
    accounts      Account[]
    sessions      Session[]
}

model VerificationToken {
    identifier String
    token      String   @unique
    expires    DateTime

    @@unique([identifier, token])
}
Clique aqui para expandir

Porque estamos mudando o tipo do Id?

No geral não faz muita diferença se você usa Int ou String, mas caso você queira usar funções do banco como Count, Sum, Avg, etc usar o Int vai dar menos trabalho pro banco, aqui estamos usando o SQlite que é um banco bem simples, mas se você estiver usando um banco mais robusto como o Postgres, usar o Int vai ser mais performático.

Adicionando tabelas do MMO

Primeiro vamos criar a tabela de Classes, ela vai ser responsavel por guardar as classes que nosso jogo vai possuir.

model Class {
    id        int   @id @default(autoincrement())
    name      String // Nome da classe
    description String // Descrição que vamos exibir para o usuario
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
}
Clique aqui para expandir

Por padrão o prisma só vai atualizar nossa base de dados quando executarmos o comando db:push que fizemos antes, esse comando vai criar as tabelas que não existem e atualizar as que já existem, mas caso você ja tenha algum dado na tabela, é possivel que o prisma não consiga atualizar a tabela, para esses casos você vai ter que recriar a tabela, mas não se preocupe, vamos ver como fazer isso mais pra frente.

Por enquanto vamos continuar criando sem rodar o comando, agora vamos criar a tabela de Character que vai ser responsavel por guardar os personagens dos usuarios.

model Character {
    id        int   @id @default(autoincrement())
    name      String // Nome do personagem
    classId   Int    // Id da classe do personagem
    level     Int    // Level do personagem
    class     Class  @relation(fields: [classId], references: [id])
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
}
Clique aqui para expandir

Agora precisamos adicionar a referencia no model de Classes para que o prisma saiba que existe uma relação entre as tabelas.

model Class {
    id        Int   @id @default(autoincrement())
    name      String // Nome da classe
    description String // Descrição que vamos exibir para o usuario
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
    characters Character[] // <- Adcionamos essa linha
}
Clique aqui para expandir

Todo personagem no nosso jogo pertence a um usuario, então vamos adicionar a tabela de User, sempre que for criar uma relação entre tabelas você precisa adicionar a referencia nos dois models. Ou seja se adicionarmos a referencia no model de Character, precisamos adicionar a referencia no model de User também.

model User {
    id            Int      @id @default(autoincrement())
    name          String?
    email         String?   @unique
    emailVerified DateTime?
    image         String?
    accounts      Account[]
    sessions      Session[]
    characters    Character[] // <- Adcionamos essa linha
}

model Character {
    id        Int   @id @default(autoincrement())
    name      String // Nome do personagem
    classId   Int    // Id da classe do personagem
    userId    Int    // Id do usuario que criou o personagem <- Adcionamos essa linha
    level     Int    // Level do personagem
    class     Class  @relation(fields: [classId], references: [id])
    user      User   @relation(fields: [userId], references: [id]) // <- Adcionamos essa linha
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
}
Clique aqui para expandir

O ideal seria a gente limitar o tamanho dos dados do tipo String, por default ele vai usar o tipo text que não tem um limite de tamanho, mas estamos usando o SQlite que não tem um tipo de dado que limita o tamanho do texto, então vamos usar o tipo String mesmo.

Lembre-se de dar uma olhada na referencia do Prisma para saber quais tipos são mais adequados para cada caso. https://www.prisma.io/docs/concepts/components/prisma-schema/data-model#scalar-types

O schema final do prisma fica assim
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
    provider = "prisma-client-js"
}

datasource db {
    provider = "sqlite"
    // NOTE: When using mysql or sqlserver, uncomment the @db.Text annotations in model Account below
    // Further reading:
    // https://next-auth.js.org/adapters/prisma#create-the-prisma-schema
    // https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string
    url      = env("DATABASE_URL")
}

// Necessary for Next auth
model Account {
    id                Int      @id @default(autoincrement())
    type              String
    provider          String
    providerAccountId String
    refresh_token     String? // @db.Text
    access_token      String? // @db.Text
    expires_at        Int?
    token_type        String?
    scope             String?
    id_token          String? // @db.Text
    session_state     String?

    user              User    @relation(fields: [userId], references: [id], onDelete: Cascade)
    userId            Int

    @@unique([provider, providerAccountId])
}

model Session {
    id           Int      @id @default(autoincrement())
    sessionToken String   @unique
    expires      DateTime

    user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
    userId       Int
}

model User {
    id            Int      @id @default(autoincrement())
    name          String?
    email         String?   @unique
    emailVerified DateTime?
    image         String?

    accounts      Account[]
    sessions      Session[]
    characters    Character[]
}

model VerificationToken {
    identifier String
    token      String   @unique
    expires    DateTime

    @@unique([identifier, token])
}

// MMORPG Models
model Skill {
    id Int @id @default(autoincrement())
    name String @unique
    description String
    emoji String
    manaCost Int @default(1)
    level Int @default(1)
    DPS Int @default(0)
    defense Int @default(0)
    health Int @default(0)
    RNG Int @default(0)

    class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
    classId Int

    characters CharSkill[]
}

model Class {
    id Int @id @default(autoincrement())
    name String @unique
    emoji String
    description String

    characters Character[]
    skills Skill[]
}

model CharStats {
    id Int @id @default(autoincrement())
    strength Int @default(1)
    agility Int @default(1)
    faith Int @default(1)
    intelligence Int @default(1)
    luck Int @default(1)
    DPS Int @default(1)
    defense Int @default(1)
    health Int @default(50)
    mana Int @default(10)
    stamina Int @default(100)

    character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
    characterId Int @unique
}

model Item {
    id Int @id @default(autoincrement())
    name String @unique
    description String
    type String
    level Int @default(1)
    DPS Int?
    defense Int?
    health Int?
    RNG Int?

    inventories ItemInventory[]
    drops MonsterDrop[]
}

model ItemInventory {
    equipped Boolean @default(false)
    type String
    quantity Int @default(1)

    item Item @relation(fields: [itemId], references: [id], onDelete: Cascade)
    itemId Int

    inventory Inventory @relation(fields: [inventoryId], references: [id], onDelete: Cascade)
    inventoryId Int

    @@id([itemId, inventoryId])
}

model Inventory {
    id Int @id @default(autoincrement())

    character Character @relation(fields: [characterId], references: [id])
    characterId Int @unique

    items ItemInventory[]
}

model CharSkill {
    level Int @default(1)

    character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
    characterId Int

    skill Skill @relation(fields: [skillId], references: [id])
    skillId Int

    @@id([characterId, skillId])
}

model Monster {
    id Int @id @default(autoincrement())
    name String @unique
    level Int @default(1)
    health Int @default(1)
    DPS Int @default(1)
    defense Int @default(1)

    map Map @relation(fields: [mapId], references: [id], onDelete: Cascade)
    mapId Int

    drops MonsterDrop[]
    quests Quest[]
}

model MonsterDrop {
    quantity Int @default(1)
    dropChance Int @default(100)

    monster Monster @relation(fields: [monsterId], references: [id], onDelete: Cascade)
    monsterId Int

    item Item @relation(fields: [itemId], references: [id])
    itemId Int

    @@id([monsterId, itemId])
}

model Quest {
    id Int @id @default(autoincrement())
    name String @unique
    description String
    level Int @default(1)
    experience Int @default(0)
    image String @default("/coin.png")

    map Map @relation(fields: [mapId], references: [id], onDelete: Cascade)
    mapId Int

    monsters Monster[]
    characters CharQuest[]
}

model CharQuest {
    completed Boolean @default(false)

    character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
    characterId Int

    quest Quest @relation(fields: [questId], references: [id])
    questId Int

    @@id([characterId, questId])
}

model Map {
    id Int @id @default(autoincrement())
    name String @unique
    description String
    level Int @default(1)

    monsters Monster[]
    quests Quest[]
    characters Character[]
}

model ExperienceTable {
    id Int @id @default(autoincrement())
    level Int @unique
    experience Int @default(0)
}

model CharLog {
    id Int @id @default(autoincrement())
    message String
    createdAt DateTime @default(now())

    character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
    characterId Int
}

model Character {
    id Int @id @default(autoincrement())
    name String @unique
    level Int @default(1)
    experience Int @default(0)
    points Int @default(0)
    pkPoints Int @default(0)
    isDead Boolean @default(false)

    map Map @relation(fields: [mapId], references: [id], onDelete: SetDefault)
    mapId Int @default(1)

    user User @relation(fields: [userId], references: [id], onDelete: Cascade)
    userId Int

    class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
    classId Int

    charStats CharStats?
    inventory Inventory?
    skills CharSkill[]
    logs CharLog[]
    quests CharQuest[]
}
```
</details>

## Criando as tabelas

Agora vamos rodar o comando db:push para atualizar nosso banco de dados.

```bash
npm run db:push
Clique aqui para expandir

Como dito anteriormente, o prisma não vai conseguir atualizar a tabela de Character porque já temos dados nela.

Erro

Como estamos em desenvolvimento vou apagar o arquivo db.sqlite e rodar o comando novamente, em bases como Postgres e Mysql o prisma consegue converter os dados automaticamente, como estamos usando o Sqlite vamos apagar o arquivo por enquanto.

Depois de apagar o arquivo vamos rodar o npm run db:push novamente.

PS mmorpg-nextjs> npm run db:push

> mmorpg-text-menthor@0.1.0 db:push
> prisma db push

Environment variables loaded from .env
Prisma schema loaded from prisma\schema.prisma
Datasource "db": SQLite database "db.sqlite" at "file:./db.sqlite"

The database is already in sync with the Prisma schema.

✔ Generated Prisma Client (v5.8.1) to .\node_modules\@prisma\client in 95ms
Clique aqui para expandir

Pronto ele criou pra gente o db.sqlite novamente agora podemos continuar.

Opa, calma aí!

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