Skip to content
Draft
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
2 changes: 1 addition & 1 deletion api/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Read [`../AGENTS.md`](../AGENTS.md) first for shared addresses/mechanics and ind
- subscribe payload: `{"type":"subscribe","channels":["summit","event"]}`

Query/pagination rules agents usually need:
- `/beasts/all`: `limit` default `25`, max `100`; `offset`; filters `prefix`, `suffix`, `beast_id`, `name`, `owner`; `sort` in `summit_held_seconds|level`; `include_total` optional (`false` skips `count(*)`).
- `/beasts/all`: `limit` default `25`, max `100`; `offset`; filters `prefix`, `suffix`, `beast_id`, `name`, `owner`, `shiny`, `animated`; `sort` in `summit_held_seconds|level`; `include_total` optional (`false` skips `count(*)`).
- `/logs`: `limit` default `50`, max `100`; `offset`; `category`, `sub_category` (comma-separated), `player`; `include_total` optional (`false` skips `count(*)`).
- `/beasts/stats/top`: `limit` default `25`, max `100`; `offset`; `include_total` optional (`false` skips `count(*)`).
- `/diplomacy`: `prefix` and `suffix` required; returns HTTP `400` if missing.
Expand Down
2 changes: 1 addition & 1 deletion api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ curl http://localhost:3001/health
### Query Parameters and Response Shapes

`GET /beasts/all`
- params: `limit` (default `25`, max `100`), `offset`, `prefix`, `suffix`, `beast_id`, `name`, `owner`, `sort` (`summit_held_seconds|level`), `include_total` (`true|false`, default `true`)
- params: `limit` (default `25`, max `100`), `offset`, `prefix`, `suffix`, `beast_id`, `name`, `owner`, `shiny`, `animated`, `sort` (`summit_held_seconds|level`), `include_total` (`true|false`, default `true`)
- returns: `{ data: Beast[], pagination: { limit, offset, total, has_more } }`

`GET /logs`
Expand Down
10 changes: 8 additions & 2 deletions api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ app.get("/health", async (c) => {
* - beast_id: Filter by beast type ID (optional, indexed)
* - name: Filter by beast name search (optional, uses beast_id index)
* - owner: Filter by owner address (optional, indexed)
* - shiny: Filter by shiny trait, 1 for shiny beasts (optional)
* - animated: Filter by animated trait, 1 for animated beasts (optional)
* - sort: Sort by "summit_held_seconds" or "level" (default: summit_held_seconds, both indexed)
* - include_total: Set to false to skip count(*) and return pagination.total=null
*/
Expand All @@ -201,16 +203,20 @@ app.get("/beasts/all", async (c) => {
const beastId = c.req.query("beast_id");
const name = c.req.query("name");
const ownerRaw = c.req.query("owner");
const shiny = c.req.query("shiny");
const animated = c.req.query("animated");
const sort = c.req.query("sort") || "summit_held_seconds";
const includeTotal = parseIncludeTotal(c.req.query("include_total"));
const owner = ownerRaw ? normalizeAddress(ownerRaw) : undefined;

// Build where conditions (all filters use indexed columns)
// Build where conditions.
const conditions = [];
if (prefix) conditions.push(eq(beasts.prefix, parseInt(prefix, 10)));
if (suffix) conditions.push(eq(beasts.suffix, parseInt(suffix, 10)));
if (beastId) conditions.push(eq(beasts.beast_id, parseInt(beastId, 10)));
if (owner) conditions.push(eq(beast_owners.owner, owner));
if (shiny) conditions.push(eq(beasts.shiny, parseInt(shiny, 10)));
if (animated) conditions.push(eq(beasts.animated, parseInt(animated, 10)));
if (name) {
// Find beast IDs that match the name search (uses beast_id index)
const lowerName = name.toLowerCase();
Expand Down Expand Up @@ -892,7 +898,7 @@ app.get("/", (c) => {
health: "GET /health",
beasts: {
by_owner: "GET /beasts/:owner",
all: "GET /beasts/all?limit=25&offset=0&prefix=&suffix=&beast_id=&name=&owner=&sort=summit_held_seconds",
all: "GET /beasts/all?limit=25&offset=0&prefix=&suffix=&beast_id=&name=&owner=&shiny=&animated=&sort=summit_held_seconds",
counts: "GET /beasts/stats/counts",
top: "GET /beasts/stats/top?limit=25&offset=0",
},
Expand Down
4 changes: 4 additions & 0 deletions client/src/api/summitApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export interface GetAllBeastsParams {
beast_id?: number;
name?: string;
owner?: string;
shiny?: number;
animated?: number;
sort?: 'summit_held_seconds' | 'level';
}

Expand Down Expand Up @@ -172,6 +174,8 @@ export const useSummitApi = () => {
if (params.beast_id) searchParams.set('beast_id', params.beast_id.toString());
if (params.name) searchParams.set('name', params.name);
if (params.owner) searchParams.set('owner', params.owner);
if (params.shiny) searchParams.set('shiny', params.shiny.toString());
if (params.animated) searchParams.set('animated', params.animated.toString());
if (params.sort) searchParams.set('sort', params.sort);

const response = await fetch(`${currentNetworkConfig.apiUrl}/beasts/all?${searchParams}`);
Expand Down
59 changes: 56 additions & 3 deletions client/src/components/dialogs/BeastDexModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,28 @@ type MySortKey = 'power' | 'level' | 'health' | 'name';
type AllSortKey = 'summit_held_seconds' | 'level';
type TypeKey = 'all' | 'Brute' | 'Hunter' | 'Magic';
type TabKey = 'mine' | 'all';
type AppearanceFilterKey = 'all' | 'animated' | 'shiny' | 'animated_shiny';

const matchesAppearanceFilter = (beast: Pick<Beast, 'shiny' | 'animated'>, filter: AppearanceFilterKey) => {
const isShiny = Boolean(beast.shiny);
const isAnimated = Boolean(beast.animated);

switch (filter) {
case 'animated':
return isAnimated;
case 'shiny':
return isShiny;
case 'animated_shiny':
return isAnimated && isShiny;
default:
return true;
}
};

const getAppearanceQueryParams = (filter: AppearanceFilterKey): { shiny?: number; animated?: number } => ({
shiny: filter === 'shiny' || filter === 'animated_shiny' ? 1 : undefined,
animated: filter === 'animated' || filter === 'animated_shiny' ? 1 : undefined,
});

// Transform AllBeast API response to Beast type for display
const transformAllBeast = (ab: AllBeast): Beast => {
Expand Down Expand Up @@ -88,6 +110,7 @@ export default function BeastDexModal(props: BeastDexModalProps) {
const [mySortBy, setMySortBy] = useState<MySortKey>('power');
const [allSortBy, setAllSortBy] = useState<AllSortKey>('level');
const [typeFilter, setTypeFilter] = useState<TypeKey>('all');
const [appearanceFilter, setAppearanceFilter] = useState<AppearanceFilterKey>('all');
const [selectedBeast, setSelectedBeast] = useState<Beast | null>(null);
const [page, setPage] = useState(1);
const pageSize = 24;
Expand Down Expand Up @@ -121,6 +144,9 @@ export default function BeastDexModal(props: BeastDexModalProps) {
if (typeFilter !== 'all') {
list = list.filter(b => (b.type || '') === typeFilter);
}
if (appearanceFilter !== 'all') {
list = list.filter(b => matchesAppearanceFilter(b, appearanceFilter));
}
switch (mySortBy) {
case 'power':
list.sort((a, b) => b.power - a.power);
Expand Down Expand Up @@ -149,7 +175,7 @@ export default function BeastDexModal(props: BeastDexModalProps) {
}

return list;
}, [collection, myPrefixFilter, mySuffixFilter, myNameFilter, mySortBy, typeFilter, summit, filterTokenIds]);
}, [collection, myPrefixFilter, mySuffixFilter, myNameFilter, mySortBy, typeFilter, appearanceFilter, summit, filterTokenIds]);

// Autocomplete options
const beastNameOptions = useMemo(() => Object.values(BEAST_NAMES), []);
Expand All @@ -173,7 +199,7 @@ export default function BeastDexModal(props: BeastDexModalProps) {

useEffect(() => {
setPage(1);
}, [myPrefixFilter, mySuffixFilter, myNameFilter, mySortBy, allSortBy, allNameFilter, allPrefixFilter, allSuffixFilter, typeFilter, collection.length, activeTab]);
}, [myPrefixFilter, mySuffixFilter, myNameFilter, mySortBy, allSortBy, allNameFilter, allPrefixFilter, allSuffixFilter, typeFilter, appearanceFilter, collection.length, activeTab]);

// Fetch all beasts when on "All Beasts" tab
useEffect(() => {
Expand All @@ -189,6 +215,7 @@ export default function BeastDexModal(props: BeastDexModalProps) {
name: allNameFilter || undefined,
prefix: allPrefixFilter || undefined,
suffix: allSuffixFilter || undefined,
...getAppearanceQueryParams(appearanceFilter),
sort: allSortBy,
});
if (cancelled) return;
Expand All @@ -215,7 +242,7 @@ export default function BeastDexModal(props: BeastDexModalProps) {
cancelled = true;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeTab, open, page, allNameFilter, allPrefixFilter, allSuffixFilter, allSortBy]);
}, [activeTab, open, page, allNameFilter, allPrefixFilter, allSuffixFilter, allSortBy, appearanceFilter]);

const handleSelect = (beast: Beast) => {
setSelectedBeast(beast);
Expand Down Expand Up @@ -416,6 +443,32 @@ export default function BeastDexModal(props: BeastDexModalProps) {
</Select>
</FormControl>
)}
<FormControl size="small" sx={styles.selectControl}>
<InputLabel id="appearance-select-label" sx={styles.inputLabel}>Appearance</InputLabel>
<Select
labelId="appearance-select-label"
id="appearance-select"
label="Appearance"
value={appearanceFilter}
onChange={(e) => setAppearanceFilter(e.target.value as AppearanceFilterKey)}
sx={styles.sortSelect}
MenuProps={{
PaperProps: {
sx: {
background: `${gameColors.darkGreen}`,
border: `1px solid ${gameColors.accentGreen}40`,
boxShadow: `0 8px 24px rgba(0,0,0,0.6)`,
'& .MuiMenuItem-root': { color: '#fff' },
}
}
}}
>
<MenuItem value="all">All</MenuItem>
<MenuItem value="animated">Animated</MenuItem>
<MenuItem value="shiny">Shiny</MenuItem>
<MenuItem value="animated_shiny">Animated + Shiny</MenuItem>
</Select>
</FormControl>
<Autocomplete
size="small"
options={beastNameOptions}
Expand Down
Loading