Skip to content

feat: implement favorites/wishlist with availability reminders in planner#367

Merged
imnaiyar merged 7 commits intomainfrom
copilot/add-favorites-feature
Oct 30, 2025
Merged

feat: implement favorites/wishlist with availability reminders in planner#367
imnaiyar merged 7 commits intomainfrom
copilot/add-favorites-feature

Conversation

Copy link
Contributor

Copilot AI commented Oct 29, 2025

User description

TODO

  • Perhaps group the available items, and usually show their icons (dk how i can then have a way to go to the source though)

Users need to track items they want to collect and be notified when those items become available through Traveling Spirits, Returning Spirits, events, seasons, or shops.

Changes

Items Display (items.ts)

  • Enabled wishlist toggle button (previously disabled)
  • Added favorite state indicators (⭐) to item lists and detail views
  • Wired up PlannerAction.ToggleFavorite action

Home Display (home.ts)

  • Added getAvailableFavoritedItems() - scans favorites against all active sources (TS, RS, events, seasons, shops)
  • Added createFavoritesReminder() - displays available favorites section (max 5 items)
  • Integrated reminder into home page between currencies and content sections

Implementation

Uses existing infrastructure:

  • favourites field in user schema (comma-separated GUIDs)
  • toggleItemFavorite() in planner-utils.ts
  • PlannerDataHelper.hasGuid() for state checks

Skips unlocked items in availability scan. Checks item presence in:

// Traveling Spirit
travelingSpirit.tree.node?.items

// Returning Spirits
rs.spirits.flatMap(s => s.tree?.node?.items)

// Season/Events
activeSeasons.spirits.flatMap(s => s.tree?.node?.items)
eventData.instance.spirits.flatMap(s => s.tree?.node?.items)
eventData.instance.shops.flatMap(sh => sh.itemList?.items)

// Permanent Shops
shops.flatMap(shop => shop.itemList?.items)

Reminder shows item name, availability source, and quick view button.

Original prompt

implement adding items to favourites (whishlist) in planner. and if any of them items that are favourite are currently available via purcahse be it through event, seasons, ts, rs, shops etc... it should show a little reminder in the home display


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.


PR Type

Enhancement


Description

  • Implement favorites/wishlist system for items with availability tracking

  • Display available favorited items in home page and dedicated favorites tab

  • Show favorite indicators (⭐) on items across list and detail views

  • Scan all purchase sources (TS, RS, events, seasons, shops) for favorited items

  • Sort spirit trees by date and improve cost display formatting


Diagram Walkthrough

flowchart LR
  User["User marks items<br/>as Favorite"]
  User -->|Toggle Action| Items["Items Display"]
  Items -->|Store in DB| Favorites["User Favorites<br/>List"]
  Favorites -->|Scan Sources| Scanner["Availability Scanner"]
  Scanner -->|Check TS/RS/Events| Sources["Purchase Sources"]
  Sources -->|Collect Available| Available["Available Items"]
  Available -->|Display in Home| Home["Home Display"]
  Available -->|Full View| FavTab["Favorites Tab"]
  Home -->|Show Reminder| Reminder["⭐ Reminder Section"]
  FavTab -->|List with Details| Details["Item Details<br/>& Navigation"]
Loading

File Walkthrough

Relevant files
Enhancement
8 files
service.ts
Add utility functions for items and cost handling               
+8/-1     
planner.ts
Add Favourite display tab to navigation enum                         
+1/-0     
favourites.ts
Implement favorites display with availability scanning     
+192/-0 
home.ts
Integrate favorites reminder into home display                     
+49/-13 
items.ts
Add favorite toggle and smart source navigation                   
+133/-66
spirits.ts
Sort spirit trees by date and add event instances               
+36/-6   
planner.ts
Register FavouriteDisplay handler in navigation                   
+2/-0     
SpiritTreeShared.ts
Draw favorite star indicator on spirit tree items               
+33/-0   
Bug fix
4 files
transformer.ts
Fix event instance spirit reference resolution                     
+4/-1     
shared.ts
Handle nullable cost formatting in displays                           
+2/-2     
shared.ts
Fix cost display and emoji formatting                                       
+3/-3     
ts.ts
Handle nullable cost formatting in TS display                       
+1/-1     
Refactoring
2 files
base.ts
Refactor tab filtering and expose filter manager                 
+5/-3     
seasons.ts
Refactor cost formatting and improve display layout           
+3/-15   
Documentation
1 files
copilot-instructions.md
Document planner architecture and new displays                     
+4/-0     
Configuration changes
1 files
package.json
Add planner emoji sync script                                                       
+2/-1     

@vercel
Copy link

vercel bot commented Oct 29, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Preview Comments Updated (UTC)
sky-helper-docs Ignored Ignored Preview Oct 30, 2025 10:59pm

- Enable wishlist button in item display (was disabled)
- Add toggle functionality for favorite/unfavorite items
- Show favorite indicator (⭐) on items in list and detail views
- Implement favorites availability checker in home display
- Show reminder when favorited items are available via:
  - Traveling Spirit
  - Returning Spirits
  - Current Season
  - Active Events
  - Permanent Shops
- Display up to 5 available favorited items in home page

Co-authored-by: imnaiyar <137700126+imnaiyar@users.noreply.github.com>
Copilot AI changed the title [WIP] Add favorites functionality to planner with reminders feat: implement favorites/wishlist with availability reminders in planner Oct 29, 2025
Copilot AI requested a review from imnaiyar October 29, 2025 15:36
@github-actions github-actions bot added the package:skyhelper Changes made to skyhelper package label Oct 29, 2025
@imnaiyar imnaiyar force-pushed the copilot/add-favorites-feature branch from 0244155 to f527058 Compare October 29, 2025 16:05
@imnaiyar imnaiyar force-pushed the copilot/add-favorites-feature branch from f527058 to 954dd6d Compare October 29, 2025 16:09
@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Oct 29, 2025

Deploying sky-utils with  Cloudflare Pages  Cloudflare Pages

Latest commit: 661aaf0
Status: ✅  Deploy successful!
Preview URL: https://a00c4c62.sky-utils.pages.dev
Branch Preview URL: https://copilot-add-favorites-featur.sky-utils.pages.dev

View logs

@github-actions github-actions bot added package:utils Changes made to utils package package:constants Changes made to constants package labels Oct 29, 2025
@imnaiyar
Copy link
Owner

/review

@qodo-code-review
Copy link
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Possible Duplicates

Items can be pushed multiple times to the reminder when they appear in more than one checked source (e.g., both an event spirit tree and the same event shop). Consider de-duplicating by GUID before slicing to the top 5.

): Array<{ item: IItem; source: string; sourceDetails: string }> {
  const favoritesString = this.settings.plannerData?.favourites ?? "";
  if (!favoritesString) return [];

  const availableItems: Array<{ item: IItem; source: string; sourceDetails: string }> = [];

  // Get all favorited item GUIDs
  const favoritedGuids = favoritesString.split(",").filter(Boolean);
  const getShopItem = (shop: IShop, guid: string, source: string) => {
    const spItem = shop.itemList?.items.find((item) => item.item.guid === guid)?.item;
    if (spItem) availableItems.push({ item: spItem, source, sourceDetails: shop.name ?? shop.spirit?.name ?? "Shop" });

    const iap = shop.iaps?.find((ip) => ip.items?.some((item) => item.guid === guid));
    if (iap) {
      const item = iap.items!.find((it) => it.guid === guid)!;
      availableItems.push({
        item,
        source,
        sourceDetails: `${iap.name ?? "IAP"} (${iap.returning ? `Returning IAP` : "New IAP"})`,
      });
    }
    if (shop.spirit?.tree) {
      const items = allTreeItems(shop.spirit.tree);
      const item = items.find((it) => it.guid === guid);
      if (item) {
        availableItems.push({ item, source, sourceDetails: `From: ${shop.spirit.name}` });
      }
    }
  };
  // Check each favorited item
  for (const guid of favoritedGuids) {
    const item = this.data.guidMap.get(guid) as IItem | undefined;
    if (!item || item.unlocked) continue; // Skip if not found or already unlocked

    // Check if available via Traveling Spirit
    if (travelingSpirit) {
      const hasItem = allTreeItems(travelingSpirit.tree).some((i) => i.guid === guid);
      if (hasItem) {
        availableItems.push({
          item,
          source: "Traveling Spirit",
          sourceDetails: travelingSpirit.spirit.name,
        });
        continue;
      }
    }

    // Check if available via Returning Spirits
    for (const rs of returningSpirits ?? []) {
      const rsItems = rs.spirits.flatMap((s) => allTreeItems(s.tree));
      if (rsItems.some((i) => i.guid === guid)) {
        availableItems.push({
          item,
          source: "Returning Spirits",
          sourceDetails: rs.name ?? "Special Visit",
        });
        break;
      }
    }

    // Check if available via current Season
    if (activeSeason) {
      const seasonItems = activeSeason.spirits.flatMap((sp) => (sp.tree ? allTreeItems(sp.tree) : []));
      if (seasonItems.some((i: any) => i.guid === guid)) {
        availableItems.push({
          item,
          source: "Current Season",
          sourceDetails: activeSeason.name,
        });
        continue;
      }
      if (activeSeason.shops) {
        // eslint-disable-next-line
        activeSeason.shops.forEach((sh) => getShopItem(sh, guid, `${activeSeason.name} Shop`));
      }
    }

    // Check if available via Events
    for (const instance of activeEventInstances ?? []) {
      const eventItems = instance.spirits.flatMap((s) => allTreeItems(s.tree));
      if (eventItems.some((i) => i.guid === guid)) {
        availableItems.push({
          item,
          source: "Event",
          sourceDetails: instance.event.name,
        });
        break;
      }
      // eslint-disable-next-line
      instance.shops.forEach((sh) => getShopItem(sh, guid, `${instance.name ?? instance.event.name} Shop`));
    }
  }

  return availableItems;
Side-Effects

Linking event instance spirits now mutates sp.events by pushing es objects; verify that events is intended to be an array of event-spirit links (not raw event instances) and that repeated transform calls won't push duplicates.

linkOne<ISpirit, IEventInstanceSpirit>(es.spirit as any, es, "spirit", data, (sp) => {
  sp.events ??= [];
  sp.events.push(es);
});
linkOne<ISpiritTree, IEventInstanceSpirit>(es.tree as any, es, "tree", data, "eventInstanceSpirit");
Drawing Artifact

The star outline uses a red-ish stroke which may clash with theme and readability; also a comment typo is present. Confirm color choice and consider aligning stroke color with existing palette or adding alpha tweaks.

if (item?.favourited) {
  const starSize = Math.max(12, Math.floor(itemSize * 0.15));
  const starX = -r * 0.5;
  const starY = r * 0.4;

  ctx.fillStyle = "#FFD700"; // yellow/gold color
  ctx.save();
  ctx.translate(starX, starY);
  ctx.beginPath();

  // Draw a 5-pointed star
  for (let i = 0; i < 5; i++) {
    const angle = (i * 4 * Math.PI) / 5 - Math.PI / 2;
    const pointX = Math.cos(angle) * starSize;
    const pointY = Math.sin(angle) * starSize;
    if (i === 0) {
      ctx.moveTo(pointX, pointY);
    } else {
      ctx.lineTo(pointX, pointY);
    }
  }
  ctx.closePath();
  ctx.fill();

  // Optional: Add a slight outline for better visibili"ty
  ctx.strokeStyle = "rgba(239, 20, 20, 0.5)";
  ctx.lineWidth = Math.max(1, Math.floor(starSize * 0.08));
  ctx.stroke();

  ctx.restore();
}

@imnaiyar imnaiyar marked this pull request as ready for review October 30, 2025 22:59
@imnaiyar imnaiyar requested a review from imnyr as a code owner October 30, 2025 22:59
@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Oct 30, 2025

PR Compliance Guide 🔍

(Compliance updated until commit 2dd1cf4)

Below is a summary of compliance checks for this PR:

Security Compliance
UI contrast/accessibility

Description: The favorite star drawing uses a hardcoded semi-transparent red stroke color which may
reduce contrast and readability against varied backgrounds; verify this does not impair
accessibility or leak sensitive visuals.
SpiritTreeShared.ts [322-325]

Referred Code
// Optional: Add a slight outline for better visibili"ty
ctx.strokeStyle = "rgba(239, 20, 20, 0.5)";
ctx.lineWidth = Math.max(1, Math.floor(starSize * 0.08));
ctx.stroke();
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing auditing: The new wishlist toggle action (PlannerAction.ToggleFavorite) and availability reminder
logic add user-affecting state changes without any added audit logging of who toggled what
and when.

Referred Code
  });
})(),
button({
  custom_id: createActionId({
    action: PlannerAction.ToggleFavorite,
    guid: item.guid,
    navState: this.state,
  }),
  label: isFavorited ? "Remove from Wishlist" : "Add to Wishlist",
  style: isFavorited ? 4 : 2,
  emoji: { name: "⭐" },
}),
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
No null checks: Functions scanning multiple sources for available favorites do not include explicit error
handling or null/empty edge-case guards around nested properties and external data
resolution beyond basic checks.

Referred Code
/**
 * Get list of favorited items that are currently available through various sources
 */
static getAvailableFavoritedItems(
  data: PlannerAssetData,
  settings: UserSchema,
): Array<{ item: IItem; source: string; cost?: ICost; price?: number; sourceDetails: string }> {
  const activeSeason = SkyPlannerData.getCurrentSeason(data);
  const activeEventInstances = SkyPlannerData.getEvents(data).current.map((e) => e.instance);
  const returningSpirits = SkyPlannerData.getCurrentReturningSpirits(data);
  const travelingSpirit = SkyPlannerData.getCurrentTravelingSpirit(data);
  const favoritesString = settings.plannerData?.favourites ?? "";
  if (!favoritesString) return [];

  const availableItems: Array<{ item: IItem; source: string; cost?: ICost; price?: number; sourceDetails: string }> = [];

  // Get all favorited item GUIDs
  const favoritedGuids = favoritesString.split(",").filter(Boolean);
  const getShopItem = (shop: IShop, guid: string, source: string) => {
    const spItem = shop.itemList?.items.find((item) => item.item.guid === guid);
    if (spItem) {


 ... (clipped 99 lines)
Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Unvalidated input: The code reads favorite GUIDs from settings.plannerData?.favourites and uses them to look
up items and build navigation without visible validation or length bounds, which may
require checks elsewhere.

Referred Code
const availableItems: Array<{ item: IItem; source: string; cost?: ICost; price?: number; sourceDetails: string }> = [];

// Get all favorited item GUIDs
const favoritedGuids = favoritesString.split(",").filter(Boolean);
const getShopItem = (shop: IShop, guid: string, source: string) => {
  const spItem = shop.itemList?.items.find((item) => item.item.guid === guid);
  if (spItem) {
    availableItems.push({ item: spItem.item, cost: spItem, source, sourceDetails: shop.name ?? shop.spirit?.name ?? "Shop" });
  }

  const iap = shop.iaps?.find((ip) => ip.items?.some((item) => item.guid === guid));
  if (iap) {
    const item = iap.items!.find((it) => it.guid === guid)!;
    availableItems.push({
      item,
      source,
      price: iap.price,
      sourceDetails: `${iap.name ?? "IAP"} (${iap.returning ? `Returning IAP` : "New IAP"})`,
    });
  }


 ... (clipped 16 lines)
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

Previous compliance checks

Compliance check up to commit 2dd1cf4
Security Compliance
Possible unsafe rendering

Description: The comment contains a likely accidental typo 'visibili"ty' which suggests recent manual
edits; verify no untrusted input is drawn onto canvas without sanitization elsewhere,
though this snippet itself does not process user input.
SpiritTreeShared.ts [322-325]

Referred Code
// Optional: Add a slight outline for better visibili"ty
ctx.strokeStyle = "rgba(239, 20, 20, 0.5)";
ctx.lineWidth = Math.max(1, Math.floor(starSize * 0.08));
ctx.stroke();
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
No audit logs: The new user-facing action to toggle wishlist favorites (PlannerAction.ToggleFavorite) is
introduced without any visible logging of who performed the action, when, and the outcome.

Referred Code
  custom_id: createActionId({
    action: PlannerAction.ToggleFavorite,
    guid: item.guid,
    navState: this.state,
  }),
  label: isFavorited ? "Remove from Wishlist" : "Add to Wishlist",
  style: isFavorited ? 4 : 2,
  emoji: { name: "⭐" },
}),
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Missing null checks: The availability scan assumes data arrays and nested properties exist (e.g.,
instance.shops.forEach, allTreeItems) without explicit try/catch or guards, which could
lead to runtime errors if inputs are undefined.

Referred Code
  if (activeSeason.shops) {
    // eslint-disable-next-line
    activeSeason.shops.forEach((sh) => getShopItem(sh, guid, `${activeSeason.name} Shop`));
  }
}

// Check if available via Events
for (const instance of activeEventInstances) {
  const node = instance.spirits.flatMap((s) => getAllNodes(s.tree)).find((n) => n.item?.guid === item.guid);
  const spirit = node ? node.root?.spiritTree?.eventInstanceSpirit : undefined;
  if (node) {
    availableItems.push({
      item,
      source: "Event",
      cost: node,
      sourceDetails: instance.event.name + (spirit ? ` (${spirit.name ?? spirit.spirit.name})` : ""),
    });
    break;
  }
  // eslint-disable-next-line
  instance.shops.forEach((sh) => getShopItem(sh, guid, `${instance.name ?? instance.event.name} Shop`));


 ... (clipped 1 lines)
Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Input validation: Favorited GUIDs from user settings are split and used to look up items and traverse nested
trees without explicit validation or sanitization, which may risk unexpected behavior with
malformed inputs.

Referred Code
const availableItems: Array<{ item: IItem; source: string; cost?: ICost; price?: number; sourceDetails: string }> = [];

// Get all favorited item GUIDs
const favoritedGuids = favoritesString.split(",").filter(Boolean);
const getShopItem = (shop: IShop, guid: string, source: string) => {
  const spItem = shop.itemList?.items.find((item) => item.item.guid === guid);
  if (spItem) {
    availableItems.push({ item: spItem.item, cost: spItem, source, sourceDetails: shop.name ?? shop.spirit?.name ?? "Shop" });
  }

  const iap = shop.iaps?.find((ip) => ip.items?.some((item) => item.guid === guid));
  if (iap) {
    const item = iap.items!.find((it) => it.guid === guid)!;
    availableItems.push({
      item,
      source,
      price: iap.price,
      sourceDetails: `${iap.name ?? "IAP"} (${iap.returning ? `Returning IAP` : "New IAP"})`,
    });
  }
  if (shop.spirit?.tree) {


 ... (clipped 15 lines)

@qodo-code-review
Copy link
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Invert the logic for finding available favorites

Refactor getAvailableFavoritedItems to first compile a collection of all
currently available items from every source. Then, filter this collection
against the user's favorites, which is more performant than the current approach
of checking all sources for each favorited item.

Examples:

packages/skyhelper/src/bot/handlers/planner-displays/favourites.ts [75-191]
  static getAvailableFavoritedItems(
    data: PlannerAssetData,
    settings: UserSchema,
  ): Array<{ item: IItem; source: string; cost?: ICost; price?: number; sourceDetails: string }> {
    const activeSeason = SkyPlannerData.getCurrentSeason(data);
    const activeEventInstances = SkyPlannerData.getEvents(data).current.map((e) => e.instance);
    const returningSpirits = SkyPlannerData.getCurrentReturningSpirits(data);
    const travelingSpirit = SkyPlannerData.getCurrentTravelingSpirit(data);
    const favoritesString = settings.plannerData?.favourites ?? "";
    if (!favoritesString) return [];

 ... (clipped 107 lines)

Solution Walkthrough:

Before:

function getAvailableFavoritedItems(data, settings):
  favoritedGuids = settings.favorites.split(',')
  availableItems = []

  for guid in favoritedGuids:
    item = data.findItem(guid)
    if not item or item.unlocked:
      continue

    // Check Traveling Spirit
    if travelingSpirit has item:
      availableItems.push(...)
      continue

    // Check Returning Spirits, Seasons, Events, Shops...
    // ... more checks for each source ...

  return availableItems

After:

function getAvailableFavoritedItems(data, settings):
  availableItemsMap = new Map()

  // 1. Collect all available items from all sources
  for itemNode in travelingSpirit.items:
    availableItemsMap.set(itemNode.item.guid, { item, source: 'TS', ... })
  
  // ... collect from returning spirits, seasons, events, shops ...

  // 2. Filter by user's favorites
  favoritedGuids = settings.favorites.split(',')
  result = []
  for guid in favoritedGuids:
    if availableItemsMap.has(guid):
      itemDetails = availableItemsMap.get(guid)
      if not itemDetails.item.unlocked:
        result.push(itemDetails)
  
  return result
Suggestion importance[1-10]: 8

__

Why: This is a significant architectural improvement for the new feature's core logic, as the current implementation in getAvailableFavoritedItems is inefficient and will scale poorly with more favorited items.

Medium
Possible issue
Prevent duplicate available favorite items

Fix a bug in getAvailableFavoritedItems to prevent duplicate item listings.
After finding an item from one source, use continue to proceed to the next
favorited item, ensuring each appears only once.

packages/skyhelper/src/bot/handlers/planner-displays/favourites.ts [117-188]

 // Check each favorited item
 for (const guid of favoritedGuids) {
   const item = data.guidMap.get(guid) as IItem | undefined;
   if (!item || item.unlocked) continue; // Skip if not found or already unlocked
 
   // Check if available via Traveling Spirit
   if (travelingSpirit) {
     const node = getAllNodes(travelingSpirit.tree).find((n) => n.item?.guid === item.guid);
     if (node) {
       availableItems.push({
         item,
         source: "Traveling Spirit",
         cost: node,
         sourceDetails: travelingSpirit.spirit.name,
       });
       continue;
     }
   }
 
   // Check if available via Returning Spirits
+  let found = false;
   for (const rs of returningSpirits) {
     const node = rs.spirits.flatMap((s) => getAllNodes(s.tree)).find((n) => n.item?.guid === item.guid);
     const spirit = node ? node.root?.spiritTree?.spirit : undefined;
     if (node) {
       availableItems.push({
         item,
         cost: node,
         source: "Returning Spirits",
         sourceDetails: `${rs.name ?? "Special Visit"}${spirit ? ` (Offered by ${spirit.name})` : ""}`,
       });
+      found = true;
       break;
     }
   }
+  if (found) continue;
 
   // Check if available via current Season
   if (activeSeason) {
     const node = activeSeason.spirits
       .flatMap((sp) => (sp.tree ? getAllNodes(sp.tree) : []))
       .find((n) => n.item?.guid === item.guid);
     const spirit = node ? node.root?.spiritTree?.spirit : undefined;
     if (node) {
       availableItems.push({
         item,
         cost: node,
         source: "Current Season",
         sourceDetails: activeSeason.name + (spirit ? ` (${spirit.name})` : ""),
       });
       continue;
     }
     if (activeSeason.shops) {
+      const initialLength = availableItems.length;
       // eslint-disable-next-line
       activeSeason.shops.forEach((sh) => getShopItem(sh, guid, `${activeSeason.name} Shop`));
+      if (availableItems.length > initialLength) continue;
     }
   }
 
   // Check if available via Events
   for (const instance of activeEventInstances) {
     const node = instance.spirits.flatMap((s) => getAllNodes(s.tree)).find((n) => n.item?.guid === item.guid);
     const spirit = node ? node.root?.spiritTree?.eventInstanceSpirit : undefined;
     if (node) {
       availableItems.push({
         item,
         source: "Event",
         cost: node,
         sourceDetails: instance.event.name + (spirit ? ` (${spirit.name ?? spirit.spirit.name})` : ""),
       });
+      found = true;
       break;
     }
-    // eslint-disable-next-line
+  }
+  if (found) continue;
+
+  const initialLength = availableItems.length;
+  // eslint-disable-next-line
+  for (const instance of activeEventInstances) {
     instance.shops.forEach((sh) => getShopItem(sh, guid, `${instance.name ?? instance.event.name} Shop`));
+    if (availableItems.length > initialLength) break;
   }
+  if (availableItems.length > initialLength) continue;
 }
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a logic bug in the new getAvailableFavoritedItems function where an item could be duplicated in the output if available from multiple sources, and the proposed fix addresses the issue.

Medium
Learned
best practice
Fix typo in comment

Fix the comment typo to maintain professional, readable code comments.

packages/utils/src/classes/SpiritTreeShared.ts [322]

-// Optional: Add a slight outline for better visibili"ty
+// Optional: Add a slight outline for better visibility
  • Apply / Chat
Suggestion importance[1-10]: 5

__

Why:
Relevant best practice - Avoid typos and unclear comments; keep code comments accurate and professional to aid maintainability.

Low
  • More

@imnaiyar imnaiyar merged commit a3d18f1 into main Oct 30, 2025
7 checks passed
@imnaiyar imnaiyar deleted the copilot/add-favorites-feature branch October 30, 2025 23:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

package:constants Changes made to constants package package:skyhelper Changes made to skyhelper package package:utils Changes made to utils package Review effort 3/5

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments