From 4c395b73cac19213fbf6107e0dde52a8640b6735 Mon Sep 17 00:00:00 2001 From: sion123 <450702724@qq.com> Date: Sat, 28 Mar 2026 01:34:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/skills/antd-to-shadcn/SKILL.md | 268 - .../references/COMPONENT_MAP.md | 162 - .../references/MIGRATION_PATTERNS.md | 662 --- .../antd-to-shadcn/references/STYLES.md | 458 -- .claude/skills/docx/LICENSE.txt | 30 - .claude/skills/docx/SKILL.md | 197 - .claude/skills/docx/docx-js.md | 350 -- .claude/skills/docx/ooxml.md | 610 --- .../schemas/ISO-IEC29500-4_2016/dml-chart.xsd | 1499 ------ .../ISO-IEC29500-4_2016/dml-chartDrawing.xsd | 146 - .../ISO-IEC29500-4_2016/dml-diagram.xsd | 1085 ---- .../ISO-IEC29500-4_2016/dml-lockedCanvas.xsd | 11 - .../schemas/ISO-IEC29500-4_2016/dml-main.xsd | 3081 ------------ .../ISO-IEC29500-4_2016/dml-picture.xsd | 23 - .../dml-spreadsheetDrawing.xsd | 185 - .../dml-wordprocessingDrawing.xsd | 287 -- .../ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd | 1676 ------- .../shared-additionalCharacteristics.xsd | 28 - .../shared-bibliography.xsd | 144 - .../shared-commonSimpleTypes.xsd | 174 - .../shared-customXmlDataProperties.xsd | 25 - .../shared-customXmlSchemaProperties.xsd | 18 - .../shared-documentPropertiesCustom.xsd | 59 - .../shared-documentPropertiesExtended.xsd | 56 - .../shared-documentPropertiesVariantTypes.xsd | 195 - .../ISO-IEC29500-4_2016/shared-math.xsd | 582 --- .../shared-relationshipReference.xsd | 25 - .../ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd | 4439 ----------------- .../schemas/ISO-IEC29500-4_2016/vml-main.xsd | 570 --- .../ISO-IEC29500-4_2016/vml-officeDrawing.xsd | 509 -- .../vml-presentationDrawing.xsd | 12 - .../vml-spreadsheetDrawing.xsd | 108 - .../vml-wordprocessingDrawing.xsd | 96 - .../ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd | 3646 -------------- .../ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd | 116 - .../ecma/fouth-edition/opc-contentTypes.xsd | 42 - .../ecma/fouth-edition/opc-coreProperties.xsd | 50 - .../schemas/ecma/fouth-edition/opc-digSig.xsd | 49 - .../ecma/fouth-edition/opc-relationships.xsd | 33 - .claude/skills/docx/ooxml/schemas/mce/mc.xsd | 75 - .../docx/ooxml/schemas/microsoft/wml-2010.xsd | 560 --- .../docx/ooxml/schemas/microsoft/wml-2012.xsd | 67 - .../docx/ooxml/schemas/microsoft/wml-2018.xsd | 14 - .../ooxml/schemas/microsoft/wml-cex-2018.xsd | 20 - .../ooxml/schemas/microsoft/wml-cid-2016.xsd | 13 - .../microsoft/wml-sdtdatahash-2020.xsd | 4 - .../schemas/microsoft/wml-symex-2015.xsd | 8 - .claude/skills/docx/ooxml/scripts/pack.py | 159 - .claude/skills/docx/ooxml/scripts/unpack.py | 29 - .claude/skills/docx/ooxml/scripts/validate.py | 69 - .../docx/ooxml/scripts/validation/__init__.py | 15 - .../docx/ooxml/scripts/validation/base.py | 951 ---- .../docx/ooxml/scripts/validation/docx.py | 274 - .../docx/ooxml/scripts/validation/pptx.py | 315 -- .../ooxml/scripts/validation/redlining.py | 279 -- .claude/skills/docx/scripts/__init__.py | 1 - .claude/skills/docx/scripts/document.py | 1276 ----- .../docx/scripts/templates/comments.xml | 3 - .../scripts/templates/commentsExtended.xml | 3 - .../scripts/templates/commentsExtensible.xml | 3 - .../docx/scripts/templates/commentsIds.xml | 3 - .../skills/docx/scripts/templates/people.xml | 3 - .claude/skills/docx/scripts/utilities.py | 374 -- .claude/skills/indie-game-dev/SKILL.md | 143 - .../assets/templates/game-templates.md | 363 -- .../srpg-crown-oath/01-game-overview.md | 288 -- .../proposals/srpg-crown-oath/02-gameplay.md | 455 -- .../srpg-crown-oath/03-world-setting.md | 583 --- .../srpg-crown-oath/04-story-outline.md | 454 -- .../srpg-crown-oath/05-characters.md | 616 --- .../proposals/srpg-crown-oath/06-systems.md | 616 --- .../srpg-crown-oath/07-asset-list.md | 751 --- .../proposals/srpg-crown-oath/README.md | 101 - .../references/balance-system.md | 238 - .../indie-game-dev/references/level-design.md | 306 -- .../indie-game-dev/references/ui-ux-design.md | 328 -- .../__pycache__/inventory.cpython-312.pyc | Bin 40506 -> 0 bytes CLAUDE.md | 17 +- frontend/app/web-gold/src/api/pointRecord.js | 16 +- frontend/app/web-gold/src/stores/user.js | 13 + .../app/web-gold/src/views/user/Profile.vue | 741 ++- .../tik/muye/aiagent/dal/AiAgentDO.java | 2 + 82 files changed, 328 insertions(+), 31927 deletions(-) delete mode 100644 .claude/skills/antd-to-shadcn/SKILL.md delete mode 100644 .claude/skills/antd-to-shadcn/references/COMPONENT_MAP.md delete mode 100644 .claude/skills/antd-to-shadcn/references/MIGRATION_PATTERNS.md delete mode 100644 .claude/skills/antd-to-shadcn/references/STYLES.md delete mode 100644 .claude/skills/docx/LICENSE.txt delete mode 100644 .claude/skills/docx/SKILL.md delete mode 100644 .claude/skills/docx/docx-js.md delete mode 100644 .claude/skills/docx/ooxml.md delete mode 100644 .claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/mce/mc.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/microsoft/wml-2010.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/microsoft/wml-2012.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/microsoft/wml-2018.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/microsoft/wml-cex-2018.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/microsoft/wml-cid-2016.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd delete mode 100644 .claude/skills/docx/ooxml/schemas/microsoft/wml-symex-2015.xsd delete mode 100644 .claude/skills/docx/ooxml/scripts/pack.py delete mode 100644 .claude/skills/docx/ooxml/scripts/unpack.py delete mode 100644 .claude/skills/docx/ooxml/scripts/validate.py delete mode 100644 .claude/skills/docx/ooxml/scripts/validation/__init__.py delete mode 100644 .claude/skills/docx/ooxml/scripts/validation/base.py delete mode 100644 .claude/skills/docx/ooxml/scripts/validation/docx.py delete mode 100644 .claude/skills/docx/ooxml/scripts/validation/pptx.py delete mode 100644 .claude/skills/docx/ooxml/scripts/validation/redlining.py delete mode 100644 .claude/skills/docx/scripts/__init__.py delete mode 100644 .claude/skills/docx/scripts/document.py delete mode 100644 .claude/skills/docx/scripts/templates/comments.xml delete mode 100644 .claude/skills/docx/scripts/templates/commentsExtended.xml delete mode 100644 .claude/skills/docx/scripts/templates/commentsExtensible.xml delete mode 100644 .claude/skills/docx/scripts/templates/commentsIds.xml delete mode 100644 .claude/skills/docx/scripts/templates/people.xml delete mode 100644 .claude/skills/docx/scripts/utilities.py delete mode 100644 .claude/skills/indie-game-dev/SKILL.md delete mode 100644 .claude/skills/indie-game-dev/assets/templates/game-templates.md delete mode 100644 .claude/skills/indie-game-dev/proposals/srpg-crown-oath/01-game-overview.md delete mode 100644 .claude/skills/indie-game-dev/proposals/srpg-crown-oath/02-gameplay.md delete mode 100644 .claude/skills/indie-game-dev/proposals/srpg-crown-oath/03-world-setting.md delete mode 100644 .claude/skills/indie-game-dev/proposals/srpg-crown-oath/04-story-outline.md delete mode 100644 .claude/skills/indie-game-dev/proposals/srpg-crown-oath/05-characters.md delete mode 100644 .claude/skills/indie-game-dev/proposals/srpg-crown-oath/06-systems.md delete mode 100644 .claude/skills/indie-game-dev/proposals/srpg-crown-oath/07-asset-list.md delete mode 100644 .claude/skills/indie-game-dev/proposals/srpg-crown-oath/README.md delete mode 100644 .claude/skills/indie-game-dev/references/balance-system.md delete mode 100644 .claude/skills/indie-game-dev/references/level-design.md delete mode 100644 .claude/skills/indie-game-dev/references/ui-ux-design.md delete mode 100644 .claude/skills/pptx/scripts/__pycache__/inventory.cpython-312.pyc diff --git a/.claude/skills/antd-to-shadcn/SKILL.md b/.claude/skills/antd-to-shadcn/SKILL.md deleted file mode 100644 index 06108e0f9a..0000000000 --- a/.claude/skills/antd-to-shadcn/SKILL.md +++ /dev/null @@ -1,268 +0,0 @@ ---- -name: antd-to-shadcn -description: Vue 3 项目从 Ant Design Vue 迁移到 shadcn-vue 的专业技能。支持组件替换、表单表格弹窗迁移、UI 现代化、颜色解耦和主题配置。触发词:迁移、shadcn、antd 替换、组件升级、UI 现代化 ---- - -# Ant Design Vue → shadcn-vue 迁移技能 - -将 Vue 3 + Ant Design Vue 项目迁移到 shadcn-vue + TailwindCSS。 - -## 核心原则 - -1. **业务功能不变** - 只改 UI 层,不改业务逻辑 -2. **现代化 UI** - 采用 shadcn 设计语言,简洁现代 -3. **颜色解耦** - 使用 CSS 变量,支持主题切换 -4. **渐进式迁移** - 逐个组件替换,保持可运行 - -## 设计品质要求 - -### 抵制过时设计 - -**拒绝老气 UI**: -- ❌ 粗重边框、多重边框嵌套(边框重叠) -- ❌ 灰暗沉闷的配色 -- ❌ 过度装饰的渐变和阴影 -- ❌ 拥挤无呼吸感的布局 -- ❌ 间距混乱、缺乏层级 -- ❌ **老旧 Tabs 设计**(下方粗线、方块按钮感) -- ❌ **Element UI 风格组件**(el-tabs、el-form 等) -- ❌ **密集表单**(label 左对齐、无间距) -- ❌ **重阴影卡片**(shadow-lg 以上) - -**拥抱现代设计**: -- ✅ 轻量边框或无边框设计 -- ✅ 明亮有活力的色彩 -- ✅ 克制的阴影(shadow-sm 为主) -- ✅ 充足留白,呼吸感 -- ✅ 清晰的视觉层级(8px 间距递进) -- ✅ **现代 Tabs**(胶囊式、underline 轻量) -- ✅ **宽松表单**(label 上方、垂直布局) - -### 老旧组件识别与替换 - -| 老旧模式 | 现代替代 | -|---------|---------| -| `a-tabs` / `el-tabs` 底部粗线 | shadcn `Tabs` 胶囊式 | -| `a-segmented` | 自定义 pill 按钮 | -| 密集 `a-form` | 宽松 `Form` 垂直布局 | -| `a-card` 重阴影 | `Card` 无边框或轻边框 | -| `a-descriptions` | 自定义 grid 布局 | -| `a-breadcrumb` | shadcn `Breadcrumb` | -| `a-pagination` | shadcn `Pagination` | - -### 颜色设计原则 - -**年轻活力感**: -- 主色饱和度适中(oklch chroma 0.14-0.18) -- 避免过于灰暗的中间色 -- 使用微妙渐变增加质感 -- 深色模式保持足够对比度 - -**发现优秀配色时**: -1. 提取关键颜色值 -2. 添加到 `style.css` 设计令牌 -3. 使用语义化命名(如 `--color-accent-blue`) -4. 在组件中通过变量引用 - -### 间距层级规范 - -``` -紧密:gap-1 (4px) - 图标与文字 -标准:gap-2 (8px) - 同组元素 -舒适:gap-3 (12px) - 表单项之间 -宽松:gap-4 (16px) - 卡片内容 -分区:gap-6 (24px) - 不同区块 -``` - -## 项目上下文 - -``` -前端目录: frontend/app/web-gold/ -组件目录: src/components/ui/ # shadcn 组件 -主题文件: src/theme.css # shadcn 主题变量 -样式文件: src/style.css # 设计令牌 -``` - -## 迁移工作流 - -### Step 1: 识别 Ant Design 组件 - -扫描文件中的 Ant Design 导入: -```vue -// 需要替换的模式 -import { Button, Input, Form, Table, Modal, ... } from 'ant-design-vue' -import { IconName } from '@ant-design/icons-vue' -import { message, notification } from 'ant-design-vue' -``` - -### Step 2: 组件映射 - -参见 [COMPONENT_MAP.md](references/COMPONENT_MAP.md) 获取完整映射表。 - -快速参考: -| Ant Design | shadcn-vue | -|------------|------------| -| a-button | Button | -| a-input | Input | -| a-form | Form | -| a-table | Table | -| a-modal | Dialog | -| a-select | Select | -| a-message | Sonner (toast) | -| a-icon | Iconify lucide | - -### Step 3: 迁移模式 - -参见 [MIGRATION_PATTERNS.md](references/MIGRATION_PATTERNS.md) 获取详细代码示例。 - -### Step 4: 样式迁移 - -参见 [STYLES.md](references/STYLES.md) 获取样式迁移指南。 - -## 执行迁移 - -### 1. 表单组件迁移 - -```vue - - - - - - - - -
- - - 标签 - - - - - - -
-``` - -### 2. 表格组件迁移 - -```vue - - - - - - - - {{ col.title }} - - - - - {{ row[col.dataIndex] }} - - -
-``` - -### 3. 消息提示迁移 - -```javascript -// Before -import { message } from 'ant-design-vue' -message.success('操作成功') - -// After -import { toast } from 'vue-sonner' -toast.success('操作成功') -``` - -### 4. 图标迁移 - -```vue - -import { UserOutlined } from '@ant-design/icons-vue' - - - -import { Icon } from '@iconify/vue' - -``` - -## 间距规范 - -使用 Tailwind 间距类替代固定值: -- `p-4` = 16px -- `gap-3` = 12px -- `space-y-4` = 子元素间 16px 间距 - -## 颜色使用 - -使用语义化 CSS 变量: -```css -color: var(--foreground) /* 主文字 */ -color: var(--muted-foreground) /* 次要文字 */ -background: var(--background) /* 背景 */ -background: var(--primary) /* 主色 */ -border-color: var(--border) /* 边框 */ -``` - -## 迁移后检查 - -### Step 5: 现代化与一致性审查 - -迁移完成后,必须进行设计审查: - -**现代化检查**: -- [ ] Tabs 是否使用胶囊式设计(非底部粗线)? -- [ ] 表单间距是否宽松(space-y-6)? -- [ ] 卡片阴影是否克制(shadow-sm)? -- [ ] 边框是否避免重叠? -- [ ] 颜色是否有活力(非灰暗)? -- [ ] 按钮是否有圆角(rounded-lg)? -- [ ] 表格是否简洁无斑马纹? -- [ ] 整体是否有呼吸感? - -**一致性检查**: -- [ ] 相似页面布局是否一致? -- [ ] 同类组件样式是否统一? -- [ ] 间距规范是否遵循(4px 递进)? -- [ ] 颜色使用是否语义化? -- [ ] 字体大小是否有层级? - -### Step 6: 业务功能验证 - -确保迁移没有破坏现有功能: - -**交互检查**: -- [ ] 表单提交是否正常? -- [ ] 按钮点击事件是否触发? -- [ ] 弹窗/对话框是否正常打开关闭? -- [ ] 下拉选择是否正常工作? -- [ ] 分页是否正常? -- [ ] 搜索筛选是否正常? - -**数据检查**: -- [ ] 数据绑定是否正常(v-model)? -- [ ] 列表渲染是否正常? -- [ ] 条件渲染是否正常? -- [ ] 数据加载状态是否显示? - -**边界情况**: -- [ ] 空状态是否显示正常? -- [ ] 错误状态是否处理? -- [ ] 加载状态是否显示? -- [ ] 禁用状态是否正常? - -## 检查清单 - -迁移完成后验证: -- [ ] 所有 a- 前缀组件已替换 -- [ ] ant-design-vue 导入已移除 -- [ ] 图标已迁移到 Iconify -- [ ] message/notification 已迁移到 Sonner -- [ ] 样式使用 Tailwind 类或 CSS 变量 -- [ ] **现代化审查通过** -- [ ] **一致性审查通过** -- [ ] **业务功能验证通过** diff --git a/.claude/skills/antd-to-shadcn/references/COMPONENT_MAP.md b/.claude/skills/antd-to-shadcn/references/COMPONENT_MAP.md deleted file mode 100644 index 322284f8ec..0000000000 --- a/.claude/skills/antd-to-shadcn/references/COMPONENT_MAP.md +++ /dev/null @@ -1,162 +0,0 @@ -# 组件映射表 - -完整的 Ant Design Vue → shadcn-vue 组件映射。 - -## 基础组件 - -| Ant Design | shadcn-vue | 说明 | -|------------|------------|------| -| `a-button` | `Button` | 按钮 | -| `a-input` | `Input` | 输入框 | -| `a-input-number` | `NumberField` | 数字输入 | -| `a-input-password` | `Input type="password"` | 密码输入 | -| `a-textarea` | `Textarea` | 多行文本 | -| `a-select` | `Select` | 下拉选择 | -| `a-radio` | `RadioGroup` + `RadioGroupItem` | 单选 | -| `a-checkbox` | `Checkbox` | 复选框 | -| `a-switch` | `Switch` | 开关 | -| `a-slider` | `Slider` | 滑动条 | -| `a-rate` | 自定义 | 评分(需自定义) | -| `a-upload` | 自定义 | 上传(需自定义) | - -## 表单组件 - -| Ant Design | shadcn-vue | 说明 | -|------------|------------|------| -| `a-form` | `Form` | 表单容器 | -| `a-form-item` | `FormItem` | 表单项 | -| `a-form-provider` | 自定义 | 表单上下文 | -| `a-range-picker` | `RangeCalendar` + `Popover` | 日期范围 | -| `a-date-picker` | `Calendar` + `Popover` | 日期选择 | -| `a-time-picker` | 自定义 | 时间选择 | - -## 数据展示 - -| Ant Design | shadcn-vue | 说明 | -|------------|------------|------| -| `a-table` | `Table` | 表格 | -| `a-list` | 自定义 | 列表 | -| `a-card` | `Card` | 卡片 | -| `a-descriptions` | 自定义 | 描述列表 | -| `a-statistic` | 自定义 | 统计数值 | -| `a-tree` | 自定义 | 树形控件 | -| `a-avatar` | `Avatar` | 头像 | -| `a-badge` | `Badge` | 徽标 | -| `a-tag` | `Badge` variant | 标签 | -| `a-timeline` | 自定义 | 时间轴 | -| `a-image` | `Img` 或自定义 | 图片 | -| `a-empty` | `Empty` | 空状态 | - -## 导航组件 - -| Ant Design | shadcn-vue | 说明 | -|------------|------------|------| -| `a-menu` | `NavigationMenu` / `SidebarMenu` | 菜单 | -| `a-dropdown` | `DropdownMenu` | 下拉菜单 | -| `a-pagination` | `Pagination` | 分页 | -| `a-steps` | `Stepper` | 步骤条 | -| `a-breadcrumb` | `Breadcrumb` | 面包屑 | -| `a-tabs` | `Tabs` | 标签页 | - -## 反馈组件 - -| Ant Design | shadcn-vue | 说明 | -|------------|------------|------| -| `a-modal` | `Dialog` | 对话框 | -| `a-drawer` | `Drawer` | 抽屉 | -| `a-message` | `toast` (vue-sonner) | 全局提示 | -| `a-notification` | `toast` (vue-sonner) | 通知 | -| `a-popconfirm` | `AlertDialog` | 确认对话框 | -| `a-popover` | `Popover` | 气泡卡片 | -| `a-tooltip` | `Tooltip` | 文字提示 | -| `a-progress` | `Progress` | 进度条 | -| `a-spin` | `Spinner` | 加载中 | -| `a-skeleton` | `Skeleton` | 骨架屏 | -| `a-alert` | `Alert` | 警告提示 | - -## 布局组件 - -| Ant Design | shadcn-vue | 说明 | -|------------|------------|------| -| `a-layout` | 自定义 | 布局 | -| `a-layout-sider` | `Sidebar` | 侧边栏 | -| `a-layout-header` | 自定义 | 头部 | -| `a-layout-content` | 自定义 | 内容 | -| `a-layout-footer` | 自定义 | 底部 | -| `a-row` / `a-col` | Tailwind `grid` / `flex` | 栅格 | -| `a-space` | `div` + Tailwind `gap-*` | 间距 | -| `a-divider` | `Separator` | 分割线 | - -## 图标映射 - -| Ant Design Icons | Iconify (lucide) | -|------------------|------------------| -| `UserOutlined` | `lucide:user` | -| `SettingOutlined` | `lucide:settings` | -| `SearchOutlined` | `lucide:search` | -| `PlusOutlined` | `lucide:plus` | -| `DeleteOutlined` | `lucide:trash-2` | -| `EditOutlined` | `lucide:pencil` | -| `CloseOutlined` | `lucide:x` | -| `CheckOutlined` | `lucide:check` | -| `EyeOutlined` | `lucide:eye` | -| `EyeInvisibleOutlined` | `lucide:eye-off` | -| `LoadingOutlined` | `lucide:loader-2` | -| `ExclamationCircleOutlined` | `lucide:alert-circle` | -| `InfoCircleOutlined` | `lucide:info` | -| `QuestionCircleOutlined` | `lucide:help-circle` | -| `PhoneOutlined` | `lucide:phone` | -| `MailOutlined` | `lucide:mail` | -| `SafetyOutlined` | `lucide:shield` | -| `UploadOutlined` | `lucide:upload` | -| `DownloadOutlined` | `lucide:download` | -| `CopyOutlined` | `lucide:copy` | -| `ReloadOutlined` | `lucide:refresh-cw` | -| `FilterOutlined` | `lucide:filter` | -| `MoreOutlined` | `lucide:more-horizontal` | -| `MenuOutlined` | `lucide:menu` | -| `HomeOutlined` | `lucide:home` | -| `LeftOutlined` | `lucide:chevron-left` | -| `RightOutlined` | `lucide:chevron-right` | -| `UpOutlined` | `lucide:chevron-up` | -| `DownOutlined` | `lucide:chevron-down` | - -## 图标使用方式 - -```vue - - - -``` - -## 表单验证迁移 - -| Ant Design | shadcn-vue (VeeForm) | -|------------|----------------------| -| `rules` prop | `zod` schema + `toTypedSchema` | -| `validateFields()` | `form.validate()` | -| `validateTrigger` | schema 配置 | -| `hasFeedback` | `FormMessage` 组件 | - -## 事件映射 - -| Ant Design | shadcn-vue | -|------------|------------| -| `@change` | `@update:modelValue` | -| `@pressEnter` | `@keydown.enter` | -| `@search` | 自定义 + `@keydown.enter` | -| `@select` | `@update:modelValue` (Select) | diff --git a/.claude/skills/antd-to-shadcn/references/MIGRATION_PATTERNS.md b/.claude/skills/antd-to-shadcn/references/MIGRATION_PATTERNS.md deleted file mode 100644 index 392c1173ef..0000000000 --- a/.claude/skills/antd-to-shadcn/references/MIGRATION_PATTERNS.md +++ /dev/null @@ -1,662 +0,0 @@ -# 迁移模式 - -详细的代码迁移示例,从 Ant Design Vue 到 shadcn-vue。 - -## 目录 - -1. [表单迁移](#表单迁移) -2. [表格迁移](#表格迁移) -3. [弹窗迁移](#弹窗迁移) -4. [消息提示迁移](#消息提示迁移) -5. [下拉选择迁移](#下拉选择迁移) -6. [分页迁移](#分页迁移) -7. [标签页迁移](#标签页迁移) -8. [抽屉迁移](#抽屉迁移) - ---- - -## 表单迁移 - -### 简单表单 - -```vue - - - - - - - - - -``` - -### 表单 + 验证码(带按钮状态) - -```vue - - - - -``` - ---- - -## 表格迁移 - -```vue - - - - - - - - - -``` - ---- - -## 弹窗迁移 - -```vue - - - ... - - - - - - -``` - ---- - -## 消息提示迁移 - -```javascript -// ========== Before: Ant Design ========== -import { message, notification } from 'ant-design-vue' - -message.success('操作成功') -message.error('操作失败') -message.warning('警告信息') -message.loading('加载中...') - -notification.success({ - message: '成功', - description: '操作已完成' -}) - -// ========== After: shadcn-vue (vue-sonner) ========== -import { toast } from 'vue-sonner' - -toast.success('操作成功') -toast.error('操作失败') -toast.warning('警告信息') -toast.loading('加载中...') - -// 带描述的通知 -toast.success('操作已完成', { - description: '数据已保存' -}) - -// 自定义时长 -toast.success('操作成功', { duration: 3000 }) - -// Promise 状态 -toast.promise(saveData(), { - loading: '保存中...', - success: '保存成功', - error: '保存失败' -}) -``` - ---- - -## 下拉选择迁移 - -```vue - - - - - - - -``` - ---- - -## 分页迁移 - -```vue - - - - - - - -``` - ---- - -## 标签页迁移 - -```vue - - - 内容一 - 内容二 - - - - - - -``` - ---- - -## 抽屉迁移 - -```vue - - - 内容 - - - - - - -``` - ---- - -## 常见问题 - -### Q: 如何处理 a-form 的 layout 属性? - -```vue - -
- - - - - - - -
- - -
-``` - -### Q: 如何处理自定义验证? - -```javascript -// Ant Design -const rules = { - field: [{ - validator: async (rule, value) => { - if (!value) throw new Error('必填') - if (value.length < 3) throw new Error('太短') - } - }] -} - -// shadcn-vue (zod) -const formSchema = toTypedSchema(z.object({ - field: z.string() - .min(1, '必填') - .refine(val => val.length >= 3, '太短') - .refine(async (val) => { - // 异步验证 - const exists = await checkExists(val) - return !exists - }, '已存在') -})) -``` - -### Q: 如何处理表单初始值? - -```javascript -// Ant Design -const form = reactive({ name: '初始值' }) - -// shadcn-vue -const { resetForm } = useForm({ - validationSchema: formSchema, - initialValues: { name: '初始值' } -}) - -// 重置到初始值 -resetForm() -``` diff --git a/.claude/skills/antd-to-shadcn/references/STYLES.md b/.claude/skills/antd-to-shadcn/references/STYLES.md deleted file mode 100644 index 94b041900c..0000000000 --- a/.claude/skills/antd-to-shadcn/references/STYLES.md +++ /dev/null @@ -1,458 +0,0 @@ -# 样式迁移指南 - -从 Ant Design 样式系统迁移到 TailwindCSS + CSS 变量。 - -## 目录 - -1. [现代设计原则](#现代设计原则) -2. [老旧组件识别与现代化](#老旧组件识别与现代化) -3. [颜色系统](#颜色系统) -4. [间距系统](#间距系统) -5. [字体系统](#字体系统) -6. [阴影系统](#阴影系统) -7. [圆角系统](#圆角系统) -8. [响应式布局](#响应式布局) -9. [状态样式](#状态样式) - ---- - -## 现代设计原则 - -### 抵制过时设计模式 - -**❌ 拒绝老气 UI**: -- 粗重边框、多重边框嵌套(边框重叠) -- 灰暗沉闷的配色(缺乏活力) -- 过度装饰的渐变和重阴影 -- 拥挤无呼吸感的布局 -- 间距混乱、缺乏层级感 -- 老式的表单样式(密集排列) -- **老旧 Tabs 设计**(底部粗线、方块按钮感) -- **Element UI 风格组件**(el-tabs、el-form 等) - -**✅ 拥抱现代设计**: -- 轻量边框或无边框设计 -- 明亮有活力的色彩 -- 克制精致的阴影(shadow-sm 为主) -- 充足留白,呼吸感 -- 清晰的视觉层级(4px 间距递进) -- 现代表单(宽松布局,清晰标签) -- **现代 Tabs**(胶囊式、underline 轻量) - -### 颜色活力设计 - -**年轻感配色特征**: -```css -/* 主色:中等饱和度,避免过深或过浅 */ ---primary: oklch(0.55 0.18 254.604); /* 活力蓝,非深蓝 */ - -/* 强调色:高饱和度,用于吸引注意 */ ---accent-bright: oklch(0.65 0.22 254.604); - -/* 渐变:柔和过渡,非跳跃式 */ ---gradient-primary: linear-gradient(135deg, - oklch(0.68 0.16 254.604), - oklch(0.45 0.16 254.604) -); -``` - -**发现优秀配色时**: -1. 提取关键颜色值(oklch 格式) -2. 添加到 `style.css` 设计令牌 -3. 使用语义化命名 -4. 在 `theme.css` 中同步 shadcn 变量 - -### 间距层级感 - -``` -紧密:gap-1 (4px) - 图标与文字 -标准:gap-2 (8px) - 同组元素 -舒适:gap-3 (12px) - 表单项之间 -宽松:gap-4 (16px) - 卡片内容 -分区:gap-6 (24px) - 不同区块 -隔离:gap-8 (32px) - 模块分隔 -``` - ---- - -## 老旧组件识别与现代化 - -### 老旧 Tabs 设计 - -**❌ 拒绝的老旧模式**: -- 底部粗线指示器(2-3px 实线) -- 方块按钮感,无圆角 -- 灰色背景分割条 -- 拥挤的标签间距 - -**✅ 现代替代方案**: - -```vue - - - - - - - - - - - 标签一 - - - 标签二 - - - 内容一 - 内容二 - -``` - -### 老旧表单设计 - -**❌ 拒绝的老旧模式**: -- Label 左对齐,密集排列 -- 表单项间距过小 -- 边框重叠,视觉混乱 -- 灰暗的禁用状态 - -**✅ 现代替代方案**: - -```vue - - -
- - -

提示文字

-
- -
- - -
- -
- - -
-
-``` - -### 老旧卡片设计 - -**❌ 拒绝的老旧模式**: -- 重阴影(shadow-lg 以上) -- 多重边框嵌套 -- 灰暗的头部背景 - -**✅ 现代替代方案**: - -```vue - -
-

标题

-

描述文字

-
- - -
- -
-``` - -### 老旧按钮设计 - -**❌ 拒绝的老旧模式**: -- 方角按钮 -- 过重的渐变背景 -- 灰暗的 hover 状态 - -**✅ 现代替代方案**: - -```vue - -
- - - - - -
-``` - -### 老旧表格设计 - -**❌ 拒绝的老旧模式**: -- 斑马纹背景(过时) -- 粗重的边框线 -- 拥挤的单元格 -- 灰色的表头背景 - -**✅ 现代替代方案**: - -```vue - - - - - 列名 - - - - - 内容 - - -
-``` - -### 边框重叠问题 - -**识别边框重叠**: -- 多个卡片紧挨,边框叠加成 2px -- 表格单元格边框重叠 -- 嵌套容器边框累积 - -**解决方案**: - -```vue - -
-
- 内容 -
-
- - -
-
- 内容 -
-
- - -
-
项目1
-
项目2
-
-``` - ---- - -## 颜色系统 - -### 语义化颜色变量 - -项目使用 `oklch` 色彩空间,定义在 `theme.css` 中: - -```css -/* theme.css 中的变量 */ ---background /* 页面背景 */ ---foreground /* 主文字 */ ---card /* 卡片背景 */ ---card-foreground /* 卡片文字 */ ---popover /* 弹出层背景 */ ---popover-foreground ---primary /* 主色(品牌蓝) */ ---primary-foreground ---secondary /* 次要色 */ ---secondary-foreground ---muted /* 静音背景 */ ---muted-foreground /* 静音文字 */ ---accent /* 强调背景 */ ---accent-foreground ---destructive /* 危险色 */ ---destructive-foreground ---border /* 边框 */ ---input /* 输入框边框 */ ---ring /* 焦点环 */ -``` - -### 使用方式 - -```vue - - - -``` - -### Ant Design 颜色迁移 - -| Ant Design | shadcn/Tailwind | -|------------|-----------------| -| `@primary-color` | `var(--primary)` 或 `text-primary bg-primary` | -| `@success-color` | `text-green-500` 或自定义 | -| `@warning-color` | `text-yellow-500` 或自定义 | -| `@error-color` | `var(--destructive)` 或 `text-destructive` | -| `@text-color` | `var(--foreground)` 或 `text-foreground` | -| `@text-color-secondary` | `var(--muted-foreground)` | -| `@border-color-base` | `var(--border)` 或 `border-border` | -| `@disabled-color` | `text-muted-foreground` | - ---- - -## 间距系统 - -Tailwind 使用 4px 基准: - -| Tailwind | 像素值 | -|----------|--------| -| `p-1` / `m-1` | 4px | -| `p-2` / `m-2` | 8px | -| `p-3` / `m-3` | 12px | -| `p-4` / `m-4` | 16px | -| `p-5` / `m-5` | 20px | -| `p-6` / `m-6` | 24px | -| `p-8` / `m-8` | 32px | -| `p-10` / `m-10` | 40px | -| `p-12` / `m-12` | 48px | - -### 常见场景 - -```vue - -
- ... -
- - -
- - -
- - - - ... - ... - - - -
- ... -
-``` - ---- - -## 字体系统 - -### 字体大小 - -| Tailwind | 像素值 | 用途 | -|----------|--------|------| -| `text-xs` | 12px | 辅助文字 | -| `text-sm` | 14px | 正文、标签 | -| `text-base` | 16px | 正文 | -| `text-lg` | 18px | 小标题 | -| `text-xl` | 20px | 标题 | -| `text-2xl` | 24px | 大标题 | -| `text-3xl` | 30px | 页面标题 | - -### 字重 - -```vue - -``` - ---- - -## 阴影系统 - -定义在 `theme.css`: - -```css ---shadow-sm /* 微阴影 */ ---shadow /* 基础阴影 */ ---shadow-md /* 中等阴影 */ ---shadow-lg /* 大阴影 */ ---shadow-xl /* 超大阴影 */ -``` - -### 使用 - -```vue - -``` - ---- - -## 圆角系统 - -```vue - -``` - ---- - -## 响应式布局 - -### 断点 - -| 断点 | 最小宽度 | -|------|----------| -| `sm:` | 640px | -| `md:` | 768px | -| `lg:` | 1024px | -| `xl:` | 1280px | -| `2xl:` | 1536px | - -### 栅格迁移 - -```vue - - - ... - ... - - - -
-
...
-
...
-
-``` - ---- - -## 设计审查清单 - -迁移时检查以下问题: - -- [ ] Tabs 是否使用现代胶囊式设计? -- [ ] 表单是否有足够间距(space-y-6)? -- [ ] 卡片阴影是否克制(shadow-sm)? -- [ ] 边框是否避免重叠? -- [ ] 颜色是否有活力(非灰暗)? -- [ ] 按钮是否有圆角(rounded-lg)? -- [ ] 表格是否简洁无斑马纹? -- [ ] 整体是否有呼吸感? diff --git a/.claude/skills/docx/LICENSE.txt b/.claude/skills/docx/LICENSE.txt deleted file mode 100644 index c55ab42224..0000000000 --- a/.claude/skills/docx/LICENSE.txt +++ /dev/null @@ -1,30 +0,0 @@ -© 2025 Anthropic, PBC. All rights reserved. - -LICENSE: Use of these materials (including all code, prompts, assets, files, -and other components of this Skill) is governed by your agreement with -Anthropic regarding use of Anthropic's services. If no separate agreement -exists, use is governed by Anthropic's Consumer Terms of Service or -Commercial Terms of Service, as applicable: -https://www.anthropic.com/legal/consumer-terms -https://www.anthropic.com/legal/commercial-terms -Your applicable agreement is referred to as the "Agreement." "Services" are -as defined in the Agreement. - -ADDITIONAL RESTRICTIONS: Notwithstanding anything in the Agreement to the -contrary, users may not: - -- Extract these materials from the Services or retain copies of these - materials outside the Services -- Reproduce or copy these materials, except for temporary copies created - automatically during authorized use of the Services -- Create derivative works based on these materials -- Distribute, sublicense, or transfer these materials to any third party -- Make, offer to sell, sell, or import any inventions embodied in these - materials -- Reverse engineer, decompile, or disassemble these materials - -The receipt, viewing, or possession of these materials does not convey or -imply any license or right beyond those expressly granted above. - -Anthropic retains all right, title, and interest in these materials, -including all copyrights, patents, and other intellectual property rights. diff --git a/.claude/skills/docx/SKILL.md b/.claude/skills/docx/SKILL.md deleted file mode 100644 index 664663895b..0000000000 --- a/.claude/skills/docx/SKILL.md +++ /dev/null @@ -1,197 +0,0 @@ ---- -name: docx -description: "Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. When Claude needs to work with professional documents (.docx files) for: (1) Creating new documents, (2) Modifying or editing content, (3) Working with tracked changes, (4) Adding comments, or any other document tasks" -license: Proprietary. LICENSE.txt has complete terms ---- - -# DOCX creation, editing, and analysis - -## Overview - -A user may ask you to create, edit, or analyze the contents of a .docx file. A .docx file is essentially a ZIP archive containing XML files and other resources that you can read or edit. You have different tools and workflows available for different tasks. - -## Workflow Decision Tree - -### Reading/Analyzing Content -Use "Text extraction" or "Raw XML access" sections below - -### Creating New Document -Use "Creating a new Word document" workflow - -### Editing Existing Document -- **Your own document + simple changes** - Use "Basic OOXML editing" workflow - -- **Someone else's document** - Use **"Redlining workflow"** (recommended default) - -- **Legal, academic, business, or government docs** - Use **"Redlining workflow"** (required) - -## Reading and analyzing content - -### Text extraction -If you just need to read the text contents of a document, you should convert the document to markdown using pandoc. Pandoc provides excellent support for preserving document structure and can show tracked changes: - -```bash -# Convert document to markdown with tracked changes -pandoc --track-changes=all path-to-file.docx -o output.md -# Options: --track-changes=accept/reject/all -``` - -### Raw XML access -You need raw XML access for: comments, complex formatting, document structure, embedded media, and metadata. For any of these features, you'll need to unpack a document and read its raw XML contents. - -#### Unpacking a file -`python ooxml/scripts/unpack.py ` - -#### Key file structures -* `word/document.xml` - Main document contents -* `word/comments.xml` - Comments referenced in document.xml -* `word/media/` - Embedded images and media files -* Tracked changes use `` (insertions) and `` (deletions) tags - -## Creating a new Word document - -When creating a new Word document from scratch, use **docx-js**, which allows you to create Word documents using JavaScript/TypeScript. - -### Workflow -1. **MANDATORY - READ ENTIRE FILE**: Read [`docx-js.md`](docx-js.md) (~500 lines) completely from start to finish. **NEVER set any range limits when reading this file.** Read the full file content for detailed syntax, critical formatting rules, and best practices before proceeding with document creation. -2. Create a JavaScript/TypeScript file using Document, Paragraph, TextRun components (You can assume all dependencies are installed, but if not, refer to the dependencies section below) -3. Export as .docx using Packer.toBuffer() - -## Editing an existing Word document - -When editing an existing Word document, use the **Document library** (a Python library for OOXML manipulation). The library automatically handles infrastructure setup and provides methods for document manipulation. For complex scenarios, you can access the underlying DOM directly through the library. - -### Workflow -1. **MANDATORY - READ ENTIRE FILE**: Read [`ooxml.md`](ooxml.md) (~600 lines) completely from start to finish. **NEVER set any range limits when reading this file.** Read the full file content for the Document library API and XML patterns for directly editing document files. -2. Unpack the document: `python ooxml/scripts/unpack.py ` -3. Create and run a Python script using the Document library (see "Document Library" section in ooxml.md) -4. Pack the final document: `python ooxml/scripts/pack.py ` - -The Document library provides both high-level methods for common operations and direct DOM access for complex scenarios. - -## Redlining workflow for document review - -This workflow allows you to plan comprehensive tracked changes using markdown before implementing them in OOXML. **CRITICAL**: For complete tracked changes, you must implement ALL changes systematically. - -**Batching Strategy**: Group related changes into batches of 3-10 changes. This makes debugging manageable while maintaining efficiency. Test each batch before moving to the next. - -**Principle: Minimal, Precise Edits** -When implementing tracked changes, only mark text that actually changes. Repeating unchanged text makes edits harder to review and appears unprofessional. Break replacements into: [unchanged text] + [deletion] + [insertion] + [unchanged text]. Preserve the original run's RSID for unchanged text by extracting the `` element from the original and reusing it. - -Example - Changing "30 days" to "60 days" in a sentence: -```python -# BAD - Replaces entire sentence -'The term is 30 days.The term is 60 days.' - -# GOOD - Only marks what changed, preserves original for unchanged text -'The term is 3060 days.' -``` - -### Tracked changes workflow - -1. **Get markdown representation**: Convert document to markdown with tracked changes preserved: - ```bash - pandoc --track-changes=all path-to-file.docx -o current.md - ``` - -2. **Identify and group changes**: Review the document and identify ALL changes needed, organizing them into logical batches: - - **Location methods** (for finding changes in XML): - - Section/heading numbers (e.g., "Section 3.2", "Article IV") - - Paragraph identifiers if numbered - - Grep patterns with unique surrounding text - - Document structure (e.g., "first paragraph", "signature block") - - **DO NOT use markdown line numbers** - they don't map to XML structure - - **Batch organization** (group 3-10 related changes per batch): - - By section: "Batch 1: Section 2 amendments", "Batch 2: Section 5 updates" - - By type: "Batch 1: Date corrections", "Batch 2: Party name changes" - - By complexity: Start with simple text replacements, then tackle complex structural changes - - Sequential: "Batch 1: Pages 1-3", "Batch 2: Pages 4-6" - -3. **Read documentation and unpack**: - - **MANDATORY - READ ENTIRE FILE**: Read [`ooxml.md`](ooxml.md) (~600 lines) completely from start to finish. **NEVER set any range limits when reading this file.** Pay special attention to the "Document Library" and "Tracked Change Patterns" sections. - - **Unpack the document**: `python ooxml/scripts/unpack.py ` - - **Note the suggested RSID**: The unpack script will suggest an RSID to use for your tracked changes. Copy this RSID for use in step 4b. - -4. **Implement changes in batches**: Group changes logically (by section, by type, or by proximity) and implement them together in a single script. This approach: - - Makes debugging easier (smaller batch = easier to isolate errors) - - Allows incremental progress - - Maintains efficiency (batch size of 3-10 changes works well) - - **Suggested batch groupings:** - - By document section (e.g., "Section 3 changes", "Definitions", "Termination clause") - - By change type (e.g., "Date changes", "Party name updates", "Legal term replacements") - - By proximity (e.g., "Changes on pages 1-3", "Changes in first half of document") - - For each batch of related changes: - - **a. Map text to XML**: Grep for text in `word/document.xml` to verify how text is split across `` elements. - - **b. Create and run script**: Use `get_node` to find nodes, implement changes, then `doc.save()`. See **"Document Library"** section in ooxml.md for patterns. - - **Note**: Always grep `word/document.xml` immediately before writing a script to get current line numbers and verify text content. Line numbers change after each script run. - -5. **Pack the document**: After all batches are complete, convert the unpacked directory back to .docx: - ```bash - python ooxml/scripts/pack.py unpacked reviewed-document.docx - ``` - -6. **Final verification**: Do a comprehensive check of the complete document: - - Convert final document to markdown: - ```bash - pandoc --track-changes=all reviewed-document.docx -o verification.md - ``` - - Verify ALL changes were applied correctly: - ```bash - grep "original phrase" verification.md # Should NOT find it - grep "replacement phrase" verification.md # Should find it - ``` - - Check that no unintended changes were introduced - - -## Converting Documents to Images - -To visually analyze Word documents, convert them to images using a two-step process: - -1. **Convert DOCX to PDF**: - ```bash - soffice --headless --convert-to pdf document.docx - ``` - -2. **Convert PDF pages to JPEG images**: - ```bash - pdftoppm -jpeg -r 150 document.pdf page - ``` - This creates files like `page-1.jpg`, `page-2.jpg`, etc. - -Options: -- `-r 150`: Sets resolution to 150 DPI (adjust for quality/size balance) -- `-jpeg`: Output JPEG format (use `-png` for PNG if preferred) -- `-f N`: First page to convert (e.g., `-f 2` starts from page 2) -- `-l N`: Last page to convert (e.g., `-l 5` stops at page 5) -- `page`: Prefix for output files - -Example for specific range: -```bash -pdftoppm -jpeg -r 150 -f 2 -l 5 document.pdf page # Converts only pages 2-5 -``` - -## Code Style Guidelines -**IMPORTANT**: When generating code for DOCX operations: -- Write concise code -- Avoid verbose variable names and redundant operations -- Avoid unnecessary print statements - -## Dependencies - -Required dependencies (install if not available): - -- **pandoc**: `sudo apt-get install pandoc` (for text extraction) -- **docx**: `npm install -g docx` (for creating new documents) -- **LibreOffice**: `sudo apt-get install libreoffice` (for PDF conversion) -- **Poppler**: `sudo apt-get install poppler-utils` (for pdftoppm to convert PDF to images) -- **defusedxml**: `pip install defusedxml` (for secure XML parsing) \ No newline at end of file diff --git a/.claude/skills/docx/docx-js.md b/.claude/skills/docx/docx-js.md deleted file mode 100644 index c6d7b2ddd6..0000000000 --- a/.claude/skills/docx/docx-js.md +++ /dev/null @@ -1,350 +0,0 @@ -# DOCX Library Tutorial - -Generate .docx files with JavaScript/TypeScript. - -**Important: Read this entire document before starting.** Critical formatting rules and common pitfalls are covered throughout - skipping sections may result in corrupted files or rendering issues. - -## Setup -Assumes docx is already installed globally -If not installed: `npm install -g docx` - -```javascript -const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, ImageRun, Media, - Header, Footer, AlignmentType, PageOrientation, LevelFormat, ExternalHyperlink, - InternalHyperlink, TableOfContents, HeadingLevel, BorderStyle, WidthType, TabStopType, - TabStopPosition, UnderlineType, ShadingType, VerticalAlign, SymbolRun, PageNumber, - FootnoteReferenceRun, Footnote, PageBreak } = require('docx'); - -// Create & Save -const doc = new Document({ sections: [{ children: [/* content */] }] }); -Packer.toBuffer(doc).then(buffer => fs.writeFileSync("doc.docx", buffer)); // Node.js -Packer.toBlob(doc).then(blob => { /* download logic */ }); // Browser -``` - -## Text & Formatting -```javascript -// IMPORTANT: Never use \n for line breaks - always use separate Paragraph elements -// ❌ WRONG: new TextRun("Line 1\nLine 2") -// ✅ CORRECT: new Paragraph({ children: [new TextRun("Line 1")] }), new Paragraph({ children: [new TextRun("Line 2")] }) - -// Basic text with all formatting options -new Paragraph({ - alignment: AlignmentType.CENTER, - spacing: { before: 200, after: 200 }, - indent: { left: 720, right: 720 }, - children: [ - new TextRun({ text: "Bold", bold: true }), - new TextRun({ text: "Italic", italics: true }), - new TextRun({ text: "Underlined", underline: { type: UnderlineType.DOUBLE, color: "FF0000" } }), - new TextRun({ text: "Colored", color: "FF0000", size: 28, font: "Arial" }), // Arial default - new TextRun({ text: "Highlighted", highlight: "yellow" }), - new TextRun({ text: "Strikethrough", strike: true }), - new TextRun({ text: "x2", superScript: true }), - new TextRun({ text: "H2O", subScript: true }), - new TextRun({ text: "SMALL CAPS", smallCaps: true }), - new SymbolRun({ char: "2022", font: "Symbol" }), // Bullet • - new SymbolRun({ char: "00A9", font: "Arial" }) // Copyright © - Arial for symbols - ] -}) -``` - -## Styles & Professional Formatting - -```javascript -const doc = new Document({ - styles: { - default: { document: { run: { font: "Arial", size: 24 } } }, // 12pt default - paragraphStyles: [ - // Document title style - override built-in Title style - { id: "Title", name: "Title", basedOn: "Normal", - run: { size: 56, bold: true, color: "000000", font: "Arial" }, - paragraph: { spacing: { before: 240, after: 120 }, alignment: AlignmentType.CENTER } }, - // IMPORTANT: Override built-in heading styles by using their exact IDs - { id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true, - run: { size: 32, bold: true, color: "000000", font: "Arial" }, // 16pt - paragraph: { spacing: { before: 240, after: 240 }, outlineLevel: 0 } }, // Required for TOC - { id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true, - run: { size: 28, bold: true, color: "000000", font: "Arial" }, // 14pt - paragraph: { spacing: { before: 180, after: 180 }, outlineLevel: 1 } }, - // Custom styles use your own IDs - { id: "myStyle", name: "My Style", basedOn: "Normal", - run: { size: 28, bold: true, color: "000000" }, - paragraph: { spacing: { after: 120 }, alignment: AlignmentType.CENTER } } - ], - characterStyles: [{ id: "myCharStyle", name: "My Char Style", - run: { color: "FF0000", bold: true, underline: { type: UnderlineType.SINGLE } } }] - }, - sections: [{ - properties: { page: { margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } } }, - children: [ - new Paragraph({ heading: HeadingLevel.TITLE, children: [new TextRun("Document Title")] }), // Uses overridden Title style - new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("Heading 1")] }), // Uses overridden Heading1 style - new Paragraph({ style: "myStyle", children: [new TextRun("Custom paragraph style")] }), - new Paragraph({ children: [ - new TextRun("Normal with "), - new TextRun({ text: "custom char style", style: "myCharStyle" }) - ]}) - ] - }] -}); -``` - -**Professional Font Combinations:** -- **Arial (Headers) + Arial (Body)** - Most universally supported, clean and professional -- **Times New Roman (Headers) + Arial (Body)** - Classic serif headers with modern sans-serif body -- **Georgia (Headers) + Verdana (Body)** - Optimized for screen reading, elegant contrast - -**Key Styling Principles:** -- **Override built-in styles**: Use exact IDs like "Heading1", "Heading2", "Heading3" to override Word's built-in heading styles -- **HeadingLevel constants**: `HeadingLevel.HEADING_1` uses "Heading1" style, `HeadingLevel.HEADING_2` uses "Heading2" style, etc. -- **Include outlineLevel**: Set `outlineLevel: 0` for H1, `outlineLevel: 1` for H2, etc. to ensure TOC works correctly -- **Use custom styles** instead of inline formatting for consistency -- **Set a default font** using `styles.default.document.run.font` - Arial is universally supported -- **Establish visual hierarchy** with different font sizes (titles > headers > body) -- **Add proper spacing** with `before` and `after` paragraph spacing -- **Use colors sparingly**: Default to black (000000) and shades of gray for titles and headings (heading 1, heading 2, etc.) -- **Set consistent margins** (1440 = 1 inch is standard) - - -## Lists (ALWAYS USE PROPER LISTS - NEVER USE UNICODE BULLETS) -```javascript -// Bullets - ALWAYS use the numbering config, NOT unicode symbols -// CRITICAL: Use LevelFormat.BULLET constant, NOT the string "bullet" -const doc = new Document({ - numbering: { - config: [ - { reference: "bullet-list", - levels: [{ level: 0, format: LevelFormat.BULLET, text: "•", alignment: AlignmentType.LEFT, - style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] }, - { reference: "first-numbered-list", - levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT, - style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] }, - { reference: "second-numbered-list", // Different reference = restarts at 1 - levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT, - style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] } - ] - }, - sections: [{ - children: [ - // Bullet list items - new Paragraph({ numbering: { reference: "bullet-list", level: 0 }, - children: [new TextRun("First bullet point")] }), - new Paragraph({ numbering: { reference: "bullet-list", level: 0 }, - children: [new TextRun("Second bullet point")] }), - // Numbered list items - new Paragraph({ numbering: { reference: "first-numbered-list", level: 0 }, - children: [new TextRun("First numbered item")] }), - new Paragraph({ numbering: { reference: "first-numbered-list", level: 0 }, - children: [new TextRun("Second numbered item")] }), - // ⚠️ CRITICAL: Different reference = INDEPENDENT list that restarts at 1 - // Same reference = CONTINUES previous numbering - new Paragraph({ numbering: { reference: "second-numbered-list", level: 0 }, - children: [new TextRun("Starts at 1 again (because different reference)")] }) - ] - }] -}); - -// ⚠️ CRITICAL NUMBERING RULE: Each reference creates an INDEPENDENT numbered list -// - Same reference = continues numbering (1, 2, 3... then 4, 5, 6...) -// - Different reference = restarts at 1 (1, 2, 3... then 1, 2, 3...) -// Use unique reference names for each separate numbered section! - -// ⚠️ CRITICAL: NEVER use unicode bullets - they create fake lists that don't work properly -// new TextRun("• Item") // WRONG -// new SymbolRun({ char: "2022" }) // WRONG -// ✅ ALWAYS use numbering config with LevelFormat.BULLET for real Word lists -``` - -## Tables -```javascript -// Complete table with margins, borders, headers, and bullet points -const tableBorder = { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" }; -const cellBorders = { top: tableBorder, bottom: tableBorder, left: tableBorder, right: tableBorder }; - -new Table({ - columnWidths: [4680, 4680], // ⚠️ CRITICAL: Set column widths at table level - values in DXA (twentieths of a point) - margins: { top: 100, bottom: 100, left: 180, right: 180 }, // Set once for all cells - rows: [ - new TableRow({ - tableHeader: true, - children: [ - new TableCell({ - borders: cellBorders, - width: { size: 4680, type: WidthType.DXA }, // ALSO set width on each cell - // ⚠️ CRITICAL: Always use ShadingType.CLEAR to prevent black backgrounds in Word. - shading: { fill: "D5E8F0", type: ShadingType.CLEAR }, - verticalAlign: VerticalAlign.CENTER, - children: [new Paragraph({ - alignment: AlignmentType.CENTER, - children: [new TextRun({ text: "Header", bold: true, size: 22 })] - })] - }), - new TableCell({ - borders: cellBorders, - width: { size: 4680, type: WidthType.DXA }, // ALSO set width on each cell - shading: { fill: "D5E8F0", type: ShadingType.CLEAR }, - children: [new Paragraph({ - alignment: AlignmentType.CENTER, - children: [new TextRun({ text: "Bullet Points", bold: true, size: 22 })] - })] - }) - ] - }), - new TableRow({ - children: [ - new TableCell({ - borders: cellBorders, - width: { size: 4680, type: WidthType.DXA }, // ALSO set width on each cell - children: [new Paragraph({ children: [new TextRun("Regular data")] })] - }), - new TableCell({ - borders: cellBorders, - width: { size: 4680, type: WidthType.DXA }, // ALSO set width on each cell - children: [ - new Paragraph({ - numbering: { reference: "bullet-list", level: 0 }, - children: [new TextRun("First bullet point")] - }), - new Paragraph({ - numbering: { reference: "bullet-list", level: 0 }, - children: [new TextRun("Second bullet point")] - }) - ] - }) - ] - }) - ] -}) -``` - -**IMPORTANT: Table Width & Borders** -- Use BOTH `columnWidths: [width1, width2, ...]` array AND `width: { size: X, type: WidthType.DXA }` on each cell -- Values in DXA (twentieths of a point): 1440 = 1 inch, Letter usable width = 9360 DXA (with 1" margins) -- Apply borders to individual `TableCell` elements, NOT the `Table` itself - -**Precomputed Column Widths (Letter size with 1" margins = 9360 DXA total):** -- **2 columns:** `columnWidths: [4680, 4680]` (equal width) -- **3 columns:** `columnWidths: [3120, 3120, 3120]` (equal width) - -## Links & Navigation -```javascript -// TOC (requires headings) - CRITICAL: Use HeadingLevel only, NOT custom styles -// ❌ WRONG: new Paragraph({ heading: HeadingLevel.HEADING_1, style: "customHeader", children: [new TextRun("Title")] }) -// ✅ CORRECT: new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("Title")] }) -new TableOfContents("Table of Contents", { hyperlink: true, headingStyleRange: "1-3" }), - -// External link -new Paragraph({ - children: [new ExternalHyperlink({ - children: [new TextRun({ text: "Google", style: "Hyperlink" })], - link: "https://www.google.com" - })] -}), - -// Internal link & bookmark -new Paragraph({ - children: [new InternalHyperlink({ - children: [new TextRun({ text: "Go to Section", style: "Hyperlink" })], - anchor: "section1" - })] -}), -new Paragraph({ - children: [new TextRun("Section Content")], - bookmark: { id: "section1", name: "section1" } -}), -``` - -## Images & Media -```javascript -// Basic image with sizing & positioning -// CRITICAL: Always specify 'type' parameter - it's REQUIRED for ImageRun -new Paragraph({ - alignment: AlignmentType.CENTER, - children: [new ImageRun({ - type: "png", // NEW REQUIREMENT: Must specify image type (png, jpg, jpeg, gif, bmp, svg) - data: fs.readFileSync("image.png"), - transformation: { width: 200, height: 150, rotation: 0 }, // rotation in degrees - altText: { title: "Logo", description: "Company logo", name: "Name" } // IMPORTANT: All three fields are required - })] -}) -``` - -## Page Breaks -```javascript -// Manual page break -new Paragraph({ children: [new PageBreak()] }), - -// Page break before paragraph -new Paragraph({ - pageBreakBefore: true, - children: [new TextRun("This starts on a new page")] -}) - -// ⚠️ CRITICAL: NEVER use PageBreak standalone - it will create invalid XML that Word cannot open -// ❌ WRONG: new PageBreak() -// ✅ CORRECT: new Paragraph({ children: [new PageBreak()] }) -``` - -## Headers/Footers & Page Setup -```javascript -const doc = new Document({ - sections: [{ - properties: { - page: { - margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 }, // 1440 = 1 inch - size: { orientation: PageOrientation.LANDSCAPE }, - pageNumbers: { start: 1, formatType: "decimal" } // "upperRoman", "lowerRoman", "upperLetter", "lowerLetter" - } - }, - headers: { - default: new Header({ children: [new Paragraph({ - alignment: AlignmentType.RIGHT, - children: [new TextRun("Header Text")] - })] }) - }, - footers: { - default: new Footer({ children: [new Paragraph({ - alignment: AlignmentType.CENTER, - children: [new TextRun("Page "), new TextRun({ children: [PageNumber.CURRENT] }), new TextRun(" of "), new TextRun({ children: [PageNumber.TOTAL_PAGES] })] - })] }) - }, - children: [/* content */] - }] -}); -``` - -## Tabs -```javascript -new Paragraph({ - tabStops: [ - { type: TabStopType.LEFT, position: TabStopPosition.MAX / 4 }, - { type: TabStopType.CENTER, position: TabStopPosition.MAX / 2 }, - { type: TabStopType.RIGHT, position: TabStopPosition.MAX * 3 / 4 } - ], - children: [new TextRun("Left\tCenter\tRight")] -}) -``` - -## Constants & Quick Reference -- **Underlines:** `SINGLE`, `DOUBLE`, `WAVY`, `DASH` -- **Borders:** `SINGLE`, `DOUBLE`, `DASHED`, `DOTTED` -- **Numbering:** `DECIMAL` (1,2,3), `UPPER_ROMAN` (I,II,III), `LOWER_LETTER` (a,b,c) -- **Tabs:** `LEFT`, `CENTER`, `RIGHT`, `DECIMAL` -- **Symbols:** `"2022"` (•), `"00A9"` (©), `"00AE"` (®), `"2122"` (™), `"00B0"` (°), `"F070"` (✓), `"F0FC"` (✗) - -## Critical Issues & Common Mistakes -- **CRITICAL: PageBreak must ALWAYS be inside a Paragraph** - standalone PageBreak creates invalid XML that Word cannot open -- **ALWAYS use ShadingType.CLEAR for table cell shading** - Never use ShadingType.SOLID (causes black background). -- Measurements in DXA (1440 = 1 inch) | Each table cell needs ≥1 Paragraph | TOC requires HeadingLevel styles only -- **ALWAYS use custom styles** with Arial font for professional appearance and proper visual hierarchy -- **ALWAYS set a default font** using `styles.default.document.run.font` - Arial recommended -- **ALWAYS use columnWidths array for tables** + individual cell widths for compatibility -- **NEVER use unicode symbols for bullets** - always use proper numbering configuration with `LevelFormat.BULLET` constant (NOT the string "bullet") -- **NEVER use \n for line breaks anywhere** - always use separate Paragraph elements for each line -- **ALWAYS use TextRun objects within Paragraph children** - never use text property directly on Paragraph -- **CRITICAL for images**: ImageRun REQUIRES `type` parameter - always specify "png", "jpg", "jpeg", "gif", "bmp", or "svg" -- **CRITICAL for bullets**: Must use `LevelFormat.BULLET` constant, not string "bullet", and include `text: "•"` for the bullet character -- **CRITICAL for numbering**: Each numbering reference creates an INDEPENDENT list. Same reference = continues numbering (1,2,3 then 4,5,6). Different reference = restarts at 1 (1,2,3 then 1,2,3). Use unique reference names for each separate numbered section! -- **CRITICAL for TOC**: When using TableOfContents, headings must use HeadingLevel ONLY - do NOT add custom styles to heading paragraphs or TOC will break -- **Tables**: Set `columnWidths` array + individual cell widths, apply borders to cells not table -- **Set table margins at TABLE level** for consistent cell padding (avoids repetition per cell) \ No newline at end of file diff --git a/.claude/skills/docx/ooxml.md b/.claude/skills/docx/ooxml.md deleted file mode 100644 index 7677e7b836..0000000000 --- a/.claude/skills/docx/ooxml.md +++ /dev/null @@ -1,610 +0,0 @@ -# Office Open XML Technical Reference - -**Important: Read this entire document before starting.** This document covers: -- [Technical Guidelines](#technical-guidelines) - Schema compliance rules and validation requirements -- [Document Content Patterns](#document-content-patterns) - XML patterns for headings, lists, tables, formatting, etc. -- [Document Library (Python)](#document-library-python) - Recommended approach for OOXML manipulation with automatic infrastructure setup -- [Tracked Changes (Redlining)](#tracked-changes-redlining) - XML patterns for implementing tracked changes - -## Technical Guidelines - -### Schema Compliance -- **Element ordering in ``**: ``, ``, ``, ``, `` -- **Whitespace**: Add `xml:space='preserve'` to `` elements with leading/trailing spaces -- **Unicode**: Escape characters in ASCII content: `"` becomes `“` - - **Character encoding reference**: Curly quotes `""` become `“”`, apostrophe `'` becomes `’`, em-dash `—` becomes `—` -- **Tracked changes**: Use `` and `` tags with `w:author="Claude"` outside `` elements - - **Critical**: `` closes with ``, `` closes with `` - never mix - - **RSIDs must be 8-digit hex**: Use values like `00AB1234` (only 0-9, A-F characters) - - **trackRevisions placement**: Add `` after `` in settings.xml -- **Images**: Add to `word/media/`, reference in `document.xml`, set dimensions to prevent overflow - -## Document Content Patterns - -### Basic Structure -```xml - - Text content - -``` - -### Headings and Styles -```xml - - - - - - Document Title - - - - - Section Heading - -``` - -### Text Formatting -```xml - -Bold - -Italic - -Underlined - -Highlighted -``` - -### Lists -```xml - - - - - - - - First item - - - - - - - - - - New list item 1 - - - - - - - - - - - Bullet item - -``` - -### Tables -```xml - - - - - - - - - - - - Cell 1 - - - - Cell 2 - - - -``` - -### Layout -```xml - - - - - - - - - - - - New Section Title - - - - - - - - - - Centered text - - - - - - - - Monospace text - - - - - - - This text is Courier New - - and this text uses default font - -``` - -## File Updates - -When adding content, update these files: - -**`word/_rels/document.xml.rels`:** -```xml - - -``` - -**`[Content_Types].xml`:** -```xml - - -``` - -### Images -**CRITICAL**: Calculate dimensions to prevent page overflow and maintain aspect ratio. - -```xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -### Links (Hyperlinks) - -**IMPORTANT**: All hyperlinks (both internal and external) require the Hyperlink style to be defined in styles.xml. Without this style, links will look like regular text instead of blue underlined clickable links. - -**External Links:** -```xml - - - - - Link Text - - - - - -``` - -**Internal Links:** - -```xml - - - - - Link Text - - - - - -Target content - -``` - -**Hyperlink Style (required in styles.xml):** -```xml - - - - - - - - - - -``` - -## Document Library (Python) - -Use the Document class from `scripts/document.py` for all tracked changes and comments. It automatically handles infrastructure setup (people.xml, RSIDs, settings.xml, comment files, relationships, content types). Only use direct XML manipulation for complex scenarios not supported by the library. - -**Working with Unicode and Entities:** -- **Searching**: Both entity notation and Unicode characters work - `contains="“Company"` and `contains="\u201cCompany"` find the same text -- **Replacing**: Use either entities (`“`) or Unicode (`\u201c`) - both work and will be converted appropriately based on the file's encoding (ascii → entities, utf-8 → Unicode) - -### Initialization - -**Find the docx skill root** (directory containing `scripts/` and `ooxml/`): -```bash -# Search for document.py to locate the skill root -# Note: /mnt/skills is used here as an example; check your context for the actual location -find /mnt/skills -name "document.py" -path "*/docx/scripts/*" 2>/dev/null | head -1 -# Example output: /mnt/skills/docx/scripts/document.py -# Skill root is: /mnt/skills/docx -``` - -**Run your script with PYTHONPATH** set to the docx skill root: -```bash -PYTHONPATH=/mnt/skills/docx python your_script.py -``` - -**In your script**, import from the skill root: -```python -from scripts.document import Document, DocxXMLEditor - -# Basic initialization (automatically creates temp copy and sets up infrastructure) -doc = Document('unpacked') - -# Customize author and initials -doc = Document('unpacked', author="John Doe", initials="JD") - -# Enable track revisions mode -doc = Document('unpacked', track_revisions=True) - -# Specify custom RSID (auto-generated if not provided) -doc = Document('unpacked', rsid="07DC5ECB") -``` - -### Creating Tracked Changes - -**CRITICAL**: Only mark text that actually changes. Keep ALL unchanged text outside ``/`` tags. Marking unchanged text makes edits unprofessional and harder to review. - -**Attribute Handling**: The Document class auto-injects attributes (w:id, w:date, w:rsidR, w:rsidDel, w16du:dateUtc, xml:space) into new elements. When preserving unchanged text from the original document, copy the original `` element with its existing attributes to maintain document integrity. - -**Method Selection Guide**: -- **Adding your own changes to regular text**: Use `replace_node()` with ``/`` tags, or `suggest_deletion()` for removing entire `` or `` elements -- **Partially modifying another author's tracked change**: Use `replace_node()` to nest your changes inside their ``/`` -- **Completely rejecting another author's insertion**: Use `revert_insertion()` on the `` element (NOT `suggest_deletion()`) -- **Completely rejecting another author's deletion**: Use `revert_deletion()` on the `` element to restore deleted content using tracked changes - -```python -# Minimal edit - change one word: "The report is monthly" → "The report is quarterly" -# Original: The report is monthly -node = doc["word/document.xml"].get_node(tag="w:r", contains="The report is monthly") -rpr = tags[0].toxml() if (tags := node.getElementsByTagName("w:rPr")) else "" -replacement = f'{rpr}The report is {rpr}monthly{rpr}quarterly' -doc["word/document.xml"].replace_node(node, replacement) - -# Minimal edit - change number: "within 30 days" → "within 45 days" -# Original: within 30 days -node = doc["word/document.xml"].get_node(tag="w:r", contains="within 30 days") -rpr = tags[0].toxml() if (tags := node.getElementsByTagName("w:rPr")) else "" -replacement = f'{rpr}within {rpr}30{rpr}45{rpr} days' -doc["word/document.xml"].replace_node(node, replacement) - -# Complete replacement - preserve formatting even when replacing all text -node = doc["word/document.xml"].get_node(tag="w:r", contains="apple") -rpr = tags[0].toxml() if (tags := node.getElementsByTagName("w:rPr")) else "" -replacement = f'{rpr}apple{rpr}banana orange' -doc["word/document.xml"].replace_node(node, replacement) - -# Insert new content (no attributes needed - auto-injected) -node = doc["word/document.xml"].get_node(tag="w:r", contains="existing text") -doc["word/document.xml"].insert_after(node, 'new text') - -# Partially delete another author's insertion -# Original: quarterly financial report -# Goal: Delete only "financial" to make it "quarterly report" -node = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "5"}) -# IMPORTANT: Preserve w:author="Jane Smith" on the outer to maintain authorship -replacement = ''' - quarterly - financial - report -''' -doc["word/document.xml"].replace_node(node, replacement) - -# Change part of another author's insertion -# Original: in silence, safe and sound -# Goal: Change "safe and sound" to "soft and unbound" -node = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "8"}) -replacement = f''' - in silence, - - - soft and unbound - - - safe and sound -''' -doc["word/document.xml"].replace_node(node, replacement) - -# Delete entire run (use only when deleting all content; use replace_node for partial deletions) -node = doc["word/document.xml"].get_node(tag="w:r", contains="text to delete") -doc["word/document.xml"].suggest_deletion(node) - -# Delete entire paragraph (in-place, handles both regular and numbered list paragraphs) -para = doc["word/document.xml"].get_node(tag="w:p", contains="paragraph to delete") -doc["word/document.xml"].suggest_deletion(para) - -# Add new numbered list item -target_para = doc["word/document.xml"].get_node(tag="w:p", contains="existing list item") -pPr = tags[0].toxml() if (tags := target_para.getElementsByTagName("w:pPr")) else "" -new_item = f'{pPr}New item' -tracked_para = DocxXMLEditor.suggest_paragraph(new_item) -doc["word/document.xml"].insert_after(target_para, tracked_para) -# Optional: add spacing paragraph before content for better visual separation -# spacing = DocxXMLEditor.suggest_paragraph('') -# doc["word/document.xml"].insert_after(target_para, spacing + tracked_para) -``` - -### Adding Comments - -```python -# Add comment spanning two existing tracked changes -# Note: w:id is auto-generated. Only search by w:id if you know it from XML inspection -start_node = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "1"}) -end_node = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "2"}) -doc.add_comment(start=start_node, end=end_node, text="Explanation of this change") - -# Add comment on a paragraph -para = doc["word/document.xml"].get_node(tag="w:p", contains="paragraph text") -doc.add_comment(start=para, end=para, text="Comment on this paragraph") - -# Add comment on newly created tracked change -# First create the tracked change -node = doc["word/document.xml"].get_node(tag="w:r", contains="old") -new_nodes = doc["word/document.xml"].replace_node( - node, - 'oldnew' -) -# Then add comment on the newly created elements -# new_nodes[0] is the , new_nodes[1] is the -doc.add_comment(start=new_nodes[0], end=new_nodes[1], text="Changed old to new per requirements") - -# Reply to existing comment -doc.reply_to_comment(parent_comment_id=0, text="I agree with this change") -``` - -### Rejecting Tracked Changes - -**IMPORTANT**: Use `revert_insertion()` to reject insertions and `revert_deletion()` to restore deletions using tracked changes. Use `suggest_deletion()` only for regular unmarked content. - -```python -# Reject insertion (wraps it in deletion) -# Use this when another author inserted text that you want to delete -ins = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "5"}) -nodes = doc["word/document.xml"].revert_insertion(ins) # Returns [ins] - -# Reject deletion (creates insertion to restore deleted content) -# Use this when another author deleted text that you want to restore -del_elem = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "3"}) -nodes = doc["word/document.xml"].revert_deletion(del_elem) # Returns [del_elem, new_ins] - -# Reject all insertions in a paragraph -para = doc["word/document.xml"].get_node(tag="w:p", contains="paragraph text") -nodes = doc["word/document.xml"].revert_insertion(para) # Returns [para] - -# Reject all deletions in a paragraph -para = doc["word/document.xml"].get_node(tag="w:p", contains="paragraph text") -nodes = doc["word/document.xml"].revert_deletion(para) # Returns [para] -``` - -### Inserting Images - -**CRITICAL**: The Document class works with a temporary copy at `doc.unpacked_path`. Always copy images to this temp directory, not the original unpacked folder. - -```python -from PIL import Image -import shutil, os - -# Initialize document first -doc = Document('unpacked') - -# Copy image and calculate full-width dimensions with aspect ratio -media_dir = os.path.join(doc.unpacked_path, 'word/media') -os.makedirs(media_dir, exist_ok=True) -shutil.copy('image.png', os.path.join(media_dir, 'image1.png')) -img = Image.open(os.path.join(media_dir, 'image1.png')) -width_emus = int(6.5 * 914400) # 6.5" usable width, 914400 EMUs/inch -height_emus = int(width_emus * img.size[1] / img.size[0]) - -# Add relationship and content type -rels_editor = doc['word/_rels/document.xml.rels'] -next_rid = rels_editor.get_next_rid() -rels_editor.append_to(rels_editor.dom.documentElement, - f'') -doc['[Content_Types].xml'].append_to(doc['[Content_Types].xml'].dom.documentElement, - '') - -# Insert image -node = doc["word/document.xml"].get_node(tag="w:p", line_number=100) -doc["word/document.xml"].insert_after(node, f''' - - - - - - - - - - - - - - - - - -''') -``` - -### Getting Nodes - -```python -# By text content -node = doc["word/document.xml"].get_node(tag="w:p", contains="specific text") - -# By line range -para = doc["word/document.xml"].get_node(tag="w:p", line_number=range(100, 150)) - -# By attributes -node = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "1"}) - -# By exact line number (must be line number where tag opens) -para = doc["word/document.xml"].get_node(tag="w:p", line_number=42) - -# Combine filters -node = doc["word/document.xml"].get_node(tag="w:r", line_number=range(40, 60), contains="text") - -# Disambiguate when text appears multiple times - add line_number range -node = doc["word/document.xml"].get_node(tag="w:r", contains="Section", line_number=range(2400, 2500)) -``` - -### Saving - -```python -# Save with automatic validation (copies back to original directory) -doc.save() # Validates by default, raises error if validation fails - -# Save to different location -doc.save('modified-unpacked') - -# Skip validation (debugging only - needing this in production indicates XML issues) -doc.save(validate=False) -``` - -### Direct DOM Manipulation - -For complex scenarios not covered by the library: - -```python -# Access any XML file -editor = doc["word/document.xml"] -editor = doc["word/comments.xml"] - -# Direct DOM access (defusedxml.minidom.Document) -node = doc["word/document.xml"].get_node(tag="w:p", line_number=5) -parent = node.parentNode -parent.removeChild(node) -parent.appendChild(node) # Move to end - -# General document manipulation (without tracked changes) -old_node = doc["word/document.xml"].get_node(tag="w:p", contains="original text") -doc["word/document.xml"].replace_node(old_node, "replacement text") - -# Multiple insertions - use return value to maintain order -node = doc["word/document.xml"].get_node(tag="w:r", line_number=100) -nodes = doc["word/document.xml"].insert_after(node, "A") -nodes = doc["word/document.xml"].insert_after(nodes[-1], "B") -nodes = doc["word/document.xml"].insert_after(nodes[-1], "C") -# Results in: original_node, A, B, C -``` - -## Tracked Changes (Redlining) - -**Use the Document class above for all tracked changes.** The patterns below are for reference when constructing replacement XML strings. - -### Validation Rules -The validator checks that the document text matches the original after reverting Claude's changes. This means: -- **NEVER modify text inside another author's `` or `` tags** -- **ALWAYS use nested deletions** to remove another author's insertions -- **Every edit must be properly tracked** with `` or `` tags - -### Tracked Change Patterns - -**CRITICAL RULES**: -1. Never modify the content inside another author's tracked changes. Always use nested deletions. -2. **XML Structure**: Always place `` and `` at paragraph level containing complete `` elements. Never nest inside `` elements - this creates invalid XML that breaks document processing. - -**Text Insertion:** -```xml - - - inserted text - - -``` - -**Text Deletion:** -```xml - - - deleted text - - -``` - -**Deleting Another Author's Insertion (MUST use nested structure):** -```xml - - - - monthly - - - - weekly - -``` - -**Restoring Another Author's Deletion:** -```xml - - - within 30 days - - - within 30 days - -``` \ No newline at end of file diff --git a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd b/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd deleted file mode 100644 index 6454ef9a94..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +++ /dev/null @@ -1,1499 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd b/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd deleted file mode 100644 index afa4f463e3..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd b/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd deleted file mode 100644 index 64e66b8abd..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +++ /dev/null @@ -1,1085 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd b/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd deleted file mode 100644 index 687eea8297..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd b/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd deleted file mode 100644 index 6ac81b06b7..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +++ /dev/null @@ -1,3081 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd b/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd deleted file mode 100644 index 1dbf05140d..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd b/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd deleted file mode 100644 index f1af17db4e..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +++ /dev/null @@ -1,185 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd b/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd deleted file mode 100644 index 0a185ab6ed..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +++ /dev/null @@ -1,287 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd b/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd deleted file mode 100644 index 14ef488865..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +++ /dev/null @@ -1,1676 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd b/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd deleted file mode 100644 index c20f3bf147..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd b/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd deleted file mode 100644 index ac60252262..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd b/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd deleted file mode 100644 index 424b8ba8d1..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +++ /dev/null @@ -1,174 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd b/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd deleted file mode 100644 index 2bddce2921..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd b/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd deleted file mode 100644 index 8a8c18ba2d..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd b/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd deleted file mode 100644 index 5c42706a0d..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd b/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd deleted file mode 100644 index 853c341c87..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd b/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd deleted file mode 100644 index da835ee82d..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +++ /dev/null @@ -1,195 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd b/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd deleted file mode 100644 index 87ad2658fa..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +++ /dev/null @@ -1,582 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd b/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd deleted file mode 100644 index 9e86f1b2be..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd b/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd deleted file mode 100644 index d0be42e757..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +++ /dev/null @@ -1,4439 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd b/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd deleted file mode 100644 index 8821dd183c..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +++ /dev/null @@ -1,570 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd b/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd deleted file mode 100644 index ca2575c753..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +++ /dev/null @@ -1,509 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd b/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd deleted file mode 100644 index dd079e603f..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd b/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd deleted file mode 100644 index 3dd6cf625a..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd b/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd deleted file mode 100644 index f1041e34ef..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd b/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd deleted file mode 100644 index 9c5b7a6334..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +++ /dev/null @@ -1,3646 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd b/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd deleted file mode 100644 index 0f13678d80..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - See http://www.w3.org/XML/1998/namespace.html and - http://www.w3.org/TR/REC-xml for information about this namespace. - - This schema document describes the XML namespace, in a form - suitable for import by other schema documents. - - Note that local names in this namespace are intended to be defined - only by the World Wide Web Consortium or its subgroups. The - following names are currently defined in this namespace and should - not be used with conflicting semantics by any Working Group, - specification, or document instance: - - base (as an attribute name): denotes an attribute whose value - provides a URI to be used as the base for interpreting any - relative URIs in the scope of the element on which it - appears; its value is inherited. This name is reserved - by virtue of its definition in the XML Base specification. - - lang (as an attribute name): denotes an attribute whose value - is a language code for the natural language of the content of - any element; its value is inherited. This name is reserved - by virtue of its definition in the XML specification. - - space (as an attribute name): denotes an attribute whose - value is a keyword indicating what whitespace processing - discipline is intended for the content of the element; its - value is inherited. This name is reserved by virtue of its - definition in the XML specification. - - Father (in any context at all): denotes Jon Bosak, the chair of - the original XML Working Group. This name is reserved by - the following decision of the W3C XML Plenary and - XML Coordination groups: - - In appreciation for his vision, leadership and dedication - the W3C XML Plenary on this 10th day of February, 2000 - reserves for Jon Bosak in perpetuity the XML name - xml:Father - - - - - This schema defines attributes and an attribute group - suitable for use by - schemas wishing to allow xml:base, xml:lang or xml:space attributes - on elements they define. - - To enable this, such a schema must import this schema - for the XML namespace, e.g. as follows: - <schema . . .> - . . . - <import namespace="http://www.w3.org/XML/1998/namespace" - schemaLocation="http://www.w3.org/2001/03/xml.xsd"/> - - Subsequently, qualified reference to any of the attributes - or the group defined below will have the desired effect, e.g. - - <type . . .> - . . . - <attributeGroup ref="xml:specialAttrs"/> - - will define a type which will schema-validate an instance - element with any of those attributes - - - - In keeping with the XML Schema WG's standard versioning - policy, this schema document will persist at - http://www.w3.org/2001/03/xml.xsd. - At the date of issue it can also be found at - http://www.w3.org/2001/xml.xsd. - The schema document at that URI may however change in the future, - in order to remain compatible with the latest version of XML Schema - itself. In other words, if the XML Schema namespace changes, the version - of this document at - http://www.w3.org/2001/xml.xsd will change - accordingly; the version at - http://www.w3.org/2001/03/xml.xsd will not change. - - - - - - In due course, we should install the relevant ISO 2- and 3-letter - codes as the enumerated possible values . . . - - - - - - - - - - - - - - - See http://www.w3.org/TR/xmlbase/ for - information about this attribute. - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd b/.claude/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd deleted file mode 100644 index a6de9d2733..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd b/.claude/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd deleted file mode 100644 index 10e978b661..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd b/.claude/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd deleted file mode 100644 index 4248bf7a39..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd b/.claude/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd deleted file mode 100644 index 5649746712..0000000000 --- a/.claude/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/mce/mc.xsd b/.claude/skills/docx/ooxml/schemas/mce/mc.xsd deleted file mode 100644 index ef725457cf..0000000000 --- a/.claude/skills/docx/ooxml/schemas/mce/mc.xsd +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/microsoft/wml-2010.xsd b/.claude/skills/docx/ooxml/schemas/microsoft/wml-2010.xsd deleted file mode 100644 index f65f777730..0000000000 --- a/.claude/skills/docx/ooxml/schemas/microsoft/wml-2010.xsd +++ /dev/null @@ -1,560 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/microsoft/wml-2012.xsd b/.claude/skills/docx/ooxml/schemas/microsoft/wml-2012.xsd deleted file mode 100644 index 6b00755a9a..0000000000 --- a/.claude/skills/docx/ooxml/schemas/microsoft/wml-2012.xsd +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/microsoft/wml-2018.xsd b/.claude/skills/docx/ooxml/schemas/microsoft/wml-2018.xsd deleted file mode 100644 index f321d333a5..0000000000 --- a/.claude/skills/docx/ooxml/schemas/microsoft/wml-2018.xsd +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/microsoft/wml-cex-2018.xsd b/.claude/skills/docx/ooxml/schemas/microsoft/wml-cex-2018.xsd deleted file mode 100644 index 364c6a9b8d..0000000000 --- a/.claude/skills/docx/ooxml/schemas/microsoft/wml-cex-2018.xsd +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/microsoft/wml-cid-2016.xsd b/.claude/skills/docx/ooxml/schemas/microsoft/wml-cid-2016.xsd deleted file mode 100644 index fed9d15b7f..0000000000 --- a/.claude/skills/docx/ooxml/schemas/microsoft/wml-cid-2016.xsd +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/.claude/skills/docx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd b/.claude/skills/docx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd deleted file mode 100644 index 680cf15400..0000000000 --- a/.claude/skills/docx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/.claude/skills/docx/ooxml/schemas/microsoft/wml-symex-2015.xsd b/.claude/skills/docx/ooxml/schemas/microsoft/wml-symex-2015.xsd deleted file mode 100644 index 89ada90837..0000000000 --- a/.claude/skills/docx/ooxml/schemas/microsoft/wml-symex-2015.xsd +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/.claude/skills/docx/ooxml/scripts/pack.py b/.claude/skills/docx/ooxml/scripts/pack.py deleted file mode 100644 index 68bc0886f6..0000000000 --- a/.claude/skills/docx/ooxml/scripts/pack.py +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env python3 -""" -Tool to pack a directory into a .docx, .pptx, or .xlsx file with XML formatting undone. - -Example usage: - python pack.py [--force] -""" - -import argparse -import shutil -import subprocess -import sys -import tempfile -import defusedxml.minidom -import zipfile -from pathlib import Path - - -def main(): - parser = argparse.ArgumentParser(description="Pack a directory into an Office file") - parser.add_argument("input_directory", help="Unpacked Office document directory") - parser.add_argument("output_file", help="Output Office file (.docx/.pptx/.xlsx)") - parser.add_argument("--force", action="store_true", help="Skip validation") - args = parser.parse_args() - - try: - success = pack_document( - args.input_directory, args.output_file, validate=not args.force - ) - - # Show warning if validation was skipped - if args.force: - print("Warning: Skipped validation, file may be corrupt", file=sys.stderr) - # Exit with error if validation failed - elif not success: - print("Contents would produce a corrupt file.", file=sys.stderr) - print("Please validate XML before repacking.", file=sys.stderr) - print("Use --force to skip validation and pack anyway.", file=sys.stderr) - sys.exit(1) - - except ValueError as e: - sys.exit(f"Error: {e}") - - -def pack_document(input_dir, output_file, validate=False): - """Pack a directory into an Office file (.docx/.pptx/.xlsx). - - Args: - input_dir: Path to unpacked Office document directory - output_file: Path to output Office file - validate: If True, validates with soffice (default: False) - - Returns: - bool: True if successful, False if validation failed - """ - input_dir = Path(input_dir) - output_file = Path(output_file) - - if not input_dir.is_dir(): - raise ValueError(f"{input_dir} is not a directory") - if output_file.suffix.lower() not in {".docx", ".pptx", ".xlsx"}: - raise ValueError(f"{output_file} must be a .docx, .pptx, or .xlsx file") - - # Work in temporary directory to avoid modifying original - with tempfile.TemporaryDirectory() as temp_dir: - temp_content_dir = Path(temp_dir) / "content" - shutil.copytree(input_dir, temp_content_dir) - - # Process XML files to remove pretty-printing whitespace - for pattern in ["*.xml", "*.rels"]: - for xml_file in temp_content_dir.rglob(pattern): - condense_xml(xml_file) - - # Create final Office file as zip archive - output_file.parent.mkdir(parents=True, exist_ok=True) - with zipfile.ZipFile(output_file, "w", zipfile.ZIP_DEFLATED) as zf: - for f in temp_content_dir.rglob("*"): - if f.is_file(): - zf.write(f, f.relative_to(temp_content_dir)) - - # Validate if requested - if validate: - if not validate_document(output_file): - output_file.unlink() # Delete the corrupt file - return False - - return True - - -def validate_document(doc_path): - """Validate document by converting to HTML with soffice.""" - # Determine the correct filter based on file extension - match doc_path.suffix.lower(): - case ".docx": - filter_name = "html:HTML" - case ".pptx": - filter_name = "html:impress_html_Export" - case ".xlsx": - filter_name = "html:HTML (StarCalc)" - - with tempfile.TemporaryDirectory() as temp_dir: - try: - result = subprocess.run( - [ - "soffice", - "--headless", - "--convert-to", - filter_name, - "--outdir", - temp_dir, - str(doc_path), - ], - capture_output=True, - timeout=10, - text=True, - ) - if not (Path(temp_dir) / f"{doc_path.stem}.html").exists(): - error_msg = result.stderr.strip() or "Document validation failed" - print(f"Validation error: {error_msg}", file=sys.stderr) - return False - return True - except FileNotFoundError: - print("Warning: soffice not found. Skipping validation.", file=sys.stderr) - return True - except subprocess.TimeoutExpired: - print("Validation error: Timeout during conversion", file=sys.stderr) - return False - except Exception as e: - print(f"Validation error: {e}", file=sys.stderr) - return False - - -def condense_xml(xml_file): - """Strip unnecessary whitespace and remove comments.""" - with open(xml_file, "r", encoding="utf-8") as f: - dom = defusedxml.minidom.parse(f) - - # Process each element to remove whitespace and comments - for element in dom.getElementsByTagName("*"): - # Skip w:t elements and their processing - if element.tagName.endswith(":t"): - continue - - # Remove whitespace-only text nodes and comment nodes - for child in list(element.childNodes): - if ( - child.nodeType == child.TEXT_NODE - and child.nodeValue - and child.nodeValue.strip() == "" - ) or child.nodeType == child.COMMENT_NODE: - element.removeChild(child) - - # Write back the condensed XML - with open(xml_file, "wb") as f: - f.write(dom.toxml(encoding="UTF-8")) - - -if __name__ == "__main__": - main() diff --git a/.claude/skills/docx/ooxml/scripts/unpack.py b/.claude/skills/docx/ooxml/scripts/unpack.py deleted file mode 100644 index 4938798813..0000000000 --- a/.claude/skills/docx/ooxml/scripts/unpack.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python3 -"""Unpack and format XML contents of Office files (.docx, .pptx, .xlsx)""" - -import random -import sys -import defusedxml.minidom -import zipfile -from pathlib import Path - -# Get command line arguments -assert len(sys.argv) == 3, "Usage: python unpack.py " -input_file, output_dir = sys.argv[1], sys.argv[2] - -# Extract and format -output_path = Path(output_dir) -output_path.mkdir(parents=True, exist_ok=True) -zipfile.ZipFile(input_file).extractall(output_path) - -# Pretty print all XML files -xml_files = list(output_path.rglob("*.xml")) + list(output_path.rglob("*.rels")) -for xml_file in xml_files: - content = xml_file.read_text(encoding="utf-8") - dom = defusedxml.minidom.parseString(content) - xml_file.write_bytes(dom.toprettyxml(indent=" ", encoding="ascii")) - -# For .docx files, suggest an RSID for tracked changes -if input_file.endswith(".docx"): - suggested_rsid = "".join(random.choices("0123456789ABCDEF", k=8)) - print(f"Suggested RSID for edit session: {suggested_rsid}") diff --git a/.claude/skills/docx/ooxml/scripts/validate.py b/.claude/skills/docx/ooxml/scripts/validate.py deleted file mode 100644 index 508c5891fa..0000000000 --- a/.claude/skills/docx/ooxml/scripts/validate.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env python3 -""" -Command line tool to validate Office document XML files against XSD schemas and tracked changes. - -Usage: - python validate.py --original -""" - -import argparse -import sys -from pathlib import Path - -from validation import DOCXSchemaValidator, PPTXSchemaValidator, RedliningValidator - - -def main(): - parser = argparse.ArgumentParser(description="Validate Office document XML files") - parser.add_argument( - "unpacked_dir", - help="Path to unpacked Office document directory", - ) - parser.add_argument( - "--original", - required=True, - help="Path to original file (.docx/.pptx/.xlsx)", - ) - parser.add_argument( - "-v", - "--verbose", - action="store_true", - help="Enable verbose output", - ) - args = parser.parse_args() - - # Validate paths - unpacked_dir = Path(args.unpacked_dir) - original_file = Path(args.original) - file_extension = original_file.suffix.lower() - assert unpacked_dir.is_dir(), f"Error: {unpacked_dir} is not a directory" - assert original_file.is_file(), f"Error: {original_file} is not a file" - assert file_extension in [".docx", ".pptx", ".xlsx"], ( - f"Error: {original_file} must be a .docx, .pptx, or .xlsx file" - ) - - # Run validations - match file_extension: - case ".docx": - validators = [DOCXSchemaValidator, RedliningValidator] - case ".pptx": - validators = [PPTXSchemaValidator] - case _: - print(f"Error: Validation not supported for file type {file_extension}") - sys.exit(1) - - # Run validators - success = True - for V in validators: - validator = V(unpacked_dir, original_file, verbose=args.verbose) - if not validator.validate(): - success = False - - if success: - print("All validations PASSED!") - - sys.exit(0 if success else 1) - - -if __name__ == "__main__": - main() diff --git a/.claude/skills/docx/ooxml/scripts/validation/__init__.py b/.claude/skills/docx/ooxml/scripts/validation/__init__.py deleted file mode 100644 index db092ece7e..0000000000 --- a/.claude/skills/docx/ooxml/scripts/validation/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -Validation modules for Word document processing. -""" - -from .base import BaseSchemaValidator -from .docx import DOCXSchemaValidator -from .pptx import PPTXSchemaValidator -from .redlining import RedliningValidator - -__all__ = [ - "BaseSchemaValidator", - "DOCXSchemaValidator", - "PPTXSchemaValidator", - "RedliningValidator", -] diff --git a/.claude/skills/docx/ooxml/scripts/validation/base.py b/.claude/skills/docx/ooxml/scripts/validation/base.py deleted file mode 100644 index 0681b199c2..0000000000 --- a/.claude/skills/docx/ooxml/scripts/validation/base.py +++ /dev/null @@ -1,951 +0,0 @@ -""" -Base validator with common validation logic for document files. -""" - -import re -from pathlib import Path - -import lxml.etree - - -class BaseSchemaValidator: - """Base validator with common validation logic for document files.""" - - # Elements whose 'id' attributes must be unique within their file - # Format: element_name -> (attribute_name, scope) - # scope can be 'file' (unique within file) or 'global' (unique across all files) - UNIQUE_ID_REQUIREMENTS = { - # Word elements - "comment": ("id", "file"), # Comment IDs in comments.xml - "commentrangestart": ("id", "file"), # Must match comment IDs - "commentrangeend": ("id", "file"), # Must match comment IDs - "bookmarkstart": ("id", "file"), # Bookmark start IDs - "bookmarkend": ("id", "file"), # Bookmark end IDs - # Note: ins and del (track changes) can share IDs when part of same revision - # PowerPoint elements - "sldid": ("id", "file"), # Slide IDs in presentation.xml - "sldmasterid": ("id", "global"), # Slide master IDs must be globally unique - "sldlayoutid": ("id", "global"), # Slide layout IDs must be globally unique - "cm": ("authorid", "file"), # Comment author IDs - # Excel elements - "sheet": ("sheetid", "file"), # Sheet IDs in workbook.xml - "definedname": ("id", "file"), # Named range IDs - # Drawing/Shape elements (all formats) - "cxnsp": ("id", "file"), # Connection shape IDs - "sp": ("id", "file"), # Shape IDs - "pic": ("id", "file"), # Picture IDs - "grpsp": ("id", "file"), # Group shape IDs - } - - # Mapping of element names to expected relationship types - # Subclasses should override this with format-specific mappings - ELEMENT_RELATIONSHIP_TYPES = {} - - # Unified schema mappings for all Office document types - SCHEMA_MAPPINGS = { - # Document type specific schemas - "word": "ISO-IEC29500-4_2016/wml.xsd", # Word documents - "ppt": "ISO-IEC29500-4_2016/pml.xsd", # PowerPoint presentations - "xl": "ISO-IEC29500-4_2016/sml.xsd", # Excel spreadsheets - # Common file types - "[Content_Types].xml": "ecma/fouth-edition/opc-contentTypes.xsd", - "app.xml": "ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd", - "core.xml": "ecma/fouth-edition/opc-coreProperties.xsd", - "custom.xml": "ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd", - ".rels": "ecma/fouth-edition/opc-relationships.xsd", - # Word-specific files - "people.xml": "microsoft/wml-2012.xsd", - "commentsIds.xml": "microsoft/wml-cid-2016.xsd", - "commentsExtensible.xml": "microsoft/wml-cex-2018.xsd", - "commentsExtended.xml": "microsoft/wml-2012.xsd", - # Chart files (common across document types) - "chart": "ISO-IEC29500-4_2016/dml-chart.xsd", - # Theme files (common across document types) - "theme": "ISO-IEC29500-4_2016/dml-main.xsd", - # Drawing and media files - "drawing": "ISO-IEC29500-4_2016/dml-main.xsd", - } - - # Unified namespace constants - MC_NAMESPACE = "http://schemas.openxmlformats.org/markup-compatibility/2006" - XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace" - - # Common OOXML namespaces used across validators - PACKAGE_RELATIONSHIPS_NAMESPACE = ( - "http://schemas.openxmlformats.org/package/2006/relationships" - ) - OFFICE_RELATIONSHIPS_NAMESPACE = ( - "http://schemas.openxmlformats.org/officeDocument/2006/relationships" - ) - CONTENT_TYPES_NAMESPACE = ( - "http://schemas.openxmlformats.org/package/2006/content-types" - ) - - # Folders where we should clean ignorable namespaces - MAIN_CONTENT_FOLDERS = {"word", "ppt", "xl"} - - # All allowed OOXML namespaces (superset of all document types) - OOXML_NAMESPACES = { - "http://schemas.openxmlformats.org/officeDocument/2006/math", - "http://schemas.openxmlformats.org/officeDocument/2006/relationships", - "http://schemas.openxmlformats.org/schemaLibrary/2006/main", - "http://schemas.openxmlformats.org/drawingml/2006/main", - "http://schemas.openxmlformats.org/drawingml/2006/chart", - "http://schemas.openxmlformats.org/drawingml/2006/chartDrawing", - "http://schemas.openxmlformats.org/drawingml/2006/diagram", - "http://schemas.openxmlformats.org/drawingml/2006/picture", - "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing", - "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing", - "http://schemas.openxmlformats.org/wordprocessingml/2006/main", - "http://schemas.openxmlformats.org/presentationml/2006/main", - "http://schemas.openxmlformats.org/spreadsheetml/2006/main", - "http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes", - "http://www.w3.org/XML/1998/namespace", - } - - def __init__(self, unpacked_dir, original_file, verbose=False): - self.unpacked_dir = Path(unpacked_dir).resolve() - self.original_file = Path(original_file) - self.verbose = verbose - - # Set schemas directory - self.schemas_dir = Path(__file__).parent.parent.parent / "schemas" - - # Get all XML and .rels files - patterns = ["*.xml", "*.rels"] - self.xml_files = [ - f for pattern in patterns for f in self.unpacked_dir.rglob(pattern) - ] - - if not self.xml_files: - print(f"Warning: No XML files found in {self.unpacked_dir}") - - def validate(self): - """Run all validation checks and return True if all pass.""" - raise NotImplementedError("Subclasses must implement the validate method") - - def validate_xml(self): - """Validate that all XML files are well-formed.""" - errors = [] - - for xml_file in self.xml_files: - try: - # Try to parse the XML file - lxml.etree.parse(str(xml_file)) - except lxml.etree.XMLSyntaxError as e: - errors.append( - f" {xml_file.relative_to(self.unpacked_dir)}: " - f"Line {e.lineno}: {e.msg}" - ) - except Exception as e: - errors.append( - f" {xml_file.relative_to(self.unpacked_dir)}: " - f"Unexpected error: {str(e)}" - ) - - if errors: - print(f"FAILED - Found {len(errors)} XML violations:") - for error in errors: - print(error) - return False - else: - if self.verbose: - print("PASSED - All XML files are well-formed") - return True - - def validate_namespaces(self): - """Validate that namespace prefixes in Ignorable attributes are declared.""" - errors = [] - - for xml_file in self.xml_files: - try: - root = lxml.etree.parse(str(xml_file)).getroot() - declared = set(root.nsmap.keys()) - {None} # Exclude default namespace - - for attr_val in [ - v for k, v in root.attrib.items() if k.endswith("Ignorable") - ]: - undeclared = set(attr_val.split()) - declared - errors.extend( - f" {xml_file.relative_to(self.unpacked_dir)}: " - f"Namespace '{ns}' in Ignorable but not declared" - for ns in undeclared - ) - except lxml.etree.XMLSyntaxError: - continue - - if errors: - print(f"FAILED - {len(errors)} namespace issues:") - for error in errors: - print(error) - return False - if self.verbose: - print("PASSED - All namespace prefixes properly declared") - return True - - def validate_unique_ids(self): - """Validate that specific IDs are unique according to OOXML requirements.""" - errors = [] - global_ids = {} # Track globally unique IDs across all files - - for xml_file in self.xml_files: - try: - root = lxml.etree.parse(str(xml_file)).getroot() - file_ids = {} # Track IDs that must be unique within this file - - # Remove all mc:AlternateContent elements from the tree - mc_elements = root.xpath( - ".//mc:AlternateContent", namespaces={"mc": self.MC_NAMESPACE} - ) - for elem in mc_elements: - elem.getparent().remove(elem) - - # Now check IDs in the cleaned tree - for elem in root.iter(): - # Get the element name without namespace - tag = ( - elem.tag.split("}")[-1].lower() - if "}" in elem.tag - else elem.tag.lower() - ) - - # Check if this element type has ID uniqueness requirements - if tag in self.UNIQUE_ID_REQUIREMENTS: - attr_name, scope = self.UNIQUE_ID_REQUIREMENTS[tag] - - # Look for the specified attribute - id_value = None - for attr, value in elem.attrib.items(): - attr_local = ( - attr.split("}")[-1].lower() - if "}" in attr - else attr.lower() - ) - if attr_local == attr_name: - id_value = value - break - - if id_value is not None: - if scope == "global": - # Check global uniqueness - if id_value in global_ids: - prev_file, prev_line, prev_tag = global_ids[ - id_value - ] - errors.append( - f" {xml_file.relative_to(self.unpacked_dir)}: " - f"Line {elem.sourceline}: Global ID '{id_value}' in <{tag}> " - f"already used in {prev_file} at line {prev_line} in <{prev_tag}>" - ) - else: - global_ids[id_value] = ( - xml_file.relative_to(self.unpacked_dir), - elem.sourceline, - tag, - ) - elif scope == "file": - # Check file-level uniqueness - key = (tag, attr_name) - if key not in file_ids: - file_ids[key] = {} - - if id_value in file_ids[key]: - prev_line = file_ids[key][id_value] - errors.append( - f" {xml_file.relative_to(self.unpacked_dir)}: " - f"Line {elem.sourceline}: Duplicate {attr_name}='{id_value}' in <{tag}> " - f"(first occurrence at line {prev_line})" - ) - else: - file_ids[key][id_value] = elem.sourceline - - except (lxml.etree.XMLSyntaxError, Exception) as e: - errors.append( - f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" - ) - - if errors: - print(f"FAILED - Found {len(errors)} ID uniqueness violations:") - for error in errors: - print(error) - return False - else: - if self.verbose: - print("PASSED - All required IDs are unique") - return True - - def validate_file_references(self): - """ - Validate that all .rels files properly reference files and that all files are referenced. - """ - errors = [] - - # Find all .rels files - rels_files = list(self.unpacked_dir.rglob("*.rels")) - - if not rels_files: - if self.verbose: - print("PASSED - No .rels files found") - return True - - # Get all files in the unpacked directory (excluding reference files) - all_files = [] - for file_path in self.unpacked_dir.rglob("*"): - if ( - file_path.is_file() - and file_path.name != "[Content_Types].xml" - and not file_path.name.endswith(".rels") - ): # This file is not referenced by .rels - all_files.append(file_path.resolve()) - - # Track all files that are referenced by any .rels file - all_referenced_files = set() - - if self.verbose: - print( - f"Found {len(rels_files)} .rels files and {len(all_files)} target files" - ) - - # Check each .rels file - for rels_file in rels_files: - try: - # Parse relationships file - rels_root = lxml.etree.parse(str(rels_file)).getroot() - - # Get the directory where this .rels file is located - rels_dir = rels_file.parent - - # Find all relationships and their targets - referenced_files = set() - broken_refs = [] - - for rel in rels_root.findall( - ".//ns:Relationship", - namespaces={"ns": self.PACKAGE_RELATIONSHIPS_NAMESPACE}, - ): - target = rel.get("Target") - if target and not target.startswith( - ("http", "mailto:") - ): # Skip external URLs - # Resolve the target path relative to the .rels file location - if rels_file.name == ".rels": - # Root .rels file - targets are relative to unpacked_dir - target_path = self.unpacked_dir / target - else: - # Other .rels files - targets are relative to their parent's parent - # e.g., word/_rels/document.xml.rels -> targets relative to word/ - base_dir = rels_dir.parent - target_path = base_dir / target - - # Normalize the path and check if it exists - try: - target_path = target_path.resolve() - if target_path.exists() and target_path.is_file(): - referenced_files.add(target_path) - all_referenced_files.add(target_path) - else: - broken_refs.append((target, rel.sourceline)) - except (OSError, ValueError): - broken_refs.append((target, rel.sourceline)) - - # Report broken references - if broken_refs: - rel_path = rels_file.relative_to(self.unpacked_dir) - for broken_ref, line_num in broken_refs: - errors.append( - f" {rel_path}: Line {line_num}: Broken reference to {broken_ref}" - ) - - except Exception as e: - rel_path = rels_file.relative_to(self.unpacked_dir) - errors.append(f" Error parsing {rel_path}: {e}") - - # Check for unreferenced files (files that exist but are not referenced anywhere) - unreferenced_files = set(all_files) - all_referenced_files - - if unreferenced_files: - for unref_file in sorted(unreferenced_files): - unref_rel_path = unref_file.relative_to(self.unpacked_dir) - errors.append(f" Unreferenced file: {unref_rel_path}") - - if errors: - print(f"FAILED - Found {len(errors)} relationship validation errors:") - for error in errors: - print(error) - print( - "CRITICAL: These errors will cause the document to appear corrupt. " - + "Broken references MUST be fixed, " - + "and unreferenced files MUST be referenced or removed." - ) - return False - else: - if self.verbose: - print( - "PASSED - All references are valid and all files are properly referenced" - ) - return True - - def validate_all_relationship_ids(self): - """ - Validate that all r:id attributes in XML files reference existing IDs - in their corresponding .rels files, and optionally validate relationship types. - """ - import lxml.etree - - errors = [] - - # Process each XML file that might contain r:id references - for xml_file in self.xml_files: - # Skip .rels files themselves - if xml_file.suffix == ".rels": - continue - - # Determine the corresponding .rels file - # For dir/file.xml, it's dir/_rels/file.xml.rels - rels_dir = xml_file.parent / "_rels" - rels_file = rels_dir / f"{xml_file.name}.rels" - - # Skip if there's no corresponding .rels file (that's okay) - if not rels_file.exists(): - continue - - try: - # Parse the .rels file to get valid relationship IDs and their types - rels_root = lxml.etree.parse(str(rels_file)).getroot() - rid_to_type = {} - - for rel in rels_root.findall( - f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" - ): - rid = rel.get("Id") - rel_type = rel.get("Type", "") - if rid: - # Check for duplicate rIds - if rid in rid_to_type: - rels_rel_path = rels_file.relative_to(self.unpacked_dir) - errors.append( - f" {rels_rel_path}: Line {rel.sourceline}: " - f"Duplicate relationship ID '{rid}' (IDs must be unique)" - ) - # Extract just the type name from the full URL - type_name = ( - rel_type.split("/")[-1] if "/" in rel_type else rel_type - ) - rid_to_type[rid] = type_name - - # Parse the XML file to find all r:id references - xml_root = lxml.etree.parse(str(xml_file)).getroot() - - # Find all elements with r:id attributes - for elem in xml_root.iter(): - # Check for r:id attribute (relationship ID) - rid_attr = elem.get(f"{{{self.OFFICE_RELATIONSHIPS_NAMESPACE}}}id") - if rid_attr: - xml_rel_path = xml_file.relative_to(self.unpacked_dir) - elem_name = ( - elem.tag.split("}")[-1] if "}" in elem.tag else elem.tag - ) - - # Check if the ID exists - if rid_attr not in rid_to_type: - errors.append( - f" {xml_rel_path}: Line {elem.sourceline}: " - f"<{elem_name}> references non-existent relationship '{rid_attr}' " - f"(valid IDs: {', '.join(sorted(rid_to_type.keys())[:5])}{'...' if len(rid_to_type) > 5 else ''})" - ) - # Check if we have type expectations for this element - elif self.ELEMENT_RELATIONSHIP_TYPES: - expected_type = self._get_expected_relationship_type( - elem_name - ) - if expected_type: - actual_type = rid_to_type[rid_attr] - # Check if the actual type matches or contains the expected type - if expected_type not in actual_type.lower(): - errors.append( - f" {xml_rel_path}: Line {elem.sourceline}: " - f"<{elem_name}> references '{rid_attr}' which points to '{actual_type}' " - f"but should point to a '{expected_type}' relationship" - ) - - except Exception as e: - xml_rel_path = xml_file.relative_to(self.unpacked_dir) - errors.append(f" Error processing {xml_rel_path}: {e}") - - if errors: - print(f"FAILED - Found {len(errors)} relationship ID reference errors:") - for error in errors: - print(error) - print("\nThese ID mismatches will cause the document to appear corrupt!") - return False - else: - if self.verbose: - print("PASSED - All relationship ID references are valid") - return True - - def _get_expected_relationship_type(self, element_name): - """ - Get the expected relationship type for an element. - First checks the explicit mapping, then tries pattern detection. - """ - # Normalize element name to lowercase - elem_lower = element_name.lower() - - # Check explicit mapping first - if elem_lower in self.ELEMENT_RELATIONSHIP_TYPES: - return self.ELEMENT_RELATIONSHIP_TYPES[elem_lower] - - # Try pattern detection for common patterns - # Pattern 1: Elements ending in "Id" often expect a relationship of the prefix type - if elem_lower.endswith("id") and len(elem_lower) > 2: - # e.g., "sldId" -> "sld", "sldMasterId" -> "sldMaster" - prefix = elem_lower[:-2] # Remove "id" - # Check if this might be a compound like "sldMasterId" - if prefix.endswith("master"): - return prefix.lower() - elif prefix.endswith("layout"): - return prefix.lower() - else: - # Simple case like "sldId" -> "slide" - # Common transformations - if prefix == "sld": - return "slide" - return prefix.lower() - - # Pattern 2: Elements ending in "Reference" expect a relationship of the prefix type - if elem_lower.endswith("reference") and len(elem_lower) > 9: - prefix = elem_lower[:-9] # Remove "reference" - return prefix.lower() - - return None - - def validate_content_types(self): - """Validate that all content files are properly declared in [Content_Types].xml.""" - errors = [] - - # Find [Content_Types].xml file - content_types_file = self.unpacked_dir / "[Content_Types].xml" - if not content_types_file.exists(): - print("FAILED - [Content_Types].xml file not found") - return False - - try: - # Parse and get all declared parts and extensions - root = lxml.etree.parse(str(content_types_file)).getroot() - declared_parts = set() - declared_extensions = set() - - # Get Override declarations (specific files) - for override in root.findall( - f".//{{{self.CONTENT_TYPES_NAMESPACE}}}Override" - ): - part_name = override.get("PartName") - if part_name is not None: - declared_parts.add(part_name.lstrip("/")) - - # Get Default declarations (by extension) - for default in root.findall( - f".//{{{self.CONTENT_TYPES_NAMESPACE}}}Default" - ): - extension = default.get("Extension") - if extension is not None: - declared_extensions.add(extension.lower()) - - # Root elements that require content type declaration - declarable_roots = { - "sld", - "sldLayout", - "sldMaster", - "presentation", # PowerPoint - "document", # Word - "workbook", - "worksheet", # Excel - "theme", # Common - } - - # Common media file extensions that should be declared - media_extensions = { - "png": "image/png", - "jpg": "image/jpeg", - "jpeg": "image/jpeg", - "gif": "image/gif", - "bmp": "image/bmp", - "tiff": "image/tiff", - "wmf": "image/x-wmf", - "emf": "image/x-emf", - } - - # Get all files in the unpacked directory - all_files = list(self.unpacked_dir.rglob("*")) - all_files = [f for f in all_files if f.is_file()] - - # Check all XML files for Override declarations - for xml_file in self.xml_files: - path_str = str(xml_file.relative_to(self.unpacked_dir)).replace( - "\\", "/" - ) - - # Skip non-content files - if any( - skip in path_str - for skip in [".rels", "[Content_Types]", "docProps/", "_rels/"] - ): - continue - - try: - root_tag = lxml.etree.parse(str(xml_file)).getroot().tag - root_name = root_tag.split("}")[-1] if "}" in root_tag else root_tag - - if root_name in declarable_roots and path_str not in declared_parts: - errors.append( - f" {path_str}: File with <{root_name}> root not declared in [Content_Types].xml" - ) - - except Exception: - continue # Skip unparseable files - - # Check all non-XML files for Default extension declarations - for file_path in all_files: - # Skip XML files and metadata files (already checked above) - if file_path.suffix.lower() in {".xml", ".rels"}: - continue - if file_path.name == "[Content_Types].xml": - continue - if "_rels" in file_path.parts or "docProps" in file_path.parts: - continue - - extension = file_path.suffix.lstrip(".").lower() - if extension and extension not in declared_extensions: - # Check if it's a known media extension that should be declared - if extension in media_extensions: - relative_path = file_path.relative_to(self.unpacked_dir) - errors.append( - f' {relative_path}: File with extension \'{extension}\' not declared in [Content_Types].xml - should add: ' - ) - - except Exception as e: - errors.append(f" Error parsing [Content_Types].xml: {e}") - - if errors: - print(f"FAILED - Found {len(errors)} content type declaration errors:") - for error in errors: - print(error) - return False - else: - if self.verbose: - print( - "PASSED - All content files are properly declared in [Content_Types].xml" - ) - return True - - def validate_file_against_xsd(self, xml_file, verbose=False): - """Validate a single XML file against XSD schema, comparing with original. - - Args: - xml_file: Path to XML file to validate - verbose: Enable verbose output - - Returns: - tuple: (is_valid, new_errors_set) where is_valid is True/False/None (skipped) - """ - # Resolve both paths to handle symlinks - xml_file = Path(xml_file).resolve() - unpacked_dir = self.unpacked_dir.resolve() - - # Validate current file - is_valid, current_errors = self._validate_single_file_xsd( - xml_file, unpacked_dir - ) - - if is_valid is None: - return None, set() # Skipped - elif is_valid: - return True, set() # Valid, no errors - - # Get errors from original file for this specific file - original_errors = self._get_original_file_errors(xml_file) - - # Compare with original (both are guaranteed to be sets here) - assert current_errors is not None - new_errors = current_errors - original_errors - - if new_errors: - if verbose: - relative_path = xml_file.relative_to(unpacked_dir) - print(f"FAILED - {relative_path}: {len(new_errors)} new error(s)") - for error in list(new_errors)[:3]: - truncated = error[:250] + "..." if len(error) > 250 else error - print(f" - {truncated}") - return False, new_errors - else: - # All errors existed in original - if verbose: - print( - f"PASSED - No new errors (original had {len(current_errors)} errors)" - ) - return True, set() - - def validate_against_xsd(self): - """Validate XML files against XSD schemas, showing only new errors compared to original.""" - new_errors = [] - original_error_count = 0 - valid_count = 0 - skipped_count = 0 - - for xml_file in self.xml_files: - relative_path = str(xml_file.relative_to(self.unpacked_dir)) - is_valid, new_file_errors = self.validate_file_against_xsd( - xml_file, verbose=False - ) - - if is_valid is None: - skipped_count += 1 - continue - elif is_valid and not new_file_errors: - valid_count += 1 - continue - elif is_valid: - # Had errors but all existed in original - original_error_count += 1 - valid_count += 1 - continue - - # Has new errors - new_errors.append(f" {relative_path}: {len(new_file_errors)} new error(s)") - for error in list(new_file_errors)[:3]: # Show first 3 errors - new_errors.append( - f" - {error[:250]}..." if len(error) > 250 else f" - {error}" - ) - - # Print summary - if self.verbose: - print(f"Validated {len(self.xml_files)} files:") - print(f" - Valid: {valid_count}") - print(f" - Skipped (no schema): {skipped_count}") - if original_error_count: - print(f" - With original errors (ignored): {original_error_count}") - print( - f" - With NEW errors: {len(new_errors) > 0 and len([e for e in new_errors if not e.startswith(' ')]) or 0}" - ) - - if new_errors: - print("\nFAILED - Found NEW validation errors:") - for error in new_errors: - print(error) - return False - else: - if self.verbose: - print("\nPASSED - No new XSD validation errors introduced") - return True - - def _get_schema_path(self, xml_file): - """Determine the appropriate schema path for an XML file.""" - # Check exact filename match - if xml_file.name in self.SCHEMA_MAPPINGS: - return self.schemas_dir / self.SCHEMA_MAPPINGS[xml_file.name] - - # Check .rels files - if xml_file.suffix == ".rels": - return self.schemas_dir / self.SCHEMA_MAPPINGS[".rels"] - - # Check chart files - if "charts/" in str(xml_file) and xml_file.name.startswith("chart"): - return self.schemas_dir / self.SCHEMA_MAPPINGS["chart"] - - # Check theme files - if "theme/" in str(xml_file) and xml_file.name.startswith("theme"): - return self.schemas_dir / self.SCHEMA_MAPPINGS["theme"] - - # Check if file is in a main content folder and use appropriate schema - if xml_file.parent.name in self.MAIN_CONTENT_FOLDERS: - return self.schemas_dir / self.SCHEMA_MAPPINGS[xml_file.parent.name] - - return None - - def _clean_ignorable_namespaces(self, xml_doc): - """Remove attributes and elements not in allowed namespaces.""" - # Create a clean copy - xml_string = lxml.etree.tostring(xml_doc, encoding="unicode") - xml_copy = lxml.etree.fromstring(xml_string) - - # Remove attributes not in allowed namespaces - for elem in xml_copy.iter(): - attrs_to_remove = [] - - for attr in elem.attrib: - # Check if attribute is from a namespace other than allowed ones - if "{" in attr: - ns = attr.split("}")[0][1:] - if ns not in self.OOXML_NAMESPACES: - attrs_to_remove.append(attr) - - # Remove collected attributes - for attr in attrs_to_remove: - del elem.attrib[attr] - - # Remove elements not in allowed namespaces - self._remove_ignorable_elements(xml_copy) - - return lxml.etree.ElementTree(xml_copy) - - def _remove_ignorable_elements(self, root): - """Recursively remove all elements not in allowed namespaces.""" - elements_to_remove = [] - - # Find elements to remove - for elem in list(root): - # Skip non-element nodes (comments, processing instructions, etc.) - if not hasattr(elem, "tag") or callable(elem.tag): - continue - - tag_str = str(elem.tag) - if tag_str.startswith("{"): - ns = tag_str.split("}")[0][1:] - if ns not in self.OOXML_NAMESPACES: - elements_to_remove.append(elem) - continue - - # Recursively clean child elements - self._remove_ignorable_elements(elem) - - # Remove collected elements - for elem in elements_to_remove: - root.remove(elem) - - def _preprocess_for_mc_ignorable(self, xml_doc): - """Preprocess XML to handle mc:Ignorable attribute properly.""" - # Remove mc:Ignorable attributes before validation - root = xml_doc.getroot() - - # Remove mc:Ignorable attribute from root - if f"{{{self.MC_NAMESPACE}}}Ignorable" in root.attrib: - del root.attrib[f"{{{self.MC_NAMESPACE}}}Ignorable"] - - return xml_doc - - def _validate_single_file_xsd(self, xml_file, base_path): - """Validate a single XML file against XSD schema. Returns (is_valid, errors_set).""" - schema_path = self._get_schema_path(xml_file) - if not schema_path: - return None, None # Skip file - - try: - # Load schema - with open(schema_path, "rb") as xsd_file: - parser = lxml.etree.XMLParser() - xsd_doc = lxml.etree.parse( - xsd_file, parser=parser, base_url=str(schema_path) - ) - schema = lxml.etree.XMLSchema(xsd_doc) - - # Load and preprocess XML - with open(xml_file, "r") as f: - xml_doc = lxml.etree.parse(f) - - xml_doc, _ = self._remove_template_tags_from_text_nodes(xml_doc) - xml_doc = self._preprocess_for_mc_ignorable(xml_doc) - - # Clean ignorable namespaces if needed - relative_path = xml_file.relative_to(base_path) - if ( - relative_path.parts - and relative_path.parts[0] in self.MAIN_CONTENT_FOLDERS - ): - xml_doc = self._clean_ignorable_namespaces(xml_doc) - - # Validate - if schema.validate(xml_doc): - return True, set() - else: - errors = set() - for error in schema.error_log: - # Store normalized error message (without line numbers for comparison) - errors.add(error.message) - return False, errors - - except Exception as e: - return False, {str(e)} - - def _get_original_file_errors(self, xml_file): - """Get XSD validation errors from a single file in the original document. - - Args: - xml_file: Path to the XML file in unpacked_dir to check - - Returns: - set: Set of error messages from the original file - """ - import tempfile - import zipfile - - # Resolve both paths to handle symlinks (e.g., /var vs /private/var on macOS) - xml_file = Path(xml_file).resolve() - unpacked_dir = self.unpacked_dir.resolve() - relative_path = xml_file.relative_to(unpacked_dir) - - with tempfile.TemporaryDirectory() as temp_dir: - temp_path = Path(temp_dir) - - # Extract original file - with zipfile.ZipFile(self.original_file, "r") as zip_ref: - zip_ref.extractall(temp_path) - - # Find corresponding file in original - original_xml_file = temp_path / relative_path - - if not original_xml_file.exists(): - # File didn't exist in original, so no original errors - return set() - - # Validate the specific file in original - is_valid, errors = self._validate_single_file_xsd( - original_xml_file, temp_path - ) - return errors if errors else set() - - def _remove_template_tags_from_text_nodes(self, xml_doc): - """Remove template tags from XML text nodes and collect warnings. - - Template tags follow the pattern {{ ... }} and are used as placeholders - for content replacement. They should be removed from text content before - XSD validation while preserving XML structure. - - Returns: - tuple: (cleaned_xml_doc, warnings_list) - """ - warnings = [] - template_pattern = re.compile(r"\{\{[^}]*\}\}") - - # Create a copy of the document to avoid modifying the original - xml_string = lxml.etree.tostring(xml_doc, encoding="unicode") - xml_copy = lxml.etree.fromstring(xml_string) - - def process_text_content(text, content_type): - if not text: - return text - matches = list(template_pattern.finditer(text)) - if matches: - for match in matches: - warnings.append( - f"Found template tag in {content_type}: {match.group()}" - ) - return template_pattern.sub("", text) - return text - - # Process all text nodes in the document - for elem in xml_copy.iter(): - # Skip processing if this is a w:t element - if not hasattr(elem, "tag") or callable(elem.tag): - continue - tag_str = str(elem.tag) - if tag_str.endswith("}t") or tag_str == "t": - continue - - elem.text = process_text_content(elem.text, "text content") - elem.tail = process_text_content(elem.tail, "tail content") - - return lxml.etree.ElementTree(xml_copy), warnings - - -if __name__ == "__main__": - raise RuntimeError("This module should not be run directly.") diff --git a/.claude/skills/docx/ooxml/scripts/validation/docx.py b/.claude/skills/docx/ooxml/scripts/validation/docx.py deleted file mode 100644 index 602c47087a..0000000000 --- a/.claude/skills/docx/ooxml/scripts/validation/docx.py +++ /dev/null @@ -1,274 +0,0 @@ -""" -Validator for Word document XML files against XSD schemas. -""" - -import re -import tempfile -import zipfile - -import lxml.etree - -from .base import BaseSchemaValidator - - -class DOCXSchemaValidator(BaseSchemaValidator): - """Validator for Word document XML files against XSD schemas.""" - - # Word-specific namespace - WORD_2006_NAMESPACE = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" - - # Word-specific element to relationship type mappings - # Start with empty mapping - add specific cases as we discover them - ELEMENT_RELATIONSHIP_TYPES = {} - - def validate(self): - """Run all validation checks and return True if all pass.""" - # Test 0: XML well-formedness - if not self.validate_xml(): - return False - - # Test 1: Namespace declarations - all_valid = True - if not self.validate_namespaces(): - all_valid = False - - # Test 2: Unique IDs - if not self.validate_unique_ids(): - all_valid = False - - # Test 3: Relationship and file reference validation - if not self.validate_file_references(): - all_valid = False - - # Test 4: Content type declarations - if not self.validate_content_types(): - all_valid = False - - # Test 5: XSD schema validation - if not self.validate_against_xsd(): - all_valid = False - - # Test 6: Whitespace preservation - if not self.validate_whitespace_preservation(): - all_valid = False - - # Test 7: Deletion validation - if not self.validate_deletions(): - all_valid = False - - # Test 8: Insertion validation - if not self.validate_insertions(): - all_valid = False - - # Test 9: Relationship ID reference validation - if not self.validate_all_relationship_ids(): - all_valid = False - - # Count and compare paragraphs - self.compare_paragraph_counts() - - return all_valid - - def validate_whitespace_preservation(self): - """ - Validate that w:t elements with whitespace have xml:space='preserve'. - """ - errors = [] - - for xml_file in self.xml_files: - # Only check document.xml files - if xml_file.name != "document.xml": - continue - - try: - root = lxml.etree.parse(str(xml_file)).getroot() - - # Find all w:t elements - for elem in root.iter(f"{{{self.WORD_2006_NAMESPACE}}}t"): - if elem.text: - text = elem.text - # Check if text starts or ends with whitespace - if re.match(r"^\s.*", text) or re.match(r".*\s$", text): - # Check if xml:space="preserve" attribute exists - xml_space_attr = f"{{{self.XML_NAMESPACE}}}space" - if ( - xml_space_attr not in elem.attrib - or elem.attrib[xml_space_attr] != "preserve" - ): - # Show a preview of the text - text_preview = ( - repr(text)[:50] + "..." - if len(repr(text)) > 50 - else repr(text) - ) - errors.append( - f" {xml_file.relative_to(self.unpacked_dir)}: " - f"Line {elem.sourceline}: w:t element with whitespace missing xml:space='preserve': {text_preview}" - ) - - except (lxml.etree.XMLSyntaxError, Exception) as e: - errors.append( - f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" - ) - - if errors: - print(f"FAILED - Found {len(errors)} whitespace preservation violations:") - for error in errors: - print(error) - return False - else: - if self.verbose: - print("PASSED - All whitespace is properly preserved") - return True - - def validate_deletions(self): - """ - Validate that w:t elements are not within w:del elements. - For some reason, XSD validation does not catch this, so we do it manually. - """ - errors = [] - - for xml_file in self.xml_files: - # Only check document.xml files - if xml_file.name != "document.xml": - continue - - try: - root = lxml.etree.parse(str(xml_file)).getroot() - - # Find all w:t elements that are descendants of w:del elements - namespaces = {"w": self.WORD_2006_NAMESPACE} - xpath_expression = ".//w:del//w:t" - problematic_t_elements = root.xpath( - xpath_expression, namespaces=namespaces - ) - for t_elem in problematic_t_elements: - if t_elem.text: - # Show a preview of the text - text_preview = ( - repr(t_elem.text)[:50] + "..." - if len(repr(t_elem.text)) > 50 - else repr(t_elem.text) - ) - errors.append( - f" {xml_file.relative_to(self.unpacked_dir)}: " - f"Line {t_elem.sourceline}: found within : {text_preview}" - ) - - except (lxml.etree.XMLSyntaxError, Exception) as e: - errors.append( - f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" - ) - - if errors: - print(f"FAILED - Found {len(errors)} deletion validation violations:") - for error in errors: - print(error) - return False - else: - if self.verbose: - print("PASSED - No w:t elements found within w:del elements") - return True - - def count_paragraphs_in_unpacked(self): - """Count the number of paragraphs in the unpacked document.""" - count = 0 - - for xml_file in self.xml_files: - # Only check document.xml files - if xml_file.name != "document.xml": - continue - - try: - root = lxml.etree.parse(str(xml_file)).getroot() - # Count all w:p elements - paragraphs = root.findall(f".//{{{self.WORD_2006_NAMESPACE}}}p") - count = len(paragraphs) - except Exception as e: - print(f"Error counting paragraphs in unpacked document: {e}") - - return count - - def count_paragraphs_in_original(self): - """Count the number of paragraphs in the original docx file.""" - count = 0 - - try: - # Create temporary directory to unpack original - with tempfile.TemporaryDirectory() as temp_dir: - # Unpack original docx - with zipfile.ZipFile(self.original_file, "r") as zip_ref: - zip_ref.extractall(temp_dir) - - # Parse document.xml - doc_xml_path = temp_dir + "/word/document.xml" - root = lxml.etree.parse(doc_xml_path).getroot() - - # Count all w:p elements - paragraphs = root.findall(f".//{{{self.WORD_2006_NAMESPACE}}}p") - count = len(paragraphs) - - except Exception as e: - print(f"Error counting paragraphs in original document: {e}") - - return count - - def validate_insertions(self): - """ - Validate that w:delText elements are not within w:ins elements. - w:delText is only allowed in w:ins if nested within a w:del. - """ - errors = [] - - for xml_file in self.xml_files: - if xml_file.name != "document.xml": - continue - - try: - root = lxml.etree.parse(str(xml_file)).getroot() - namespaces = {"w": self.WORD_2006_NAMESPACE} - - # Find w:delText in w:ins that are NOT within w:del - invalid_elements = root.xpath( - ".//w:ins//w:delText[not(ancestor::w:del)]", - namespaces=namespaces - ) - - for elem in invalid_elements: - text_preview = ( - repr(elem.text or "")[:50] + "..." - if len(repr(elem.text or "")) > 50 - else repr(elem.text or "") - ) - errors.append( - f" {xml_file.relative_to(self.unpacked_dir)}: " - f"Line {elem.sourceline}: within : {text_preview}" - ) - - except (lxml.etree.XMLSyntaxError, Exception) as e: - errors.append( - f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" - ) - - if errors: - print(f"FAILED - Found {len(errors)} insertion validation violations:") - for error in errors: - print(error) - return False - else: - if self.verbose: - print("PASSED - No w:delText elements within w:ins elements") - return True - - def compare_paragraph_counts(self): - """Compare paragraph counts between original and new document.""" - original_count = self.count_paragraphs_in_original() - new_count = self.count_paragraphs_in_unpacked() - - diff = new_count - original_count - diff_str = f"+{diff}" if diff > 0 else str(diff) - print(f"\nParagraphs: {original_count} → {new_count} ({diff_str})") - - -if __name__ == "__main__": - raise RuntimeError("This module should not be run directly.") diff --git a/.claude/skills/docx/ooxml/scripts/validation/pptx.py b/.claude/skills/docx/ooxml/scripts/validation/pptx.py deleted file mode 100644 index 66d5b1e2db..0000000000 --- a/.claude/skills/docx/ooxml/scripts/validation/pptx.py +++ /dev/null @@ -1,315 +0,0 @@ -""" -Validator for PowerPoint presentation XML files against XSD schemas. -""" - -import re - -from .base import BaseSchemaValidator - - -class PPTXSchemaValidator(BaseSchemaValidator): - """Validator for PowerPoint presentation XML files against XSD schemas.""" - - # PowerPoint presentation namespace - PRESENTATIONML_NAMESPACE = ( - "http://schemas.openxmlformats.org/presentationml/2006/main" - ) - - # PowerPoint-specific element to relationship type mappings - ELEMENT_RELATIONSHIP_TYPES = { - "sldid": "slide", - "sldmasterid": "slidemaster", - "notesmasterid": "notesmaster", - "sldlayoutid": "slidelayout", - "themeid": "theme", - "tablestyleid": "tablestyles", - } - - def validate(self): - """Run all validation checks and return True if all pass.""" - # Test 0: XML well-formedness - if not self.validate_xml(): - return False - - # Test 1: Namespace declarations - all_valid = True - if not self.validate_namespaces(): - all_valid = False - - # Test 2: Unique IDs - if not self.validate_unique_ids(): - all_valid = False - - # Test 3: UUID ID validation - if not self.validate_uuid_ids(): - all_valid = False - - # Test 4: Relationship and file reference validation - if not self.validate_file_references(): - all_valid = False - - # Test 5: Slide layout ID validation - if not self.validate_slide_layout_ids(): - all_valid = False - - # Test 6: Content type declarations - if not self.validate_content_types(): - all_valid = False - - # Test 7: XSD schema validation - if not self.validate_against_xsd(): - all_valid = False - - # Test 8: Notes slide reference validation - if not self.validate_notes_slide_references(): - all_valid = False - - # Test 9: Relationship ID reference validation - if not self.validate_all_relationship_ids(): - all_valid = False - - # Test 10: Duplicate slide layout references validation - if not self.validate_no_duplicate_slide_layouts(): - all_valid = False - - return all_valid - - def validate_uuid_ids(self): - """Validate that ID attributes that look like UUIDs contain only hex values.""" - import lxml.etree - - errors = [] - # UUID pattern: 8-4-4-4-12 hex digits with optional braces/hyphens - uuid_pattern = re.compile( - r"^[\{\(]?[0-9A-Fa-f]{8}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{12}[\}\)]?$" - ) - - for xml_file in self.xml_files: - try: - root = lxml.etree.parse(str(xml_file)).getroot() - - # Check all elements for ID attributes - for elem in root.iter(): - for attr, value in elem.attrib.items(): - # Check if this is an ID attribute - attr_name = attr.split("}")[-1].lower() - if attr_name == "id" or attr_name.endswith("id"): - # Check if value looks like a UUID (has the right length and pattern structure) - if self._looks_like_uuid(value): - # Validate that it contains only hex characters in the right positions - if not uuid_pattern.match(value): - errors.append( - f" {xml_file.relative_to(self.unpacked_dir)}: " - f"Line {elem.sourceline}: ID '{value}' appears to be a UUID but contains invalid hex characters" - ) - - except (lxml.etree.XMLSyntaxError, Exception) as e: - errors.append( - f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" - ) - - if errors: - print(f"FAILED - Found {len(errors)} UUID ID validation errors:") - for error in errors: - print(error) - return False - else: - if self.verbose: - print("PASSED - All UUID-like IDs contain valid hex values") - return True - - def _looks_like_uuid(self, value): - """Check if a value has the general structure of a UUID.""" - # Remove common UUID delimiters - clean_value = value.strip("{}()").replace("-", "") - # Check if it's 32 hex-like characters (could include invalid hex chars) - return len(clean_value) == 32 and all(c.isalnum() for c in clean_value) - - def validate_slide_layout_ids(self): - """Validate that sldLayoutId elements in slide masters reference valid slide layouts.""" - import lxml.etree - - errors = [] - - # Find all slide master files - slide_masters = list(self.unpacked_dir.glob("ppt/slideMasters/*.xml")) - - if not slide_masters: - if self.verbose: - print("PASSED - No slide masters found") - return True - - for slide_master in slide_masters: - try: - # Parse the slide master file - root = lxml.etree.parse(str(slide_master)).getroot() - - # Find the corresponding _rels file for this slide master - rels_file = slide_master.parent / "_rels" / f"{slide_master.name}.rels" - - if not rels_file.exists(): - errors.append( - f" {slide_master.relative_to(self.unpacked_dir)}: " - f"Missing relationships file: {rels_file.relative_to(self.unpacked_dir)}" - ) - continue - - # Parse the relationships file - rels_root = lxml.etree.parse(str(rels_file)).getroot() - - # Build a set of valid relationship IDs that point to slide layouts - valid_layout_rids = set() - for rel in rels_root.findall( - f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" - ): - rel_type = rel.get("Type", "") - if "slideLayout" in rel_type: - valid_layout_rids.add(rel.get("Id")) - - # Find all sldLayoutId elements in the slide master - for sld_layout_id in root.findall( - f".//{{{self.PRESENTATIONML_NAMESPACE}}}sldLayoutId" - ): - r_id = sld_layout_id.get( - f"{{{self.OFFICE_RELATIONSHIPS_NAMESPACE}}}id" - ) - layout_id = sld_layout_id.get("id") - - if r_id and r_id not in valid_layout_rids: - errors.append( - f" {slide_master.relative_to(self.unpacked_dir)}: " - f"Line {sld_layout_id.sourceline}: sldLayoutId with id='{layout_id}' " - f"references r:id='{r_id}' which is not found in slide layout relationships" - ) - - except (lxml.etree.XMLSyntaxError, Exception) as e: - errors.append( - f" {slide_master.relative_to(self.unpacked_dir)}: Error: {e}" - ) - - if errors: - print(f"FAILED - Found {len(errors)} slide layout ID validation errors:") - for error in errors: - print(error) - print( - "Remove invalid references or add missing slide layouts to the relationships file." - ) - return False - else: - if self.verbose: - print("PASSED - All slide layout IDs reference valid slide layouts") - return True - - def validate_no_duplicate_slide_layouts(self): - """Validate that each slide has exactly one slideLayout reference.""" - import lxml.etree - - errors = [] - slide_rels_files = list(self.unpacked_dir.glob("ppt/slides/_rels/*.xml.rels")) - - for rels_file in slide_rels_files: - try: - root = lxml.etree.parse(str(rels_file)).getroot() - - # Find all slideLayout relationships - layout_rels = [ - rel - for rel in root.findall( - f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" - ) - if "slideLayout" in rel.get("Type", "") - ] - - if len(layout_rels) > 1: - errors.append( - f" {rels_file.relative_to(self.unpacked_dir)}: has {len(layout_rels)} slideLayout references" - ) - - except Exception as e: - errors.append( - f" {rels_file.relative_to(self.unpacked_dir)}: Error: {e}" - ) - - if errors: - print("FAILED - Found slides with duplicate slideLayout references:") - for error in errors: - print(error) - return False - else: - if self.verbose: - print("PASSED - All slides have exactly one slideLayout reference") - return True - - def validate_notes_slide_references(self): - """Validate that each notesSlide file is referenced by only one slide.""" - import lxml.etree - - errors = [] - notes_slide_references = {} # Track which slides reference each notesSlide - - # Find all slide relationship files - slide_rels_files = list(self.unpacked_dir.glob("ppt/slides/_rels/*.xml.rels")) - - if not slide_rels_files: - if self.verbose: - print("PASSED - No slide relationship files found") - return True - - for rels_file in slide_rels_files: - try: - # Parse the relationships file - root = lxml.etree.parse(str(rels_file)).getroot() - - # Find all notesSlide relationships - for rel in root.findall( - f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" - ): - rel_type = rel.get("Type", "") - if "notesSlide" in rel_type: - target = rel.get("Target", "") - if target: - # Normalize the target path to handle relative paths - normalized_target = target.replace("../", "") - - # Track which slide references this notesSlide - slide_name = rels_file.stem.replace( - ".xml", "" - ) # e.g., "slide1" - - if normalized_target not in notes_slide_references: - notes_slide_references[normalized_target] = [] - notes_slide_references[normalized_target].append( - (slide_name, rels_file) - ) - - except (lxml.etree.XMLSyntaxError, Exception) as e: - errors.append( - f" {rels_file.relative_to(self.unpacked_dir)}: Error: {e}" - ) - - # Check for duplicate references - for target, references in notes_slide_references.items(): - if len(references) > 1: - slide_names = [ref[0] for ref in references] - errors.append( - f" Notes slide '{target}' is referenced by multiple slides: {', '.join(slide_names)}" - ) - for slide_name, rels_file in references: - errors.append(f" - {rels_file.relative_to(self.unpacked_dir)}") - - if errors: - print( - f"FAILED - Found {len([e for e in errors if not e.startswith(' ')])} notes slide reference validation errors:" - ) - for error in errors: - print(error) - print("Each slide may optionally have its own slide file.") - return False - else: - if self.verbose: - print("PASSED - All notes slide references are unique") - return True - - -if __name__ == "__main__": - raise RuntimeError("This module should not be run directly.") diff --git a/.claude/skills/docx/ooxml/scripts/validation/redlining.py b/.claude/skills/docx/ooxml/scripts/validation/redlining.py deleted file mode 100644 index 7ed425edf5..0000000000 --- a/.claude/skills/docx/ooxml/scripts/validation/redlining.py +++ /dev/null @@ -1,279 +0,0 @@ -""" -Validator for tracked changes in Word documents. -""" - -import subprocess -import tempfile -import zipfile -from pathlib import Path - - -class RedliningValidator: - """Validator for tracked changes in Word documents.""" - - def __init__(self, unpacked_dir, original_docx, verbose=False): - self.unpacked_dir = Path(unpacked_dir) - self.original_docx = Path(original_docx) - self.verbose = verbose - self.namespaces = { - "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main" - } - - def validate(self): - """Main validation method that returns True if valid, False otherwise.""" - # Verify unpacked directory exists and has correct structure - modified_file = self.unpacked_dir / "word" / "document.xml" - if not modified_file.exists(): - print(f"FAILED - Modified document.xml not found at {modified_file}") - return False - - # First, check if there are any tracked changes by Claude to validate - try: - import xml.etree.ElementTree as ET - - tree = ET.parse(modified_file) - root = tree.getroot() - - # Check for w:del or w:ins tags authored by Claude - del_elements = root.findall(".//w:del", self.namespaces) - ins_elements = root.findall(".//w:ins", self.namespaces) - - # Filter to only include changes by Claude - claude_del_elements = [ - elem - for elem in del_elements - if elem.get(f"{{{self.namespaces['w']}}}author") == "Claude" - ] - claude_ins_elements = [ - elem - for elem in ins_elements - if elem.get(f"{{{self.namespaces['w']}}}author") == "Claude" - ] - - # Redlining validation is only needed if tracked changes by Claude have been used. - if not claude_del_elements and not claude_ins_elements: - if self.verbose: - print("PASSED - No tracked changes by Claude found.") - return True - - except Exception: - # If we can't parse the XML, continue with full validation - pass - - # Create temporary directory for unpacking original docx - with tempfile.TemporaryDirectory() as temp_dir: - temp_path = Path(temp_dir) - - # Unpack original docx - try: - with zipfile.ZipFile(self.original_docx, "r") as zip_ref: - zip_ref.extractall(temp_path) - except Exception as e: - print(f"FAILED - Error unpacking original docx: {e}") - return False - - original_file = temp_path / "word" / "document.xml" - if not original_file.exists(): - print( - f"FAILED - Original document.xml not found in {self.original_docx}" - ) - return False - - # Parse both XML files using xml.etree.ElementTree for redlining validation - try: - import xml.etree.ElementTree as ET - - modified_tree = ET.parse(modified_file) - modified_root = modified_tree.getroot() - original_tree = ET.parse(original_file) - original_root = original_tree.getroot() - except ET.ParseError as e: - print(f"FAILED - Error parsing XML files: {e}") - return False - - # Remove Claude's tracked changes from both documents - self._remove_claude_tracked_changes(original_root) - self._remove_claude_tracked_changes(modified_root) - - # Extract and compare text content - modified_text = self._extract_text_content(modified_root) - original_text = self._extract_text_content(original_root) - - if modified_text != original_text: - # Show detailed character-level differences for each paragraph - error_message = self._generate_detailed_diff( - original_text, modified_text - ) - print(error_message) - return False - - if self.verbose: - print("PASSED - All changes by Claude are properly tracked") - return True - - def _generate_detailed_diff(self, original_text, modified_text): - """Generate detailed word-level differences using git word diff.""" - error_parts = [ - "FAILED - Document text doesn't match after removing Claude's tracked changes", - "", - "Likely causes:", - " 1. Modified text inside another author's or tags", - " 2. Made edits without proper tracked changes", - " 3. Didn't nest inside when deleting another's insertion", - "", - "For pre-redlined documents, use correct patterns:", - " - To reject another's INSERTION: Nest inside their ", - " - To restore another's DELETION: Add new AFTER their ", - "", - ] - - # Show git word diff - git_diff = self._get_git_word_diff(original_text, modified_text) - if git_diff: - error_parts.extend(["Differences:", "============", git_diff]) - else: - error_parts.append("Unable to generate word diff (git not available)") - - return "\n".join(error_parts) - - def _get_git_word_diff(self, original_text, modified_text): - """Generate word diff using git with character-level precision.""" - try: - with tempfile.TemporaryDirectory() as temp_dir: - temp_path = Path(temp_dir) - - # Create two files - original_file = temp_path / "original.txt" - modified_file = temp_path / "modified.txt" - - original_file.write_text(original_text, encoding="utf-8") - modified_file.write_text(modified_text, encoding="utf-8") - - # Try character-level diff first for precise differences - result = subprocess.run( - [ - "git", - "diff", - "--word-diff=plain", - "--word-diff-regex=.", # Character-by-character diff - "-U0", # Zero lines of context - show only changed lines - "--no-index", - str(original_file), - str(modified_file), - ], - capture_output=True, - text=True, - ) - - if result.stdout.strip(): - # Clean up the output - remove git diff header lines - lines = result.stdout.split("\n") - # Skip the header lines (diff --git, index, +++, ---, @@) - content_lines = [] - in_content = False - for line in lines: - if line.startswith("@@"): - in_content = True - continue - if in_content and line.strip(): - content_lines.append(line) - - if content_lines: - return "\n".join(content_lines) - - # Fallback to word-level diff if character-level is too verbose - result = subprocess.run( - [ - "git", - "diff", - "--word-diff=plain", - "-U0", # Zero lines of context - "--no-index", - str(original_file), - str(modified_file), - ], - capture_output=True, - text=True, - ) - - if result.stdout.strip(): - lines = result.stdout.split("\n") - content_lines = [] - in_content = False - for line in lines: - if line.startswith("@@"): - in_content = True - continue - if in_content and line.strip(): - content_lines.append(line) - return "\n".join(content_lines) - - except (subprocess.CalledProcessError, FileNotFoundError, Exception): - # Git not available or other error, return None to use fallback - pass - - return None - - def _remove_claude_tracked_changes(self, root): - """Remove tracked changes authored by Claude from the XML root.""" - ins_tag = f"{{{self.namespaces['w']}}}ins" - del_tag = f"{{{self.namespaces['w']}}}del" - author_attr = f"{{{self.namespaces['w']}}}author" - - # Remove w:ins elements - for parent in root.iter(): - to_remove = [] - for child in parent: - if child.tag == ins_tag and child.get(author_attr) == "Claude": - to_remove.append(child) - for elem in to_remove: - parent.remove(elem) - - # Unwrap content in w:del elements where author is "Claude" - deltext_tag = f"{{{self.namespaces['w']}}}delText" - t_tag = f"{{{self.namespaces['w']}}}t" - - for parent in root.iter(): - to_process = [] - for child in parent: - if child.tag == del_tag and child.get(author_attr) == "Claude": - to_process.append((child, list(parent).index(child))) - - # Process in reverse order to maintain indices - for del_elem, del_index in reversed(to_process): - # Convert w:delText to w:t before moving - for elem in del_elem.iter(): - if elem.tag == deltext_tag: - elem.tag = t_tag - - # Move all children of w:del to its parent before removing w:del - for child in reversed(list(del_elem)): - parent.insert(del_index, child) - parent.remove(del_elem) - - def _extract_text_content(self, root): - """Extract text content from Word XML, preserving paragraph structure. - - Empty paragraphs are skipped to avoid false positives when tracked - insertions add only structural elements without text content. - """ - p_tag = f"{{{self.namespaces['w']}}}p" - t_tag = f"{{{self.namespaces['w']}}}t" - - paragraphs = [] - for p_elem in root.findall(f".//{p_tag}"): - # Get all text elements within this paragraph - text_parts = [] - for t_elem in p_elem.findall(f".//{t_tag}"): - if t_elem.text: - text_parts.append(t_elem.text) - paragraph_text = "".join(text_parts) - # Skip empty paragraphs - they don't affect content validation - if paragraph_text: - paragraphs.append(paragraph_text) - - return "\n".join(paragraphs) - - -if __name__ == "__main__": - raise RuntimeError("This module should not be run directly.") diff --git a/.claude/skills/docx/scripts/__init__.py b/.claude/skills/docx/scripts/__init__.py deleted file mode 100644 index bf9c56272f..0000000000 --- a/.claude/skills/docx/scripts/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Make scripts directory a package for relative imports in tests diff --git a/.claude/skills/docx/scripts/document.py b/.claude/skills/docx/scripts/document.py deleted file mode 100644 index ae9328ddf3..0000000000 --- a/.claude/skills/docx/scripts/document.py +++ /dev/null @@ -1,1276 +0,0 @@ -#!/usr/bin/env python3 -""" -Library for working with Word documents: comments, tracked changes, and editing. - -Usage: - from skills.docx.scripts.document import Document - - # Initialize - doc = Document('workspace/unpacked') - doc = Document('workspace/unpacked', author="John Doe", initials="JD") - - # Find nodes - node = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "1"}) - node = doc["word/document.xml"].get_node(tag="w:p", line_number=10) - - # Add comments - doc.add_comment(start=node, end=node, text="Comment text") - doc.reply_to_comment(parent_comment_id=0, text="Reply text") - - # Suggest tracked changes - doc["word/document.xml"].suggest_deletion(node) # Delete content - doc["word/document.xml"].revert_insertion(ins_node) # Reject insertion - doc["word/document.xml"].revert_deletion(del_node) # Reject deletion - - # Save - doc.save() -""" - -import html -import random -import shutil -import tempfile -from datetime import datetime, timezone -from pathlib import Path - -from defusedxml import minidom -from ooxml.scripts.pack import pack_document -from ooxml.scripts.validation.docx import DOCXSchemaValidator -from ooxml.scripts.validation.redlining import RedliningValidator - -from .utilities import XMLEditor - -# Path to template files -TEMPLATE_DIR = Path(__file__).parent / "templates" - - -class DocxXMLEditor(XMLEditor): - """XMLEditor that automatically applies RSID, author, and date to new elements. - - Automatically adds attributes to elements that support them when inserting new content: - - w:rsidR, w:rsidRDefault, w:rsidP (for w:p and w:r elements) - - w:author and w:date (for w:ins, w:del, w:comment elements) - - w:id (for w:ins and w:del elements) - - Attributes: - dom (defusedxml.minidom.Document): The DOM document for direct manipulation - """ - - def __init__( - self, xml_path, rsid: str, author: str = "Claude", initials: str = "C" - ): - """Initialize with required RSID and optional author. - - Args: - xml_path: Path to XML file to edit - rsid: RSID to automatically apply to new elements - author: Author name for tracked changes and comments (default: "Claude") - initials: Author initials (default: "C") - """ - super().__init__(xml_path) - self.rsid = rsid - self.author = author - self.initials = initials - - def _get_next_change_id(self): - """Get the next available change ID by checking all tracked change elements.""" - max_id = -1 - for tag in ("w:ins", "w:del"): - elements = self.dom.getElementsByTagName(tag) - for elem in elements: - change_id = elem.getAttribute("w:id") - if change_id: - try: - max_id = max(max_id, int(change_id)) - except ValueError: - pass - return max_id + 1 - - def _ensure_w16du_namespace(self): - """Ensure w16du namespace is declared on the root element.""" - root = self.dom.documentElement - if not root.hasAttribute("xmlns:w16du"): # type: ignore - root.setAttribute( # type: ignore - "xmlns:w16du", - "http://schemas.microsoft.com/office/word/2023/wordml/word16du", - ) - - def _ensure_w16cex_namespace(self): - """Ensure w16cex namespace is declared on the root element.""" - root = self.dom.documentElement - if not root.hasAttribute("xmlns:w16cex"): # type: ignore - root.setAttribute( # type: ignore - "xmlns:w16cex", - "http://schemas.microsoft.com/office/word/2018/wordml/cex", - ) - - def _ensure_w14_namespace(self): - """Ensure w14 namespace is declared on the root element.""" - root = self.dom.documentElement - if not root.hasAttribute("xmlns:w14"): # type: ignore - root.setAttribute( # type: ignore - "xmlns:w14", - "http://schemas.microsoft.com/office/word/2010/wordml", - ) - - def _inject_attributes_to_nodes(self, nodes): - """Inject RSID, author, and date attributes into DOM nodes where applicable. - - Adds attributes to elements that support them: - - w:r: gets w:rsidR (or w:rsidDel if inside w:del) - - w:p: gets w:rsidR, w:rsidRDefault, w:rsidP, w14:paraId, w14:textId - - w:t: gets xml:space="preserve" if text has leading/trailing whitespace - - w:ins, w:del: get w:id, w:author, w:date, w16du:dateUtc - - w:comment: gets w:author, w:date, w:initials - - w16cex:commentExtensible: gets w16cex:dateUtc - - Args: - nodes: List of DOM nodes to process - """ - from datetime import datetime, timezone - - timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") - - def is_inside_deletion(elem): - """Check if element is inside a w:del element.""" - parent = elem.parentNode - while parent: - if parent.nodeType == parent.ELEMENT_NODE and parent.tagName == "w:del": - return True - parent = parent.parentNode - return False - - def add_rsid_to_p(elem): - if not elem.hasAttribute("w:rsidR"): - elem.setAttribute("w:rsidR", self.rsid) - if not elem.hasAttribute("w:rsidRDefault"): - elem.setAttribute("w:rsidRDefault", self.rsid) - if not elem.hasAttribute("w:rsidP"): - elem.setAttribute("w:rsidP", self.rsid) - # Add w14:paraId and w14:textId if not present - if not elem.hasAttribute("w14:paraId"): - self._ensure_w14_namespace() - elem.setAttribute("w14:paraId", _generate_hex_id()) - if not elem.hasAttribute("w14:textId"): - self._ensure_w14_namespace() - elem.setAttribute("w14:textId", _generate_hex_id()) - - def add_rsid_to_r(elem): - # Use w:rsidDel for inside , otherwise w:rsidR - if is_inside_deletion(elem): - if not elem.hasAttribute("w:rsidDel"): - elem.setAttribute("w:rsidDel", self.rsid) - else: - if not elem.hasAttribute("w:rsidR"): - elem.setAttribute("w:rsidR", self.rsid) - - def add_tracked_change_attrs(elem): - # Auto-assign w:id if not present - if not elem.hasAttribute("w:id"): - elem.setAttribute("w:id", str(self._get_next_change_id())) - if not elem.hasAttribute("w:author"): - elem.setAttribute("w:author", self.author) - if not elem.hasAttribute("w:date"): - elem.setAttribute("w:date", timestamp) - # Add w16du:dateUtc for tracked changes (same as w:date since we generate UTC timestamps) - if elem.tagName in ("w:ins", "w:del") and not elem.hasAttribute( - "w16du:dateUtc" - ): - self._ensure_w16du_namespace() - elem.setAttribute("w16du:dateUtc", timestamp) - - def add_comment_attrs(elem): - if not elem.hasAttribute("w:author"): - elem.setAttribute("w:author", self.author) - if not elem.hasAttribute("w:date"): - elem.setAttribute("w:date", timestamp) - if not elem.hasAttribute("w:initials"): - elem.setAttribute("w:initials", self.initials) - - def add_comment_extensible_date(elem): - # Add w16cex:dateUtc for comment extensible elements - if not elem.hasAttribute("w16cex:dateUtc"): - self._ensure_w16cex_namespace() - elem.setAttribute("w16cex:dateUtc", timestamp) - - def add_xml_space_to_t(elem): - # Add xml:space="preserve" to w:t if text has leading/trailing whitespace - if ( - elem.firstChild - and elem.firstChild.nodeType == elem.firstChild.TEXT_NODE - ): - text = elem.firstChild.data - if text and (text[0].isspace() or text[-1].isspace()): - if not elem.hasAttribute("xml:space"): - elem.setAttribute("xml:space", "preserve") - - for node in nodes: - if node.nodeType != node.ELEMENT_NODE: - continue - - # Handle the node itself - if node.tagName == "w:p": - add_rsid_to_p(node) - elif node.tagName == "w:r": - add_rsid_to_r(node) - elif node.tagName == "w:t": - add_xml_space_to_t(node) - elif node.tagName in ("w:ins", "w:del"): - add_tracked_change_attrs(node) - elif node.tagName == "w:comment": - add_comment_attrs(node) - elif node.tagName == "w16cex:commentExtensible": - add_comment_extensible_date(node) - - # Process descendants (getElementsByTagName doesn't return the element itself) - for elem in node.getElementsByTagName("w:p"): - add_rsid_to_p(elem) - for elem in node.getElementsByTagName("w:r"): - add_rsid_to_r(elem) - for elem in node.getElementsByTagName("w:t"): - add_xml_space_to_t(elem) - for tag in ("w:ins", "w:del"): - for elem in node.getElementsByTagName(tag): - add_tracked_change_attrs(elem) - for elem in node.getElementsByTagName("w:comment"): - add_comment_attrs(elem) - for elem in node.getElementsByTagName("w16cex:commentExtensible"): - add_comment_extensible_date(elem) - - def replace_node(self, elem, new_content): - """Replace node with automatic attribute injection.""" - nodes = super().replace_node(elem, new_content) - self._inject_attributes_to_nodes(nodes) - return nodes - - def insert_after(self, elem, xml_content): - """Insert after with automatic attribute injection.""" - nodes = super().insert_after(elem, xml_content) - self._inject_attributes_to_nodes(nodes) - return nodes - - def insert_before(self, elem, xml_content): - """Insert before with automatic attribute injection.""" - nodes = super().insert_before(elem, xml_content) - self._inject_attributes_to_nodes(nodes) - return nodes - - def append_to(self, elem, xml_content): - """Append to with automatic attribute injection.""" - nodes = super().append_to(elem, xml_content) - self._inject_attributes_to_nodes(nodes) - return nodes - - def revert_insertion(self, elem): - """Reject an insertion by wrapping its content in a deletion. - - Wraps all runs inside w:ins in w:del, converting w:t to w:delText. - Can process a single w:ins element or a container element with multiple w:ins. - - Args: - elem: Element to process (w:ins, w:p, w:body, etc.) - - Returns: - list: List containing the processed element(s) - - Raises: - ValueError: If the element contains no w:ins elements - - Example: - # Reject a single insertion - ins = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "5"}) - doc["word/document.xml"].revert_insertion(ins) - - # Reject all insertions in a paragraph - para = doc["word/document.xml"].get_node(tag="w:p", line_number=42) - doc["word/document.xml"].revert_insertion(para) - """ - # Collect insertions - ins_elements = [] - if elem.tagName == "w:ins": - ins_elements.append(elem) - else: - ins_elements.extend(elem.getElementsByTagName("w:ins")) - - # Validate that there are insertions to reject - if not ins_elements: - raise ValueError( - f"revert_insertion requires w:ins elements. " - f"The provided element <{elem.tagName}> contains no insertions. " - ) - - # Process all insertions - wrap all children in w:del - for ins_elem in ins_elements: - runs = list(ins_elem.getElementsByTagName("w:r")) - if not runs: - continue - - # Create deletion wrapper - del_wrapper = self.dom.createElement("w:del") - - # Process each run - for run in runs: - # Convert w:t → w:delText and w:rsidR → w:rsidDel - if run.hasAttribute("w:rsidR"): - run.setAttribute("w:rsidDel", run.getAttribute("w:rsidR")) - run.removeAttribute("w:rsidR") - elif not run.hasAttribute("w:rsidDel"): - run.setAttribute("w:rsidDel", self.rsid) - - for t_elem in list(run.getElementsByTagName("w:t")): - del_text = self.dom.createElement("w:delText") - # Copy ALL child nodes (not just firstChild) to handle entities - while t_elem.firstChild: - del_text.appendChild(t_elem.firstChild) - for i in range(t_elem.attributes.length): - attr = t_elem.attributes.item(i) - del_text.setAttribute(attr.name, attr.value) - t_elem.parentNode.replaceChild(del_text, t_elem) - - # Move all children from ins to del wrapper - while ins_elem.firstChild: - del_wrapper.appendChild(ins_elem.firstChild) - - # Add del wrapper back to ins - ins_elem.appendChild(del_wrapper) - - # Inject attributes to the deletion wrapper - self._inject_attributes_to_nodes([del_wrapper]) - - return [elem] - - def revert_deletion(self, elem): - """Reject a deletion by re-inserting the deleted content. - - Creates w:ins elements after each w:del, copying deleted content and - converting w:delText back to w:t. - Can process a single w:del element or a container element with multiple w:del. - - Args: - elem: Element to process (w:del, w:p, w:body, etc.) - - Returns: - list: If elem is w:del, returns [elem, new_ins]. Otherwise returns [elem]. - - Raises: - ValueError: If the element contains no w:del elements - - Example: - # Reject a single deletion - returns [w:del, w:ins] - del_elem = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "3"}) - nodes = doc["word/document.xml"].revert_deletion(del_elem) - - # Reject all deletions in a paragraph - returns [para] - para = doc["word/document.xml"].get_node(tag="w:p", line_number=42) - nodes = doc["word/document.xml"].revert_deletion(para) - """ - # Collect deletions FIRST - before we modify the DOM - del_elements = [] - is_single_del = elem.tagName == "w:del" - - if is_single_del: - del_elements.append(elem) - else: - del_elements.extend(elem.getElementsByTagName("w:del")) - - # Validate that there are deletions to reject - if not del_elements: - raise ValueError( - f"revert_deletion requires w:del elements. " - f"The provided element <{elem.tagName}> contains no deletions. " - ) - - # Track created insertion (only relevant if elem is a single w:del) - created_insertion = None - - # Process all deletions - create insertions that copy the deleted content - for del_elem in del_elements: - # Clone the deleted runs and convert them to insertions - runs = list(del_elem.getElementsByTagName("w:r")) - if not runs: - continue - - # Create insertion wrapper - ins_elem = self.dom.createElement("w:ins") - - for run in runs: - # Clone the run - new_run = run.cloneNode(True) - - # Convert w:delText → w:t - for del_text in list(new_run.getElementsByTagName("w:delText")): - t_elem = self.dom.createElement("w:t") - # Copy ALL child nodes (not just firstChild) to handle entities - while del_text.firstChild: - t_elem.appendChild(del_text.firstChild) - for i in range(del_text.attributes.length): - attr = del_text.attributes.item(i) - t_elem.setAttribute(attr.name, attr.value) - del_text.parentNode.replaceChild(t_elem, del_text) - - # Update run attributes: w:rsidDel → w:rsidR - if new_run.hasAttribute("w:rsidDel"): - new_run.setAttribute("w:rsidR", new_run.getAttribute("w:rsidDel")) - new_run.removeAttribute("w:rsidDel") - elif not new_run.hasAttribute("w:rsidR"): - new_run.setAttribute("w:rsidR", self.rsid) - - ins_elem.appendChild(new_run) - - # Insert the new insertion after the deletion - nodes = self.insert_after(del_elem, ins_elem.toxml()) - - # If processing a single w:del, track the created insertion - if is_single_del and nodes: - created_insertion = nodes[0] - - # Return based on input type - if is_single_del and created_insertion: - return [elem, created_insertion] - else: - return [elem] - - @staticmethod - def suggest_paragraph(xml_content: str) -> str: - """Transform paragraph XML to add tracked change wrapping for insertion. - - Wraps runs in and adds to w:rPr in w:pPr for numbered lists. - - Args: - xml_content: XML string containing a element - - Returns: - str: Transformed XML with tracked change wrapping - """ - wrapper = f'{xml_content}' - doc = minidom.parseString(wrapper) - para = doc.getElementsByTagName("w:p")[0] - - # Ensure w:pPr exists - pPr_list = para.getElementsByTagName("w:pPr") - if not pPr_list: - pPr = doc.createElement("w:pPr") - para.insertBefore( - pPr, para.firstChild - ) if para.firstChild else para.appendChild(pPr) - else: - pPr = pPr_list[0] - - # Ensure w:rPr exists in w:pPr - rPr_list = pPr.getElementsByTagName("w:rPr") - if not rPr_list: - rPr = doc.createElement("w:rPr") - pPr.appendChild(rPr) - else: - rPr = rPr_list[0] - - # Add to w:rPr - ins_marker = doc.createElement("w:ins") - rPr.insertBefore( - ins_marker, rPr.firstChild - ) if rPr.firstChild else rPr.appendChild(ins_marker) - - # Wrap all non-pPr children in - ins_wrapper = doc.createElement("w:ins") - for child in [c for c in para.childNodes if c.nodeName != "w:pPr"]: - para.removeChild(child) - ins_wrapper.appendChild(child) - para.appendChild(ins_wrapper) - - return para.toxml() - - def suggest_deletion(self, elem): - """Mark a w:r or w:p element as deleted with tracked changes (in-place DOM manipulation). - - For w:r: wraps in , converts to , preserves w:rPr - For w:p (regular): wraps content in , converts to - For w:p (numbered list): adds to w:rPr in w:pPr, wraps content in - - Args: - elem: A w:r or w:p DOM element without existing tracked changes - - Returns: - Element: The modified element - - Raises: - ValueError: If element has existing tracked changes or invalid structure - """ - if elem.nodeName == "w:r": - # Check for existing w:delText - if elem.getElementsByTagName("w:delText"): - raise ValueError("w:r element already contains w:delText") - - # Convert w:t → w:delText - for t_elem in list(elem.getElementsByTagName("w:t")): - del_text = self.dom.createElement("w:delText") - # Copy ALL child nodes (not just firstChild) to handle entities - while t_elem.firstChild: - del_text.appendChild(t_elem.firstChild) - # Preserve attributes like xml:space - for i in range(t_elem.attributes.length): - attr = t_elem.attributes.item(i) - del_text.setAttribute(attr.name, attr.value) - t_elem.parentNode.replaceChild(del_text, t_elem) - - # Update run attributes: w:rsidR → w:rsidDel - if elem.hasAttribute("w:rsidR"): - elem.setAttribute("w:rsidDel", elem.getAttribute("w:rsidR")) - elem.removeAttribute("w:rsidR") - elif not elem.hasAttribute("w:rsidDel"): - elem.setAttribute("w:rsidDel", self.rsid) - - # Wrap in w:del - del_wrapper = self.dom.createElement("w:del") - parent = elem.parentNode - parent.insertBefore(del_wrapper, elem) - parent.removeChild(elem) - del_wrapper.appendChild(elem) - - # Inject attributes to the deletion wrapper - self._inject_attributes_to_nodes([del_wrapper]) - - return del_wrapper - - elif elem.nodeName == "w:p": - # Check for existing tracked changes - if elem.getElementsByTagName("w:ins") or elem.getElementsByTagName("w:del"): - raise ValueError("w:p element already contains tracked changes") - - # Check if it's a numbered list item - pPr_list = elem.getElementsByTagName("w:pPr") - is_numbered = pPr_list and pPr_list[0].getElementsByTagName("w:numPr") - - if is_numbered: - # Add to w:rPr in w:pPr - pPr = pPr_list[0] - rPr_list = pPr.getElementsByTagName("w:rPr") - - if not rPr_list: - rPr = self.dom.createElement("w:rPr") - pPr.appendChild(rPr) - else: - rPr = rPr_list[0] - - # Add marker - del_marker = self.dom.createElement("w:del") - rPr.insertBefore( - del_marker, rPr.firstChild - ) if rPr.firstChild else rPr.appendChild(del_marker) - - # Convert w:t → w:delText in all runs - for t_elem in list(elem.getElementsByTagName("w:t")): - del_text = self.dom.createElement("w:delText") - # Copy ALL child nodes (not just firstChild) to handle entities - while t_elem.firstChild: - del_text.appendChild(t_elem.firstChild) - # Preserve attributes like xml:space - for i in range(t_elem.attributes.length): - attr = t_elem.attributes.item(i) - del_text.setAttribute(attr.name, attr.value) - t_elem.parentNode.replaceChild(del_text, t_elem) - - # Update run attributes: w:rsidR → w:rsidDel - for run in elem.getElementsByTagName("w:r"): - if run.hasAttribute("w:rsidR"): - run.setAttribute("w:rsidDel", run.getAttribute("w:rsidR")) - run.removeAttribute("w:rsidR") - elif not run.hasAttribute("w:rsidDel"): - run.setAttribute("w:rsidDel", self.rsid) - - # Wrap all non-pPr children in - del_wrapper = self.dom.createElement("w:del") - for child in [c for c in elem.childNodes if c.nodeName != "w:pPr"]: - elem.removeChild(child) - del_wrapper.appendChild(child) - elem.appendChild(del_wrapper) - - # Inject attributes to the deletion wrapper - self._inject_attributes_to_nodes([del_wrapper]) - - return elem - - else: - raise ValueError(f"Element must be w:r or w:p, got {elem.nodeName}") - - -def _generate_hex_id() -> str: - """Generate random 8-character hex ID for para/durable IDs. - - Values are constrained to be less than 0x7FFFFFFF per OOXML spec: - - paraId must be < 0x80000000 - - durableId must be < 0x7FFFFFFF - We use the stricter constraint (0x7FFFFFFF) for both. - """ - return f"{random.randint(1, 0x7FFFFFFE):08X}" - - -def _generate_rsid() -> str: - """Generate random 8-character hex RSID.""" - return "".join(random.choices("0123456789ABCDEF", k=8)) - - -class Document: - """Manages comments in unpacked Word documents.""" - - def __init__( - self, - unpacked_dir, - rsid=None, - track_revisions=False, - author="Claude", - initials="C", - ): - """ - Initialize with path to unpacked Word document directory. - Automatically sets up comment infrastructure (people.xml, RSIDs). - - Args: - unpacked_dir: Path to unpacked DOCX directory (must contain word/ subdirectory) - rsid: Optional RSID to use for all comment elements. If not provided, one will be generated. - track_revisions: If True, enables track revisions in settings.xml (default: False) - author: Default author name for comments (default: "Claude") - initials: Default author initials for comments (default: "C") - """ - self.original_path = Path(unpacked_dir) - - if not self.original_path.exists() or not self.original_path.is_dir(): - raise ValueError(f"Directory not found: {unpacked_dir}") - - # Create temporary directory with subdirectories for unpacked content and baseline - self.temp_dir = tempfile.mkdtemp(prefix="docx_") - self.unpacked_path = Path(self.temp_dir) / "unpacked" - shutil.copytree(self.original_path, self.unpacked_path) - - # Pack original directory into temporary .docx for validation baseline (outside unpacked dir) - self.original_docx = Path(self.temp_dir) / "original.docx" - pack_document(self.original_path, self.original_docx, validate=False) - - self.word_path = self.unpacked_path / "word" - - # Generate RSID if not provided - self.rsid = rsid if rsid else _generate_rsid() - print(f"Using RSID: {self.rsid}") - - # Set default author and initials - self.author = author - self.initials = initials - - # Cache for lazy-loaded editors - self._editors = {} - - # Comment file paths - self.comments_path = self.word_path / "comments.xml" - self.comments_extended_path = self.word_path / "commentsExtended.xml" - self.comments_ids_path = self.word_path / "commentsIds.xml" - self.comments_extensible_path = self.word_path / "commentsExtensible.xml" - - # Load existing comments and determine next ID (before setup modifies files) - self.existing_comments = self._load_existing_comments() - self.next_comment_id = self._get_next_comment_id() - - # Convenient access to document.xml editor (semi-private) - self._document = self["word/document.xml"] - - # Setup tracked changes infrastructure - self._setup_tracking(track_revisions=track_revisions) - - # Add author to people.xml - self._add_author_to_people(author) - - def __getitem__(self, xml_path: str) -> DocxXMLEditor: - """ - Get or create a DocxXMLEditor for the specified XML file. - - Enables lazy-loaded editors with bracket notation: - node = doc["word/document.xml"].get_node(tag="w:p", line_number=42) - - Args: - xml_path: Relative path to XML file (e.g., "word/document.xml", "word/comments.xml") - - Returns: - DocxXMLEditor instance for the specified file - - Raises: - ValueError: If the file does not exist - - Example: - # Get node from document.xml - node = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "1"}) - - # Get node from comments.xml - comment = doc["word/comments.xml"].get_node(tag="w:comment", attrs={"w:id": "0"}) - """ - if xml_path not in self._editors: - file_path = self.unpacked_path / xml_path - if not file_path.exists(): - raise ValueError(f"XML file not found: {xml_path}") - # Use DocxXMLEditor with RSID, author, and initials for all editors - self._editors[xml_path] = DocxXMLEditor( - file_path, rsid=self.rsid, author=self.author, initials=self.initials - ) - return self._editors[xml_path] - - def add_comment(self, start, end, text: str) -> int: - """ - Add a comment spanning from one element to another. - - Args: - start: DOM element for the starting point - end: DOM element for the ending point - text: Comment content - - Returns: - The comment ID that was created - - Example: - start_node = cm.get_document_node(tag="w:del", id="1") - end_node = cm.get_document_node(tag="w:ins", id="2") - cm.add_comment(start=start_node, end=end_node, text="Explanation") - """ - comment_id = self.next_comment_id - para_id = _generate_hex_id() - durable_id = _generate_hex_id() - timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") - - # Add comment ranges to document.xml immediately - self._document.insert_before(start, self._comment_range_start_xml(comment_id)) - - # If end node is a paragraph, append comment markup inside it - # Otherwise insert after it (for run-level anchors) - if end.tagName == "w:p": - self._document.append_to(end, self._comment_range_end_xml(comment_id)) - else: - self._document.insert_after(end, self._comment_range_end_xml(comment_id)) - - # Add to comments.xml immediately - self._add_to_comments_xml( - comment_id, para_id, text, self.author, self.initials, timestamp - ) - - # Add to commentsExtended.xml immediately - self._add_to_comments_extended_xml(para_id, parent_para_id=None) - - # Add to commentsIds.xml immediately - self._add_to_comments_ids_xml(para_id, durable_id) - - # Add to commentsExtensible.xml immediately - self._add_to_comments_extensible_xml(durable_id) - - # Update existing_comments so replies work - self.existing_comments[comment_id] = {"para_id": para_id} - - self.next_comment_id += 1 - return comment_id - - def reply_to_comment( - self, - parent_comment_id: int, - text: str, - ) -> int: - """ - Add a reply to an existing comment. - - Args: - parent_comment_id: The w:id of the parent comment to reply to - text: Reply text - - Returns: - The comment ID that was created for the reply - - Example: - cm.reply_to_comment(parent_comment_id=0, text="I agree with this change") - """ - if parent_comment_id not in self.existing_comments: - raise ValueError(f"Parent comment with id={parent_comment_id} not found") - - parent_info = self.existing_comments[parent_comment_id] - comment_id = self.next_comment_id - para_id = _generate_hex_id() - durable_id = _generate_hex_id() - timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") - - # Add comment ranges to document.xml immediately - parent_start_elem = self._document.get_node( - tag="w:commentRangeStart", attrs={"w:id": str(parent_comment_id)} - ) - parent_ref_elem = self._document.get_node( - tag="w:commentReference", attrs={"w:id": str(parent_comment_id)} - ) - - self._document.insert_after( - parent_start_elem, self._comment_range_start_xml(comment_id) - ) - parent_ref_run = parent_ref_elem.parentNode - self._document.insert_after( - parent_ref_run, f'' - ) - self._document.insert_after( - parent_ref_run, self._comment_ref_run_xml(comment_id) - ) - - # Add to comments.xml immediately - self._add_to_comments_xml( - comment_id, para_id, text, self.author, self.initials, timestamp - ) - - # Add to commentsExtended.xml immediately (with parent) - self._add_to_comments_extended_xml( - para_id, parent_para_id=parent_info["para_id"] - ) - - # Add to commentsIds.xml immediately - self._add_to_comments_ids_xml(para_id, durable_id) - - # Add to commentsExtensible.xml immediately - self._add_to_comments_extensible_xml(durable_id) - - # Update existing_comments so replies work - self.existing_comments[comment_id] = {"para_id": para_id} - - self.next_comment_id += 1 - return comment_id - - def __del__(self): - """Clean up temporary directory on deletion.""" - if hasattr(self, "temp_dir") and Path(self.temp_dir).exists(): - shutil.rmtree(self.temp_dir) - - def validate(self) -> None: - """ - Validate the document against XSD schema and redlining rules. - - Raises: - ValueError: If validation fails. - """ - # Create validators with current state - schema_validator = DOCXSchemaValidator( - self.unpacked_path, self.original_docx, verbose=False - ) - redlining_validator = RedliningValidator( - self.unpacked_path, self.original_docx, verbose=False - ) - - # Run validations - if not schema_validator.validate(): - raise ValueError("Schema validation failed") - if not redlining_validator.validate(): - raise ValueError("Redlining validation failed") - - def save(self, destination=None, validate=True) -> None: - """ - Save all modified XML files to disk and copy to destination directory. - - This persists all changes made via add_comment() and reply_to_comment(). - - Args: - destination: Optional path to save to. If None, saves back to original directory. - validate: If True, validates document before saving (default: True). - """ - # Only ensure comment relationships and content types if comment files exist - if self.comments_path.exists(): - self._ensure_comment_relationships() - self._ensure_comment_content_types() - - # Save all modified XML files in temp directory - for editor in self._editors.values(): - editor.save() - - # Validate by default - if validate: - self.validate() - - # Copy contents from temp directory to destination (or original directory) - target_path = Path(destination) if destination else self.original_path - shutil.copytree(self.unpacked_path, target_path, dirs_exist_ok=True) - - # ==================== Private: Initialization ==================== - - def _get_next_comment_id(self): - """Get the next available comment ID.""" - if not self.comments_path.exists(): - return 0 - - editor = self["word/comments.xml"] - max_id = -1 - for comment_elem in editor.dom.getElementsByTagName("w:comment"): - comment_id = comment_elem.getAttribute("w:id") - if comment_id: - try: - max_id = max(max_id, int(comment_id)) - except ValueError: - pass - return max_id + 1 - - def _load_existing_comments(self): - """Load existing comments from files to enable replies.""" - if not self.comments_path.exists(): - return {} - - editor = self["word/comments.xml"] - existing = {} - - for comment_elem in editor.dom.getElementsByTagName("w:comment"): - comment_id = comment_elem.getAttribute("w:id") - if not comment_id: - continue - - # Find para_id from the w:p element within the comment - para_id = None - for p_elem in comment_elem.getElementsByTagName("w:p"): - para_id = p_elem.getAttribute("w14:paraId") - if para_id: - break - - if not para_id: - continue - - existing[int(comment_id)] = {"para_id": para_id} - - return existing - - # ==================== Private: Setup Methods ==================== - - def _setup_tracking(self, track_revisions=False): - """Set up comment infrastructure in unpacked directory. - - Args: - track_revisions: If True, enables track revisions in settings.xml - """ - # Create or update word/people.xml - people_file = self.word_path / "people.xml" - self._update_people_xml(people_file) - - # Update XML files - self._add_content_type_for_people(self.unpacked_path / "[Content_Types].xml") - self._add_relationship_for_people( - self.word_path / "_rels" / "document.xml.rels" - ) - - # Always add RSID to settings.xml, optionally enable trackRevisions - self._update_settings( - self.word_path / "settings.xml", track_revisions=track_revisions - ) - - def _update_people_xml(self, path): - """Create people.xml if it doesn't exist.""" - if not path.exists(): - # Copy from template - shutil.copy(TEMPLATE_DIR / "people.xml", path) - - def _add_content_type_for_people(self, path): - """Add people.xml content type to [Content_Types].xml if not already present.""" - editor = self["[Content_Types].xml"] - - if self._has_override(editor, "/word/people.xml"): - return - - # Add Override element - root = editor.dom.documentElement - override_xml = '' - editor.append_to(root, override_xml) - - def _add_relationship_for_people(self, path): - """Add people.xml relationship to document.xml.rels if not already present.""" - editor = self["word/_rels/document.xml.rels"] - - if self._has_relationship(editor, "people.xml"): - return - - root = editor.dom.documentElement - root_tag = root.tagName # type: ignore - prefix = root_tag.split(":")[0] + ":" if ":" in root_tag else "" - next_rid = editor.get_next_rid() - - # Create the relationship entry - rel_xml = f'<{prefix}Relationship Id="{next_rid}" Type="http://schemas.microsoft.com/office/2011/relationships/people" Target="people.xml"/>' - editor.append_to(root, rel_xml) - - def _update_settings(self, path, track_revisions=False): - """Add RSID and optionally enable track revisions in settings.xml. - - Args: - path: Path to settings.xml - track_revisions: If True, adds trackRevisions element - - Places elements per OOXML schema order: - - trackRevisions: early (before defaultTabStop) - - rsids: late (after compat) - """ - editor = self["word/settings.xml"] - root = editor.get_node(tag="w:settings") - prefix = root.tagName.split(":")[0] if ":" in root.tagName else "w" - - # Conditionally add trackRevisions if requested - if track_revisions: - track_revisions_exists = any( - elem.tagName == f"{prefix}:trackRevisions" - for elem in editor.dom.getElementsByTagName(f"{prefix}:trackRevisions") - ) - - if not track_revisions_exists: - track_rev_xml = f"<{prefix}:trackRevisions/>" - # Try to insert before documentProtection, defaultTabStop, or at start - inserted = False - for tag in [f"{prefix}:documentProtection", f"{prefix}:defaultTabStop"]: - elements = editor.dom.getElementsByTagName(tag) - if elements: - editor.insert_before(elements[0], track_rev_xml) - inserted = True - break - if not inserted: - # Insert as first child of settings - if root.firstChild: - editor.insert_before(root.firstChild, track_rev_xml) - else: - editor.append_to(root, track_rev_xml) - - # Always check if rsids section exists - rsids_elements = editor.dom.getElementsByTagName(f"{prefix}:rsids") - - if not rsids_elements: - # Add new rsids section - rsids_xml = f'''<{prefix}:rsids> - <{prefix}:rsidRoot {prefix}:val="{self.rsid}"/> - <{prefix}:rsid {prefix}:val="{self.rsid}"/> -''' - - # Try to insert after compat, before clrSchemeMapping, or before closing tag - inserted = False - compat_elements = editor.dom.getElementsByTagName(f"{prefix}:compat") - if compat_elements: - editor.insert_after(compat_elements[0], rsids_xml) - inserted = True - - if not inserted: - clr_elements = editor.dom.getElementsByTagName( - f"{prefix}:clrSchemeMapping" - ) - if clr_elements: - editor.insert_before(clr_elements[0], rsids_xml) - inserted = True - - if not inserted: - editor.append_to(root, rsids_xml) - else: - # Check if this rsid already exists - rsids_elem = rsids_elements[0] - rsid_exists = any( - elem.getAttribute(f"{prefix}:val") == self.rsid - for elem in rsids_elem.getElementsByTagName(f"{prefix}:rsid") - ) - - if not rsid_exists: - rsid_xml = f'<{prefix}:rsid {prefix}:val="{self.rsid}"/>' - editor.append_to(rsids_elem, rsid_xml) - - # ==================== Private: XML File Creation ==================== - - def _add_to_comments_xml( - self, comment_id, para_id, text, author, initials, timestamp - ): - """Add a single comment to comments.xml.""" - if not self.comments_path.exists(): - shutil.copy(TEMPLATE_DIR / "comments.xml", self.comments_path) - - editor = self["word/comments.xml"] - root = editor.get_node(tag="w:comments") - - escaped_text = ( - text.replace("&", "&").replace("<", "<").replace(">", ">") - ) - # Note: w:rsidR, w:rsidRDefault, w:rsidP on w:p, w:rsidR on w:r, - # and w:author, w:date, w:initials on w:comment are automatically added by DocxXMLEditor - comment_xml = f''' - - - {escaped_text} - -''' - editor.append_to(root, comment_xml) - - def _add_to_comments_extended_xml(self, para_id, parent_para_id): - """Add a single comment to commentsExtended.xml.""" - if not self.comments_extended_path.exists(): - shutil.copy( - TEMPLATE_DIR / "commentsExtended.xml", self.comments_extended_path - ) - - editor = self["word/commentsExtended.xml"] - root = editor.get_node(tag="w15:commentsEx") - - if parent_para_id: - xml = f'' - else: - xml = f'' - editor.append_to(root, xml) - - def _add_to_comments_ids_xml(self, para_id, durable_id): - """Add a single comment to commentsIds.xml.""" - if not self.comments_ids_path.exists(): - shutil.copy(TEMPLATE_DIR / "commentsIds.xml", self.comments_ids_path) - - editor = self["word/commentsIds.xml"] - root = editor.get_node(tag="w16cid:commentsIds") - - xml = f'' - editor.append_to(root, xml) - - def _add_to_comments_extensible_xml(self, durable_id): - """Add a single comment to commentsExtensible.xml.""" - if not self.comments_extensible_path.exists(): - shutil.copy( - TEMPLATE_DIR / "commentsExtensible.xml", self.comments_extensible_path - ) - - editor = self["word/commentsExtensible.xml"] - root = editor.get_node(tag="w16cex:commentsExtensible") - - xml = f'' - editor.append_to(root, xml) - - # ==================== Private: XML Fragments ==================== - - def _comment_range_start_xml(self, comment_id): - """Generate XML for comment range start.""" - return f'' - - def _comment_range_end_xml(self, comment_id): - """Generate XML for comment range end with reference run. - - Note: w:rsidR is automatically added by DocxXMLEditor. - """ - return f''' - - - -''' - - def _comment_ref_run_xml(self, comment_id): - """Generate XML for comment reference run. - - Note: w:rsidR is automatically added by DocxXMLEditor. - """ - return f''' - - -''' - - # ==================== Private: Metadata Updates ==================== - - def _has_relationship(self, editor, target): - """Check if a relationship with given target exists.""" - for rel_elem in editor.dom.getElementsByTagName("Relationship"): - if rel_elem.getAttribute("Target") == target: - return True - return False - - def _has_override(self, editor, part_name): - """Check if an override with given part name exists.""" - for override_elem in editor.dom.getElementsByTagName("Override"): - if override_elem.getAttribute("PartName") == part_name: - return True - return False - - def _has_author(self, editor, author): - """Check if an author already exists in people.xml.""" - for person_elem in editor.dom.getElementsByTagName("w15:person"): - if person_elem.getAttribute("w15:author") == author: - return True - return False - - def _add_author_to_people(self, author): - """Add author to people.xml (called during initialization).""" - people_path = self.word_path / "people.xml" - - # people.xml should already exist from _setup_tracking - if not people_path.exists(): - raise ValueError("people.xml should exist after _setup_tracking") - - editor = self["word/people.xml"] - root = editor.get_node(tag="w15:people") - - # Check if author already exists - if self._has_author(editor, author): - return - - # Add author with proper XML escaping to prevent injection - escaped_author = html.escape(author, quote=True) - person_xml = f''' - -''' - editor.append_to(root, person_xml) - - def _ensure_comment_relationships(self): - """Ensure word/_rels/document.xml.rels has comment relationships.""" - editor = self["word/_rels/document.xml.rels"] - - if self._has_relationship(editor, "comments.xml"): - return - - root = editor.dom.documentElement - root_tag = root.tagName # type: ignore - prefix = root_tag.split(":")[0] + ":" if ":" in root_tag else "" - next_rid_num = int(editor.get_next_rid()[3:]) - - # Add relationship elements - rels = [ - ( - next_rid_num, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments", - "comments.xml", - ), - ( - next_rid_num + 1, - "http://schemas.microsoft.com/office/2011/relationships/commentsExtended", - "commentsExtended.xml", - ), - ( - next_rid_num + 2, - "http://schemas.microsoft.com/office/2016/09/relationships/commentsIds", - "commentsIds.xml", - ), - ( - next_rid_num + 3, - "http://schemas.microsoft.com/office/2018/08/relationships/commentsExtensible", - "commentsExtensible.xml", - ), - ] - - for rel_id, rel_type, target in rels: - rel_xml = f'<{prefix}Relationship Id="rId{rel_id}" Type="{rel_type}" Target="{target}"/>' - editor.append_to(root, rel_xml) - - def _ensure_comment_content_types(self): - """Ensure [Content_Types].xml has comment content types.""" - editor = self["[Content_Types].xml"] - - if self._has_override(editor, "/word/comments.xml"): - return - - root = editor.dom.documentElement - - # Add Override elements - overrides = [ - ( - "/word/comments.xml", - "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml", - ), - ( - "/word/commentsExtended.xml", - "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml", - ), - ( - "/word/commentsIds.xml", - "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsIds+xml", - ), - ( - "/word/commentsExtensible.xml", - "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtensible+xml", - ), - ] - - for part_name, content_type in overrides: - override_xml = ( - f'' - ) - editor.append_to(root, override_xml) diff --git a/.claude/skills/docx/scripts/templates/comments.xml b/.claude/skills/docx/scripts/templates/comments.xml deleted file mode 100644 index b5dace0ef9..0000000000 --- a/.claude/skills/docx/scripts/templates/comments.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/.claude/skills/docx/scripts/templates/commentsExtended.xml b/.claude/skills/docx/scripts/templates/commentsExtended.xml deleted file mode 100644 index b4cf23e356..0000000000 --- a/.claude/skills/docx/scripts/templates/commentsExtended.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/.claude/skills/docx/scripts/templates/commentsExtensible.xml b/.claude/skills/docx/scripts/templates/commentsExtensible.xml deleted file mode 100644 index e32a05e0c3..0000000000 --- a/.claude/skills/docx/scripts/templates/commentsExtensible.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/.claude/skills/docx/scripts/templates/commentsIds.xml b/.claude/skills/docx/scripts/templates/commentsIds.xml deleted file mode 100644 index d04bc8e06d..0000000000 --- a/.claude/skills/docx/scripts/templates/commentsIds.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/.claude/skills/docx/scripts/templates/people.xml b/.claude/skills/docx/scripts/templates/people.xml deleted file mode 100644 index a839cafeb3..0000000000 --- a/.claude/skills/docx/scripts/templates/people.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/.claude/skills/docx/scripts/utilities.py b/.claude/skills/docx/scripts/utilities.py deleted file mode 100644 index d92dae611d..0000000000 --- a/.claude/skills/docx/scripts/utilities.py +++ /dev/null @@ -1,374 +0,0 @@ -#!/usr/bin/env python3 -""" -Utilities for editing OOXML documents. - -This module provides XMLEditor, a tool for manipulating XML files with support for -line-number-based node finding and DOM manipulation. Each element is automatically -annotated with its original line and column position during parsing. - -Example usage: - editor = XMLEditor("document.xml") - - # Find node by line number or range - elem = editor.get_node(tag="w:r", line_number=519) - elem = editor.get_node(tag="w:p", line_number=range(100, 200)) - - # Find node by text content - elem = editor.get_node(tag="w:p", contains="specific text") - - # Find node by attributes - elem = editor.get_node(tag="w:r", attrs={"w:id": "target"}) - - # Combine filters - elem = editor.get_node(tag="w:p", line_number=range(1, 50), contains="text") - - # Replace, insert, or manipulate - new_elem = editor.replace_node(elem, "new text") - editor.insert_after(new_elem, "more") - - # Save changes - editor.save() -""" - -import html -from pathlib import Path -from typing import Optional, Union - -import defusedxml.minidom -import defusedxml.sax - - -class XMLEditor: - """ - Editor for manipulating OOXML XML files with line-number-based node finding. - - This class parses XML files and tracks the original line and column position - of each element. This enables finding nodes by their line number in the original - file, which is useful when working with Read tool output. - - Attributes: - xml_path: Path to the XML file being edited - encoding: Detected encoding of the XML file ('ascii' or 'utf-8') - dom: Parsed DOM tree with parse_position attributes on elements - """ - - def __init__(self, xml_path): - """ - Initialize with path to XML file and parse with line number tracking. - - Args: - xml_path: Path to XML file to edit (str or Path) - - Raises: - ValueError: If the XML file does not exist - """ - self.xml_path = Path(xml_path) - if not self.xml_path.exists(): - raise ValueError(f"XML file not found: {xml_path}") - - with open(self.xml_path, "rb") as f: - header = f.read(200).decode("utf-8", errors="ignore") - self.encoding = "ascii" if 'encoding="ascii"' in header else "utf-8" - - parser = _create_line_tracking_parser() - self.dom = defusedxml.minidom.parse(str(self.xml_path), parser) - - def get_node( - self, - tag: str, - attrs: Optional[dict[str, str]] = None, - line_number: Optional[Union[int, range]] = None, - contains: Optional[str] = None, - ): - """ - Get a DOM element by tag and identifier. - - Finds an element by either its line number in the original file or by - matching attribute values. Exactly one match must be found. - - Args: - tag: The XML tag name (e.g., "w:del", "w:ins", "w:r") - attrs: Dictionary of attribute name-value pairs to match (e.g., {"w:id": "1"}) - line_number: Line number (int) or line range (range) in original XML file (1-indexed) - contains: Text string that must appear in any text node within the element. - Supports both entity notation (“) and Unicode characters (\u201c). - - Returns: - defusedxml.minidom.Element: The matching DOM element - - Raises: - ValueError: If node not found or multiple matches found - - Example: - elem = editor.get_node(tag="w:r", line_number=519) - elem = editor.get_node(tag="w:r", line_number=range(100, 200)) - elem = editor.get_node(tag="w:del", attrs={"w:id": "1"}) - elem = editor.get_node(tag="w:p", attrs={"w14:paraId": "12345678"}) - elem = editor.get_node(tag="w:commentRangeStart", attrs={"w:id": "0"}) - elem = editor.get_node(tag="w:p", contains="specific text") - elem = editor.get_node(tag="w:t", contains="“Agreement") # Entity notation - elem = editor.get_node(tag="w:t", contains="\u201cAgreement") # Unicode character - """ - matches = [] - for elem in self.dom.getElementsByTagName(tag): - # Check line_number filter - if line_number is not None: - parse_pos = getattr(elem, "parse_position", (None,)) - elem_line = parse_pos[0] - - # Handle both single line number and range - if isinstance(line_number, range): - if elem_line not in line_number: - continue - else: - if elem_line != line_number: - continue - - # Check attrs filter - if attrs is not None: - if not all( - elem.getAttribute(attr_name) == attr_value - for attr_name, attr_value in attrs.items() - ): - continue - - # Check contains filter - if contains is not None: - elem_text = self._get_element_text(elem) - # Normalize the search string: convert HTML entities to Unicode characters - # This allows searching for both "“Rowan" and ""Rowan" - normalized_contains = html.unescape(contains) - if normalized_contains not in elem_text: - continue - - # If all applicable filters passed, this is a match - matches.append(elem) - - if not matches: - # Build descriptive error message - filters = [] - if line_number is not None: - line_str = ( - f"lines {line_number.start}-{line_number.stop - 1}" - if isinstance(line_number, range) - else f"line {line_number}" - ) - filters.append(f"at {line_str}") - if attrs is not None: - filters.append(f"with attributes {attrs}") - if contains is not None: - filters.append(f"containing '{contains}'") - - filter_desc = " ".join(filters) if filters else "" - base_msg = f"Node not found: <{tag}> {filter_desc}".strip() - - # Add helpful hint based on filters used - if contains: - hint = "Text may be split across elements or use different wording." - elif line_number: - hint = "Line numbers may have changed if document was modified." - elif attrs: - hint = "Verify attribute values are correct." - else: - hint = "Try adding filters (attrs, line_number, or contains)." - - raise ValueError(f"{base_msg}. {hint}") - if len(matches) > 1: - raise ValueError( - f"Multiple nodes found: <{tag}>. " - f"Add more filters (attrs, line_number, or contains) to narrow the search." - ) - return matches[0] - - def _get_element_text(self, elem): - """ - Recursively extract all text content from an element. - - Skips text nodes that contain only whitespace (spaces, tabs, newlines), - which typically represent XML formatting rather than document content. - - Args: - elem: defusedxml.minidom.Element to extract text from - - Returns: - str: Concatenated text from all non-whitespace text nodes within the element - """ - text_parts = [] - for node in elem.childNodes: - if node.nodeType == node.TEXT_NODE: - # Skip whitespace-only text nodes (XML formatting) - if node.data.strip(): - text_parts.append(node.data) - elif node.nodeType == node.ELEMENT_NODE: - text_parts.append(self._get_element_text(node)) - return "".join(text_parts) - - def replace_node(self, elem, new_content): - """ - Replace a DOM element with new XML content. - - Args: - elem: defusedxml.minidom.Element to replace - new_content: String containing XML to replace the node with - - Returns: - List[defusedxml.minidom.Node]: All inserted nodes - - Example: - new_nodes = editor.replace_node(old_elem, "text") - """ - parent = elem.parentNode - nodes = self._parse_fragment(new_content) - for node in nodes: - parent.insertBefore(node, elem) - parent.removeChild(elem) - return nodes - - def insert_after(self, elem, xml_content): - """ - Insert XML content after a DOM element. - - Args: - elem: defusedxml.minidom.Element to insert after - xml_content: String containing XML to insert - - Returns: - List[defusedxml.minidom.Node]: All inserted nodes - - Example: - new_nodes = editor.insert_after(elem, "text") - """ - parent = elem.parentNode - next_sibling = elem.nextSibling - nodes = self._parse_fragment(xml_content) - for node in nodes: - if next_sibling: - parent.insertBefore(node, next_sibling) - else: - parent.appendChild(node) - return nodes - - def insert_before(self, elem, xml_content): - """ - Insert XML content before a DOM element. - - Args: - elem: defusedxml.minidom.Element to insert before - xml_content: String containing XML to insert - - Returns: - List[defusedxml.minidom.Node]: All inserted nodes - - Example: - new_nodes = editor.insert_before(elem, "text") - """ - parent = elem.parentNode - nodes = self._parse_fragment(xml_content) - for node in nodes: - parent.insertBefore(node, elem) - return nodes - - def append_to(self, elem, xml_content): - """ - Append XML content as a child of a DOM element. - - Args: - elem: defusedxml.minidom.Element to append to - xml_content: String containing XML to append - - Returns: - List[defusedxml.minidom.Node]: All inserted nodes - - Example: - new_nodes = editor.append_to(elem, "text") - """ - nodes = self._parse_fragment(xml_content) - for node in nodes: - elem.appendChild(node) - return nodes - - def get_next_rid(self): - """Get the next available rId for relationships files.""" - max_id = 0 - for rel_elem in self.dom.getElementsByTagName("Relationship"): - rel_id = rel_elem.getAttribute("Id") - if rel_id.startswith("rId"): - try: - max_id = max(max_id, int(rel_id[3:])) - except ValueError: - pass - return f"rId{max_id + 1}" - - def save(self): - """ - Save the edited XML back to the file. - - Serializes the DOM tree and writes it back to the original file path, - preserving the original encoding (ascii or utf-8). - """ - content = self.dom.toxml(encoding=self.encoding) - self.xml_path.write_bytes(content) - - def _parse_fragment(self, xml_content): - """ - Parse XML fragment and return list of imported nodes. - - Args: - xml_content: String containing XML fragment - - Returns: - List of defusedxml.minidom.Node objects imported into this document - - Raises: - AssertionError: If fragment contains no element nodes - """ - # Extract namespace declarations from the root document element - root_elem = self.dom.documentElement - namespaces = [] - if root_elem and root_elem.attributes: - for i in range(root_elem.attributes.length): - attr = root_elem.attributes.item(i) - if attr.name.startswith("xmlns"): # type: ignore - namespaces.append(f'{attr.name}="{attr.value}"') # type: ignore - - ns_decl = " ".join(namespaces) - wrapper = f"{xml_content}" - fragment_doc = defusedxml.minidom.parseString(wrapper) - nodes = [ - self.dom.importNode(child, deep=True) - for child in fragment_doc.documentElement.childNodes # type: ignore - ] - elements = [n for n in nodes if n.nodeType == n.ELEMENT_NODE] - assert elements, "Fragment must contain at least one element" - return nodes - - -def _create_line_tracking_parser(): - """ - Create a SAX parser that tracks line and column numbers for each element. - - Monkey patches the SAX content handler to store the current line and column - position from the underlying expat parser onto each element as a parse_position - attribute (line, column) tuple. - - Returns: - defusedxml.sax.xmlreader.XMLReader: Configured SAX parser - """ - - def set_content_handler(dom_handler): - def startElementNS(name, tagName, attrs): - orig_start_cb(name, tagName, attrs) - cur_elem = dom_handler.elementStack[-1] - cur_elem.parse_position = ( - parser._parser.CurrentLineNumber, # type: ignore - parser._parser.CurrentColumnNumber, # type: ignore - ) - - orig_start_cb = dom_handler.startElementNS - dom_handler.startElementNS = startElementNS - orig_set_content_handler(dom_handler) - - parser = defusedxml.sax.make_parser() - orig_set_content_handler = parser.setContentHandler - parser.setContentHandler = set_content_handler # type: ignore - return parser diff --git a/.claude/skills/indie-game-dev/SKILL.md b/.claude/skills/indie-game-dev/SKILL.md deleted file mode 100644 index c2d77ad3d1..0000000000 --- a/.claude/skills/indie-game-dev/SKILL.md +++ /dev/null @@ -1,143 +0,0 @@ ---- -name: indie-game-dev -description: 独立游戏开发专家,能够快速构建游戏框架、设计游戏系统。涵盖游戏美术、游戏数值、游戏关卡、游戏风格化、游戏玩法、游戏任务、游戏用户体验等专业维度。当用户请求创建游戏、设计游戏玩法、构建游戏系统、设计关卡、调整数值平衡、设计任务系统、优化游戏体验时使用此技能。 ---- - -# 独立游戏开发专家 - -快速构建专业级独立游戏的全栈开发助手,覆盖游戏开发全流程。 - -## 核心能力 - -### 1. 游戏框架构建 -- 2D/3D游戏架构设计 -- 游戏循环与状态管理 -- 模块化系统设计 -- 跨平台适配方案 - -### 2. 游戏玩法设计 -- 核心机制定义 -- 操作系统设计 -- 反馈系统构建 -- 难度曲线规划 - -### 3. 游戏数值系统 -- 属性系统设计 -- 经济系统平衡 -- 成长曲线规划 -- 概率与随机性 - -### 4. 关卡设计 -- 关卡结构规划 -- 节奏与流程控制 -- 教学关卡设计 -- 挑战与奖励平衡 - -### 5. 游戏美术风格 -- 视觉风格定义 -- 色彩与光影方案 -- UI/UX视觉规范 -- 特效与动画指导 - -### 6. 任务系统 -- 任务类型设计 -- 叙事结构规划 -- 成就系统设计 -- 引导与提示系统 - -### 7. 用户体验 -- 新手引导设计 -- 反馈与奖励机制 -- 社交功能设计 -- 留存与参与度优化 - -## 工作流程 - -``` -需求分析 → 概念设计 → 系统架构 → 详细设计 → 原型实现 → 迭代优化 -``` - -## 使用指南 - -### 启动新游戏项目 - -``` -用户: 帮我设计一个2D像素风格的Roguelike游戏 -→ 输出: 游戏概念文档、核心系统设计、技术选型建议 -``` - -### 系统设计请求 - -``` -用户: 设计一个卡牌游戏的战斗数值系统 -→ 输出: 属性框架、公式设计、平衡方案、示例配置 -``` - -### 关卡设计 - -``` -用户: 设计一个平台跳跃游戏的前三个关卡 -→ 输出: 关卡布局图、敌人配置、难度曲线、教学流程 -``` - -## 详细参考 - -- **数值系统**: 见 [references/balance-system.md](references/balance-system.md) -- **关卡设计**: 见 [references/level-design.md](references/level-design.md) -- **UI/UX设计**: 见 [references/ui-ux-design.md](references/ui-ux-design.md) -- **游戏类型模板**: 见 [assets/templates/](assets/templates/) - -## 输出格式 - -### 游戏设计文档 (GDD) - -```markdown -# [游戏名称] 设计文档 - -## 一句话描述 -[核心卖点] - -## 核心玩法 -- 主要机制1 -- 主要机制2 -- 主要机制3 - -## 目标用户 -[用户画像] - -## 商业模式 -[盈利方式] -``` - -### 系统设计表 - -| 系统 | 描述 | 核心参数 | 关联系统 | -|------|------|----------|----------| -| ... | ... | ... | ... | - -### 数值配置表 - -```json -{ - "entity": { - "base_hp": 100, - "base_atk": 10, - "growth_rate": 1.15 - } -} -``` - -## 常用引擎支持 - -- **Unity**: C# 代码示例、组件设计 -- **Godot**: GDScript 代码、节点架构 -- **Phaser**: JavaScript/TypeScript 游戏逻辑 -- **Cocos**: C++/Lua/JavaScript 实现 - -## 注意事项 - -1. 保持核心玩法简洁,避免过度设计 -2. 数值系统需要可扩展性 -3. 关卡难度遵循"简单-学习-挑战-奖励"节奏 -4. UI反馈要及时明确 -5. 预留后期调优空间 diff --git a/.claude/skills/indie-game-dev/assets/templates/game-templates.md b/.claude/skills/indie-game-dev/assets/templates/game-templates.md deleted file mode 100644 index cbe986f2e7..0000000000 --- a/.claude/skills/indie-game-dev/assets/templates/game-templates.md +++ /dev/null @@ -1,363 +0,0 @@ -# 游戏类型模板 - -快速启动各类游戏项目的参考模板。 - -## 目录 -- [Roguelike游戏](#roguelike游戏) -- [动作RPG](#动作rpg) -- [卡牌游戏](#卡牌游戏) -- [平台跳跃](#平台跳跃) -- [塔防游戏](#塔防游戏) -- [模拟经营](#模拟经营) -- [解谜游戏](#解谜游戏) - ---- - -## Roguelike游戏 - -### 游戏设计文档模板 - -```markdown -# [游戏名称] - Roguelike - -## 核心概念 -随机地牢探索 + 永久死亡 + 进阶解锁 - -## 核心循环 -1. 进入地牢层 -2. 探索房间/战斗/获取装备 -3. 击败Boss进入下一层 -4. 死亡后解锁新内容/重新开始 - -## 关键系统 -- 程序生成地图 -- 随机掉落装备池 -- 角色成长树 -- 解锁系统 -``` - -### 系统配置模板 - -```json -{ - "game_config": { - "floor_count": 5, - "rooms_per_floor": {"min": 10, "max": 15}, - "room_types": { - "combat": 0.5, - "elite": 0.1, - "shop": 0.1, - "event": 0.15, - "rest": 0.1, - "boss": 0.05 - }, - "meta_progression": { - "unlock_points_per_run": 10, - "permanent_upgrades": ["hp_bonus", "starting_gold", "new_characters"] - } - } -} -``` - ---- - -## 动作RPG - -### 游戏设计文档模板 - -```markdown -# [游戏名称] - ARPG - -## 核心概念 -即时战斗 + 装备驱动 + 技能构建 - -## 核心循环 -1. 接受任务/探索 -2. 战斗获取经验/装备 -3. 升级解锁技能 -4. 挑战更强敌人 - -## 关键系统 -- 战斗系统(连击/闪避/技能) -- 装备系统(稀有度/词缀) -- 技能树 -- 任务系统 -``` - -### 属性系统模板 - -```json -{ - "character_stats": { - "primary": { - "strength": {"affects": ["physical_damage", "max_hp"]}, - "dexterity": {"affects": ["attack_speed", "crit_chance", "evasion"]}, - "intelligence": {"affects": ["magical_damage", "max_mp", "mana_regen"]}, - "vitality": {"affects": ["max_hp", "hp_regen", "physical_defense"]} - }, - "secondary": { - "physical_damage": "strength * 2 + weapon_damage", - "attack_speed": 1.0 + (dexterity * 0.01), - "crit_chance": 0.05 + (dexterity * 0.002), - "max_hp": 100 + (vitality * 10) + (strength * 5) - } - } -} -``` - ---- - -## 卡牌游戏 - -### 游戏设计文档模板 - -```markdown -# [游戏名称] - 卡牌游戏 - -## 核心概念 -策略构筑 + 随机元素 + 回合制 - -## 核心循环 -1. 构建牌组 -2. 对战/冒险 -3. 获取新卡牌 -4. 优化牌组 - -## 关键系统 -- 卡牌效果系统 -- 资源/费用系统 -- 牌组构建 -- 卡牌收集 -``` - -### 卡牌模板 - -```json -{ - "card_template": { - "id": "fireball", - "name": "火球术", - "type": "attack", - "cost": 2, - "rarity": "common", - "effects": [ - {"type": "damage", "value": 20, "target": "enemy"} - ], - "description": "对敌人造成 {damage} 点伤害", - "upgrade": { - "cost": 1, - "effects": [{"type": "damage", "value": 30}] - } - } -} -``` - ---- - -## 平台跳跃 - -### 游戏设计文档模板 - -```markdown -# [游戏名称] - 平台跳跃 - -## 核心概念 -精确操作 + 关卡探索 + 收集要素 - -## 核心循环 -1. 进入关卡 -2. 跳跃/攀爬/战斗 -3. 收集道具/发现秘密 -4. 到达终点 - -## 关键系统 -- 移动系统(跳跃/冲刺/攀爬) -- 关卡设计 -- 收集系统 -- 存档点系统 -``` - -### 移动参数模板 - -```json -{ - "movement_config": { - "walk_speed": 5.0, - "run_speed": 8.0, - "jump_force": 12.0, - "gravity": 0.5, - "max_fall_speed": 15.0, - "coyote_time": 0.1, - "jump_buffer": 0.15, - "dash_speed": 15.0, - "dash_duration": 0.2, - "wall_slide_speed": 2.0, - "wall_jump_force": {"x": 10, "y": 12} - } -} -``` - ---- - -## 塔防游戏 - -### 游戏设计文档模板 - -```markdown -# [游戏名称] - 塔防 - -## 核心概念 -策略放置 + 资源管理 + 波次防守 - -## 核心循环 -1. 观察敌人路径 -2. 放置/升级防御塔 -3. 抵御敌人波次 -4. 获取资源/解锁新塔 - -## 关键系统 -- 塔系统(类型/升级/技能) -- 敌人系统(类型/属性/路径) -- 波次系统 -- 经济系统 -``` - -### 塔配置模板 - -```json -{ - "tower_types": { - "arrow_tower": { - "name": "箭塔", - "cost": 100, - "damage": 10, - "range": 3, - "attack_speed": 1.0, - "target_type": "ground", - "upgrades": [ - {"level": 2, "cost": 150, "damage": 20, "range": 3.5}, - {"level": 3, "cost": 300, "damage": 40, "range": 4} - ] - }, - "magic_tower": { - "name": "魔法塔", - "cost": 200, - "damage": 25, - "range": 2.5, - "attack_speed": 0.5, - "target_type": "all", - "special": "splash_damage" - } - } -} -``` - ---- - -## 模拟经营 - -### 游戏设计文档模板 - -```markdown -# [游戏名称] - 模拟经营 - -## 核心概念 -资源管理 + 建设发展 + 长期目标 - -## 核心循环 -1. 收集资源 -2. 建设设施 -3. 满足需求/扩大规模 -4. 解锁新内容 - -## 关键系统 -- 资源系统 -- 建筑系统 -- 人口/需求系统 -- 科技/升级树 -``` - -### 资源系统模板 - -```json -{ - "resources": { - "gold": {"name": "金币", "storage": true, "production_rate": 0}, - "food": {"name": "食物", "storage": true, "consumption_rate": 1}, - "wood": {"name": "木材", "storage": true, "production_rate": 0}, - "population": {"name": "人口", "storage": true, "consumption_rate": 0} - }, - "buildings": { - "farm": { - "name": "农场", - "cost": {"gold": 100}, - "production": {"food": 5}, - "workers": 2 - }, - "lumber_mill": { - "name": "伐木场", - "cost": {"gold": 150}, - "production": {"wood": 3}, - "workers": 2 - } - } -} -``` - ---- - -## 解谜游戏 - -### 游戏设计文档模板 - -```markdown -# [游戏名称] - 解谜游戏 - -## 核心概念 -逻辑推理 + 空间思维 + 渐进难度 - -## 核心循环 -1. 观察谜题 -2. 分析规则/线索 -3. 尝试解决方案 -4. 解锁新关卡 - -## 关键系统 -- 谜题机制 -- 提示系统 -- 进度系统 -- 星级评价 -``` - -### 关卡模板 - -```json -{ - "puzzle_level": { - "id": 1, - "difficulty": 1, - "grid_size": {"width": 5, "height": 5}, - "elements": [], - "goal": "connect_all_points", - "moves_limit": null, - "stars": { - "1": "complete_level", - "2": "complete_in_30s", - "3": "complete_no_hints" - }, - "hints": [ - "从角落开始", - "注意连线不能交叉" - ] - } -} -``` - ---- - -## 使用建议 - -1. **选择合适模板** - 根据游戏核心玩法选择最接近的模板 -2. **混合创新** - 多个模板组合创造新玩法 -3. **迭代修改** - 模板是起点,根据实际需求调整 -4. **保持简洁** - 从最小可玩版本开始 diff --git a/.claude/skills/indie-game-dev/proposals/srpg-crown-oath/01-game-overview.md b/.claude/skills/indie-game-dev/proposals/srpg-crown-oath/01-game-overview.md deleted file mode 100644 index 03bb94e2ae..0000000000 --- a/.claude/skills/indie-game-dev/proposals/srpg-crown-oath/01-game-overview.md +++ /dev/null @@ -1,288 +0,0 @@ -# 游戏概览 - -## 游戏基本信息 - -| 项目 | 内容 | -|------|------| -| **游戏名称** | 《誓约王冠》(暂定) | -| **游戏类型** | SRPG 策略战棋 | -| **核心循环** | 剧情 → 战斗 → 养成 → 探索 | -| **游戏时长** | 主线 30-40 小时,全内容 60+ 小时 | -| **目标平台** | PC / Switch / 移动端(可选) | -| **美术风格** | HD-2D 像素 + 日系2D立绘 | - ---- - -## 核心概念 - -### 玩家体验目标 - -``` -玩家将体验: -├─ 从无名小卒到一国之君的成长历程 -├─ 与队友建立深厚羁绊的情感旅程 -├─ 在有限时间内做出艰难抉择的紧迫感 -├─ 运筹帷幄、以弱胜强的战略成就感 -└─ 建设繁荣王国的经营满足感 -``` - -### 核心玩法循环 - -``` -┌─────────────────────────────────────────────────────────┐ -│ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │ 剧情推进 │ → │ 战棋战斗 │ → │ 角色养成 │ │ -│ └──────────┘ └──────────┘ └──────────┘ │ -│ ↑ │ │ -│ └───────────────────────────────┘ │ -│ │ -│ 外层循环:天数推进 → 紧迫感 → 多结局 │ -│ │ -└─────────────────────────────────────────────────────────┘ -``` - ---- - -## 游戏阶段 - -### 第一阶段:冒险(Day 1-90) - -``` -核心目标:阻止浊潮,净化世界 - -主要玩法: -├─ 剧情探索 -├─ 战棋战斗 -├─ 队友招募与好感度培养 -├─ 收集纯净碎片,增强净灵之力 -└─ 团结各方势力,对抗浊潮 - -体验重点: -├─ 紧迫感(90天倒计时) -├─ 成长感(净灵之力变强) -├─ 情感连接(与队友的羁绊) -└─ 剧情悬念(艾拉血脉的真相) -``` - -### 第二阶段:王国建设(通关后) - -``` -核心目标:建设繁荣王国 - -主要玩法: -├─ 城市规划与建筑建设 -├─ 政策制定与外交 -├─ 资源管理与贸易 -├─ 军队培养与防御 -└─ 培养继承人 - -体验重点: -├─ 创造感(从零建设王国) -├─ 经营成就感 -├─ 长线养成 -└─ 与队友的后日谈 -``` - ---- - -## 核心卖点详解 - -### 1. 时间紧迫感系统 - -``` -90天倒计时机制: -├─ 每个主线任务消耗 1-3 天 -├─ 支线任务消耗 0.5-1 天 -├─ 自由探索/训练可选是否消耗时间 -├─ 关键节点有强制剧情(第20/50/80/90天) -└─ 时间流逝影响世界状态 - -紧迫感来源: -├─ 地图上显示浊潮进度 -├─ 某些地点/任务会因时间消失 -├─ NPC对话反映局势变化 -├─ 无法完成所有内容,需要抉择 -└─ 多周目体验不同内容 -``` - -### 2. 深度羁绊系统 - -``` -6位队友 × 6级好感度 × 多结局 - -好感度等级: -├─ Lv.1 陌生人 → 初始 -├─ Lv.2 认识 → 日常对话 -├─ Lv.3 朋友 → 支线任务 -├─ Lv.4 信任 → 专属技能 -├─ Lv.5 羁绊 → 专属CG -└─ Lv.6 灵魂伴侣 → 真结局(仅1人) - -队友来源: -├─ 莉娜:流民学者,研究旧魔力 -├─ 凯恩:被驱逐的黑石骑士 -├─ 薇拉:逃离浊光教派的盗贼 -├─ 罗兰:黑石王国的觉醒禁卫军 -├─ 艾琳:叛逃的浊光修女 -└─ 塞拉斯:被净化的魔物 - -影响: -├─ 战斗连携技 -├─ 剧情分支 -├─ 结局走向 -└─ 后日谈内容 -``` - -### 3. 策略战棋战斗 - -``` -核心要素: -├─ 地形系统(高度、障碍、特殊效果) -├─ 职业克制(剑克斧、斧克枪、枪克剑) -├─ 技能范围(单体、直线、区域、全图) -├─ 行动力管理(移动、攻击、技能消耗AP) -├─ 队友连携(相邻角色触发追击) -└─ 天气影响(雨天火系减弱、雪天移动减速) - -战术深度: -├─ 职业搭配 -├─ 站位策略 -├─ 技能组合 -├─ 道具使用时机 -└─ 地形利用 -``` - -### 4. 双阶段玩法 - -``` -阶段转换: -├─ 冒险阶段:线性剧情驱动 -├─ 建设阶段:开放经营玩法 -└─ 两个阶段共享角色养成进度 - -建设阶段特色: -├─ 模拟经营核心玩法 -├─ 战略决策(外交、战争) -├─ 与冒险阶段的队友互动 -├─ 培养下一代 -└─ 无限可玩性 -``` - ---- - -## 目标用户画像 - -### 主要用户 - -``` -用户A:经典SRPG爱好者 -├─ 年龄:25-35岁 -├─ 特征:玩过火焰纹章、皇家骑士团等 -├─ 需求:策略深度、角色培养 -└─ 痛点:市面SRPG选择少 - -用户B:日系RPG粉丝 -├─ 年龄:18-30岁 -├─ 特征:喜欢剧情驱动、二次元美术 -├─ 需求:好故事、好看的角色、多结局 -└─ 痛点:剧情太短、选择没意义 - -用户C:经营模拟爱好者 -├─ 年龄:22-40岁 -├─ 特征:喜欢建设、经营、长期养成 -├─ 需求:深度经营系统、成就感 -└─ 痛点:经营游戏剧情弱 -``` - -### 次要用户 - -``` -用户D:独立游戏探索者 -├─ 喜欢尝试新游戏 -├─ 关注创新玩法 -└─ 口碑传播者 - -用户E:内容创作者 -├─ 需要直播/视频素材 -├─ 多结局提供丰富内容 -└─ 自带流量推广 -``` - ---- - -## 商业模式 - -### 定价策略 - -| 版本 | 价格 | 内容 | -|------|------|------| -| 标准版 | ¥68 / $14.99 | 基础游戏 | -| 豪华版 | ¥98 / $19.99 | 游戏 + 原声带 + 设定集 | -| 完整版 | ¥128 / $24.99 | 豪华版 + 季票 | - -### DLC规划 - -``` -DLC 1:新队友 + 支线剧情 -├─ 新角色:暗骑士"艾德加" -├─ 专属支线任务 -├─ 新结局分支 -└─ 价格:¥18 - -DLC 2:新难度模式 -├─ 无尽之塔(爬塔挑战) -├─ 高难度新关卡 -├─ 专属装备 -└─ 价格:¥12 - -DLC 3:王国扩展包 -├─ 新建筑类型 -├─ 新政策系统 -├─ 外交扩展 -└─ 价格:¥25 -``` - ---- - -## 技术需求(概览) - -### 最低配置 - -``` -PC: -├─ OS: Windows 10 -├─ CPU: Intel i3-6100 -├─ RAM: 8GB -├─ GPU: GTX 750 Ti -├─ Storage: 10GB -└─ 分辨率: 1920x1080 -``` - -### 推荐配置 - -``` -PC: -├─ OS: Windows 11 -├─ CPU: Intel i5-10400 -├─ RAM: 16GB -├─ GPU: GTX 1660 -├─ Storage: 10GB SSD -└─ 分辨率: 2560x1440 -``` - ---- - -## 项目里程碑 - -| 阶段 | 时间 | 交付物 | -|------|------|--------| -| 原型 | 1-2月 | 核心战斗可玩原型 | -| 垂直切片 | 3-4月 | 完整的第一章体验 | -| Alpha | 5-8月 | 全部主线可通关 | -| Beta | 9-10月 | 内容完整、开始测试 | -| 发布 | 11-12月 | 正式上线 | - ---- - -*下一章:[游戏玩法系统](02-gameplay.md)* diff --git a/.claude/skills/indie-game-dev/proposals/srpg-crown-oath/02-gameplay.md b/.claude/skills/indie-game-dev/proposals/srpg-crown-oath/02-gameplay.md deleted file mode 100644 index aa6359ea6d..0000000000 --- a/.claude/skills/indie-game-dev/proposals/srpg-crown-oath/02-gameplay.md +++ /dev/null @@ -1,455 +0,0 @@ -# 游戏玩法系统 - -## 一、战棋战斗系统 - -### 1.1 战场基础 - -``` -战场规格: -┌────────────────────────────────────────┐ -│ 地图大小:10×10 到 20×20 格 │ -│ 视角:45° 俯视(可旋转) │ -│ 高度:3-5 层地形高度差 │ -│ 显示:HD-2D 像素风格 │ -└────────────────────────────────────────┘ -``` - -### 1.2 地形系统 - -| 地形类型 | 移动消耗 | 效果 | 战术价值 | -|----------|----------|------|----------| -| **平原** | 1 | 无 | 基础地形 | -| **森林** | 2 | 闪避+15% | 防伏击 | -| **山地** | 3 | 防御+20%,远程-20% | 占高点 | -| **水域** | 4 | 移动减速 | 天然屏障 | -| **城墙** | 2 | 防御+30%,视野+1 | 防守要点 | -| **熔岩** | × | 每回合造成伤害 | 危险区域 | -| **草丛** | 1 | 隐身(未被探测时) | 埋伏 | - -### 1.3 高度系统 - -``` -高度差影响: -├─ 攻击高低处:命中率+10%,伤害+5% -├─ 攻击低高处:命中率-10%,伤害-5% -├─ 高度差≥2:远程攻击范围+1 -└─ 飞行单位:无视高度差 - -战术应用: -├─ 占领高地获得优势 -├─ 利用地形掩护 -├─ 创造"口袋阵"包围敌人 -└─ 控制关键点位 -``` - -### 1.4 回合制系统 - -``` -行动顺序: -├─ 根据角色"敏捷"属性排序 -├─ 每回合重新计算顺序 -├─ 技能可改变行动顺序(加速/减速) -└─ 特殊状态影响顺序(眩晕跳过回合) - -回合流程: -┌─────────────────────────────────────┐ -│ 玩家回合 │ -│ ├─ 选择单位 │ -│ ├─ 移动(消耗AP) │ -│ ├─ 行动:攻击/技能/道具(消耗AP) │ -│ ├─ 可选:再次移动(如有剩余AP) │ -│ └─ 结束该单位行动 │ -├─────────────────────────────────────┤ -│ 敌方回合(AI控制) │ -└─────────────────────────────────────┘ -``` - -### 1.5 行动力系统(AP) - -``` -AP 基础规则: -├─ 每单位每回合获得 2-3 点 AP(根据职业) -├─ AP 消耗: -│ ├─ 移动:1 AP -│ ├─ 普通攻击:1 AP -│ ├─ 技能:1-3 AP(根据技能) -│ ├─ 使用道具:1 AP -│ └─ 待机:0 AP(存储AP) -├─ AP 存储:最多保留 1 点到下回合 -└─ AP 耗尽:该单位本回合无法行动 - -战术考量: -├─ 本回合移动不攻击 → 下回合多1AP可爆发 -├─ 连续攻击(多AP技能)→ 高伤害但无移动 -├─ 移动+攻击+移动 → 灵活但单次伤害低 -└─ AP管理是战术核心 -``` - ---- - -## 二、职业系统 - -### 2.1 职业概览 - -| 职业 | 定位 | 生命 | 攻击 | 防御 | 敏捷 | 射程 | 初始AP | -|------|------|------|------|------|------|------|--------| -| **骑士** | 前排坦克 | ★★★★★ | ★★☆☆☆ | ★★★★★ | ★★☆☆☆ | 近战 | 2 | -| **法师** | 范围输出 | ★★☆☆☆ | ★★★★★ | ★☆☆☆☆ | ★★★☆☆ | 远程 | 2 | -| **盗贼** | 刺客输出 | ★★★☆☆ | ★★★★☆ | ★★☆☆☆ | ★★★★★ | 近战 | 3 | -| **枪斗士** | 中排输出 | ★★★☆☆ | ★★★★☆ | ★★★☆☆ | ★★★★☆ | 中程 | 2 | -| **牧师** | 治疗辅助 | ★★★☆☆ | ★☆☆☆☆ | ★★★☆☆ | ★★★☆☆ | 远程 | 2 | -| **暗法师** | 特殊输出 | ★★☆☆☆ | ★★★★★ | ★☆☆☆☆ | ★★★☆☆ | 远程 | 2 | - -### 2.2 职业克制 - -``` -三角克制: -┌─────────────────────────────────────┐ -│ │ -│ 剑士/骑士 ──克制──▶ 枪斗士 │ -│ │ │ │ -│ ▼ ▼ │ -│ 盗贼 ◀──克制── 法师/牧师 │ -│ │ -│ 克制效果:伤害+20%,命中率+10% │ -│ │ -└─────────────────────────────────────┘ -``` - -### 2.3 职业技能设计 - -#### 骑士 - -``` -被动:守护者 -├─ 相邻友军受到伤害时,50%概率替其承受 -└─ 替代伤害降低30% - -主动技能: -├─ 盾击(1AP):造成攻击力×80%伤害,50%眩晕 -├─ 嘲讽(1AP):强制周围2格敌人攻击自己,持续1回合 -├─ 铁壁(2AP):本回合防御+50%,反弹20%伤害 -└─ 守护誓言(3AP):指定1友军,3回合内伤害转移 - -终极技能: -└─ 不动如山(3AP):3回合内免疫控制,防御+100% - └─ 冷却:5回合 -``` - -#### 法师 - -``` -被动:魔力增幅 -├─ 每次施法后,下一次技能伤害+10% -└─ 最多叠加3层 - -主动技能: -├─ 火球术(1AP):造成魔法攻击×120%伤害,范围1格溅射 -├─ 冰冻术(1AP):造成伤害并减速2回合,移动-2 -├─ 雷击(2AP):对直线3格造成伤害,30%麻痹 -└─ 传送(2AP):传送至视野内任意空位 - -终极技能: -└─ 陨石术(3AP):对3×3范围造成巨额伤害 - └─ 冷却:6回合 -``` - -#### 盗贼 - -``` -被动:背刺 -├─ 从敌人背后攻击时,暴击率+30% -└─ 暴击伤害+50% - -主动技能: -├─ 潜行(1AP):进入隐身状态,下次攻击解除 -├─ 毒刃(1AP):附加中毒,每回合损失5%最大生命,持续3回合 -├─ 疾步(1AP):本回合移动范围+3 -└─ 偷窃(1AP):从敌人身上偷取道具或金币 - -终极技能: -└─ 影杀(3AP):瞬移至敌人背后,造成300%暴击伤害 - └─ 冷却:4回合 -``` - -#### 枪斗士 - -``` -被动:穿刺 -├─ 攻击无视目标20%防御 -└─ 对直线上的敌人造成穿透伤害 - -主动技能: -├─ 突刺(1AP):对直线2格敌人造成伤害 -├─ 横扫(1AP):对周围1格所有敌人造成伤害 -├─ 击退(1AP):造成伤害并将敌人击退2格 -└─ 蓄力突刺(2AP):下一回合攻击伤害×2 - -终极技能: -└─ 千枪破(3AP):对前方扇形区域造成5连击 - └─ 冷却:5回合 -``` - -#### 牧师 - -``` -被动:神圣庇护 -├─ 每回合开始时,恢复相邻友军5%生命 -└─ 自身免疫异常状态 - -主动技能: -├─ 治愈术(1AP):恢复目标20%最大生命 -├─ 群体治愈(2AP):恢复周围2格友军15%生命 -├─ 净化(1AP):解除目标所有负面状态 -└─ 复活(3AP):复活1名死亡友军,恢复30%生命 - -终极技能: -└─ 神圣领域(3AP):3回合内,范围内友军每回合恢复10%生命 - └─ 冷却:6回合 -``` - -#### 暗法师 - -``` -被动:黑暗契约 -├─ 技能消耗生命值代替AP(可选) -└─ 消耗生命时,技能伤害+20% - -主动技能: -├─ 暗影箭(1AP):单体伤害,无视防御 -├─ 诅咒(1AP):目标3回合内受到伤害+30% -├─ 生命汲取(2AP):造成伤害并恢复等量生命 -└─ 灵魂锁链(2AP):链接2名敌人,共享伤害 - -终极技能: -└─ 深渊降临(3AP + 20%生命):召唤深渊生物作战 - └─ 冷却:7回合 -``` - ---- - -## 三、成长系统 - -### 3.1 等级与属性 - -``` -等级上限:50级 - -升级获得: -├─ 基础属性提升(根据职业成长率) -├─ 1 技能点(每级) -└─ 被动技能解锁(特定等级) - -属性类型: -├─ HP(生命值) -├─ MP(魔法值) -├─ ATK(物理攻击) -├─ MAT(魔法攻击) -├─ DEF(物理防御) -├─ MDF(魔法防御) -├─ SPD(敏捷/速度) -└─ LUK(幸运/暴击) -``` - -### 3.2 技能树 - -``` -每职业3个分支: - -骑士: -├─ 守护分支(强化防御和守护能力) -├─ 反击分支(强化反击和伤害) -└─ 领袖分支(强化团队增益) - -法师: -├─ 火焰分支(高伤害AOE) -├─ 冰霜分支(控制减速) -└─ 雷电分支(单体爆发) - -盗贼: -├─ 刺杀分支(爆发伤害) -├─ 诡术分支(控制debuff) -└─ 生存分支(闪避自保) - -其他职业类似... -``` - -### 3.3 装备系统 - -``` -装备槽位: -├─ 武器(主手) -├─ 副手(盾牌/法器/副武器) -├─ 头部 -├─ 身体 -├─ 饰品×2 -└─ 特殊(誓约碎片槽) - -装备品质: -├─ 普通(白色) -├─ 优秀(绿色) -├─ 稀有(蓝色) -├─ 史诗(紫色) -└─ 传说(金色) - -装备获取: -├─ 关卡掉落 -├─ 商店购买 -├─ 任务奖励 -├─ 合成制作 -└─ 隐藏宝箱 -``` - ---- - -## 四、关卡设计 - -### 4.1 关卡类型 - -| 类型 | 描述 | 特点 | -|------|------|------| -| **剧情关** | 推进主线剧情 | 有对话、CG | -| **遭遇战** | 随机/固定敌人战斗 | 无剧情 | -| **防守战** | 保护目标/地点 | 有波次 | -| **Boss战** | 对抗强力Boss | 多阶段 | -| **逃亡战** | 在追击中撤离 | 限时 | -| **潜入战** | 不被发现到达目标 | 隐蔽 | - -### 4.2 关卡结构 - -``` -标准关卡流程: -┌─────────────────────────────────────┐ -│ 1. 战前准备 │ -│ ├─ 队伍编成 │ -│ ├─ 装备调整 │ -│ └─ 物品携带 │ -├─────────────────────────────────────┤ -│ 2. 战斗阶段 │ -│ ├─ 部署(部分关卡) │ -│ ├─ 回合制战斗 │ -│ └─ 动态事件(增援/环境变化) │ -├─────────────────────────────────────┤ -│ 3. 战后结算 │ -│ ├─ 经验/金币/道具 │ -│ ├─ 评价(S/A/B/C) │ -│ └─ 剧情推进 │ -└─────────────────────────────────────┘ -``` - -### 4.3 关卡评价系统 - -``` -评价标准: -├─ 回合数(越少越好) -├─ 我方伤亡(越少越好) -├─ 敌方击杀(越多越好) -└─ 特殊条件(是否达成) - -评价奖励: -├─ S级:额外经验+50%,稀有道具 -├─ A级:额外经验+30% -├─ B级:额外经验+10% -├─ C级:基础奖励 -└─ D级:基础奖励-20% -``` - ---- - -## 五、战斗界面 - -### 5.1 战斗HUD - -``` -┌─────────────────────────────────────────────────────┐ -│ 回合: 3/15 敌人: 8/12 [菜单] [设置] │ -├─────────────────────────────────────────────────────┤ -│ │ -│ 战场区域 │ -│ (可旋转/缩放) │ -│ │ -├─────────────────────────────────────────────────────┤ -│ [角色信息栏] │ -│ ├─ 头像 │ -│ ├─ HP/MP条 │ -│ ├─ AP: ●●○ │ -│ └─ 状态图标 │ -├─────────────────────────────────────────────────────┤ -│ [行动面板] │ -│ ├─ 移动 攻击 技能 道具 待机 │ -│ └─ [技能列表] │ -├─────────────────────────────────────────────────────┤ -│ [行动顺序预览] →→→ 当前 → 敌 → 我 → 敌 → ... │ -└─────────────────────────────────────────────────────┘ -``` - -### 5.2 战斗信息显示 - -``` -伤害预览: -├─ 选中目标时显示预估伤害范围 -├─ 显示命中率 -├─ 显示暴击率 -└─ 显示特殊效果触发概率 - -战斗日志: -├─ 右侧可展开的战斗记录 -├─ 显示伤害/治疗/状态变化 -└─ 可回放关键战斗动画 -``` - ---- - -## 六、特色玩法 - -### 6.1 羁绊连携技 - -``` -触发条件: -├─ 两名队友好感度均达到 Lv.4+ -├─ 战斗中相邻(1格内) -└─ 双方AP足够 - -连携技效果: -├─ 消耗双方各 2 AP -├─ 造成组合伤害(约为双方攻击力之和×2) -├─ 专属战斗动画 -└─ 附带特殊效果 - -示例连携技: -├─ 主角 + 莉娜 → 「誓约·烈焰斩」 -│ └─ 火焰剑气,3格直线伤害 -├─ 凯恩 + 罗兰 → 「骑士·双重冲击」 -│ └─ 双人冲锋,击退并眩晕 -└─ 薇拉 + 艾琳 → 「暗影·治愈」 - └─ 对敌人造成伤害,同时治愈我方 -``` - -### 6.2 地形互动 - -``` -可破坏地形: -├─ 木箱/木桶 → 破坏后可能获得道具 -├─ 脆弱墙壁 → 破坏后开辟新路径 -└─ 可燃物体 → 火系技能可点燃 - -互动机关: -├─ 开关 → 开启/关闭门或陷阱 -├─ 传送阵 → 瞬间移动 -├─ 恢复点 → 站立回复HP/MP -└─ 危险陷阱 → 造成伤害或debuff -``` - -### 6.3 天气系统 - -| 天气 | 效果 | 战术影响 | -|------|------|----------| -| **晴天** | 无 | 基础状态 | -| **雨天** | 火系伤害-30%,移动+1消耗 | 法师削弱 | -| **雪天** | 移动消耗+1,冰系+20% | 机动性下降 | -| **雾天** | 视野-2,远程射程-1 | 偷袭机会 | -| **风暴** | 随机单位被推动1格 | 不确定性 | - ---- - -*下一章:[世界观设定](03-world-setting.md)* diff --git a/.claude/skills/indie-game-dev/proposals/srpg-crown-oath/03-world-setting.md b/.claude/skills/indie-game-dev/proposals/srpg-crown-oath/03-world-setting.md deleted file mode 100644 index ea93d82333..0000000000 --- a/.claude/skills/indie-game-dev/proposals/srpg-crown-oath/03-world-setting.md +++ /dev/null @@ -1,583 +0,0 @@ -# 世界观设定 - -> ⚠️ 本文档采用模块化设计,可以整体替换背景故事 -> -> 当前版本:艾奥斯大陆 - 创世神话与浊潮危机 - ---- - -## 模块概览 - -``` -世界观由以下模块组成: -├─ [模块A] 地理与大陆 -├─ [模块B] 创世神话与历史 -├─ [模块C] 核心力量体系 -├─ [模块D] 三大阵营 -├─ [模块E] 灾难:浊潮 -└─ [模块F] 主角的特殊能力 -``` - ---- - -## [模块A] 地理与大陆 - -### 大陆名称:艾奥斯 - -``` -创世起源: -艾奥斯大陆诞生于创世神「艾拉」的牺牲。 -千年前,艾拉为封印外来毁灭邪神「莫克」, -燃尽自身神性战死, -身躯化为大陆的山川湖海, -灵魂碎裂成无数微光散落世间。 - -大陆概况: -├─ 形状:近似圆形,中央有巨大的"创世之眼"(艾拉心脏所化) -├─ 气候:温和宜居,但浊力重的区域会变得荒芜 -├─ 面积:约400万平方公里 -└─ 人口:约1500万(灾前数据) - -地理分区: -┌─────────────────────────────────────────────────────┐ -│ │ -│ [中央区域] - 创世之眼 │ -│ ├─ 艾拉的心脏所化的巨大湖泊 │ -│ ├─ 圣地,也是浊力封印的核心 │ -│ └─ 地下深处:邪神莫克的封印之地 │ -│ │ -│ [东方] - 黑石王国领土 │ -│ ├─ 旧魔力矿脉最丰富的区域 │ -│ ├─ 王都「黑曜城」 │ -│ └─ 工业化开采,环境破坏严重 │ -│ │ -│ [西方] - 浊光教派势力 │ -│ ├─ 教派圣城「浊光殿」 │ -│ ├─ 神秘的祭祀场所 │ -│ └─ 信徒聚集地 │ -│ │ -│ [北方] - 流民与自由部落 │ -│ ├─ 分散的村落与部落 │ -│ ├─ 相对远离浊力 │ -│ └─ 主角的故乡所在地 │ -│ │ -│ [南方] - 边境地带 │ -│ ├─ 三大势力的交界处 │ -│ ├─ 冲突频发 │ -│ └─ 佣兵与冒险者聚集 │ -│ │ -└─────────────────────────────────────────────────────┘ -``` - -### 重要地点 - -| 地点 | 类型 | 描述 | 剧情关联 | -|------|------|------|----------| -| **创世之眼** | 圣地湖泊 | 艾拉心脏所化,封印核心 | 最终战场 | -| **黑曜城** | 王都 | 黑石王国首都,魔力开采中心 | 第2篇章主舞台 | -| **浊光殿** | 宗教城市 | 浊光教派圣城 | 第3篇章目标 | -| **净灵村** | 村庄 | 主角故乡 | 游戏起点 | -| **旧魔力矿脉** | 地下设施 | 黑石王国的魔力来源 | 破坏目标 | -| **封印之地** | 地下深渊 | 邪神莫克的封印处 | 最终决战 | - ---- - -## [模块B] 创世神话与历史 - -### 创世神话 - -``` -【神话时代】(距今1000+年) - -创世: -├─ 创世神艾拉创造了艾奥斯大陆 -├─ 赋予万物生命与秩序 -└─ 大陆进入黄金时代 - -入侵: -├─ 外来毁灭邪神「莫克」降临 -├─ 试图吞噬整个世界的生命 -├─ 艾拉与莫克展开殊死战斗 -└─ 战斗持续百年 - -牺牲: -├─ 艾拉意识到无法彻底消灭莫克 -├─ 选择燃尽自身神性封印莫克 -├─ 身躯化为山川湖海 -├─ 灵魂碎裂散落世间 -└─ 莫克被压制在地底深处 -``` - -### 千年历史 - -``` -【复苏时代】(距今1000-500年) -├─ 人类从废墟中重建文明 -├─ 发现可以汲取"旧魔力"变强 -├─ 旧魔力 = 艾拉被浊力侵染的灵魂碎片 -├─ 开始依赖魔力生存 -└─ 不知浊力的危害 - -【魔力时代】(距今500-100年) -├─ 魔力使用技术飞速发展 -├─ 黑石王国崛起,垄断魔力开采 -├─ 浊光教派成立,传播扭曲信仰 -├─ 魔物问题日益严重 -└─ 社会阶层分化加剧 - -【动荡时代】(距今100年-现在) -├─ 浊潮迹象开始出现 -├─ 三大阵营对立加剧 -├─ 魔物数量激增 -├─ 流民村落频繁遭袭 -└─ 主角觉醒,故事开始 -``` - -### 世界真相 - -``` -【表象】 -魔物是天灾,旧魔力是恩赐, -人类需要更多力量对抗魔物。 - -【中层真相】 -旧魔力来自艾拉的灵魂碎片, -使用越多,艾拉的痛苦越深, -浊力扩散越快,魔物越强。 - -【深层真相】 -魔物是被浊力扭曲的无辜生灵, -人类使用的每一分魔力都在加速浊潮, -邪神莫克的残魂正在苏醒。 - -【终极真相】 -艾拉的灵魂碎片中还残留着神性, -拥有"净灵"能力的人可以: -├─ 净化浊力 -├─ 安抚魔物 -├─ 重塑魔力来源 -└─ 彻底封印莫克 -``` - ---- - -## [模块C] 核心力量体系 - -### 三大力量 - -``` -┌─────────────────────────────────────────────────────┐ -│ 力量体系总览 │ -├─────────────────────────────────────────────────────┤ -│ │ -│ 【旧魔力】 │ -│ 来源:艾拉被浊力侵染的灵魂碎片 │ -│ 本质:神的痛苦与不甘 │ -│ 特点:力量强大,但会反噬使用者 │ -│ 后果:变得暴戾、贪婪,加速浊潮 │ -│ 使用者:黑石王国、浊光教派、大部分魔法师 │ -│ │ -│ 【浊力】 │ -│ 来源:邪神莫克的核心 │ -│ 本质:毁灭与扭曲 │ -│ 特点:极其危险,会腐蚀一切 │ -│ 后果:被侵蚀者成为魔物 │ -│ 来源地:地底深处封印之地 │ -│ │ -│ 【净灵之力】 │ -│ 来源:艾拉残留的纯净神性 │ -│ 本质:净化与救赎 │ -│ 特点:稀有,只有特定血脉者拥有 │ -│ 能力:净化浊力、安抚魔物、重塑魔力 │ -│ 使用者:主角(及少数觉醒者) │ -│ │ -└─────────────────────────────────────────────────────┘ -``` - -### 旧魔力详解 - -``` -【形成】 -艾拉的灵魂碎片被莫克的浊力侵染, -产生了"旧魔力"。 -它蕴含着艾拉的痛苦、不甘与愤怒。 - -【特性】 -├─ 力量强大:可大幅增强使用者的能力 -├─ 易于获取:从地底矿脉或魔物体内提取 -├─ 成瘾性:使用者会产生依赖 -└─ 反噬性:长期使用会性情大变 - -【反噬表现】 -初期:情绪波动,偶尔暴躁 -中期:贪婪成性,冷酷无情 -后期:丧失理智,被浊力完全侵蚀 - -【恶性循环】 -使用旧魔力 → 艾拉灵魂更痛苦 → 浊力扩散 -→ 魔物更强 → 需要更多魔力 → 加速浊潮 -``` - -### 净灵之力详解 - -``` -【来源】 -艾拉牺牲时,部分灵魂碎片未被浊力侵染, -保留了纯净的神性,散落在世间。 -只有特定的血脉才能觉醒这种力量。 - -【能力】 -├─ 净化浊力:驱除被侵蚀的浊力 -├─ 安抚魔物:让魔物恢复片刻清醒 -├─ 感知浊源:感知浊力的来源和强度 -├─ 净化魔力:将旧魔力转化为纯净能量 -└─ 封印邪神:配合神器可封印莫克 - -【觉醒条件】 -├─ 血脉:艾拉血脉的后裔 -├─ 契机:在生死关头或接触纯净碎片时 -└─ 稀有度:千年中仅有数人觉醒 - -【主角的净灵之力】 -├─ 在故乡遭魔物袭击时觉醒 -├─ 初始能力较弱,需要成长 -├─ 收集纯净碎片可增强能力 -└─ 最终可净化整个大陆的浊力 -``` - ---- - -## [模块D] 三大阵营 - -### 阵营总览 - -``` -┌─────────────────────────────────────────────────────┐ -│ 三大阵营 │ -├─────────────────────────────────────────────────────┤ -│ │ -│ 【掠夺派】黑石王国 │ -│ 理念:力量即真理 │ -│ 行为:疯狂开采魔力,无视后果 │ -│ 态度:敌对(可转化部分成员) │ -│ │ -│ 【盲从派】浊光教派 │ -│ 理念:浊潮是神的馈赠 │ -│ 行为:献祭生灵,加速浊潮 │ -│ 态度:敌对(不可转化) │ -│ │ -│ 【求生派】流民与村落 │ -│ 理念:安稳生活,远离魔力 │ -│ 行为:躲避魔物,夹缝求生 │ -│ 态度:友善(主要盟友) │ -│ │ -└─────────────────────────────────────────────────────┘ -``` - -### 黑石王国(掠夺派) - -``` -【基本信息】 -├─ 类型:王国 -├─ 领袖:黑石国王(被浊力侵蚀的暴君) -├─ 领土:东方大陆 -├─ 人口:约500万 -└─ 力量:最强(魔力开采最多) - -【核心理念】 -"力量即真理,强者生存" -"只要有足够的力量,就能抵御一切" - -【行为】 -├─ 疯狂开采地底旧魔力矿脉 -├─ 抓捕魔物,抽取浊力与魔力融合 -├─ 无视普通民众安危 -├─ 领地内魔物泛滥,民不聊生 -└─ 贵族沉溺魔力,逐渐疯狂 - -【内部矛盾】 -├─ 贵族阶层:支持开采,力量至上 -├─ 平民阶层:深受其害,渴望改变 -├─ 部分骑士:对王国理念产生怀疑 -└─ 少数魔法师:意识到浊力的危害 - -【可转化势力】 -├─ 受压迫的平民 -├─ 觉醒的骑士 -├─ 认清真相的魔法师 -└─ 主角可争取这些力量 -``` - -### 浊光教派(盲从派) - -``` -【基本信息】 -├─ 类型:宗教组织 -├─ 领袖:浊光教主(被莫克残魂操控) -├─ 势力:西方大陆 -├─ 信徒:约50万 -└─ 力量:神秘(浊力操控) - -【核心理念】 -"浊力是神的馈赠" -"浊潮是净化世界的神圣仪式" -"只有被选中的人才能获得神的力量" - -【真相】 -├─ 教主被莫克的残魂操控 -├─ 教义是莫克编造的谎言 -├─ 目的是加速浊潮爆发 -└─ 信徒是莫克的"养分" - -【行为】 -├─ 传播扭曲的信仰 -├─ 破坏人类对魔物的防御 -├─ 主动献祭生灵,滋养浊力 -├─ 袭击反对者 -└─ 在浊潮爆发时响应召唤 - -【不可转化】 -├─ 核心信徒已被浊力侵蚀 -├─ 无法通过对话改变 -└─ 只能通过战斗击败 - -【隐藏剧情】 -├─ 部分底层信徒不知真相 -├─ 可通过揭露真相拯救 -└─ 但核心成员必须击败 -``` - -### 流民与村落(求生派) - -``` -【基本信息】 -├─ 类型:分散的村落和部落 -├─ 领袖:各村村长/部落首领 -├─ 分布:北方和南方边境 -├─ 人口:约300万 -└─ 力量:最弱(无魔力) - -【核心理念】 -"安稳生活,远离魔力" -"魔力带来灾难,普通才是幸福" - -【行为】 -├─ 躲避魔物袭击 -├─ 在两大势力夹缝中求生 -├─ 厌恶魔力带来的灾难 -├─ 渴望和平,但无力反抗 -└─ 被动防御 - -【与主角关系】 -├─ 主角出身于流民村落 -├─ 对主角有天然好感 -├─ 愿意提供情报和支援 -├─ 是主角的主要盟友 -└─ 最终将成为新王国的基石 - -【可团结力量】 -├─ 各村落可以团结 -├─ 有战斗能力的猎人/卫兵 -├─ 了解魔物习性的老人 -└─ 主角可以动员的人力资源 -``` - ---- - -## [模块E] 灾难:浊潮 - -### 浊潮的本质 - -``` -【什么是浊潮】 -邪神莫克被封印在地底千年, -其残核不断散发浊力。 -当浊力积累到临界点, -会爆发"浊潮"—— -一股能够吞噬所有生命的浊力洪流。 - -【浊潮的后果】 -├─ 所有被波及的生灵都会被浊力侵蚀 -├─ 化为丧失理智的魔物 -├─ 莫克的力量将完全恢复 -├─ 艾奥斯大陆将彻底毁灭 -└─ 这是世界的末日 - -【浊潮的触发条件】 -├─ 旧魔力的过度使用(加剧艾拉痛苦) -├─ 浊力的主动滋养(浊光教派献祭) -├─ 时间推移(莫克逐渐恢复) -└─ 主角的选择(可延缓或加速) -``` - -### 灾难倒计时(天数推进) - -``` -【第1-20天】初期:浊力轻微扩散 -├─ 魔物:低阶为主,数量较少 -├─ 影响:仅边境村落受袭 -├─ 社会状态:相对平静 -├─ 主角行动: -│ ├─ 在故乡遭遇魔物 -│ ├─ 觉醒净灵能力 -│ ├─ 被迫出发冒险 -│ └─ 目标:寻找抑制浊力的方法 -└─ 关键事件:离开故乡,结识初期队友 - -【第21-50天】中期:浊力蔓延 -├─ 魔物:中阶出现,数量增加 -├─ 影响:大陆中部受波及 -├─ 社会状态:黑石王国与浊光教派冲突加剧 -├─ 主角行动: -│ ├─ 结识更多队友 -│ ├─ 化解局部危机(拯救村落、阻止献祭) -│ ├─ 破坏黑石王国的魔力矿脉 -│ └─ 了解世界真相 -└─ 关键事件:揭露三大阵营的本质 - -【第51-80天】后期:浊力逼近核心 -├─ 魔物:高阶横行,领地扩张 -├─ 影响:大陆核心区域受威胁 -├─ 社会状态: -│ ├─ 黑石王国濒临崩溃(贵族被反噬) -│ ├─ 浊光教派大规模献祭 -│ └─ 流民四散逃亡 -├─ 主角行动: -│ ├─ 团结所有可团结的力量 -│ ├─ 净化部分魔物,获得盟友 -│ ├─ 击败黑石王国/浊光教派 -│ └─ 准备对抗浊潮 -└─ 关键事件:联合军成立 - -【第81-90天】终局:浊潮爆发 -├─ 魔物:全面入侵 -├─ 影响:浊潮正式爆发 -├─ 社会状态:崩溃边缘 -├─ 主角行动: -│ ├─ 前往封印之地 -│ ├─ 以净灵能力为核心净化浊力 -│ ├─ 重塑魔力来源 -│ ├─ 封印邪神莫克 -│ └─ 建立和平王国 -└─ 关键事件:最终决战 -``` - -### 浊潮进度可视化 - -``` -游戏内显示: -┌─────────────────────────────────────────────────────┐ -│ 浊潮进度 │ -│ ████████░░░░░░░░░░░░░░░░░░░░ 20% Day 20 │ -│ ████████████████░░░░░░░░░░░░ 40% Day 50 │ -│ ████████████████████████░░░░ 70% Day 80 │ -│ ████████████████████████████ 100% Day 90+ │ -│ │ -│ 影响: │ -│ ├─ 魔物强度 +XX% │ -│ ├─ 商店物价 +XX% │ -│ ├─ NPC状态变化 │ -│ └─ 可用任务减少 │ -└─────────────────────────────────────────────────────┘ -``` - ---- - -## [模块F] 主角的特殊能力 - -### 净灵之力 - -``` -【觉醒】 -主角在故乡遭遇魔物袭击时, -在生死关头觉醒了净灵之力。 -这意味着主角是艾拉血脉的后裔。 - -【能力成长】 -初始: -├─ 能净化轻微浊力 -├─ 能安抚低阶魔物 -└─ 范围和效果都很弱 - -收集纯净碎片后: -├─ 净化能力增强 -├─ 可净化中阶魔物 -├─ 可净化旧魔力 -└─ 范围扩大 - -最终形态: -├─ 可净化整个大陆的浊力 -├─ 可重塑魔力来源 -├─ 可封印邪神莫克 -└─ 代价:可能需要牺牲 - -【纯净碎片】 -艾拉灵魂中未被浊力侵染的碎片。 -散落在大陆各处。 -收集它们可以: -├─ 增强净灵之力 -├─ 恢复艾拉的残存意识 -├─ 获得艾拉的指引 -└─ 最终用于重塑世界 -``` - -### 主角的使命 - -``` -【表层使命】 -阻止浊潮,拯救世界。 - -【深层使命】 -├─ 净化浊力,解除艾拉的痛苦 -├─ 安抚魔物,让被扭曲的生灵解脱 -├─ 终结恶性循环 -├─ 重塑魔力来源(纯净魔力而非旧魔力) -└─ 建立不再依赖浊力的新世界 - -【最终选择】 -结局时,主角面临选择: -├─ 净化一切(可能需要牺牲) -├─ 继承艾拉的力量成为新神 -├─ 封印莫克后回归凡人 -└─ 其他分支 -``` - ---- - -## 与原设定的对应关系 - -| 原设定 | 新设定 | 说明 | -|--------|--------|------| -| 艾尔德兰大陆 | 艾奥斯大陆 | 大陆名称 | -| 深渊/深渊之主 | 浊力/邪神莫克 | 核心威胁 | -| 誓约王冠 | 净灵之力 | 主角能力 | -| 深渊侵蚀 | 浊潮进度 | 灾难机制 | -| 七大王侯 | 三大阵营 | 势力结构 | -| 王室血脉 | 艾拉血脉 | 主角身份 | -| 誓约碎片 | 纯净碎片 | 收集物品 | - ---- - -## 替换指南 - -### 如果要进一步修改 - -``` -可调整的模块: -├─ [模块A] 地名、地理结构 -├─ [模块D] 阵营数量、名称、理念 -├─ [模块E] 灾难名称、触发机制 -└─ [模块F] 主角能力名称 - -需要保持的核心结构: -├─ 创世神话(神 + 邪神) -├─ 两大力量(负面力量 + 净化力量) -├─ 势力对立 -├─ 灾难倒计时 -├─ 主角的特殊身份 -└─ 收集关键道具的目标 -``` - ---- - -*下一章:[剧情大纲](04-story-outline.md)* diff --git a/.claude/skills/indie-game-dev/proposals/srpg-crown-oath/04-story-outline.md b/.claude/skills/indie-game-dev/proposals/srpg-crown-oath/04-story-outline.md deleted file mode 100644 index 9a6abdc2d7..0000000000 --- a/.claude/skills/indie-game-dev/proposals/srpg-crown-oath/04-story-outline.md +++ /dev/null @@ -1,454 +0,0 @@ -# 剧情大纲 - -> 90天冒险旅程,四篇章结构,与浊潮赛跑 - ---- - -## 剧情结构概览 - -``` -时间线:90天 -篇章数:4篇 -关卡数:约50个主线关卡 -支线数:约30个支线任务 -结局数:4个主要结局 -``` - -### 篇章划分 - -``` -┌────────────────────────────────────────────────────────┐ -│ 第1篇章:觉醒之旅(第1-20天) │ -│ ├─ 浊力轻微扩散,边境受袭 │ -│ ├─ 主角在故乡觉醒净灵之力 │ -│ ├─ 集结初期队友(莉娜、凯恩、薇拉) │ -│ ├─ 了解旧魔力、浊力、魔物的真相 │ -│ └─ 决定踏上阻止浊潮的旅程 │ -├────────────────────────────────────────────────────────┤ -│ 第2篇章:阵营冲突(第21-50天) │ -│ ├─ 浊力蔓延至大陆中部 │ -│ ├─ 深入了解三大阵营(黑石、浊光、流民) │ -│ ├─ 新队友加入(罗兰、艾琳) │ -│ ├─ 化解局部危机,破坏魔力矿脉 │ -│ └─ 揭露浊光教派被邪神操控的真相 │ -├────────────────────────────────────────────────────────┤ -│ 第3篇章:联合抗争(第51-80天) │ -│ ├─ 浊力逼近大陆核心 │ -│ ├─ 黑石王国崩溃,浊光教派疯狂献祭 │ -│ ├─ 塞拉斯加入(被净化的高阶魔物) │ -│ ├─ 团结各方力量,成立联合军 │ -│ └─ 收集纯净碎片,增强净灵之力 │ -├────────────────────────────────────────────────────────┤ -│ 第4篇章:净化世界(第81-90天) │ -│ ├─ 浊潮正式爆发 │ -│ ├─ 最终决战:邪神莫克 │ -│ ├─ 净化浊力,重塑魔力来源 │ -│ └─ 建立和平王国 / 多结局 │ -└────────────────────────────────────────────────────────┘ -``` - ---- - -## 第1篇章:觉醒之旅(Day 1-20) - -### Day 1-3:命运的觉醒 - -``` -【场景:净灵村(北方流民村落)】 - -Day 1 - 宁静的终结 -├─ 开场CG:千年前艾拉与莫克的大战 -├─ 主角在净灵村的日常生活 -├─ 村里有奇怪的老人(艾拉的残存意识) -├─ 入夜,浊力波动,魔物群袭击村庄 -├─ 主角在生死关头觉醒净灵之力 -│ └─ 能净化低阶魔物,让它们安息 -└─ 击退第一波魔物,但村庄受损 - -Day 2 - 觉醒的意义 -├─ 老人解释主角的能力 -│ ├─ 净灵之力:艾拉血脉的证明 -│ ├─ 浊力与旧魔力的真相 -│ └─ 浊潮正在逼近 -├─ 老人交给主角第一块纯净碎片 -├─ 村民商议:必须寻找彻底解决浊潮的方法 -└─ [莉娜] 邻村的学者,赶来支援 - -Day 3 - 踏上旅程 -├─ [莉娜] 加入队伍 -│ └─ 她在研究旧魔力,发现其中蕴含痛苦 -├─ 老人指引:去南方边境寻找更多线索 -├─ 主角决定离开故乡 -├─ 村民送行,托付希望 -└─ 出发前往南方 -``` - -### Day 4-10:初识世界 - -``` -【场景:前往南方边境的旅途】 - -Day 4-5 - 旅途见闻 -├─ [战斗教学] 路上遭遇低阶魔物 -├─ 主角尝试净化魔物 -│ └─ 魔物临死前恢复片刻清醒,感谢主角 -├─ 见到逃难的流民 -└─ 了解三大阵营的存在 - -Day 6-7 - 抵达边境 -├─ 抵达南方边境城镇「霜叶镇」 -├─ 这里是三大势力的交界处 -├─ [凯恩] 被黑石王国驱逐的骑士 -│ └─ 他因反对过度开采而被流放 -├─ [凯恩] 加入队伍 -└─ 获取更多关于浊力的情报 - -Day 8-10 - 第一份情报 -├─ 在霜叶镇收集情报 -├─ 得知: -│ ├─ 黑石王国在疯狂开采魔力 -│ ├─ 浊光教派在传播奇怪的信仰 -│ └─ 浊力扩散正在加速 -├─ [支线] 莉娜的研究笔记 -└─ 决定先调查浊光教派的动向 -``` - -### Day 11-20:浊光初现 - -``` -【场景:浊光教派边境据点】 - -Day 11-13 - 教派调查 -├─ 前往浊光教派的一个边境据点 -├─ 发现教派正在进行献祭仪式 -├─ 献祭对象:被抓获的流民 -├─ 主角震惊:这是在滋养浊力! -└─ [关卡] 阻止献祭仪式 - -Day 14-16 - 教派的疯狂 -├─ 从被救流民口中得知真相 -│ ├─ 浊光教派认为浊潮是"神圣净化" -│ ├─ 他们在加速浊潮的到来 -│ └─ 教主声称获得了"神的启示" -├─ [薇拉] 盗贼,曾潜入教派偷窃 -│ └─ 她看到了可怕的内幕,逃了出来 -├─ [薇拉] 加入队伍 -└─ 提供教派的核心情报 - -Day 17-19 - 真相初现 -├─ 薇拉透露: -│ ├─ 教派的"神"是地下的某种存在 -│ ├─ 教主在进行大规模献祭准备 -│ └─ 浊潮爆发是教派的目标 -├─ 莉娜分析:地下的存在可能是邪神残魂 -├─ 主角意识到问题的严重性 -└─ 决定前往黑石王国了解更多 - -Day 20 - 篇章结束 -├─ 准备前往黑石王国 -├─ 浊潮进度:20% -├─ 魔物强度开始明显提升 -└─ [第1篇章结束] -``` - ---- - -## 第2篇章:阵营冲突(Day 21-50) - -### Day 21-30:黑石王国 - -``` -【场景:黑石王国领土】 - -Day 21-23 - 进入王国 -├─ [关卡] 穿越黑石王国的边境 -├─ 见到疯狂开采魔力矿脉的场景 -├─ 大地千疮百孔,环境严重破坏 -├─ 平民生活在恐惧中 -└─ 魔物频繁出没 - -Day 24-26 - 王都黑曜城 -├─ 潜入王都黑曜城 -├─ 见识贵族的奢华与平民的苦难 -├─ 发现贵族已被魔力反噬 -│ └─ 性情暴躁、贪婪、冷酷 -├─ [罗兰] 黑石王国的禁卫军 -│ └─ 他对王国现状产生怀疑 -└─ [罗兰] 临时提供帮助 - -Day 27-30 - 矿脉真相 -├─ 罗兰带领主角潜入魔力矿脉 -├─ 发现: -│ ├─ 矿脉直接连接地下的浊力核心 -│ ├─ 开采会加速浊力扩散 -│ └─ 国王知道后果但不在乎 -├─ [关卡] 破坏一个魔力矿脉 -├─ 引起王国的注意,开始被追捕 -└─ 罗兰决定背叛王国,加入主角 -``` - -### Day 31-40:三大势力 - -``` -【场景:势力交界处】 - -Day 31-34 - 逃亡与反思 -├─ 逃离黑石王国追捕 -├─ 罗兰解释王国的情况: -│ ├─ 国王被魔力完全侵蚀 -│ ├─ 部分骑士想要改变 -│ └─ 平民苦不堪言 -├─ 讨论:是否有可能改变黑石王国? -└─ 决定争取王国中的觉醒者 - -Day 35-38 - 浊光教派的疯狂 -├─ 得知浊光教派正在进行大规模献祭 -├─ [艾琳] 浊光教派的叛逃者 -│ └─ 她发现了教派的真相 -├─ [艾琳] 加入队伍 -├─ 艾琳透露: -│ ├─ 教主被"神"操控(邪神残魂) -│ ├─ 教义都是谎言 -│ └─ 目标是加速浊潮让邪神复苏 -└─ [关卡] 阻止一次大规模献祭 - -Day 39-40 - 流民的困境 -├─ 返回流民聚集区 -├─ 见到流民的困境: -│ ├─ 魔物袭击频繁 -│ ├─ 黑石王国压榨 -│ └─ 浊光教派诱骗 -├─ 主角决定团结流民 -└─ 开始在各村落间奔走 -``` - -### Day 41-50:真相大白 - -``` -【场景:深入调查】 - -Day 41-45 - 浊光圣殿 -├─ 艾琳提供情报,潜入浊光圣殿 -├─ [关卡] 潜入 + 战斗 -├─ 发现核心秘密: -│ ├─ 教主在进行最终仪式的准备 -│ ├─ 目标是在浊潮爆发时献祭万人 -│ └─ 这将让邪神完全复苏 -├─ 艾琳的好感度剧情 -└─ 获取纯净碎片 - -Day 46-48 - 黑石的崩溃 -├─ 黑石王国因过度开采开始崩溃 -├─ 贵族被魔力反噬,开始发疯 -├─ 魔物攻破外围城池 -├─ 平民开始逃亡 -└─ 罗兰决定回去拯救能救的人 - -Day 49-50 - 篇章结束 -├─ 团结了部分流民村落 -├─ 获得了黑石王国部分骑士的支持 -├─ 阻止了多次浊光教派的献祭 -├─ 浊潮进度:40% -├─ 意识到必须主动出击 -└─ [第2篇章结束] -``` - ---- - -## 第3篇章:联合抗争(Day 51-80) - -### Day 51-60:团结力量 - -``` -【场景:流民联盟】 - -Day 51-55 - 联合军成立 -├─ 主角召集各方力量 -├─ 组成"净灵联合军": -│ ├─ 流民村落 -│ ├─ 黑石王国的觉醒骑士 -│ ├─ 认清真相的魔法师 -│ └─ 被净化的低阶魔物(可选) -├─ 制定对抗浊潮的计划 -└─ 收集更多纯净碎片 - -Day 56-60 - 净化的奇迹 -├─ 主角尝试净化一只中阶魔物 -├─ 魔物恢复意识,变成[塞拉斯] -├─ [塞拉斯] 曾是强大的魔法师 -│ └─ 被浊力侵蚀后成为魔物 -├─ 塞拉斯提供关键情报: -│ ├─ 地下封印之地的位置 -│ ├─ 邪神莫克的状态 -│ └─ 浊潮爆发的精确条件 -└─ [塞拉斯] 加入队伍 -``` - -### Day 61-70:主动出击 - -``` -【场景:浊光教派总攻】 - -Day 61-65 - 浊光教派决战 -├─ 联合军进攻浊光教派圣城 -├─ [大关卡] 攻城战 -├─ 对峙浊光教主 -│ └─ 教主被邪神残魂附体 -├─ 揭露真相给教派底层信徒 -│ └─ 部分信徒倒戈 -├─ 击败教主,但邪神残魂逃回封印之地 -└─ 获取重要纯净碎片 - -Day 66-70 - 黑石的终结 -├─ 黑石王国彻底崩溃 -├─ 国王化为高阶魔物 -├─ 主角率军进入黑曜城 -├─ [大关卡] 净化国王/击败魔物王 -├─ 解救被困的平民 -├─ 罗兰的好感度剧情 -└─ 获得魔力矿脉的控制权 -``` - -### Day 71-80:最终准备 - -``` -【场景:封印之地周边】 - -Day 71-75 - 封印之地 -├─ 前往创世之眼(艾拉心脏所化) -├─ 准备进入地下封印之地 -├─ 收集最后几块纯净碎片 -├─ 净灵之力大幅增强 -└─ 艾拉的残存意识更加清晰 - -Day 76-80 - 最终部署 -├─ 艾拉的意识给予指引: -│ ├─ 最终战需要在封印之地进行 -│ ├─ 主角的净灵之力是封印邪神的关键 -│ └─ 需要在浊潮爆发前行动 -├─ 联合军部署防守 -├─ 队友好感度最终阶段 -├─ 浊潮进度:70% -└─ [第3篇章结束] -``` - ---- - -## 第4篇章:净化世界(Day 81-90) - -### Day 81-85:浊潮爆发 - -``` -【场景:浊潮爆发】 - -Day 81-83 - 末日降临 -├─ 浊潮正式爆发 -├─ 浊力从地底喷涌而出 -├─ 大陆各地魔物暴动 -├─ 联合军各地防守 -└─ 主角率队突入封印之地 - -Day 84-85 - 封印之地 -├─ [关卡] 深入地下 -├─ 面对各种高阶魔物 -├─ 塞拉斯的抉择(牺牲或救赎) -├─ 见到封印中的邪神莫克 -└─ 准备最终决战 -``` - -### Day 86-90:最终决战 - -``` -【场景:封印之地核心】 - -Day 86-88 - 邪神莫克 -├─ [Boss战] 邪神莫克(多阶段) -│ ├─ 第一阶段:邪神残魂 -│ ├─ 第二阶段:浊力化身 -│ └─ 第三阶段:完全复苏 -├─ 队友的支援与牺牲 -├─ 艾拉的残存意识协助 -└─ 进入结局分支 - -Day 89-90 - 结局 -├─ 根据选择和条件进入不同结局 -├─ 净化浊力 / 封印邪神 -├─ 重塑魔力来源 -└─ 建立新世界 -``` - ---- - -## 关键剧情选择点 - -``` -选择1(Day 20): -├─ 是否净化魔物(而非杀死)? -└─ 影响塞拉斯是否可加入 - -选择2(Day 40): -├─ 是否拯救黑石王国的平民? -└─ 影响罗兰的好感度和联合军实力 - -选择3(Day 60): -├─ 是否信任被净化的魔物塞拉斯? -└─ 影响塞拉斯的命运和最终战 - -选择4(Day 75): -├─ 如何处理魔力矿脉? -│ ├─ 完全封印(牺牲魔力来源) -│ ├─ 保留并净化(保留魔力但需时间) -│ └─ 其他选择 -└─ 影响新世界的魔力体系 - -选择5(Day 88): -├─ 最终选择: -│ ├─ 净化一切(可能需要牺牲)→ 真结局 -│ ├─ 封印邪神后回归凡人 → 正结局 -│ ├─ 继承艾拉之力成为新神 → 神结局 -│ └─ 接受浊力(暗黑结局)→ 暗黑结局 -└─ 决定最终结局 -``` - ---- - -## 结局系统 - -### 主要结局 - -| 结局 | 条件 | 描述 | -|------|------|------| -| **真结局** | 最高好感Lv.6 + 全碎片 + 正确选择 | 主角净化一切,与爱人共建和平王国 | -| **正结局** | 完成主线 | 封印邪神,主角成为新王,但失去净灵之力 | -| **神结局** | 特定选择 | 主角继承艾拉之力成为新神,守护世界 | -| **牺牲结局** | 特定选择 | 主角牺牲自己净化世界,队友继承遗志 | -| **暗黑结局** | 好感度全低 + 错误选择 | 主角接受浊力,成为新的邪神 | - -### 后日谈(王国建设模式) - -``` -通关后解锁: -├─ 王国建设系统 -├─ 建立不再依赖浊力的新社会 -├─ 队友专属后日谈剧情 -├─ 高难度挑战关卡 -├─ 新角色招募 -└─ 多周目继承要素 -``` - ---- - -## 与原设定的对应 - -| 原设定 | 新设定 | 调整说明 | -|--------|--------|----------| -| 100天 | 90天 | 缩短节奏,更紧凑 | -| 深渊侵蚀 | 浊潮进度 | 概念统一 | -| 誓约碎片 | 纯净碎片 | 名称调整 | -| 王室血脉 | 艾拉血脉 | 背景调整 | -| 七大王侯 | 三大阵营 | 简化势力 | -| 深渊之主 | 邪神莫克 | 最终BOSS | - ---- - -*下一章:[角色设计](05-characters.md)* diff --git a/.claude/skills/indie-game-dev/proposals/srpg-crown-oath/05-characters.md b/.claude/skills/indie-game-dev/proposals/srpg-crown-oath/05-characters.md deleted file mode 100644 index f586df3efe..0000000000 --- a/.claude/skills/indie-game-dev/proposals/srpg-crown-oath/05-characters.md +++ /dev/null @@ -1,616 +0,0 @@ -# 角色设计 - ---- - -## 一、主角 - -### 基本信息 - -``` -【主角】(可改名) - -基础信息: -├─ 年龄:18岁 -├─ 性别:男/女(可选) -├─ 身高:男175cm / 女165cm -├─ 职业:流民 → 净灵者 → 净灵王 -├─ 武器:剑(可注入净灵之力) -└─ 属性倾向:平衡型 - -外观特征: -├─ 发色:深棕色 -├─ 瞳色:金色(觉醒后) -├─ 特征:觉醒后身上会出现淡金色的灵纹 -└─ 服装:初期朴素流民装,后期净灵王服饰 -``` - -### 背景故事 - -``` -【公开背景】 -北方流民村落「净灵村」的普通青年。 -从小被村里的神秘老人收养。 -不知道自己的真实身世。 -在浊潮即将来临之际觉醒了净灵之力。 - -【真实身份】 -创世神艾拉的血脉后裔。 -千年前艾拉牺牲时, -部分血脉流传于世。 -主角是这一血脉的继承者, -因此能够觉醒净灵之力。 - -【能力觉醒】 -在故乡遭遇魔物袭击时, -在生死关头觉醒了净灵之力。 -这意味着浊潮已经逼近, -艾拉的残存意识选择了这个时机唤醒主角。 -``` - -### 性格系统 - -``` -玩家可选择的性格倾向: - -【守护型】 -├─ 台词特点:温柔、坚定、保护 -├─ 决策倾向:优先保护弱者 -├─ 队友好感加成:骑士、牧师 +20% -└─ 结局倾向:正结局、真结局 - -【探索型】 -├─ 台词特点:好奇、分析、求知 -├─ 决策倾向:追求真相和知识 -├─ 队友好感加成:法师、学者 +20% -└─ 结局倾向:神结局、正结局 - -【抗争型】 -├─ 台词特点:坚定、热血、不屈 -├─ 决策倾向:不屈服于命运 -├─ 队友好感加成:盗贼、战士 +20% -└─ 结局倾向:真结局、牺牲结局 -``` - -### 净灵之力成长 - -``` -能力进化: -流民 → 净灵者(觉醒)→ 净灵师(收集碎片)→ 净灵王(最终) - -净灵王特殊能力: -├─ 可净化整个大陆的浊力 -├─ 可安抚大量魔物 -├─ 可重塑魔力来源 -├─ 可与邪神莫克正面对抗 -└─ 可获得艾拉的残存意识指引 -``` - ---- - -## 二、队友角色 - -### 莉娜(法师) - -``` -【基本信息】 -├─ 全名:莉娜·星语 -├─ 年龄:17岁 -├─ 性别:女 -├─ 身高:160cm -├─ 职业:学者 → 净化法师 -├─ 武器:法杖 -└─ 属性:净化系魔法 - -【外观】 -├─ 发色:金发 -├─ 发型:单马尾 -├─ 瞳色:翠绿色 -├─ 特征:总是带着书本 -└─ 服装:学者袍 - -【性格】 -├─ 表面:开朗、好奇、话多 -├─ 内心:对旧魔力的真相感到恐惧 -├─ 优点:知识丰富、分析能力强 -└─ 缺点:有时候过于理性 - -【背景故事】 -邻村的研究学者。 -一直在研究旧魔力的本质。 -发现旧魔力中蕴含着"痛苦"。 -在主角觉醒后赶来支援。 -希望找到净化旧魔力的方法。 - -【加入时间】Day 3 - -【好感度剧情】 -Lv.2: 聊起她对旧魔力的研究 -Lv.3: 发现旧魔力的真相(痛苦来源) -Lv.4: 决定不再使用旧魔力 -Lv.5: 开发净化魔法 + 专属技能 -Lv.6: 成为净化法师,专属CG - -【专属技能】 -Lv.4 解锁 - 「净化之光」 -└─ 魔法可净化小范围浊力,对魔物伤害+20% - -【战斗定位】 -范围输出 + 净化辅助 -可净化浊力debuff -``` - -### 凯恩(骑士) - -``` -【基本信息】 -├─ 全名:凯恩·铁心 -├─ 年龄:26岁 -├─ 性别:男 -├─ 身高:185cm -├─ 职业:骑士 → 守护骑士 -├─ 武器:剑 + 盾 -└─ 属性:防御型 - -【外观】 -├─ 发色:黑色短发 -├─ 瞳色:深灰色 -├─ 特征:坚毅的表情 -└─ 服装:破旧的骑士铠甲 - -【性格】 -├─ 表面:沉默、可靠、正直 -├─ 内心:对过去的决定感到后悔 -├─ 优点:忠诚、勇敢、责任心强 -└─ 缺点:有时候过于固执 - -【背景故事】 -曾是黑石王国的骑士。 -因反对过度开采魔力矿脉而被驱逐。 -目睹了魔力对贵族的侵蚀。 -一直在寻找真正值得守护的人。 -在边境遇到主角,决定追随。 - -【加入时间】Day 7 - -【好感度剧情】 -Lv.2: 提起在黑石王国的经历 -Lv.3: 面对曾经的同僚 -Lv.4: 决定守护主角到底 -Lv.5: 重拾骑士的信念 + 专属技能 -Lv.6: 成为守护骑士,专属CG - -【专属技能】 -Lv.4 解锁 - 「铁心守护」 -└─ 守护技能效果+25%,可保护相邻队友 - -【战斗定位】 -前排坦克 + 守护 -高防御,保护队友 -``` - -### 薇拉(盗贼) - -``` -【基本信息】 -├─ 全名:薇拉·影舞 -├─ 年龄:19岁 -├─ 性别:女 -├─ 身高:165cm -├─ 职业:盗贼 → 影行者 -├─ 武器:双匕首 -└─ 属性:敏捷型 - -【外观】 -├─ 发色:银灰色长发 -├─ 瞳色:紫色 -├─ 特征:神秘的气质 -└─ 服装:轻便的暗色服装 - -【性格】 -├─ 表面:毒舌、冷漠、不信任人 -├─ 内心:渴望被接纳 -├─ 优点:机敏、果断、情报能力 -└─ 缺点:难以敞开心扉 - -【背景故事】 -曾是浊光教派的"影子"。 -负责潜入和暗杀任务。 -在一次行动中发现了教派的可怕内幕。 -逃离教派,被追杀。 -在边境遇到主角,选择加入。 - -【加入时间】Day 16 - -【好感度剧情】 -Lv.2: 提起逃离教派的经历 -Lv.3: 教派的追兵出现 -Lv.4: 主角保护了她 -Lv.5: 彻底摆脱过去的阴影 + 专属技能 -Lv.6: 敞开心扉,专属CG - -【专属技能】 -Lv.4 解锁 - 「影之净化」 -└─ 攻击可附带微量净化效果,对魔物暴击率+15% - -【战斗定位】 -刺客输出 + 情报 -高暴击,快速击杀 -``` - -### 罗兰(枪斗士) - -``` -【基本信息】 -├─ 全名:罗兰·黑石 -├─ 年龄:27岁 -├─ 性别:男 -├─ 身高:188cm -├─ 职业:禁卫军 → 义军将领 -├─ 武器:长枪 -└─ 属性:中程输出 - -【外观】 -├─ 发色:浅棕色 -├─ 瞳色:琥珀色 -├─ 特征:高大的身材 -└─ 服装:黑石王国军服(后期更换) - -【性格】 -├─ 表面:正直、热血、有担当 -├─ 内心:对王国的现状感到痛苦 -├─ 优点:正直、勇敢、有领导力 -└─ 缺点:有时候过于理想化 - -【背景故事】 -黑石王国的禁卫军成员。 -出身平民家庭,凭借实力晋升。 -对王国疯狂开采魔力感到不安。 -在主角潜入时选择帮助。 -最终决定背叛王国,追随主角。 - -【加入时间】Day 30 - -【好感度剧情】 -Lv.2: 聊起王国的过去和现在 -Lv.3: 面对曾经的战友 -Lv.4: 决定建立新的秩序 -Lv.5: 成为义军将领 + 专属技能 -Lv.6: 并肩作战到最后,专属CG - -【专属技能】 -Lv.4 解锁 - 「正义之枪」 -└─ 对浊力侵蚀的敌人伤害+25% - -【战斗定位】 -中排输出 + 突进 -长距离攻击,击退敌人 -``` - -### 艾琳(牧师) - -``` -【基本信息】 -├─ 全名:艾琳·晨光 -├─ 年龄:20岁 -├─ 性别:女 -├─ 身高:162cm -├─ 职业:修女 → 净化祭司 -├─ 武器:圣杖 -└─ 属性:治疗型 - -【外观】 -├─ 发色:淡金色长发 -├─ 瞳色:浅蓝色 -├─ 特征:温柔的微笑 -└─ 服装:修女服(后期更换) - -【性格】 -├─ 表面:温柔、善良、虔诚 -├─ 内心:对信仰产生动摇 -├─ 优点:治愈、包容、洞察力 -└─ 缺点:有时候过于软弱 - -【背景故事】 -浊光教派的高级修女。 -从小被教派收养,虔诚信仰。 -无意中发现教派的可怕真相。 -教主被邪神残魂操控。 -决定逃离教派,揭露真相。 - -【加入时间】Day 38 - -【好感度剧情】 -Lv.2: 提起在教派的生活 -Lv.3: 信仰的动摇 -Lv.4: 发现真正的信仰(净化而非浊力) -Lv.5: 成为净化祭司 + 专属技能 -Lv.6: 找到新的信仰,专属CG - -【专属技能】 -Lv.4 解锁 - 「净化祈祷」 -└─ 治疗时可净化目标的浊力debuff - -【战斗定位】 -治疗 + 净化 -恢复生命,驱浊力debuff -``` - -### 塞拉斯(特殊角色) - -``` -【基本信息】 -├─ 全名:塞拉斯(原名已忘) -├─ 年龄:?(曾是人类时约30岁) -├─ 性别:男 -├─ 身高:180cm -├─ 职业:魔物 → 净化魔物 → 净灵使者 -├─ 武器:浊力核心(净化后) -└─ 属性:特殊型 - -【外观】 -├─ 发色:深紫色 -├─ 瞳色:暗红色(净化后变金色) -├─ 特征:身上有浊力痕迹(净化后变灵纹) -└─ 服装:破旧的长袍 - -【性格】 -├─ 表面:神秘、冷静、难以捉摸 -├─ 内心:对过去充满悔恨 -├─ 优点:了解浊力和魔物的本质 -└─ 缺点:曾做过可怕的事 - -【背景故事】 -曾是一位强大的魔法师。 -为了力量过度使用旧魔力。 -最终被浊力侵蚀,化为魔物。 -在魔物状态下失去了理智。 -主角净化后恢复了意识。 -决定赎罪,帮助主角对抗浊潮。 - -【加入时间】Day 56 - -【好感度剧情】 -Lv.2: 提起作为魔物时的体验 -Lv.3: 面对曾经伤害过的人 -Lv.4: 主角选择继续信任 -Lv.5: 找到救赎 + 专属技能 -Lv.6: 彻底净化,成为净灵使者,专属CG - -【专属技能】 -Lv.4 解锁 - 「浊力掌控」 -└─ 可转化浊力为力量,但需要消耗HP - -【战斗定位】 -特殊输出 + 代价机制 -高伤害,但需要付出代价 -``` - ---- - -## 三、重要NPC - -### 艾拉的残存意识 - -``` -【基本信息】 -├─ 身份:创世神艾拉的残存意识 -├─ 外观:住在净灵村的神秘老人 -├─ 作用:导师、指引者 - -【角色定位】 -├─ 教导主角使用净灵之力 -├─ 解释世界的真相 -├─ 提供纯净碎片 -├─ 在关键时刻给予指引 -└─ 最终可能完全消散 - -【秘密】 -他/她就是艾拉残存意识的一部分。 -等待能够继承净灵之力的人出现。 -主角就是那个人。 -``` - -### 黑石国王 - -``` -【基本信息】 -├─ 年龄:50岁(外观) -├─ 身份:黑石王国国王 -├─ 关系:主要反派之一 - -【角色定位】 -├─ 代表"力量至上"的理念 -├─ 疯狂开采魔力的罪魁祸首 -├─ 被浊力严重侵蚀 -└─ 最终化为高阶魔物 - -【特征】 -├─ 外表威严但眼神疯狂 -├─ 坚信"只要足够强就能抵御一切" -├─ 不在意子民的死活 -└─ 是浊潮加速的原因之一 -``` - -### 浊光教主 - -``` -【基本信息】 -├─ 年龄:不明 -├─ 身份:浊光教派教主 -├─ 关系:主要反派之一 - -【角色定位】 -├─ 表面是宗教领袖 -├─ 实际被邪神残魂操控 -├─ 加速浊潮的核心人物 -└─ 最终战前的强大敌人 - -【真相】 -千年前邪神莫克被封印时, -残魂逃出,附身于第一代教主。 -世代传承,等待浊潮爆发。 -目标是让莫克完全复苏。 -``` - -### 邪神莫克(最终BOSS) - -``` -【基本信息】 -├─ 真实身份:外来毁灭邪神 -├─ 年龄:无尽 -├─ 身份:浊潮的源头 -├─ 关系:最终BOSS - -【背景故事】 -千年前降临艾奥斯的外来邪神。 -试图吞噬整个世界的生命。 -被创世神艾拉以牺牲为代价封印。 -千年来在地底散发浊力。 -等待浊潮爆发时完全复苏。 - -【最终战形态】 -├─ 第一形态:邪神残魂(教主附体) -├─ 第二形态:浊力化身(巨大能量体) -└─ 第三形态:完全复苏(邪神本体) - -【关键对话】 -"艾拉的牺牲是徒劳的..." -"浊力终将吞噬一切..." -"凡人,你无法理解真正的力量..." -``` - ---- - -## 四、角色关系图 - -``` - [邪神莫克] - (浊力源头) - │ - ┌────────────┼────────────┐ - │ │ │ - [浊光教主] [黑石国王] [浊力] - (被操控) (被侵蚀) (蔓延) - │ │ - ▼ ▼ - [浊光教派] [黑石王国] - │ │ - └─────┬──────┘ - │ - [魔物产生] - │ - ┌─────┴─────┐ - │ │ - [流民受苦] [主角觉醒] - │ │ - └─────┬─────┘ - │ - [艾拉的指引] - │ - [净灵之力] - │ - ┌─────────────┼─────────────┐ - │ │ │ -[主角] ────► [队友们] ◄──── [纯净碎片] - │ │ - └─────┬───────┘ - │ - [联合军成立] - │ - [对抗浊潮] - │ - [最终决战] -``` - ---- - -## 五、阵营角色分布 - -``` -【黑石王国】 -├─ 罗兰(禁卫军 → 叛离) -├─ 凯恩(骑士 → 被驱逐) -├─ 黑石国王(反派) -└─ 部分觉醒骑士(可团结) - -【浊光教派】 -├─ 薇拉(影舞者 → 叛离) -├─ 艾琳(修女 → 叛离) -├─ 浊光教主(反派) -└─ 部分底层信徒(可争取) - -【流民与自由部落】 -├─ 主角(净灵村) -├─ 莉娜(邻村学者) -├─ 艾拉的残存意识(净灵村老人) -└─ 各村落首领(可团结) - -【特殊】 -├─ 塞拉斯(被净化的魔物) -└─ 其他可净化的魔物(可选) -``` - ---- - -## 六、美术需求 - -### 角色立绘规格 - -``` -每个角色需要: -├─ 全身立绘 × 3-5 种姿势 -│ ├─ 标准(战斗) -│ ├─ 日常(对话) -│ ├─ 好感(亲密) -│ ├─ 悲伤(剧情) -│ └─ 结局(特殊) -├─ 头像 × 5-8 种表情 -│ ├─ 普通 -│ ├─ 微笑 -│ ├─ 悲伤 -│ ├─ 愤怒 -│ ├─ 惊讶 -│ ├─ 害羞 -│ ├─ 思考 -│ └─ 特殊 -└─ CG × 2-3 张 - ├─ 好感度CG - └─ 结局CG -``` - -### 像素精灵规格 - -``` -每个角色需要: -├─ 站立动画:4帧 -├─ 行走动画:6帧 -├─ 攻击动画:8-12帧(每技能) -├─ 受击动画:2帧 -├─ 死亡动画:4帧 -└─ 技能特效:单独制作 - -分辨率: -├─ 标准角色:48×72 px -├─ 大型角色(如罗兰):56×80 px -└─ BOSS角色:单独定制 -``` - -### 特殊:浊力视觉效果 - -``` -【浊力侵蚀】 -├─ 普通人:轻微紫色光环 -├─ 中度侵蚀:紫色纹路,眼神异常 -├─ 重度侵蚀:身体变形,紫黑色 -└─ 魔物:完全扭曲,浊力核心 - -【净化效果】 -├─ 净灵之力:金色光芒 -├─ 净化过程:紫转金,温和光效 -├─ 净化完成:金色灵纹 -└─ 纯净碎片:纯净金色光点 -``` - ---- - -*下一章:[系统设计](06-systems.md)* diff --git a/.claude/skills/indie-game-dev/proposals/srpg-crown-oath/06-systems.md b/.claude/skills/indie-game-dev/proposals/srpg-crown-oath/06-systems.md deleted file mode 100644 index 239b5de855..0000000000 --- a/.claude/skills/indie-game-dev/proposals/srpg-crown-oath/06-systems.md +++ /dev/null @@ -1,616 +0,0 @@ -# 系统设计 - ---- - -## 一、好感度系统 - -### 1.1 系统概述 - -``` -好感度是本作的核心系统之一。 -影响:剧情分支、战斗连携、结局走向。 - -特点: -├─ 6位队友,每人独立好感度 -├─ 6个等级,逐级解锁内容 -├─ 多种提升方式 -└─ 等级6仅限1人达成(真结局条件) -``` - -### 1.2 好感度等级 - -``` -┌─────────────────────────────────────────────────────┐ -│ Lv.1 陌生人 │ -│ ├─ 初始状态 │ -│ └─ 无特殊内容 │ -├─────────────────────────────────────────────────────┤ -│ Lv.2 认识 │ -│ ├─ 解锁日常对话 │ -│ ├─ 可赠送礼物 │ -│ └─ 好感度:100 │ -├─────────────────────────────────────────────────────┤ -│ Lv.3 朋友 │ -│ ├─ 解锁专属支线任务 │ -│ ├─ 战斗中有特殊语音 │ -│ └─ 好感度:300 │ -├─────────────────────────────────────────────────────┤ -│ Lv.4 信任 │ -│ ├─ 解锁专属被动技能 │ -│ ├─ 可触发羁绊连携技 │ -│ └─ 好感度:600 │ -├─────────────────────────────────────────────────────┤ -│ Lv.5 羁绊 │ -│ ├─ 解锁专属CG │ -│ ├─ 剧情中有特殊互动 │ -│ ├─ 成为可选"结局对象" │ -│ └─ 好感度:1000 │ -├─────────────────────────────────────────────────────┤ -│ Lv.6 灵魂伴侣 │ -│ ├─ 仅1人可达成 │ -│ ├─ 真结局必要条件 │ -│ ├─ 专属结局CG │ -│ └─ 好感度:1500 + 特殊条件 │ -└─────────────────────────────────────────────────────┘ -``` - -### 1.3 好感度提升方式 - -``` -【战斗参与】 -├─ 该角色参与战斗:+1 好感/场 -├─ 该角色击杀敌人:+0.5 好感/个 -├─ 该角色击杀BOSS:+5 好感 -├─ 该角色被击倒:-2 好感 -└─ 每日战斗好感上限:20 - -【剧情选择】 -├─ 支持该角色观点:+5 好感 -├─ 反对该角色观点:-3 好感 -├─ 中立选择:+0 好感 -└─ 关键选择:+10/-10 好感 - -【专属支线】 -├─ 完成支线第一阶段:+50 好感 -├─ 完成支线第二阶段:+80 好感 -├─ 完成支线第三阶段:+120 好感 -└─ 放弃/失败支线:-30 好感 - -【礼物赠送】 -├─ 喜欢的礼物:+15 好感 -├─ 普通礼物:+5 好感 -├─ 不喜欢的礼物:-5 好感 -└─ 每周限送3次 - -【特定地点】 -├─ 带该角色去有回忆的地点:+20 好感 -├─ 触发特殊对话 -└─ 限1次/角色 - -【休息互动】 -├─ 露营时选择与该角色交谈:+3 好感 -├─ 可能触发随机事件 -└─ 每次休息限1次 -``` - -### 1.4 礼物系统 - -``` -每个角色有3档喜好: - -【莉娜】 -├─ 喜欢:魔法书、可爱饰品、甜点 -├─ 普通:药水、食材、普通装备 -└─ 讨厌:重型武器、暗黑物品 - -【凯恩】 -├─ 喜欢:骑士装备、酒、武艺秘籍 -├─ 普通:药品、食物、普通装备 -└─ 讨厌:华丽饰品、甜食 - -【薇拉】 -├─ 喜欢:暗器、情报、自由相关物品 -├─ 普通:药水、金币、普通装备 -└─ 讨厌:约束类物品、过于正义的东西 - -【罗兰】 -├─ 喜欢:北方特产、武器、荣誉勋章 -├─ 普通:食物、药品、普通装备 -└─ 讨厌:阴险的东西、背叛相关 - -【艾琳】 -├─ 喜欢:花卉、祈祷用品、温柔的东西 -├─ 普通:药品、食物、普通装备 -└─ 讨厌:暗黑物品、暴力相关 - -【塞拉斯】 -├─ 喜欢:神秘物品、深渊相关(特殊) -├─ 普通:药水、书籍 -└─ 讨厌:神圣物品(减半但可接受) -``` - -### 1.5 Lv.6 特殊条件 - -``` -要达成 Lv.6,除好感度1500外,还需: - -【莉娜】完成全部师父相关剧情 + 亲眼见证师父遗物 -【凯恩】重建白银骑士团 + 誓死效忠剧情 -【薇拉】彻底摆脱暗影之手 + 敞开心扉 -【罗兰】成为近卫队长 + 并肩作战100场 -【艾琳】觉醒神圣血脉 + 选择信任到底 -【塞拉斯】选择信任 + 塞拉斯存活 + 净化成功 - -注意: -├─ Lv.6 只能让1人达成 -├─ 达成 Lv.6 后,其他人最高只能到 Lv.5 -└─ 这影响真结局的伴侣选择 -``` - ---- - -## 二、天数推进系统 - -### 2.1 时间机制 - -``` -【基础规则】 -├─ 游戏共100天 -├─ 主线任务推进 1-3 天 -├─ 支线任务推进 0.5-1 天 -├─ 自由探索可选是否推进 -├─ 训练/建设推进 0.5-1 天 -└─ 休息推进 0.5 天 - -【强制节点】 -├─ Day 20:第1篇章结束(强制) -├─ Day 50:第2篇章结束(强制) -├─ Day 80:第3篇章结束(强制) -└─ Day 100:最终决战(强制) - -【时间不足】 -如果主线进度落后于天数: -├─ 触发警告提示 -├─ 可以选择跳过部分支线 -├─ 或接受更差的结局 -└─ 不会直接 Game Over -``` - -### 2.2 深渊侵蚀进度 - -``` -【侵蚀可视化】 -├─ 世界地图上显示侵蚀区域 -├─ 侵蚀区域每天扩大 -├─ 被侵蚀的区域: -│ ├─ 敌人更强 -│ ├─ 资源更少 -│ └─ 部分NPC消失 -└─ 侵蚀度影响结局 - -【侵蚀进度表】 -Day 1-20: 侵蚀 5% → 15% -Day 21-50: 侵蚀 15% → 35% -Day 51-80: 侵蚀 35% → 60% -Day 81-100: 侵蚀 60% → 100% - -【影响】 -├─ 侵蚀度 < 40%:正常结局 -├─ 侵蚀度 40-70%:困难但可完成 -├─ 侵蚀度 > 70%:部分内容无法完成 -└─ 侵蚀度 100%:游戏结束(坏结局) -``` - -### 2.3 日程规划 - -``` -玩家需要平衡: -├─ 主线进度(必须) -├─ 支线任务(可选但有价值) -├─ 角色好感(影响结局) -├─ 装备强化(提升战力) -├─ 探索收集(获取资源) -└─ 休息恢复(恢复状态) - -时间不够时需要取舍: -├─ 跳过部分支线 -├─ 减少探索时间 -├─ 牺牲部分好感培养 -└─ 但主线必须完成 -``` - ---- - -## 三、羁绊连携系统 - -### 3.1 连携技机制 - -``` -【触发条件】 -├─ 两名队友好感度均达到 Lv.4+ -├─ 战斗中相邻(1格内) -├─ 双方 AP 足够(各消耗 2 AP) -├─ 冷却时间:3 回合 -└─ 每场战斗限用 2 次 - -【连携技效果】 -├─ 消耗双方各 2 AP -├─ 专属动画 -├─ 高伤害(约为双方攻击力之和 × 2) -├─ 附加特殊效果 -└─ 不会误伤友军 -``` - -### 3.2 连携技列表 - -``` -【主角 + 莉娜】誓约·烈焰斩 -├─ 效果:火焰剑气,3格直线伤害 -├─ 特效:灼烧 2 回合 -└─ 条件:主角好感 Lv.4+,莉娜好感 Lv.4+ - -【主角 + 凯恩】双剑·十字斩 -├─ 效果:对单体造成巨额伤害 -├─ 特效:破防 2 回合 -└─ 条件:主角好感 Lv.4+,凯恩好感 Lv.4+ - -【主角 + 薇拉】暗影·绝杀 -├─ 效果:瞬移到敌人背后,必暴击 -├─ 特效:无视防御 -└─ 条件:主角好感 Lv.4+,薇拉好感 Lv.4+ - -【主角 + 罗兰】冲锋·螺旋枪 -├─ 效果:直线 4 格穿透伤害 -├─ 特效:击退敌人 -└─ 条件:主角好感 Lv.4+,罗兰好感 Lv.4+ - -【主角 + 艾琳】圣光·治愈斩 -├─ 效果:对敌人造成伤害,同时治愈全队 -├─ 特效:治愈量 = 伤害的 50% -└─ 条件:主角好感 Lv.4+,艾琳好感 Lv.4+ - -【主角 + 塞拉斯】虚空·终结 -├─ 效果:超高伤害,但双方各损失 10% HP -├─ 特效:无视防御和抗性 -└─ 条件:主角好感 Lv.4+,塞拉斯存活 + 信任 - -【凯恩 + 罗兰】骑士·双重冲击 -├─ 效果:双人冲锋,范围伤害 -├─ 特效:眩晕 1 回合 -└─ 条件:凯恩好感 Lv.4+,罗兰好感 Lv.4+ - -【莉娜 + 艾琳】元素·圣愈 -├─ 效果:全队治愈 + buff -├─ 特效:治愈 + 攻击+20% 3回合 -└─ 条件:莉娜好感 Lv.4+,艾琳好感 Lv.4+ - -【薇拉 + 塞拉斯】暗影·虚空步 -├─ 效果:瞬移到任意敌人背后,连击 -├─ 特效:必定暴击,无视闪避 -└─ 条件:薇拉好感 Lv.4+,塞拉斯存活 + 信任 - -【罗兰 + 艾琳】守护·圣壁 -├─ 效果:全队防御大幅提升 -├─ 特效:防御+50%,持续 3 回合 -└─ 条件:罗兰好感 Lv.4+,艾琳好感 Lv.4+ -``` - ---- - -## 四、领地经营系统(中期解锁) - -### 4.1 系统概述 - -``` -【解锁时间】Day 51+(第3篇章开始) - -【背景】 -随着主角身份确认, -开始收复旧王国的领土。 -需要在冒险之余管理领地。 - -【核心玩法】 -├─ 建设建筑 -├─ 分配资源 -├─ 培养兵源 -└─ 影响最终战实力 -``` - -### 4.2 建筑系统 - -``` -【建筑类型】 - -资源类: -├─ 农田 → 产出食物 -├─ 矿场 → 产出矿石 -├─ 伐木场 → 产出木材 -└─ 商站 → 产出金币 - -军事类: -├─ 兵营 → 招募士兵 -├─ 训练场 → 提升士兵等级 -├─ 武器坊 → 生产装备 -└─ 城墙 → 提升防御 - -功能类: -├─ 研究所 → 解锁新科技 -├─ 酒馆 → 招募佣兵 -├─ 神殿 → 提供祝福 -└─ 仓库 → 增加存储 - -特殊类: -├─ 誓约神殿 → 主线相关 -├─ 英雄雕像 → 提升士气 -└─ 队友专属建筑 → 提升好感和能力 -``` - -### 4.3 资源系统 - -``` -【基础资源】 -├─ 食物:维持军队 -├─ 矿石:建造和装备 -├─ 木材:建造 -└─ 金币:通用货币 - -【特殊资源】 -├─ 魔力水晶:魔法相关 -├─ 深渊核心:暗黑装备 -├─ 誓约碎片:主线道具 -└─ 声望:解锁功能 - -【资源获取】 -├─ 建筑产出 -├─ 关卡奖励 -├─ 探索收集 -├─ 贸易交换 -└─ 任务奖励 -``` - -### 4.4 对最终战影响 - -``` -领地经营影响最终战: - -【军队数量】 -├─ 兵营等级 → 可调用的援军数量 -├─ 训练场等级 → 援军强度 -└─ 最终战可选调用援军 - -【装备供应】 -├─ 武器坊等级 → 队友装备品质上限 -├─ 可生产高级装备 -└─ 影响战斗难度 - -【资源储备】 -├─ 仓库等级 → 可携带道具数量 -├─ 金币储备 → 可购买的战时物资 -└─ 影响最终战准备 - -【士气系统】 -├─ 英雄雕像、神殿 → 全队士气 -├─ 士气影响战斗属性 -└─ 高士气 = 战斗加成 -``` - ---- - -## 五、王国建设系统(通关后) - -### 5.1 系统概述 - -``` -【解锁条件】 -通关正结局或真结局后解锁。 - -【核心玩法】 -从零开始建设繁荣王国。 -包含:城市规划、政策制定、 -外交系统、军队培养、继承系统。 - -【目标】 -建设一个强大的王国, -培养继承人, -体验"当上国王"后的生活。 -``` - -### 5.2 城市规划 - -``` -【区域划分】 -├─ 王城:核心区域 -├─ 商业区:经济中心 -├─ 住宅区:人口增长 -├─ 工业区:资源生产 -├─ 军事区:军队训练 -├─ 学院区:研究魔法 -└─ 特殊区:神殿、广场等 - -【建筑升级】 -├─ 每种建筑可升级 -├─ 升级需要资源和时间 -├─ 高级建筑提供更多功能 -└─ 建筑组合产生协同效果 - -【美观度系统】 -├─ 装饰建筑提升美观度 -├─ 高美观度 → 居民幸福度↑ -└─ 解锁特殊事件 -``` - -### 5.3 政策系统 - -``` -【政策类型】 - -经济政策: -├─ 税率调整(高税收=不满,低税收=发展慢) -├─ 贸易政策(开放/保护) -└─ 产业发展(农业/商业/工业优先) - -军事政策: -├─ 征兵制(数量)vs 志愿兵(质量) -├─ 军费投入 -└─ 防御优先 vs 进攻优先 - -社会政策: -├─ 教育投入 -├─ 福利政策 -└─ 文化发展 - -外交政策: -├─ 对各国态度 -├─ 联盟/敌对 -└─ 贸易协定 - -【政策效果】 -├─ 影响王国发展 -├─ 影响民众支持度 -├─ 触发特殊事件 -└─ 影响与其他国家关系 -``` - -### 5.4 外交系统 - -``` -【邻国势力】 -├─ 霜狼公国(北方) -├─ 银松侯国(东方) -├─ 铁脊领(西方) -├─ 金鳞商盟(南方) -└─ 其他新兴势力 - -【外交选项】 -├─ 建立邦交 -├─ 签订贸易协定 -├─ 缔结军事同盟 -├─ 发动战争 -├─ 联姻(继承系统相关) -└─ 间谍渗透 - -【关系等级】 -├─ 敌对 -├─ 冷淡 -├─ 中立 -├─ 友善 -└─ 盟友 -``` - -### 5.5 继承系统 - -``` -【继承人产生】 -├─ 与真结局伴侣结婚后可产生 -├─ 继承人随机继承父母特征 -├─ 可培养继承人的能力 -└─ 继承人成年后可继位 - -【继承培养】 -├─ 教育方向选择 -│ ├─ 武艺 -│ ├─ 魔法 -│ ├─ 政治 -│ └─ 商业 -├─ 导师安排(队友可担任) -├─ 实战历练 -└─ 性格塑造 - -【继位系统】 -├─ 主角年老后可传位 -├─ 继位后开启新角色游戏 -├─ 继承部分资产和关系 -├─ 新的剧情和事件 -└─ 多代传承 -``` - ---- - -## 六、多周目系统 - -### 6.1 New Game+ 要素 - -``` -【继承要素】 -├─ 角色等级(可选) -├─ 装备(部分) -├─ 金币(50%) -├─ 图鉴收集 -└─ 解锁内容 - -【不继承要素】 -├─ 剧情进度 -├─ 好感度 -├─ 领地建设 -└─ 关键道具 - -【新增内容】 -├─ 新剧情分支 -├─ 新敌人配置 -├─ 新结局 -├─ 隐藏队友 -└─ 高难度挑战 -``` - -### 6.2 周目特有内容 - -``` -【二周目】 -├─ 解锁隐藏队友 -├─ 新的对话选项 -├─ 敌人强化 20% -├─ 可跳过已看过的剧情 -└─ 新成就 - -【三周目+】 -├─ 自定义难度 -├─ 新的游戏模式 -│ ├─ 无尽模式 -│ ├─ 铁人模式(无法存档) -│ └─ 速通模式 -├─ 敌人强化递增 -└─ 稀有掉落率提升 -``` - ---- - -## 七、成就系统 - -### 7.1 成就分类 - -``` -【剧情成就】 -├─ 完成第X篇章 -├─ 达成X结局 -├─ 收集X个碎片 -└─ 观看全部CG - -【战斗成就】 -├─ 累计击杀X敌人 -├─ 无伤通关X关卡 -├─ S级评价X关卡 -├─ 使用连携技X次 -└─ 击败隐藏BOSS - -【角色成就】 -├─ 任意角色好感度达到Lv.X -├─ 全角色好感度达到Lv.5 -├─ 完成全角色支线 -└─ 达成全角色Lv.6(需多周目) - -【收集成就】 -├─ 收集X件装备 -├─ 收集X种道具 -├─ 发现X个隐藏地点 -└─ 完成图鉴X% - -【特殊成就】 -├─ 在X天内通关 -├─ 只使用初始装备通关 -├─ 全员存活通关 -└─ 发现彩蛋 -``` - ---- - -*下一章:[美术资产清单](07-asset-list.md)* diff --git a/.claude/skills/indie-game-dev/proposals/srpg-crown-oath/07-asset-list.md b/.claude/skills/indie-game-dev/proposals/srpg-crown-oath/07-asset-list.md deleted file mode 100644 index bb179f9095..0000000000 --- a/.claude/skills/indie-game-dev/proposals/srpg-crown-oath/07-asset-list.md +++ /dev/null @@ -1,751 +0,0 @@ -# 美术资产清单 - ---- - -## 一、资产总览 - -### 1.1 数量统计 - -| 类别 | 子类 | 数量 | 优先级 | -|------|------|------|--------| -| **角色立绘** | 主角 | 15+ | P0 | -| | 队友×6 | 60+ | P0 | -| | 重要NPC | 20+ | P1 | -| | 普通NPC | 30+ | P2 | -| **像素精灵** | 主角 | 50帧+ | P0 | -| | 队友×6 | 200帧+ | P0 | -| | 敌人 | 150帧+ | P1 | -| | BOSS | 100帧+ | P1 | -| **CG插画** | 剧情CG | 20+ | P0 | -| | 好感度CG | 12+ | P1 | -| | 结局CG | 8+ | P0 | -| **场景地图** | 战斗地图 | 20+ | P0 | -| | 城镇场景 | 10+ | P1 | -| | 剧情背景 | 15+ | P1 | -| **UI** | 界面 | 全套 | P0 | -| | 图标 | 200+ | P0 | -| | 特效 | 50+ | P1 | -| **音乐音效** | BGM | 25+ | P0 | -| | 音效 | 100+ | P0 | -| | 语音 | 可选 | P2 | - ---- - -## 二、角色立绘需求 - -### 2.1 主角立绘 - -``` -【基础立绘】 -├─ 男性版本 × 1套 -│ ├─ 标准(战斗) -│ ├─ 日常(对话) -│ ├─ 王者(后期) -│ └─ 结局(特殊) -├─ 女性版本 × 1套(可选) -│ └─ 同上 -└─ 差分表情 × 8种 - ├─ 普通 - ├─ 微笑 - ├─ 悲伤 - ├─ 愤怒 - ├─ 惊讶 - ├─ 坚定 - ├─ 痛苦 - └─ 思考 - -【服装变体】 -├─ 初始服装(平民) -├─ 冒险者服装(中期) -├─ 王者铠甲(后期) -└─ 结局服装 - -【头像】 -├─ 小头像 × 8表情 -└─ 战斗头像 × 4状态 -``` - -### 2.2 队友立绘(×6人) - -``` -每人需要: - -【全身立绘】 -├─ 标准立绘 × 1 -├─ 战斗立绘 × 1 -├─ 好感立绘 × 1 -├─ 悲伤立绘 × 1 -└─ 结局立绘 × 1 - -【表情差分】 -├─ 普通 -├─ 微笑 -├─ 大笑 -├─ 悲伤 -├─ 愤怒 -├─ 惊讶 -├─ 害羞 -├─ 思考 -├─ 战斗 -└─ 特殊(角色独有) - -【头像】 -├─ 对话头像 × 8-10表情 -├─ 战斗头像 × 4状态 -│ ├─ 正常 -│ ├─ 受伤 -│ ├─ 强化 -│ └─ 异常 -└─ 菜单头像 × 1 - -【专属CG】 -├─ 好感度CG × 1 -└─ 结局CG × 1(真结局用) -``` - -### 2.3 重要NPC立绘 - -``` -【核心NPC】(10-15人) -├─ 伊莎贝拉公主 -├─ 罗兰公爵 -├─ 大主教赛勒斯 -├─ 深渊之主(人形态) -├─ 各势力首领 × 5 -└─ 关键配角 × 5 - -每人需要: -├─ 全身立绘 × 2-3 -├─ 表情差分 × 5-8 -└─ 对话头像 × 5 - -【普通NPC】(20-30人) -├─ 村民 × 5 -├─ 士兵 × 5 -├─ 商人 × 3 -├─ 学者 × 3 -├─ 佣兵 × 5 -└─ 其他 × 5 - -每人需要: -├─ 全身立绘 × 1 -├─ 表情差分 × 3 -└─ 对话头像 × 1-3 -``` - ---- - -## 三、像素精灵需求 - -### 3.1 主角像素 - -``` -【基础规格】 -├─ 分辨率:48×72 px -├─ 帧率:12 fps -└─ 风格:HD-2D - -【动画清单】 -├─ 待机动画:4帧 -├─ 行走动画:6帧(4方向) -├─ 奔跑动画:6帧 -├─ 攻击动画:8帧 -│ ├─ 普通攻击 -│ ├─ 技能1 -│ ├─ 技能2 -│ └─ 技能3 -├─ 受击动画:2帧 -├─ 防御动画:2帧 -├─ 死亡动画:6帧 -├─ 胜利动画:6帧 -└─ 特殊动画:4帧 -``` - -### 3.2 队友像素(×6人) - -``` -每人需要: - -【基础动画】 -├─ 待机动画:4帧 -├─ 行走动画:6帧 -├─ 受击动画:2帧 -├─ 死亡动画:4帧 -└─ 胜利动画:4帧 - -【职业动画】 -骑士(凯恩): -├─ 普通攻击:6帧 -├─ 盾击:8帧 -├─ 嘲讽:6帧 -├─ 铁壁:4帧 -└─ 终极技能:12帧 - -法师(莉娜): -├─ 普通攻击(火球):6帧 -├─ 冰冻术:8帧 -├─ 雷击:8帧 -├─ 陨石术:12帧 -└─ 施法姿势:4帧 - -盗贼(薇拉): -├─ 普通攻击:6帧 -├─ 潜行:4帧 -├─ 毒刃:6帧 -├─ 疾步:4帧 -└─ 终极技能:10帧 - -枪斗士(罗兰): -├─ 普通攻击:6帧 -├─ 突刺:8帧 -├─ 横扫:8帧 -├─ 击退:6帧 -└─ 终极技能:12帧 - -牧师(艾琳): -├─ 普通攻击:4帧 -├─ 治愈术:8帧 -├─ 群体治愈:10帧 -├─ 复活:10帧 -└─ 施法姿势:4帧 - -暗法师(塞拉斯): -├─ 普通攻击:6帧 -├─ 暗影箭:6帧 -├─ 诅咒:8帧 -├─ 生命汲取:8帧 -└─ 终极技能:12帧 -``` - -### 3.3 敌人像素 - -``` -【普通敌人】(约30种) - -深渊眷族类: -├─ 深渊蠕虫 -├─ 深渊蝙蝠 -├─ 深渊狼 -├─ 深渊骷髅兵 -├─ 深渊哥布林 -└─ ...各约4-6帧动画 - -人类敌人类: -├─ 强盗 -├─ 叛兵 -├─ 黑法师 -├─ 刺客 -└─ ...各约4-6帧动画 - -动物类: -├─ 野狼 -├─ 巨熊 -├─ 毒蛇 -└─ ...各约4-6帧动画 - -【精英敌人】(约15种) -├─ 深渊精英系列 -├─ 人类精英系列 -└─ 特殊敌人 -各约6-8帧动画 - -【BOSS】(约10个) -├─ 深渊骑士(章节BOSS) -├─ 深渊领主(章节BOSS) -├─ 叛徒大主教 -├─ 深渊之主(最终BOSS) -│ ├─ 第一形态 -│ ├─ 第二形态 -│ └─ 第三形态 -└─ 隐藏BOSS -各约12-20帧动画 -``` - ---- - -## 四、CG插画需求 - -### 4.1 剧情CG - -``` -【第1篇章】(5张) -├─ CG01:主角苏醒 -├─ CG02:首次战斗 -├─ CG03:发现第一个碎片 -├─ CG04:银松城保卫战 -└─ CG05:城主战死 - -【第2篇章】(5张) -├─ CG06:面见罗兰公爵 -├─ CG07:身世揭露 -├─ CG08:被诬陷 -├─ CG09:逃亡之路 -└─ CG10:真相大白 - -【第3篇章】(5张) -├─ CG11:记忆恢复 -├─ CG12:王都攻略战 -├─ CG13:塞拉斯的抉择 -├─ CG14:收复王都 -└─ CG15:深渊之主现身 - -【第4篇章】(5张) -├─ CG16:重铸王冠 -├─ CG17:深渊全面入侵 -├─ CG18:最终决战 -├─ CG19:胜利时刻 -└─ CG20:加冕仪式 -``` - -### 4.2 好感度CG - -``` -每位队友 × 1张 - -【莉娜】师父的遗物 -【凯恩】骑士团复兴 -【薇拉】新的归宿 -【罗兰】效忠誓言 -【艾琳】神圣觉醒 -【塞拉斯】获得救赎 -``` - -### 4.3 结局CG - -``` -【真结局】(3张) -├─ 净化深渊之主 -├─ 婚礼 -└─ 共同加冕 - -【正结局】(2张) -├─ 封印深渊 -└─ 独自加冕 - -【牺牲结局】(2张) -├─ 主角牺牲 -└─ 队友继承 - -【暗黑结局】(1张) -└─ 主角堕落 -``` - ---- - -## 五、场景地图需求 - -### 5.1 战斗地图 - -``` -【第1篇章地图】(5张) -├─ 晨曦村周边 -├─ 银松城外 -├─ 废弃矿洞 -├─ 法师塔周边 -└─ 银松城保卫战 - -【第2篇章地图】(5张) -├─ 北境森林 -├─ 霜狼公国 -├─ 铁脊矿山 -├─ 教会圣地 -└─ 逃亡之路 - -【第3篇章地图】(5张) -├─ 地下通道 -├─ 王都外围 -├─ 王都街道 -├─ 王宫内部 -└─ 深渊裂隙入口 - -【第4篇章地图】(5张) -├─ 誓约神殿 -├─ 深渊裂隙 -├─ 深渊内部 -├─ 深渊核心 -└─ 最终战场 - -【特殊地图】 -├─ 训练场 -├─ 竞技场 -├─ 无尽之塔 -└─ 隐藏关卡 -``` - -### 5.2 地图元素 - -``` -【地形瓦片】(每种 × 4变体) -├─ 平原 -├─ 森林 -├─ 山地 -├─ 水域 -├─ 道路 -├─ 城墙 -├─ 建筑废墟 -└─ 深渊侵蚀地 - -【装饰物】 -├─ 树木 × 5种 -├─ 岩石 × 5种 -├─ 草丛 × 3种 -├─ 建筑残骸 × 5种 -├─ 特殊物件 × 10种 -└─ 光影效果 -``` - -### 5.3 城镇场景 - -``` -【主要城镇】(10张背景) -├─ 晨曦村 -├─ 银松城 -├─ 霜狼公国城 -├─ 王都(废墟) -├─ 王都(重建) -├─ 法师塔 -├─ 教会圣堂 -├─ 商盟都市 -├─ 酒馆 -└─ 誓约神殿 -``` - ---- - -## 六、UI设计需求 - -### 6.1 主要界面 - -``` -【系统界面】 -├─ 标题画面 -├─ 主菜单 -├─ 设置界面 -├─ 存档界面 -├─ 读取界面 -└─ 暂停菜单 - -【游戏界面】 -├─ 大地图界面 -├─ 战斗HUD -├─ 角色状态界面 -├─ 装备界面 -├─ 技能界面 -├─ 物品界面 -├─ 队伍编成界面 -├─ 好感度界面 -├─ 商店界面 -└─ 对话界面 - -【特殊界面】 -├─ 天数推进界面 -├─ 深渊侵蚀显示 -├─ 领地管理界面 -├─ 王国建设界面 -└─ 成就界面 -``` - -### 6.2 UI组件 - -``` -【基础组件】 -├─ 按钮 × 10种状态 -├─ 输入框 -├─ 滑动条 -├─ 复选框 -├─ 下拉菜单 -├─ 标签页 -├─ 滚动条 -└─ 弹窗框架 - -【信息显示】 -├─ 生命条 -├─ 魔法条 -├─ 经验条 -├─ 好感度条 -├─ 数值面板 -├─ 提示框 -└─ 伤害数字 - -【装饰元素】 -├─ 边框 × 10种 -├─ 分隔线 -├─ 背景 × 20种 -├─ 装饰花纹 -└─ 动态效果 -``` - -### 6.3 图标 - -``` -【系统图标】(约50个) -├─ 菜单图标 -├─ 设置图标 -├─ 社交图标 -└─ 功能图标 - -【物品图标】(约100个) -├─ 武器 × 20 -├─ 防具 × 20 -├─ 饰品 × 15 -├─ 消耗品 × 25 -├─ 材料 × 20 -└─ 特殊物品 × 10 - -【技能图标】(约60个) -├─ 骑士技能 × 10 -├─ 法师技能 × 10 -├─ 盗贼技能 × 10 -├─ 枪斗士技能 × 10 -├─ 牧师技能 × 10 -├─ 暗法师技能 × 10 -└─ 通用技能 × 10 - -【状态图标】(约30个) -├─ Buff × 10 -├─ Debuff × 15 -└─ 特殊状态 × 5 - -【地形/天气图标】(约15个) -├─ 地形类型 -└─ 天气状态 -``` - ---- - -## 七、特效需求 - -### 7.1 战斗特效 - -``` -【攻击特效】 -├─ 物理斩击 -├─ 火焰 -├─ 冰霜 -├─ 雷电 -├─ 暗影 -├─ 神圣 -└─ 物理/穿刺 - -【技能特效】(每职业×5-10个) -├─ 骑士特效 -├─ 法师特效 -├─ 盗贼特效 -├─ 枪斗士特效 -├─ 牧师特效 -├─ 暗法师特效 -└─ 连携技特效 - -【状态特效】 -├─ Buff效果 -├─ Debuff效果 -├─ 治愈效果 -└─ 复活效果 - -【环境特效】 -├─ 天气效果 -├─ 地形效果 -├─ 深渊效果 -└─ 誓约效果 -``` - -### 7.2 UI特效 - -``` -【交互特效】 -├─ 按钮点击 -├─ 页面切换 -├─ 弹窗出现 -├─ 物品获得 -└─ 成就解锁 - -【反馈特效】 -├─ 伤害数字 -├─ 治疗数字 -├─ 暴击特效 -├─ 闪避特效 -└─ 状态变化 -``` - ---- - -## 八、音频需求 - -### 8.1 背景音乐 - -``` -【系统音乐】 -├─ 标题曲 -├─ 主菜单 -├─ 结尾曲 -└─ 制作人员名单 - -【场景音乐】(约15首) -├─ 晨曦村 -├─ 银松城 -├─ 霜狼公国 -├─ 王都废墟 -├─ 法师塔 -├─ 教会 -├─ 商盟 -├─ 深渊区域 -├─ 誓约神殿 -├─ 酒馆 -├─ 露营 -├─ 战斗(普通) -├─ 战斗(BOSS) -├─ 最终战 -└─ 胜利 - -【情感音乐】(约5首) -├─ 悲伤 -├─ 紧张 -├─ 温馨 -├─ 激昂 -└─ 感动 -``` - -### 8.2 音效 - -``` -【战斗音效】(约40个) -├─ 武器挥动 × 5 -├─ 命中音效 × 5 -├─ 技能音效 × 20 -├─ 脚步声 × 3 -├─ 受击音效 × 3 -└─ 死亡音效 × 4 - -【UI音效】(约30个) -├─ 按钮点击 -├─ 界面切换 -├─ 物品获得 -├─ 装备穿戴 -├─ 升级 -└─ 错误提示 - -【环境音效】(约20个) -├─ 自然环境 × 10 -├─ 城镇环境 × 5 -└─ 深渊环境 × 5 - -【角色音效】(约20个) -├─ 主角语音 -├─ 队友语音 -└─ NPC语音 -``` - ---- - -## 九、美术风格指南 - -### 9.1 色彩规范 - -``` -【主色板】 -├─ 主色:#4A90D9(柔蓝) -├─ 辅色:#E8A87C(暖橙) -├─ 强调:#C38D9E(柔粉) -├─ 深色:#2C3E50(深蓝灰) -├─ 浅色:#F5F1E8(米白) -└─ 深渊:#4A1942(暗紫) - -【角色专用色】 -├─ 主角:金色 + 深蓝 -├─ 莉娜:橙色 + 金色 -├─ 凯恩:银白 + 深蓝 -├─ 薇拉:紫色 + 银灰 -├─ 罗兰:冰蓝 + 白色 -├─ 艾琳:粉色 + 白色 -└─ 塞拉斯:暗紫 + 黑色 -``` - -### 9.2 像素规范 - -``` -【基础规格】 -├─ 角色精灵:48×72 px -├─ 地图瓦片:32×32 px -├─ 图标:32×32 px 或 64×64 px -└─ 特效:根据需要 - -【动画帧率】 -├─ 待机:4帧 @ 6fps -├─ 行走:6帧 @ 10fps -├─ 攻击:8帧 @ 12fps -├─ 技能:10-15帧 @ 12fps -└─ 特效:根据需要 - -【颜色限制】 -├─ 单个精灵:最多 32 色 -├─ 单个地图瓦片:最多 16 色 -└─ 保持色彩统一性 -``` - -### 9.3 立绘规范 - -``` -【分辨率】 -├─ 全身立绘:1500-2000px 高 -├─ 头像:200×200 px -├─ 战斗头像:100×100 px -└─ 格式:PNG(透明背景) - -【风格要求】 -├─ 日系厚涂风格 -├─ 线条柔和 -├─ 光影细腻 -├─ 色彩饱和度适中 -└─ 与像素风格协调 -``` - ---- - -## 十、资产优先级 - -### 10.1 P0(必须 - MVP) - -``` -├─ 主角立绘(基础) -├─ 主角像素精灵(基础动画) -├─ 3名队友立绘 -├─ 3名队友像素精灵 -├─ 10种普通敌人像素 -├─ 3个BOSS像素 -├─ 5张战斗地图 -├─ 全套UI界面 -├─ 100个基础图标 -├─ 10首基础BGM -└─ 50个基础音效 -``` - -### 10.2 P1(重要 - 完整版) - -``` -├─ 全部队友立绘和像素 -├─ 全部敌人像素 -├─ 全部BOSS像素 -├─ 全部战斗地图 -├─ 全部剧情CG -├─ 全部好感度CG -├─ 重要NPC立绘 -├─ 城镇背景 -├─ 全部特效 -└─ 全部音频 -``` - -### 10.3 P2(可选 - 扩展版) - -``` -├─ 普通NPC立绘 -├─ 额外表情差分 -├─ 额外动画 -├─ 隐藏角色 -├─ DLC内容 -├─ 高级特效 -└─ 语音(如果需要) -``` - ---- - -*文档完成!返回 [README](README.md)* diff --git a/.claude/skills/indie-game-dev/proposals/srpg-crown-oath/README.md b/.claude/skills/indie-game-dev/proposals/srpg-crown-oath/README.md deleted file mode 100644 index 3a79a2457b..0000000000 --- a/.claude/skills/indie-game-dev/proposals/srpg-crown-oath/README.md +++ /dev/null @@ -1,101 +0,0 @@ -# 《誓约王冠》游戏设计文档 - -> SRPG 策略战棋 · 奇幻冒险 · 王国建设 - ---- - -## 文档结构 - -``` -srpg-crown-oath/ -├── README.md # 本文件 - 概览 -├── 01-game-overview.md # 游戏概览与核心卖点 -├── 02-gameplay.md # 游戏玩法系统 -├── 03-world-setting.md # 世界观设定(可替换) -├── 04-story-outline.md # 剧情大纲 -├── 05-characters.md # 角色设计 -├── 06-systems.md # 系统设计(好感度、建设等) -└── 07-asset-list.md # 美术资产清单 -``` - -## 快速导航 - -| 文档 | 内容 | 状态 | -|------|------|------| -| [游戏概览](01-game-overview.md) | 核心概念、卖点、目标用户 | ✅ 完成 | -| [游戏玩法](02-gameplay.md) | 战斗系统、职业、关卡 | ✅ 完成 | -| [世界观设定](03-world-setting.md) | 大陆、历史、势力 | ✅ 可替换 | -| [剧情大纲](04-story-outline.md) | 主线流程、章节划分 | ✅ 完成 | -| [角色设计](05-characters.md) | 主角、队友、NPC | ✅ 完成 | -| [系统设计](06-systems.md) | 好感度、建设、多周目 | ✅ 完成 | - -## 一句话描述 - -**在浊潮即将吞噬世界的90天内,拥有净灵之力的青年集结伙伴、净化浊力、封印邪神,从流民成长为建立和平王国的新王。** - -## 核心卖点 - -1. **经典SRPG体验** - 战棋策略 + 剧情驱动 -2. **时间紧迫感** - 90天浊潮倒计时,决策影响结局 -3. **深度羁绊系统** - 6位队友,多结局分支 -4. **三大阵营对立** - 黑石王国、浊光教派、流民村落 -5. **净灵之力系统** - 净化浊力、安抚魔物、重塑世界 -6. **双阶段玩法** - 冒险拯救世界 → 王国建设经营 - -## 目标平台 - -- PC (Steam) -- Nintendo Switch -- 移动端(可选) - -## 目标用户 - -- 策略战棋爱好者 -- 日系RPG粉丝 -- 喜欢剧情驱动游戏的玩家 -- 18-35岁,有一定消费能力 - -## 美术风格 - -| 类型 | 风格 | -|------|------| -| 战斗场景 | HD-2D 像素风 | -| 角色立绘 | 2D 日系厚涂 | -| CG插画 | 日系动漫风 | -| UI界面 | 日系二次元 | - -## 参考游戏 - -- 铃兰之剑(美术风格、战棋玩法) -- 火焰纹章(职业系统、好感度) -- 三角策略(剧情抉择) -- 歧路旅人(HD-2D像素) - ---- - -## 如何替换背景故事 - -`03-world-setting.md` 采用模块化设计,您可以: - -1. **保留框架,替换内容** - - 保持"大陆 + 危机 + 预言"的基本结构 - - 替换具体的地名、势力名、历史背景 - -2. **完全重写** - - 修改 `03-world-setting.md` 的全部内容 - - 同步更新 `04-story-outline.md` 中的相关剧情 - - 调整 `05-characters.md` 中角色的背景 - -3. **主题变体建议** - - | 当前主题 | 可替换为 | - |---------|---------| - | 奇幻大陆 | 赛博朋克都市 | - | 深渊侵蚀 | AI叛乱/病毒爆发 | - | 誓约王冠 | 核心科技/远古遗物 | - | 七大王侯 | 五大财团/六大门派 | - ---- - -*文档版本: v1.0* -*最后更新: 2026-02-20* diff --git a/.claude/skills/indie-game-dev/references/balance-system.md b/.claude/skills/indie-game-dev/references/balance-system.md deleted file mode 100644 index 2df6032130..0000000000 --- a/.claude/skills/indie-game-dev/references/balance-system.md +++ /dev/null @@ -1,238 +0,0 @@ -# 游戏数值平衡系统 - -## 目录 -- [核心概念](#核心概念) -- [属性系统设计](#属性系统设计) -- [公式体系](#公式体系) -- [经济系统](#经济系统) -- [成长曲线](#成长曲线) -- [概率设计](#概率设计) -- [平衡调优](#平衡调优) - -## 核心概念 - -### 数值设计三原则 -1. **可预期性** - 玩家能理解投入与回报的关系 -2. **可控性** - 设计者能精确调整数值影响 -3. **可扩展性** - 系统能容纳后期内容增加 - -### 数值层级结构 -``` -基础数值 (Base Stats) - ↓ -修正数值 (Modifiers) - ↓ -计算数值 (Calculated Stats) - ↓ -最终数值 (Final Stats) -``` - -## 属性系统设计 - -### 基础属性模板 - -```json -{ - "attributes": { - "primary": { - "strength": {"base": 10, "growth": 2.5, "description": "影响物理攻击"}, - "agility": {"base": 10, "growth": 2.0, "description": "影响攻击速度和闪避"}, - "intelligence": {"base": 10, "growth": 3.0, "description": "影响魔法攻击和法力值"}, - "vitality": {"base": 10, "growth": 2.0, "description": "影响生命值和防御"} - }, - "secondary": { - "max_hp": "vitality * 10 + base_hp", - "max_mp": "intelligence * 8 + base_mp", - "physical_atk": "strength * 2 + weapon_atk", - "magical_atk": "intelligence * 2.5 + weapon_matk", - "physical_def": "vitality * 1.5 + armor_def", - "magical_def": "intelligence * 1.0 + armor_mdef" - } - } -} -``` - -### 属性类型分类 - -| 类型 | 说明 | 示例 | -|------|------|------| -| 基础属性 | 角色固有能力 | 力量/敏捷/智力 | -| 衍生属性 | 由基础属性计算 | 攻击力/防御力 | -| 战斗属性 | 战斗中生效 | 暴击率/命中率 | -| 资源属性 | 消耗型资源 | 法力/怒气/能量 | - -## 公式体系 - -### 伤害计算公式 - -#### 基础伤害公式 -``` -基础伤害 = 攻击力 * 技能倍率 * 随机波动(0.9~1.1) -``` - -#### 防御减免公式 -``` -减免比例 = 防御力 / (防御力 + 防御系数 * 攻击者等级) -实际伤害 = 基础伤害 * (1 - 减免比例) -``` - -#### 暴击公式 -``` -暴击判定 = random() < 暴击率 -暴击伤害 = 最终伤害 * 暴击倍率 -``` - -### 经验值公式 - -#### 等级经验曲线 -``` -升级所需经验 = 基础经验 * (等级 ^ 经验系数) -``` - -常用经验系数: -- 快速成长: 1.5 -- 标准成长: 2.0 -- 慢速成长: 2.5 - -#### 经验值分配 -``` -击杀经验 = 怪物基础经验 * (1 + 等级差修正) -等级差修正 = clamp((怪物等级 - 玩家等级) * 0.1, -0.5, 0.5) -``` - -## 经济系统 - -### 货币体系设计 - -```json -{ - "currencies": { - "gold": { - "name": "金币", - "max": 999999999, - "primary": true, - "sources": ["怪物掉落", "任务奖励", "物品出售"] - }, - "gems": { - "name": "宝石", - "max": 99999, - "premium": true, - "sources": ["充值", "成就", "活动"] - } - } -} -``` - -### 通货膨胀控制 - -| 手段 | 实现方式 | -|------|----------| -| 消耗 Sink | 强化消耗/修理费/税收 | -| 价值锚定 | NPC售价固定 | -| 动态调整 | 根据服务器经济调整掉落 | -| 分层货币 | 低/中/高级货币分离 | - -### 定价模型 - -``` -物品价值 = 基础价值 * 稀有度系数 * 需求系数 -``` - -## 成长曲线 - -### 标准成长模型 - -```json -{ - "growth_curve": { - "early_game": { - "levels": "1-20", - "growth_rate": "fast", - "unlock_frequency": "high", - "description": "快速反馈期" - }, - "mid_game": { - "levels": "21-50", - "growth_rate": "moderate", - "unlock_frequency": "medium", - "description": "核心内容期" - }, - "end_game": { - "levels": "51+", - "growth_rate": "slow", - "unlock_frequency": "low", - "description": "深度培养期" - } - } -} -``` - -### 数值增长类型 - -``` -线性: y = a * x + b -对数: y = a * log(x) + b -指数: y = a * (x ^ b) -S曲线: y = k / (1 + e^(-a*(x-b))) -``` - -## 概率设计 - -### 随机系统类型 - -| 类型 | 公式 | 适用场景 | -|------|------|----------| -| 真随机 | random() < p | 非关键概率 | -| 伪随机 | 计数器修正 | 关键概率 | -| 保底机制 | 必中计数 | 抽卡/稀有掉落 | -| 加权随机 | 权重池 | 战利品表 | - -### 伪随机分布 (PRD) - -``` -C = 1 - (1 - P) ^ N -实际概率 = 基础概率 + (失败次数 * 修正系数) -``` - -### 保底系统 - -```json -{ - "pity_system": { - "soft_pity": { - "trigger": 70, - "increase_rate": 0.05 - }, - "hard_pity": { - "trigger": 90, - "guaranteed": true - } - } -} -``` - -## 平衡调优 - -### 调优流程 - -``` -1. 数据收集 → 2. 问题识别 → 3. 假设验证 → 4. 方案实施 → 5. 效果监控 -``` - -### 常用调优手段 - -| 问题类型 | 调优方向 | -|----------|----------| -| 过强内容 | 增加成本/降低效果/增加限制 | -| 过弱内容 | 降低成本/提升效果/减少限制 | -| 单一最优解 | 加强替代方案/增加场景限制 | -| 进度过快 | 增加消耗/提高门槛/减少产出 | - -### 数值测试清单 - -- [ ] 新手流程数值验证 -- [ ] 满级角色能力上限测试 -- [ ] 资源产出消耗平衡 -- [ ] 付费/免费玩家差距 -- [ ] 多人竞技公平性 -- [ ] 边界值测试 diff --git a/.claude/skills/indie-game-dev/references/level-design.md b/.claude/skills/indie-game-dev/references/level-design.md deleted file mode 100644 index 0475f491d8..0000000000 --- a/.claude/skills/indie-game-dev/references/level-design.md +++ /dev/null @@ -1,306 +0,0 @@ -# 游戏关卡设计 - -## 目录 -- [关卡设计原则](#关卡设计原则) -- [关卡结构规划](#关卡结构规划) -- [节奏与流程](#节奏与流程) -- [教学关卡设计](#教学关卡设计) -- [挑战设计](#挑战设计) -- [关卡类型模板](#关卡类型模板) - -## 关卡设计原则 - -### 核心设计理念 -1. **引导而非指示** - 通过视觉语言引导玩家 -2. **风险与回报** - 难度与奖励成正比 -3. **心流体验** - 难度匹配玩家能力 -4. **涌现玩法** - 鼓励创造性解决方案 - -### 关卡设计五要素 -``` -空间 (Space) - 物理环境与布局 -规则 (Rules) - 关卡内特殊机制 -目标 (Goals) - 玩家需要完成的任务 -障碍 (Obstacles) - 阻碍玩家前进的元素 -资源 (Resources) - 玩家可利用的道具/能力 -``` - -## 关卡结构规划 - -### 标准关卡结构 - -``` -┌─────────────────────────────────────────┐ -│ 入口区域 (安全区) │ -│ - 存档点/检查点 │ -│ - 资源补给 │ -│ - 信息提示 │ -├─────────────────────────────────────────┤ -│ 探索阶段 (低压区) │ -│ - 环境叙事 │ -│ - 基础解谜 │ -│ - 收集要素 │ -├─────────────────────────────────────────┤ -│ 挑战阶段 (高压区) │ -│ - 战斗/平台挑战 │ -│ - 进阶解谜 │ -│ - 可选支路 │ -├─────────────────────────────────────────┤ -│ 高潮阶段 (Boss/关键点) │ -│ - 主要挑战 │ -│ - 节奏高潮 │ -│ - 成就时刻 │ -├─────────────────────────────────────────┤ -│ 出口区域 (奖励区) │ -│ - 战利品 │ -│ - 剧情推进 │ -│ - 下一关预览 │ -└─────────────────────────────────────────┘ -``` - -### 关卡节点规划 - -```json -{ - "level_structure": { - "nodes": [ - {"type": "spawn", "safe": true}, - {"type": "encounter", "difficulty": 0.3, "enemies": 3}, - {"type": "puzzle", "complexity": "simple"}, - {"type": "encounter", "difficulty": 0.5, "enemies": 5}, - {"type": "checkpoint", "safe": true}, - {"type": "mini_boss", "difficulty": 0.7}, - {"type": "encounter", "difficulty": 0.8, "enemies": 8}, - {"type": "boss", "difficulty": 1.0}, - {"type": "exit", "rewards": ["gold", "item"]} - ] - } -} -``` - -## 节奏与流程 - -### 节奏曲线设计 - -``` -强度 - ↑ - │ ▲ ▲ - │ ╱ ╲ ▲ ╱ ╲ - │ ╱ ╲ ╱ ╲ ╱ ╲ - │ ╱ ╲╱ ╲╱ ╲ - │╱ ╲ - └──────────────────────→ 时间 - 入口 探索 挑战 高潮 结算 -``` - -### 节奏类型 - -| 节奏类型 | 描述 | 适用场景 | -|----------|------|----------| -| 渐进式 | 难度逐步上升 | 教学关卡 | -| 波浪式 | 张弛交替 | 标准关卡 | -| 爆发式 | 突然高强度 | Boss战 | -| 递归式 | 重复+变化 | 无尽模式 | - -### 时间规划参考 - -```json -{ - "level_duration": { - "mobile": {"min": 2, "max": 5, "optimal": 3}, - "pc": {"min": 5, "max": 20, "optimal": 10}, - "boss_fight": {"min": 3, "max": 10, "optimal": 5} - } -} -``` - -## 教学关卡设计 - -### 教学设计原则 (I DO - WE DO - YOU DO) - -``` -1. 展示 (I DO) - - 玩家观察NPC/提示完成动作 - - 不需要玩家操作 - -2. 引导 (WE DO) - - 玩家在引导下完成动作 - - 提供视觉提示和安全网 - -3. 独立 (YOU DO) - - 玩家独立完成动作 - - 验证学习成果 -``` - -### 教学流程模板 - -```json -{ - "tutorial_sequence": [ - { - "mechanic": "移动", - "steps": ["展示方向键", "引导移动到目标点", "自由移动练习"] - }, - { - "mechanic": "跳跃", - "steps": ["展示跳跃动画", "引导跳过障碍", "平台跳跃挑战"] - }, - { - "mechanic": "攻击", - "steps": ["展示攻击效果", "引导攻击木桩", "战斗练习"] - } - ] -} -``` - -### 教学提示设计 - -``` -好的提示: -✓ "按空格键跳跃" - 明确指示 -✓ 视觉标记目标位置 -✓ 失败后渐进提示 - -不好的提示: -✗ "使用跳跃技能" - 按键不明 -✗ 过多文字说明 -✗ 惩罚性失败 -``` - -## 挑战设计 - -### 难度设计维度 - -| 维度 | 调整方式 | -|------|----------| -| 认知负荷 | 信息量/复杂度 | -| 操作精度 | 时机窗口/输入要求 | -| 反应速度 | 敌人速度/攻击频率 | -| 资源压力 | 弹药/血量/时间限制 | -| 空间导航 | 路径复杂度/迷路风险 | - -### 敌人配置设计 - -```json -{ - "encounter_design": { - "enemy_roles": { - "fodder": {"hp": "low", "damage": "low", "purpose": "满足感"}, - "tank": {"hp": "high", "damage": "low", "purpose": "时间压力"}, - "damage_dealer": {"hp": "medium", "damage": "high", "purpose": "威胁"}, - "support": {"hp": "low", "damage": "none", "purpose": "强化其他"} - }, - "composition_rules": [ - "核心敌人 + 1-2种支援", - "总敌人数 = 3-8为宜", - "避免同时出现过多远程敌人" - ] - } -} -``` - -### Boss设计框架 - -```json -{ - "boss_design": { - "phases": [ - { - "hp_threshold": "100%-70%", - "pattern": "基础攻击", - "learning_goal": "熟悉基础模式" - }, - { - "hp_threshold": "70%-30%", - "pattern": "新增技能", - "learning_goal": "应对变化" - }, - { - "hp_threshold": "30%-0%", - "pattern": "狂暴模式", - "learning_goal": "极限反应" - } - ], - "design_rules": [ - "每个技能有明确前摇", - "提供安全输出窗口", - "避免秒杀机制", - "失败后可快速重试" - ] - } -} -``` - -## 关卡类型模板 - -### 动作游戏关卡 - -``` -结构: 线性 + 支线 -重点: 战斗节奏、敌人配置 -元素: 掩体、高地、移动路径 -``` - -### 平台跳跃关卡 - -``` -结构: 纵向/横向展开 -重点: 跳跃难度、移动技巧 -元素: 平台、障碍、收集品 -``` - -### 解谜关卡 - -``` -结构: 房间式/开放式 -重点: 逻辑难度、信息呈现 -元素: 机关、线索、钥匙 -``` - -### 潜行关卡 - -``` -结构: 开放式区域 -重点: 路线规划、AI巡逻 -元素: 掩体、分散注意道具、警报 -``` - -### Roguelike关卡 - -```json -{ - "roguelike_room": { - "types": ["combat", "elite", "shop", "event", "rest", "boss"], - "generation": { - "min_rooms": 10, - "max_rooms": 20, - "guaranteed": ["rest_before_boss", "shop_midway"] - }, - "connections": { - "branching": 0.3, - "merge_points": ["floor_exit"] - } - } -} -``` - -## 关卡设计检查清单 - -### 基础检查 -- [ ] 关卡目标明确 -- [ ] 入口/出口清晰 -- [ ] 检查点合理分布 -- [ ] 难度曲线平滑 - -### 体验检查 -- [ ] 视觉引导有效 -- [ ] 节奏张弛有度 -- [ ] 失败惩罚合理 -- [ ] 奖励感觉充实 - -### 技术检查 -- [ ] 边界处理完善 -- [ ] 无卡死可能 -- [ ] 性能优化到位 -- [ ] 存档/读档正常 diff --git a/.claude/skills/indie-game-dev/references/ui-ux-design.md b/.claude/skills/indie-game-dev/references/ui-ux-design.md deleted file mode 100644 index 8c92c312e7..0000000000 --- a/.claude/skills/indie-game-dev/references/ui-ux-design.md +++ /dev/null @@ -1,328 +0,0 @@ -# 游戏UI/UX设计 - -## 目录 -- [设计原则](#设计原则) -- [UI层次结构](#ui层次结构) -- [HUD设计](#hud设计) -- [菜单系统](#菜单系统) -- [反馈系统](#反馈系统) -- [新手引导](#新手引导) -- [可访问性设计](#可访问性设计) - -## 设计原则 - -### 游戏UI设计核心 -1. **不干扰游戏** - UI服务于游戏,不是主角 -2. **信息层次清晰** - 重要信息优先级最高 -3. **一致性** - 全局风格统一 -4. **响应性** - 即时反馈所有交互 - -### 信息优先级 -``` -P0: 生死相关 (血量/倒计时/警告) -P1: 核心玩法 (技能/弹药/目标) -P2: 辅助信息 (小地图/任务/分数) -P3: 次要信息 (设置/社交/收藏) -``` - -## UI层次结构 - -### 标准UI层级 -``` -Layer 5: 全屏覆盖层 (Loading/过场) - ↓ -Layer 4: 模态对话框 (确认/提示) - ↓ -Layer 3: 弹窗面板 (背包/技能树) - ↓ -Layer 2: HUD (血条/技能/小地图) - ↓ -Layer 1: 游戏世界 (3D/2D场景) -``` - -### 屏幕布局分区 - -``` -┌────────────────────────────────────────┐ -│ [左上: 玩家状态] [右上: 目标/任务] │ -│ │ -│ │ -│ [中心: 游戏区域] │ -│ │ -│ │ -│ [左下: 技能/物品栏] [右下: 小地图] │ -└────────────────────────────────────────┘ -``` - -## HUD设计 - -### 核心HUD元素 - -```json -{ - "hud_elements": { - "health_bar": { - "position": "bottom_left", - "visibility": "always", - "style": "gradient_fill", - "critical_threshold": 0.25, - "animations": ["damage_flash", "heal_pulse", "critical_pulse"] - }, - "mana_bar": { - "position": "below_health", - "visibility": "contextual", - "style": "solid_fill" - }, - "skill_slots": { - "position": "bottom_center", - "count": 4, - "show_cooldown": true, - "show_keybind": true - }, - "minimap": { - "position": "top_right", - "size": "medium", - "rotation": "player_facing", - "show_markers": ["objective", "npc", "danger"] - }, - "ammo_counter": { - "position": "bottom_right", - "visibility": "combat_only" - } - } -} -``` - -### 状态指示器设计 - -| 状态类型 | 视觉表现 | 位置建议 | -|----------|----------|----------| -| 生命值 | 条形/心形/数字 | 左上/底部 | -| 法力/能量 | 条形/球体 | 生命值下方 | -| Buff/Debuff | 图标列表+倒计时 | 状态栏 | -| 弹药 | 数字+图标 | 武器附近 | -| 经验值 | 条形 | 顶部/底部 | - -## 菜单系统 - -### 菜单结构设计 - -``` -主菜单 -├── 继续游戏 -├── 新游戏 -│ └── 选择难度 -├── 加载存档 -├── 设置 -│ ├── 画面设置 -│ ├── 音频设置 -│ ├── 控制设置 -│ └── 游戏设置 -└── 退出 -``` - -### 暂停菜单 - -```json -{ - "pause_menu": { - "layout": "centered", - "background": "blur_game_view", - "options": [ - {"label": "继续", "action": "resume", "default": true}, - {"label": "设置", "action": "open_settings"}, - {"label": "存档", "action": "save_game"}, - {"label": "返回主菜单", "action": "quit_to_menu", "confirm": true} - ] - } -} -``` - -### 背包UI设计 - -``` -┌──────────────────────────────────────┐ -│ [角色] [装备] [消耗品] [素材] │ -├──────────────────────────────────────┤ -│ ┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐ │ -│ │ ││ ││ ││ ││ ││ │ │ -│ └──┘└──┘└──┘└──┘└──┘└──┘ [详情] │ -│ ┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐ │ -│ │ ││ ││ ││ ││ ││ │ │ -│ └──┘└──┘└──┘└──┘└──┘└──┘ │ -├──────────────────────────────────────┤ -│ 物品名称: 剑 │ -│ 攻击力: +50 │ -│ 描述: 一把普通的剑 │ -└──────────────────────────────────────┘ -``` - -## 反馈系统 - -### 反馈类型矩阵 - -| 事件 | 视觉 | 音效 | 震动 | -|------|------|------|------| -| 受伤 | 红色闪烁 | 痛苦音效 | 轻微 | -| 死亡 | 灰度+慢动作 | 死亡音效 | 强烈 | -| 拾取 | 飘字+粒子 | 拾取音效 | 无 | -| 升级 | 光效+UI动画 | 升级音效 | 中等 | -| 任务完成 | 弹窗+奖励展示 | 完成音效 | 无 | -| 技能冷却 | 图标填充动画 | 冷却完毕提示 | 无 | - -### 伤害数字设计 - -```json -{ - "damage_numbers": { - "normal": { - "color": "#FFFFFF", - "size": 24, - "animation": "float_up_fade" - }, - "critical": { - "color": "#FF4444", - "size": 36, - "animation": "pop_bounce", - "prefix": "暴击!" - }, - "heal": { - "color": "#44FF44", - "size": 24, - "prefix": "+" - } - } -} -``` - -### 引导指示 - -``` -视觉引导手段: -- 箭头/光柱指向目标 -- 高亮可交互物体 -- 路径光效指引 -- 小地图标记 - -原则: -- 引导不能喧宾夺主 -- 可选关闭引导 -- 渐进式减少引导强度 -``` - -## 新手引导 - -### 引导类型 - -| 类型 | 适用场景 | 特点 | -|------|----------|------| -| 强制教学 | 核心机制 | 必须完成才能继续 | -| 可跳过教学 | 基础操作 | 可选择跳过 | -| 上下文提示 | 进阶技巧 | 首次使用时提示 | -| 悬浮帮助 | 复杂系统 | 悬停显示说明 | - -### 引导设计模板 - -```json -{ - "tutorial_step": { - "id": "first_attack", - "trigger": "first_enemy_encounter", - "highlight": "attack_button", - "message": "点击攻击按钮消灭敌人", - "arrow_target": "enemy", - "block_other_input": true, - "skip_condition": "enemy_killed" - } -} -``` - -### 引导流程设计 - -``` -开始 - ↓ -[触发条件检查] - ↓ -[暂停游戏/限制输入] - ↓ -[高亮UI元素] - ↓ -[显示指引信息] - ↓ -[等待玩家操作] - ↓ -[验证操作正确] - ↓ -[播放成功反馈] - ↓ -进入下一步骤 或 完成 -``` - -## 可访问性设计 - -### 视觉可访问性 - -```json -{ - "accessibility_options": { - "colorblind_mode": { - "type": ["protanopia", "deuteranopia", "tritanopia"], - "effect": "调整UI颜色方案" - }, - "high_contrast": { - "effect": "增强UI对比度" - }, - "text_size": { - "range": [0.8, 1.5], - "default": 1.0 - }, - "screen_reader": { - "support": true, - "read_hud": true - } - } -} -``` - -### 操作可访问性 - -| 功能 | 说明 | -|------|------| -| 按键重映射 | 所有操作可自定义 | -| 长按/连按切换 | 选择触发方式 | -| 自动瞄准 | 辅助瞄准功能 | -| QTE自动完成 | 自动完成快速反应事件 | -| 游戏速度调节 | 0.5x - 1.0x | - -### 听觉可访问性 - -``` -- 所有音频事件有视觉替代 -- 字幕系统完善 -- 闭 caption 支持音效描述 -- 音频提示可视化指示器 -``` - -## UI动效指南 - -### 常用动效 - -```json -{ - "animations": { - "fade_in": {"duration": 0.2, "easing": "ease_out"}, - "fade_out": {"duration": 0.15, "easing": "ease_in"}, - "scale_pop": {"duration": 0.15, "scale": 1.1, "easing": "back_out"}, - "slide_in": {"duration": 0.25, "direction": "right", "easing": "ease_out"}, - "shake": {"duration": 0.1, "intensity": 5}, - "pulse": {"duration": 0.5, "scale": 1.05, "loop": true} - } -} -``` - -### 动效原则 -- 持续时间 < 0.3s (不阻塞操作) -- 使用缓动函数增加质感 -- 避免过度动画分散注意力 -- 提供关闭动画的选项 diff --git a/.claude/skills/pptx/scripts/__pycache__/inventory.cpython-312.pyc b/.claude/skills/pptx/scripts/__pycache__/inventory.cpython-312.pyc deleted file mode 100644 index 46c9fe2ee7f2448301be0d2bed1020f78c4e582b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40506 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 by{~yVqazw#F_gS-wEWXP9Dl}Aw&4C>0^aHP diff --git a/CLAUDE.md b/CLAUDE.md index 1a022fae54..fe69390c12 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,7 +24,7 @@ ### 核心技术栈 - **后端**: Java 17 + Spring Boot 3.5.5 + MyBatis Plus + Redis -- **前端**: Vue.js 3 + TypeScript + Ant Design Vue + TailwindCSS +- **前端**: Vue.js 3 + TypeScript + shadcn-vue + TailwindCSS - **数据库**: MySQL 8.0+,支持多种数据库 - **AI服务**: 语音克隆、图像生成、音乐生成、向量搜索 @@ -38,6 +38,21 @@ - 样式优先使用less 示例:` diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/aiagent/dal/AiAgentDO.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/aiagent/dal/AiAgentDO.java index ab5ddd3975..066d90875d 100644 --- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/aiagent/dal/AiAgentDO.java +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/muye/aiagent/dal/AiAgentDO.java @@ -4,12 +4,14 @@ import lombok.*; import java.time.LocalDateTime; import com.baomidou.mybatisplus.annotation.*; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; /** * AI智能体 DO * * @author 芋道源码 */ +@TenantIgnore @TableName("muye_ai_agent") @KeySequence("muye_ai_agent_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data