diff --git a/platform_web/theme/base.css b/platform_web/theme/base.css index 37e156b..4894bcd 100644 --- a/platform_web/theme/base.css +++ b/platform_web/theme/base.css @@ -452,12 +452,19 @@ to{transform:translate(-50%,-50%) rotate(calc(var(--construct-drift) + 360deg))} .scum-catalog-picker-body{display:grid;gap:8px;padding-top:8px} .scum-catalog-picker-body>input{min-height:34px;border:1px solid var(--line-strong);border-radius:6px;padding:0 10px;background:var(--surface-solid);color:var(--ink);font:inherit} .scum-catalog-kinds{display:flex;align-items:center;gap:6px;flex-wrap:wrap} -.scum-catalog-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:6px;max-height:320px;overflow:auto;padding-right:2px} -.scum-catalog-chip{display:grid;grid-template-columns:24px minmax(0,1fr);align-items:center;gap:6px;padding:5px 7px;border:1px solid var(--line);border-radius:8px;background:var(--control-surface);color:var(--ink-soft);text-align:left;cursor:pointer} -.scum-catalog-chip:hover{border-color:var(--accent);color:var(--ink)} -.scum-catalog-meta{display:grid;min-width:0} -.scum-catalog-meta strong{font-size:12px;font-weight:700;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -.scum-catalog-meta .provider-id{font-size:10px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.scum-catalog-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:8px;max-height:min(56vh,560px);overflow:auto;padding:2px} +.scum-catalog-card{display:grid;grid-template-columns:62px minmax(0,1fr);align-items:center;gap:10px;min-height:84px;padding:8px;border:1px solid var(--line);border-radius:8px;background:var(--control-surface);color:var(--ink-soft);text-align:left;cursor:pointer} +.scum-catalog-card:focus-visible,.scum-catalog-card:hover{border-color:var(--accent);outline:0;color:var(--ink);box-shadow:0 0 0 2px var(--accent-soft),0 10px 22px var(--glass-shadow)} +.scum-catalog-thumb{display:grid;place-items:center;width:62px;height:62px;border:1px solid color-mix(in srgb,var(--line-strong) 72%,transparent);border-radius:8px;background:color-mix(in srgb,var(--surface-solid) 72%,transparent)} +.scum-catalog-thumb img{width:54px;height:54px;object-fit:contain} +.scum-catalog-thumb-empty{color:var(--ink-faint);font-size:20px} +.scum-catalog-meta{display:grid;min-width:0;gap:2px} +.scum-catalog-meta strong{display:-webkit-box;overflow:hidden;color:var(--ink);font-size:13px;font-weight:800;line-height:1.25;-webkit-box-orient:vertical;-webkit-line-clamp:2} +.scum-catalog-category{color:var(--accent);font-size:10px;font-weight:800;line-height:1.2} +.scum-catalog-source,.scum-catalog-code{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.scum-catalog-source{color:var(--ink-soft);font-size:11px;line-height:1.25} +.scum-catalog-code{color:var(--ink-faint);font-family:var(--font-mono);font-size:10px;line-height:1.25} +@media (max-width:760px){.scum-catalog-grid{grid-template-columns:repeat(auto-fill,minmax(170px,1fr));max-height:48vh}.scum-catalog-card{grid-template-columns:52px minmax(0,1fr);min-height:72px;gap:8px}.scum-catalog-thumb{width:52px;height:52px}.scum-catalog-thumb img{width:44px;height:44px}} .icon-command.is-active{border-color:var(--accent);color:var(--ink);box-shadow:0 0 0 1px color-mix(in srgb,var(--accent) 46%,transparent)} .scum-workbench{padding:12px;overflow:visible}.scum-workbench>.console-record-list{gap:8px}.scum-filter-bar{margin:0;padding:0 0 10px;border-bottom:1px solid color-mix(in srgb,var(--line) 68%,transparent)}.scum-filter-bar .console-field{flex:0 1 210px}.scum-filter-bar .console-field:first-child{flex:1 1 320px}.scum-filter-bar .page-status{margin-left:auto}.scum-layer-toggle{display:inline-flex;align-items:center;gap:5px;flex:0 0 auto;min-height:28px;padding:0 8px;border:1px solid var(--line);border-radius:999px;background:var(--control-surface);color:var(--ink-soft);font-size:11px;font-weight:760}.scum-filter-bar .scum-layer-toggle{min-height:26px;padding:0 8px;gap:5px;font-size:11px;white-space:nowrap}.scum-filter-bar .scum-layer-toggle input{flex:0 0 auto;width:14px;height:14px;min-width:0;min-height:0;margin:0;accent-color:var(--accent)}.scum-layer-group{display:flex;align-items:center;gap:6px;flex-wrap:wrap;flex:0 0 auto}.scum-map-summary{margin-left:auto;white-space:nowrap}.scum-table-wrap{border-color:color-mix(in srgb,var(--line) 72%,transparent);background:linear-gradient(180deg,color-mix(in srgb,var(--surface-solid) 84%,transparent),color-mix(in srgb,var(--surface) 66%,transparent));-webkit-backdrop-filter:blur(8px) saturate(.92);backdrop-filter:blur(8px) saturate(.92);box-shadow:inset 0 1px 0 color-mix(in srgb,var(--crystal-rim) 46%,transparent)}.scum-table-wrap::before,.scum-table-wrap::after{content:none}.scum-user-table{min-width:1080px;table-layout:fixed}.scum-user-table caption{caption-side:top;padding:0 0 8px;text-align:left;color:var(--ink-faint);font-size:12px;font-weight:800}.scum-user-table th,.scum-user-table td{height:38px;padding:6px 10px;border-bottom-color:color-mix(in srgb,var(--line) 64%,transparent);white-space:nowrap;font-size:13px;line-height:1.2}.scum-user-table th{background:color-mix(in srgb,var(--accent-soft) 52%,transparent);color:color-mix(in srgb,var(--ink) 70%,var(--accent-deep))}.scum-user-table tbody tr:hover{background:color-mix(in srgb,var(--accent-soft) 24%,transparent)}.scum-user-table th:nth-child(1),.scum-user-table td:nth-child(1){width:150px}.scum-user-table th:nth-child(2),.scum-user-table td:nth-child(2){width:170px}.scum-user-table th:nth-child(3),.scum-user-table td:nth-child(3){width:76px}.scum-user-table th:nth-child(4),.scum-user-table td:nth-child(4){width:116px}.scum-user-table th:nth-child(5),.scum-user-table td:nth-child(5){width:108px}.scum-user-table th:nth-child(6),.scum-user-table td:nth-child(6){width:108px}.scum-user-table th:nth-child(7),.scum-user-table td:nth-child(7){width:156px}.scum-user-table th:last-child,.scum-user-table td:last-child{width:76px;text-align:right}.scum-user-table td>*{max-width:100%;overflow:hidden;text-overflow:ellipsis}.scum-user-table td.provider-actions-cell{padding-right:8px;overflow:visible}.scum-user-table td.provider-actions-cell>*{overflow:visible}.scum-mono-cell{font-family:var(--font-mono);font-size:12px}.scum-user-table .icon-command{min-height:28px;padding:0 9px}.inline-action-control{position:relative;display:inline-block}.inline-action-control>summary{list-style:none}.inline-action-control>summary::-webkit-details-marker{display:none}.inline-action-control[open]>summary{border-color:var(--accent);color:var(--ink)}.inline-action-control .inline-action-menu{right:0} .gift-definitions-surface{display:grid;gap:12px;min-width:0}.gift-toolbar{margin:0;padding:0 0 10px;border-bottom:1px solid color-mix(in srgb,var(--line) 68%,transparent)}.gift-toolbar .console-field{flex:0 1 190px}.gift-toolbar .console-field:first-child{flex:1 1 320px}.gift-toolbar .page-status{margin-left:auto}.gift-create-command{width:auto;min-width:112px}.gift-table-wrap{min-width:0}.scum-gift-table{min-width:940px;table-layout:fixed}.scum-gift-table th,.scum-gift-table td{height:54px;padding:9px 10px;border-bottom-color:color-mix(in srgb,var(--line) 64%,transparent);vertical-align:middle;font-size:12.5px}.scum-gift-table th{background:color-mix(in srgb,var(--accent-soft) 52%,transparent);color:color-mix(in srgb,var(--ink) 70%,var(--accent-deep));white-space:nowrap}.scum-gift-table tbody tr:hover{background:color-mix(in srgb,var(--accent-soft) 24%,transparent)}.scum-gift-table th:nth-child(1),.scum-gift-table td:nth-child(1){width:175px}.scum-gift-table th:nth-child(2),.scum-gift-table td:nth-child(2){width:330px}.scum-gift-table th:nth-child(3),.scum-gift-table td:nth-child(3){width:220px}.scum-gift-table th:nth-child(4),.scum-gift-table td:nth-child(4){width:150px}.scum-gift-table th:last-child,.scum-gift-table td:last-child{width:150px;text-align:right}.scum-gift-table td>*{max-width:100%;overflow-wrap:anywhere}.scum-gift-table td.provider-actions-cell{padding-right:8px;overflow:visible}.scum-gift-table td.provider-actions-cell>*{overflow:visible}.scum-gift-code{font-family:var(--font-mono);font-size:11px}.scum-gift-content-cell{display:flex;align-items:center;gap:8px;min-width:0}.scum-gift-content-cell>span:last-child{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.gift-rule-primary{display:block;color:var(--ink);font-weight:780;line-height:1.35}.gift-status-cell{display:grid;gap:5px;min-width:0}.gift-status-cell .provider-id{white-space:normal}.scum-gift-table-actions{display:inline-flex;align-items:center;justify-content:flex-end;gap:6px}.scum-gift-table-actions .icon-command{min-height:30px;padding:0 9px;white-space:nowrap}.gift-form-wide{grid-column:1/-1}.gift-form-section{display:grid;gap:9px;padding:11px;border:1px solid var(--line);border-radius:8px;background:color-mix(in srgb,var(--surface-solid) 64%,transparent);box-shadow:inset 0 1px 0 var(--crystal-rim)}.gift-form-section-title{display:flex;align-items:baseline;justify-content:space-between;gap:10px;flex-wrap:wrap}.gift-form-section-title strong{color:var(--ink);font-size:13px}.gift-form-section-title span{color:var(--ink-faint);font-size:11px}.gift-dialog-heading{display:grid;gap:3px;min-width:0}.gift-dialog-heading h2{margin:0;color:var(--ink);font-size:18px}.gift-dialog-actions{grid-column:1/-1}.gift-dialog-actions .confirm-primary{min-width:128px}.gift-catalog-reference{margin-top:2px} diff --git a/plugins/examples/scum-server-plugin/features/page.ts b/plugins/examples/scum-server-plugin/features/page.ts index 71013d6..b1f9e36 100644 --- a/plugins/examples/scum-server-plugin/features/page.ts +++ b/plugins/examples/scum-server-plugin/features/page.ts @@ -1448,6 +1448,16 @@ function catalogIconStrip(e: ReactLike["createElement"], icons: string[]) { return icons.length ? e("span", { className: "scum-catalog-icons" }, icons.map((url, index) => e("img", { key: `${url}-${index}`, className: "scum-catalog-icon", src: url, alt: "" }))) : null; } +function catalogCard(e: ReactLike["createElement"], view: ViewState, target: CatalogPickTarget, entry: ScumCatalogEntry) { + const icon = scumCatalogIconUrl(entry.icon); + const displayName = scumCatalogDisplayName(entry); + const sourceName = entry.name || entry.code; + return e("button", { key: entry.code, type: "button", className: "scum-catalog-card", title: `${displayName} · ${sourceName} · ${entry.code} · ${entry.command}`, onClick: () => target === "gift-items" ? view.setGiftItems((items) => appendCatalogItem(items, entry.code)) : view.setEventItems((items) => appendCatalogItem(items, entry.code)) }, + e("span", { className: "scum-catalog-thumb" }, icon ? e("img", { src: icon, alt: "" }) : e("span", { className: "scum-catalog-thumb-empty", "aria-hidden": "true" }, "—")), + e("span", { className: "scum-catalog-meta" }, e("strong", null, displayName), e("span", { className: "scum-catalog-category" }, scumCatalogCategoryLabel(entry)), e("span", { className: "scum-catalog-source", title: sourceName }, sourceName), e("code", { className: "scum-catalog-code", title: entry.code }, entry.code)) + ); +} + function giftCatalogIcons(gift: RecordMap): string[] { const items = field(gift, "items"); return Array.isArray(items) ? items.flatMap((item) => isRecord(item) ? catalogIconsForValue(textField(item, "catalogCode", "catalogItemKey", "key", "className")) : []).slice(0, 12) : []; @@ -1458,11 +1468,11 @@ function catalogPicker(e: ReactLike["createElement"], view: ViewState, target: C const activeFilter = filters.some(([key]) => key === view.catalogKind) ? view.catalogKind : "all"; const matches = catalogMatches(view.catalogSearch, activeFilter, target); return e("details", { className: "console-module scum-editor scum-catalog-picker", open: view.catalogOpen, onToggle: (event: InputEvent) => view.setCatalogOpen(detailOpen(event)) }, - e("summary", null, e("strong", null, target === "gift-items" ? "从 SCUM 目录选择礼包内容" : "从 SCUM 目录选择活动物品 / NPC"), e("span", { className: "page-status" }, "图标 / 名称 / 代码")), + e("summary", null, e("strong", null, target === "gift-items" ? "从 SCUM 目录选择礼包内容" : "从 SCUM 目录选择活动物品 / NPC"), e("span", { className: "page-status" }, `${matches.length} 个条目`)), view.catalogOpen ? e("div", { className: "scum-catalog-picker-body" }, e("input", { type: "search", value: view.catalogSearch, "aria-label": target === "gift-items" ? "搜索礼包目录" : "搜索活动物品目录", placeholder: "中文名 / 英文名 / 代码,例如 莱卡", onChange: (event: InputEvent) => view.setCatalogSearch(inputValue(event)) }), e("div", { className: "scum-catalog-kinds", role: "group", "aria-label": target === "gift-items" ? "礼包目录分类" : "活动目录分类" }, filters.map(([key, label]) => e("button", { key, type: "button", className: activeFilter === key ? "icon-command is-active" : "icon-command", "aria-pressed": activeFilter === key, onClick: () => view.setCatalogKind(key) }, label))), - e("div", { className: "scum-catalog-grid" }, matches.length ? matches.map((entry) => { const icon = scumCatalogIconUrl(entry.icon); const displayName = scumCatalogDisplayName(entry); return e("button", { key: entry.code, type: "button", className: "scum-catalog-chip", title: `${displayName} · ${scumCatalogCategoryLabel(entry)} · ${entry.command}`, onClick: () => target === "gift-items" ? view.setGiftItems((items) => appendCatalogItem(items, entry.code)) : view.setEventItems((items) => appendCatalogItem(items, entry.code)) }, icon ? e("img", { className: "scum-catalog-icon", src: icon, alt: "" }) : e("span", { className: "scum-catalog-icon scum-catalog-icon-empty" }), e("span", { className: "scum-catalog-meta" }, e("strong", null, displayName), e("span", { className: "provider-id" }, `${scumCatalogCategoryLabel(entry)} · ${entry.code}`))); }) : e("p", { className: "page-status" }, "没有匹配的目录条目。")) + e("div", { className: "scum-catalog-grid" }, matches.length ? matches.map((entry) => catalogCard(e, view, target, entry)) : e("p", { className: "page-status" }, "没有匹配的目录条目。")) ) : null ); } @@ -1486,7 +1496,7 @@ export function scumGiftCatalogCategory(entry: ScumCatalogEntry): string { export function scumGiftCatalogFilterKey(entry: ScumCatalogEntry): string { return entry.kind === "item" ? scumGiftCatalogCategory(entry) : entry.kind; } -export function scumCatalogCategoryLabel(entry: ScumCatalogEntry): string { if (entry.kind === "vehicle") return "载具"; if (entry.kind === "animal") return "动物"; if (entry.kind === "zombie") return "丧尸"; if (entry.kind === "armed-npc") return "NPC"; return ({ hunting: "狩猎", food: "食物", metabolism: "代谢", medical: "医疗", weapons: "武器", crafting: "制作", gear: "装备", fishing: "钓鱼", building: "建筑", tools: "工具", farming: "农业", misc: "其他" } as Record)[scumGiftCatalogCategory(entry)] ?? "其他"; } +export function scumCatalogCategoryLabel(entry: ScumCatalogEntry): string { if (entry.kind === "vehicle") return "载具"; if (entry.kind === "animal") return "动物"; if (entry.kind === "zombie") return "丧尸"; if (entry.kind === "armed-npc") return "非玩家角色"; return ({ hunting: "狩猎", food: "食物", metabolism: "代谢", medical: "医疗", weapons: "武器", crafting: "制作", gear: "装备", fishing: "钓鱼", building: "建筑", tools: "工具", farming: "农业", misc: "其他" } as Record)[scumGiftCatalogCategory(entry)] ?? "其他"; } function labeledField(e: ReactLike["createElement"], label: string, control: unknown) { return e("label", { className: "console-field" }, e("span", null, label), control); } function detailOpen(event: InputEvent): boolean { return Boolean(event.target?.open); } function giftTabButton(e: ReactLike["createElement"], view: ViewState, tab: GiftTab, label: string) { return e("button", { type: "button", role: "tab", "aria-selected": view.giftTab === tab, className: view.giftTab === tab ? "primary-command" : "icon-command", onClick: () => view.setGiftTab(tab) }, label); } diff --git a/plugins/examples/scum-server-plugin/features/scum-catalog-zh.ts b/plugins/examples/scum-server-plugin/features/scum-catalog-zh.ts index 779b468..c639b36 100644 --- a/plugins/examples/scum-server-plugin/features/scum-catalog-zh.ts +++ b/plugins/examples/scum-server-plugin/features/scum-catalog-zh.ts @@ -13,11 +13,11 @@ const directNames: Record = { BP_Seagull: "海鸥", BP_Wolf3: "狼", WildDuck: "鹿(目录别名)", - BP_Drifter_Lvl_1: "武装 NPC 1 级", - BP_Drifter_Lvl_2: "武装 NPC 2 级", - BP_Drifter_Lvl_3: "武装 NPC 3 级", - BP_Drifter_Lvl_4: "武装 NPC 4 级", - BP_Drifter_Lvl_5: "武装 NPC 5 级", + BP_Drifter_Lvl_1: "武装非玩家角色 1 级", + BP_Drifter_Lvl_2: "武装非玩家角色 2 级", + BP_Drifter_Lvl_3: "武装非玩家角色 3 级", + BP_Drifter_Lvl_4: "武装非玩家角色 4 级", + BP_Drifter_Lvl_5: "武装非玩家角色 5 级", BP_Guard_Lvl_1: "武装守卫 1 级", BP_Guard_Lvl_2: "武装守卫 2 级", BP_Guard_Lvl_3: "武装守卫 3 级", @@ -48,21 +48,23 @@ const directNames: Record = { FishingRod: "鱼竿", FishingHook: "鱼钩", Bait_trap_for_small_animals: "小型动物诱饵陷阱", + Improvised_Backpack_Big: "大背包", + Military_Helmet_01_01: "军用头盔 深绿", Watermelon: "西瓜" }; const wordNames: Record = { absinthe: "苦艾酒", animal: "动物", animals: "动物", apple: "苹果", armor: "护甲", arrow: "箭", arrows: "箭矢", aspirin: "阿司匹林", advanced: "高级", automatic: "自动", - backpack: "背包", bait: "诱饵", ball: "球", banana: "香蕉", bandage: "绷带", baseball: "棒球", battery: "电池", bear: "熊", beer: "啤酒", beet: "甜菜", berries: "浆果", blackberry: "黑莓", blueberries: "蓝莓", bolt: "弩箭", bottle: "瓶", bow: "弓", bread: "面包", brick: "砖块", broccoli: "西兰花", buck: "雄鹿", bullet: "子弹", bulletproof: "防弹", burger: "汉堡", burn: "烧伤", cake: "蛋糕", canned: "罐装", carbon: "碳纤维", carrot: "胡萝卜", cartridge: "弹药", cash: "现金", cheese: "奶酪", chicken: "鸡", civilian: "平民", cleaver: "菜刀", cola: "可乐", coffee: "咖啡", compound: "复合", construction: "施工", cooked: "熟制", cooking: "烹饪", crow: "乌鸦", crossbow: "弩", crucian: "鲫鱼", cup: "杯", dark: "深色", deer: "鹿", detonator: "雷管", dildo: "假阳具", donkey: "驴", double: "双头", drink: "饮料", drinks: "饮料", eagle: "鹰", edible: "可食用", energy: "能量", explosive: "爆炸", female: "女性", fish: "鱼", fishing: "钓鱼", floater: "浮漂", freshwater: "淡水", fruit: "水果", gas: "燃气", gasoline: "汽油", goat: "山羊", green: "绿色", grenade: "手雷", grey: "灰色", guts: "内脏", hammer: "锤子", hands: "手部", hat: "帽子", head: "头部", helmet: "头盔", hospital: "医院", hook: "鱼钩", horse: "马", human: "人类", improvised: "简易", injection: "注射剂", iron: "铁制", juice: "果汁", katana: "武士刀", knife: "刀", large: "大型", launcher: "发射器", leather: "皮革", level: "等级", lime: "青柠", lock: "锁", lockpick: "撬锁工具", magazine: "弹匣", male: "男性", medical: "医疗", medium: "中型", metal: "金属", milk: "牛奶", mushroom: "蘑菇", muzzle: "枪口", necklace: "项链", nylon: "尼龙", orange: "橙色", pack: "包", packaged: "包装", painkiller: "止痛药", pepper: "胡椒", pill: "药片", pills: "药片", pizza: "披萨", police: "警用", potato: "土豆", red: "红色", repair: "维修", rifle: "步枪", rope: "绳子", rabbit: "兔子", rice: "米饭", revolver: "左轮手枪", salad: "沙拉", sardine: "沙丁鱼", shotgun: "霰弹枪", shovel: "铲子", small: "小型", soda: "汽水", soup: "汤", stone: "石制", suppressor: "消音器", sword: "剑", syringe: "注射器", tea: "茶", tuna: "金枪鱼", vegetables: "蔬菜", vehicle: "载具", vest: "背心", vitamins: "维生素", vodka: "伏特加", water: "水", watermelon: "西瓜", whiskey: "威士忌", white: "白色", wolf: "狼", wooden: "木制", yellow: "黄色", zombie: "丧尸" + backpack: "背包", bait: "诱饵", ball: "球", banana: "香蕉", bandage: "绷带", baseball: "棒球", battery: "电池", bear: "熊", beer: "啤酒", beet: "甜菜", berries: "浆果", blackberry: "黑莓", blueberries: "蓝莓", bolt: "弩箭", bottle: "瓶", bow: "弓", bread: "面包", brick: "砖块", broccoli: "西兰花", buck: "雄鹿", bullet: "子弹", bulletproof: "防弹", burger: "汉堡", burn: "烧伤", cake: "蛋糕", canned: "罐装", carbon: "碳纤维", carrot: "胡萝卜", cartridge: "弹药", cash: "现金", cheese: "奶酪", chicken: "鸡", civilian: "平民", cleaver: "菜刀", cola: "可乐", coffee: "咖啡", compound: "复合", construction: "施工", cooked: "熟制", cooking: "烹饪", crow: "乌鸦", crossbow: "弩", crucian: "鲫鱼", cup: "杯", dark: "深色", deer: "鹿", detonator: "雷管", dildo: "假阳具", donkey: "驴", double: "双头", drink: "饮料", drinks: "饮料", eagle: "鹰", edible: "可食用", energy: "能量", explosive: "爆炸", female: "女性", fish: "鱼", fishing: "钓鱼", floater: "浮漂", freshwater: "淡水", fruit: "水果", gas: "燃气", gasoline: "汽油", goat: "山羊", goggles: "护目镜", green: "绿色", grenade: "手雷", grey: "灰色", guts: "内脏", hammer: "锤子", hands: "手部", hat: "帽子", head: "头部", helmet: "头盔", hospital: "医院", hook: "鱼钩", horse: "马", human: "人类", improvised: "简易", injection: "注射剂", iron: "铁制", juice: "果汁", katana: "武士刀", knife: "刀", large: "大型", launcher: "发射器", leather: "皮革", level: "等级", lime: "青柠", lock: "锁", lockpick: "撬锁工具", magazine: "弹匣", male: "男性", medical: "医疗", medium: "中型", metal: "金属", milk: "牛奶", mushroom: "蘑菇", muzzle: "枪口", necklace: "项链", nylon: "尼龙", orange: "橙色", pack: "包", packaged: "包装", painkiller: "止痛药", pepper: "胡椒", pill: "药片", pills: "药片", pizza: "披萨", police: "警用", potato: "土豆", red: "红色", repair: "维修", rifle: "步枪", rope: "绳子", rabbit: "兔子", rice: "米饭", revolver: "左轮手枪", salad: "沙拉", sardine: "沙丁鱼", shotgun: "霰弹枪", shovel: "铲子", small: "小型", soda: "汽水", soup: "汤", stone: "石制", suppressor: "消音器", sword: "剑", syringe: "注射器", tea: "茶", tuna: "金枪鱼", vegetables: "蔬菜", vehicle: "载具", vest: "背心", vitamins: "维生素", vodka: "伏特加", water: "水", watermelon: "西瓜", whiskey: "威士忌", white: "白色", wolf: "狼", wooden: "木制", yellow: "黄色", zombie: "丧尸" }; const additionalWordNames: Record = { - activated: "活性炭", alcohol: "酒精", alcoholic: "酒精类", anti: "抗", antibiotic: "抗生素", antimalaria: "抗疟疾", antiparasitic: "抗寄生虫", antipsychotic: "抗精神病", ammunition: "弹药", attachment: "配件", back: "后部", big: "大", body: "身体", boots: "靴子", box: "盒", bucket: "桶", camp: "营地", charcoal: "木炭", clothes: "服装", component: "组件", crafted: "制作的", crafting: "制作", dirty: "脏污", door: "车门", electrodes: "电极", farming: "农业", feet: "脚部", front: "前部", gear: "装备", gloves: "手套", hiking: "徒步", hood: "引擎盖", inventory: "背包栏", item: "物品", legs: "腿部", light: "轻型", military: "军用", normal: "普通", other: "其他", parts: "零件", rear: "后部", rubber: "橡胶", security: "安全", seat: "座椅", side: "侧面", single: "单发", skin: "皮肤", spare: "备用", standing: "立式", steel: "钢制", tactical: "战术", top: "顶部", trap: "陷阱", underwear: "内衣", weapon: "武器", wheel: "车轮" + activated: "活性炭", alcohol: "酒精", alcoholic: "酒精类", anti: "抗", antibiotic: "抗生素", antimalaria: "抗疟疾", antiparasitic: "抗寄生虫", antipsychotic: "抗精神病", ammunition: "弹药", assault: "突击", attachment: "配件", back: "后部", big: "大", black: "黑色", body: "身体", boots: "靴子", box: "盒", bucket: "桶", camp: "营地", cannabis: "大麻", charcoal: "木炭", clothes: "服装", component: "组件", corn: "玉米", crafted: "制作的", crafting: "制作", cucumber: "黄瓜", dirty: "脏污", desert: "沙漠", door: "车门", electrodes: "电极", expansion: "扩展", farming: "农业", feet: "脚部", fig: "无花果", front: "前部", garlic: "大蒜", gear: "装备", gloves: "手套", grapefruit: "葡萄柚", hiking: "徒步", hood: "引擎盖", intelligence: "智力", inventory: "背包栏", item: "物品", kit: "套件", legs: "腿部", lemon: "柠檬", light: "轻型", lettuce: "生菜", marpat: "MAR-PAT", memory: "存储", military: "军用", module: "模块", normal: "普通", other: "其他", parts: "零件", quiver: "箭袋", rear: "后部", rubber: "橡胶", scorpion: "蝎子", security: "安全", seat: "座椅", side: "侧面", single: "单发", skin: "皮肤", snow: "雪地", spare: "备用", standing: "立式", steel: "钢制", tactical: "战术", tip: "尖端", top: "顶部", trap: "陷阱", urban: "城市", underwear: "内衣", weapon: "武器", wheel: "车轮", woodland: "林地" }; const phraseNames: Array = [ [/^Armed Guard NPC Level (\d+)$/i, "武装守卫 $1 级"], - [/^Armed NPC Level (\d+)$/i, "武装 NPC $1 级"], + [/^Armed NPC Level (\d+)$/i, "武装非玩家角色 $1 级"], [/^Zombie (.+) Female$/i, "女性丧尸 $1"], [/^Zombie (.+) Male$/i, "男性丧尸 $1"], [/^Zombie (.+)$/i, "丧尸 $1"], @@ -80,15 +82,30 @@ const phraseNames: Array = [ [/^(.+) Level (\d+)$/i, "$1 $2 级"] ]; -const catalogKindNames: Record = { item: "物品", vehicle: "载具", animal: "动物", zombie: "丧尸", "armed-npc": "NPC", razor: "其他" }; +const catalogKindNames: Record = { item: "物品", vehicle: "载具", animal: "动物", zombie: "丧尸", "armed-npc": "非玩家角色", razor: "其他" }; +const fallbackNameGroups: Array = [ + ["/backpacks/", "背包"], ["/head armor/", "头部装备"], ["/weapons/", "武器"], ["/medicals/", "医疗物品"], ["/food/", "食物"], ["/clothes/", "服装"], ["/vehicles/", "载具部件"], ["/fishing/", "钓鱼用品"], ["/farming/", "农业用品"], ["/crafting/", "制作材料"] +]; export function scumCatalogNameZh(entry: ScumCatalogEntry): string { const direct = directNames[entry.code]; if (direct) return direct; + const code = entry.code.replace(/[_-]+/g, " "); + if (/^Military_Backpack_/i.test(entry.code)) { + const variant = translateWords(usableName(entry).replace(/^(?:Military\s+)?(?:(?:Camo\s+)?Hiking|Assault)?\s*Backpack\s*/i, "")).trim(); + return compactLabel("军用背包" + (variant ? " " + variant : "")); + } + if (/^Military_Helmet_/i.test(entry.code)) { + const variant = translateWords(usableName(entry).replace(/^Military Helmet\s*/i, "")).trim(); + return compactLabel("军用头盔" + (variant ? " " + variant : "")); + } const source = usableName(entry); for (const [pattern, replacement] of phraseNames) if (pattern.test(source)) return translateWords(source.replace(pattern, replacement)); - const translated = translateWords(source); - return translated === source ? `${catalogKindNames[entry.kind] ?? "物品"} · ${source}` : translated; + const translated = compactLabel(translateWords(source)); + if (!/[A-Za-z]/.test(translated)) return translated; + const translatedCode = compactLabel(translateWords(code)); + if (!/[A-Za-z]/.test(translatedCode)) return translatedCode; + return catalogFallbackName(entry); } export function scumCatalogDisplayName(entry: ScumCatalogEntry): string { return scumCatalogNameZh(entry); } @@ -109,6 +126,17 @@ function usableName(entry: ScumCatalogEntry): string { } function translateWords(value: string): string { - const spaced = value.replace(/([a-z])([A-Z])/g, "$1 $2"); + const normalized = value.replace(/\bDark Green\b/gi, "深绿").replace(/\bLight Green\b/gi, "浅绿"); + const spaced = normalized.replace(/([a-z])([A-Z])/g, "$1 $2"); return spaced.replace(/[A-Za-z][A-Za-z'-]*/g, (word) => additionalWordNames[word.toLowerCase()] ?? wordNames[word.toLowerCase()] ?? word); } + +function catalogFallbackName(entry: ScumCatalogEntry): string { + const icon = entry.icon.toLowerCase(); + const group = fallbackNameGroups.find(([needle]) => icon.includes(needle)); + const prefix = group?.[1] ?? catalogKindNames[entry.kind] ?? "物品"; + const serial = entry.code.match(/\d+/g)?.join("-"); + return serial ? prefix + " " + serial : prefix + "条目"; +} + +function compactLabel(value: string): string { return value.replace(/\s+/g, " ").trim(); } diff --git a/plugins/tests/scum-feature-module.test.ts b/plugins/tests/scum-feature-module.test.ts index 859f6f8..23bbd0c 100644 --- a/plugins/tests/scum-feature-module.test.ts +++ b/plugins/tests/scum-feature-module.test.ts @@ -347,12 +347,16 @@ describe("SCUM plugin feature module", () => { const aloe = scumCatalogEntries.find((item) => item.code === "Aloe_Vera")!; const vehicle = scumCatalogEntries.find((item) => item.code === "BPC_Laika")!; const bigBackpack = scumCatalogEntries.find((item) => item.code === "Improvised_Backpack_Big")!; + const militaryBackpack = scumCatalogEntries.find((item) => item.code === "Military_Backpack_01_02")!; const militaryHelmet = scumCatalogEntries.find((item) => item.code === "Military_Helmet_01_01")!; expect(scumCatalogDisplayName(aloe)).toBe("芦荟"); expect(scumCatalogDisplayName(vehicle)).toBe("莱卡"); + expect(scumCatalogDisplayName(militaryBackpack)).toBe("军用背包 黑色"); expect(scumCatalogSearchText(aloe)).toContain("芦荟"); expect(scumCatalogSearchText(bigBackpack)).toContain("大背包"); expect(scumCatalogSearchText(militaryHelmet)).toContain("军用头盔"); + expect(pageSource).toContain("scum-catalog-card"); + expect(pageSource).toContain("scum-catalog-source"); }); it("uses catalog spawn commands for mixed gift contents", async () => {