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