Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions PLANS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Keep this checklist aligned with repository behavior. Check off work in the same
## Characters: Available

- [x] Add Brisshell as a Tier 5 Earth Assassin with her catalog images.
- [x] Stage Vivian and her signature artifact as hidden catalog records outside local development, with local-development catalog counts, until official metadata is available.
- [x] Filter owned and selectable characters by Tier 4 and Tier 5.
- [x] Sort owned characters by name or awakening level in either direction.
- [x] Show owned/total roster progress and disable additions when every character is owned.
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/Icon_Artifact/ArtifactVivian.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 20 additions & 1 deletion src/components/artifacts/artifacts-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ import { ARTIFACTS_DATA } from "@/data/artifacts/ARTIFACTS_DATA";
import { useAppStore } from "@/stores/app-store";

describe("ArtifactsPage", () => {
afterEach(cleanup);
afterEach(() => {
cleanup();
vi.unstubAllEnvs();
});

beforeEach(() => {
vi.stubEnv("VITE_NODE_ENV", "production");
Element.prototype.scrollIntoView = vi.fn();
useAppStore.setState({
artifactsOwned: {
Expand Down Expand Up @@ -208,6 +212,21 @@ describe("ArtifactsPage", () => {
expect(screen.getByRole("dialog")).toBeTruthy();
});

it("keeps Vivian's hidden artifact out of the add dialog", () => {
render(<ArtifactsPage />);
fireEvent.click(screen.getByRole("button", { name: "Add Artifact" }));

expect(screen.queryByText("Vivian's Artifact")).toBeNull();
});

it("shows Vivian's hidden artifact in local development", () => {
vi.stubEnv("VITE_NODE_ENV", "development");
render(<ArtifactsPage />);
fireEvent.click(screen.getByRole("button", { name: "Add Artifact" }));

expect(screen.getByText("Vivian's Artifact")).toBeTruthy();
});

it("allows another owned copy and resets fusion after closing", async () => {
render(<ArtifactsPage />);
const open = () =>
Expand Down
4 changes: 4 additions & 0 deletions src/components/artifacts/utils/artifact-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import type { CharacterClassId } from "@/data/character-classes/CHARACTER_CLASS_
import type { ElementId } from "@/data/elements/ELEMENTS_DATA";
import type { TierId } from "@/data/tiers/TIERS_DATA";

export const isArtifactVisible = ({ is_hidden }: Pick<Artifact, "is_hidden">) =>
import.meta.env.VITE_NODE_ENV === "development" || !is_hidden;

export const FUSION_LEVELS = [1, 2, 3, 4, 5] as const;

export type ArtifactFilters = {
Expand All @@ -26,6 +29,7 @@ export const filterArtifacts = (
const search = filters.search?.trim().toLowerCase();
return artifacts.filter(
(a) =>
isArtifactVisible(a) &&
(!search || a.name.toLowerCase().includes(search)) &&
(!filters.selectedTiers?.length ||
filters.selectedTiers.includes(a.tier_id)) &&
Expand Down
7 changes: 5 additions & 2 deletions src/components/characters/components/add-character.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import CharacterCard from "@/components/characters/components/character-card";
import { CharacterOwnedDetailsForm } from "@/components/characters/components/character-details-form";
import { CharacterFilter } from "@/components/characters/components/character-filter";
import { emptyCharacterFilters } from "@/components/characters/store/characters-filter-store";
import { matchesCharacterFilters } from "@/components/characters/utils/character-utils";
import {
isCharacterVisible,
matchesCharacterFilters,
} from "@/components/characters/utils/character-utils";
import { TierPortrait } from "@/components/shared/tier-portrait";
import { Button } from "@/components/ui/button";
import {
Expand All @@ -31,7 +34,7 @@ export function AddCharacter() {
const charToAddInfo = hasSelectedChar ? CHARACTERS_DATA[charToAdd] : null;

const ownedSet = new Set(Object.values(charactersOwned).map((c) => c.id));
const characters = Object.values(CHARACTERS_DATA);
const characters = Object.values(CHARACTERS_DATA).filter(isCharacterVisible);
const ownedCount = characters.filter((character) =>
ownedSet.has(character.id),
).length;
Expand Down
39 changes: 33 additions & 6 deletions src/components/characters/components/character-filter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,13 @@ const ownedCharacterNames = () =>
.map((element) => element.textContent);

describe("character search", () => {
afterEach(cleanup);
afterEach(() => {
cleanup();
vi.unstubAllEnvs();
});

beforeEach(() => {
vi.stubEnv("VITE_NODE_ENV", "production");
Element.prototype.scrollIntoView = vi.fn();
useAppStore.setState({ charactersOwned: owned });
useCharacterFilter.setState({
Expand Down Expand Up @@ -93,7 +97,10 @@ describe("character search", () => {
});

it("shows roster progress and disables adding when every character is owned", () => {
const total = Object.keys(CHARACTERS_DATA).length;
const visibleCharacters = Object.values(CHARACTERS_DATA).filter(
({ is_hidden }) => !is_hidden,
);
const total = visibleCharacters.length;
render(<AddCharacter />);

expect(screen.getByText(`3/${total}`)).toBeTruthy();
Expand All @@ -106,10 +113,7 @@ describe("character search", () => {
act(() => {
useAppStore.setState({
charactersOwned: Object.fromEntries(
Object.values(CHARACTERS_DATA).map(({ id }) => [
id,
{ ...owned[1], id },
]),
visibleCharacters.map(({ id }) => [id, { ...owned[1], id }]),
),
});
});
Expand All @@ -124,6 +128,29 @@ describe("character search", () => {
expect(screen.queryByRole("dialog")).toBeNull();
});

it("keeps hidden Vivian out of the add list and roster total", () => {
render(<AddCharacter />);

expect(
screen.getByText(`3/${Object.keys(CHARACTERS_DATA).length - 1}`),
).toBeTruthy();
fireEvent.click(screen.getByRole("button", { name: "Add Character" }));

expect(screen.queryByText("Vivian")).toBeNull();
});

it("shows hidden Vivian in local development", () => {
vi.stubEnv("VITE_NODE_ENV", "development");
render(<AddCharacter />);

expect(
screen.getByText(`3/${Object.keys(CHARACTERS_DATA).length}`),
).toBeTruthy();
fireEvent.click(screen.getByRole("button", { name: "Add Character" }));

expect(screen.getByText("Vivian")).toBeTruthy();
});

it("filters candidates locally and clears before closing Add Character", () => {
useAppStore.setState({ charactersOwned: { 1: owned[1] } });
render(<AddCharacter />);
Expand Down
11 changes: 10 additions & 1 deletion src/components/characters/utils/character-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { CharacterFilters } from "@/components/characters/store/characters-filter-store";
import type { Character } from "@/data/characters/CHARACTERS_DATA";

export const isCharacterVisible = ({
is_hidden,
}: Pick<Character, "is_hidden">) =>
import.meta.env.VITE_NODE_ENV === "development" || !is_hidden;

export function getAwakeningBonus(awakeningBoost: number) {
if (awakeningBoost >= 5) return 4;
if (awakeningBoost >= 3) return 2;
Expand All @@ -12,9 +17,13 @@ export const isMaxSkill = (level: number) => {
};

export const matchesCharacterFilters = (
character: Pick<Character, "class_id" | "element_id" | "name" | "tier_id">,
character: Pick<
Character,
"class_id" | "element_id" | "is_hidden" | "name" | "tier_id"
>,
filters: CharacterFilters,
) =>
isCharacterVisible(character) &&
(!filters.search ||
character.name.toLowerCase().includes(filters.search.toLowerCase())) &&
(!filters.selectedCharacterClass.length ||
Expand Down
14 changes: 12 additions & 2 deletions src/components/loadouts/components/loadout-card-character-row.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isArtifactVisible } from "@/components/artifacts/utils/artifact-utils";
import { isCharacterVisible } from "@/components/characters/utils/character-utils";
import { TierPortrait } from "@/components/shared/tier-portrait";
import { ARTIFACTS_DATA } from "@/data/artifacts/ARTIFACTS_DATA";
import { CHARACTERS_DATA } from "@/data/characters/CHARACTERS_DATA";
Expand Down Expand Up @@ -40,15 +42,23 @@ export const LoadoutCardCharacterRow = ({
onEditMonsterling,
onEditArtifact,
}: LoadoutCardCharacterRowProps) => {
const character =
const catalogCharacter =
slot.characterId !== null ? CHARACTERS_DATA[slot.characterId] : null;
const character =
catalogCharacter && isCharacterVisible(catalogCharacter)
? catalogCharacter
: null;
const characterOwned =
slot.characterId !== null ? charactersOwned[slot.characterId] : null;
const artifactId = slot.artifactInstanceId;
const artifactOwned = artifactId ? artifactsOwned[artifactId] : null;
const artifact = artifactOwned
const catalogArtifact = artifactOwned
? ARTIFACTS_DATA[artifactOwned.artifact_id]
: null;
const artifact =
catalogArtifact && isArtifactVisible(catalogArtifact)
? catalogArtifact
: null;
return (
<div className="grid grid-cols-5 gap-1 rounded-md border bg-muted/20 p-2">
<LoadoutCardCharacterTile
Expand Down
20 changes: 17 additions & 3 deletions src/components/loadouts/components/loadout-editor.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isArtifactVisible } from "@/components/artifacts/utils/artifact-utils";
import { isCharacterVisible } from "@/components/characters/utils/character-utils";
import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
Expand Down Expand Up @@ -87,8 +89,12 @@ export const LoadoutEditor = ({
<TabsList className="grid w-full grid-cols-3 divide-x divide-border group-data-[orientation=horizontal]/tabs:h-[4.375rem]">
{SLOT_INDEXES.map((index) => {
const characterId = draft.characters[index].characterId;
const character =
const catalogCharacter =
characterId === null ? null : CHARACTERS_DATA[characterId];
const character =
catalogCharacter && isCharacterVisible(catalogCharacter)
? catalogCharacter
: null;
return (
<TabsTrigger
key={index}
Expand All @@ -111,13 +117,21 @@ export const LoadoutEditor = ({
</TabsList>
{SLOT_INDEXES.map((index) => {
const slot = draft.characters[index];
const character =
const catalogCharacter =
slot.characterId === null ? null : CHARACTERS_DATA[slot.characterId];
const character =
catalogCharacter && isCharacterVisible(catalogCharacter)
? catalogCharacter
: null;
const artifactId = slot.artifactInstanceId;
const artifactOwned = artifactId ? artifactsOwned[artifactId] : null;
const artifact = artifactOwned
const catalogArtifact = artifactOwned
? ARTIFACTS_DATA[artifactOwned.artifact_id]
: null;
const artifact =
catalogArtifact && isArtifactVisible(catalogArtifact)
? catalogArtifact
: null;
const equipmentIds = slot.equipment_ids ?? [null, null, null, null];
return (
<TabsContent
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isArtifactVisible } from "@/components/artifacts/utils/artifact-utils";
import { PortraitWithName } from "@/components/shared/portrait-with-name";
import { TierPortrait } from "@/components/shared/tier-portrait";
import { ARTIFACTS_DATA } from "@/data/artifacts/ARTIFACTS_DATA";
Expand All @@ -17,7 +18,11 @@ export const LoadoutPreviewArtifact = ({
onEdit,
}: LoadoutPreviewArtifactProps) => {
const item = id ? owned[id] : null;
const artifact = item ? ARTIFACTS_DATA[item.artifact_id] : null;
const catalogArtifact = item ? ARTIFACTS_DATA[item.artifact_id] : null;
const artifact =
catalogArtifact && isArtifactVisible(catalogArtifact)
? catalogArtifact
: null;
if (!item || !artifact || !id)
return <LoadoutPreviewPlaceholder label="Artifact unavailable" />;
const card = (
Expand Down
7 changes: 6 additions & 1 deletion src/components/loadouts/components/loadout-preview-row.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isCharacterVisible } from "@/components/characters/utils/character-utils";
import {
MONSTERLING_CARD_WIDTH,
MONSTERLING_COMPACT_CARD_WIDTH,
Expand Down Expand Up @@ -42,8 +43,12 @@ export const LoadoutPreviewRow = ({
onEditMonsterling,
onEditArtifact,
}: LoadoutPreviewRowProps) => {
const character =
const catalogCharacter =
slot.characterId === null ? null : CHARACTERS_DATA[slot.characterId];
const character =
catalogCharacter && isCharacterVisible(catalogCharacter)
? catalogCharacter
: null;
const monsterlingCardWidth =
monsterlingStatsDisplay === "icons"
? MONSTERLING_COMPACT_CARD_WIDTH
Expand Down
36 changes: 36 additions & 0 deletions src/components/loadouts/components/loadouts-list-ui.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ describe("LoadoutsList", () => {
cleanup();
vi.restoreAllMocks();
vi.unstubAllGlobals();
vi.unstubAllEnvs();
});

it("renders the shared centered empty-state treatment", () => {
Expand All @@ -135,6 +136,41 @@ describe("LoadoutsList", () => {
).toBeTruthy();
});

it("hides persisted hidden catalog assignments outside local development", () => {
vi.stubEnv("VITE_NODE_ENV", "production");
useAppStore.setState({
charactersOwned: {
25: {
id: 25,
awakening: 0,
skills: { basic: 1, switch: 1, special: 1, ultimate: 1 },
},
},
artifactsOwned: { vivian: { artifact_id: 39, fusion_level: 1 } },
loadouts: {
team: {
...teamLoadout,
characters: [
{
...teamLoadout.characters[0],
characterId: 25,
artifactInstanceId: "vivian",
},
teamLoadout.characters[1],
teamLoadout.characters[2],
],
},
},
});

render(<LoadoutsList />);

expect(screen.queryByAltText("Vivian portrait")).toBeNull();
expect(screen.queryByAltText("Vivian's Artifact portrait")).toBeNull();
expect(screen.getByText("Artifact unavailable")).toBeTruthy();
expect(screen.getByAltText("Unknown character portrait")).toBeTruthy();
});

it("searches loadout and assigned character names while preserving alphabetical order", () => {
useAppStore.setState({
charactersOwned,
Expand Down
7 changes: 6 additions & 1 deletion src/components/loadouts/components/loadouts-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import toast from "react-hot-toast";
import { useGoogleAnalytics } from "tanstack-router-ga4";
import { EditArtifactDetailsDialog } from "@/components/artifacts/components/edit-artifact-details-dialog";
import { EditCharacterDetailsDialog } from "@/components/characters/components/edit-character-details-dialog";
import { isCharacterVisible } from "@/components/characters/utils/character-utils";
import { CreateLoadoutSnapshotDialog } from "@/components/loadout-snapshots/components/create-loadout-snapshot-dialog";
import { LoadoutCard } from "@/components/loadouts/components/loadout-card";
import {
Expand Down Expand Up @@ -65,7 +66,11 @@ export const LoadoutsList = () => {
loadout.name,
...loadout.characters.flatMap(({ characterId }) => {
if (characterId === null) return [];
const character = CHARACTERS_DATA[characterId];
const catalogCharacter = CHARACTERS_DATA[characterId];
const character =
catalogCharacter && isCharacterVisible(catalogCharacter)
? catalogCharacter
: null;
return character ? [character.name] : [];
}),
].some((searchableName) =>
Expand Down
12 changes: 10 additions & 2 deletions src/components/loadouts/hooks/use-loadout-dialog-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import {
filterArtifacts,
} from "@/components/artifacts/utils/artifact-utils";
import { emptyCharacterFilters } from "@/components/characters/store/characters-filter-store";
import { matchesCharacterFilters } from "@/components/characters/utils/character-utils";
import {
isCharacterVisible,
matchesCharacterFilters,
} from "@/components/characters/utils/character-utils";
import {
type EquipmentFilters,
emptyEquipmentFilters,
Expand Down Expand Up @@ -319,9 +322,14 @@ export function useLoadoutDialogController(
character_slot: characterIndex,
});
const id = draft.characters[characterIndex].characterId;
const catalogCharacter = id === null ? null : CHARACTERS_DATA[id];
const character =
catalogCharacter && isCharacterVisible(catalogCharacter)
? catalogCharacter
: null;
setCharacterFilters({
...emptyCharacterFilters(),
search: id === null ? "" : (CHARACTERS_DATA[id]?.name ?? ""),
search: character?.name ?? "",
});
setPickerTarget({ type: LOADOUT_TARGET_TYPES.CHARACTER, characterIndex });
};
Expand Down
Loading
Loading