# Portal Pro — Design & Implementation Plan

**Target:** WoW Classic Era / Hardcore (Interface `11509`, patch 1.15.9)
**Audience:** Mages running a portal/teleport service
**Status:** Planning — no code written yet

---

## 1. Platform research — what Classic Era actually allows

Everything below was verified against the live 1.15.9 client environment (installed addons using these
APIs today) and Warcraft Wiki. This section exists because **one restriction fundamentally shapes the
advertising feature**, and it is better to design around it than discover it in week three.

### 1.1 The hard constraint: `SendChatMessage` is partially protected

Since patch 8.2.5 / Classic 1.13.3, `SendChatMessage` requires a **hardware event** (a real mouse click,
mouse wheel, key press, or gamepad input) for these chat types:

| chatType | Hardware event required? | Usable for automation? |
|---|---|---|
| `SAY` | ✅ yes (outdoors) | ❌ |
| `YELL` | ✅ yes (outdoors) | ❌ |
| `CHANNEL` (Trade / General / LFG) | ✅ **always**, indoors and out | ❌ |
| `WHISPER` | ❌ no | ✅ |
| `PARTY` / `RAID` | ❌ no | ✅ |
| `GUILD` / `OFFICER` | ❌ no | ✅ |

**Consequence:** Portal Pro **cannot** post an advert to Trade chat on a timer while you are AFK. Any addon
claiming to do so is either lying or broken. Auto-*whispering* a customer who spoke first is fine — that is
why MageMart (installed here) works.

**Our design response — the "Armed Broadcaster":**

1. The ad is composed, validated and staged in the addon.
2. A big **Broadcast** button, plus a bindable key (`Bindings.xml`), fires it. That click *is* the hardware
   event, so the send is legal and instant.
3. Optional **Armed mode**: the ad is queued and sent on your *next* hardware input of any kind — clicking a
   spell, moving with a keypress, scrolling. Practically this means you press one key while playing normally
   and the ad goes out. This is the same mechanism the `MessageQueue` addon uses; it is compliant, not a bypass.
4. A cooldown ring + "READY" pulse tells you when the next ad is due, so you keep a rhythm without spamming.

This is honest and reliable. It is also a *feature*: a visible ready-timer beats a fire-and-forget timer that
silently fails.

### 1.2 Confirmed-working APIs (verified in this client)

**Chat / leads**
- `CHAT_MSG_SAY`, `CHAT_MSG_YELL`, `CHAT_MSG_CHANNEL`, `CHAT_MSG_WHISPER`, `CHAT_MSG_GUILD`, `CHAT_MSG_PARTY`
- `CHAT_MSG_CHANNEL` arg 7 = channel name → match `general` / `trade` / `lookingforgroup`
- `Ambiguate(sender, "short")` to strip realm
- `GetChannelName()`, `JoinChannelByName()`
- Raid target tokens **do** render in outgoing messages: `{rt1}`–`{rt8}`. Prefer `{rt1}` over `{star}` —
  the word forms are **localised** and break on non-English clients.
- `|cffRRGGBB…|r` colour codes work in chat. 255-char message limit.

**Grouping**
- `C_PartyInfo.InviteUnit(name)` — **not** hardware-gated, safe to automate
- `UninviteUnit`, `LeaveParty`, `GROUP_ROSTER_UPDATE`, `IsInGroup`, `GetNumGroupMembers`

**Gold & trades**
- `TRADE_SHOW`, `TRADE_MONEY_CHANGED`, `TRADE_ACCEPT_UPDATE`, `TRADE_CLOSED`
- `GetTargetTradeMoney()`, `GetPlayerTradeMoney()`, `TradeFrameRecipientNameText:GetText()`
- ⚠️ `TRADE_CLOSED` fires on **both** success and cancel. Do not trust it alone.
  Ground truth = `PLAYER_MONEY` + `GetMoney()` delta, correlated with the trade partner captured at
  `TRADE_SHOW`. Cross-check with `UI_INFO_MESSAGE` → `ERR_TRADE_COMPLETE`.
- Mail refunds/payments: `MAIL_INBOX_UPDATE`, `GetInboxHeaderInfo`, `GetInboxText`, `MAIL_SUCCESS`

**Casting**
- `UNIT_SPELLCAST_SUCCEEDED` (unit `"player"`, gives spellID) — reliable portal-cast counter
- `UNIT_SPELLCAST_START` / `_STOP` / `_FAILED` for the in-progress state

**Spell & item IDs (Classic Era)**

| Destination | Teleport | Portal |
|---|---|---|
| Stormwind | 3561 | 10059 |
| Ironforge | 3562 | 11416 |
| Darnassus | 3565 | 11419 |
| Orgrimmar | 3567 | 11417 |
| Undercity | 3563 | 11418 |
| Thunder Bluff | 3566 | 11420 |

Reagents: **Rune of Teleportation** `17031` (teleports), **Rune of Portals** `17032` (portals).
These give us exact cost-basis for net-profit accounting.

**UI**
- `CreateFrame(..., "BackdropTemplate")` exists — but we mostly won't need it (see §4)
- `Texture:SetColorTexture(r,g,b,a)` — solid fills with **zero art assets**
- Custom `.ttf` fonts and `.tga` textures load fine (MageMart ships NotoSans)
- `AnimationGroup` / `Alpha` / `Translation` / `Scale` for transitions
- `C_Timer.After`, `C_Timer.NewTimer`, `C_Timer.NewTicker`
- `PlaySoundFile(path, "Master")`
- `SavedVariables` / `SavedVariablesPerCharacter`

### 1.3 Deprecated / avoid
- `EasyMenu` + `UIDropDownMenuTemplate` — legacy, being removed. **Roll our own dropdowns/menus.** We want a
  custom look anyway.
- 1.15.9 introduced **Edit Mode** and swapped nameplates/raid frames to the Midnight versions. We touch none
  of that; our frames are self-anchored to `UIParent` with our own drag/scale, so Edit Mode is irrelevant to us.
- 1.15.9's addon API tracks **TBC Classic (2.5.6)**, not retail. Do **not** assume modern namespaces exist —
  wrap `GetSpellInfo` / `C_Spell.GetSpellInfo` and `GetItemCount` / `C_Item.GetItemCount` in a compat shim.

### 1.4 Policy note
Auto-whispering people who asked for a portal in chat is normal and widely used. Blanket-whispering people who
did *not* ask, or hammering Trade chat, gets you squelched by the server spam filter and is a ToS risk.
Portal Pro will ship with conservative defaults: whisper only on a matched request, per-player cooldowns, and a
broadcast throttle. Aggressive settings will be possible but opt-in and clearly labelled.

---

## 2. Feature list

### 2.1 Lead detection & pipeline
- Multi-channel scanner: Say, Yell, General, Trade, LookingForGroup, Guild, Party, Whisper (each toggleable)
- **Destination parser** — resolves `sw`, `stormwind`, `if`, `ironforge`, `darn`, `dt`, `og`, `orgri`, `uc`,
  `undercity`, `tb`, `thunder bluff`, `bluff` → canonical city + faction check
- **Intent matching** with a rules engine, not one `string.find`:
  - positive: `lf port`, `need portal`, `wtb port`, `port to X`, `any mage`, `portal?`
  - negative/anti-match: your own ads, competing mages (`wts port`, `selling port`), `no port`, already-served
- Per-player cooldown so one person spamming Trade doesn't generate ten leads
- **Lead queue**: card per request — name, destination, verbatim message, channel, timestamp, age
- One-click **Whisper / Invite / Dismiss / Blacklist** per card
- Auto-whisper and auto-invite modes (both off by default)
- Blacklist and **Regulars/VIP** list (VIPs get a different greeting and jump the queue)
- Lead expiry with fade-out animation

### 2.2 Advertising
- **Template editor** with live preview rendered exactly as chat will show it
- Variables: `{price}`, `{dest}`, `{city}`, `{me}`, `{tips}` — expanded at send time
- **Icon picker**: click a raid marker to insert `{rt1}`–`{rt8}`; colour picker inserts `|cff…|r`
- Multiple ads with rotation (round-robin or random) so you don't look like a bot
- Per-channel targeting and per-channel throttle
- Armed Broadcaster (see §1.1) — button, keybind, cooldown ring, READY pulse
- 255-char counter with overflow warning before you waste a send
- "Ad performance": leads attributed to the ad that was live when they came in

### 2.3 Whisper templates
Named, editable, variable-expanding templates for each stage:
- **Greeting** (auto-reply to a matched request)
- **Quote** — price per destination
- **Location** — "I'm at the bank, come to me"
- **Invited** — sent alongside the group invite
- **Portal up** — also fires to `PARTY` (unrestricted, so this one *can* be automatic)
- **Thanks** — post-payment, includes tip acknowledgement
- **Busy / Declined**
- Quick-reply buttons on the lead card map to these

### 2.4 Gold tracking & ledger  ← *the headline feature*
- **Transaction record**: timestamp, customer, destination, spell cast, gold in, gold out, reagent cost, net,
  source (trade / mail / manual), notes
- Automatic capture: trade partner + `GetMoney()` delta = truth. Cancelled trades record nothing.
- **Refunds, first-class:**
  - outgoing trade gold to a known customer → auto-flagged as a refund against their last transaction
  - mail with gold attached → matched to customer by sender name
  - manual "Refund" action on any ledger row → writes a linked negative entry, never mutates the original
  - refunds shown as a distinct row type and subtracted from all net figures
- **Reagent cost basis**: watch Rune of Portals/Teleportation consumption → true net profit, not gross
- **Stats**: session / today / this week / all-time
  - gross, refunds, reagent cost, **net**
  - gold per hour (active-session clock, pauses when idle)
  - portals cast, teleports cast, by destination
  - conversion rate: leads → invited → cast → paid
  - average payment, average tip vs. asking price, best destination, top 10 customers
- **Charts** drawn from plain textures: 7-day bar chart, destination breakdown, session sparkline
- Copy-to-clipboard export box (CSV) for anyone who wants it in a spreadsheet

### 2.5 Operations & QoL
- **Reagent watch**: live Rune count in the header, low-stock warning, "you can cast N more portals"
- Auto-message to `PARTY` when the portal lands (legal without a hardware event)
- Post-portal group cleanup: auto-uninvite after configurable delay, or auto-disband
- Cast-blocked warnings: no reagent, wrong faction destination, not enough mana, in combat
- **Hardcore-aware**: rescue-zone detection (STV, Badlands, Tanaris…) with suggested premium pricing;
  a lead from a dangerous zone is highlighted in the queue
- Minimap button (LibDBIcon) + LDB feed showing session net gold
- Sound + screen-flash alerts, individually toggleable
- Full slash command surface (`/pp`, `/portalpro`)
- Per-character and account-wide profiles

---

## 3. Architecture

**Dependency stance: near-zero.** Ace3 pulls in AceGUI, whose widgets look like Blizzard's 2009 options panel —
exactly the aesthetic we're avoiding. We hand-roll a small widget kit (§4) and keep total control.
Only embed: `LibStub`, `LibDataBroker-1.1`, `LibDBIcon-1.0` for the minimap button, and `ChatThrottleLib`.

```
PortalPro/
├─ PortalPro.toc                 # ## Interface: 11509
├─ Bindings.xml                  # keybind for Broadcast / Arm
├─ embeds.xml
├─ Libs/
│  ├─ LibStub/  LibDataBroker-1.1/  LibDBIcon-1.0/  ChatThrottleLib/
├─ Core/
│  ├─ Init.lua                   # namespace, addon table, event dispatcher
│  ├─ Compat.lua                 # GetSpellInfo / C_Spell shims, version guards
│  ├─ Database.lua               # SavedVariables schema, migrations, defaults
│  ├─ Events.lua                 # central registrar → module callbacks
│  └─ Util.lua                   # money formatting, time, string, table helpers
├─ Modules/
│  ├─ Leads.lua                  # chat scan, intent + destination parsing, queue
│  ├─ Destinations.lua           # city table, aliases, spell IDs, faction rules
│  ├─ Broadcaster.lua            # ad templates, arming, hardware-event send, throttle
│  ├─ Whispers.lua               # templates, variable expansion, auto-reply
│  ├─ Ledger.lua                 # transactions, refunds, reagent cost basis
│  ├─ TradeWatch.lua             # TRADE_* + PLAYER_MONEY correlation
│  ├─ MailWatch.lua              # gold received/sent via mail
│  ├─ CastTracker.lua            # UNIT_SPELLCAST_SUCCEEDED → portal records
│  ├─ Group.lua                  # invite, roster, auto-cleanup
│  └─ Stats.lua                  # aggregation, gold/hour, conversion funnel
├─ UI/
│  ├─ Theme.lua                  # design tokens: colour, spacing, radius, type scale
│  ├─ Widgets/                   # Button, Input, Toggle, Slider, Dropdown, Tab,
│  │                             # ScrollList, Card, Tooltip, Chart, IconPicker
│  ├─ MainFrame.lua              # shell, header, tab router, drag, scale
│  ├─ Tab_Leads.lua
│  ├─ Tab_Ads.lua
│  ├─ Tab_Ledger.lua
│  ├─ Tab_Stats.lua
│  ├─ Tab_Settings.lua
│  └─ Toast.lua                  # transient lead alert near the cursor/anchor
├─ Media/
│  ├─ fonts/                     # one clean sans (Inter / Noto Sans SemiCondensed)
│  └─ textures/                  # rounded 9-slice, shadow, gradient, logo
└─ Locales/enUS.lua
```

**Principles**
- Modules never touch each other's state; they publish/subscribe through a tiny internal event bus.
  `TradeWatch` doesn't know the Ledger exists — it fires `PP:Fire("PAYMENT", data)`.
- All persistent writes go through `Database.lua` with a schema `version` field and forward migrations.
- Every user-facing string in `Locales/` from day one, even if enUS is the only locale.
- No global namespace pollution beyond `PortalProDB` and the addon table.

---

## 4. UI design system

Goal: reads as a **modern desktop app**, not a WoW panel. No Blizzard chrome, no gold filigree, no
`UIPanelButtonTemplate`. (We still use Blizzard *spell icons* — those are the correct visual language for
"Portal: Stormwind" and drawing our own would be worse.)

**Tokens** (`UI/Theme.lua`, single source of truth — never hardcode a colour in a widget)

```
surface.base    #14161A  0.95     canvas
surface.raised  #1C1F26           cards, rows
surface.overlay #23272F           popovers, dropdowns
border.subtle   #2E333D
border.strong   #3D444F
text.primary    #E8EAED
text.secondary  #9BA1AC
text.muted      #6B7280
accent          #4C9AFF           Portal Pro blue — primary actions
positive        #34D399           gold in, ready state
negative        #F87171           refunds, errors
warning         #FBBF24           low reagents, throttle
gold            #F0C674           currency figures

radius  4 / 8 / 12       spacing  4 8 12 16 24 32 (4px grid)
type    11 / 12 / 14 / 18 / 24, weights via two font files
```

**Construction technique**
- Panels: `SetColorTexture` fills + 1px hairline borders (four thin textures). Costs nothing, scales perfectly.
- Rounded corners and the outer shadow: one small 9-slice `.tga`, tinted per token — one asset, every surface.
- Custom `.ttf` throughout; never `GameFontNormal`.
- Hover/press/focus states on every interactive element, driven by short `AnimationGroup` alpha fades
  (~120ms). Motion is what separates "modern" from "flat and dead".
- Lead cards slide in and fade out. Gold figures count up on change. The Broadcast button pulses at READY.

**Layout**
- Single resizable main window, 720×480 default, min 560×360, with a **scale slider (60%–150%)** and
  drag-to-resize corner. Position and scale persist.
- Left rail of icon tabs (Leads / Ads / Ledger / Stats / Settings), content pane on the right.
- Persistent header strip: session net gold · portals cast · rune count · Broadcast button + cooldown ring.
- **Compact mode**: collapses to just that header strip — a slim bar you leave on screen while playing.
  This is what most users will actually run day-to-day; design it first-class, not as an afterthought.
- Separate small **toast** frame for incoming leads, independently positioned, with accept/dismiss inline.

---

## 5. Data model

```lua
PortalProDB = {
  version = 1,
  profileKeys = { ["Char - Realm"] = "profileName" },
  profiles = {
    [name] = { ads = {...}, whispers = {...}, prices = {...}, detection = {...}, ui = {...} }
  },
  global = {
    ledger = {            -- append-only; refunds are linked entries, never edits
      { id, ts, customer, dest, spellID, kind = "payment"|"refund"|"reagent"|"manual",
        copper, linkedID, source = "trade"|"mail"|"manual", note }
    },
    customers = { [name] = { first, last, count, totalCopper, vip, blacklisted, note } },
    sessions  = { { start, stop, netCopper, casts, leads, conversions } },
  },
}
```

All money in **copper** as integers — never floats, never strings. Formatting happens at the UI edge only.
Ledger is append-only so history is auditable and refunds can never corrupt an original record.

---

## 6. Build order

| # | Milestone | Contents | Why here |
|---|---|---|---|
| 1 | **Skeleton** | TOC, namespace, event bus, DB + migrations, `/pp`, compat shims | Everything else sits on this |
| 2 | **Design system** | Theme tokens + widget kit + main shell with tabs, drag, scale | Built once, used by every tab; building it late means rewriting every screen |
| 3 | **Leads** | Chat scanner, destination parser, intent rules, queue UI, toasts | The core loop; usable on its own |
| 4 | **Whispers & group** | Templates, variable expansion, auto-reply, invite, cleanup | Completes lead → customer |
| 5 | **Ledger** | TradeWatch + MailWatch + CastTracker + reagent cost, ledger UI, refunds | The differentiator; needs 3–4 to generate data |
| 6 | **Broadcaster** | Ad editor, icon/colour pickers, preview, armed send, keybind, throttle | Self-contained; deliberately after the money features |
| 7 | **Stats** | Aggregation, gold/hour, funnel, charts, export | Needs real ledger data to design against |
| 8 | **Polish** | Minimap/LDB, sounds, HC rescue zones, VIP list, animation pass, locale sweep | |

**Testing:** Classic Era has no unit-test harness, so we build a `/pp debug` panel that injects synthetic chat
events, fake trades and fake mail. That lets us exercise the parser and the ledger — including refund edge
cases — without needing a live customer or a second account. Worth the small cost up front; the alternative is
debugging gold accounting in Orgrimmar at 2am.

---

## 7. Open questions

1. **Multi-flavour?** Plan targets Classic Era `11509` only. Adding SoD/Anniversary is trivial (extra `.toc`);
   Wrath/Cata/retail would need extra destinations (Shattrath, Dalaran, Theramore) and TOC variants.
2. **Cross-mage comms?** `C_ChatInfo.SendAddonMessage` could let a guild's mages avoid double-answering the
   same lead. Genuinely useful, meaningfully more complex. Post-1.0 unless you want it.
3. ~~**Font licence**~~ — **Decided: ship both.** Inter and Noto Sans are both OFL 1.1 and
   redistribute freely, so both are offered and selectable in Settings rather than picking
   one. Registry lives in `UI/Theme.lua`; drop-in instructions in `Media/fonts/README.md`.
   Binaries aren't in the working copy and must be added before a release build.
   Constraint found while implementing: WoW needs a **static** `.ttf` — variable-axis TTFs
   and `.otf` fail silently, so every path is probe-tested rather than trusted.
4. ~~**Distribution**~~ — **Decided: both.** The addon folder is the repo root, so it stays
   directly loadable from `AddOns/` while the same tree packages for CurseForge, Wago and
   WoWInterface via the BigWigs packager on tag push. WoWUp installs from those sources, so
   it needs no separate handling. See `.pkgmeta` and `.github/workflows/`.
