2026 10 28 my party overhaul - xopherdeep/do-it-for-the-xp GitHub Wiki

My Party Overhaul Implementation Plan

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

Global Constraints

  • 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 default ask, adults open. 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-xp under xp-party/v1. Retro SNES look, StatusPlz HUD font, duotone fad icons, play$fx(key) for sounds.
  • Pages are defineComponent with sibling .ts/.scss; new components use <script setup lang="ts">. Game rules live in pure modules.
  • Gates: chemx test <spec>, chemx typecheck, chemx lint only. Never raw pnpm/jest, never chemx verify or the full suite locally. Commit only your own paths (chemx commit), lock files with chemx team lock, work on a branch feat/my-party-overhaul (feature = PR with auto-merge). docs/ is a submodule.

Review Focus

  • A strike for a chore that is awaiting approval, or later rejected, must not count (pending never strikes).
  • The same onQuestPaid signal 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 ask receiving 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).

Phase A: Home party, Synergies, household raids, Settings (free, local; spec slice 1)

Task 1: Raid rules (pure)

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 nearest FIBONACCI_STEPS step, times 10)
    • DAILY_STRIKE_CAP: number (60, damage per hero per day) and cappedDamage(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 on strike.key, applies the daily cap by day of strike.at)
  • Step 1: Write failing tests in tests/unit/raidRules.spec.ts: strikeDamage(5) is 50 and strikeDamage(4) rounds to the nearest step; cappedDamage(55, 30) is 5; raidMaxHp("standard", 4) > raidMaxHp("standard", 2) and raidMaxHp("epic", 3) > raidMaxHp("quick", 3); addStrike called twice with the same key leaves one strike; hp reaches 0 gives defeated with topStriker/lastHit set; now exactly at startedAt + 7 days for a standard raid gives retreated while hp > 0 and defeated if hp = 0 first; rewardFor returns 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.ts with the signatures above. HP scales roughly daysFactor * members * 120; reward is { xp: 300, ap: 5, gp: 20 } as starting constants exported as RAID_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

Task 2: Party rules and presets (pure)

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 } and toHeroCard(user: any): PartyHeroCard (drops every other field)
    • PRESETS: readonly { id: string; text: string }[] (fixed allow-list, at least 8 entries) and isPreset(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",...}) is refused; ask with partyApproved:false, leaderAccepted:true is needs-game-master; open with leader accepted is join; open without leader is needs-leader; defaultModeFor(false) is ask, (true) is open; toHeroCard output has exactly the five card keys for an input carrying pin, 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.

Task 3: Party store (Home raids, settings, approvals)

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); onQuestPaid and QuestPaid from @/lib/services/multiplayer/gameSignals; persistence via persistUser-style helpers used in src/lib/store/stores/pro.ts / StorageService.

  • Produces: usePartyStore with state { heroModes: Record<string, HeroPartyMode>; approvedParties: Record<string, string[]>; raids: Raid[]; paid: string[] } and actions modeOf(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, via paid containing raid:{raid_id}:{hero_id}).

  • Step 1: Write failing tests: modeOf falls back to defaultModeFor; a started Home raid receives a strike from a hero in memberIds; claimReward twice 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.

Task 4: Strike capture from paid quests

Files:

  • Modify: src/lib/services/multiplayer/gameSignals.ts:8-15 (add optional difficulty?: number to QuestPaid), src/lib/services/quests/QuestCompletionService.ts:113-116 and its two announcePaid callers (pass the quest's difficulty; approveCompletion reads it from the quest by entry.questId)
  • Create: src/lib/services/party/strikeCapture.ts
  • Test: tests/unit/strikeCapture.spec.ts

Interfaces:

  • Consumes: usePartyStore().strike, strikeDamage, cappedDamage.

  • Produces: startStrikeCapture(): () => void (subscribes with onQuestPaid, returns the stop function; strike key is quest:{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 (only emitQuestClaimed, 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 (search onQuestPaid( in src/lib/store/stores/family.ts and 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.

Task 5: My Party shell: switcher, Raids and Synergies tabs

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 from MyParty.ts (partyMembers).

  • Produces: PartySwitcher props { parties: {id: string; name: string; banner?: string}[]; active: string }, emits select(id: string); MyParty.ts exposes activePartyId (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.1 for 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 raidState is defeated and claimReward is non-null (pay via applyQuestRewards(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.

Task 6: Rebuilt Settings > Party

Files:

  • Modify: src/app/SideMenu/XpSettings/components/PartySettings.vue, comingSoon.ts:31-40 (shrink PARTY_COMING_SOON to 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 ask for a kid; changing it calls setMode; 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 in src/lib/services/notifications/alertBridge.ts).

  • Step 4: Run the spec; chemx lint. Expected: PASS.

  • Step 5: Commit.


Phase B: The site: xp-party/v1 (spec slice 2; independent of Phase A)

Task 7: Party schema and rules

Files (plugin root ../Xophz-COMPASS/wp-content/plugins/xophz-compass-xp/):

  • Create: includes/party/class-xophz-compass-xp-party-schema.php (tables xp_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 in tests/run-tests.mjs

Interfaces:

  • Produces (PHP): Xophz_Compass_Xp_Party_Rules::card( array $hero ): array (only the five public keys; throws a WP_Error-style code xp_party_card_invalid on any extra key), ::is_preset( string $id ): bool, ::raid_state( array $raid, array $strikes, int $now ): array (same maths as the app's raidRules.ts), ::new_code(): string (8 characters, no look-alikes).

  • Step 1: Write failing tests mirroring the family tests' style (runPhpJson on the pure class): card with an extra pin key is refused; preset allow-list matches the app's ids; raid_state for 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.mjs from 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.

Task 8: Party REST routes and the Pro gate

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, like xp-family/v1)
  • Test: tests/party-routes.test.mjs (uses family-store-fake.mjs style 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_required on every route; a member's POST strikes twice with one key leaves one strike; a strike above the daily cap is trimmed; a strike after the window is refused xp_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=N returns 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.


Phase C: Outside parties in the app (spec slice 3; needs Phase B)

Task 9: Party client and feed

Files:

  • Create: src/lib/services/party/partyClient.ts, src/hooks/usePartyFeed.ts
  • Modify: src/lib/store/stores/party.ts (add parties, 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: partyClient functions formParty(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 since since.

  • Step 1: Write failing tests against a mocked fetch: not-Pro callers get xp_party_pro_required without a network call; joinByCode posts the hero card only (no pin/email keys in the body); the feed applies events in order and advances since.

  • Step 2: Run chemx test tests/unit/partyClient.spec.ts. Expected: FAIL.

  • Step 3: Implement.

  • Step 4: Run. Expected: PASS.

  • Step 5: Commit.

Task 10: Form, join, approve (UI) and outside Roster/Activity

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-cy hooks party-form, party-join, party-request-approve, party-preset-bar.

  • Step 1: Write failing tests: a kid hero with mode ask shows "Waiting for the Game Master" until approved; a Game Master sees the request and approving calls resolveRequest; mode home hides the Join panel; the preset bar shows only PRESETS and 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.


Phase D: Outside raids (spec slice 4; needs Phases B and C)

Task 11: Outside strikes, outbox and defeat rewards

Files:

  • Create: src/lib/services/party/strikeOutbox.ts
  • Modify: src/lib/services/party/strikeCapture.ts (also queue strikes for outside raids), party/RaidCard.vue and PartyRaidsTab.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 with xp_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.

Task 12: End-to-end and PR

Files:

  • Modify: tests/e2e/specs/party.spec.js; plugin tests/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 :8100 dev 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 run chemx verify locally.


Self-review notes

  • 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 q finds 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_CAP are starting values), the boss-tier beast list.
⚠️ **GitHub.com Fallback** ⚠️