Skip to content

feat(raids): redesign level selector with named tiers + custom palette (#259)#280

Open
hokiepokedad2 wants to merge 5 commits into
mainfrom
feat/raid-level-selector-redesign
Open

feat(raids): redesign level selector with named tiers + custom palette (#259)#280
hokiepokedad2 wants to merge 5 commits into
mainfrom
feat/raid-level-selector-redesign

Conversation

@hokiepokedad2
Copy link
Copy Markdown
Contributor

@hokiepokedad2 hokiepokedad2 commented May 22, 2026

Summary

Closes #259 (reported by @prof-miles0). Supersedes the closed [1..10] hardcode PR #278.

The raid/egg add dialog had three level-pickers (raid checkboxes, egg checkboxes, boss-level dropdown) all driven by levels = [1, 2, 3, 4, 5, 6], even though PoracleNG accepts any positive integer. Users with Elite Raids (level 7) or custom server schemes had to configure those via the bot's !command interface; the UI silently locked them out.

Design

<app-level-selector> — a new shared Material 3 chip listbox in three sections:

   STANDARD
   [ T1 ]  [ T2 ]  [ T3 ]  [ T4 ]  [ T5 ]

   SPECIAL
   [ Mega · 6 ]  [ Elite · 7 ]  [ Any ]    ← Any only when showAny=true

   CUSTOM
   [ 42 × ]  [ 9999 × ]               [ + Add level ]
  • Common cases at a glance with named labels: "Mega", "Elite", "Any" — not bare numbers.
  • Power users add arbitrary integers via an inline + Add level affordance that transforms into a numeric input (Enter to commit, Esc to cancel).
  • Persistence: custom values save to localStorage('poracle.custom-raid-levels') (LRU cap 20) so "I always alarm on level 8" is a one-time setup.
  • Backfill: saved alarms with non-standard levels (e.g., level: 42) seed the palette on dialog open and pre-select their chip.
  • Single source of truth: resolveLevel(value) in core/models/raid-level.models.ts is the mapping from stored integer → display option. Adopted by raid alarm cards too, so dialog and cards now agree (an alarm at level 7 says "Elite" on both surfaces).

Edge cases

  • 0 / negatives / non-integers in custom input → inline validation error
  • 9000 typed in → snaps to the canonical "Any" chip, never creates a duplicate
  • Duplicate of an existing built-in → flashes the existing chip + selects it
  • Duplicate of an existing custom → flashes + selects + shows hint
  • Very large integers → accepted as-is (PoracleNG doesn't bound them)

i18n

New RAIDS.LEVEL.* keys added in all 11 locales with English fallbacks for non-en. Translation volunteers can localize the placeholder English text in a follow-up (see discussion #211).

Subtle correctness fix

The boss tab used to default the level filter to 0 ("any") and pass that to PoracleNG, but the canonical "any" sentinel is 9000 (the same one used everywhere else in the codebase including the alarm cards). New alarms now use 9000. Existing alarms saved with level: 0 continue to render and edit fine; they just keep using 0.

Tests

37 new unit tests across:

  • raid-level.models.spec.ts — resolveLevel + isBuiltInLevel coverage
  • custom-level-store.service.spec.ts — localStorage, LRU eviction, malformed JSON resilience, seedFrom semantics
  • level-label.pipe.spec.ts — display rendering for each category
  • level-selector.component.spec.ts — toggle, single-vs-multi, add/remove, snap-to-Any, dupe handling, Escape

Test plan

  • Frontend CI green
  • Open the raid/egg add dialog: STANDARD chips T1–T5 visible, SPECIAL shows Mega · 6 and Elite · 7
  • Add level 42 via + Add level → chip appears, selected, persists on next dialog open
  • Edit an existing alarm at level 7 → Elite chip preselects (was invisible before)
  • Boss tab: "Any" chip visible, defaults to selected; saved alarms use level 9000
  • Cards show "Mega" / "Elite" / "Any" labels instead of bare numbers for matching levels

#259)

The raid/egg add dialog had three level-pickers (raid checkboxes, egg
checkboxes, boss-level dropdown) all driven by a hardcoded
`levels = [1, 2, 3, 4, 5, 6]` array, even though PoracleNG accepts
any positive integer. Users with Elite Raids (level 7) or custom
server schemes had to configure those via the bot's `!command`
interface; the UI silently locked them out.

Replace all three sites with a new `<app-level-selector>` shared
component — a Material 3 chip listbox in three sections:

- STANDARD: T1-T5
- SPECIAL:  Mega (6), Elite (7), plus "Any" (9000) when showAny=true
- CUSTOM:   any user-added integer, persisted per-user in localStorage

Power users add a custom level via an inline "+ Add level" affordance
that transforms into a numeric input. The chip then persists across
dialog opens (one-click selection on subsequent alarms) and seeds
itself from saved alarm data on open, so editing an existing
level-42 alarm renders the chip pre-selected rather than orphaned.

Single source of truth for label resolution lives in
`core/models/raid-level.models.ts`. `resolveLevel(value)` maps any
integer to the right LevelOption — adopted by raid-list cards too,
so the dialog and the cards now speak the same vocabulary
("Elite", not "Level 7" vs "7" on different surfaces).

Edge cases:
- 0 / negatives / non-integers in custom input -> inline validation error
- 9000 in custom input -> snaps to the "Any" chip (no duplicate)
- duplicate of built-in -> flashes existing chip + selects, no new entry
- 20-entry LRU cap on the localStorage palette

i18n: new `RAIDS.LEVEL.*` keys added in all 11 locales with English
fallbacks for non-en (translation volunteers can localize later,
per discussion #211).

Bonus correctness: the boss tab used to default level=0 ("any") but
PoracleNG's canonical wildcard sentinel is 9000. New alarms now use
9000; old alarms with 0 continue to work and edit fine.

37 new unit tests across the model, store, pipe, and component.

Closes #259, reported by @prof-miles0.
@github-actions github-actions Bot added the feat label May 22, 2026
@prof-miles0
Copy link
Copy Markdown

Sorry for another comment here… I have edited my previous comment adding info from where you can get raid level info. You can find it in the gamemaster. Currently raid levels range from 1 to 19.

You can find the gamemaster here: https://github.com/WatWowMap/Masterfile-Generator/blob/8db1fd5c1a9401db59f4ce053be76f04e0e88812/master-latest-poracle-v2.json

Search for raid_{level}, you can also see game tags/names there.

…nblock save

Follow-up on d58752e (the initial #259 redesign), addressing visual,
correctness, and backend-validation issues found during testing.

UI / UX
- Collapse the three-section (Standard/Special/Custom) chip layout
  into one wrapping row per picker. Categories are encoded in chip
  content (T1 / "Mega · 6" / "42 ⊗") rather than container labels;
  cuts dialog height roughly in half.
- Replace the heavy mat-form-field "+ Add custom level" with a
  chip-sized inline numeric input. Enter commits, Esc cancels,
  blur commits. Help text only renders when the input is open or
  there's a validation error.
- Override Material 3 selected-chip font-weight so the selected
  state actually pops; bind `hideSingleSelectionIndicator` to
  `!multiple` so multi-select chips get a leading checkmark.
- Cap card star icons to levels 1-7 (was 1-100) — alarms at
  level 23 no longer render 23 stars in the card.

Per-type palette
- Adding a custom level on the raid picker was leaking it into the
  egg and boss pickers. `CustomLevelStore` now keys palettes by
  `paletteKey` ("raid" / "egg" / "boss"), each persisted to its own
  localStorage slot. Required `paletteKey` input on
  `<app-level-selector>`.

Any chip surfaced where PoracleNG actually honors it
- Raid + boss pickers show the `Any` chip (PoracleNG treats
  level=9000 as the wildcard sentinel — see trackingRaid.go).
- Egg picker deliberately omits Any: PoracleNG's trackingEgg.go
  only validates level >= 1 with no wildcard semantic, so an "Any
  egg" alarm at 9000 would simply never fire.

Server-side fix that was blocking custom-level alarms
- `[Range(0, 10)]` on `RaidCreate.Level`, `RaidUpdate.Level`,
  `EggCreate.Level`, `EggUpdate.Level` was rejecting custom
  integers (8+) and the new Any=9000 sentinel with 400 Bad
  Request before they could reach PoracleNG. Relaxed to
  `[Range(0, int.MaxValue)]` matching PoracleNG's actual range.

Label vocabulary consistency
- Edit dialog (raid/egg) now uses the same `resolveLevel` resolver
  as the cards via the new `LevelLabelPipe` — an alarm at level 7
  reads "Elite" on the card AND in the edit dialog (was "Level 7"
  in the dialog before). Egg image alt-text in the card list now
  uses the pipe too.

Error UX
- Removing a custom chip (`⊗`) opens a 3-second snackbar with
  Undo — accidental click is recoverable; intentional removal
  still wipes the palette entry.

State-machine clarity
- `addInputOpen: signal(boolean)` replaced with an explicit
  `addMode: signal<'closed' | 'open'>` and named `isAddClosed()` /
  `isAddOpen()` getters in the template — removes the `!` negation
  pattern that prior renders sometimes appeared to misread.

Tests
- Updated `custom-level-store.service.spec.ts` for the keyed API.
- Updated `level-selector.component.spec.ts` to set `paletteKey`
  per test and assert per-key isolation.
- 697/697 frontend tests pass, 1063/1063 backend tests pass.

i18n
- Added `RAIDS.LEVEL.REMOVED` and `COMMON.UNDO` keys in all 11
  locales (English placeholder text for non-en — translation
  volunteers per discussion #211).
…levels)

Builds on the v2 review-pass (2c2f0aa). Follow-up driven by the issue
reporter pointing to the canonical Pokémon GO raid level vocabulary in
the WatWowMap masterfile — there are 19 named raid types (1-Star
through Coordinated 2), not 7, and the prior UI labeled level 7 as
"Elite" when the masterfile says it's "Mega Legendary".

Backend
- `GET /api/masterdata/raid-levels` returns the canonical list with
  per-level integer, category, and singular/plural English names.
- New `IRaidLevelService` / `RaidLevelService` returns a baked-in
  snapshot of the masterfile. A TODO documents how to swap the
  implementation for a live fetch from
  raw.githubusercontent.com/WatWowMap/Masterfile-Generator without
  changing the wire contract.
- 6 new unit tests cover the service + controller endpoint.

Frontend
- `RaidLevelService` (Angular) calls the new API on first dialog use,
  caches the result in a signal. Falls back to `KNOWN_LEVELS`
  baked-in constants when the network fails or before resolve.
- `raid-level.models.ts` rewritten around 19 canonical levels keyed
  by `RAIDS.LEVEL.RAID_1` through `RAID_19` (plus `_PLURAL` variants).
  Removed the bogus `T1`-`T5` / `MEGA` / `ELITE` keys.
- `LevelSelectorComponent` simplified to a `pickerType` input
  (`'raid' | 'egg' | 'boss'`). Inputs:
    • raid  → primary chips 1-7, overflow menu for 8-19, Any chip, +Add
    • egg   → star tiers (1-5) only, +Add (no overflow, no Any)
    • boss  → single-select with the same primary + overflow as raid
  "More raid types…" overlay menu (mat-menu) surfaces the 12 less
  common levels without crowding the chip row.
- i18n: 19 singular + 19 plural keys in all 11 locales, with English
  placeholders for the 10 non-en locales (volunteers per #211).
- Card star icons now render only for the literal 1-5 "N Star Raid"
  tier (was 1-7, producing ~23 stars for custom-level alarms).
- Label vocabulary consistency: alarm at level 7 now reads "Mega
  Legendary Raid" on the card and in the edit dialog (was "Elite"
  on the card, "Level 7" in the edit dialog).

Forward compatibility
- Any positive integer remains addable via the `+ Add` chip. When
  the WatWowMap masterfile adds raid_20+ in the future, the backend
  service can pick it up automatically (once the live-fetch path is
  wired); existing custom alarms at that level continue to work.

Tests: 711/711 frontend, 1069/1069 backend. Lint + prettier clean.
Follow-up on 12be778 (v3 masterfile alignment). User feedback +
internal PR review revealed several issues; this commit addresses
them.

User feedback
- Custom levels typed via `+ Add` were persisting across modal
  close AND page refresh because the per-type palette was backed
  by localStorage. Deleted CustomLevelStore entirely; LevelSelector
  now tracks the palette in a local `customPalette` signal that
  lives for the component lifetime only. Refresh or close-and-reopen
  wipes typed-but-not-saved chips. Existing alarms still seed the
  palette through the `[value]` input.
- Chip labels were too long ("Mega Legendary Raid"), and the same
  string caused "All Mega Legendary Raid Raids" double-Raid in card
  titles. Dropped the "Raid" suffix from the 19 RAID_N keys in all
  11 locales — chips now read "Mega Legendary", "Legendary",
  "1 Star", etc. The card-title template
  (`RAIDS.ALL_LEVEL_RAIDS = "All {{level}} Raids"`) supplies the
  noun once; result reads natural English. Also dropped the unused
  `pluralKey` from `LevelOption` and the `_PLURAL` i18n keys (the
  shortened `RAID_N` strings work in both card and chip contexts).
  Backend `RaidLevelInfo.Name` is now the modifier form
  ("Mega Legendary") while `NamePlural` retains the full
  "Mega Legendary Raids" for any future standalone use.

Review fixes (MUST FIX)
- Snackbar undo subscription in `LevelSelector.removeCustom` now
  pipes through `takeUntilDestroyed(this.destroyRef)` so closing
  the dialog mid-toast can't fire the callback against a destroyed
  component.
- `raid-list.getRaidLevelName` was bypassing the live
  `RaidLevelService.byValue()` and using the baked-in `KNOWN_LEVELS`
  constant — cards would drift from the dialog if the API ever
  extended the canonical list. Cards now consult the service first,
  fall back to the baked-in resolver. `raid-list.ngOnInit` primes
  the cache so the list page doesn't depend on a dialog open.
- `LevelLabelPipe` now detects ngx-translate's "key not found"
  pass-through (translated string === key) and falls back to
  "Level {n}" instead of leaking "RAIDS.LEVEL.RAID_20" into the UI.
  Graceful degradation for future masterfile additions before
  locales catch up.

Review fixes (SHOULD FIX)
- `raid-edit-dialog.formatLevel` deleted — the dialog now injects
  `LevelLabelPipe` and calls `.transform()`, eliminating the
  duplicate label-resolution logic. Single source of truth.

Analyzers
- xUnit2032 in `MasterDataControllerRaidLevelsTests`: switched
  `Assert.IsAssignableFrom<T>` to `Assert.IsType<T>(..., exactMatch:
  false)`.
- CA1707 in `RaidLevelServiceTests` and the new controller test:
  test method names renamed to PascalCase to match the project's
  preferred style and silence the analyzer.

Dev workflow
- New `proxy.conf.json` forwards `/api/*` and `/auth/*` from the
  Angular dev server to the API. `environment.development.ts`
  apiUrl set to `''` so all HTTP calls are same-origin from the
  browser's view — works identically for `ng serve` + proxy and
  for the production single-port deployment. OAuth flows survive
  the proxy because Host is preserved.

Tests: 700/700 frontend (+1 fallback test), 1069/1069 backend.
Lint + prettier + dotnet format (scoped) all clean.
@hokiepokedad2
Copy link
Copy Markdown
Contributor Author

@prof-miles0 How does this look?

image image image image

- features/alarms.md: replace the generic "tier" wording on Raids/Eggs
  rows with pointers to a new "Raid level selector" subsection that
  documents the chip layout, the 19 masterfile-defined raid types,
  primary vs overflow split per pickerType, the ephemeral custom-add
  affordance, and the wildcard sentinel.
- architecture/backend.md: add a "Raid level service" section
  documenting IRaidLevelService, GET /api/masterdata/raid-levels, the
  baked-in fallback + live-fetch upgrade path, and the [Range] relax
  that lets PoracleNG-accepted custom integers pass validation. Also
  register the singleton in the service lifetimes table.
- architecture/frontend.md: document LevelSelectorComponent + the
  Angular RaidLevelService consumer (signal cache, baked-in fallback,
  LevelLabelPipe missing-key fallback), and the ephemeral palette
  behavior.
- getting-started/development-setup.md: the "proxies API requests"
  claim is now accurate thanks to the committed proxy.conf.json —
  describe it explicitly (changeOrigin: false to preserve Host for
  OAuth callbacks), note the empty apiUrl + same-origin dev flow,
  and document the --port override for matching a non-default
  Discord OAuth redirect URI.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Raid and Egg level selector only supports 6 levels, but PoracleNG supports many more

2 participants