2026 10 28 my party overhaul - xopherdeep/do-it-for-the-xp GitHub Wiki
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Turn My Party into real parties: a household Home party with raids and a Synergies buff, outside parties joined by code with Game Master approval, async shared-boss raids, and a rebuilt Settings > Party page.
Architecture: Pure rule modules under src/lib/services/party/ (jest-tested) feed thin Vue tabs. The Home party and household raids are local (a persisted partyStore, strikes driven by the existing onQuestPaid signal). Outside parties and cross-family raids use a new xp-party/v1 REST group in the xophz-compass-xp plugin (own tables, per-party change number, strikes summed server-side), polled by a usePartyFeed composable, behind the XP Pro gate.
Tech Stack: Vue 3 + Ionic 7 + Pinia + TypeScript (jest via chemx test); PHP 7.4+ WordPress plugin tested with the plugin's .mjs harness.
Spec: docs/superpowers/specs/2026-10-28-my-party-overhaul-design.md
- Four tabs stay: Roster, Activity, Synergies, Raids. Never hide, relabel or reorder tabs; the bottom nav is untouched.
- Controls (switcher actions, segments, preset bar, action buttons) sit at the bottom of phone screens.
- Raid windows: Quick 3 days, Standard 7, Epic 14. A raid that outlasts its window retreats: no HP loss, no streak break, no penalty.
- Strikes come only from approved quests; one completion = one strike; damage from the Fibonacci difficulty (
FIBONACCI_STEPS= 1,2,3,5,8,13), never hero level; a daily cap per hero. - Reward on defeat: full base reward to every hero with at least one strike; only the top-striker title and last-hit flourish depend on contribution. Paid once per hero, keyed
raid:{raid_id}:{hero_id}. GP stays small (economy epic #1633, 1 GP = 1 cent). - Hero setting
open | ask | home; kids defaultask, adultsopen. A Game Master approves a given party once. - Cross-family chat is preset phrases/emoji, Cheer and Poke only. A party only ever carries a public hero card: nickname, level, class, look.
- Outside parties require XP Pro (
useProStore().isPro; site:Gatekeeper_Xp_Pro_Licenses). Home party and household raids are free and need no site. - No new backend: routes live in
xophz-compass-xpunderxp-party/v1. Retro SNES look,StatusPlzHUD font, duotonefadicons,play$fx(key)for sounds. - Pages are
defineComponentwith sibling.ts/.scss; new components use<script setup lang="ts">. Game rules live in pure modules. - Gates:
chemx test <spec>,chemx typecheck,chemx lintonly. Never raw pnpm/jest, neverchemx verifyor the full suite locally. Commit only your own paths (chemx commit), lock files withchemx team lock, work on a branchfeat/my-party-overhaul(feature = PR with auto-merge).docs/is a submodule.
- A strike for a chore that is awaiting approval, or later rejected, must not count (pending never strikes).
- The same
onQuestPaidsignal firing twice (a second approval, an outbox retry) must add one strike only. - A hero who is removed from a party (or whose setting flips to
home) mid-raid: strikes stay counted, later rewards stop. - A kid with
askreceiving a code must not join before the Game Master approves, even if the leader accepts first. - A raid with a one-member party, a raid that ends exactly at the window edge, and a hero who struck zero times at defeat (no reward).
Files:
- Create:
src/lib/services/party/raidRules.ts - Test:
tests/unit/raidRules.spec.ts
Interfaces:
-
Produces:
-
type RaidWindow = "quick" | "standard" | "epic";RAID_DAYS: Record<RaidWindow, number>(3, 7, 14) -
strikeDamage(difficulty: number): number(the nearestFIBONACCI_STEPSstep, times 10) -
DAILY_STRIKE_CAP: number(60, damage per hero per day) andcappedDamage(alreadyToday: number, damage: number): number -
raidMaxHp(window: RaidWindow, members: number): number(members >= 1) interface Strike { key: string; heroId: string; damage: number; at: string }interface Raid { id: string; partyId: string; beastId: string; window: RaidWindow; startedAt: string; maxHp: number; strikes: Strike[] }raidState(raid: Raid, now: Date): { hp: number; status: "active" | "defeated" | "retreated"; topStriker?: string; lastHit?: string }-
rewardFor(raid: Raid, heroId: string): { xp: number; ap: number; gp: number } | null(null when the hero never struck) -
addStrike(raid: Raid, strike: Strike): Raid(idempotent onstrike.key, applies the daily cap by day ofstrike.at)
-
-
Step 1: Write failing tests in
tests/unit/raidRules.spec.ts:strikeDamage(5)is 50 andstrikeDamage(4)rounds to the nearest step;cappedDamage(55, 30)is 5;raidMaxHp("standard", 4) > raidMaxHp("standard", 2)andraidMaxHp("epic", 3) > raidMaxHp("quick", 3);addStrikecalled twice with the same key leaves one strike; hp reaches 0 givesdefeatedwithtopStriker/lastHitset;nowexactly atstartedAt + 7 daysfor a standard raid givesretreatedwhile hp > 0 anddefeatedif hp = 0 first;rewardForreturns null for a hero with no strike and the same reward for a one-strike and a ten-strike hero. -
Step 2: Run
chemx test tests/unit/raidRules.spec.ts. Expected: FAIL (module missing). -
Step 3: Implement
raidRules.tswith the signatures above. HP scales roughlydaysFactor * members * 120; reward is{ xp: 300, ap: 5, gp: 20 }as starting constants exported asRAID_BASE_REWARD(tuned later with epic #1633). -
Step 4: Run the spec. Expected: PASS.
-
Step 5: Commit
chemx commit -m "feat(party): raid rules" -- src/lib/services/party/raidRules.ts tests/unit/raidRules.spec.ts
Files:
- Create:
src/lib/services/party/partyRules.ts,src/lib/services/party/presets.ts - Test:
tests/unit/partyRules.spec.ts
Interfaces:
-
Produces:
-
type HeroPartyMode = "open" | "ask" | "home";defaultModeFor(isAdult: boolean): HeroPartyMode canJoin(args: { mode: HeroPartyMode; partyApproved: boolean; leaderAccepted: boolean }): "join" | "needs-game-master" | "needs-leader" | "refused"-
PartyHeroCard { heroId: string; nickname: string; level: number; jobClass: string; look: unknown }andtoHeroCard(user: any): PartyHeroCard(drops every other field) -
PRESETS: readonly { id: string; text: string }[](fixed allow-list, at least 8 entries) andisPreset(id: string): boolean -
synergyBuff(heroIdsFinishedToday: string[]): number(1 for fewer than 2 heroes, 1.1 for 2, 1.2 for 3 or more)
-
-
Step 1: Write failing tests:
canJoin({mode:"home",...})isrefused;askwithpartyApproved:false, leaderAccepted:trueisneeds-game-master;openwith leader accepted isjoin;openwithout leader isneeds-leader;defaultModeFor(false)isask,(true)isopen;toHeroCardoutput has exactly the five card keys for an input carryingpin,email,realName;isPreset("free text")is false;synergyBuff([])1,["a","b"]1.1. -
Step 2: Run
chemx test tests/unit/partyRules.spec.ts. Expected: FAIL. -
Step 3: Implement both modules per the signatures.
-
Step 4: Run. Expected: PASS.
-
Step 5: Commit both files and the spec with
chemx commit.
Files:
- Create:
src/lib/store/stores/party.ts - Test:
tests/unit/partyStore.spec.ts
Interfaces:
-
Consumes:
Raid,addStrike,raidState,rewardFor,strikeDamage,cappedDamage(Task 1);HeroPartyMode,defaultModeFor(Task 2);onQuestPaidandQuestPaidfrom@/lib/services/multiplayer/gameSignals; persistence viapersistUser-style helpers used insrc/lib/store/stores/pro.ts/StorageService. -
Produces:
usePartyStorewith state{ heroModes: Record<string, HeroPartyMode>; approvedParties: Record<string, string[]>; raids: Raid[]; paid: string[] }and actionsmodeOf(heroId),setMode(heroId, mode),approveParty(heroId, partyId),startHomeRaid(args: { beastId: string; window: RaidWindow; memberIds: string[] }): Raid,strike(raidId: string, strike: Strike),claimReward(raidId: string, heroId: string): { xp: number; ap: number; gp: number } | null(null if already paid, viapaidcontainingraid:{raid_id}:{hero_id}). -
Step 1: Write failing tests:
modeOffalls back todefaultModeFor; a started Home raid receives a strike from a hero inmemberIds;claimRewardtwice returns the reward once and then null; a strike from a hero not in the raid is ignored. -
Step 2: Run
chemx test tests/unit/partyStore.spec.ts. Expected: FAIL. -
Step 3: Implement the store (Pinia, persisted like
pro.ts). -
Step 4: Run. Expected: PASS.
-
Step 5: Commit.
Files:
- Modify:
src/lib/services/multiplayer/gameSignals.ts:8-15(add optionaldifficulty?: numbertoQuestPaid),src/lib/services/quests/QuestCompletionService.ts:113-116and its twoannouncePaidcallers (pass the quest's difficulty;approveCompletionreads it from the quest byentry.questId) - Create:
src/lib/services/party/strikeCapture.ts - Test:
tests/unit/strikeCapture.spec.ts
Interfaces:
-
Consumes:
usePartyStore().strike,strikeDamage,cappedDamage. -
Produces:
startStrikeCapture(): () => void(subscribes withonQuestPaid, returns the stop function; strike key isquest:{questId}:{heroId}:{dayKey}, so a re-emit adds nothing). -
Step 1: Write failing tests: emitting
emitQuestPaid({heroId, questId, xp, fight:false, difficulty:5})adds a 50-damage strike to the hero's active Home raid; emitting it twice leaves one strike; a held claim (onlyemitQuestClaimed, no paid) adds none; a hero with no active raid is ignored. -
Step 2: Run
chemx test tests/unit/strikeCapture.spec.ts. Expected: FAIL. -
Step 3: Implement the signal field, the callers, and
strikeCapture.ts. Start it where the other app signals are wired (searchonQuestPaid(insrc/lib/store/stores/family.tsand start capture beside it in app init). -
Step 4: Run this spec and
chemx test tests/unit/QuestCompletionService.spec.ts(if present) to confirm no payment regression. Expected: PASS. -
Step 5: Commit.
Files:
- Create:
src/app/Console/MyPortal/party/PartySwitcher.vue,src/app/Console/MyPortal/party/RaidCard.vue,src/app/Console/MyPortal/party/_Party.scss - Modify:
src/app/Console/MyPortal/MyParty.vue,MyParty.ts,party/PartyRaidsTab.vue,party/PartySynergiesTab.vue - Test:
tests/unit/PartyTabs.spec.ts(extend),tests/e2e/specs/party.spec.js(extend)
Interfaces:
-
Consumes:
usePartyStore,raidState,synergyBuff, the Home members fromMyParty.ts(partyMembers). -
Produces:
PartySwitcherprops{ parties: {id: string; name: string; banner?: string}[]; active: string }, emitsselect(id: string);MyParty.tsexposesactivePartyId(default"home"). -
Step 1: Write failing tests in
PartyTabs.spec.ts: the Raids tab with no raid shows a "Start a raid" button (data-cy="party-start-raid") and, with an active raid, its HP bar and strike ledger; Synergies shows the buff (x1.1for two heroes finished today); the switcher lists Home first; the four segment buttons still render in order. -
Step 2: Run
chemx test tests/unit/PartyTabs.spec.ts. Expected: FAIL. -
Step 3: Implement the components. The raid start sheet picks a boss-tier beast (filter the bestiary by its existing boss flag) and a window; the Raids tab, a reward-claim button when
raidStateisdefeatedandclaimRewardis non-null (pay viaapplyQuestRewards(heroId, reward, "raid:{id}")), and the switcher strip at the bottom of the header area for thumb reach. -
Step 4: Run the spec; run
chemx typecheck. Expected: PASS and no errors. -
Step 5: Commit.
Files:
- Modify:
src/app/SideMenu/XpSettings/components/PartySettings.vue,comingSoon.ts:31-40(shrinkPARTY_COMING_SOONto the planned family circle posts) - Create:
src/app/SideMenu/XpSettings/components/PartyHeroModes.vue(per-hero Open/Ask/Home segment) - Test:
tests/unit/PartySettings.spec.ts
Interfaces:
-
Consumes:
usePartyStore().modeOf/setMode,useUserStore().usersAz,canManageFamily(Game Masters only edit). -
Produces: sections
data-cy:party-hero-modes,party-requests,party-my-parties,party-my-code,party-raid-defaults. -
Step 1: Write failing tests: each hero row shows a mode control defaulting to
askfor a kid; changing it callssetMode; a non-Game-Master sees the modes read-only; the planned card lists only "Family circle posts". -
Step 2: Run
chemx test tests/unit/PartySettings.spec.ts. Expected: FAIL. -
Step 3: Implement the page. Requests, My parties and My code sections render an empty state ("No outside parties yet") until Phase C fills them. Raid defaults hold the default window (store field
defaultWindow) and the raid alerts toggle (reuse the alert bridge's preference pattern insrc/lib/services/notifications/alertBridge.ts). -
Step 4: Run the spec;
chemx lint. Expected: PASS. -
Step 5: Commit.
Files (plugin root ../Xophz-COMPASS/wp-content/plugins/xophz-compass-xp/):
- Create:
includes/party/class-xophz-compass-xp-party-schema.php(tablesxp_parties,xp_party_members,xp_party_requests,xp_party_events,xp_raids,xp_raid_strikes; unique key on the strike key),includes/party/class-xophz-compass-xp-party-rules.php(pure PHP: card whitelist, preset allow-list, daily cap, raid HP/status derivation, code format) - Modify:
includes/class-xophz-compass-xp-activator.php(create on activation and on the stored schema-version check, as the family schema does) - Test:
tests/party-rules.test.mjs,tests/party-schema.test.mjs; register both intests/run-tests.mjs
Interfaces:
-
Produces (PHP):
Xophz_Compass_Xp_Party_Rules::card( array $hero ): array(only the five public keys; throws aWP_Error-style codexp_party_card_invalidon any extra key),::is_preset( string $id ): bool,::raid_state( array $raid, array $strikes, int $now ): array(same maths as the app'sraidRules.ts),::new_code(): string(8 characters, no look-alikes). -
Step 1: Write failing tests mirroring the family tests' style (
runPhpJsonon the pure class): card with an extrapinkey is refused; preset allow-list matches the app's ids;raid_statefor equal inputs returns the same hp/status as the jest cases of Task 1; two inserts with one strike key leave one row (schema test). -
Step 2: Run the plugin tests (
node tests/run-tests.mjsfrom the plugin dir). Expected: new tests FAIL. -
Step 3: Implement both classes and the activator hook.
-
Step 4: Run. Expected: PASS.
-
Step 5: Commit in the plugin's repo, path-scoped.
Files:
- Create:
includes/party/class-xophz-compass-xp-party-rest.php,includes/party/class-xophz-compass-xp-party-store.php - Modify:
includes/class-xophz-compass-xp.php(register the routes on the family sites only, likexp-family/v1) - Test:
tests/party-routes.test.mjs(usesfamily-store-fake.mjsstyle fakes)
Interfaces:
-
Produces (routes exactly as the spec's table):
POST /parties,POST /parties/join,POST /parties/{id}/requests/{rid},DELETE /parties/{id}/members/{hero},POST /parties/{id}/code/rotate,POST /codes/{code}/block,GET /parties/mine,GET /parties/{id}/feed?since=,POST /parties/{id}/raids,POST /raids/{id}/strikes,DELETE /raids/{id}/strikes/{key},POST /parties/{id}/presets. Error codes:xp_party_pro_required(403),xp_party_forbidden(403),xp_party_card_invalid,xp_party_code_blocked,xp_party_raid_closed. -
Step 1: Write failing tests: a non-entitled family gets 403
xp_party_pro_requiredon every route; a member'sPOST strikestwice with one key leaves one strike; a strike above the daily cap is trimmed; a strike after the window is refusedxp_party_raid_closed; a non-leader cannot start a raid; a rotated code rejects an old join; a free-text preset is refused;GET feed?since=Nreturns only newer events in order. -
Step 2: Run. Expected: FAIL.
-
Step 3: Implement routes and store, with the party change number allocated under a named lock like the family records (
GET_LOCK("xp_party_{id}")). -
Step 4: Run the plugin suite. Expected: PASS.
-
Step 5: Commit.
Files:
- Create:
src/lib/services/party/partyClient.ts,src/hooks/usePartyFeed.ts - Modify:
src/lib/store/stores/party.ts(addparties,members,requests,since) - Test:
tests/unit/partyClient.spec.ts
Interfaces:
-
Consumes: the Town Hall REST base and auth the family client uses (find with
chemx q "xp-family/v1"and reuse its fetch helper);useProStore().isPro. -
Produces:
partyClientfunctionsformParty(args),joinByCode(code, heroId),resolveRequest(partyId, requestId, decision),leave(partyId, heroId),rotateCode(partyId),blockCode(code),sendPreset(partyId, heroId, presetId), each returning the typed response or a{ code, message }error;usePartyFeed()returns{ refresh(): Promise<void>; start(): () => void }and applies events to the store sincesince. -
Step 1: Write failing tests against a mocked fetch: not-Pro callers get
xp_party_pro_requiredwithout a network call;joinByCodeposts the hero card only (no pin/email keys in the body); the feed applies events in order and advancessince. -
Step 2: Run
chemx test tests/unit/partyClient.spec.ts. Expected: FAIL. -
Step 3: Implement.
-
Step 4: Run. Expected: PASS.
-
Step 5: Commit.
Files:
- Create:
src/app/Console/MyPortal/party/PartyFormJoin.vue,party/PartyPresetBar.vue,src/app/SideMenu/XpSettings/components/PartyRequests.vue - Modify:
party/PartyRosterTab.vue,party/PartyActivityTab.vue,MyParty.ts(the switcher lists outside parties),PartySettings.vue(fill Requests, My parties, My code with QR) - Test:
tests/unit/PartyOutside.spec.ts
Interfaces:
-
Consumes: Tasks 2, 3, 9 (
canJoin,partyClient,usePartyStore). -
Produces: UI
data-cyhooksparty-form,party-join,party-request-approve,party-preset-bar. -
Step 1: Write failing tests: a kid hero with mode
askshows "Waiting for the Game Master" until approved; a Game Master sees the request and approving callsresolveRequest; modehomehides the Join panel; the preset bar shows onlyPRESETSand offers no text input; losing Pro shows outside parties read-only (switcher still lists Home). -
Step 2: Run
chemx test tests/unit/PartyOutside.spec.ts. Expected: FAIL. -
Step 3: Implement the screens. The QR for a code uses the QR component the "Link a phone" flow already uses (
chemx q "XpFamilyPhones"to find it). -
Step 4: Run the spec;
chemx typecheck. Expected: PASS. -
Step 5: Commit.
Files:
- Create:
src/lib/services/party/strikeOutbox.ts - Modify:
src/lib/services/party/strikeCapture.ts(also queue strikes for outside raids),party/RaidCard.vueandPartyRaidsTab.vue(outside raid view and history),src/lib/store/stores/party.ts - Test:
tests/unit/strikeOutbox.spec.ts
Interfaces:
-
Consumes:
partyClient,usePartyFeed,claimReward. -
Produces:
queueStrike(raidId: string, strike: Strike): void,flushStrikes(): Promise<void>(idempotent resend on the strike key; drops a strike the site refuses withxp_party_raid_closed). -
Step 1: Write failing tests: offline queue survives and flushes in order; a double flush posts each key once; a refused-closed strike is dropped, not retried; when the feed reports a defeat the reward pays once for a striker and never for a non-striker; a hero removed from the party stops receiving later rewards but keeps earlier strikes counted.
-
Step 2: Run
chemx test tests/unit/strikeOutbox.spec.ts. Expected: FAIL. -
Step 3: Implement.
-
Step 4: Run this spec and the Phase A specs. Expected: PASS.
-
Step 5: Commit.
Files:
-
Modify:
tests/e2e/specs/party.spec.js; plugintests/rehearsal/(add a two-family join and raid rehearsal in the local Docker WordPress, following the existing rehearsal files) -
Step 1: Write the e2e: seed a world, start a Home raid, complete a chore, see HP drop, defeat it and claim once (the Cypress
cy.seedWorld()helper, run against the shared:8100dev server). -
Step 2: Run it per CLAUDE.md (
pnpm vue-cli-service test:e2e --headless --url http://localhost:8100 --spec tests/e2e/specs/party.spec.js). Expected: PASS. -
Step 3: Run the rehearsal on the local WordPress: two families, form, join with approval, strike, defeat. Expected: both families see the same HP.
-
Step 4: Open the PR with auto-merge (
gh pr merge <n> --auto --squash) and read the Tests check; fix and push on red. Do not runchemx verifylocally.
- Spec coverage: Home party/raids/Synergies (T1-5), Settings page (T6), site (T7-8), form/join/approvals/presets (T9-10), outside raids/rewards (T11), tests/e2e/PR (T12). Out-of-scope items (friend list, rally, board, free chat) have no task by design.
- A reverted approved chore has no revert path in the codebase today (
chemx qfinds none), so the spec's "withdraw on revert" has no caller yet:DELETE /raids/{id}/strikes/{key}and the client call exist (T8, T9) for the day a revert path lands. - Open owner items from the spec stay open: Pro assumption, reward constants (
RAID_BASE_REWARD,DAILY_STRIKE_CAPare starting values), the boss-tier beast list.