diff --git a/lexicons/ai.syui.card.admin.json b/lexicons/ai.syui.card.admin.json index d459aa3..954e06b 100644 --- a/lexicons/ai.syui.card.admin.json +++ b/lexicons/ai.syui.card.admin.json @@ -6,24 +6,55 @@ "main": { "type": "record", "key": "literal:self", - "description": "Card game configuration (admin only)", + "description": "Card game configuration and master data (admin only)", "record": { "type": "object", - "required": ["card", "rate", "createdAt", "updatedAt"], + "required": ["gacha", "card", "createdAt", "updatedAt"], "properties": { - "card": { + "gacha": { "type": "object", - "required": ["pickup"], + "required": ["pickup", "rate"], "properties": { - "pickup": { "type": "integer", "description": "Pickup card ID" } + "pickup": { "type": "integer", "description": "Pickup card ID" }, + "rate": { + "type": "object", + "required": ["pickup", "rare"], + "properties": { + "pickup": { "type": "integer", "description": "1/n for pickup rate (100 = 1%)" }, + "rare": { "type": "integer", "description": "1/n for rare:1 rate (10 = 10%), rare:2 = 1/(n*10), rare:3 = 1/(n*100)" } + } + } } }, - "rate": { - "type": "object", - "required": ["pickup", "rare"], - "properties": { - "pickup": { "type": "integer", "description": "1/n for pickup rate (100 = 1%)" }, - "rare": { "type": "integer", "description": "1/n for rare:1 rate (10 = 10%), rare:2 = 1/(n*10), rare:3 = 1/(n*100)" } + "card": { + "type": "array", + "description": "Card master data", + "items": { + "type": "object", + "required": ["id", "character", "name", "text", "cp", "effect"], + "properties": { + "id": { "type": "integer", "description": "Card ID" }, + "character": { "type": "integer", "description": "Associated character ID" }, + "name": { + "type": "object", + "required": ["ja", "en"], + "properties": { + "ja": { "type": "string" }, + "en": { "type": "string" } + } + }, + "text": { + "type": "object", + "required": ["ja", "en"], + "properties": { + "ja": { "type": "string" }, + "en": { "type": "string" } + } + }, + "cp": { "type": "string", "description": "CP type (status, time, damage)" }, + "effect": { "type": "string", "description": "Effect type (status, fly, mode, damage)" }, + "key": { "type": "string", "description": "Key binding (R1, L1, Y, X, etc.)" } + } } }, "createdAt": { "type": "string", "format": "datetime" }, diff --git a/lexicons/ai.syui.rse.admin.json b/lexicons/ai.syui.rse.admin.json index 0e90123..2c9cbd4 100644 --- a/lexicons/ai.syui.rse.admin.json +++ b/lexicons/ai.syui.rse.admin.json @@ -21,10 +21,12 @@ "id": { "type": "integer", "description": "Ability ID" }, "name": { "type": "string", "description": "Ability name (ai, quark, neutron, atom, sun)" }, "kind": { "type": "string", "description": "Attribute type (consciousness, matter)" }, + "color": { "type": "string", "description": "Color code (e.g., #ffd700)" }, "level": { "type": "integer", "description": "Hierarchy level (0=fundamental)" }, "relation": { "type": "array", "items": { "type": "integer" }, "description": "Advantage IDs" }, "weakness": { "type": "array", "items": { "type": "integer" }, "description": "Weakness IDs" }, - "multiplier": { "type": "integer", "description": "Damage multiplier percent (e.g., 150 = 1.5x)" } + "multiplier": { "type": "integer", "description": "Damage multiplier percent (e.g., 150 = 1.5x)" }, + "phantom": { "type": "boolean", "description": "Whether this ability is phantom/lost" } } } }, @@ -55,6 +57,26 @@ } } }, + "item": { + "type": "array", + "description": "Item definitions", + "items": { + "type": "object", + "required": ["id", "name", "text"], + "properties": { + "id": { "type": "integer", "description": "Item ID" }, + "name": { "type": "string", "description": "Item name" }, + "text": { + "type": "object", + "description": "Item description (localized)", + "properties": { + "en": { "type": "string", "description": "English text" }, + "ja": { "type": "string", "description": "Japanese text" } + } + } + } + } + }, "collection": { "type": "array", "description": "ATProto collection definitions", diff --git a/public/card/100.webp b/public/card/100.webp index ae74088..fea4b63 100644 Binary files a/public/card/100.webp and b/public/card/100.webp differ diff --git a/public/card/200.webp b/public/card/200.webp index ae74088..fea4b63 100644 Binary files a/public/card/200.webp and b/public/card/200.webp differ diff --git a/public/card/201.webp b/public/card/201.webp new file mode 100644 index 0000000..ae74088 Binary files /dev/null and b/public/card/201.webp differ diff --git a/public/card/300.webp b/public/card/300.webp index 6ba4d5f..fea4b63 100644 Binary files a/public/card/300.webp and b/public/card/300.webp differ diff --git a/public/card/301.webp b/public/card/301.webp new file mode 100644 index 0000000..6ba4d5f Binary files /dev/null and b/public/card/301.webp differ diff --git a/public/card/400.webp b/public/card/400.webp deleted file mode 100644 index cfe9d52..0000000 Binary files a/public/card/400.webp and /dev/null differ diff --git a/public/content/did:plc:6qyecktefllvenje24fcxnie/ai.syui.card.admin/self.json b/public/content/did:plc:6qyecktefllvenje24fcxnie/ai.syui.card.admin/self.json new file mode 100644 index 0000000..07ce76c --- /dev/null +++ b/public/content/did:plc:6qyecktefllvenje24fcxnie/ai.syui.card.admin/self.json @@ -0,0 +1,31 @@ +{ + "uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.card.admin/self", + "cid": "", + "value": { + "$type": "ai.syui.card.admin", + "gacha": { + "pickup": 13, + "rate": { + "rare": 10, + "pickup": 100 + } + }, + "card": [ + { "id": 0, "character": 0, "name": { "ja": "アイ", "en": "ai" }, "text": { "ja": "アイの基礎ステータスは、このカードのcpを基準にアップする。", "en": "This card's CP is referenced for the character's base status" }, "cp": "status", "effect": "status" }, + { "id": 1, "character": 0, "name": { "ja": "夢幻", "en": "dream" }, "text": { "ja": "質量解放により星間空間を自由に移動する。飛行時間は、このカードのcpを基準に延長される。", "en": "This card's CP is referenced for flight duration" }, "cp": "time", "effect": "fly", "key": "R1" }, + { "id": 2, "character": 0, "name": { "ja": "光彩", "en": "shiny" }, "text": { "ja": "モードと呼ばれる変身を行う。スキル発動時、動作スピードがアップする。継続時間及び効果は、このカードのcpを基準にアップする。", "en": "This card's CP is referenced for transformation duration" }, "cp": "time", "effect": "mode", "key": "L1" }, + { "id": 3, "character": 0, "name": { "ja": "中性子", "en": "neutron" }, "text": { "ja": "手のひらに中性子星を作り出して前方に放つ。ヒットした敵に物理と属性の大ダメージを与える。敵に与える物理ダメージは、このカードのcpを基準にアップする。", "en": "This card's CP is referenced for physical damage" }, "cp": "damage", "effect": "damage" , "key": "Y" }, + { "id": 13, "character": 0, "name": { "ja": "創造", "en": "create" }, "text": { "ja": "自身の周囲に真空を作り出し、物理と属性の範囲ダメージを与える。敵に与える属性ダメージは、このカードのcpを基準にアップする。", "en": "This card's CP is referenced for elemental damage" }, "cp": "damage", "effect": "damage", "key": "X" }, + { "id": 100, "character": 1, "name": { "ja": "ユイ", "en": "yui" }, "text": { "ja": "", "en": "" }, "cp": "", "effect": "" }, + { "id": 101, "character": 1, "name": { "ja": "", "en": "" }, "text": { "ja": "", "en": "" }, "cp": "", "effect": "" }, + { "id": 200, "character": 2, "name": { "ja": "ドラゴン", "en": "dragon" }, "text": { "ja": "", "en": "" }, "cp": "", "effect": "" }, + { "id": 201, "character": 2, "name": { "ja": "", "en": "" }, "text": { "ja": "", "en": "" }, "cp": "", "effect": "" }, + { "id": 300, "character": 3, "name": { "ja": "ロボット", "en": "robot" }, "text": { "ja": "", "en": "" }, "cp": "", "effect": "" }, + { "id": 301, "character": 3, "name": { "ja": "", "en": "" }, "text": { "ja": "", "en": "" }, "cp": "", "effect": "" }, + { "id": 400, "character": 4, "name": { "ja": "こんにゃーん", "en": "conyan" }, "text": { "ja": "", "en": "" }, "cp": "", "effect": "" }, + { "id": 401, "character": 4, "name": { "ja": "", "en": "" }, "text": { "ja": "", "en": "" }, "cp": "", "effect": "" } + ], + "createdAt": "2026-01-25T09:02:20.000Z", + "updatedAt": "2026-01-25T09:02:20.000Z" + } +} diff --git a/public/content/did:plc:6qyecktefllvenje24fcxnie/ai.syui.card.user/self.json b/public/content/did:plc:6qyecktefllvenje24fcxnie/ai.syui.card.user/self.json new file mode 100644 index 0000000..e565b69 --- /dev/null +++ b/public/content/did:plc:6qyecktefllvenje24fcxnie/ai.syui.card.user/self.json @@ -0,0 +1,46 @@ +{ + "uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.card.user/self", + "cid": "bafyreicjbb6qdl77sdsma5ep674znlgvnhenqqogppx3mmawlpengoxol4", + "value": { + "card": [ + { + "cp": 464, + "id": 1, + "cid": "5w4xmnyzukh62", + "rare": 1, + "unique": false + }, + { + "cp": 634, + "id": 0, + "cid": "6lzqdddvggzcs", + "rare": 0, + "unique": false + }, + { + "cp": 2, + "id": 2, + "cid": "5cjxquqzywetg", + "rare": 0, + "unique": false + }, + { + "cp": 3, + "id": 3, + "cid": "bmdlhjwedjlmu", + "rare": 0, + "unique": false + }, + { + "cp": 43, + "id": 13, + "cid": "25kp6sbgcdug7", + "rare": 4, + "unique": true + } + ], + "$type": "ai.syui.card.user", + "createdAt": "2026-01-28T17:10:10.535Z", + "updatedAt": "2026-01-28T17:10:10.535Z" + } +} diff --git a/public/content/did:plc:6qyecktefllvenje24fcxnie/ai.syui.rse.admin/self.json b/public/content/did:plc:6qyecktefllvenje24fcxnie/ai.syui.rse.admin/self.json new file mode 100644 index 0000000..0f4857a --- /dev/null +++ b/public/content/did:plc:6qyecktefllvenje24fcxnie/ai.syui.rse.admin/self.json @@ -0,0 +1,285 @@ +{ + "uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.rse.admin/self", + "cid": "bafyreihgv33l3kdbpghum3ul5uugscfoaooyyx7nu3eaiuge4l6pru6je4", + "value": { + "item": [ + { "id": 0, "name": "a", "text": { "en": "world", "ja": "世界" } }, + { "id": 1, "name": "i", "text": { "en": "The Pillar of Creation at the center of the galaxy stands tall in a place called the Eternal Stone, which is said to tolerate no change. The three attributes of gold, silver, and copper are engraved on it.", "ja": "銀河の中心にある創造の柱。そこにそびえ立つ永遠の石には金、銀、銅の3つの属性が刻み込まれている" } }, + { "id": 2, "name": "gordbox", "text": { "en": "The first color in this world", "ja": "この世界の最初の色" } }, + { "id": 3, "name": "silverbox", "text": { "en": "The second color in this world", "ja": "この世界の2番目の色" } }, + { "id": 4, "name": "bluebox", "text": { "en": "The third color in this world. It was once red, but was rewritten by someone", "ja": "この世界の3番目の色。かつては赤だったが、何者かに書き換えられた" } }, + { "id": 5, "name": "copperbox", "text": { "en": "The Third Lost Color of the World", "ja": "この世界から失われた3番目の色" } } + ], + "$type": "ai.syui.rse.admin", + "system": [ + { + "id": 0, + "name": "ai", + "domain": "ability" + }, + { + "id": 1, + "name": "yui", + "domain": "unique" + }, + { + "id": 2, + "name": "at", + "domain": "account" + }, + { + "id": 3, + "name": "world", + "domain": "planet" + }, + { + "id": 4, + "name": "dream", + "domain": "origin" + } + ], + "ability": [ + { + "id": 0, + "kind": "i", + "name": "ai", + "color": "#000", + "level": 0, + "relation": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "weakness": [ + 1 + ], + "multiplier": 150 + }, + { + "id": 1, + "kind": "unique", + "name": "yui", + "color": "#fff", + "level": 0, + "relation": [], + "weakness": [], + "multiplier": 100 + }, + { + "id": 2, + "kind": "phantom", + "name": "axion", + "color": "#8b00ff", + "level": 1, + "relation": [ + 3, + 5, + 6 + ], + "weakness": [ + 4 + ], + "multiplier": 150 + }, + { + "id": 3, + "kind": "phantom", + "name": "baryon", + "color": "#0066cc", + "level": 1, + "relation": [ + 4, + 6, + 7 + ], + "weakness": [ + 2 + ], + "multiplier": 150 + }, + { + "id": 4, + "kind": "phantom", + "name": "photon", + "color": "#ffff00", + "level": 1, + "relation": [ + 2, + 7, + 8 + ], + "weakness": [ + 3 + ], + "multiplier": 150 + }, + { + "id": 5, + "kind": "matter", + "name": "quark", + "color": "#ff0000", + "level": 2, + "relation": [ + 6 + ], + "weakness": [ + 0, + 2 + ], + "multiplier": 150 + }, + { + "id": 6, + "kind": "matter", + "name": "neutron", + "color": "#808080", + "level": 2, + "relation": [ + 7 + ], + "weakness": [ + 5, + 2, + 3 + ], + "multiplier": 150 + }, + { + "id": 7, + "kind": "matter", + "name": "atom", + "color": "#00cc66", + "level": 2, + "relation": [ + 8 + ], + "weakness": [ + 6, + 3, + 4 + ], + "multiplier": 150 + }, + { + "id": 8, + "kind": "matter", + "name": "sun", + "color": "#ff8c00", + "level": 2, + "relation": [ + 5 + ], + "weakness": [ + 7, + 4 + ], + "multiplier": 150 + }, + { + "id": 9, + "kind": "element", + "name": "gold", + "color": "#ffd700", + "level": 3, + "relation": [ + 10 + ], + "weakness": [ + 11, + 12 + ], + "multiplier": 150 + }, + { + "id": 10, + "kind": "element", + "name": "silver", + "color": "#c0c0c0", + "level": 3, + "relation": [ + 11, + 12 + ], + "weakness": [ + 9 + ], + "multiplier": 150 + }, + { + "id": 11, + "kind": "element", + "name": "copper", + "color": "#b87333", + "level": 3, + "relation": [ + 9 + ], + "weakness": [ + 10 + ], + "multiplier": 150, + "phantom": true + }, + { + "id": 12, + "kind": "element", + "name": "oxygen", + "color": "#87ceeb", + "level": 3, + "relation": [ + 9 + ], + "weakness": [ + 10 + ], + "multiplier": 150 + } + ], + "character": [ + { + "id": 0, + "mode": 2, + "name": "ai", + "ability": 0 + }, + { + "id": 1, + "mode": 2, + "name": "yui", + "ability": 1 + }, + { + "id": 2, + "mode": 0, + "name": "dragon", + "ability": 2 + }, + { + "id": 3, + "mode": 0, + "name": "robot", + "ability": 5 + } + ], + "createdAt": "2026-01-27T16:25:09.000Z", + "updatedAt": "2026-01-27T16:25:09.000Z", + "collection": [ + { + "id": 0, + "name": "card", + "nsid": "ai.syui.card" + }, + { + "id": 1, + "name": "rse", + "nsid": "ai.syui.rse" + } + ] + } +} diff --git a/public/content/did:plc:6qyecktefllvenje24fcxnie/ai.syui.rse.user/self.json b/public/content/did:plc:6qyecktefllvenje24fcxnie/ai.syui.rse.user/self.json new file mode 100644 index 0000000..29b0069 --- /dev/null +++ b/public/content/did:plc:6qyecktefllvenje24fcxnie/ai.syui.rse.user/self.json @@ -0,0 +1,83 @@ +{ + "uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.rse.user/self", + "cid": "bafyreih36zbcu2jx3yqxpjftvagarinwalaiodwxgyi5fqnw4iqkgslmya", + "value": { + "item": [ + { + "cp": 0, + "id": 0, + "cid": "0", + "rare": 0, + "unique": true + }, + { + "cp": 1, + "id": 1, + "cid": "1", + "rare": 0, + "unique": false + }, + { + "cp": 2, + "id": 2, + "cid": "2", + "rare": 0, + "unique": false + }, + { + "cp": 3, + "id": 3, + "cid": "3", + "rare": 0, + "unique": false + }, + { + "cp": 4, + "id": 4, + "cid": "4", + "rare": 0, + "unique": false + }, + { + "cp": 5, + "id": 5, + "cid": "5", + "rare": 0, + "unique": false + } + ], + "$type": "ai.syui.rse.user", + "character": [ + { + "cp": 0, + "id": 0, + "cid": "0", + "rare": 0, + "unique": true + }, + { + "cp": 1, + "id": 1, + "cid": "1", + "rare": 0, + "unique": false + }, + { + "cp": 2, + "id": 2, + "cid": "2", + "rare": 0, + "unique": false + }, + { + "cp": 3, + "id": 3, + "cid": "3", + "rare": 0, + "unique": false + } + ], + "createdAt": "2026-01-28T12:04:27.000Z", + "updatedAt": "2026-01-28T12:04:27.000Z" + } +} diff --git a/public/rse/item/0.webp b/public/rse/item/0.webp index 749a39d..749921d 100644 Binary files a/public/rse/item/0.webp and b/public/rse/item/0.webp differ diff --git a/public/rse/item/1.webp b/public/rse/item/1.webp new file mode 100644 index 0000000..749a39d Binary files /dev/null and b/public/rse/item/1.webp differ diff --git a/public/rse/item/2.webp b/public/rse/item/2.webp new file mode 100644 index 0000000..96f265c Binary files /dev/null and b/public/rse/item/2.webp differ diff --git a/public/rse/item/3.webp b/public/rse/item/3.webp new file mode 100644 index 0000000..f0f8a90 Binary files /dev/null and b/public/rse/item/3.webp differ diff --git a/public/rse/item/4.webp b/public/rse/item/4.webp new file mode 100644 index 0000000..19f8548 Binary files /dev/null and b/public/rse/item/4.webp differ diff --git a/public/rse/item/5.webp b/public/rse/item/5.webp new file mode 100644 index 0000000..7a698ab Binary files /dev/null and b/public/rse/item/5.webp differ diff --git a/src/web/components/card.ts b/src/web/components/card.ts index 41357a8..4a0877c 100644 --- a/src/web/components/card.ts +++ b/src/web/components/card.ts @@ -14,6 +14,33 @@ export interface CardCollection { updatedAt: string } +export interface CardAdminEntry { + id: number + character: number + name: { ja: string; en: string } + text: { ja: string; en: string } + cp: string + effect: string + key?: string | null +} + +export interface CardAdminData { + gacha: { pickup: number; rate: { rare: number; pickup: number } } + card: CardAdminEntry[] +} + +// Get current language +function getLang(): string { + return localStorage.getItem('preferredLang') || 'ja' +} + +// Get localized text +function getLocalizedText(obj: { ja: string; en: string } | undefined): string { + if (!obj) return '' + const lang = getLang() + return obj[lang as 'ja' | 'en'] || obj.ja || obj.en || '' +} + // Get rarity class name function getRarityClass(card: UserCard): string { if (card.unique) return 'unique' @@ -21,9 +48,13 @@ function getRarityClass(card: UserCard): string { return '' } - -// Render single card with optional count badge -export function renderCard(card: UserCard, baseUrl: string = '/card', count?: number): string { +// Render single card with optional count badge and admin info +export function renderCard( + card: UserCard, + baseUrl: string = '/card', + count?: number, + adminEntry?: CardAdminEntry +): string { const rarityClass = getRarityClass(card) const imageUrl = `${baseUrl}/${card.id}.webp` @@ -34,6 +65,21 @@ export function renderCard(card: UserCard, baseUrl: string = '/card', count?: nu const countBadge = count && count > 1 ? `x${count}` : '' + // Admin info (name, key, text) + const name = adminEntry ? getLocalizedText(adminEntry.name) : '' + const text = adminEntry ? getLocalizedText(adminEntry.text) : '' + const key = adminEntry?.key || '' + + const infoHtml = (name || text || key) ? ` +
+
+ ${name} + ${key ? `` : ''} +
+ ${text ? `
${text}
` : ''} +
+ ` : '' + return `
@@ -46,6 +92,7 @@ export function renderCard(card: UserCard, baseUrl: string = '/card', count?: nu
${card.cp}
+ ${infoHtml}
` } @@ -109,13 +156,9 @@ export function renderCardPage( } } - // Sort by unique first, then rarity (desc), then by id + // Sort by id const sortedGroups = Array.from(cardGroups.values()) - .sort((a, b) => { - if (a.card.unique !== b.card.unique) return a.card.unique ? -1 : 1 - if (b.card.rare !== a.card.rare) return b.card.rare - a.card.rare - return a.card.id - b.card.id - }) + .sort((a, b) => a.card.id - b.card.id) const cardsHtml = sortedGroups.map(({ card, count }) => { return renderCard(card, '/card', count) diff --git a/src/web/components/rse.ts b/src/web/components/rse.ts index 7842816..c487102 100644 --- a/src/web/components/rse.ts +++ b/src/web/components/rse.ts @@ -1,4 +1,15 @@ // RSE display component for ai.syui.rse.user collection +import { renderCard, type UserCard, type CardAdminEntry, type CardAdminData } from './card' + +export interface RseAdminItem { + id: number + name: string + text: { ja: string; en: string } +} + +export interface RseAdminData { + item: RseAdminItem[] +} export interface RseItem { id: number @@ -14,31 +25,135 @@ export interface RseCollection { updatedAt: string } +// Get current language +function getLang(): string { + return localStorage.getItem('preferredLang') || 'ja' +} + +// Get localized text +function getLocalizedText(obj: { ja: string; en: string } | undefined): string { + if (!obj) return '' + const lang = getLang() + return obj[lang as 'ja' | 'en'] || obj.ja || obj.en || '' +} + // Get rarity class from unique flag function getRarityClass(item: RseItem): string { if (item.unique) return 'unique' return '' } -// Render single item/character -function renderRseItem(item: RseItem, type: 'item' | 'character'): string { +// Get cards for a character (character 0 = cards 0-99, character 1 = cards 100-199, etc.) +function getCardsForCharacter( + characterId: number, + userCards: UserCard[], + adminData: CardAdminData | null +): { card: UserCard; adminEntry?: CardAdminEntry }[] { + const minId = characterId * 100 + const maxId = minId + 99 + + // Build admin lookup map + const adminMap = new Map() + if (adminData?.card) { + for (const entry of adminData.card) { + adminMap.set(entry.id, entry) + } + } + + // Filter and dedupe cards for this character + const cardGroups = new Map() + for (const card of userCards) { + if (card.id >= minId && card.id <= maxId) { + const existing = cardGroups.get(card.id) + if (!existing || card.cp > existing.cp || card.unique) { + cardGroups.set(card.id, card) + } + } + } + + // Sort by ID and add admin entries + return Array.from(cardGroups.values()) + .sort((a, b) => a.id - b.id) + .map(card => ({ + card, + adminEntry: adminMap.get(card.id) + })) +} + +// Render character section with its cards below +function renderCharacterSection( + item: RseItem, + userCards: UserCard[], + adminData: CardAdminData | null +): string { const rarityClass = getRarityClass(item) const effectsHtml = rarityClass ? `
` : '' + // Get cards for this character + const characterCards = getCardsForCharacter(item.id, userCards, adminData) + const cardsHtml = characterCards.map(({ card, adminEntry }) => + renderCard(card, '/card', undefined, adminEntry) + ).join('') + + return ` +
+
+
+
+
+ character ${item.id} +
+ ${effectsHtml} +
+
+ ${item.cp} +
+
+
+ ${characterCards.length > 0 ? ` +
${cardsHtml}
+ ` : ''} +
+ ` +} + +// Render single item +function renderRseItem(item: RseItem, rseAdminData: RseAdminData | null): string { + const rarityClass = getRarityClass(item) + const effectsHtml = rarityClass ? ` +
+
+ ` : '' + + // Get admin entry for this item + const adminEntry = rseAdminData?.item?.find(i => i.id === item.id) + const name = adminEntry?.name || '' + const text = adminEntry ? getLocalizedText(adminEntry.text) : '' + + const infoHtml = (name || text) ? ` +
+
+ ${name} +
+ ${text ? `
${text}
` : ''} +
+ ` : '' + return `
- ${type} ${item.id} + item ${item.id}
${effectsHtml}
${item.cp}
+ ${infoHtml}
` } @@ -46,7 +161,10 @@ function renderRseItem(item: RseItem, type: 'item' | 'character'): string { // Render RSE page export function renderRsePage( collection: RseCollection | null, - handle: string + handle: string, + userCards: UserCard[] = [], + adminData: CardAdminData | null = null, + rseAdminData: RseAdminData | null = null ): string { const jsonUrl = `/@${handle}/at/collection/ai.syui.rse.user/self` @@ -70,19 +188,14 @@ export function renderRsePage( const totalItems = items.length const uniqueChars = characters.filter(c => c.unique).length - // Sort by unique > id - const sortedChars = [...characters].sort((a, b) => { - if (a.unique !== b.unique) return a.unique ? -1 : 1 - return a.id - b.id - }) + // Sort by id + const sortedChars = [...characters].sort((a, b) => a.id - b.id) + const sortedItems = [...items].sort((a, b) => a.id - b.id) - const sortedItems = [...items].sort((a, b) => { - if (a.unique !== b.unique) return a.unique ? -1 : 1 - return a.id - b.id - }) - - const charsHtml = sortedChars.map(c => renderRseItem(c, 'character')).join('') - const itemsHtml = sortedItems.map(i => renderRseItem(i, 'item')).join('') + const charsHtml = sortedChars.map(c => + renderCharacterSection(c, userCards, adminData) + ).join('') + const itemsHtml = sortedItems.map(i => renderRseItem(i, rseAdminData)).join('') return `
@@ -107,7 +220,7 @@ export function renderRsePage(
${charsHtml ? `

Characters

-
${charsHtml}
+
${charsHtml}
` : ''} ${itemsHtml ? `

Items

diff --git a/src/web/lib/api.ts b/src/web/lib/api.ts index b6e3752..94efc3d 100644 --- a/src/web/lib/api.ts +++ b/src/web/lib/api.ts @@ -679,6 +679,105 @@ export interface LinkCollection { updatedAt?: string } +// Card admin data types +export interface CardAdminEntry { + id: number + character: number + name: { ja: string; en: string } + text: { ja: string; en: string } + cp: string + effect: string + key?: string | null +} + +export interface CardAdminData { + gacha: { pickup: number; rate: { rare: number; pickup: number } } + card: CardAdminEntry[] +} + +// Get card admin data (ai.syui.card.admin) +export async function getCardAdmin(did: string): Promise { + const collection = 'ai.syui.card.admin' + + // Try local first + try { + const res = await fetch(`/content/${did}/${collection}/self.json`) + if (res.ok && isJsonResponse(res)) { + const record = await res.json() + return record.value as CardAdminData + } + } catch { + // Try remote + } + + // Remote fallback + const pds = await getPds(did) + if (!pds) return null + + try { + const host = pds.replace('https://', '') + const url = `${xrpcUrl(host, comAtprotoRepo.getRecord)}?repo=${did}&collection=${collection}&rkey=self` + const res = await fetch(url) + if (res.ok) { + const record = await res.json() + return record.value as CardAdminData + } + } catch { + // Failed + } + return null +} + +// RSE admin data types +export interface RseAdminItem { + id: number + name: string + text: { ja: string; en: string } +} + +export interface RseAdminData { + item: RseAdminItem[] + ability: unknown[] + character: unknown[] + system: unknown[] + collection: unknown[] + createdAt: string + updatedAt: string +} + +// Get RSE admin data (ai.syui.rse.admin) +export async function getRseAdmin(did: string): Promise { + const collection = 'ai.syui.rse.admin' + + // Try local first + try { + const res = await fetch(`/content/${did}/${collection}/self.json`) + if (res.ok && isJsonResponse(res)) { + const record = await res.json() + return record.value as RseAdminData + } + } catch { + // Try remote + } + + // Remote fallback + const pds = await getPds(did) + if (!pds) return null + + try { + const host = pds.replace('https://', '') + const url = `${xrpcUrl(host, comAtprotoRepo.getRecord)}?repo=${did}&collection=${collection}&rkey=self` + const res = await fetch(url) + if (res.ok) { + const record = await res.json() + return record.value as RseAdminData + } + } catch { + // Failed + } + return null +} + // Get user's links (ai.syui.at.link) export async function getLinks(did: string): Promise { const collection = 'ai.syui.at.link' diff --git a/src/web/main.ts b/src/web/main.ts index 459034f..4170f9b 100644 --- a/src/web/main.ts +++ b/src/web/main.ts @@ -1,7 +1,7 @@ import './styles/main.css' import './styles/card.css' import './styles/card-migrate.css' -import { getConfig, resolveHandle, getProfile, getPosts, getPost, describeRepo, listRecords, getRecord, getPds, getNetworks, getChatMessages, getCards, getRse, getLinks } from './lib/api' +import { getConfig, resolveHandle, getProfile, getPosts, getPost, describeRepo, listRecords, getRecord, getPds, getNetworks, getChatMessages, getCards, getCardAdmin, getRse, getRseAdmin, getLinks } from './lib/api' import { parseRoute, onRouteChange, navigate, type Route } from './lib/router' import { login, logout, handleCallback, restoreSession, isLoggedIn, getLoggedInHandle, getLoggedInDid, deleteRecord, updatePost, updateChat, updateLinks } from './lib/auth' import { validateRecord } from './lib/lexicon' @@ -253,9 +253,17 @@ async function render(route: Route): Promise { html += `` } else if (route.type === 'rse') { - // RSE page - const rseData = await getRse(did) - html += `
${renderRsePage(rseData, handle)}
` + // RSE page with character cards + const cardCollection = config.cardCollection || 'ai.syui.card.user' + const adminDid = config.bot?.did || config.did || did + const [rseData, cards, adminData, rseAdminData] = await Promise.all([ + getRse(did), + getCards(did, cardCollection), + getCardAdmin(adminDid), + getRseAdmin(adminDid) + ]) + const userCards = cards?.card || [] + html += `
${renderRsePage(rseData, handle, userCards, adminData, rseAdminData)}
` html += `` } else if (route.type === 'link') { diff --git a/src/web/styles/card.css b/src/web/styles/card.css index 0cd61ed..31ea6c0 100644 --- a/src/web/styles/card.css +++ b/src/web/styles/card.css @@ -522,3 +522,133 @@ color: var(--text-secondary, #aaa); } } + +/* RSE section title */ +.rse-section-title { + margin: 24px 0 12px; + font-size: 1.1em; + color: var(--text-primary, #333); +} + +/* RSE character sections */ +.rse-characters { + display: flex; + flex-direction: column; + gap: 32px; + width: 100%; +} + +.rse-character-section { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + width: 100%; +} + +.rse-character-main { + display: flex; + justify-content: center; +} + +.rse-character-main .card-wrapper { + width: 250px; + max-width: 250px; +} + +/* RSE card grid (cards below character) */ +.rse-card-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 20px; + width: 100%; + padding: 20px; + background: rgba(128, 128, 128, 0.08); + border-radius: 12px; +} + +.rse-card-grid .card-wrapper { + max-width: 180px; +} + +.rse-card-grid .card-info-name { + font-size: 11px; +} + +.rse-card-grid .card-info-text { + font-size: 12px; +} + +.rse-card-grid .card-key-btn { + font-size: 9px; + padding: 2px 8px; +} + +/* Card info (below card) */ +.card-info { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + margin-top: 8px; + text-align: center; +} + +.card-info-header { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + width: 100%; + position: relative; +} + +.card-info-name { + font-size: 13px; + font-weight: 600; + color: var(--text-primary, #333); +} + +.card-info-header .card-key-btn { + position: absolute; + right: 0; +} + +.card-info-text { + font-size: 11px; + color: var(--text-secondary, #666); + line-height: 1.5; +} + +.card-key-btn { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + color: white; + padding: 3px 10px; + border-radius: 10px; + font-size: 10px; + font-weight: 600; + cursor: default; + box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* Dark mode for RSE */ +@media (prefers-color-scheme: dark) { + .rse-section-title { + color: var(--text-primary, #eee); + } + + .rse-card-grid { + background: rgba(255, 255, 255, 0.05); + } + + .card-info-name { + color: var(--text-primary, #eee); + } + + .card-info-text { + color: var(--text-secondary, #aaa); + } +}