# Portal Pro

Portal service assistant for WoW Classic Era mages. Finds customers in chat, handles the
whisper-and-invite dance, and tracks every copper — including refunds and reagent cost, so
the number it shows you is **net**, not gross.

**Target:** Classic Era / Hardcore, Interface `11509` (patch 1.15.9)
**Dependencies:** none

---

## Install

Drop the `PortalPro` folder into:

```
World of Warcraft/_classic_era_/Interface/AddOns/
```

Then `/reload`, or restart the client. Type `/pp` to open.

---

## The thing you need to know first

**Portal Pro cannot post adverts to Trade chat on a timer, and neither can anything else.**

Since patch 8.2.5 / Classic 1.13.3, `SendChatMessage` is partially protected. `SAY`, `YELL`
and `CHANNEL` must originate from a genuine hardware event — a real click or keypress.
`CHANNEL` is restricted indoors and out. Any addon advertising AFK auto-broadcast is either
broken or lying to you.

What Portal Pro does instead:

| | How it works |
|---|---|
| **Broadcast button** | The click *is* the hardware event. One press, ad goes out. |
| **Keybind** | `PORTALPRO_BROADCAST` in Key Bindings. Also a hardware event. |
| **Arm mode** (opt-in) | Stages the ad; your next keypress while playing releases it. |

Every send still happens inside a real input handler. Arming just means you don't have to
aim for a button. A cooldown bar under the button shows when the next ad is due.

Whispers, party, raid and guild are **not** restricted — so auto-replying to someone who
asked for a portal, and announcing "portal's up" to your party, genuinely do happen on
their own.

---

## Features

**Leads** — scans Say, Yell, General, Trade, LFG, Guild, Party and Whispers. Intent is
scored against a rule set rather than a substring match, so "selling ports to SW" (a
competitor) and "the portal room" don't generate leads. Parses destinations from
abbreviations (`sw`, `if`, `og`, `uc`, `tb`, `darn`). Per-player cooldown, blacklist, VIP
list, and returning-customer detection.

**Ads** — multiple templates with rotation, `{price}` style variables, a raid-icon picker
that inserts `{rt1}`–`{rt8}` (not `{star}`, which is localised and breaks on non-English
clients), colour codes, live preview rendered exactly as chat will show it, and a 255-char
counter.

**Whispers** — templates for greeting, quote, location, invited, portal-up, thanks and
busy, with variable expansion and quick-reply buttons on every lead card.

**Ledger** — append-only. Payments are captured from trades and mail by watching the actual
`GetMoney()` delta, because `TRADE_CLOSED` fires on cancelled trades too and would
otherwise book them as income. Refunds are linked negative entries that never edit the
original sale. Rune consumption is recorded as cost basis. CSV export.

**Stats** — net / gross / refunds / reagents, gold per hour on an idle-pausing session
clock, leads→invited→paid funnel, 7-day chart, destination breakdown, top customers.

**Ops** — rune counter with low-stock warning, party announce on cast, optional group
cleanup after each portal, minimap button, compact always-on bar, Hardcore rescue-zone
highlighting.

---

## Commands

| Command | Does |
|---|---|
| `/pp` | Toggle the window |
| `/pp compact` | Toggle the slim bar |
| `/pp on` / `/pp off` | Enable/disable chat detection |
| `/pp leads` | List the queue |
| `/pp price sw 12g` | Set a destination price |
| `/pp prices` | Show current pricing |
| `/pp stats` | Session summary |
| `/pp export` | Dump the ledger as CSV |
| `/pp test lf port to sw` | Score a message against the lead rules |
| `/pp sandbox` | Open the test harness |
| `/pp debug` | Toggle verbose logging |

---

## Testing without customers

`/pp sandbox` opens a panel that injects synthetic leads, payments, refunds and casts.
Classic Era has no unit-test runner, and the alternative is debugging gold accounting in
Orgrimmar at 2am with a real customer waiting. It also has a scorer so you can paste a chat
line and see exactly which rules fired and why it did or didn't match.

Nothing in the sandbox touches other players.

---

## Notes on defaults

Auto-whisper and auto-invite ship **off**. Whispering people who asked for a portal is
normal; blanket-messaging people who didn't gets you squelched by the server spam filter
and is a ToS risk. The aggressive settings exist and are one click away — they're just not
the default.

---

## Build notes

Two deliberate deviations from `PLAN.md`:

1. **Zero libraries.** The plan called for embedding LibStub, LibDataBroker, LibDBIcon and
   ChatThrottleLib. The minimap button turned out to be ~80 lines hand-rolled, and rolling
   it ourselves means it matches the theme instead of Blizzard's bevelled ring. Broadcast
   volume is low enough that our own throttle covers what ChatThrottleLib would. The cost
   is no Titan Panel / Bazooka integration.

2. **Square corners.** Rounded corners need a 9-slice texture, which couldn't be authored
   here. The theme instead builds everything from solid fills and 1px hairlines — a
   legitimate flat-UI look that needs no art assets and stays sharp at any scale. The
   Broadcast cooldown is a progress bar rather than the planned ring, for the same reason.

Everything else follows the plan. Milestones 1–8 are implemented.

---

## Fonts

Both **Inter** and **Noto Sans** are supported and selectable under
**Settings → Interface → Interface font**. Both are SIL OFL 1.1, so both can ship in the
release zip.

The binaries are **not in this working copy** — add them once:

1. Download Inter or Noto Sans.
2. Rename to `Inter.ttf` / `NotoSans.ttf`.
3. Drop into `Media/fonts/`.
4. Copy the font's `OFL.txt` into `Media/licence/`.

It **must be a static `.ttf`.** Variable-axis TTFs (Inter's headline download) and `.otf`
files fail *silently* in WoW — `SetFont` appears to succeed and nothing renders. Portal Pro
measures a probe string rather than trusting `SetFont`, so an unusable file shows as
"not installed" in the picker instead of blanking the interface.

Full detail, including which file to take out of the Inter archive:
`Media/fonts/README.md`.

Without any font installed the addon falls back to Arial Narrow and works fine.

---

## Development & release

The addon folder is also the repo root, so it stays directly loadable from `AddOns/` while
being packaged from the same tree.

```
.pkgmeta                     BigWigs packager config
.github/workflows/release.yml  tag-triggered build + upload
.github/workflows/lint.yml     luacheck on every push
.luacheckrc                  WoW API globals so luacheck is usable
CHANGELOG.md                 shipped as the release notes
```

**To cut a release:**

1. Add the project on CurseForge / Wago / WoWInterface as desired.
2. Uncomment and fill the matching `X-*-ID` lines in `PortalPro.toc`.
3. Add repo secrets for whichever you're using: `CF_API_KEY`, `WAGO_API_TOKEN`,
   `WOWI_API_TOKEN`. Each upload is skipped when its token is absent, so you can enable
   one at a time. `GITHUB_TOKEN` is automatic.
4. `git tag v1.0.0 && git push --tags`.

The packager substitutes `@project-version@` in the TOC with the tag. Running from a
working copy leaves the token unreplaced; `Core/Compat.lua` detects that and reports the
version as `dev` rather than showing a literal keyword.

**WoWUp** installs from CurseForge, Wago and GitHub releases, so it's covered by the same
pipeline — no separate step.
