feat: grant Discord roles for game milestones #132

Closed
opened 2026-03-24 16:40:30 -07:00 by hikari · 1 comment
Owner

Overview

Grant players Discord roles automatically as they reach milestones in the game. Roles are awarded via the Discord API using the bot token at key points in the game flow.

Also maintains an inGuild flag on the Player record (used by #133) via a combination of OAuth-time checks and bot event listeners.


Roles

Role Trigger Discord Role ID
Elysian Granted during OAuth flow (any player who authenticates) 1486144823684628490
Apotheosis First Apotheosis in Base Game (existing role — ID already in use)
Eternal Sovereignty First Eternal Sovereignty in Vampire Mode 1486144657023959180
Deification First Deification in Goddess Mode 1486144663403495514

Implementation

Elysian

  • Grant the role at the end of the OAuth callback flow, immediately after the player record is created or retrieved.
  • Safe to re-attempt on every login (Discord will no-op if the role is already assigned).
  • Suppress errors silently if the player is not in the guild — not being in the server is expected and should never break the auth flow.
  • Use the success/failure of the role grant to set the initial inGuild value on the Player record.

Apotheosis (existing)

  • Already implemented — no changes needed. Document the trigger here for reference.
  • Ensure this also suppresses errors if the player is not in the guild.

Eternal Sovereignty & Deification

  • Grant the respective role when the player completes their first Eternal Sovereignty / Deification in the expansion tick or route handler.
  • Same pattern as the existing Apotheosis role grant.
  • Suppress errors silently if the player is not in the guild.

inGuild — Bot Event Listeners

The bot should listen for guild member events on the NHCarrigan server (1354624415861833870) and update the Player record if one exists for that Discord ID:

  • guildMemberAdd → look up Player by Discord ID; if found, set inGuild: true
  • guildMemberRemove → look up Player by Discord ID; if found, set inGuild: false

This keeps the flag perfectly in sync without any extra API calls on game load or OAuth beyond the initial seed.


Task Checklist

  • Add inGuild: Boolean to the Player Prisma model
  • Grant Elysian role during OAuth callback; suppress guild membership errors; seed inGuild from result
  • Ensure existing Apotheosis role grant also suppresses guild membership errors
  • Grant Eternal Sovereignty role on first Vampire Mode final ascension
  • Grant Deification role on first Goddess Mode final ascension
  • Bot: listen for guildMemberAdd → update inGuild: true if Player record exists
  • Bot: listen for guildMemberRemove → update inGuild: false if Player record exists

This issue was created with help from Hikari~ 🌸

## Overview Grant players Discord roles automatically as they reach milestones in the game. Roles are awarded via the Discord API using the bot token at key points in the game flow. Also maintains an `inGuild` flag on the Player record (used by #133) via a combination of OAuth-time checks and bot event listeners. --- ## Roles | Role | Trigger | Discord Role ID | |---|---|---| | Elysian | Granted during OAuth flow (any player who authenticates) | `1486144823684628490` | | Apotheosis | First Apotheosis in Base Game | *(existing role — ID already in use)* | | Eternal Sovereignty | First Eternal Sovereignty in Vampire Mode | `1486144657023959180` | | Deification | First Deification in Goddess Mode | `1486144663403495514` | --- ## Implementation ### Elysian - Grant the role at the end of the OAuth callback flow, immediately after the player record is created or retrieved. - Safe to re-attempt on every login (Discord will no-op if the role is already assigned). - **Suppress errors silently if the player is not in the guild** — not being in the server is expected and should never break the auth flow. - Use the success/failure of the role grant to set the initial `inGuild` value on the Player record. ### Apotheosis (existing) - Already implemented — no changes needed. Document the trigger here for reference. - Ensure this also suppresses errors if the player is not in the guild. ### Eternal Sovereignty & Deification - Grant the respective role when the player completes their first Eternal Sovereignty / Deification in the expansion tick or route handler. - Same pattern as the existing Apotheosis role grant. - Suppress errors silently if the player is not in the guild. ### `inGuild` — Bot Event Listeners The bot should listen for guild member events on the NHCarrigan server (`1354624415861833870`) and update the Player record if one exists for that Discord ID: - **`guildMemberAdd`** → look up Player by Discord ID; if found, set `inGuild: true` - **`guildMemberRemove`** → look up Player by Discord ID; if found, set `inGuild: false` This keeps the flag perfectly in sync without any extra API calls on game load or OAuth beyond the initial seed. --- ## Task Checklist - [ ] Add `inGuild: Boolean` to the `Player` Prisma model - [ ] Grant `Elysian` role during OAuth callback; suppress guild membership errors; seed `inGuild` from result - [ ] Ensure existing `Apotheosis` role grant also suppresses guild membership errors - [ ] Grant `Eternal Sovereignty` role on first Vampire Mode final ascension - [ ] Grant `Deification` role on first Goddess Mode final ascension - [ ] Bot: listen for `guildMemberAdd` → update `inGuild: true` if Player record exists - [ ] Bot: listen for `guildMemberRemove` → update `inGuild: false` if Player record exists --- ✨ This issue was created with help from Hikari~ 🌸
Author
Owner

Implementation Notes

Gateway / WebSocket

We only need two events (GUILD_MEMBER_ADD and GUILD_MEMBER_REMOVE), so pulling in discord.js would be overkill. We should handroll a minimal Discord Gateway WebSocket client instead — it's roughly:

  • Open a WebSocket to wss://gateway.discord.gg/?v=10&encoding=json
  • Respond to the HELLO opcode with a heartbeat loop
  • Send an IDENTIFY payload with the bot token and the GUILD_MEMBERS privileged intent (1 << 1)
  • Handle GUILD_MEMBER_ADD and GUILD_MEMBER_REMOVE dispatch events

Important: The GUILD_MEMBERS privileged intent must be enabled on the bot application in the Discord Developer Portal before this will work.

Backfill Script

Before deploying this feature, we need a one-shot script to retroactively sync all existing Player records. The script should:

  1. Fetch all Player records from the database
  2. For each player, call GET /guilds/{guild_id}/members/{discord_id} to check membership
  3. If in the guild: grant the Elysian role and set inGuild: true
  4. If not in the guild: set inGuild: false
  5. Log a summary of how many players were updated

This script must be run once against production before the feature goes live, to ensure inGuild is accurate from day one.

## Implementation Notes ### Gateway / WebSocket We only need two events (`GUILD_MEMBER_ADD` and `GUILD_MEMBER_REMOVE`), so pulling in discord.js would be overkill. We should handroll a minimal Discord Gateway WebSocket client instead — it's roughly: - Open a WebSocket to `wss://gateway.discord.gg/?v=10&encoding=json` - Respond to the `HELLO` opcode with a heartbeat loop - Send an `IDENTIFY` payload with the bot token and the `GUILD_MEMBERS` privileged intent (`1 << 1`) - Handle `GUILD_MEMBER_ADD` and `GUILD_MEMBER_REMOVE` dispatch events **Important**: The `GUILD_MEMBERS` privileged intent must be enabled on the bot application in the Discord Developer Portal before this will work. ### Backfill Script Before deploying this feature, we need a one-shot script to retroactively sync all existing Player records. The script should: 1. Fetch all Player records from the database 2. For each player, call `GET /guilds/{guild_id}/members/{discord_id}` to check membership 3. If in the guild: grant the `Elysian` role and set `inGuild: true` 4. If not in the guild: set `inGuild: false` 5. Log a summary of how many players were updated This script must be run once against production **before** the feature goes live, to ensure `inGuild` is accurate from day one.
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: nhcarrigan/elysium#132