File Details
v0.3.0-beta.1
- B
- May 13, 2026
- 942.07 KB
- 2
- 12.0.7+4
- MoP Classic + 1
File Name
SplitWatch-v0.3.0-beta.1.zip
Supported Versions
- 12.0.7
- 12.0.5
- 12.0.1
- 12.0.0
- 5.5.0
SplitWatch
v0.3.0-beta.1 (2026-05-12)
Full Changelog Previous Releases
- Preview: anchor warnings line + separator below the longest team
User report: 'tout est bon — aucun avertissement.' overlapped with team
rows when the test roster was bumped to 40-man. Cause: warnFS was at a
fixed Y=-540 but a 40-man team has 20 rows that extend down to ~-680.
Reposition both the warnings line and the vertical separator in refresh
based on max(#teamA, #teamB):
warnFS: TOPLEFT y = -(298 + rowCount * 20 + 24)
separator: TOPLEFT y = -270 (anchor unchanged)
BOTTOMLEFT y = -(298 + rowCount * 20 + 10)
For rowCount=5 → warnings ~ -422, separator stops at ~-408.
For rowCount=20 → warnings ~ -722, separator stops at ~-708.
The page content height calc (380 + rowCount * 20) still covers both
positions comfortably. - Move test-size slider + auto-rebalance toggle to Aperçu next to buttons
User: 'les options que tru as ajouté devrais être dans apercu! a côté du
bouton en question tu est illogique!'
Two options previously dumped on Réglages → Général:- 'Auto-rebalance after manual swap' — affects the click-swap UX which
happens on Aperçu only. - 'Test roster size' — only meaningful when the Aperçu test toggle is
on.
Moved both to Aperçu right under the three-button row (Calculer / Apply
/ Test toggle), at y=-68: checkbox on the left at x=14, slider on the
right at x=342 (under the Test button it gates).
Layout shift downstream — everything below the buttons row pushed by
+40px so the new option row doesn't overlap: - modeFS -68 → -108
- srcFS -86 → -126
- dirtyBanner -104 → -144
- Before/After labels -140 → -180
- Stats lines -166 → -206
- Team titles -230 → -270
- Vertical separator y range -230..-490 → -270..-530
- Warnings line -500 → -540
- Content-height base in refresh: 260 → 300 (titles new origin)
- 'Auto-rebalance after manual swap' — affects the click-swap UX which
- Test mode: configurable roster size 10-40 with dynamic role ratios
Replace the fixed 20-man TEST_ROSTER with three role-keyed pools
(TEST_TANKS 4, TEST_HEALERS 10, TEST_DPS 30 — 44 total fictional
class-themed names). Roster:Scan in test mode now reads the user's
chosen size from db.testRosterSize (default 20) and slices each pool
to the count computed by targetComposition():
10-man → 2T 2H 6DPS
15-man → 2T 3H 10DPS
20-man → 2T 4H 14DPS (existing default)
25-man → 2T 5H 18DPS
30-man → 3T 7H 20DPS
40-man → 3T 9H 28DPS
Roster:GetTestSize() clamps to [10, 40].
UI: NEW-badged slider on Réglages → Général (under the auto-rebalance
toggle) labelled 'Taille du raid test', range 10-40 step 1. Tooltip
spells out the ratio scale so the user knows what to expect.
The mode status line on Aperçu now reads 'Mode test ACTIF (%d simulés)'
instead of the hard-coded 20. - Manual swap: direct in-place by default + auto-rebalance toggle
User report: 2-click manual swap on Aperçu was also moving OTHER players
('l'autre groupe se mets à jour et reprend un joueur'). Cause: the
swap path called recomputeAndRefresh which re-runs Splitter — the snake
redistributes every non-locked player against the new score balance,
so beyond the swapped pair, other DPS shift around too.
Two changes:- Default swap path is now a DIRECT in-place mutation of lastSplit:
- pull both players from their current team arrays,
- swap their .team property and re-insert in the opposite team,
- recount tanksA/healsA/dpsA/scoreA + B equivalents from the new
composition (so titles, the After block, and warnings stay
coherent), - SetLock both so a future explicit Calculer le split preserves
the user's choice.
No call to Splitter:Compute — every other player stays exactly
where they were on screen.
- New Réglages toggle 'Auto-rebalance after manual swap'
(db.autoRebalanceAfterSwap, default false). When ON, the swap
path runs recomputeAndRefresh instead of the direct path — RL
gets the old behaviour back if they want the algorithm to
redistribute against the new locks.
- Default swap path is now a DIRECT in-place mutation of lastSplit:
- Swap UX: OnClick two-step selection, not OnMouseDown/OnMouseUp drag
Previous attempt used OnMouseDown to arm dragSource and OnMouseUp on
another row to commit the swap. Doesn't work reliably on Button frames
with RegisterForClicks: WoW's click-detection consumes the mouse-up on
the same frame the mouse-down landed on, so OnMouseUp on a different
target rarely fires the swap branch. User reported: 'le clique gauche- clique sur l'autre joueur fonctionne pas'.
Switch to OnClick two-step:
- 1st left-click on a row: arms dragSource + dims the row's text to 0.5.
- 2nd left-click on a different-team row: performs the swap via auto-locks.
- Click same row again: cancels (re-shows alpha 1).
- Click another row on the SAME team: re-arms onto the new selection
(lets the RL change their mind mid-flow).
Right-click still opens the lock context menu via OnMouseUp.
Tooltip hint reworded: 'Left-click to select, then left-click an
opposing-team player to swap.' FR translation matches.
Removed the stale broken drag block left over from the prior attempt.
- clique sur l'autre joueur fonctionne pas'.
- PLAYER_LOGIN: wipe persisted lastSplit (test-mode ghost data)
User report: opening the panel after a /reload showed the team columns
already populated with the test-mode 20-man roster, as if test mode was
'directly activated without it being [on]'.
Root cause: lastSplit was in Defaults so its value persisted via
SavedVariables. After a /reload, Roster._testMode resets to false (file-
load default), but the previously-computed split (cached in DB) is still
rendered by the Preview refresh. The user sees test rows without having
toggled test mode this session.
Fix: clear db.lastSplit at PLAYER_LOGIN. The proposed split is runtime
state — it should never survive a reload. lastAppliedSplit (used by the
diff indicator) stays persisted because it represents an explicit Apply
commit and matters across sessions. - Lock context menu: MenuUtil (retail 12.x) with UIDropDownMenu fallback
Bug: EasyMenu was removed in retail 12.x. Right-clicking a player row in
the Aperçu team columns raised 'attempt to call a nil value' at the
EasyMenu(...) line.
Replace with MenuUtil.CreateContextMenu when available (modern retail
path: cleaner anchored-to-owner-frame placement, SetEnabled gating per
button). Fall back to UIDropDownMenu_Initialize + ToggleDropDownMenu for
Mists Classic 5.5 which still ships the legacy helpers.
Row OnMouseUp now passes self as ownerFrame so MenuUtil anchors the
context menu next to the clicked row instead of the global cursor. - Preview rows: Button not Frame so RegisterForClicks works
RegisterForClicks is Button-only; calling it on a plain Frame raises
attempt to call a nil value, aborting the renderTeamRows loop on the
very first row. Result: no rows ever paint, the (vide) placeholder
stays visible. User stack pointed at Panel.lua:1118 inside the row
creation block. Switching CreateFrame('Frame', ...) to CreateFrame
('Button', ...) — Button inherits every Frame script we wire
(OnEnter, OnLeave, OnMouseDown, OnMouseUp) so behaviour is identical
otherwise. - Preview rows: explicit framelevel + earlier empty-state clamp
Despite the previous fix dropping _registerInSection(row), the test-mode
roster still rendered as '(vide)' for both teams. Two defensive changes:- Bump per-row framelevel to parent + 10. Without an explicit level,
rows inherit the page content frame's level and end up at the same
stratum as the section.container that holds the emptyA / emptyB
placeholders. If the section's frame layers happen to draw at a
higher level, the row text gets shadowed even when emptyA is
conceptually Hidden. - Clamp emptyA:Hide / emptyB:Hide BEFORE renderTeamRows runs. The
previous order called Show/Hide on the empties AFTER rendering;
if renderTeamRows ever errored partway, emptyA could remain
Shown from a prior refresh. Hiding first guarantees the (vide)
text is gone before the row text paints over it.
- Bump per-row framelevel to parent + 10. Without an explicit level,
- Fix Preview content height for 20+ man + capture conventions in CLAUDE.md
Bug: warnings line and the tail of team rows at high raid counts (20+
man) were rendered outside the ScrollFrame's reachable viewport and
silently clipped. makePage's deferred sizing only counts registered
sections; Preview's team rows + warnings are direct children of the
content frame (per the _currentSection-leak fix) so they didn't enter
that calculation.
Fix: buildPreviewPage's refresh now computes a needed-height based on
max(#teamA, #teamB) and expands parent:SetHeight() if the current value
is smaller. Formula: 260 (titles at -230 + ~30 inset) + rowCount * 20 +
80 (warnings + margin).
Captures three conventions in CLAUDE.md (restored after the prior
gitignore-untrack removed the local copy; the file is still .gitignore'd
but reborn locally from the last tracked snapshot):- _currentSection cross-page leak: do NOT _registerInSection lazy
widgets. Use the three safe patterns documented. - Destructive actions confirm via StaticPopup_Show. Compute/Apply/Save
stay one-click. - RL decides — no auto-action on external WoW triggers
(GROUP_ROSTER_UPDATE etc set a flag + surface a button, never
silently re-run the algorithm). - Page content height must be expanded explicitly when a page adds
lazy direct children of the content frame.
Memory entries written in parallel (project_section_leak_gotcha,
feedback_destructive_actions_confirm, feedback_no_auto_action_rl_decides)
- MEMORY.md index updated.
- _currentSection cross-page leak: do NOT _registerInSection lazy
- Fix: Preview team rows invisible after Compute (cross-page section leak)
renderTeamRows was calling _registerInSection on each row at render time
(i.e. when the user clicks 'Compute split'). But the module-level
_currentSection variable points at the LAST section built across all
pages — typically an About section, since build order is Setup →
Composition → Preview → About. So the rows ended up parented to About's
section container, which is hidden whenever the Preview tab is active.
User symptom: clicking Aperçu then Compute showed empty team columns
(the rows existed but lived on a hidden parent).
Fix: drop the _registerInSection call inside renderTeamRows. Rows stay
parented to the page content frame, which the tab Show/Hide pipeline
already cascades correctly. Trade-off: collapsing the Preview section
no longer hides the team rows. Acceptable — collapsing the only section
on the Preview tab is rare, and the title still hides correctly.
Same lazy-render path on Réglages source-preview and Composition active
locks is unaffected: those rows live inside a ScrollFrame's content
child, and only the ScrollFrame is registered (during build, when
_currentSection is correct). The rows nest naturally inside. - v0.3.0 cont'd: built-in preset library + roster-change banner + destructive confirmations
Built-in presets:- SplitW.BUILTIN_PRESETS table with 4 ready-to-load configs:
Spirit Kings (Mass Dispel + Decurse + MR + Externals),
Lei Shen (MR + Soak + Externals),
Council of Elders (Decurse + Externals),
Conclave of Wind (MR + Externals). - SplitW:RestoreBuiltinPresets() copies them into db.presets, overwriting
same-name entries. RL can edit / delete as user presets thereafter. - Réglages → Presets gains a 'Restore built-ins' button next to Save.
StaticPopup confirms the overwrite.
Roster-change banner (RL-controlled, not auto-recompute): - SplitWatch.lua registers GROUP_ROSTER_UPDATE. When fired AND a lastSplit
exists, SplitW._rosterDirty = true and RefreshAll is called. - Aperçu renders a yellow Backdrop banner (line + inline 'Recompute'
UIPanelButtonTemplate) between srcFS and Before/After. Banner is
hidden when _rosterDirty is false. Clicking Recompute re-runs Splitter
and clears the flag. - computeBtn and recomputeAndRefresh (drag/lock-menu paths) also clear
the flag so the banner disappears after any compute, not just the
one inside the banner. - Layout shift: Before/After labels y=-110 → -140, stats y=-136 → -166,
team titles y=-200 → -230, separator y/range shifted, warnings y=-460
→ -500. The banner sits at y=-104 height 28.
Destructive-action confirmations (StaticPopup): - 'Clear all locks' → SPLITWATCH_CONFIRM_CLEAR_LOCKS popup
- 'Reset all weights' → SPLITWATCH_CONFIRM_RESET_WEIGHTS popup
- 'Delete preset' per-row → SPLITWATCH_CONFIRM_DELETE_PRESET with
per-instance .data so the popup OnAccept knows which preset to drop. - Built-in restore also confirms (SPLITWATCH_RESTORE_PRESETS) because
it overwrites same-name user presets.
Locales + CHANGELOG.md + in-addon changelog tab all updated with FR
translations for every new string.
- SplitW.BUILTIN_PRESETS table with 4 ready-to-load configs:
- v0.3.0 cont'd: 2 new constraints + diff indicator + drag-and-drop swap
Constraints:- CONSTRAINTS table extended with requireRole field so a class set can be
filtered by entry.role (the EXTERNAL constraint requires a HEALER, not
any Pala/Priest/Druid/Monk). teamHasClass(team, classSet, requireRole)
reflects that filter; ensurePresence picks the swap role from
constraint.requireRole (defaults to DAMAGER). - EXTERNAL: Paladin / Priest / Druid / Monk healers (BoP, Pain Sup,
Ironbark, Life Cocoon) — ≥1 per team. Swap candidates are HEALER role
only so we don't move a Ret Pally to satisfy 'external CD'. - SOAK: Paladin / Mage / Hunter immunities (Divine Shield / Ice Block /
Aspect of the Turtle) — ≥1 per team, swap candidates are DPS. - Two new NEW-badged checkboxes on Composition.
Diff indicator: - Apply.lua snapshots the just-applied split into SplitWatchDB.lastAppliedSplit
on Apply queue completion ({ [name] = 'A' | 'B' }). - buildRowText prepends an orange arrow inline (UI-SpellbookIcon-NextPage)
next to any player whose current-split team differs from their team in
lastAppliedSplit. New players (not in lastAppliedSplit) get no marker —
only churn relative to what was last actually applied is highlighted.
Drag-and-drop swap: - Per-row Frames in the Preview team columns now handle OnMouseDown +
OnMouseUp. Left-click down marks the row as drag source and dims its
text to 0.5 alpha. Left-click up on any row clears the drag state; if
the target is on the OTHER team, SplitW:SetLock is called on BOTH
players with their new teams, then recomputeAndRefresh runs. The pair
is now pinned, so the swap survives every subsequent recompute. - Right-click path unchanged — still opens the lock menu.
- Tooltip gains a second hint line: 'Left-click + click another team's
player to swap'.
Layout: Manual weights section on Composition pushed to y=-460 (was -400),
action buttons to -800 (was -740), to make room for the 2 new constraint
toggles.
- CONSTRAINTS table extended with requireRole field so a class set can be
- v0.3.0 polish: locks UI + presets UI + spec inspect + class-list fixes
UI buttons for every v0.3.0 feature, deferred items shipped, bug fixes
to the constraint class lists, and a tab rename for logical clarity.
Bug fixes:- Battle Rez constraint: removed Hunter / Paladin / DH (none of them have
an in-combat resurrection in retail). Only Druid / DK / Warlock remain. - Decurse constraint: removed Monk (Detox handles Magic + Disease, NOT
Curse). Only Mage / Druid / Shaman remain.
Locks UI: - Aperçu team rows refactored into per-player Frame widgets (was a single
multi-line FontString). Each row captures right-click → EasyMenu shows
'Lock to Team A / Lock to Team B / Free lock' with disabled states for
the current placement. Hover → per-player GameTooltip showing class,
role, DPS, HPS, manual weight, lock status. - Composition tab gains an 'Active locks' section with a scrollable list
of pinned players, per-row Free button, and a 'Clear all locks'
bottom button. Refreshes on any lock change.
Presets UI: - Réglages → 'Presets' section: edit box + Save button + scrollable list
of saved presets with per-row Load / Delete buttons. Wired into
parent.refresh so the list updates after CLI changes too.
Spec inspect refinement: - DPSSource captures GetInspectSpecialization at the same time as ilvl
(no extra inspect call). Maps spec ID → MELEE/RANGED via SPEC_RANGE
table covering all hybrid DPS specs. Stored on ilvlCache[name].range. - Roster.lua reads SplitW.DPSSource:GetCachedRange(name) and sets
entry.attackRange — Splitter.entryRange already honours this override
over the class-based default. Result: Feral druid → MELEE, Balance →
RANGED, Survival hunter → MELEE, BM/MM → RANGED, etc.
Tab rename: - 'Joueurs' → 'Composition' (FR). Source key L['Players'] →
L['Composition']. Covers Constraints + Active locks + Manual weights.
Layout: Manual weights section moved from y=-210 to y=-400 on Composition
tab to make room for the Active locks section. Action buttons follow at
y=-740 (was -550).
Docs: in-addon Changelog tab leads with the v0.3.0 entry. CHANGELOG.md
[Unreleased] section finalised as [0.3.0]. README updated to reflect
the new constraint class lists, the locks/presets/broadcast UI, the
per-player tooltip, and the new slash commands.
- Battle Rez constraint: removed Hunter / Paladin / DH (none of them have
- v0.3.0 (WIP): manual locks + broadcast on Apply + named presets
NOT PUSHED — local beta dev only until in-game testing validates the new
features.
Locks:- SplitW:GetLock / SetLock / ClearLocks / CountLocks API on lockedTeams DB
table (per-character via the active profile). - Splitter:Compute tags every entry with entry.locked from the DB before
running role passes. Locked entries are placed on their pinned team
first; the snake distribution seeds its score totals with those entries
then snake-distributes the unlocked rest. - Rebalance + constraint resolver + melee/ranged equaliser all skip
locked entries — locks are honoured end-to-end, never swapped out. - Lock icon (🔒, PetBattles texture) appended after locked names in the
Aperçu team columns so the RL sees what's pinned at a glance. - Slash commands: /splitw lock <name> A|B|free, /splitw lock clear.
Broadcast: - broadcastOnApply (default false) + broadcastChannel (default RAID).
- After Apply queue completes, Apply.lua posts two SendChatMessage calls
'SplitWatch Team A: name, name…' and 'SplitWatch Team B: …' to the
configured channel. RAID_WARNING auto-falls-back to RAID when the
player isn't lead/assist. - Réglages → Général: NEW-badged checkbox + channel dropdown
(RAID / RAID_WARNING / PARTY / SAY).
Presets: - SplitW:SavePreset / LoadPreset / DeletePreset / ListPresets API. Each
preset stores constraint toggles + dpsSource + the current locks table. - Slash: /splitw preset save|load|delete <name>, /splitw preset list.
Locales: frFR strings added for all the above. CHANGELOG.md [Unreleased]
section documents the three features. TOCs bumped to 0.3.0.
- SplitW:GetLock / SetLock / ClearLocks / CountLocks API on lockedTeams DB
- Add .pkgmeta + GitHub release workflow (BigWigsMods/packager)
.pkgmeta:- package-as: SplitWatch
- curse-project-id: 1541355
- wago-project-id: QNlzMEKe
- 5 externals fetched at build time (LibStub, CallbackHandler, LSM,
LibDataBroker, LibDBIcon) — same set as the bundled Libs/ folder, so
release zips contain upstream-fresh copies instead of our snapshots. - ignore list strips .gitignore, .github, README.md, LICENSE, root
logo.png, CLAUDE.md, CHANGELOG.md, .pkgmeta, scripts/ from the zip.
.github/workflows/release.yml: - Fires on any tag push.
- Runs BigWigsMods/packager@v2 with CF_API_KEY / WAGO_API_TOKEN /
GITHUB_TOKEN secrets. Same shape as BossWatch — drop in, tag,
and the packager handles CurseForge + Wago + GitHub release in one go.
User still needs to add the two repo secrets (CF_API_KEY from
curseforge.com/account/api-tokens and WAGO_API_TOKEN from
addons.wago.io/account/api) before the first tag.
- Honor expanded .gitignore — untrack CLAUDE.md
User expanded the .gitignore to cover OS junk, editor scratch, the full
.claude/ + .claude.local/ workspace + CLAUDE.md, .env secrets, packager
output (.release/ .pkgmeta.local *.zip *.tar.gz), runtime SavedVariables
(WTF/ *.lua.bak) and *.log. CLAUDE.md was the only file already tracked
that the new patterns cover — git rm --cached so the local copy stays
but the repo no longer contains it. - Add CurseForge + Wago project IDs + untrack .claude state
TOCs: X-Curse-Project-ID 1541355, X-Wago-ID QNlzMEKe.
README: badges linking to both publishing portals.
.gitignore: ignore .claude/ (Claude Code workspace state) and .env / .env.*
(secrets). Untrack the previously-committed .claude/settings.local.json. - README: realign with v0.2.0 surface area
Reflects the actual current addon: 5 weight sources (added Item Level via
inspect), 5 toggleable team-composition constraints (BR / Lust / melee-
ranged / Mass Dispel / Decurse), resizable panel, ilvl/DPS suffix on team
columns, source-used line on Preview, Escape-to-close, full FR locale.
Drops the per-feature blurbs that no longer matched the UI (the 'Players
tab is just sliders' description, the missing constraint mention, the
missing resize feature). Added a new 'Standalone — 0 required addons'
badge to the title block for instant scannability. - Réglages: bring back the live source preview
Removed too aggressively in the previous re-org. The inline [ilvl X] /
[245.6k] suffixes on team columns only show up AFTER computing a split,
so users have no way to verify their damage meter is wired up before
hitting Calculer. Put the live preview back on Réglages between Source
DPS and Permission status (y=-340), with a hint that calls out its
diagnostic purpose. Auto-refresh ticker (1.5s) still attached to the
Réglages tab's outer ScrollFrame so it only ticks while the tab is
visible. - Resizable main frame + logical re-org (Constraints → Joueurs)
Resize:- Panel min 720x500, max 1400x1100, default 720x540. Persisted size restored
on open and clamped against UIParent dimensions (no off-screen overflow
when moving to a smaller monitor). - BOTTOMRIGHT grip with UI-ChatIM-SizeGrabber textures; updates panelW/panelH
on mouse-up. ScrollFrame OnSizeChanged grows content height to fill the
viewport when the panel gets taller so no empty band shows under the last
section.
Re-org — Réglages was a dumping ground (5 sections); split logically: - Réglages now contains only Général + Source DPS + Permissions + Classic
banner. Removed the 'Aperçu de la source' section: the data is now visible
inline next to each player on the Aperçu tab's team columns (the [ilvl X]
/ [245.6k] suffix shipped in v0.2.0), so the dedicated live preview was
redundant. - Joueurs gains the Contraintes section at the top — composition rules
conceptually belong with the player roster + Manual weight sliders, not
with the addon-level settings. Manual-weights section now starts at
y=-210, scroll at -258 with 280px height, action buttons at -550. - Manual-weights hint now reminds the reader the sliders are only used
when source = Manual.
- Panel min 720x500, max 1400x1100, default 720x540. Persisted size restored
- v0.2.0: constraint resolver — Battle Rez / Lust / melee-ranged / dispels
The RL can now require composition rules that the algorithm enforces with
minimal-disruption swaps. Each constraint is independently toggleable on a
new 'Constraints' section of the Réglages tab. Defaults: Battle Rez ON,
Bloodlust ON, the rest OFF.
Splitter additions:- CONSTRAINTS table: class sets for BR / LUST / MASS_DISPEL / DECURSE.
- CLASS_RANGE table: melee/ranged classification (Druid/Shaman/Hunter
default to RANGED — the more common DPS spec). entry.attackRange
overrides the default so a future inspect-spec scan can refine it. - ensurePresence(constraint): if one team lacks the class, find the DPS
pair with the smallest score delta and swap. Records
CONSTRAINT_MISSING_<id> or CONSTRAINT_UNSWAPPABLE_<id> in warnings
when the swap is impossible. - Melee/ranged equaliser: count melee per team and swap melee↔ranged DPS
until the diff is ≤ 1. 20-iteration guard against pathological cases. - Constraint pass runs AFTER the team-size rebalance, so basic shape
guarantees (gap ≤ 1) are preserved before composition rules kick in.
UI: - Setup → new 'Constraints' section with 5 NEW-badged checkboxes.
- Preview → new warning strings for every CONSTRAINT_MISSING_* and
CONSTRAINT_UNSWAPPABLE_* case (with full FR translations).
TOCs bumped to 0.2.0. CHANGELOG.md gains a full [0.2.0] section and
the in-addon Changelog tab leads with the v0.2.0 entry above v0.1.0.
- Panel: close on Escape via UISpecialFrames
Register SplitWatchOptionsPanel in Blizzard's UISpecialFrames list so the
panel responds to Escape like every other Blizzard UI window. One-liner
that I missed when porting the BossWatch panel skeleton. - Fix refresh chain after ScrollFrame wrap — content holds .refresh
Regression introduced when each tab page got wrapped in a ScrollFrame:
pages[id] now points at the SF, but builder code sets parent.refresh on
the content child (sf.content). Both the module-level refresh closure
(used by every widget factory's onChange) and selectTab's tab-switch
hook called pages[id]:refresh() directly — which is nil, so the call
was a no-op and side-effects like _syncIlvlBtn() never ran. Symptom
reported by the user: selecting the Item Level source left the
'Scan raid ilvl' button greyed out forever.
Both call sites now unwrap p.content first. - Setup: 'Scan raid ilvl' button is disabled unless ILVL source is selected
The inspect cache feeds only the Item Level source — scanning while a
damage meter is active is wasted work. Wire a _syncIlvlBtn() callback
into parent.refresh so picking a source in the dropdown immediately
re-evaluates the button enabled state. Tooltip updated to spell out the
requirement. - Preview: surface which damage-meter source is feeding the algorithm
Adds a 'Source used for the split: <label>' line below the roster-status
line on the Preview page (label same as Réglages → Active source — e.g.
'Details!', 'Item Level (inspect) — 12 cached', 'Manual', etc.). Saves
the round-trip to the Réglages tab when you just want to confirm what
the algorithm is reading.
Layout: Before/After labels and stats blocks pushed down by 18px to make
room. Team-column titles unchanged at y=-200. - Splitter: rebalance team sizes after per-role distribution
Bug: snake-distributing tanks, healers, and DPS independently can produce
team counts that diverge by 2 when role counts are odd. Example from a
real 20-man pull (2T + 1H + 17DPS):- tanks alternate → A=1, B=1
- 1 healer to A → A=2, B=1
- 17 DPS snake by DPS→ A=2+9=11, B=1+8=9 ← gap of 2
Add a rebalance pass after the per-role loops: while |#A - #B| > 1, take
the weakest DPS from the larger team (last entry, since DPS list is sorted
desc) and move them to the smaller team. Picking the weakest minimises the
score-balance disruption — that DPS contributes the least to the score
gap, so moving them keeps DPS scores close.
Result for the same 20-man: 11/9 → 10/10. For a 19-man (1H + 17DPS) the
expected 10/9 stays 10/9 since gap is already 1. The earlier 'weakest
player fills the larger team for odd raids' contract still holds.
- Preview: show per-player ilvl/DPS suffix in team columns + tighten layout
- Team A/B title now includes the player count badge: 'Équipe A (5)'.
- After-stats block collapsed from 4 lines per team to 1: 'A: 5 (1T 0H 4DPS — DPS 200)'. HPS suffix only added when at least one team has heal data, so empty raids don't get noise. Matches the Before block's 2-line shape.
- Each team-list line now appends a grey suffix after the player name: '[ilvl 432]' when source = ILVL, or '[245.6k]' (k/M-formatted DPS) otherwise. Players see at a glance who's strongest on which side.
- fmtNum hoisted to module scope so both the Source preview and the team-column formatter share the same formatting.
- Empty-split branch resets the team titles to the bare label so the player count doesn't stick from a previous compute.
- Preview: tighten Team A/B columns
- Drop the redundant '[N players: T H DPS — score X]' line at the top of
each team list — the same info is shown in the 'After' stats block right
above the columns, so duplicating it just adds noise. - Bump role icons from 14 → 16 inside the listing so they read cleanly next
to the class-coloured name. - Two-space gap between icon and name (was one) for visual breathing.
- listFS width 300 → 320 to fit Name-Realm suffixes on EU/PvP realms without
early wrap. SetSpacing(4) between rows so multi-line lists don't feel
cramped. - listFS y offset from title BOTTOMLEFT 0,-6 → 0,-10 for a clearer header
separation.
- Drop the redundant '[N players: T H DPS — score X]' line at the top of
- CLAUDE.md: document the on-screen position clamp convention
Mirror the BossWatch CLAUDE.md note (lines 118-139 of u:\WoW_BossWatch\CLAUDE.md):
SetClampedToScreen(true) does not protect the initial SetPoint at load, so
saved coords from a different monitor / resolution need an explicit
UIParent:GetWidth/GetHeight comparison before being applied. Records the
pattern actually in use (restorePosition + the mirror in ShowOptionsAt) so
future persisted-position widgets follow the same recipe. - Off-screen window recovery: clamp saved position to current UIParent
If the user drags the panel on a 4K monitor and later launches WoW on a
1080p screen (or windowed mode at a smaller size), SetClampedToScreen alone
doesn't help — clamp only kicks in during drag/SetPoint, not for a
SetPoint(...) call with already-off-screen offsets at load.
restorePosition() now validates abs(x) <= UIParent.width and abs(y) <=
UIParent.height before applying. If either is exceeded, the saved position
is wiped from the DB and the panel falls back to CENTER. Same guard applied
to ShowOptionsAt (sister-addon handoff path) so a bad handoff doesn't lock
the panel off-screen.
Matches BossWatch's v0.7.3 behaviour. Documented in CHANGELOG under
[Unreleased]. - Add Item Level source (Blizzard inspect API, zero deps)
New 5th source 'Item Level (inspect)' wired into DPSSource:- ilvlQueue + ilvlBusy throttle: one NotifyInspect at a time, 1.5s between
inspects, 3s safety timeout if INSPECT_READY never fires (out of range etc). - INSPECT_READY handler resolves the GUID to a raid/party unit, reads
C_PaperDollInfo.GetInspectItemLevel(unit), caches the ilvl per name with
a 90s TTL, then ClearInspectPlayer + chains the next queue entry. - RefreshIlvl(): enqueue every raid/party member except self (own ilvl is
read directly via GetInspectItemLevel('player') / GetAverageItemLevel). - GetDPS and GetHPS both return the same cached ilvl when source='ILVL' —
it's a single character-power number used for both halves of the snake
distribution. - ListActors returns every cached entry so the Source preview shows what
the inspect sweep has gathered so far (trickles in over ~30s for a 20-man). - IsAvailable always true (no addon dependency, Blizzard API only).
- ActiveSourceLabel shows 'Item Level (inspect) — N cached'.
UI: Setup page gains a 'Scan raid ilvl' button next to the active-source
label. README + CHANGELOG callout the new source as the recommended
zero-dependency fallback when no damage meter is loaded.
User-facing: same as right-click → Inspect a player. Pure Blizzard API, no
'sniffing' beyond what every PvE addon already does.
- ilvlQueue + ilvlBusy throttle: one NotifyInspect at a time, 1.5s between
- README: GitHub-callout warning recommending Details!/Recount/Skada
Without a damage meter loaded, GetEffectiveWeight/GetEffectiveHPS fall back
to the Manual slider value (default 50). The split still computes, but it's
not informed by who actually deals/heals in your raid. Make this explicit at
the top of the README so users don't install bare and wonder why the
balance feels random. Details! recommended (most accurate combat parsing). - Add README.md, CHANGELOG.md (Keep a Changelog), MIT LICENSE
Pre-publish package — mirrors the BossWatch / TankWatch family layout. README
highlights the standalone (0 required addons) positioning, the sister-addon
family, the 10-40 man support range, and the optional Details/Recount/Skada
integrations. CHANGELOG documents v0.1.0 in Keep-a-Changelog format with the
combat-lockdown / built-in-tracker technical notes captured at the bottom. - Changelog: enforce '0 required addons' + sister-family convention
User-confirmed convention for the Timikana addon family (BossWatch, TankWatch,
SplitWatch, future siblings): every changelog batch must call out at the top
that the addon is standalone (0 required deps, optional integrations listed)
AND that it belongs to the sister-addon family.
v0.1.0 entry now leads with:- '0 required addons — works standalone. Details!/Recount/Skada optional.'
- 'Sister addon to BossWatch + TankWatch — shared side-tab nav + gold UI.'
Both lines translated in frFR. Also tweaked the damage-meter line to mention
that unavailable ones grey out (recently shipped feature).
- Bump max raid size to 40 (Classic Era ceiling)
Splitter:GetTargetGroups now caps perTeam at 4 — a WoW raid has 8 subgroups
total (1-8), so the maximum addressable team layout is groups 1-4 vs 5-8 =
20 vs 20. The math.min(4, ceil(N/5)) cap stops the helper from returning
target subgroups beyond 8 (which SetRaidSubgroup would reject).
Coverage:- 10-man → A=group 1, B=group 2
- 11-20 → A=1+2, B=3+4
- 21-30 → A=1+2+3, B=4+5+6
- 31-40 → A=1+2+3+4, B=5+6+7+8 (maxes out subgroups)
About + locale strings updated from '10-30 man' to '10-40 man'.
- Details!: use GetActorList instead of GetContainer for solo reads
GetContainer returns a structured object whose iteration (._ActorTable /
GetIterator) varies between Details! versions, so my pairs() walk over the
container was inconsistently surfacing actors in solo. Switch to
combat:GetActorList(attr), which always returns a plain array of actor
tables.
Also fall back to Details:GetCombat(1) (last completed combat) when the
current one is empty — needed so the preview still shows the player's
DPS/HPS between pulls.
Listing: drop only the synthetic [*] aggregate Details inserts at index 1
(name starts with '['); keep everything else (player, pets, environment)
so solo damage/heal lines are visible in the source preview. - Dropdown: grey out + disable unavailable sources
makeDropdown takes an optional availabilityFn(value)->bool. When provided and
returning false for an option:- the radio text gets a grey '(non installé)' suffix
- the onClick handler refuses to update the DB
- if the descriptor supports SetEnabled, it's disabled (further suppresses click)
Wired to the DPS source dropdown so Details!/Recount/Skada are visibly
unselectable when the corresponding addon isn't loaded. Manual stays always
available.
- Pages: leave room for the outer scrollbar by trimming widget widths
Two text widgets had no SetWidth so they rendered on a single overflowing line
that the outer ScrollFrame scrollbar then visually clipped:- previewEmpty ('Pas de données — ...') on the Source preview section.
- row_empty ('Aucun membre de raid détecté ...') on the Joueurs page.
Both now SetWidth(560) + SetJustifyH('LEFT') so they wrap cleanly.
Everything that was 640px wide (hint texts, status lines, mode FS, warnings,
banner, slash-cmd list, perm status FS, inner Weights scroll, source-preview
scroll) is now 600px, and inner scroll content/rows shrink from 620 to 580.
This keeps content well clear of the UIPanelScrollFrameTemplate scrollbar
that lives on the right edge of every page.
- UI polish: test-toggle label flips, team separator, scrollbar fix, role icons, Players tab
- Test-mode button text now flips between 'Activer le mode test' / 'Désactiver le mode test' on click (Preview + Weights pages). Preview's parent.refresh syncs the label so toggling from one page is reflected on the other.
- Add a vertical gold separator between Team A and Team B columns on the Preview page (1px gradient, y=-200 to y=-450).
- Stop forcing min content height to 800 in makePage — use max(viewport, naturalSpan) instead. The outer Blizzard scrollbar now stays hidden when nothing overflows (was double-stacking with the inner Weights scrollbar).
- Weights rows: role icon moves BEFORE the name (was sandwiched between name and slider) and the icon size bumps from 14px to 20px via a new size arg on roleIcon().
- Rename tab 'Poids' to 'Joueurs' (FR) — the source key changes from L['Weights'] to L['Players'] so future locales translate accordingly.
- Pages: wrap in ScrollFrame, kill BOTTOMLEFT anchors
Scrolling:- Each tab page is now a UIPanelScrollFrameTemplate ScrollFrame with a content
child, mirroring BossWatch. Page builders receive the content frame as their
parent, so widgets sit on a scrollable canvas. Long pages (Setup with 4
chained sections, About with the expanded changelog, Preview with 20-30 man
team lists, Weights with the per-player slider list) all get a Blizzard
scrollbar when content exceeds the viewport. - After build, content height is sized to cover the lowest section's bottom +
24px padding (deferred 50ms so anchors resolve). - Setup-page auto-refresh ticker now hooks the ScrollFrame's OnShow/OnHide
instead of the content frame (OnShow on a frame inside a ScrollFrame doesn't
fire when the SF is shown/hidden).
Overlap fixes — every BOTTOMLEFT anchor on a page-level widget was breaking
because _captureAndReparent rebound them to the section container's bottom,
which is itself derived from children GetBottom (circular sizing). Converted
to fixed-Y TOPLEFT anchors: - Preview warnings line: was BOTTOMLEFT 14,14 → TOPLEFT 14,-460 (below team
columns, scrolls if needed for 30-man). - Weights inner scroll: was BOTTOMRIGHT-relative → fixed 640x360.
- Weights action buttons (Reset / Refresh / Toggle test): were BOTTOMLEFT
14,14 + LEFT-chained → TOPLEFT 14,-428 / 182,-428 / 350,-428. - Setup Classic banner: was BOTTOMLEFT 14,14 → TOPLEFT 14,-580.
- Each tab page is now a UIPanelScrollFrameTemplate ScrollFrame with a content
- Sections: chained containers — collapsing one reflows everything below
BossWatch-style section chain:- makeSection now builds a Frame container that anchors to the previous
section's BOTTOMLEFT/BOTTOMRIGHT (so consecutive sections stack vertically
with a fixed gap). The first section on a page uses its passed (x, y) as
the anchor; subsequent sections ignore (x, y) and just chain. - Container TOPRIGHT pins to parent TOPRIGHT, so every section is full-width
(no more cramped left/right columns on the Setup page). - _captureAndReparent: when a widget registers, it gets reparented onto the
container and its anchors translated from page-relative-Y to container-
relative-Y (page_y - section._originY). When the container's height shrinks,
the widget moves with it. - SetCollapsed sets container height to COLLAPSED_HEIGHT (22) or natural span.
UpdateNaturalHeight scans children's GetBottom and sizes the container to fit.
Deferred via C_Timer.After(0) so widgets that register after the call get
picked up. - Setup page reordered as single full-width column: General → DPS source →
Source preview → Permission status. Permission status no longer lives in
the right column. - About page reflowed: header block (logo + title) unchanged, then chained
Slash commands → Panel (opacity slider + reset window button side-by-side)
→ Changelog.
Net result: collapsing the General section moves DPS source and everything
below it up by the height difference. Same for any section on any page.
- makeSection now builds a Frame container that anchors to the previous
- About page: BossWatch-style layout (logo + 2 columns + changelog)
- Logo (120x120) top-left as visual anchor.
- Right of logo: title 'SplitWatch vX.Y.Z', subtitle, author, slash command + alias, sister-addons line.
- Left column at y=-160: 'Slash commands' section listing /splitw, /splitw preview, /splitw apply, /splitw test, /splitw reset (each in yellow).
- Right column at y=-160: 'Panel' section with opacity slider + 'Reset window position' button.
- Full-width below: Changelog with expanded v0.1.0 entries (damage meters, HPS heal balancing, 10-30 raid support, collapsible sections).
- Source preview: filter nameless actors from meter fallback
Details!/Recount/Skada containers can include entries without a usable string
name (pets, environment actors, or otherwise-broken rows in the meter's data).
Pushing those into the preview list crashes nameFS:SetText with 'bad argument #1'.
Filter to type(a.name) == 'string' before adding, and default to '?' as a final
guard in the row builder. - Source preview: list actors from Details/Recount/Skada when roster is empty
Adds DPSSource:ListActors() that enumerates everyone the active meter has data
for — walks Details GetContainer(1)+(2), Recount's Fight table, or Skada's
players list — and returns name/class/dps/hps tuples. The Setup-page preview
now falls back to that list when SplitW.Roster:Scan() returns no members, so
solo players can still verify their damage meter is hooked up and the
DPS/HPS values are flowing.
Roster path is unchanged; it stays preferred since it provides accurate role
icons + class colors. The fallback skips role icons (everyone defaults to
DAMAGER) but keeps class coloring and the raw numbers. - Drop built-in combat-log tracker — keep Details!/Recount/Skada/Manual
pcall does not suppress ADDON_ACTION_FORBIDDEN because that's a UI event fired
by Blizzard's taint system, not a Lua error — BugGrabber still reports it. The
root cause of why SplitWatch specifically can't register COMBAT_LOG_EVENT_UNFILTERED
(while Details!/Recount/Skada/BigWigs all can) is undiagnosed; deferring the
registration to PLAYER_LOGIN doesn't help either.
Pragmatic resolution: stop registering the event at all. DPSSource._builtinAvailable
stays false, the tracker frame is created but has no events wired, the OnEvent
handler never fires, and getDPSFromBuiltin/getHPSFromBuiltin always return nil.
Remove the 'Built-in (combat log)' option from the source dropdown so users don't
land on a dead option. Their saved-DB BUILTIN value still works (returns nil →
falls back to manual weight), but the picker only offers Details!/Recount/Skada/Manual. - Fix Source Preview overlap with Permissions + harden COMBAT_LOG registration
Layout fix:- Move Source preview from y=-250 to y=-340 so it no longer overlaps with the
right-column Permission Status section at y=-250 (the source preview was using
full width 640, which crossed into the right column at x=360).
Event registration hardening: - Wrap clf:RegisterEvent calls in pcall to survive WoW 12.0's
ADDON_ACTION_FORBIDDEN on COMBAT_LOG_EVENT_UNFILTERED (cause unclear — other
damage meters succeed, but ours triggers Blizzard's taint check). Without the
wrap the error was firing 6× on load and the addon never finished initialising. - Track DPSSource._builtinAvailable and surface 'Built-in (blocked by Blizzard
event protection)' in the Active source line when registration failed, so the
user knows why the live DPS numbers stay at —. - Change default dpsSource from BUILTIN to DETAILS so a fresh install doesn't
land on the broken source. Built-in stays in the dropdown for users who want
to retry it.
- Move Source preview from y=-250 to y=-340 so it no longer overlaps with the
- Sections: fix click area + register free-floating widgets
Two bugs causing collapse to look broken:- Click area was 30x20 only — header:GetStringWidth() returns 0 at frame-build
time (FontString not yet measured), and Lua's '0 or 100' evaluates to 0 since
0 is truthy. So the area only covered the first ~3 characters of the header.
Fixed by switching to two SetPoints (TOPLEFT + BOTTOMRIGHT) so the area
spans the full section-line width minus the reset button slot, regardless of
the FontString's measured size. Also bumped frame level above siblings. - Many widgets (hint FontStrings, ScrollFrame previews, stats lines, team-column
list FontStrings, changelog entries, warning line, alpha slider) were created
directly via CreateFontString/CreateFrame and never passed through the widget
factories, so they weren't tracked in _currentSection.children — collapse hid
nothing visible. Explicit _registerInSection() calls added everywhere a free-
floating widget exists, on all four pages.
- Click area was 30x20 only — header:GetStringWidth() returns 0 at frame-build
- Panel: collapsible sections + per-section reset (BW parity)
- Replace makeHeader with makeSection: each section has a click area on the
header (toggle collapse), a chevron texture (UI-PlusButton-Up/UI-MinusButton-Up),
and a reset button (UI-RefreshButton) that resets only the dbKeys registered
in that section to SplitW.Defaults. - Widget factories (makeCheck/makeSlider/makeDropdown/makeButton/makeLabel) push
themselves and their dbKey into _currentSection so collapse hides them and
reset wipes their values. - Collapsed state persists in SplitWatchDB.collapsedSections keyed per section
(setup.general, setup.dps_source, setup.source_preview, setup.permissions,
weights.main, preview.main, about.info, about.panel, about.changelog). - Phase 1 fix: Preview-page status FontString moved to its own row (y=-68, full
width) so 'Pas de raid détecté — active le mode test pour visualiser' no longer
overflows off-screen. Before/After labels and team columns pushed down to fit. - Phase 3: panel-opacity slider moved from Setup (right column) to About (right
column), since it's a cosmetic/window pref, not a per-character setting.
- Replace makeHeader with makeSection: each section has a click area on the
- Setup page: live DPS/HPS preview of the active source
Adds a scrollable list under the source dropdown showing each roster member with the current DPS and HPS readings from the selected source. Updates every 1.5s while the Setup tab is visible (cancelled on hide) and on dropdown change via the shared refresh hook. Numbers formatted as k/M for readability; dash for no data. Moves Permission status to the right column to make room. - Support 10-30 man raids + HPS-based heal balancing + built-in meter
Algorithm:- Dynamic team→subgroup mapping in Splitter:GetTargetGroups: 10-man uses groups 1 vs 2, 11-20 uses 1+2 vs 3+4, 21-30 uses 1+2+3 vs 4+5+6.
- Healers no longer alternate by count — they snake-distribute by HPS (greedy: weakest healer goes to the lower-throughput team). Matches the DPS algorithm so weak/strong asymmetry filters into both roles.
- Compute() now returns healScoreA/healScoreB alongside scoreA/scoreB.
- Drop UNEVEN_25 warning (raid sizes >20 now handled natively); add UNEVEN_TEAMS info warning when team counts differ by more than 1.
DPS / HPS sources: - Add built-in combat-log tracker (parses COMBAT_LOG_EVENT_UNFILTERED, sums damage + effective healing per source name, snapshots on PLAYER_REGEN_ENABLED). Zero addon dependency.
- Add Skada alongside Details!/Recount; each meter now exposes both DPS and HPS.
- New default = BUILTIN so the addon works out of the box without Details!/Recount/Skada installed.
- Panel: source dropdown lists all 5 (Built-in, Details!, Recount, Skada, Manual) with a single localised tooltip.
- Preview shows per-team DPS + HPS scores so the user can see both balances.
Note: PR description-grade summary kept here intentionally because the surface area touched is large.
- Preview: surface test-mode button + roster status line
When the user isn't in a raid the roster scan returns 0 members and the preview shows empty teams with no clue why. Add a 'Toggle test mode' button on the Preview page (was only on Weights) and a status line that says 'no raid detected — enable test mode' / 'live roster (N members)' / 'test mode ON' so the empty-state is no longer mysterious. - Panel: clear bottom-tabs strip + fix missing glyphs
- Move pageHolder.BOTTOMRIGHT from y=8 to y=32 so docked bottom tabs aren't visually clipped by page content (BW gets away with 8 because its pages are scrollable).
- Replace unicode → arrow (U+2192) and ⚠ warning (U+26A0) with Blizzard texture inlines — FRIZQT__ doesn't include those glyphs and renders them as empty boxes.
- Panel UI: match BossWatch look — bottom tabs, alpha slider, TGA portrait
- Switch bottom tabs from PanelTopTabButtonTemplate to PanelTabButtonTemplate (mirrors BossWatch).
- Add multi-row tab wrap layout so narrow panels stack tabs cleanly.
- Apply panel:SetAlpha (0.85 default) + opacity slider on Setup page, account-wide DB key.
- Convert logo.png to logo.tga (WoW textures need TGA/BLP; PNG paths render as empty squares).
- Set portrait via panel.portrait and panel.PortraitContainer.portrait (covers both PortraitFrameTemplate variants).
- Side-tab icons now reference .tga explicitly (Media/logo.tga for self + BossWatch sister).
- pageHolder bounds match BossWatch (8,-60 / -8,8).
- Initial v0.1.0 — MVP manual weights mode
- Roster scan (real raid + simulated 20-man test mode)
- Snake-distribution algorithm: tanks alternate, healers alternate, DPS by descending weight greedy
- Apply.lua: permission-gated SetRaidSubgroup queue with combat-lockdown deferral
- DPSSource.lua: Details! and Recount fallbacks (manual is the only active source for v0.1.0)
- Options panel: Setup / Weights / Preview (before+after stats) / About+Changelog
- Side tabs to BossWatch + TankWatch (cross-addon panel handoff)
- Minimap launcher, Blizzard Settings host, /splitw + /splitwatch slash
- frFR locale at parity with enUS
- Multi-toc: SplitWatch.toc (retail 12.x) + SplitWatch-Mists.toc (MoP Classic 5.5)

