# GW2 Discord Raid Signup Bot — Design Spec

**Date:** 2026-07-01  
**Stack:** Node.js, discord.js, SQLite (`better-sqlite3`), node-cron

---

## Overview

A Discord bot for Guild Wars 2 raid signups. Raid leaders create events via slash command + modal, specifying which GW2 classes are needed, per-class player caps, and an overall raid cap. Players sign up by selecting a class from a dropdown or marking themselves absent. The embed updates live as players sign up. Events auto-archive when their scheduled time passes.

---

## Project Structure

```
discord-raid-bot/
├── index.js                  # Entry point — boots bot, registers handlers
├── deploy-commands.js        # One-off script to register slash commands with Discord
├── config.js                 # Token, guild ID, DB path from .env
│
├── commands/
│   ├── raid-create.js        # /raid create — opens modal
│   ├── raid-close.js         # /raid close — manually close an event
│   └── raid-list.js          # /raid list — show active events
│
├── handlers/
│   ├── interactionHandler.js # Routes all interactions (commands, buttons, selects, modals)
│   ├── modalHandler.js       # Handles modal submit → creates event in DB + posts embed
│   ├── signupHandler.js      # Handles dropdown class selection + absent button
│   └── scheduler.js          # node-cron job — checks for expired events, auto-archives
│
├── db/
│   ├── database.js           # Opens SQLite connection, runs migrations
│   └── queries.js            # All DB read/write functions
│
├── utils/
│   ├── embedBuilder.js       # Builds/updates the raid embed from event data
│   └── classConfig.js        # Maps GW2 class names to their server emoji IDs
│
└── data/
    └── raid.db               # SQLite file (gitignored)
```

---

## Database Schema

### `events`
| column | type | notes |
|---|---|---|
| id | TEXT PK | Discord message ID of the embed post |
| channel_id | TEXT | Channel where the embed was posted |
| title | TEXT | Event name |
| description | TEXT | Optional flavor text |
| event_time | INTEGER | Unix timestamp of the raid start |
| overall_cap | INTEGER | Max total players |
| status | TEXT | `open` / `closed` / `archived` |
| created_by | TEXT | Discord user ID of the raid leader |

### `class_slots`
| column | type | notes |
|---|---|---|
| id | INTEGER PK | Auto-increment |
| event_id | TEXT FK | References events.id |
| class_name | TEXT | e.g. `Guardian` |
| emoji_id | TEXT | Discord custom emoji ID for this class |
| cap | INTEGER | Max players for this class |

### `signups`
| column | type | notes |
|---|---|---|
| event_id | TEXT FK | References events.id |
| user_id | TEXT | Discord user ID |
| class_name | TEXT | Selected class name, or `absent` |
| signed_up_at | INTEGER | Unix timestamp |
| PRIMARY KEY | (event_id, user_id) | One signup per user per event |

The composite PK on `(event_id, user_id)` enforces one slot per player and makes class changes a simple `INSERT OR REPLACE`.

---

## Event Creation Flow

1. Raid leader runs `/raid create`
2. Discord opens a modal with:
   - **Title** — event name
   - **Description** — optional flavor text
   - **Date & Time** — text field, format `YYYY-MM-DD HH:MM` (parsed as server local time)
   - **Overall Cap** — max total players (number)
   - **Classes** — comma-separated `ClassName:Cap` pairs, e.g. `Guardian:2, Warrior:3, Elementalist:1`
     - Class names must match server custom emoji names exactly
3. On modal submit:
   - Bot parses and validates the classes string
   - Creates `events` row and `class_slots` rows in SQLite
   - Posts the raid embed to the current channel
   - Attaches a class select menu and an "I'm Absent" button
   - Stores the posted message ID as the event's primary key

### Embed Layout
```
⚔️  Weekly Wing 1 — Saturday 20:00
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Clear W1 for weekly achievements

[Guardian] 🛡️  @Alice, @Bob          2/2
[Warrior]  ⚔️  @Carol                 1/3
[Elementalist] 🔥  —                  0/1
[Druid]    🌿  —                      0/2
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Players: 3/8   |  🕐 Closes in 2 days
```

---

## Signup & Change Flow

### Signing up
- Player selects a class from the dropdown
- Bot validates: event is `open`, class slot is not full
- `INSERT OR REPLACE INTO signups` — handles both new signup and class change atomically
- Bot edits the embed in-place to reflect updated state
- Bot sends an ephemeral reply to the player: `"You're signed up as Guardian ✅"`

### Changing class
- Player selects a different class from the same dropdown
- `INSERT OR REPLACE` overwrites previous signup; previous class slot count decrements on the embed

### Going absent
- Player clicks the "I'm Absent" button
- Signup row set to `class_name = 'absent'`
- Player removed from all class slot counts on the embed
- Player can re-select a class later to return

### Full slot behavior
- Full classes still appear in the dropdown labelled `Guardian (FULL)`
- Selecting a full class returns an ephemeral error: `"That slot is full."`

---

## Scheduling & Auto-Archive

- `node-cron` runs every 5 minutes
- Queries for `open` events where `event_time <= now`
- For each expired event:
  - Sets `status = 'archived'` in DB
  - Edits the Discord embed: removes dropdown and button components, adds `🔒 Event closed` footer

---

## Commands

| Command | Description | Who can use |
|---|---|---|
| `/raid create` | Opens modal to create a new raid event | Anyone (configurable) |
| `/raid close` | Manually closes an active event | Event creator or admins |
| `/raid list` | Lists all active events in the server | Anyone |

---

## Key Constraints

- Class names in the modal must match server custom emoji names exactly — this is how the bot resolves the emoji to display
- One signup per user per event (enforced at DB and application level)
- The Discord message ID is used as the event primary key — the bot must post the embed before writing the final event row
- SQLite is synchronous via `better-sqlite3` — no async DB calls needed
