Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ project(hgserver
LANGUAGES CXX
)

# Windows: stop windows.h from defining min/max macros (they clobber std::min/std::max)
if(WIN32)
add_compile_definitions(NOMINMAX WIN32_LEAN_AND_MEAN)
endif()

# Output directories - all binaries go to bin/
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_SOURCE_DIR}/bin)
Expand Down
163 changes: 65 additions & 98 deletions bin/game_configs/loot_tables.yaml

Large diffs are not rendered by default.

16 changes: 12 additions & 4 deletions bin/game_configs/magic.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1321,7 +1321,9 @@ magic:
attribute: 2
- id: 76
name: Cancellation
type: 21
# Legacy cfg had a placeholder row copied from Lightning-Strike (type 21);
# corrected to the real cancellation type
type: 28
delay: 0
duration: 0
mana_cost: 90
Expand All @@ -1345,7 +1347,9 @@ magic:
attribute: 2
- id: 77
name: Illusion-Movement
type: 21
# Legacy cfg had a placeholder row copied from Lightning-Strike (type 21);
# corrected to the confusion/illusion type
type: 16
delay: 0
duration: 0
mana_cost: 90
Expand Down Expand Up @@ -1537,7 +1541,9 @@ magic:
attribute: 4
- id: 95
name: Mass-Illusion-Movement
type: 21
# Legacy cfg had a placeholder row copied from Lightning-Strike (type 21);
# corrected to the confusion/illusion type
type: 16
delay: 0
duration: 0
mana_cost: 90
Expand All @@ -1561,7 +1567,9 @@ magic:
attribute: 2
- id: 94
name: Resurrection
type: 21
# Legacy cfg had a placeholder row copied from Lightning-Strike (type 21);
# corrected to the real resurrection type
type: 32
delay: 0
duration: 0
mana_cost: 90
Expand Down
172 changes: 86 additions & 86 deletions bin/game_configs/npcs.yaml

Large diffs are not rendered by default.

22 changes: 11 additions & 11 deletions bin/game_configs/shops.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# shops.yaml
# shops.yaml
# NPC shop configurations
# buy_categories: item_category values the shop will purchase from players
# repair_categories: item_category values the shop can repair
Expand All @@ -19,18 +19,18 @@ shops:
buy_categories: [10, 11, 12, 13, 14, 30, 31, 32, 99]
repair_categories: [11, 12, 13, 14]
items:
- { item_id: 308, count: 1 } # HealthPotion
- { item_id: 309, count: 1 } # ManaPotion
- { item_id: 310, count: 1 } # StaminaPotion
- { item_id: 91, count: 1 } # RedPotion (HP)
- { item_id: 93, count: 1 } # BluePotion (MP)
- { item_id: 95, count: 1 } # GreenPotion (SP)

ShopKeeper-E:
type: general
buy_categories: [10, 11, 12, 13, 14, 30, 31, 32, 99]
repair_categories: [11, 12, 13, 14]
items:
- { item_id: 308, count: 1 } # HealthPotion
- { item_id: 309, count: 1 } # ManaPotion
- { item_id: 310, count: 1 } # StaminaPotion
- { item_id: 91, count: 1 } # RedPotion (HP)
- { item_id: 93, count: 1 } # BluePotion (MP)
- { item_id: 95, count: 1 } # GreenPotion (SP)

# Blacksmiths - buy/sell/repair weapons and armor
Gandlf:
Expand All @@ -39,14 +39,14 @@ shops:
repair_categories: [1, 2, 3, 4, 5, 6, 10, 11, 12, 13, 14, 15]
items:
- { item_id: 1 } # Dagger
- { item_id: 2 } # ShortSword
- { item_id: 3 } # MainGauche
- { item_id: 8 } # ShortSword
- { item_id: 12 } # MainGauche

William:
type: blacksmith
buy_categories: [1, 2, 3, 4, 5, 6, 10, 11, 12, 13, 14, 15]
repair_categories: [1, 2, 3, 4, 5, 6, 10, 11, 12, 13, 14, 15]
items:
- { item_id: 1 } # Dagger
- { item_id: 2 } # ShortSword
- { item_id: 3 } # MainGauche
- { item_id: 8 } # ShortSword
- { item_id: 12 } # MainGauche
19 changes: 19 additions & 0 deletions bin/mapdata/aresden.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Map config for 'aresden' — cidade de Aresden habitável para bots
# Área validada por tools/bot/scan-map.mjs: 200-260 x 180-260 (>99% andável)
# npc_type: 10 = Slime, 14 = Orc, 16 = Giant-Ant, 15 = ShopKeeper-W, 19 = Gandlf

name: "Aresden"

initial_points:
- { id: 0, x: 240, y: 220 }
- { id: 1, x: 235, y: 225 }
- { id: 2, x: 245, y: 225 }

spawners:
# Densidade para ~25 bots
- { id: 1, type: 0, x1: 205, y1: 185, x2: 255, y2: 255, npc_type: 10, max_count: 60 }
- { id: 2, type: 0, x1: 205, y1: 185, x2: 255, y2: 255, npc_type: 16, max_count: 30 }
- { id: 3, type: 0, x1: 210, y1: 190, x2: 250, y2: 250, npc_type: 14, max_count: 20 }
# Mercadores fixos perto do ponto inicial
- { id: 4, type: 0, x1: 236, y1: 216, x2: 236, y2: 216, npc_type: 15, max_count: 1 }
- { id: 5, type: 0, x1: 244, y1: 216, x2: 244, y2: 216, npc_type: 19, max_count: 1 }
17 changes: 17 additions & 0 deletions bin/mapdata/default.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Map config for 'default' — dev/bot testing map
# npc_type uses legacy spot-mob codes (see src/npc/spot_mob_mapping.h):
# 10 = Slime, 14 = Orc, 16 = Giant-Ant, 15 = ShopKeeper-W, 19 = Gandlf

name: "Default"

initial_points:
- { id: 0, x: 85, y: 85 }

spawners:
# Densidade para ~10 bots caçando simultaneamente (faixa 55-115 testada como andável)
- { id: 1, type: 0, x1: 60, y1: 60, x2: 110, y2: 110, npc_type: 10, max_count: 30 }
- { id: 2, type: 0, x1: 55, y1: 55, x2: 115, y2: 115, npc_type: 16, max_count: 15 }
- { id: 3, type: 0, x1: 60, y1: 60, x2: 110, y2: 110, npc_type: 14, max_count: 10 }
# Mercadores (fixos, 1 de cada): loja geral (poções) e ferreiro (armas/reparo)
- { id: 4, type: 0, x1: 82, y1: 80, x2: 82, y2: 80, npc_type: 15, max_count: 1 }
- { id: 5, type: 0, x1: 88, y1: 80, x2: 88, y2: 80, npc_type: 19, max_count: 1 }
19 changes: 19 additions & 0 deletions bin/mapdata/elvine.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Map config for 'elvine' — cidade de Elvine habitável para bots
# Área validada por tools/bot/scan-map.mjs: 40-140 x 180-260 (>98% andável)
# npc_type: 10 = Slime, 14 = Orc, 16 = Giant-Ant, 15 = ShopKeeper-W, 19 = Gandlf

name: "Elvine"

initial_points:
- { id: 0, x: 60, y: 200 }
- { id: 1, x: 55, y: 205 }
- { id: 2, x: 65, y: 205 }

spawners:
# Densidade para ~25 bots
- { id: 1, type: 0, x1: 45, y1: 185, x2: 115, y2: 255, npc_type: 10, max_count: 60 }
- { id: 2, type: 0, x1: 45, y1: 185, x2: 115, y2: 255, npc_type: 16, max_count: 30 }
- { id: 3, type: 0, x1: 50, y1: 190, x2: 110, y2: 250, npc_type: 14, max_count: 20 }
# Mercadores fixos perto do ponto inicial
- { id: 4, type: 0, x1: 56, y1: 196, x2: 56, y2: 196, npc_type: 15, max_count: 1 }
- { id: 5, type: 0, x1: 64, y1: 196, x2: 64, y2: 196, npc_type: 19, max_count: 1 }
150 changes: 150 additions & 0 deletions docs/BOT_SYSTEM_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Plano: Sistema de Bots Autônomos (bot_system)

> **Status:** em implementação. Arquitetura escolhida: **cliente headless** (`tools/bot/bot.mjs`, Node,
> protocolo WebSocket JSON) em vez do subsistema C++ — zero mudança de arquitetura no servidor e
> visibilidade de graça. `node bot.mjs all` roda todos os bots de `bots.json` num único processo.
>
> **Concluído:** Fase 1 (login/conta/personagem), Fase 3 núcleo (caça, combate, fuga/recuperação,
> respawn, loot, level-up com XP real), Fase 4 (party via protocolo JSON novo — invite/accept/leave/
> update, XP compartilhado equal_split; líder convida, membros aceitam e seguem o líder), Fase 5
> (ouro de loot, lógica de loja: comprar poções/arma, vender sobras, reparar, equipar).
>
> **Correções de servidor feitas no caminho:** party no protocolo JSON (não existia); ouro de loot
> creditado ao entity id do ECS em vez do player_id (inventário fantasma); `npc_registry` YAML não
> lia `gold_min/gold_max`; `npcs.yaml` usava `exp_dice:`/loja com item ids errados; spot codes de
> mercadores (15/19) para spawners de mapa. Ver PROGRESS.md 2026-08-30.
>
> **Pendências:** magias (Fase 3), auto-alocação de stat points no level-up, chaves ainda divergentes
> em npcs.yaml (`defense_ratio` vs `defense`, `size` vs `body_size`), equip responde ack v2 sem eco de
> seq (bot usa fire-and-forget), remover LOG_INFO temporário "loot roll" em game_handlers_npc.cpp
> antes de escalar, Fase 6 (escala 10 → 50 → 200 + correções O(n) da Fase 0).
> **Objetivo:** ~200 bots 100% autônomos no mapa: upam, tomam poção automática, usam magias, formam party, matam inimigos, compram itens e reparam equipamentos.

## Premissas verificadas no código (2026-08-30)

1. **O servidor real é a árvore moderna `src/`** (C++20, namespace `hb`). O código legado
(`Game.cpp`, `Client.h`, `Portion.cpp`, `XSocket`) está desabilitado no `CMakeLists.txt`
(linhas ~294–317) e não compila. Todo o plano é contra `src/`.
2. **Jogador sem conexão é um estado válido:**
- `player_system::create_player()` não toca em rede; `bind_connection()` é passo separado e opcional.
- Broadcast ignora `connection == 0` — `src/bridge/handlers/broadcast_util.cpp:27`.
- Auto-save ignora `connection == 0` — `src/bridge/handlers/auth_handlers.cpp:1955`.
- Logo: **bot = `player` real sem `bind_connection()`**. Nada quebra, nada vaza para o banco.
3. **Todas as ações necessárias já existem como API pura, sem rede** — os testes
(`tests/test_melee_pve.cpp`, `tests/test_party_exp_share.cpp`) já criam players e rodam
combate/party inteiros sem nenhuma conexão.

## Arquitetura

**Modelo mental: o bot é um NPC com corpo de `player`.**

- Novo subsistema `src/bot/bot_system.{h,cpp}` herdando `hb::subsystem`, registrado via
`subsystems().create_subsystem<bot::bot_system>()` em `application.cpp` → recebe
`update(delta_time)` no `on_tick()` a 50 Hz (tick de 20 ms).
- Máquina de estados por bot, copiando o padrão de `npc_system` / `ai_behavior.h`
(`ai_state`: idle → hunt → combat → flee → town → regroup).
- "Think interval" de 100–250 ms **com offset aleatório por bot** (não pensar todos no mesmo
tick). Precedente: `ai_config::think_interval_ms{500}` e o acumulador de
`npc_system::update_all_ai()` (`npc_system.cpp:951`, intervalo de 100 ms).

### Mapa requisito → API existente

| Requisito | Chamada direta (sem handler, sem `ws_connection`) |
|---|---|
| Andar / caçar | `player_system::try_move(pid, pos, dir)` — `src/player/player_system.h:157` (colisão completa, retorna `move_result` com teleport) |
| Matar inimigos | `combat_system::process_attack(attack_event{...})` — `src/combat/combat_system.h:59` |
| Usar magias | `magic_system::instant_cast(ecs_entity, spell, target)` — `src/magic/magic_system.h:65`; custos via `calculate_mana_cost()`, cooldowns via `get_cooldown_remaining()` |
| Poção automática | `inventory->remove_item()` + `plr->heal_hp/mp/sp()` (lógica de referência: `game_handlers_equipment.cpp:483+`) |
| Party | `social_system::create_party / invite_to_party / accept_party_invite / leave_party` — `src/social/social_system.h:242-253` (100% por `player_id`, zero rede) |
| Comprar itens | `item_ops::shop_buy(entity_id, template_id, price, items, inv)` — `src/item/item_ops.h:114`; preços em `src/npc/shop_pricing.h`, catálogo em `src/registry/shop_registry.cpp` |
| Reparar equipamento | `item_ops::shop_repair(...)` — `src/item/item_ops.h:151` (custo proporcional, deduz ouro, restaura durabilidade) |
| Upar / XP | `player_system::add_experience(pid, amount)` (level-up e stat points automáticos, `player_system.cpp:308-325`); recompensas via `combat_system::calculate_kill_rewards()` |
| Alvos próximos | `world->get_all_entities_in_range(map, pos, r)` (índice espacial, `spatial_index.h`, célula 32) |

### Ciclo de vida do bot

1. `players_->create_player(create_info)` — **nunca** chamar `bind_connection`.
2. Registrar `ecs_entity` no entity manager e no `spatial()` do mapa — sem isso NPCs não
agridem o bot (`find_aggro_target` só enxerga `entity_type::player` no índice espacial)
e players reais não o veem.
3. Despawn: remover do índice espacial/occupant antes de destruir (ordem de remoção — ver
padrão de memory safety no CLAUDE.md).

## Fases

### Fase 0 — Correções de performance (ANTES de escalar)

Dois gargalos O(n) que 200 bots amplificam:

1. `get_players_who_can_see()` — `src/player/player_system.cpp:1196-1199`: varre **todos** os
players a cada broadcast só para achar admins com `sees_all`.
Correção: manter `std::vector<player_id> sees_all_players_` incremental.
2. `find_aggro_target()` — `src/npc/npc_system.cpp:1319-1355`: `for_each_player(...)` (varredura
linear) **dentro do laço** de entidades no raio, a cada 100 ms.
Correção: usar `player_system::get_player_by_entity(entity)` que é O(1)
(índice `ecs_index_to_id_`).

Ligar o `perf_stats_system` (`src/perf/`, macro `PERF_TIMER`) em `tick_total`,
`spatial_query_visibility`, `npc_ai_update` para medir antes/depois.

### Fase 1 — Esqueleto do bot_system + spawn

- `src/bot/bot_system.{h,cpp}` registrado em `application.cpp`.
- Spawn/despawn de bots (ciclo de vida acima).
- Config YAML: quantidade, mapas, distribuição de classes (guerreiro/mago), faixa de level,
nomes gerados.
- Comando de GM para spawn/despawn em runtime (via `admin_system`).

### Fase 2 — Visibilidade (o principal trabalho de "cola")

Bots pulam os handlers, logo pulam os broadcasts que os handlers emitem. Para jogadores
reais **verem** os bots se movendo/atacando, o `bot_system` deve emitir
`broadcast_to_visible(...)` / `broadcast_player_action(...)` (`broadcast_util.h`) após cada
ação — ou, mais limpo, publicar eventos no `event_bus` e deixar o bridge traduzir.
Sem esta fase os bots funcionam mas ficam invisíveis para clientes reais.

### Fase 3 — IA de sobrevivência e combate

- Perfis por classe (melee / mago / híbrido).
- Seleção de alvo via índice espacial; replicar as validações do handler humano que fizerem
sentido: cooldown de ataque de 100 ms, `combat_mode` ligado, alvo vivo.
- Rotação de magia com checagem de mana e cooldown.
- **Auto-pot por threshold:** HP < 40% → poção; HP < 15% → estado flee. MP baixo → pot de mana.
- Anti-stuck: reaproveitar a ideia de `stuck_count` de `ai_runtime_state`.

### Fase 4 — Party

- Líder cria party, convida bots próximos de level compatível; XP share já funciona.
- Comportamento de grupo: seguir líder, focar mesmo alvo, healer prioriza aliados.
- Modos via `set_loot_mode` / `set_exp_mode`.

### Fase 5 — Economia (comprar / reparar)

- Gatilhos para estado "town": poções < N no inventário, ou equipamento danificado
(o sinal natural vem de `item_ops::damage_equipment()`).
- Rotina: caminhar até NPC da loja → `shop_buy` (pots) → `shop_repair` (itens danificados)
→ voltar ao grind. Ouro vem dos kill rewards normais.

### Fase 6 — Escala até 200

- Subir 10 → 50 → 200 medindo tick com `perf_stats_system`.
- Headroom teórico: `max_players{2000}`, `max_npcs{10000}` — o risco real é custo de
broadcast quando bots e players reais compartilham tela (mitigado pela Fase 0 e pelo
escalonamento de think).

## Pontos de decisão em aberto

- **Pathfinding:** não existe A* no servidor — movimento é guloso de 1 passo
(`move_towards()` / `direction_to()` + `move_in_direction()`). Para grind em campo aberto
basta; para a rotina de cidade (achar NPC da loja) recomenda-se A* simples sobre
`map::is_walkable()` + waypoints (`map::get_waypoint`). Único componente genuinamente novo.
- **Persistência dos bots:** com `connection == 0` os bots nunca são salvos (padrão efêmero,
de graça). Se os bots devem manter progresso entre restarts, será preciso save opt-in
reaproveitando o fluxo de `save_all_players()` — decidir na Fase 1.
- **Equilibrium:** se qualquer comportamento de bot tocar no sistema legado "Equilibrium",
perguntar antes (regra do CLAUDE.md — não portar por padrão).

## Estimativa

Fases 0–3 são o núcleo (~70% do esforço; Fase 2 é a mais delicada). Fases 4–5 são rápidas
porque as APIs já existem. A* é opcional e isolado.
4 changes: 2 additions & 2 deletions docs/JSON_PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ All messages follow a common envelope structure:
|----------|------|-------------|
| System | [protocol/system.md](protocol/system.md) | ping, pong, error |
| Authentication | [protocol/auth.md](protocol/auth.md) | Login, accounts, characters, game entry |
| Player State | [protocol/player.md](protocol/player.md) | Game state objects, entity visibility, stat updates, view mode |
| Player State | [protocol/player.md](protocol/player.md) | Game state objects, entity visibility, stat updates, experience/level-up updates, view mode |
| Movement | [protocol/movement.md](protocol/movement.md) | Movement, teleportation |
| Combat | [protocol/combat.md](protocol/combat.md) | Attacks, damage, death, magic, skills, combat mode, action broadcasts |
| Items (v2) | [protocol/items-v2.md](protocol/items-v2.md) | Inventory, equipment, ground items, trade, shop, bank, loot |
Expand All @@ -77,7 +77,7 @@ All messages follow a common envelope structure:
| Chat | [protocol/chat.md](protocol/chat.md) | Chat messages, commands |
| Crafting | [protocol/crafting.md](protocol/crafting.md) | Manufacturing, alchemy, mining, fishing |
| Guild | [protocol/guild.md](protocol/guild.md) | Guild management, ranks, invites |
| Social | [protocol/social.md](protocol/social.md) | Friend system |
| Social | [protocol/social.md](protocol/social.md) | Friend system, party (invite/accept/leave/update) |
| War | [protocol/war.md](protocol/war.md) | Crusade, Heldenian, Apocalypse, force recall, rewards |
| Commands | [protocol/commands.md](protocol/commands.md) | Command list, availability updates |
| Admin | [ADMIN_PROTOCOL.md](ADMIN_PROTOCOL.md) | Admin web tool (32 request/response pairs) |
Expand Down
Loading