Skip to content

feat: effects cosmetic category (transport-ship trail) + UI#4418

Open
evanpelle wants to merge 5 commits into
mainfrom
effects-ui
Open

feat: effects cosmetic category (transport-ship trail) + UI#4418
evanpelle wants to merge 5 commits into
mainfrom
effects-ui

Conversation

@evanpelle

Copy link
Copy Markdown
Collaborator

What

Adds a new effects cosmetic category alongside skins/flags. Each effect is discriminated by effectType (only transportShipTrail today), whose visual config lives in attributes (solid / rainbow / pulse / gradient). Schema matches the production cosmetics.json shape exactly (incl. the url field).

This PR is UI + taxonomy only — the in-game WebGL trail rendering is intentionally deferred.

UI

  • Store gains an "Effects" tab.
  • Home page gains an "Effects" button opening a picker modal.
  • Both render effects grouped by effectType with a sub-header per type, via a shared <effects-grid> Lit element (mode="select" for the picker, mode="purchase" for the store). The picker shows owned effects + a Default tile and persists per-type; the store shows purchasable effects.

Data flow

  • Ownership via effect:* / effect:<name> flares (reuses cosmeticRelationship).
  • Selection is a per-effectType map persisted in UserSettings (settings.effects).
  • Server validates in isEffectAllowed, wired into isAllowed.
  • getPlayerCosmeticsRefs / getPlayerCosmetics resolve effects the same way as skins/flags (kept-on-fetch-failure, server is authority).

Tests

  • tsc --noEmit, ESLint, Prettier clean; full suite green.
  • New: CosmeticSchemas parse tests (incl. parsing the real read_transport_trail entry), UserSettings per-type selection, and Privilege effect validation.

Notes / follow-ups

  • The effect's display label shows "Boat Trail" for the transportShipTrail type (friendlier than the id).
  • Closed-source API gap: /shop/purchase (purchaseWithCurrency) needs to learn "effect" for currency purchase of effects; the dollar/product purchase path already works. Client types were widened accordingly.
  • In-game wake rendering can be ported from feat: transport-ship trail color cosmetics #4416.

🤖 Generated with Claude Code

Add an "effects" cosmetic category alongside skins/flags, discriminated by
`effectType` (only `transportShipTrail` today), whose visual config lives in
`attributes` (solid/rainbow/pulse/gradient). Ownership via `effect:*` /
`effect:<name>` flares; selection is a per-effectType map persisted in
UserSettings and validated server-side in `isEffectAllowed`.

UI: a Store "Effects" tab and a home "Effects" picker, both rendering effects
grouped by effectType with a sub-header per type via a shared <effects-grid>
Lit element. In-game WebGL rendering of the trail is deferred.

Schema matches the production cosmetics.json shape (incl. `url`); a test parses
the real effect entry. Core changes covered by schema/UserSettings/Privilege
tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds effect cosmetics across schema validation, selection storage, privilege checks, UI selection, store browsing, and client modal wiring. The PR also adds localization copy and tests for the new effect data and storage paths.

Changes

Effects cosmetics flow

Layer / File(s) Summary
Effect contracts and storage
src/core/CosmeticSchemas.ts, src/core/Schemas.ts, src/core/game/UserSettings.ts, src/client/Api.ts, tests/CosmeticSchemas.test.ts, tests/UserSettings.test.ts
Defines effect schema types, player effect records, persisted effect selection storage, the effect purchase type, and schema and storage tests.
Effect resolution and privilege
src/client/Cosmetics.ts, src/server/Privilege.ts, tests/Privilege.test.ts
Extends cosmetic resolution with effect entries and validates effect refs and flares on the server, with privilege tests for allowed and forbidden effect cases.
Effect preview and grid
src/client/components/EffectPreview.ts, src/client/components/CosmeticButton.ts, src/client/components/EffectsGrid.ts, src/client/Store.ts, resources/lang/en.json, src/client/LangSelector.ts
Adds effect swatch rendering, effect-aware cosmetic buttons, the effects grid component, the effects store tab text, and effect-related translation updates.
Effect modal and input wiring
src/client/EffectsInput.ts, src/client/EffectsModal.ts, src/client/Main.ts, src/client/components/PlayPage.ts, index.html
Adds the effects input and modal components, wires them into the main client flow, and adds the modal shell and play-page entry points.

Sequence Diagram(s)

sequenceDiagram
  participant EffectsInput
  participant Main
  participant EffectsModal
  participant fetchCosmetics
  participant EffectsGrid
  participant UserSettings
  EffectsInput->>Main: effects-input-click
  Main->>EffectsModal: open effects-modal
  EffectsModal->>fetchCosmetics: fetchCosmetics()
  fetchCosmetics-->>EffectsModal: Cosmetics
  EffectsModal->>EffectsGrid: mode="select", cosmetics, userMeResponse
  EffectsGrid->>UserSettings: getSelectedEffects()
  UserSettings-->>EffectsGrid: selected effect refs
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • openfrontio/OpenFrontIO#3918: Both PRs modify the same cosmetics plumbing in src/client/Cosmetics.ts to support another cosmetic type in the shared resolution flow.
  • openfrontio/OpenFrontIO#4006: Both PRs extend the shared cosmetics validation path across client and server code for a new cosmetic category.

Suggested labels

UI/UX, Feature, Backend

Poem

A trail of colors drifts through code,
New swatches light the modal road.
Effects take shape, both bold and neat,
And store tabs hum in tidy beat.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: adding an effects cosmetic category and its UI.
Description check ✅ Passed The description is directly related to the changeset and explains the new effects category, UI, data flow, and tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/Store.ts (1)

256-268: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a neutral affiliate empty-state message here.

This filter now serves effects too, but the empty state below still says store.no_skins. An affiliate link for an effect can now show the wrong message when nothing is purchasable. Please switch this path to a generic affiliate/store-empty key.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/Store.ts` around lines 256 - 268, The affiliate grid empty state
is still using a skin-specific message even though `renderAffiliateGrid()` now
includes effects and other purchasable cosmetics. Update the empty-state key in
`Store.renderAffiliateGrid()` to a neutral affiliate/store-empty localization
key instead of `store.no_skins`, so the message matches any empty purchasable
result.
🧹 Nitpick comments (1)
tests/Privilege.test.ts (1)

559-624: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Nice coverage — one extra case would lock the flare scope.

The exact-match path is tested in the happy direction, but nothing proves a name-scoped flare stays scoped. A short test where the user holds effect:crimson yet asks for spectrum should be forbidden — this guards against a future change that accidentally widens the match.

🧪 Suggested extra test
test("exact-match flare does not unlock a different effect", () => {
  const result = effectChecker.isAllowed(["effect:crimson"], {
    effects: { transportShipTrail: "spectrum" },
  });
  expect(result.type).toBe("forbidden");
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Privilege.test.ts` around lines 559 - 624, Add a missing negative test
in Privilege.test.ts for effectChecker.isAllowed to verify exact-match flares
stay scoped: when the caller has effect:crimson but requests transportShipTrail:
spectrum, the result should be forbidden. Place it alongside the existing Effect
validation in isAllowed tests to cover the effectChecker.isAllowed path and
prevent broadening exact-match permissions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/client/EffectsInput.ts`:
- Around line 38-48: The async mount flow in connectedCallback can race with
disconnectedCallback, causing access to a nulled _abortController after
getPlayerCosmetics() resolves. Capture the AbortController in a local variable
at the start of connectedCallback, then after awaiting getPlayerCosmetics()
verify it is still the current controller before setting trailAttributes or
registering the window listener. Use the existing connectedCallback and
disconnectedCallback lifecycle methods in EffectsInput to guard against
teardown/navigation races and only read controller.signal when the instance is
still mounted.

---

Outside diff comments:
In `@src/client/Store.ts`:
- Around line 256-268: The affiliate grid empty state is still using a
skin-specific message even though `renderAffiliateGrid()` now includes effects
and other purchasable cosmetics. Update the empty-state key in
`Store.renderAffiliateGrid()` to a neutral affiliate/store-empty localization
key instead of `store.no_skins`, so the message matches any empty purchasable
result.

---

Nitpick comments:
In `@tests/Privilege.test.ts`:
- Around line 559-624: Add a missing negative test in Privilege.test.ts for
effectChecker.isAllowed to verify exact-match flares stay scoped: when the
caller has effect:crimson but requests transportShipTrail: spectrum, the result
should be forbidden. Place it alongside the existing Effect validation in
isAllowed tests to cover the effectChecker.isAllowed path and prevent broadening
exact-match permissions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 12a0ef6b-597f-4dd4-a257-a663a32a92a9

📥 Commits

Reviewing files that changed from the base of the PR and between 2bd2039 and 0cbb915.

📒 Files selected for processing (19)
  • index.html
  • resources/lang/en.json
  • src/client/Api.ts
  • src/client/Cosmetics.ts
  • src/client/EffectsInput.ts
  • src/client/EffectsModal.ts
  • src/client/Main.ts
  • src/client/Store.ts
  • src/client/components/CosmeticButton.ts
  • src/client/components/EffectPreview.ts
  • src/client/components/EffectsGrid.ts
  • src/client/components/PlayPage.ts
  • src/core/CosmeticSchemas.ts
  • src/core/Schemas.ts
  • src/core/game/UserSettings.ts
  • src/server/Privilege.ts
  • tests/CosmeticSchemas.test.ts
  • tests/Privilege.test.ts
  • tests/UserSettings.test.ts

Comment on lines +38 to +48
async connectedCallback() {
super.connectedCallback();
this._abortController = new AbortController();
const cosmetics = await getPlayerCosmetics();
this.trailAttributes =
cosmetics.effects?.["transportShipTrail"]?.attributes ?? null;
window.addEventListener(
`${USER_SETTINGS_CHANGED_EVENT}:${EFFECTS_KEY}`,
this._onCosmeticSelected,
{ signal: this._abortController.signal },
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard the async mount path against disconnects.

If getPlayerCosmetics() resolves after disconnectedCallback() runs, Line 47 reads this._abortController.signal after _abortController was nulled, which can throw during teardown/navigation. Capture the controller in a local variable and bail out if it is no longer current before mutating state or registering the listener.

Suggested fix
   async connectedCallback() {
     super.connectedCallback();
-    this._abortController = new AbortController();
+    const abortController = new AbortController();
+    this._abortController = abortController;
     const cosmetics = await getPlayerCosmetics();
+    if (this._abortController !== abortController) {
+      return;
+    }
     this.trailAttributes =
       cosmetics.effects?.["transportShipTrail"]?.attributes ?? null;
     window.addEventListener(
       `${USER_SETTINGS_CHANGED_EVENT}:${EFFECTS_KEY}`,
       this._onCosmeticSelected,
-      { signal: this._abortController.signal },
+      { signal: abortController.signal },
     );
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async connectedCallback() {
super.connectedCallback();
this._abortController = new AbortController();
const cosmetics = await getPlayerCosmetics();
this.trailAttributes =
cosmetics.effects?.["transportShipTrail"]?.attributes ?? null;
window.addEventListener(
`${USER_SETTINGS_CHANGED_EVENT}:${EFFECTS_KEY}`,
this._onCosmeticSelected,
{ signal: this._abortController.signal },
);
async connectedCallback() {
super.connectedCallback();
const abortController = new AbortController();
this._abortController = abortController;
const cosmetics = await getPlayerCosmetics();
if (this._abortController !== abortController) {
return;
}
this.trailAttributes =
cosmetics.effects?.["transportShipTrail"]?.attributes ?? null;
window.addEventListener(
`${USER_SETTINGS_CHANGED_EVENT}:${EFFECTS_KEY}`,
this._onCosmeticSelected,
{ signal: abortController.signal },
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/EffectsInput.ts` around lines 38 - 48, The async mount flow in
connectedCallback can race with disconnectedCallback, causing access to a nulled
_abortController after getPlayerCosmetics() resolves. Capture the
AbortController in a local variable at the start of connectedCallback, then
after awaiting getPlayerCosmetics() verify it is still the current controller
before setting trailAttributes or registering the window listener. Use the
existing connectedCallback and disconnectedCallback lifecycle methods in
EffectsInput to guard against teardown/navigation races and only read
controller.signal when the instance is still mounted.

@github-project-automation github-project-automation Bot moved this from Triage to Development in OpenFront Release Management Jun 26, 2026
Match the production cosmetics.json shape: the effects catalog is now nested
two levels — effects[effectType][effectName] — and the effectType value is
snake_case `transport_ship_trail`.

- CosmeticsSchema.effects -> record(effectType, record(name, Effect))
- catalog reads (resolveCosmetics, Privilege.isEffectAllowed, getPlayerCosmetics,
  getPlayerCosmeticsRefs) use effects[effectType][name]; drop the now-redundant
  effectType-mismatch branch
- player-side (refs/resolved/UserSettings) stays flat-by-effectType (one effect
  per type)
- rename effectType value + i18n key + EffectsInput lookup to transport_ship_trail
- tests updated (nested fixtures, snake_case, parses the real nested blob)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/core/CosmeticSchemas.ts (1)

141-142: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Constrain effects to EffectTypeSchema

effects still accepts any string key, so effects["bogus"] parses and the outer map no longer tracks the inner discriminator. Switch the outer record to z.partialRecord(EffectTypeSchema, z.record(z.string(), EffectSchema)); add a superRefine only if you also want to reject key/effectType mismatches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/CosmeticSchemas.ts` around lines 141 - 142, The `effects` field in
`CosmeticSchemas` is still keyed by any string, so update the outer map to use
`EffectTypeSchema` instead of a plain string key. In `CosmeticSchemas`, change
the `effects` definition to `z.partialRecord(EffectTypeSchema,
z.record(z.string(), EffectSchema)).optional()` so only valid effect types are
accepted; add a `superRefine` only if you also need to validate that inner
`effectType` values match their outer key.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/core/CosmeticSchemas.ts`:
- Around line 141-142: The `effects` field in `CosmeticSchemas` is still keyed
by any string, so update the outer map to use `EffectTypeSchema` instead of a
plain string key. In `CosmeticSchemas`, change the `effects` definition to
`z.partialRecord(EffectTypeSchema, z.record(z.string(),
EffectSchema)).optional()` so only valid effect types are accepted; add a
`superRefine` only if you also need to validate that inner `effectType` values
match their outer key.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 58dd465c-3341-4972-8f0e-d58eeee22117

📥 Commits

Reviewing files that changed from the base of the PR and between 0cbb915 and d6b6bb5.

📒 Files selected for processing (9)
  • resources/lang/en.json
  • src/client/Cosmetics.ts
  • src/client/EffectsInput.ts
  • src/client/components/CosmeticButton.ts
  • src/core/CosmeticSchemas.ts
  • src/server/Privilege.ts
  • tests/CosmeticSchemas.test.ts
  • tests/Privilege.test.ts
  • tests/UserSettings.test.ts
✅ Files skipped from review due to trivial changes (1)
  • resources/lang/en.json
🚧 Files skipped from review as they are similar to previous changes (7)
  • tests/UserSettings.test.ts
  • tests/CosmeticSchemas.test.ts
  • tests/Privilege.test.ts
  • src/client/EffectsInput.ts
  • src/client/Cosmetics.ts
  • src/server/Privilege.ts
  • src/client/components/CosmeticButton.ts

…bute types

The served cosmetics.json nests effects as effects[effectType][effectName] with
a camelCase effectType outer key (transportShipTrail) and NO effectType field on
each effect. The previous discriminated-union-on-effectType rejected the whole
cosmetics payload.

- EffectSchema: drop the effectType field + discriminated union; effectType is
  the outer catalog key (any string, so unknown effectTypes don't fail the parse)
- attributes: lenient { type: string, color?, color2? } so unknown attribute
  types don't fail the parse
- carry effectType from the outer key onto ResolvedCosmetic + resolved
  PlayerEffect (server + client)
- swatch renderer: neutral fallback for unknown attribute types; UI renders only
  EFFECT_TYPES, so unknown effectTypes are ignored
- tests: nested fixtures, no effectType field, + tolerate-unknown cases

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/CosmeticSchemas.ts`:
- Around line 110-113: EffectSchema is over-constraining all effect entries by
always using TransportShipTrailAttributesSchema for attributes, which causes
unknown outer effect types to fail parsing. Update EffectSchema to branch on the
outer effectType key: keep the transportShipTrail-specific attributes schema for
known trail effects, and use a looser fallback attributes schema for any unknown
effect type so the catalog still parses without rejecting future entries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d08da30a-d509-4cb7-b68c-da4e7768932c

📥 Commits

Reviewing files that changed from the base of the PR and between d6b6bb5 and 35d0171.

📒 Files selected for processing (11)
  • resources/lang/en.json
  • src/client/Cosmetics.ts
  • src/client/EffectsInput.ts
  • src/client/components/CosmeticButton.ts
  • src/client/components/EffectPreview.ts
  • src/client/components/EffectsGrid.ts
  • src/core/CosmeticSchemas.ts
  • src/server/Privilege.ts
  • tests/CosmeticSchemas.test.ts
  • tests/Privilege.test.ts
  • tests/UserSettings.test.ts
✅ Files skipped from review due to trivial changes (1)
  • resources/lang/en.json
🚧 Files skipped from review as they are similar to previous changes (8)
  • tests/UserSettings.test.ts
  • src/client/components/EffectPreview.ts
  • src/client/EffectsInput.ts
  • src/client/components/CosmeticButton.ts
  • src/server/Privilege.ts
  • src/client/components/EffectsGrid.ts
  • src/client/Cosmetics.ts
  • tests/Privilege.test.ts

Comment on lines +110 to +113
export const EffectSchema = CosmeticSchema.extend({
attributes: TransportShipTrailAttributesSchema,
url: z.string().optional(),
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Unknown outer effect types still must look like transportShipTrail.

Line 111 hard-codes every effect entry to TransportShipTrailAttributesSchema. So effects.someFutureEffect.* only parses if its attributes also has the trail-style { type, color?, color2? } shape, which breaks the stated “unknown effect types should not fail the parse” contract and can reject the whole catalog at the client/server validation gates. Please validate known shapes from the outer effectType key and fall back to a loose attributes schema for unknown keys.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/CosmeticSchemas.ts` around lines 110 - 113, EffectSchema is
over-constraining all effect entries by always using
TransportShipTrailAttributesSchema for attributes, which causes unknown outer
effect types to fail parsing. Update EffectSchema to branch on the outer
effectType key: keep the transportShipTrail-specific attributes schema for known
trail effects, and use a looser fallback attributes schema for any unknown
effect type so the catalog still parses without rejecting future entries.

Catalog lookups assumed effects[effectType][key] where key === effect.name,
but selection and ownership flares are name-based; a catalog whose object key
differs from the effect's name made the effect silently unselectable. Add
findEffect() (key fast-path, name-field fallback) and use it at all three
lookup sites (getPlayerCosmeticsRefs, getPlayerCosmetics, isEffectAllowed).

Also register effects-input/effects-modal/effects-grid in
LangSelector.applyTranslation so they re-render once translations load —
without this the effects button rendered before i18n was ready and stayed
stuck on the literal "effects.title".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/server/Privilege.ts (1)

261-269: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject unknown effect buckets before casting them to EffectType.

CosmeticsSchema now tolerates unknown outer effects keys, but Line 267 force-casts every incoming key to EffectType and Line 294 returns it as if it were a supported value. If the catalog ever contains a future bucket, findEffect() will allow it through here and downstream code will see an out-of-union effectType even though this PR only supports transportShipTrail. Please validate the key against the supported effect-type set at this boundary, or widen the shared PlayerEffect contract everywhere instead of casting.

Also applies to: 280-295

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/Privilege.ts` around lines 261 - 269, Reject unsupported effect
buckets at the Privilege boundary instead of force-casting them to EffectType.
In Privilege.ts, update the refs.effects handling in the loop around
isEffectAllowed/findEffect so only supported keys (currently transportShipTrail)
are accepted; unknown keys should be skipped or explicitly rejected before
assigning to cosmetics.effects. Also make sure the returned value from
findEffect and the PlayerEffect/effects contract stay aligned so downstream code
never receives an out-of-union effectType from supported helpers like
isEffectAllowed and findEffect.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/server/Privilege.ts`:
- Around line 261-269: Reject unsupported effect buckets at the Privilege
boundary instead of force-casting them to EffectType. In Privilege.ts, update
the refs.effects handling in the loop around isEffectAllowed/findEffect so only
supported keys (currently transportShipTrail) are accepted; unknown keys should
be skipped or explicitly rejected before assigning to cosmetics.effects. Also
make sure the returned value from findEffect and the PlayerEffect/effects
contract stay aligned so downstream code never receives an out-of-union
effectType from supported helpers like isEffectAllowed and findEffect.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 696d0b3b-2fd2-4031-b950-559bb02283e2

📥 Commits

Reviewing files that changed from the base of the PR and between 35d0171 and 9894c00.

📒 Files selected for processing (7)
  • resources/lang/en.json
  • src/client/Cosmetics.ts
  • src/client/LangSelector.ts
  • src/core/CosmeticSchemas.ts
  • src/server/Privilege.ts
  • tests/CosmeticSchemas.test.ts
  • tests/Privilege.test.ts
✅ Files skipped from review due to trivial changes (2)
  • src/client/LangSelector.ts
  • resources/lang/en.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/Privilege.test.ts
  • src/client/Cosmetics.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Development

Development

Successfully merging this pull request may close these issues.

1 participant