# 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.