ji-haitang/char_card_1/gal-presentation

Claude Code and agent skill by ji-haitang/char_card_1

Trust tier 2
  • community
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

姬侠传「GAL 演出」迭代的完整开发方案——LLM 正文段尾格式升级(每段自带时间 + 最多 2 个 NPC 各带表情)、时间源从 SIDE_NOTE 迁到末段、以及 GameMode 0 门派养成界面(2D/3D)上叠加可进出的 GAL 演出层。分 6 个阶段,每阶段含目标/改动点/验收标准/测试。当用户要求推进、修改、验收这项迭代的任一阶段,或需要回忆本迭代已拍板的决策时使用。

Install

npx vskill@latest install ji-haitang/char_card_1/gal-presentation

SKILL.md

GAL 演出迭代开发方案(格式升级 + GameMode 0 演出层)

状态:代码与自动化测试已完成(Phase 0-6 的代码改动、单测、规范/提示词更新均已落地;完成情况见第四节各 Phase 验收勾选)。待手工验收:浏览器脚本、真机/APK、真实模型 5 轮统计、旧格式特殊事件链端到端——清单见第四节末「状态与待办」。所有决策均已与用户逐条确认(见第二节决策表),落地时不要再重新讨论已拍板项。
用户补充决定:解析器与控制器不拆独立文件,直接写在 module/game-ui.js 内(见第三节)。
执行顺序固定:Phase 0 → 1 → 2 → 3 → 4 → 5。每个 Phase 独立可验收、可回滚;前一阶段验收未过不进下一阶段。

一、动手前必读(按顺序)

#文件 / 位置看什么
1module/game-ui.js parseSlgMainText(约 L49-188)现有五元组解析:parts.length !== 5 当纯文本累积;isNpcAllowed 只认 companionNPC;lastValidDisplay 兜底流式残留
2module/game-ui.js updateStoryText(约 L517)、_appendSlgLayers(约 L573-679)、updateStoryDisplay(约 L682)、doGoToPage/doToggleStoryExpand(约 L915/931)GameMode 1 才走解析并加 slg-global class;GameMode 0 剥离 `\ 后内容;图层重建、分页点击左右 1/3;两处隐藏副作用:emotion === '发情' → enamor = 1、写 window.__lastValidSceneUrl/__lastValidNpcEmotionUrl/__lastValidCgUrl`
3module/pipeline.js _commitResponse(约 L793)、_applyParsedSideNote(约 L1194)、withSceneGuard(约 L1344)、四处 renderMainText( 调用(流式 L732/L774、提交 L856、回滚 L1068)提交顺序:SIDE_NOTE 先于 renderMainText;流式与提交都调 renderMainText,演出进入只能挂在提交那一次
4module/game-events.js parseLLMResponse(约 L349-381)现有时间消费:currentGameTime / GameSceneBridge.captureTime / dayNightStatus / updateSceneBackgrounds()
5module/scene3d-bridge.js playPageTurn(约 L206-250)、afterRestore(约 L608-633)、setBusy(约 L639)、snapshot eligible(约 L73)翻页纸被 preferences.enabled 门控;turn 所有权;busy 集合暂停渲染
6module/game-helpers.js displayNpcs(约 L494-576)、switchScene(约 L1023-1062)、updateSceneBackgrounds(约 L1450-1482)站位类 .single/.double-left/.double-right;switchScene('player-stats'/'relationships') 会强删 .slg-layer-container 与 mask;主场景图 img/location/天山派_{季}_{昼夜}.webp
7module/game-utils.js matchNPC(约 L609-651)、matchScene(约 L543)、matchEmotion(约 L576)、stringSimilarity(约 L432)matchNPC 四层:精确名→精确 ID→包含→相似度≥0.6;演出需收紧
8module/game-styles.css 约 L1735-1850.slg-layer-container z:10、.slg-interaction-mask z:5、.slg-global .scene{display:none}、.slg-loading-layer 白底
9module/game-config.js npcs(约 L197-278)、locationNames(约 L173,id→中文名)、npcNameToId(约 L281)、legacySceneOptions(约 L670)全花名册(A-O)= GameMode 0 演出 NPC 白名单;门派地点中文名反查用 locationNames 反转(无现成 sceneNameToId,需在 parser 内建 Object.entries(locationNames) 反查表)
10char_card_information/110格式规范_精简版.txt 与 _独立前端.txt、105列表.txt、200COT思考引导.txt输出契约;105 的 GameMode 0 分支已含 ExpressionList/SceneList/NpcList
11module/special-event.js存量事件文本仍是旧五元组(含 GameMode 0 下播放的龟兹场景,如 SYN_Qiuzi_7)
12tests/scene3d/bridge-unit.mjs、tests/scene3d/README.md单测沙箱写法(node:test + vm)、浏览器验收 runner 与夹具(含 Gal 夹具)

二、已拍板决策(不再讨论)

2.1 核心原则

演出层 = 纯表现层覆盖。 viewport 里原本是业务层(2D .scene / 3D #sect-3d-root),演出就是在其上叠 gal-layer-container + mask;进入 = 挂上、退出 = 摘掉;底下一切业务逻辑照常运转、不感知演出。演出层挂上后只认自己那份 frames,不响应业务层变化;摘掉后用户看到业务层此刻的真实状态。

有意例外仅 4 处:① mask 拦 viewport 内点击(viewport 外控件仍可用);② setBusy('gal') 暂停 3D 渲染(性能,不改状态机);③ 末帧时间写 currentGameTime/dayNightStatus(属 commit 流水线,与是否进演出无关);④ switchScene('player-stats'/'relationships') 触发 exit(那两页现有代码会删图层)。

2.2 决策表
ID决策
F1新段尾格式 `正文\场景\HH:MM\NPC:表情;NPC:表情\NSFW,恒 5 段;第 3 段匹配 ^\d{1,2}[::]\d{2}$ 判定为新格式,否则走旧五元组 正文\NPC\场景\表情\CG` 分支(保留,服务 special-event.js 存量)
F2每段 最多 2 个 NPC(用户由 3 改为 2);首个为焦点;:/; 兼容全角 :/;;无 NPC 写 none
F3时间字段为该幕演出时间;LLM 规则:单调不倒退、单回合跨度 ≤ 2h、末段 ≤ 23:00、回忆/闪回段一律填主线当前时间
T1引擎只在 commit 阶段 取时间,流式不更新;兜底链:末帧有效时间 → 逆序前序帧 → SIDE_NOTE 时间(字段保留读取,规范里删除输出要求)→ 原值不变
T2跨回合时间回退合法(LLM 可能重述),不做单调约束校验,仅 console.warn
G0进出演出播翻页纸(复用 3D 翻页动画);演出内部段落切换按 G19 判定播纸
G1进入条件(自动):GameMode===0 && 新回复 commit 完成 && frames.length ≥ 1 && !isStoryExpanded && newWeek !== 1;同一回复只自动进入一次
G2进入条件(手动):GameMode 0 下点 story-area 中间 1/3,或任何翻页动作(左右 1/3 / 箭头 / 页码点 → doGoToPage)→ 进入并显示 min(currentPage, frames.length-1) 对应帧;已在演出中则退化为 goto;展开态不响应
G3退出:点舞台任意处(mask click);点"展开全文";收起全文不自动重进
G4不进入:展开态收到回复、newWeek===1、存档加载/回滚重渲染(renderMainText 非 commit 路径)
G5GameMode 0 NPC 白名单 = npcs 全花名册;匹配收紧为 精确名 / 精确 ID / 全名被 token 包含且 token.length ≤ name.length+2(+2 而非 +1:容“雨烛姑娘”“破阵子长老”“萧白瑚师姐”这类 2 字敬称,拒“唐沐梨的侍女”),不走相似度层;非法 NPC 逐个丢弃(console.log),其余继续
G6GameMode 0 场景不在 10 个门派地点(locationNames 的中文名,精确匹配)内 → 背景回退 `img/location/天山派_{seasonStatus}_{昼夜}.webp`
G7GameMode 0 演出 不触发 `enamor`、不写 `__lastValid*` 三个全局;场景/NPC/表情只做展示,唯一写变量的是末帧时间(T1)
G8翻页纸文案:进入 = "话本 · 第n回"(n = 进入时帧索引+1,按演出帧计,不按 storyPages 计);退出 = "话本 · 收卷"
G93D 换房进行中(bridge turn 非空)用户进演出:不播翻页直接进
G10位置切换(backToMap/goToLocation/switchScene 到地图或房间)不退出演出,在图层下静默完成
G11switchScene('player-stats'/'relationships') → exit({curtain:false}),保持状态一致
G12退出时若 3D 有待切换房间(busy 解除后会 apply 并自播翻页),跳过演出自身翻页,由 3D 换房翻页兼作退场
G13GameMode 0 演出用 body class gal-presenting,不用 slg-global(后者会 display:none 掉 .scene);不用白底 .slg-loading-layer;恒加 mask
G14GameMode 1 现有演出行为保持不变(回归基准),只是改为经过统一的解析层与控制器
G15纸归 3D 换房所有时(与 G9 同一判定 hasPendingApply()),enter/paint 经 syncOver3dTurn() 给 body 加 gal-over-3d-turn:.slg-layer-container/.slg-interaction-mask 抬到 z:70/65(3D 翻页纸 z:60 之上),首段立即可见、mask 可点退出;演出自己播纸时不加此类,正常进出场 z 序(层 10/纸 60)与逐帧时序不变。exit/setPresenting(false) 同步移除该类,保证 curtain:true 的盖纸能盖住演出层。paint 后补同步一次,覆盖 enter 时刻 3D turn 尚未创建的 observer/微任务竞态
G163D 显示中的 GM0 演出素材变体:use3dVer()(bridge getDiagnostics() 的 preferences.enabled && snapshot.visible,visible 内含 protocol!=='file:')为真时——场景图走 img/location/3D.ver/<场景>-<季>-<昼夜>.webp(连字符命名,G6 回退口径不变:非门派场景回退 天山派-<季>-<昼夜>);表情立绘走 img/表情差分和CG/像素/<npc>/<表情>.webp(扁平目录,15 NPC 全齐);容器加 gal-3dver 类,CSS 承担毛玻璃(blur(3px) 场景图)与左侧焦点立绘镜像(.slg-cast-left 加 scaleX(-1))。特殊 CG 色图路径不动。判定为假(2D/GM1/file:///沙箱)时所有路径与结构与现状逐字节一致。【2026-10 形体补充】.gal-3dver 内非特殊CG立绘另作形体调整:高 56%(基础 70%×0.8)、bottom: 20%(整体上移视口高 20%)、并借 img 自身 background 的 radial-gradient(ellipse 11.5% 2.5% at 50% 97.5%, …) 画脚底扁椭圆阴影(透过透明像素显示、层级在人物后,水平直径=立绘宽 23%,底边与立绘底边相切)
G17GM0 操作层表情立绘(不变量 3 的登记例外):displayNpcs 在 GameMode===0 时把 .npc-portrait img 源换成 img/表情差分和CG/<npc>/表情差分/<表情>.webp(404 逐级兜底 表情→平静→npcPortraits),表情来自 npcTurnEmotions 回合表——每回合开始 withSceneGuard 调 resetNpcTurnEmotions() 回默认「平静」,仅 commit 路径(_commitResponse renderMainText 后)调 updateNpcTurnEmotions(slgModeData)+refreshNpcEmotionPortraits() 按本次回复各 NPC 末次表情统一切换(流式/回滚不切)。纯展示图源替换,不写任何游戏变量;换 src 时 _npcOpaqueCanvasCache.delete(img) 作透明度缓存失效;只换 src 不重建 DOM(避免 3 人随机洗牌重 roll)。GM1 与 3D 公告板(bridge renderedNpcs 的 portraitUrl)不受影响
G19演出内部翻页播纸判定(goto,2026-10 用户要求):① CG 身份变化播——CG 身份键 = 焦点NPC:cg标记(与色图层实际内容同源,cgContentEnabled 关闭或 cg===none 或无焦点均视为无 CG;randIdx 随机变体不计入),前后键不同即播(无↔有 / 前后都有但不一致,含同标记换焦点 NPC);② 前后都无 CG 时 NPC 阵容集合变化播——阵容键 = 出场成员名字排序拼接,单纯主次互换、表情变化不算;其余换帧(含纯场景切换)不播。封面文案同 enter(第n回+时间·场景)。所有换帧经 chain 串行保持点击序;goto 不递增 gen(不抢占进行中的 enter),封面期间被 exit/reset 抢先(gen 变化或 state!=='playing')时补揭开纸张后放弃换帧(与 enter 同一兜底);3D 换房持纸时 bridge playCurtain 自动 no-op,退化为无纸换帧。【2026-10 连播修复】goto 的同目标合并检查与幕布判定推迟到 chain 串行执行时刻(不能在调用时刻算):doGoToPage 一次翻页会同帧连发 enter({curtain:true})(playing 退化为 goto)+ updateStoryDisplay 的 goto(currentPage) 两个换帧,调用时刻 index 还是旧值(paint 在前序 run 内才更新),同步段判不出来;执行时刻目标已被前序 run 画过(target===index)则合并跳过返回 true,幕布不连播第二次;abort 预检也提前到盖纸之前(排队期被 exit 的 goto 静默放弃、不留纸)。真机探针 .scene3d-work/gal-double-goto-verify.mjs 以 .scene3d-page-turn hidden 序列断言(修复前 [false,true,false,true],修复后 [false,true])
G20演出层场景标签(右上角地点名匾额+小字时间,2026-10-15 用户要求):renderLayers 在立绘层与 CG 层之间插 .slg-scene-tag(div > .scene-title + 可选 .slg-scene-time)。地点名 sceneLabelFor(frame, mode):GM0 与 G6 场景图判定同源(sectSceneNames() 抽出共用)——命中门派子场景显子场景名,否则(回退天山派图的帧)显「天山派」;GM1 恒显当前 mapLocation(与帧 scene 无关,scene=none 也显)。时间仅 frame.time 非空才渲染(旧格式帧无)。复用 .scene-title 类名嵌入新容器,古风 theme.css / 扁平 beautify.css 匾额样式(竖排/隶楷体/背景.png 伪元素/top:3% right:4%)零复制自动命中——嵌套定位用 .slg-layer-container .scene-title { position:static!important; transform:none!important } 压主题规则(主题定位无 !important);注意 beautify.css 165 body.ui-style-ancient .scene-title 的 font-family:'Kaiti','STKaiti'!important 特异性压过 theme.css 的 LiSu/KaiTi,古风实际计算字体是 Kaiti 族(与操作层匾额一致)。tag z:2 与立绘同级、DOM 在其后(压立绘),CG 层 z:3 在其上——有 CG 的帧标签被盖住;pointer-events:none 不挡 mask。时间 .slg-scene-time:横排小字 clamp(10px,1.3vmin,18px) 白字黑描边。操作层 .scene-title 演出时被不透明场景图盖住,无双标签问题。真机探针 .scene3d-work/gal-scene-tag-verify.mjs(GM0 四帧+竖屏 390×844 自适应+GM1 mapLocation/无时间帧;断言 computed 样式/z 序/vmin 字号收放)。【2026-10-16 补充】表情栏为「特殊CGn」的整幅立绘帧与 NSFW CG 帧同口径不显示地名时间:.slg-emotion-layer.slg-emotion-special-cg 抬 z:3(与 NSFW CG 层同级;同帧若另有 NSFW CG,DOM 序 CG 层在后仍居上),盖住 z:2 标签;只抬层级不改渲染(标签 DOM 仍在),常规立绘 z:2 不受影响(仍在标签下)。探针帧4(苓雪妃:特殊CG1)实测 specialCg z:3 > tag z:2,截图满屏盖住。【2026-10-16 vmin 补充】手机实测横屏下匾名 3.5vw 按视口宽度计、相对视口高度占比远超竖屏——嵌套覆盖改 font-size:3.5vmin;padding:1.5vmin 2vmin;border-radius:.5vmin(含 ::before),时间改 clamp(10px,1.3vmin,18px):竖屏 vmin=vw 与原值逐像素一致,同一屏幕横竖朝向字号绝对一致(探针 844×390 横屏与 390×844 竖屏均 13.65px);均带 !important(flat 版 body.ui-style-flat .scene-title 无 important 的 font-size 特异性更高,须压)

三、目标架构

LLM MAIN_TEXT
│ GalParser.parseFrames() ← game-ui.js 内 `// ==== GAL_PARSER_BEGIN/END ====` 块(纯函数 IIFE)
▼
frames[] = [{ text, scene, time, cast:[{npc,emotion}], cg, legacy, npc, emotion }]
│ (npc/emotion = cast[0] 的镜像,兼容旧消费者)
├── 数据层:slgModeData / storyPages / currentPage(game-ui.js 现有)
├── 时间源:parseLLMResponse 读 frames 末帧时间(game-events.js)
└── 演出层:galPresentation 控制器 ← game-ui.js 内 `// ==== GAL_PRESENTATION_BEGIN/END ====` 块(IIFE)
enter({source, pageIndex}) / goto(i) / exit({reason}) / isPlaying()
渲染 .slg-layer-container(背景/立绘/CG)+ .slg-interaction-mask
GameMode 1:不走状态机,updateStoryDisplay 直接调 renderLayers/clearLayers(行为同现状)
GameMode 0:按 G1-G13 进出,加 gal-presenting,setBusy('gal')
业务层:.scene / #sect-3d-root / displayNpcs / switchScene —— 不改

不新增模块文件(用户决定)。两个 IIFE 直接写在 game-ui.js 顶部(parseSlgMainText 原位置),各用一对切片标记注释包裹;单测用 source.slice(indexOf('// ==== GAL_PARSER_BEGIN ===='), indexOf('// ==== GAL_PARSER_END ====')) 把块切出来在 vm 沙箱运行(同 bridge-unit.mjs 切函数体的做法)。两个块内部只能引用:自身参数 / ctx 传入项 / 明确以 typeof x !== 'undefined' 守卫的全局,保证在最小沙箱可加载。

编辑纪律:game-ui.js 含 \n 正则字面量,SearchReplace 曾两次把文件写坏——本文件的所有修改一律用 node 手术脚本(Write 临时 .js → node 执行 → node --check → 删脚本)。

3.1 GalParser 契约
GalParser.parseFrames(mainText, ctx) → Frame[] // 实际实现:直接返回数组;末尾无标记残文作为最后一帧 tail=true,沿用上一有效展示配置
ctx = buildGalParseContext(gameMode) // game-ui.js 内的共用构造,game-events.js 也用它
= { gameMode, roster:[{id,name}], matchNpc, isNpcAllowed, matchScene, matchEmotion, matchCG, log }
Frame = { text, scene, time|null, cast:[{npc,emotion}](≤2), cg, legacy:boolean, npc, emotion, tail?:true }
  • 分隔符:按行切;一行 split('|') 恰得 5 段才是标记行;否则累积到当前段正文(与现状一致)。
  • 新格式判定:parts[2] trim 后匹配 ^\d{1,2}[::]\d{2}$。时间归一为 HH:MM(补零,全角冒号转半角)。
  • cast 解析:parts[3] 按 [;;] 切,每项按 [::] 切成 NPC:表情;缺表情视为 none;none/无/空 → 空 cast;白名单过滤后取前 2 个。
  • 旧格式:parts[1]=NPC、parts[2]=场景、parts[3]=表情、parts[4]=CG,cast 最多 1 个,legacy=true,time=null。
  • 纯函数、无 DOM、无全局读写。
3.2 galPresentation 契约

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
72
Repository forks
14
7-day trend score
0

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/ji-haitang/char_card_1/gal-presentation/badge)](https://verified-skill.com/skills/ji-haitang/char_card_1/gal-presentation)

Something wrong with this skill or its page? Tell us.