# mod-eternal-bond — Plan

FFXIV-inspired wedding/marriage system: propose with a ring, plan a wedding, hold a scripted
ceremony at a faction venue, come away with matching bands and a lasting perk (spouse recall).
Built AC-native rather than importing FFXIV terminology wholesale, similar in spirit to how
[mod-hirelings](../mod-hirelings/PLAN.md) reflavored FFXIV retainers as goblin-contracted agents.

## Design pillars

- **Ceremony-first.** The scripted multi-NPC finale (vows, ring exchange, guest reactions) is the
  centerpiece, not an afterthought bolted onto a quest turn-in. Modeled directly on
  `src/server/scripts/Northrend/Ulduar/HallsOfStone/brann_bronzebeard.cpp`'s gossip-choice-seeds-
  `EventMap` pattern.
- **Cosmetic-primary, one mechanical perk.** Matching Wedding Bands + ceremony are the core
  reward. The one mechanical addition is a long-cooldown **spouse recall** teleport (a
  hearthstone-to-your-spouse), FFXIV's standout feature from the original system -- everything
  else stays vanity so this doesn't become a power-creep item.
- **No title/achievement system in v1.** Both `CharTitlesEntry` and `AchievementEntry` are
  DBC-only (`src/server/game/DataStores/DBCStores.cpp`) -- no SQL/runtime path to add a new one,
  and no module in this repo has ever shipped a patched client DBC. A server-wide chat
  announcement + visual flourish at ceremony completion is the deliberate cosmetic stand-in.
  Revisit real titles only if DBC-patch distribution infra is ever built for some other reason.
- **Consent-gated, not one-sided.** Both partners must actively agree before anything commits --
  modeled on `Player.h`'s `DuelInfo`/`DuelState` (challenge -> countdown -> in-progress -> complete),
  but using `ObjectGuid` instead of raw `Player*` since a proposal can span a logout/login, unlike
  a duel's single continuous session (per this repo's AGENTS.md long-lived-reference rule).

## Key technical findings

- **Rings are pure SQL.** `item_template` (`data/sql/base/db_world/item_template.sql`) supports
  everything needed with no C++: `bonding` (BoP), `ItemLimitCategory` (+ a new
  `itemlimitcategory_dbc` row for "only one equipped at a time"), and `spellid_1`/`spelltrigger_1`
  for an on-use proc slot -- confirmed real columns.
- **Consent handshake state must outlive a single session.** Unlike a duel, a proposal target may
  be offline when the ring is used. Store `BondProposal { ObjectGuid proposer; ObjectGuid target;
  time_t expiresAt; BondProposalState state; }` in a module-local
  `std::unordered_map` keyed by target, resolve players at use-time via
  `ObjectAccessor::FindPlayer()`. A full server restart drops pending proposals silently -- a
  known, documented v1 gap, not something to quietly swallow if it surprises a player later.
- **Temp NPC pattern**: `TEMPSUMMON_TIMED_DESPAWN` with a duration in ms for officiant/guest NPCs
  during the ceremony window (`src/server/scripts/EasternKingdoms/zone_undercity.cpp:137`).
- **This repo's modules use raw `WorldDatabase.Query`/`CharacterDatabase.Query` with `{}`
  StringFormat placeholders**, not `PreparedStatement` -- a confirmed local deviation from
  AGENTS.md's general guidance, followed by every installed module here. Follow suit for
  consistency, not AGENTS.md's PreparedStatement default.
- **ID range 940000s confirmed clean**: after mod-dragon-legacy (900000-901099), mod-waygate-network
  (910000-911001), mod-hirelings (920000-921099), mod-bounty-board (930000-931099) -- see this
  module's `mod_eternal_bond.h` for the exact reserved sub-ranges. Check all four before
  reassigning.

## Venues (decided, 2026-08-16)

- **Alliance**: Stormwind Cathedral of Light, main hall near the altar, approximately
  `(-8457, 871, 121, map 0)`. **Not yet verified in-game** -- run `.gps` at the intended spot
  before placing the Matchmaker/Officiant NPCs.
- **Horde**: Thunder Bluff, Elder Rise, approximately `(-1256, 22, 129, map 1)`. **Not yet
  verified in-game** -- same caveat.

Cross-faction bonding is explicitly **not supported in v1** (it needs its own venue-selection
design -- which chain does a cross-faction pair use?); same-sex bonding is unrestricted by default
since nothing in the design checks partner gender.

## Quest chain (six quests, faction-paired IDs)

| # | Quest ID (A/H) | Title | Gate | Giver -> Ender |
|---|---|---|---|---|
| 1 | 941000/941001 | "A Question Worth Asking" | Formalizes a proposal already accepted via the ring's consent handshake | Matchmaker NPC (venue) |
| 2 | 941010/941011 | "Book the Cathedral" / "Speak to the Elders" | Travel to venue-booking NPC | Matchmaker -> Venue NPC |
| 3 | 941020/941021 | "Meet the Officiant" | Talk to Officiant (942000/942001), permanently placed at venue | Venue NPC -> Officiant |
| 4 | 941030/941031 | "Something Borrowed" (optional flavor, cuttable without renumbering) | Turn in an existing cheap flavor item | Officiant -> Officiant |
| 5 | 941040/941041 | **"The Ceremony of Eternal Bonding"** (finale) | Both partners present at venue, both prior quests done | Officiant -> Officiant |

Quest 5's *accept* triggers the Officiant's scripted `EventMap` ceremony; *completion* (after the
sequence finishes) grants the Wedding Band pair, writes the bond row, and fires the announcement.

## Items

**Engagement Ring (943000)** -- `INVTYPE_FINGER`, `bonding = 1` (BoP on pickup, can't be traded to
fake a proposal), no stats, `ScriptName = item_eternal_bond_engagement_ring` routing to
`ItemScript::OnUse` (not a spell proc) for the consent handshake. Consumed only on **successful**
proposal (`player->DestroyItemCount()` after both sides accept) -- not consumed on decline/timeout.
Sold by the Matchmaker NPC (price TBD).

**Wedding Band pair (943001/943002)** -- `INVTYPE_FINGER`, `bonding = 1`, minimal cosmetic stats,
`ItemLimitCategory` -> new `itemlimitcategory_dbc` row (`Quantity = 1`, equip-mode flag) so only
one can be *equipped* at a time without hard-blocking ownership (a dissolved-bond player keeps it
as a keepsake). `spellid_1`/`spelltrigger_1` = on-use trigger for spell 940000, the spouse-recall
perk -- **recommend the actual recall logic live in the band's `ItemScript::OnUse`, not a real
`SpellScript`**, sidestepping the need for genuine DBC spell-effect data; spell 940000 is then
just an informational/cooldown-icon placeholder. Confirm this choice at implementation time.
Granted as a matched pair only via the finale quest reward, never purchasable.

## DB schema

**Characters DB -- `mod_eternal_bond_pairs`** (modeled on `mod-waygate-network`'s
`waygate_network_discovered`):

```sql
CREATE TABLE `mod_eternal_bond_pairs` (
  `guid_a` int unsigned NOT NULL,
  `guid_b` int unsigned NOT NULL,
  `bonded_time` int unsigned NOT NULL DEFAULT 0,
  `dissolved_time` int unsigned DEFAULT NULL,
  PRIMARY KEY (`guid_a`, `guid_b`),
  KEY `idx_guid_b` (`guid_b`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

Always store `guid_a < guid_b` (enforced in C++ before insert) so lookup is a single
`WHERE (guid_a=? OR guid_b=?) AND dissolved_time IS NULL`. `dissolved_time` is the annulment path:
**v1 ships GM-only** `.eternalbond divorce `, which *sets* (not deletes) this column,
preserving history. Player-initiated mutual-consent annulment is explicitly deferred (it would
need its own consent handshake, not worth building before the core ceremony is proven). Clean up
rows referencing a deleted character in `OnPlayerDeleteFromDB`, mirroring
`WaygateNetwork_Player::OnPlayerDeleteFromDB`.

**Venue coordinates**: hardcode as `constexpr` in `mod_eternal_bond.h` for v1 (only two venues
exist) rather than building a `mod_eternal_bond_venues` table -- promote to a real table only if a
third venue is ever wanted.

## C++ class design (for v0.1 onward)

- **`item_eternal_bond_engagement_ring : ItemScript`** -- `OnUse` validates target-selected player
  (via `SpellCastTargets`): not self, not already bonded (DB lookup), same faction unless
  configured otherwise, not a playerbot (`#ifdef MOD_PLAYERBOTS` guard, same idiom as
  `mod-waygate-network`). Inserts `BondProposal{Pending}`, messages the target with
  `.eternalbond accept/decline` instructions, schedules a timeout via
  `target->m_Events.AddEventAtOffset(...)` (same idiom as `mod-waygate-network`'s
  `BeginWaygateChannel`).
- **`EternalBondCommandScript : CommandScript`** -- extend the v0 stub with
  `.eternalbond accept|decline|status|divorce ` (divorce is GM-only).
- **`npc_eternal_bond_officiant : CreatureScript` + `CreatureAI`** -- gossip ->
  `DoAction(ACTION_START_CEREMONY)` on finale quest accept. `EventMap events` drives
  Talk/emote/guest-NPC-summon beats over ~3 minutes, ending in `EVENT_CEREMONY_COMPLETE`: writes
  the pairs row, grants bands, completes the quest, fires `sWorld->SendServerMessage(...)` + a
  visual flourish (`SendPlaySpellVisual` + `HandleEmoteCommand`, same idiom as Waygate
  attunement). Each tick re-resolves both partners via `ObjectAccessor::FindPlayer`; if either
  disconnects or wanders off, abort gracefully (despawn guests, message the remaining partner,
  leave the quest re-attemptable).
- **`EternalBond_Player : PlayerScript`** -- `OnPlayerLogin` re-sends the accept/decline reminder
  if a pending proposal targets this player. `OnPlayerDeleteFromDB` cleans `mod_eternal_bond_pairs`.
- **`EternalBond_World : WorldScript`** -- already has `OnStartup()` from v0; no
  `OnLoadCustomDatabaseTable()` needed while venues stay hardcoded.

## Spouse recall design

Triggered by the Wedding Band's on-use slot:

1. Cooldown via item/spell cooldown system (default 1h, `EternalBond.RecallCooldownSeconds`) --
   keeps the client's own cooldown swirl in sync for free.
2. Blocked in combat (`player->IsInCombat()`, same idiom as `BeginWaygateChannel`).
3. Blocked to/from instanced content (`Map::IsDungeon()`, `IsBattlegroundOrArena()`) -- checked on
   both caller's and spouse's map.
4. Spouse lookup via `mod_eternal_bond_pairs` + `ObjectAccessor::FindPlayer`. Offline or
   unreachable (instanced) spouse -> chat message, **no-op, cooldown not consumed** (don't punish
   a failed attempt). Otherwise `player->TeleportTo(spouse's map/x/y/z)` -- no same-map
   restriction needed, `TeleportTo` handles cross-continent.
5. Arrival flourish: `SendPlaySpellVisual` + emote, chat line to the spouse.

Deferred: opt-out toggle for being a recall target.

## Status

- **v0 (this session, not yet in-game tested):** hello-world scaffold only. Module loads
  (`EternalBond_World::OnStartup` logs a confirmation), `EternalBond.Enable` config flag reads
  correctly, `.eternalbond` command registers and responds with a placeholder line. No DB schema,
  no items, no quests, no ceremony yet.

## Roadmap (agreed direction, 2026-08-16)

1. **v0.1 -- consent handshake vertical slice.** Engagement Ring SQL + `OnUse` +
   `.eternalbond accept/decline` + in-memory `BondProposal` map + `mod_eternal_bond_pairs` table,
   written directly on accept (no ceremony yet). This proves the riskiest new mechanism --
   cross-session two-player consent -- before any content investment. Full config block
   (`EternalBond.AllowSameSex`, `AllowCrossFaction`, `ProposalTimeoutSeconds`, etc. -- see below)
   should land alongside this phase as each option becomes load-bearing, not all at once in v0.
2. **v0.2 -- quest chain, no ceremony.** All six quests wired with placeholder finale text. Proves
   NPC placement (after `.gps` verification of both venues) and quest gating
   (`GetQuestRewardStatus` prerequisites) for both factions.
3. **v0.3 -- Officiant EventMap ceremony.** The real scripted sequence, Wedding Band grant,
   announcement. Centerpiece, deliberately last.
4. **v1 -- spouse recall perk.** Cooldown, combat/instance gating, graceful offline handling,
   flourish.

Eventual full config surface (add incrementally, not all in v0):

```
EternalBond.Enable = 1
EternalBond.AllowSameSex = 1
EternalBond.AllowCrossFaction = 0
EternalBond.ProposalTimeoutSeconds = 300
EternalBond.CeremonyDurationSeconds = 180
EternalBond.RecallCooldownSeconds = 3600
EternalBond.RecallVisualKitId = 3394        # reuse the Teleport ImpactKit already vetted in mod-waygate-network
EternalBond.AnnounceServerWide = 1          # cosmetic stand-in for the skipped title/achievement system
```

## Later ideas (explicitly deferred past v1)

- Player-initiated mutual-consent annulment (v1 ships GM-only `.eternalbond divorce` instead).
- Promoting hardcoded venue coordinates to a `mod_eternal_bond_venues` DB table, if a third venue
  is ever wanted.
- Cross-faction bonding support (needs its own venue-selection design).
- Recall opt-out toggle (some players may not want to be surprise-teleported to).
- The cuttable "Something Borrowed" flavor quest (#4 in the chain) -- can be dropped entirely
  without renumbering if it turns out to be filler.

## Open questions for whoever picks up v0.1 next

- Exact flavor/dialogue text for the Matchmaker and Officiant NPCs, and the ceremony's actual
  vows/beats -- not written yet, this plan covers mechanics only.
- Engagement Ring purchase price and vendor flow specifics.
- Whether the recall lives in `ItemScript::OnUse` directly (recommended above, simplest) or a real
  `SpellScript` -- confirm before writing spell 940000's DBC-adjacent plumbing.
- Both venues' exact coordinates need `.gps` verification in-game before NPCs are placed --
  current numbers are best-effort from known WotLK geography, not confirmed against this server's
  data.