Skip to content

[feature] inventory system v1 #192

@danielhe4rt

Description

@danielhe4rt

He4rt Developers — Sistema de Inventário e Equipamento (V1)

Visão geral

Este documento descreve a modelagem e regras de negócio do sistema de itens, inventário, equipamento, loja e trade P2P para a plataforma He4rt Developers. O sistema é puramente cosmético na V1 — itens não possuem efeitos mecânicos (boosts de XP, multiplicadores, etc.) mas a modelagem está preparada para suportá-los no futuro.

Decisões de design

Decisão Escolha V1
Natureza dos itens Cosméticos (visual do avatar)
Durabilidade Permanente (sem desgaste)
Obtenção Drop por atividade, compra com coins, reward de season
Trade Direto entre 2 usuários (P2P)
Classes de personagem Referência futura (ver seção final)
Boosts mecânicos Modelado mas desativado na V1
Consumíveis Roadmap futuro

Slots de equipamento

O avatar é composto por slots de base (aparência do personagem) e equipamento (itens cosméticos equipáveis).

Slots base (aparência)

Slot Descrição
skin Cor/tipo de pele
gender Tipo corporal
hair Estilo de cabelo
hair_color Cor do cabelo
face Formato/expressão facial

Slots de equipamento

Slot Descrição
cap Chapéu/boné/capacete
mask Máscara facial
eyes Óculos/acessório ocular
ears Brincos/acessório auricular
coat Roupa superior/casaco
pants Calça/roupa inferior
shoes Sapatos/botas
glove Luvas
cape Capa
shield Escudo
weapon Arma

Cada item possui seu próprio asset gráfico que é renderizado na composição do avatar.


Modelo de dados

Tabelas novas

item_slots

Define os slots disponíveis para equipamento.

CREATE TABLE public.item_slots (
    id          BIGSERIAL PRIMARY KEY,
    tenant_id   BIGINT NOT NULL REFERENCES public.tenants ON DELETE SET NULL,
    name        VARCHAR(255) NOT NULL,
    slug        VARCHAR(255) NOT NULL,
    slot_type   VARCHAR(50) NOT NULL DEFAULT 'equipment',
    display_order INTEGER NOT NULL DEFAULT 0,
    created_at  TIMESTAMP(0),
    updated_at  TIMESTAMP(0),
    CONSTRAINT item_slots_tenant_slug_unique UNIQUE (tenant_id, slug)
);

COMMENT ON COLUMN public.item_slots.slot_type IS 'base (aparência) ou equipment (equipável)';

item_rarities

Tiers de raridade com peso para cálculo de drop.

CREATE TABLE public.item_rarities (
    id          BIGSERIAL PRIMARY KEY,
    tenant_id   BIGINT NOT NULL REFERENCES public.tenants ON DELETE SET NULL,
    name        VARCHAR(255) NOT NULL,
    slug        VARCHAR(255) NOT NULL,
    color       VARCHAR(7) NOT NULL,
    drop_weight INTEGER NOT NULL DEFAULT 100,
    created_at  TIMESTAMP(0),
    updated_at  TIMESTAMP(0),
    CONSTRAINT item_rarities_tenant_slug_unique UNIQUE (tenant_id, slug)
);

COMMENT ON COLUMN public.item_rarities.drop_weight IS 'Peso relativo para cálculo de drop. Maior = mais comum.';
COMMENT ON COLUMN public.item_rarities.color IS 'Cor hex para exibição (ex: #FFFFFF)';

Seed data sugerido:

Nome Slug Cor Drop Weight
Comum common #B0B0B0 1000
Incomum uncommon #4CAF50 500
Raro rare #2196F3 200
Épico epic #9C27B0 50
Lendário legendary #FF9800 10

items

Catálogo de itens — definição do item, não instância.

CREATE TABLE public.items (
    id              UUID PRIMARY KEY,
    tenant_id       BIGINT NOT NULL REFERENCES public.tenants ON DELETE SET NULL,
    slot_id         BIGINT NOT NULL REFERENCES public.item_slots,
    rarity_id       BIGINT NOT NULL REFERENCES public.item_rarities,
    name            VARCHAR(255) NOT NULL,
    slug            VARCHAR(255) NOT NULL,
    description     TEXT,
    is_tradeable    BOOLEAN NOT NULL DEFAULT TRUE,
    is_purchasable  BOOLEAN NOT NULL DEFAULT FALSE,
    price           INTEGER,
    drop_rate       DECIMAL(5, 4),
    level_required  INTEGER NOT NULL DEFAULT 0,
    active          BOOLEAN NOT NULL DEFAULT TRUE,
    metadata        JSONB,
    created_at      TIMESTAMP(0),
    updated_at      TIMESTAMP(0),
    CONSTRAINT items_tenant_slug_unique UNIQUE (tenant_id, slug)
);

CREATE INDEX idx_items_slot ON public.items (slot_id);
CREATE INDEX idx_items_rarity ON public.items (rarity_id);
CREATE INDEX idx_items_active ON public.items (tenant_id, active);

COMMENT ON COLUMN public.items.price IS 'Preço em coins para compra direta (se is_purchasable)';
COMMENT ON COLUMN public.items.drop_rate IS 'Probabilidade de drop (0.0000 a 1.0000)';
COMMENT ON COLUMN public.items.level_required IS 'Nível mínimo do character para usar o item';
COMMENT ON COLUMN public.items.metadata IS 'Dados extensíveis: asset_url, tags, efeitos futuros';

Estrutura sugerida do metadata JSONB:

{
  "asset_url": "/items/cap/pirate-hat.png",
  "asset_layer_order": 5,
  "tags": ["pirate", "event-halloween-2026"],
  "effects": []
}

O campo effects fica vazio na V1 mas está preparado para boosts futuros.

character_items

Inventário — registro de posse de um item por um character.

CREATE TABLE public.character_items (
    id           UUID PRIMARY KEY,
    character_id UUID NOT NULL REFERENCES public.characters ON DELETE CASCADE,
    item_id      UUID NOT NULL REFERENCES public.items,
    tenant_id    BIGINT NOT NULL REFERENCES public.tenants ON DELETE SET NULL,
    acquired_via VARCHAR(50) NOT NULL,
    acquired_at  TIMESTAMP(0) NOT NULL,
    created_at   TIMESTAMP(0),
    updated_at   TIMESTAMP(0),
    CONSTRAINT character_items_unique UNIQUE (character_id, item_id)
);

CREATE INDEX idx_character_items_character ON public.character_items (character_id);
CREATE INDEX idx_character_items_item ON public.character_items (item_id);

COMMENT ON COLUMN public.character_items.acquired_via IS 'drop, purchase, trade, reward';

Nota: A constraint UNIQUE(character_id, item_id) significa que um character não pode ter duplicatas do mesmo item. Cada item é único no inventário. Se no futuro quiser permitir múltiplas cópias, remover essa constraint e adicionar um campo quantity.

character_equipment

Itens atualmente equipados por slot.

CREATE TABLE public.character_equipment (
    id                UUID PRIMARY KEY,
    character_id      UUID NOT NULL REFERENCES public.characters ON DELETE CASCADE,
    slot_id           BIGINT NOT NULL REFERENCES public.item_slots,
    character_item_id UUID NOT NULL REFERENCES public.character_items ON DELETE CASCADE,
    tenant_id         BIGINT NOT NULL REFERENCES public.tenants ON DELETE SET NULL,
    equipped_at       TIMESTAMP(0) NOT NULL,
    created_at        TIMESTAMP(0),
    updated_at        TIMESTAMP(0),
    CONSTRAINT character_equipment_slot_unique UNIQUE (character_id, slot_id)
);

CREATE INDEX idx_character_equipment_character ON public.character_equipment (character_id);

COMMENT ON CONSTRAINT character_equipment_slot_unique ON public.character_equipment IS 'Um item por slot por character';

shop_listings

Itens disponíveis para compra na loja.

CREATE TABLE public.shop_listings (
    id              UUID PRIMARY KEY,
    tenant_id       BIGINT NOT NULL REFERENCES public.tenants ON DELETE SET NULL,
    item_id         UUID NOT NULL REFERENCES public.items,
    price           INTEGER NOT NULL,
    stock           INTEGER,
    available_from  TIMESTAMP(0),
    available_until TIMESTAMP(0),
    active          BOOLEAN NOT NULL DEFAULT TRUE,
    created_at      TIMESTAMP(0),
    updated_at      TIMESTAMP(0)
);

CREATE INDEX idx_shop_listings_active ON public.shop_listings (tenant_id, active);
CREATE INDEX idx_shop_listings_item ON public.shop_listings (item_id);

COMMENT ON COLUMN public.shop_listings.stock IS 'NULL = estoque ilimitado';
COMMENT ON COLUMN public.shop_listings.price IS 'Preço em coins (pode sobrescrever o preço do item)';

trades

Registro de trade P2P entre dois characters.

CREATE TABLE public.trades (
    id                      UUID PRIMARY KEY,
    tenant_id               BIGINT NOT NULL REFERENCES public.tenants ON DELETE SET NULL,
    initiator_character_id  UUID NOT NULL REFERENCES public.characters ON DELETE CASCADE,
    receiver_character_id   UUID NOT NULL REFERENCES public.characters ON DELETE CASCADE,
    status                  VARCHAR(50) NOT NULL DEFAULT 'pending',
    resolved_at             TIMESTAMP(0),
    created_at              TIMESTAMP(0),
    updated_at              TIMESTAMP(0)
);

CREATE INDEX idx_trades_initiator ON public.trades (initiator_character_id, status);
CREATE INDEX idx_trades_receiver ON public.trades (receiver_character_id, status);

COMMENT ON COLUMN public.trades.status IS 'pending, accepted, rejected, cancelled';

trade_items

Itens envolvidos em um trade.

CREATE TABLE public.trade_items (
    id                UUID PRIMARY KEY,
    trade_id          UUID NOT NULL REFERENCES public.trades ON DELETE CASCADE,
    character_item_id UUID NOT NULL REFERENCES public.character_items,
    direction         VARCHAR(20) NOT NULL,
    created_at        TIMESTAMP(0)
);

CREATE INDEX idx_trade_items_trade ON public.trade_items (trade_id);

COMMENT ON COLUMN public.trade_items.direction IS 'offer (A→B) ou request (B→A)';

Regras de negócio

Image

Inventário

  1. Um character possui no máximo 1 unidade de cada item (UNIQUE constraint em character_items).
  2. O campo acquired_via rastreia a origem: drop, purchase, trade, reward.
  3. Itens permanecem no inventário indefinidamente (sem durabilidade/expiração na V1).

Equipamento

  1. Apenas 1 item por slot pode estar equipado (UNIQUE constraint em character_equipment).
  2. Para equipar, o item deve estar no inventário (character_items) do character.
  3. O character deve atender ao level_required do item.
  4. Desequipar remove o registro de character_equipment mas mantém em character_items.
  5. Um item equipado não pode ser tradado — deve ser desequipado primeiro.

Loja

Image
  1. shop_listings pode sobrescrever o price do items (preços promocionais).
  2. stock = NULL significa estoque ilimitado.
  3. available_from e available_until permitem itens sazonais/evento.
  4. Compra debita coins da wallets via transactions (sistema existente).
  5. Se stock > 0, decrementar atomicamente. Se stock = 0, compra bloqueada.

Drop

  1. Ao processar uma interaction (mensagem, voice, meeting), rolar drop chance.
  2. Algoritmo de drop:
    • Verificar se a interaction é elegível para drop (cooldown, tipo).
    • Rolar probabilidade geral de drop (ex: 5% por mensagem qualificada).
    • Se drop ativado, selecionar item usando weighted random baseado em item_rarities.drop_weight.
    • Verificar se o character já possui o item. Se sim, re-rolar ou dar coins como compensação.
    • Criar character_items com acquired_via = 'drop'.
  3. Itens com drop_rate específico usam esse valor em vez do sistema geral de raridade.

Trade P2P

  1. Player A cria trade selecionando itens do próprio inventário para oferecer.
  2. Validações ao criar:
    • Item deve ter is_tradeable = true.
    • Item não pode estar equipado.
    • Item não pode estar em outro trade pendente.
  3. Player B recebe notificação (via notifications existente).
  4. Player B pode: aceitar, rejeitar.
  5. Player A pode: cancelar (enquanto pendente).
  6. Ao aceitar:
    • Dentro de uma DB transaction:
      • Revalidar que todos os itens ainda são válidos.
      • Atualizar character_items.character_id dos itens de A → B.
      • Atualizar character_items.acquired_via para 'trade'.
      • Atualizar character_items.acquired_at para o momento da troca.
      • Atualizar trades.status para 'accepted'.
      • Atualizar trades.resolved_at.
  7. Trades com mais de 24h pendentes podem ser auto-cancelados (configurável).

Reward de season

  1. Ao finalizar uma season, itens exclusivos podem ser distribuídos baseado no ranking.
  2. Criar character_items com acquired_via = 'reward'.
  3. Itens de reward podem ter is_tradeable = false (exclusividade).

Avatar rendering

O avatar é renderizado compondo layers na seguinte ordem (de trás pra frente):

  1. cape (capa — atrás do corpo)
  2. skin (base corporal)
  3. pants (calça)
  4. shoes (sapatos)
  5. coat (roupa superior)
  6. glove (luvas)
  7. shield (escudo — mão esquerda)
  8. weapon (arma — mão direita)
  9. face (rosto)
  10. eyes (óculos/acessório)
  11. ears (brincos)
  12. hair (cabelo, com hair_color aplicado)
  13. mask (máscara)
  14. cap (chapéu — por cima de tudo)

O campo metadata.asset_layer_order no item pode sobrescrever a ordem padrão se necessário.

Cada asset é uma imagem PNG com transparência, dimensões padronizadas (ex: 256x256 ou 512x512), registrada em items.metadata.asset_url.


Integração com sistema existente

Sistema existente Integração
characters FK em character_items e character_equipment
wallets / transactions Débito de coins na compra via loja
interactions Trigger de drop ao processar interaction
seasons_rankings Distribuição de itens reward ao encerrar season
notifications Notificação ao receber proposta de trade
media Assets dos itens podem usar o sistema de media existente
tenants Todas as novas tabelas possuem tenant_id

Roadmap (pós-V1)

Feature Descrição Prioridade
Boosts mecânicos metadata.effects com XP multiplier, coin bonus, etc. Alta
Consumíveis Itens de uso único (XP potion, name change token) Média
Durabilidade Campo durability e desgaste por uso/tempo Baixa
Crafting Combinar itens para criar novos Baixa
Marketplace Loja P2P com listagem e busca Média
Sets de itens Bônus por equipar conjunto completo Média
Loot boxes Pacotes com itens aleatórios (compráveis) Baixa

Referência futura: Sistema de classes

O brainstorming mencionou classes de personagem que restringem o uso de itens. Esta feature não faz parte da V1 mas a modelagem a suporta:

Conceito: Cada character teria uma class (ex: Warrior, Mage, Rogue, Healer) que determina quais itens pode equipar.

Impacto na modelagem:

  • Adicionar tabela character_classes (id, name, slug, description).
  • Adicionar coluna class_id em characters.
  • Adicionar tabela pivot item_class_restrictions (item_id, class_id) — se vazia, item é universal.
  • Regra de equip: verificar se o item é compatível com a classe do character.

Impacto no campo metadata de items:

{
  "class_restrictions": ["warrior", "rogue"],
  "effects": [
    { "type": "xp_multiplier", "value": 1.5, "duration_hours": 24 }
  ]
}

A abordagem via metadata JSONB permite experimentação sem migrações, mas a tabela pivot é recomendada para queries performáticas quando o sistema de classes for implementado.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions