Skip to content

Add TypeScript SDK for Beast NFT data and SVG rendering#11

Open
loothero wants to merge 5 commits into
mainfrom
add-sdk
Open

Add TypeScript SDK for Beast NFT data and SVG rendering#11
loothero wants to merge 5 commits into
mainfrom
add-sdk

Conversation

@loothero

@loothero loothero commented Feb 8, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds a sdk/ directory with a standalone TypeScript SDK for Beast NFTs
  • Provides static lookup tables for all 75 beast names, tiers, and combat types
  • Embeds all beast image data as inline base64 URIs (PNG/GIF, regular/shiny) extracted from the Cairo source files
  • Includes an SVG card renderer that produces output matching the on-chain token_uri exactly (verified against tokens 100-110 via sncast)
  • Extraction scripts to regenerate image-data.ts if Cairo source images change

Test plan

  • npx tsc --noEmit passes (no type errors)
  • npx vitest run passes (59 tests)
  • On-chain comparison script verified SVG output matches for 11 token IDs
  • Verify SDK can be consumed from an external project via npm install

🤖 Generated with Claude Code

TypeScript SDK that provides:
- Static lookup tables for all 75 beast names, tiers, and types
- Inline base64 data URIs for all beast images (PNG/GIF, regular/shiny)
- SVG card renderer matching the on-chain token_uri output exactly
- Utility functions (calculatePower, getBeastInfo, getFullBeastName)

Includes 59 tests and extraction scripts to regenerate image data
from the Cairo source files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 8, 2026 04:59
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello @loothero, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the tooling for Loot Survivor Beast NFTs by introducing a comprehensive TypeScript SDK. This SDK centralizes beast data lookups and provides a robust SVG rendering capability, ensuring that off-chain representations of NFT cards are pixel-perfect matches to their on-chain counterparts. This will streamline development for applications interacting with Beast NFTs, offering reliable data and consistent visual rendering.

Highlights

  • New TypeScript SDK: A new standalone TypeScript SDK has been introduced under the sdk/ directory, centralizing beast data and rendering logic.
  • Static Lookup Tables: Static lookup tables for all 75 beast names, tiers, and combat types are now available within the SDK for quick data retrieval.
  • Embedded Image Data: Beast image data (PNG/GIF, regular/shiny variants) is embedded as inline base64 URIs, extracted directly from the Cairo source files.
  • On-Chain Matching SVG Renderer: An SVG card renderer has been implemented in TypeScript, capable of producing output that exactly matches the on-chain token_uri for Beast NFTs.
  • Automated Extraction and Verification Scripts: Scripts are included to automate the extraction of image data from Cairo sources into image-data.ts and to compare SDK-generated SVGs against on-chain versions for verification.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • sdk/.gitignore
    • Added standard ignore rules for build artifacts and dependencies.
  • sdk/tests/images.test.ts
    • New test file for image URL generation functions (getBeastImageUrl, getBeastImageUrls, getBeastStaticImageUrl, isLocalBeastImage).
  • sdk/tests/index.test.ts
    • New test file for core SDK utility functions (getFullBeastName, isGenesisBeast, getTierColor, calculatePower, getBeastInfo).
  • sdk/tests/lookups.test.ts
    • New test file for beast data lookup tables and their respective getter functions.
  • sdk/images.ts
    • New module defining functions to generate local image URLs for beasts, handling name formatting and variants.
  • sdk/index.ts
    • New main entry point for the Beast SDK, re-exporting modules and providing core utility functions.
  • sdk/lookups.ts
    • New module containing static lookup tables for 75 beast names, tiers, combat types, and item prefixes/suffixes.
  • sdk/package-lock.json
    • Added to manage SDK dependencies.
  • sdk/package.json
    • Added, defining the new @lootsurvivor/beast-sdk package, build/test scripts, and dev dependencies.
  • sdk/scripts/compare-onchain-svg.mjs
    • New script to compare SDK-generated SVGs with on-chain token_uri SVGs for a set of token IDs, including normalization logic for accurate comparison.
  • sdk/scripts/extract-beast-images.mjs
    • New script to extract base64 image data URIs (PNG/GIF, regular/shiny) from Cairo source files and generate image-data.ts.
  • sdk/svg-constants.ts
    • New module containing static SVG fragments and constants (gradients, icons, patterns, font base64) extracted directly from the Cairo beast_svg.cairo contract.
  • sdk/svg.ts
    • New module implementing the TypeScript port of the Cairo SVG renderer, providing generateBeastSvg and generateBeastSvgFromNFT functions.
  • sdk/tsconfig.json
    • Added TypeScript configuration for the new SDK.
  • sdk/types.ts
    • New module defining TypeScript interfaces and types for Beast NFT data, including BeastApiResponse, BeastCombatType, BeastTier, and BeastId.
Activity
  • The author, loothero, has initiated this pull request to introduce a new TypeScript SDK.
  • The SDK includes comprehensive test coverage, with 59 tests passing successfully.
  • On-chain comparison scripts have been run, verifying that the SDK's SVG output for 11 token IDs precisely matches the on-chain token_uri data.
  • A pending task is to verify the SDK's consumption from an external project via npm install.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a2e146531a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread sdk/svg.ts Outdated
Comment on lines +184 to +187
"<g transform='translate(15 210)'>" +
"<rect width='70' height='50' rx='5' fill='url(#panel)'/>" +
"<text x='35' y='18' text-anchor='middle' class='beast-label'>TIER</text>" +
`<text x='35' y='38' text-anchor='middle' class='beast-val'>${tier}</text>` +

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include SVG font/style definitions

These text elements rely on CSS classes (beast-label/beast-val), but the SVG defs never include a <style> block or the VT323 @font-face. As a result, rendered cards fall back to the browser’s default font/sizes and won’t match the on-chain output (which defines VT323 and label/value styles in src/beast_svg.cairo). This breaks the stated guarantee that the SVG matches the on-chain renderer.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch. The <style> block with VT323 @font-face and CSS class definitions (.label, .valL) was indeed missing from buildDefs(). Fixed — the VT323_FONT_BASE64 constant is now imported and embedded in the <style> block.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds a standalone TypeScript SDK under sdk/ for working with Beast NFTs, including static lookups, local image helpers, and an SVG renderer intended to match the on-chain token_uri output.

Changes:

  • Introduces lookup tables + utility functions for beast names/tiers/types/prefixes/suffixes and image URL generation.
  • Adds an SVG card renderer and extracted SVG constants intended to mirror the Cairo on-chain renderer.
  • Adds extraction/verification scripts plus a dedicated sdk/ package setup (tsconfig, package.json, tests).

Reviewed changes

Copilot reviewed 14 out of 16 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
sdk/types.ts Adds SDK-facing type definitions for API responses and domain types.
sdk/tsconfig.json TypeScript build configuration for emitting the SDK to dist/.
sdk/index.ts Barrel exports + core SDK utility functions (names, tiers, power, etc.).
sdk/lookups.ts Static lookup tables (names/types/prefix/suffix) and lookup helpers.
sdk/images.ts Local image URL construction helpers for beast assets.
sdk/svg.ts SVG renderer + NFT wrapper for generating the on-chain-matching card SVG.
sdk/svg-constants.ts Static SVG fragments (gradients/icons/etc.) for the renderer.
sdk/scripts/extract-beast-images.mjs Script to regenerate image-data.ts from Cairo sources.
sdk/scripts/compare-onchain-svg.mjs Script to diff SDK SVG output vs on-chain token_uri SVG.
sdk/package.json Defines @lootsurvivor/beast-sdk package metadata, scripts, deps.
sdk/package-lock.json Locks the SDK’s dev dependency graph.
sdk/tests/lookups.test.ts Tests for lookup tables and lookup helper functions.
sdk/tests/index.test.ts Tests for SDK utility functions (naming, tiers, power, etc.).
sdk/tests/images.test.ts Tests for image URL generation helpers.
sdk/.gitignore Ignores node_modules/ and dist/ within the SDK directory.
Files not reviewed (1)
  • sdk/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread sdk/svg.ts Outdated
Comment on lines +8 to +10
import { getBeastName, getBeastTier, getBeastType } from "./lookups";
import { getBeastImageDataUri } from "./image-data";
import { calculatePower } from "./index";

Copilot AI Feb 8, 2026

Copy link

Choose a reason for hiding this comment

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

svg.ts imports calculatePower from ./index while index.ts re-exports from ./svg, creating an ESM circular dependency (svg -> index -> svg). This can cause undefined imports or subtle init-order issues depending on bundler/runtime. Consider moving calculatePower into a small utility module (e.g. utils.ts) and importing it from there, or importing directly from the module that defines it without going through the barrel file.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good point. Moved calculatePower to a new utils.ts module to break the circular dependency (svg.tsindex.tssvg.ts). svg.ts now imports from ./utils, and index.ts re-exports from ./utils.

Comment on lines +110 to +118
// Dynamically load the SDK via tsx
// First, let's build the SDK SVG generator
const { generateBeastSvg } = await import("tsx/esm/api").then(tsx => {
return tsx.register();
}).then(() => {
return import("../svg.ts");
}).catch(() => {
return import("../svg.ts");
});

Copilot AI Feb 8, 2026

Copy link

Choose a reason for hiding this comment

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

This script attempts to load TypeScript modules via import("tsx/esm/api"), but tsx is not listed in sdk/package.json dependencies/devDependencies. Additionally, the catch(() => import("../svg.ts")) fallback still tries to import a .ts file without a loader, so node scripts/compare-onchain-svg.mjs will fail in a plain Node environment. Either add tsx as a devDependency and ensure the script runs with it, or import the compiled JS from dist/ (and build first).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added tsx as a devDependency. The script is a development-time comparison tool, so tsx is the right fit for loading TS modules directly.

Comment on lines +4 to +15
*
* Calls the beast contract for 10 token IDs via sncast, decodes the
* on-chain SVG, generates the SDK SVG, and diffs them.
*
* Usage: node scripts/compare-onchain-svg.mjs
*/

import { execSync } from "child_process";

const CONTRACT = "0x046da8955829adf2bda310099a0063451923f02e648cf25a1203aac6335cf0e4";
const TOKEN_IDS = [100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110];

Copilot AI Feb 8, 2026

Copy link

Choose a reason for hiding this comment

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

Comment says the script compares/calls "10 token IDs", but TOKEN_IDS contains 11 entries (100-110 inclusive). Update the comment or the list so they stay consistent.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed — updated comment to match the actual count of 11 token IDs.

Comment thread sdk/scripts/extract-beast-images.mjs Outdated
*
* Parses the 4 Cairo data files (png regular/shiny, gif regular/shiny),
* maps function names to beast IDs via beast_definitions.cairo,
* and outputs src/lib/beast-sdk/image-data.ts with 4 lookup maps.

Copilot AI Feb 8, 2026

Copy link

Choose a reason for hiding this comment

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

The header comment says this script outputs src/lib/beast-sdk/image-data.ts, but OUTPUT is ../image-data.ts (inside sdk/). Please update the comment to reflect the actual output path to avoid confusion for future regenerations.

Suggested change
* and outputs src/lib/beast-sdk/image-data.ts with 4 lookup maps.
* and outputs ../image-data.ts (inside sdk/) with 4 lookup maps.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed — updated comment to reflect actual output path (../image-data.ts inside sdk/).

Comment thread sdk/package.json
Comment on lines +1 to +13
{
"name": "@lootsurvivor/beast-sdk",
"version": "0.1.0",
"description": "TypeScript SDK for Beast NFT data lookups and local SVG rendering",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},

Copilot AI Feb 8, 2026

Copy link

Choose a reason for hiding this comment

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

Since this is a scoped package (@lootsurvivor/beast-sdk) and the PR goal mentions external consumption, publishing to npm will default to restricted access unless --access public is used. To make publishing reproducible, consider adding publishConfig: { "access": "public" } (or documenting the publish command).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added publishConfig with "access": "public" to make publishing straightforward for this scoped package.

Comment thread sdk/svg-constants.ts
Comment on lines +8 to +14
/**
* VT323 font as base64-encoded woff2.
* Verbatim from Cairo beast_svg.cairo line 109.
*/
export const VT323_FONT_BASE64 =
"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";

Copilot AI Feb 8, 2026

Copy link

Choose a reason for hiding this comment

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

VT323_FONT_BASE64 is exported but not referenced anywhere in the SDK. If the font embedding is required to match on-chain SVG output, it should be incorporated into the generated <defs>/<style>; otherwise consider removing the constant to avoid carrying a very large unused export.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Now imported and used in the <style> block within buildDefs(). The VT323 font is embedded as a @font-face declaration matching the on-chain contract exactly.

Comment thread sdk/svg.ts Outdated
power: number;
shiny: boolean;
rank: number | null; // 1=crown, >1=trophy, 0/null=none
imageUrl?: string; // Optional override, defaults to getBeastStaticImageUrl()

Copilot AI Feb 8, 2026

Copy link

Choose a reason for hiding this comment

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

BeastSvgInput.imageUrl is documented as defaulting to getBeastStaticImageUrl(), but generateBeastSvg() actually defaults to getBeastImageDataUri(beastId, shiny). Please update the comment (or the behavior) so consumers aren't surprised by getting an inline data URI by default.

Suggested change
imageUrl?: string; // Optional override, defaults to getBeastStaticImageUrl()
imageUrl?: string; // Optional override; if omitted, generateBeastSvg() uses getBeastImageDataUri(beastId, shiny) (inline data URI)

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed — updated JSDoc to match actual default behavior (getBeastImageDataUri(beastId, shiny)).

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a well-structured TypeScript SDK for Beast NFTs, including data lookups, image URL generation, and SVG rendering. The code is generally clean, well-tested, and includes helpful scripts for data extraction and on-chain verification. My review focuses on ensuring the SVG rendering perfectly matches the on-chain output as intended. I've identified a few discrepancies in sdk/svg.ts related to styling, fonts, and element usage that prevent an exact match with the on-chain SVGs. Addressing these will improve the accuracy and reliability of the SDK.

Comment thread sdk/svg.ts
Comment on lines +72 to +92
/** Build the <defs> block with gradients, icons, patterns, and font style. */
function buildDefs(isShiny: boolean): string {
// The isShiny param is accepted for future use (e.g., conditional defs).
// Currently all defs are always included, matching the Cairo contract.
void isShiny;

return (
"<defs>" +
SVG_GOLD_GRADIENT +
SVG_PANEL_GRADIENT +
SVG_SHINY_RIM_GRADIENT +
SVG_SPARKLE_GRADIENT +
SVG_LOGO_RAINBOW_GRADIENT +
SVG_HEART_ICON +
SVG_BOLT_ICON +
SVG_CROWN_ICON +
SVG_TROPHY_ICON +
SVG_PIN_PATTERN +
"</defs>"
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

The generated SVG is missing the <style> block which defines the VT323 font-face and the .label and .valL CSS classes. This is a critical omission as it will cause rendering inconsistencies compared to the on-chain SVG, particularly with text appearance. To ensure the output matches the on-chain renderer exactly, this block must be included in the <defs>.

You'll also need to add VT323_FONT_BASE64 to the import from ./svg-constants at the top of the file:

import { VT323_FONT_BASE64, ... } from "./svg-constants";
function buildDefs(isShiny: boolean): string {
  // The isShiny param is accepted for future use (e.g., conditional defs).
  // Currently all defs are always included, matching the Cairo contract.
  void isShiny;

  const style =
    "<style>" +
    `@font-face { font-family: 'VT323'; src: url(data:application/font-woff2;charset=utf-8;base64,${VT323_FONT_BASE64}) format('woff2'); font-weight: normal; font-style: normal; }` +
    ".label { fill: #c9c9d1; font-size: 14px; letter-spacing: 0.5px; font-weight: 500 }" +
    ".valL { fill: #fff; font-size: 18px; font-weight: 500 }" +
    "text { font-family: 'VT323', monospace; }" +
    "</style>";

  return (
    "<defs>" +
    style +
    SVG_GOLD_GRADIENT +
    SVG_PANEL_GRADIENT +
    SVG_SHINY_RIM_GRADIENT +
    SVG_SPARKLE_GRADIENT +
    SVG_LOGO_RAINBOW_GRADIENT +
    SVG_HEART_ICON +
    SVG_BOLT_ICON +
    SVG_CROWN_ICON +
    SVG_TROPHY_ICON +
    SVG_PIN_PATTERN +
    "</defs>"
  );
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed — added complete <style> block to buildDefs() including VT323 @font-face and all CSS class definitions (.label, .valL, and global text font-family).

Comment thread sdk/svg.ts
Comment on lines +177 to +199
/** Build the TIER/LEVEL/TYPE stats panels. */
function buildStatsPanel(
tier: number,
level: number,
beastType: string
): string {
return (
"<g transform='translate(15 210)'>" +
"<rect width='70' height='50' rx='5' fill='url(#panel)'/>" +
"<text x='35' y='18' text-anchor='middle' class='beast-label'>TIER</text>" +
`<text x='35' y='38' text-anchor='middle' class='beast-val'>${tier}</text>` +
"<g transform='translate(75)'>" +
"<rect width='70' height='50' rx='5' fill='url(#panel)'/>" +
"<text x='35' y='18' text-anchor='middle' class='beast-label'>LEVEL</text>" +
`<text x='35' y='38' text-anchor='middle' class='beast-val'>${level}</text>` +
"</g>" +
"<g transform='translate(150)'>" +
"<rect width='70' height='50' rx='5' fill='url(#panel)'/>" +
"<text x='35' y='18' text-anchor='middle' class='beast-label'>TYPE</text>" +
`<text x='35' y='37' text-anchor='middle' class='beast-val'>${escapeXml(beastType)}</text>` +
"</g>" +
"</g>"
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The CSS class names beast-label and beast-val do not match the on-chain contract, which uses label and valL. This will cause the text styling to be incorrect once the proper <style> block is added.

function buildStatsPanel(
  tier: number,
  level: number,
  beastType: string
): string {
  return (
    "<g transform='translate(15 210)'>" +
    "<rect width='70' height='50' rx='5' fill='url(#panel)'/>" +
    "<text x='35' y='18' text-anchor='middle' class='label'>TIER</text>" +
    `<text x='35' y='38' text-anchor='middle' class='valL'>${tier}</text>` +
    "<g transform='translate(75)'>" +
    "<rect width='70' height='50' rx='5' fill='url(#panel)'/>" +
    "<text x='35' y='18' text-anchor='middle' class='label'>LEVEL</text>" +
    `<text x='35' y='38' text-anchor='middle' class='valL'>${level}</text>` +
    "</g>" +
    "<g transform='translate(150)'>" +
    "<rect width='70' height='50' rx='5' fill='url(#panel)'/>" +
    "<text x='35' y='18' text-anchor='middle' class='label'>TYPE</text>" +
    `<text x='35' y='37' text-anchor='middle' class='valL'>${escapeXml(beastType)}</text>` +
    "</g>" +
    "</g>"
  );
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed — aligned CSS class names to match on-chain contract (label and valL instead of beast-label and beast-val).

Comment thread sdk/svg.ts
Comment on lines +202 to +224
/** Build the POWER and HEALTH panels with icons. */
function buildPowerHealthPanel(power: number, health: number): string {
return (
// Power panel
"<g transform='translate(15 265)'>" +
"<rect width='107' height='65' rx='5' fill='url(#panel)'/>" +
"<text x='35' y='22' text-anchor='middle' class='beast-label'>POWER</text>" +
`<text x='35' y='46' text-anchor='middle' class='beast-val'>${power}</text>` +
"<g transform='translate(82,31.15) scale(1)'>" +
"<use href='#bolt' pointer-events='none'/>" +
"</g>" +
"</g>" +
// Health panel
"<g transform='translate(128 265)'>" +
"<rect width='107' height='65' rx='5' fill='url(#panel)'/>" +
"<text x='35' y='22' text-anchor='middle' class='beast-label'>HEALTH</text>" +
`<text x='35' y='46' text-anchor='middle' class='beast-val'>${health}</text>` +
"<g transform='translate(77.3,32.744) scale(0.75)'>" +
"<use href='#heart' pointer-events='none'/>" +
"</g>" +
"</g>"
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The CSS class names beast-label and beast-val do not match the on-chain contract, which uses label and valL. This will cause the text styling to be incorrect once the proper <style> block is added.

function buildPowerHealthPanel(power: number, health: number): string {
  return (
    // Power panel
    "<g transform='translate(15 265)'>" +
    "<rect width='107' height='65' rx='5' fill='url(#panel)'/>" +
    "<text x='35' y='22' text-anchor='middle' class='label'>POWER</text>" +
    `<text x='35' y='46' text-anchor='middle' class='valL'>${power}</text>` +
    "<g transform='translate(82,31.15) scale(1)'>" +
    "<use href='#bolt' pointer-events='none'/>" +
    "</g>" +
    "</g>" +
    // Health panel
    "<g transform='translate(128 265)'>" +
    "<rect width='107' height='65' rx='5' fill='url(#panel)'/>" +
    "<text x='35' y='22' text-anchor='middle' class='label'>HEALTH</text>" +
    `<text x='35' y='46' text-anchor='middle' class='valL'>${health}</text>` +
    "<g transform='translate(77.3,32.744) scale(0.75)'>" +
    "<use href='#heart' pointer-events='none'/>" +
    "</g>" +
    "</g>"
  );
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed — same class name alignment applied to buildPowerHealthPanel() as well.

Comment thread sdk/svg.ts Outdated
Comment on lines +163 to +174
/** Build the beast image with clip path and native SVG image for crisp scaling. */
function buildBeastImage(imageUrl: string): string {
return (
"<clipPath id='artClip'>" +
"<rect width='144' height='144' rx='8'/>" +
"</clipPath>" +
"<rect x='15' y='58' width='220' height='144' rx='8' fill='#000'/>" +
"<g transform='translate(61, 65)' clip-path='url(#artClip)'>" +
`<image href='${escapeXml(imageUrl)}' x='1' y='1' width='128' height='128' image-rendering='pixelated'/>` +
"</g>" +
"<rect x='15' y='58' width='220' height='144' rx='8' fill='none' stroke='#fff' stroke-opacity='.08' stroke-width='2'/>"
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The on-chain SVG renderer uses <foreignObject> with an xhtml:img tag to display the beast image, reportedly for "crisper scaling on iOS Safari". This implementation uses an <image> tag instead. While <image> is more standard, this causes a deviation from the on-chain SVG. To match the on-chain output exactly as stated in the PR description, you should consider using <foreignObject> as well.

function buildBeastImage(imageUrl: string): string {
  return (
    "<clipPath id='artClip'>" +
    "<rect width='144' height='144' rx='8'/>" +
    "</clipPath>" +
    "<rect x='15' y='58' width='220' height='144' rx='8' fill='#000'/>" +
    "<g transform='translate(61, 65)' clip-path='url(#artClip)'>" +
    `<foreignObject x='1' y='1' width='128' height='128'><xhtml:img xmlns:xhtml='http://www.w3.org/1999/xhtml' src='${escapeXml(
      imageUrl
    )}' style='width:100%;height:100%;image-rendering:-webkit-optimize-contrast;-ms-interpolation-mode:nearest-neighbor;image-rendering:-moz-crisp-edges;image-rendering:pixelated;'/></foreignObject>` +
    "</g>" +
    "<rect x='15' y='58' width='220' height='144' rx='8' fill='none' stroke='#fff' stroke-opacity='.08' stroke-width='2'/>"
  );
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed — switched to <foreignObject> with xhtml:img to match the on-chain renderer exactly, including iOS Safari crisp-scaling CSS properties (-webkit-optimize-contrast, -moz-crisp-edges, pixelated).

pg-agent and others added 4 commits February 7, 2026 21:26
- Break circular dependency by moving calculatePower to utils.ts
- Add <style> block to buildDefs() with VT323 @font-face and CSS classes
- Fix CSS class names to match on-chain contract (label/valL)
- Use <foreignObject> with xhtml:img matching on-chain renderer
- Fix BeastSvgInput.imageUrl JSDoc to match actual default
- Add tsx devDependency for compare-onchain-svg script
- Add publishConfig for scoped package publishing
- Fix comment inaccuracies in scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add token URI encode/decode, schema validation, and safe parsing utilities.
Add genesis defaults and beast input validators to match on-chain rules.
Support animated image selection in SVG generation and add ranking utils.
Add tests for SVG, metadata, validation, and utils.
Introduce a Beast SDK client that fetches ownership via Summit API, Voyager, or RPC using the data provider and render-ready helpers. Add reverse lookup utilities, canonical on-chain types, and client helper tests plus README usage snippet.
Always try Voyager when configured for account token ID queries. Add lightweight token-id fetchers for Summit/Voyager and reduce RPC owner scan overhead with loose address normalization.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants