feat(art): 样张 v1 制作与机检批——四源对照表先行+脚本 v1 重写(方案甲词表查表拼装+三区 mockup+机检 12 断言生成即验全绿)+visual-judge 预筛三轮过闸(素材透明留白裁除/§1.2 栈序修正/画布溢出断言化;游戏侧小人脚浮缺陷登记移交)+台账 169;样张交用户异步终审
This commit is contained in:
1 parent
e91a21afcf
commit
dc33c09260
5 files changed
+948
-1
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 286 KiB |
@@ -0,0 +1,20 @@
|
||||
# 样张 v1 机检报告(2026-09-26)
|
||||
|
||||
生成:`python tools/render_sample_sheet.py`;断言事实源 = 画法参数四源对照表-2026-09-26.md §6。
|
||||
|
||||
| 断言 | 判据 | 结果 | 明细 |
|
||||
|---|---|---|---|
|
||||
| F1 | 方案甲 8 邻拼装(词表查表,禁整块) | PASS | 错位 0 处;用件 15 种 ['center', 'center_crack', 'center_speckle', 'end_n', 'end_s', 'end_w', 'hrun', 'in_nw', 'in_sw', 'n', 'nw', 's', 'sw', 'vrun', 'w'] |
|
||||
| F2 | 旗仅出现于未开格 | PASS | 旗位 (1, 2) 格态=W |
|
||||
| F3 | 小人脚锚=自底边 25% 线(两态) | PASS | 脚 y=[213.75, 258.75] 期望=[213.75, 258.75] |
|
||||
| F4 | 数字 0 格无字形(0=纯安全格裸地面) | PASS | 0 态格 40 个(示例 (4,4));数字表含 0 键=False |
|
||||
| F5a | 接触带微光⊆英雄格周界(空间侧,不进墙格) | PASS | 微光 bbox=(86, 176, 119, 230) 英雄格=(90, 180, 135, 225) |
|
||||
| F5b | 类型微光禁整格染色(中心 50% 区零染色像素) | PASS | 五格中心区全净 |
|
||||
| A1a | 色板对表圣经 §4.1 冻结值 | PASS | {'INK': '#33261B', '青墨': '#132E36', '赭墨': '#3C250C', '锈墨': '#4A1E14', 'PLATE': '#F5EAD3', '旗朱': '#C63D22', '危险红': '#A8321F', '奖励金': '#F0A828'} |
|
||||
| A1b | 禁入带 HSV300°-345° 零命中 | PASS | 命中 0 像素(抽样) |
|
||||
| A2 | 拼缝周界闭合(墙侧 2px 墨线 <70) | PASS | 全部墙地边界墨线在位 |
|
||||
| A3 | 墨×牌 WCAG≥4.5:1 + 图底分离 | PASS | [('青墨', 12.0), ('赭墨', 12.0), ('锈墨', 11.8)];墙顶 113 vs 地面 51 |
|
||||
| A5 | 画布四缘 2px 净空(零溢出) | PASS | 缘带内容像素 0 |
|
||||
| A4 | 混排相融(游戏在库资产+UiKit 语义色) | PASS | 小人源 32×32(裁透明留白后 (19, 20))/立绘 48×48/状态图标 32×32 原件;INK/旗朱/奖励金 == ui_kit.gd |
|
||||
|
||||
结论:**全绿**(12/12 PASS)。气质终审 = 用户(异步,不阻塞 T2-T7)。
|
||||
@@ -0,0 +1,74 @@
|
||||
# 画法参数四源对照表(样张 v1 · T1 对表先行,2026-09-26)
|
||||
|
||||
> 任务书:`.zcode/plans/battle-stage-node2d-loop-plan-2026-09-26.md` T1①。四源 = A battle.md 布局线框 / B 机制代码(格态·旗语义·脚锚)/ C 引擎常量 / D art-bible 画法章节。**一处对不上 = L3**。本表逐参数登记取值与出处;样张 v1 脚本(tools/render_sample_sheet.py)只准消费本表参数。
|
||||
|
||||
## 1. 屏面布局(源 A:battle.md §Layout + §2 现状)
|
||||
|
||||
| 参数 | 取值 | 出处 |
|
||||
|---|---|---|
|
||||
| 基座分辨率 | 1280×800 | art-bible §8.1 两档定案 |
|
||||
| B 区 | 全宽 × 252px:主角站桩(左)+ 兑现演出区(中)+ 敌方站桩 ×1–3(右);顶部中央轮次·阶段指示;底部结算明细带(挖掘期收起 24px) | battle.md §Layout / battle_stage_view.gd `_init`(custom_minimum_size 0×252) |
|
||||
| C 区 | 左下 240px × 548px:攻击数据框(攻击值+修正来源角标+护甲 AC)+ 魔法道具槽(预留 4)+ 本关强化列表(预留) | battle.md §Layout(2026-09-26 信息归属批后口径)/ hero_info_panel_view |
|
||||
| D 区 | 右侧余幅(~1036px):顶行 AP 点阵+RAP+弃图刷新+图例+停手;下为棋盘 | battle.md §Layout / ap_bar_view |
|
||||
| 站桩盒 | 150×176:血条(10px)+状态行(16px)头顶悬浮;立绘盒 96px;底座底条(10px);敌名行(护甲图标+纯名字,12px);顺位徽标①②③ | standee_view.gd `_init` 几何 |
|
||||
| B 区立绘 | 主角 = 48×48 朝右贴图(96px 盒 = 48×2 整数档);敌人 = 字形占位(▂地精/▆熊地精/█食人魔酋长——素材零张在库) | battle_stage_view.gd:38 / standee_view.gd `_refresh_portrait_glyph` / §2 现状「敌人立绘素材一张都不存在」 |
|
||||
|
||||
## 2. 格态与语义(源 B:机制代码)
|
||||
|
||||
| 参数 | 取值 | 出处 |
|
||||
|---|---|---|
|
||||
| 格态 | 未开(墙)/ 已开(地面)两态;雷 = 已开态巢穴纹更凶一档 | board_view.gd paint 分支 / art-bible §3.2 地砖条 |
|
||||
| 数字 0 格 | **无字形**(`if d > 0: glyph = str(d)`——0 = 纯安全格裸地面) | board_view.gd:583-586(FATAL④ 事实源) |
|
||||
| 旗语义 | 旗只插**未开格**(旗分支在 `else` 未开侧);旗 = 封印印记呈现名;旗面恒画右 | board_view.gd:589-590 / art-bible §3.2 旗条(FATAL② 事实源) |
|
||||
| 悬停金框 | 预承诺光标态画在目标格(逐格指认=悬停金框) | art-bible §3.2 接触带条尾句 / board_view `_apply_hover_visuals` |
|
||||
| 小人同格 | 小人与数字同格:数字色牌不废、蓝框不覆盖文本 | art-bible §1.4 小人两态条 |
|
||||
|
||||
## 3. 引擎常量(源 C:board_view.gd / standee_view.gd / ui_kit.gd)
|
||||
|
||||
| 参数 | 取值 | 出处 |
|
||||
|---|---|---|
|
||||
| 格边长(1280 基座) | 45px(10×10 板)/ 36px(12×12 板);样张 D 区按 45px 实比 | art-bible §8.1 / board_view clamp(26,46) |
|
||||
| 小人脚锚 | **自底边往上 25% 线**(HERO_GROUND_FRAC=0.25;⚠ 纵向比例一律自底边口径) | board_view.gd HERO_GROUND_FRAC(FATAL③ 事实源) |
|
||||
| 小人档 | 32×32 源 ×1.5 = 48px(台账 161 用户实机拍板档;45px×32 源非整数 NEAREST 已接受——台账 166 同级) | board_view.gd HERO_SPRITE_SCALE |
|
||||
| 椭圆影 | 贴脚(中心=脚锚),32×12px @1×,黑 35% | board_view.gd HERO_SHADOW_PX / 台账 162 |
|
||||
| 立绘缩放 | B 区 96px 盒 = 48 源 ×2 整数(C5) | standee_view.gd set_portrait_texture |
|
||||
| 变异遮蔽 | hide_number 数字格显示 `?`(样张不含变异格——六类型纹带另批) | board_view.gd:584 |
|
||||
| 游戏 UiKit 语义色 | INK #33261B / FLAG_RED #C63D22 / GOLD #F0A828 / RED_DEEP #963C30 / 数字=INK | ui_kit.gd 常量(A4 混排对表基准) |
|
||||
|
||||
## 4. 画法章节(源 D:art-bible)
|
||||
|
||||
| 参数 | 取值 | 出处 |
|
||||
|---|---|---|
|
||||
| 未开格 | **方案甲高墙瓦片 8 邻拼装**:16 唯一件+运行时 flip_h 镜像(禁旋转);词表=board-tile-vocab-plan-a;板外视为墙;center 变体 crack/speckle 60/20/20 哈希分配 | §3.2 高墙瓦片体系条 / design/research/board-tile-vocab-plan-a-2026-09-26.md(FATAL① 事实源) |
|
||||
| 墨线纪律 | 墙/地分界 1px 深墨线贴格边(缘件自带,实测朝地边亮度 ~22-36 vs 本体 ~122);南缘立面窄暗条;北缘内侧 1px 提亮 | §3.2 墨线纪律 / v9.3 实测 |
|
||||
| 图底分离 | 已开地面明度 < 未开墙顶(实测 Δ≈75,0-255 简易亮度) | §8.5 图底分离条 |
|
||||
| 已开格 | 地面瓦片 5 变体哈希;产资源格 = 家族物件纹样(道具点剪影)角嵌砖缘+**砖心留白予数字色牌**;纯安全格 = 干净裸地面 | §3.2 结果整格表达 / §1.2 原则三 |
|
||||
| 数字色牌 | #F5EAD3 素色直角小牌,恒定不随格底;数字墨三档:1-3 青墨 #132E36 / 4-5 赭墨 #3C250C / 6-8 锈墨 #4A1E14;对牌对比 ≥4.5:1(实测 11.8-12.3:1);字号下限 ≥18px | §4.1 / HUD框架 基线表 |
|
||||
| 语义色 | 掩尘 #C9AF83 / 裸土 #B39A78 / 旗朱 #C63D22 / 危险红 #A8321F / 奖励金 #F0A828 | §4.1 冻结 |
|
||||
| 类型角嵌 | 未开格四角/边框墨族单色纹样(九家族键 §4.6.3),砖心留白;形状主通道 | §3.2 类型角嵌条 |
|
||||
| 类型微光 | 五组候选:通用增益 #D6C13A / 治疗 #5CB83A / 穿刺 #2E9E9E / 魔法 #7B5BD6 / 群伤 #FF2D55;底效泛光层不入遮挡栈;**禁整格染色**(v1 画法=内缘带状泛光,机检断言中心 50% 区零微光像素);不以明度增量承载信息 | §4.2 类型微光色带 / §1.4 验收 2(FATAL⑤ 事实源之一) |
|
||||
| 接触带微光 | 英雄格周界**空间侧**:正交邻墙=整条边、斜向=共享角点+两侧各半条边;蓝族浅一档;光不进墙格 | §3.2 可挖域接触带微光条 / §2.1(FATAL⑤ 事实源之二) |
|
||||
| 禁入带 | HSV 色相 300°–345° 全场禁入(微光群伤组 349° 刻意避开) | §4.2 紫赤条 |
|
||||
| 换皮 | §4.3 四槽换色(tile_closed/tile_open/wall_ambient/variant_tint),信息层恒定;样张 ≥两套(dungeon+underdark) | §4.3 / §1.4 内容清单 |
|
||||
| 小人占位 | 独立资产 32×32;样张 48px ≈ 0.75× 格边符合 §8.2 占位上限 | §8.2 画布基准 |
|
||||
|
||||
## 5. 候选值与偏差登记(非矛盾,样张批冻结项)
|
||||
|
||||
1. **小人蓝**:art-bible §4.1 = #2E86DE(候选,样张批冻结终稿);游戏 UiKit = #2E6E8E。两值并存系圣经明标「候选值」——样张以 #2E86DE 出图;冻结后游戏侧换色归循环 2,本批不改代码。
|
||||
2. **接触带微光色值**:空间侧+蓝族已定稿(v2.8),具体色值样张批冻结——v1 取小人蓝族浅两档 #D6ECFF(近白蓝)出样。
|
||||
3. **格底 L≥0.30 的适用面**:§4.1/§4.3 系换皮四槽**色槽参数域**(新图类型色板须守);dungeon 定稿瓦片(v9.3 用户验收入本体)为素材实态,本样张不再以 L≥0.30 复裁(防对已验收资产误报 FATAL),改以图底分离+可读性硬指标承载。
|
||||
4. **巢穴纹/封印印记**:素材缺口,程序化占位画面内标注(§1.4 允许——「素材缺口见清单」v0 先例);正式纹样归绘画批。
|
||||
|
||||
## 6. 机检断言映射(v0 五画错 + 程序化四件 → 生成脚本自检)
|
||||
|
||||
| 断言 | 判据 |
|
||||
|---|---|
|
||||
| F1 方案甲拼装 | 每墙瓦片位 = 词表查表(四邻+对角)结果;禁整块(center 之外的件 ≥6 种实出);变体 60/20/20 |
|
||||
| F2 旗∈未开格 | 旗位集 ⊆ 墙位集 |
|
||||
| F3 脚锚自底 25% | 两小人态脚 y = 格底 − 0.25×45;影心=脚点 |
|
||||
| F4 0 格无字形 | 数字 0 格 glyph 记录 = 空 |
|
||||
| F5 微光带限 | 接触带微光像素 ⊆ 英雄格∩带几何(≤8px 距离);类型微光中心 50% 区零像素 |
|
||||
| A1 色板+禁入带 | 冻结色逐字节对表 §4.1;微光五组对表 §4.2;全图像素扫 HSV 300°-345°(S>0.15,V>0.20)零命中 |
|
||||
| A2 周界闭合 | 每段墙↔地边界墙侧 2px 带均亮度 <70(墨线在位);墙群外轮廓无缺口 |
|
||||
| A3 明度对比 | 三档墨×色牌 WCAG ≥4.5:1;地面均亮 < 墙顶均亮 −40 |
|
||||
| A4 混排相融 | 语义色 == 游戏 UiKit 常量(小人蓝候选偏差见 §5.1);小人/立绘/状态图标 = 游戏在库资产原件 |
|
||||
@@ -455,4 +455,5 @@
|
||||
|
||||
164. **高墙瓦片 v9.3 定稿交付+备选案清场批(2026-09-26,本会话——用户验收 v9.3 后指令「直接移入游戏本体+备选案打包挪走」;零代码、素材不入 git,worktree 不适用)**:**定稿交付**=21 枚入 `game/assets/art/board/`(16 唯一墙瓦片〔center/center_crack/center_speckle/n/s/w/nw/sw/hrun/vrun/end_n/end_s/end_w/lone/in_nw/in_sw〕+5 地面瓦片变体,`terrain_*_dungeon.png`;东/NE/SE/端头E/凹角NE/SE=运行时 flip_h 镜像不出文件,旧 beast-cave 砖零触碰)。**终版路线**=材质归模型(用户 Gemini 生图①岩石/④泥土→周期对齐无缝裁剪+BOX 降采样 32×32+环绕混合+量化 12 色)+结构归程序(v9 墨线纪律:全开缘 1px 深墨线直线零抖动/北缘内侧 1px 就地提亮/南缘立面窄暗条 11px/凹角角点+立面侧 1px 齐平收头——v9.1 悬空/v9.2 突出/v9.3 齐平三轮定位,前后对比+逐行像素审计存 `_work/corner_cmp_v91_v93_5x.png`)。**术语共识(用户立)**:未开格=墙瓦片,已开格=地面瓦片。**备选案清场**=v1–v9.2 全迭代+被否素材(含 v7 程序砌石/v3 生图 5×5 表)打包 `备选案-terrain-walls-v1-v9_2-2026-09-26.zip`(383 项 7.5MB,用户挪出库),`_work/` 仅留生图①④源图+风格参考+终版证据+定稿件。**复盘机制落记忆**(pixel-structure-verify-discipline):先抄后造/验收口径写像素级视觉判据/±1px 一律坐标推导/修复默认附前后对比。**遗留**:underdark/deep_underdark 换色(AI 纹理色调映射)+实机查表合成器(BoardWorld 接线,含镜像翻转)归后续批;任务书验收单逐条结果以本批 v9.3 终测为准(图底分离 67.6/无死黑/墨线周界闭合/覆盖层可读)。
|
||||
|
||||
168. **战斗舞台 Node2D 循环 1 · T0 开工批(2026-09-26,本会话——计划书 .zcode/plans/battle-stage-node2d-loop-plan-2026-09-26.md 已批准从 T0 自治开跑;worktree battle-stage-node2d-20260926)**:worktree 自 master 70d2320 建+editor --quit 刷缓存 0 error;git status 归属核对=主区脏文件(.gitignore/.zcode config/ui-reuse 指引/playnote/project.godot/loop-dashboard/active.md 165-167 未提交并行登记+untracked 美术文档组)均非本批只标旗不碰。**基线复读全绿记档**——冒烟 557/0+五探针 PASS(ui_probe cells=144 board=12x12/ui_click 问题=0/ui_tune 全链/ui_layout dump:stage 1280×252·board_view 1036×965·info_panel 240×1028/ui_input 十一断言)+static_scan exit0+文档三闸基线存档(死链 7/退役词命中/gdd_lint 8 文件 8 不通过 69 项级)+MC 12 局 both(max 路由) 24 行 RUNSUM sha256=de8225b3 存 /tmp 供 T7 逐位对拍。**§2 现状事实逐条 grep 复核零漂移**——battle_stage_view(PanelContainer/252px/五成员/setter 面)+standee_view(Control/行为面 17 方法/slot_row compact/字形占位)+hud_presenter 53-54/79-80 单投喂+main 309/441/503+ui_assembler 23+ui_probe 95-129/ui_click 223 穿透面+ui_input_probe 零 B 区耦合(grep stage/standee/enemy=0 命中)+TEX_HERO_PORTRAIT 素材 worktree 缺位=C7 回退字形路径预期。**Alpha 清单初稿落盘** production/alpha-acceptance-battle-screen.md——六源拼装(battle.md AC+§13 义务表/HUD框架 验收标准+基线表/art-bible §8 判据/07§⑨ 试玩指引/06 挂 Alpha 条目备查/工程回归面),七节 30 条=判据+验法三档(headless/真机渲染/人)+状态列,销项归 Alpha 验收批。轻闸全绿,进入 T1。
|
||||
168. **战斗舞台 Node2D 循环 1 · T0 开工批(2026-09-26,本会话——计划书 .zcode/plans/battle-stage-node2d-loop-plan-2026-09-26.md 已批准从 T0 自治开跑;worktree battle-stage-node2d-20260926)**:worktree 自 master 70d2320 建+editor --quit 刷缓存 0 error;git status 归属核对=主区脏文件(.gitignore/.zcode config/ui-reuse 指引/playnote/project.godot/loop-dashboard/active.md 165-167 未提交并行登记+untracked 美术文档组)均非本批只标旗不碰。**基线复读全绿记档**——冒烟 557/0+五探针 PASS(ui_probe cells=144 board=12x12/ui_click 问题=0/ui_tune 全链/ui_layout dump:stage 1280×252·board_view 1036×965·info_panel 240×1028/ui_input 十一断言)+static_scan exit0+文档三闸基线存档(死链 7/退役词命中/gdd_lint 8 文件 8 不通过 69 项级)+MC 12 局 both(max 路由) 24 行 RUNSUM sha256=de8225b3 存 /tmp 供 T7 逐位对拍。**§2 现状事实逐条 grep 复核零漂移**——battle_stage_view(PanelContainer/252px/五成员/setter 面)+standee_view(Control/行为面 17 方法/slot_row compact/字形占位)+hud_presenter 53-54/79-80 单投喂+main 309/441/503+ui_assembler 23+ui_probe 95-129/ui_click 223 穿透面+ui_input_probe 零 B 区耦合(grep stage/standee/enemy=0 命中)+TEX_HERO_PORTRAIT 素材 worktree 缺位=C7 回退字形路径预期。**Alpha 清单初稿落盘** production/alpha-acceptance-battle-screen.md——六源拼装(battle.md AC+§13 义务表/HUD框架 验收标准+基线表/art-bible §8 判据/07§⑨ 试玩指引/06 挂 Alpha 条目备查/工程回归面),七节 30 条=判据+验法三档(headless/真机渲染/人)+状态列,销项归 Alpha 验收批。轻闸全绿,进入 T1。
|
||||
169. **样张 v1 制作与机检批(2026-09-26,本会话——循环 1 T1;worktree battle-stage-node2d-20260926 批次本提交)**:①**四源对照表先行** production/mockups/画法参数四源对照表-2026-09-26.md(battle.md 布局/机制代码〔0 格无字形 board_view:586·旗只插未开 :589·脚锚自底 25%〕/引擎常量〔45px·1.5× 档·96px 盒〕/art-bible §1.2-§4.6 画法)——候选值偏差两笔登记(小人蓝 #2E86DE 候选 vs UiKit #2E6E8E=样张批冻结项非矛盾;格底 L≥0.30 系换皮槽参数域不复裁已验收 dungeon 瓦片)。②**脚本重写** tools/render_sample_sheet.py v1:方案甲词表查表拼装(16 情形+对角回落 nw>ne>sw>se+flip 镜像+变体 60/20/20 哈希)+三区全内容 1280×800 实比 mockup+S2 数字档 0.5× 抽检+S3 underdark 四槽换色+S4 图例;素材两级回退(worktree→主检出,素材不入 git)。③**机检 12 断言生成即验全绿**——F1 词表拼装零错位 15 种用件/F2 旗∈未开/F3 脚锚自底 25%(两态)〔预筛抓出素材底部 7 行全透明→裁除留白后锚定,视觉脚=锚线;**游戏内 BoardWorld.ensure_hero 同病(视觉脚浮锚线 ~10px)=呈现缺陷登记移交循环 2/接线批**〕/F4 0 格无字形/F5a 接触带微光⊆英雄格/F5b 类型微光禁整格染色(中心 50% 区零像素——角嵌臂长两轮收紧 L9→L5 留白砖心)/A1 色板对表+禁入带 300-345° 零命中(腐蚀两轮滤 AA 混色)/A2 周界闭合(墙侧 2px 带墨线存在性 min<55——45px 非整数缩放下均值判误报改最暗值判)/A3 墨×牌 11.8-12.0:1+图底分离 113 vs 51/A4 混排在库原件/A5 画布四缘净空(溢出永久断言)。④**visual-judge 预筛三轮**(≤3 预算内):R1 四 FATAL→triage=站桩横向重叠/小人脚浮(素材留白)/同格态色牌被小人盖(改 §1.2 栈序数字>小人)/缘件裸拼误报(1px 墨线逐像素 zoom 自证 end_n 三向墨线俱在+15 种用件机检反证,判官 3-6px 采样带均值漏 1px 线)+地精无徽标误报(普攻零表示=设计);R2 两 FATAL=画布右缘溢出×2(说明行+S4 末项)→画布 1560+A5 断言化;R3 窄域复核 pass。⑤**交付**=样张 v1+机检报告+对照表三件已拷主区 production/mockups/(评审产物不入 git 同 v0 先例),**用户异步终审**(过=冻结为循环 2 输入;不过=对表改参重出);终审不阻塞 T2-T7。轻闸全绿(当轮基线=T0 记档),进入 T2。
|
||||
@@ -0,0 +1,852 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""战斗一屏样张验收图生成脚本 v1(2026-09-26,循环 1 T1)。
|
||||
|
||||
v0 四 FATAL 案底(2026-09-26 用户否决)全部断言化——生成即验,不过闸不出图:
|
||||
F1 未开格 = 方案甲缘件 8 邻拼装(词表查表,禁整块 center 铺满)
|
||||
F2 旗仅出现于未开格
|
||||
F3 小人脚锚 = 自底边 25% 线(⚠ 纵向比例一律自底边口径)
|
||||
F4 数字 0 格无字形(纯安全格 = 干净裸地面)
|
||||
F5 微光仅带状:接触带微光只画英雄格周界空间侧;类型微光禁整格染色(中心 50% 区零像素)
|
||||
程序化四件审计(§9 初级验收授权):
|
||||
A1 色板对表圣经冻结色板 + 禁入带(HSV 300°-345°)全图扫描
|
||||
A2 拼缝周界闭合(墙↔地边界墙侧 2px 墨线带亮度 <70)
|
||||
A3 明度对比(数字墨×色牌 WCAG ≥4.5:1;图底分离)
|
||||
A4 与现有素材混排相融(语义色 == 游戏 UiKit 常量;小人/立绘/状态图标 = 在库资产原件)
|
||||
|
||||
一屏样张 = B/C/D 三区全内容(1280×800 1:1 实比) + 附录 S2 数字全档 0-8 / S3 underdark
|
||||
换皮对照 / S4 图例。内容清单 = art-bible §1.4(九组,缺项即无效)。
|
||||
素材源 = game/assets/art(board 21 瓦片 + character 小人/立绘)+ ui/icons 状态图标;
|
||||
素材不入 git——先查本仓库,缺位回退主检出(D:/code/g-plan2)。
|
||||
词表事实源 = design/research/board-tile-vocab-plan-a-2026-09-26.md;画法参数事实源 =
|
||||
production/mockups/画法参数四源对照表-2026-09-26.md(四源对表先行产物)。
|
||||
输出 = production/mockups/sample-batch-v1-2026-09-26.png + sample-v1-机检报告-2026-09-26.md。
|
||||
用法:python tools/render_sample_sheet.py
|
||||
"""
|
||||
from PIL import Image, ImageDraw, ImageFilter, ImageChops, ImageFont
|
||||
import os
|
||||
import sys
|
||||
import math
|
||||
import hashlib
|
||||
from collections import deque
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
MAIN_ROOT = "D:/code/g-plan2"
|
||||
|
||||
|
||||
def asset_root(*parts):
|
||||
"""素材路径两级回退:worktree → 主检出(素材不入 git,fresh worktree 缺位)。"""
|
||||
for base in (ROOT, MAIN_ROOT):
|
||||
p = os.path.join(base, *parts)
|
||||
if os.path.exists(p):
|
||||
return p
|
||||
raise FileNotFoundError(os.path.join(*parts))
|
||||
|
||||
|
||||
BOARD_DIR = ("game", "assets", "art", "board")
|
||||
CHAR_DIR = ("game", "assets", "art", "character")
|
||||
ICON_DIR = ("game", "assets", "ui", "icons")
|
||||
OUT_PNG = os.path.join(ROOT, "production", "mockups", "sample-batch-v1-2026-09-26.png")
|
||||
OUT_REPORT = os.path.join(ROOT, "production", "mockups", "sample-v1-机检报告-2026-09-26.md")
|
||||
T_FONT = asset_root("design", "art", "candidates", "ui-reuse-2026-09-23",
|
||||
"4-字体", "fusion-pixel-12px(OFL)", "fusion-pixel-12px-proportional-latin.ttf")
|
||||
ZH_FONT = r"C:\Windows\Fonts\msyh.ttc"
|
||||
|
||||
# ══ 冻结色板(art-bible §4.1——A1 逐字节对表基准,禁改动) ══
|
||||
INK = (51, 38, 27) # 主墨 #33261B
|
||||
INK_COLD = (19, 46, 54) # 青墨 1-3 #132E36
|
||||
INK_OCHRE = (60, 37, 12) # 赭墨 4-5 #3C250C
|
||||
INK_RUST = (74, 30, 20) # 锈墨 6-8 #4A1E14
|
||||
PLATE = (245, 234, 211) # 数字色牌 #F5EAD3
|
||||
FLAG_RED = (198, 61, 34) # 旗朱 #C63D22
|
||||
DANGER_RED = (168, 50, 31) # 危险红 #A8321F
|
||||
REWARD_GOLD = (240, 168, 40) # 奖励金 #F0A828
|
||||
HERO_BLUE = (46, 134, 222) # 小人蓝 #2E86DE(候选,样张批冻结——对照表 §5.1)
|
||||
RED_MINE = (150, 44, 36) # 引爆雷字朱(游戏 UiKit.RED_MINE)
|
||||
# 候选(样张批冻结项——对照表 §5)
|
||||
CONTACT_GLOW = (214, 236, 255) # 接触带微光:小人蓝族浅两档(近白蓝)
|
||||
GLOW_GROUPS = { # §4.2 类型微光五组候选
|
||||
"attack": (214, 193, 58), "heal": (92, 184, 58), "pierce": (46, 158, 158),
|
||||
"magic": (123, 91, 214), "blast": (255, 45, 85), "gp": (214, 193, 58),
|
||||
}
|
||||
GLOW_FAMILY = {"attack": "通用增益", "defense": "通用增益", "weaken": "通用增益",
|
||||
"action": "通用增益", "gp": "通用增益", "heal": "治疗",
|
||||
"pierce": "穿刺", "magic": "魔法", "blast": "群伤"}
|
||||
# 游戏 UiKit 常量(A4 混排对表——ui_kit.gd 实测值)
|
||||
UIKIT = {"INK": INK, "FLAG_RED": FLAG_RED, "GOLD": REWARD_GOLD, "RED_MINE": RED_MINE,
|
||||
"PANEL_STAGE": (216, 200, 172), "PANEL_HERO": (232, 220, 196),
|
||||
"PANEL_DATA": (226, 210, 182), "PANEL_BORDER": (120, 106, 88),
|
||||
"BRICK": (201, 175, 131), "FLOOR": (179, 154, 120)}
|
||||
ARMOR_ICONS = {"light": "◇", "ward": "◆", "heavy": "▣"}
|
||||
ARMOR_COLORS = {"light": (120, 110, 98), "ward": HERO_BLUE, "heavy": (176, 122, 20)}
|
||||
|
||||
F_DIGIT = ImageFont.truetype(T_FONT, 30) # S2 64px 格数字
|
||||
F_DIGIT_D = ImageFont.truetype(T_FONT, 18) # D 区 45px 格数字(下限 ≥18px)
|
||||
F_SMALL = ImageFont.truetype(T_FONT, 16)
|
||||
F_LBL = ImageFont.truetype(ZH_FONT, 14)
|
||||
F_LBL2 = ImageFont.truetype(ZH_FONT, 12)
|
||||
F_TITLE = ImageFont.truetype(ZH_FONT, 19)
|
||||
F_STANDEE = ImageFont.truetype(ZH_FONT, 42) # 敌像字形占位
|
||||
F_NAME = ImageFont.truetype(ZH_FONT, 12)
|
||||
F_ORDER = ImageFont.truetype(ZH_FONT, 13)
|
||||
F_INTENT = ImageFont.truetype(ZH_FONT, 15)
|
||||
|
||||
CHECKS = [] # (id, name, ok, detail)
|
||||
|
||||
|
||||
def check(cid, name, ok, detail=""):
|
||||
CHECKS.append((cid, name, bool(ok), detail))
|
||||
print("[%s] %s — %s%s" % ("PASS" if ok else "FAIL", cid, name,
|
||||
(";" + detail) if detail else ""))
|
||||
|
||||
|
||||
# ══ 素材装载 ══
|
||||
def load_tile(name):
|
||||
return Image.open(asset_root(*(BOARD_DIR + (name,)))).convert("RGB")
|
||||
|
||||
|
||||
WALLS = {}
|
||||
for k in ["center", "center_crack", "center_speckle", "n", "s", "w", "nw", "sw",
|
||||
"hrun", "vrun", "end_n", "end_s", "end_w", "lone", "in_nw", "in_sw"]:
|
||||
WALLS[k] = load_tile("terrain_wall_%s_dungeon.png" % k)
|
||||
FLOORS = [load_tile("terrain_floor_dungeon_%d.png" % i) for i in range(1, 6)]
|
||||
HERO_SRC = Image.open(asset_root(*(CHAR_DIR + ("character_hero_idle.png",)))).convert("RGBA")
|
||||
PORTRAIT_SRC = Image.open(asset_root(*(CHAR_DIR + ("character_hero_portrait.png",)))).convert("RGBA")
|
||||
STATUS_ICONS = {k: Image.open(asset_root(*(ICON_DIR + ("status_%s.png" % k,)))).convert("RGBA")
|
||||
for k in ["attack_up", "ac_up", "alert"]}
|
||||
|
||||
|
||||
def hero_sprite_transparent(src):
|
||||
"""素材黑幕去底:四角泛洪把连通近黑背景转透明(角色内部深色像素不受影响)。"""
|
||||
im = src.copy()
|
||||
w, h = im.size
|
||||
px = im.load()
|
||||
seen = [[False] * w for _ in range(h)]
|
||||
q = deque()
|
||||
for sx, sy in [(0, 0), (w - 1, 0), (0, h - 1), (w - 1, h - 1)]:
|
||||
q.append((sx, sy))
|
||||
while q:
|
||||
x, y = q.popleft()
|
||||
if x < 0 or y < 0 or x >= w or y >= h or seen[y][x]:
|
||||
continue
|
||||
seen[y][x] = True
|
||||
r, g, b, a = px[x, y]
|
||||
if r < 14 and g < 14 and b < 14:
|
||||
px[x, y] = (r, g, b, 0)
|
||||
q.extend([(x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)])
|
||||
return im
|
||||
|
||||
|
||||
HERO = hero_sprite_transparent(HERO_SRC)
|
||||
# 裁除透明留白(v1 预筛发现:素材底部 7 行全透明——直接缩放则视觉脚浮于锚线上方 ~10px。
|
||||
# 裁后按内容 bbox 缩放,视觉脚底 = 脚锚线。游戏内 BoardWorld.ensure_hero 用原素材同病
|
||||
# (视觉脚浮 ~10px)——呈现缺陷登记移交,本样张按设计规则出图)
|
||||
_HB = HERO.getbbox()
|
||||
HERO = HERO.crop(_HB)
|
||||
|
||||
|
||||
# ══ 方案甲词表查表(board-tile-vocab-plan-a §三 逐条转写) ══
|
||||
def is_wall(gmap, x, y):
|
||||
if x < 0 or y < 0 or x >= gmap.w or y >= gmap.h:
|
||||
return True # 板外视为墙(词表 §三.1)
|
||||
return gmap.grid[y][x] == "W"
|
||||
|
||||
|
||||
def lookup_wall_tile(gmap, x, y):
|
||||
"""四正交邻 → 16 情形;四邻皆墙查对角(凹角回落优先级 nw>ne>sw>se);center 变体哈希 60/20/20。"""
|
||||
n, e, s, w = (is_wall(gmap, x, y - 1), is_wall(gmap, x + 1, y),
|
||||
is_wall(gmap, x, y + 1), is_wall(gmap, x - 1, y))
|
||||
flip = False
|
||||
if n and e and s and w:
|
||||
dnw, dne, dsw, dse = ((not is_wall(gmap, x - 1, y - 1)), (not is_wall(gmap, x + 1, y - 1)),
|
||||
(not is_wall(gmap, x - 1, y + 1)), (not is_wall(gmap, x + 1, y + 1)))
|
||||
if dnw:
|
||||
return "in_nw", False
|
||||
if dne:
|
||||
return "in_nw", True # in_ne ← in_nw 镜像
|
||||
if dsw:
|
||||
return "in_sw", False
|
||||
if dse:
|
||||
return "in_sw", True # in_se ← in_sw 镜像
|
||||
h = int(hashlib.md5(("wall%d_%d" % (x, y)).encode()).hexdigest(), 16) % 100
|
||||
if h < 60:
|
||||
return "center", False
|
||||
if h < 80:
|
||||
return "center_crack", False
|
||||
return "center_speckle", False
|
||||
if not n and s and e and w:
|
||||
return "n", False
|
||||
if not s and n and e and w:
|
||||
return "s", False
|
||||
if not w and n and e and s:
|
||||
return "w", False
|
||||
if not e and n and s and w:
|
||||
return "w", True # wall_e ← wall_w 镜像
|
||||
if not n and not s and e and w:
|
||||
return "hrun", False
|
||||
if not w and not e and n and s:
|
||||
return "vrun", False
|
||||
if not n and not w and s and e:
|
||||
return "nw", False
|
||||
if not n and not e and s and w:
|
||||
return "nw", True # wall_ne ← wall_nw 镜像
|
||||
if not s and not w and n and e:
|
||||
return "sw", False
|
||||
if not s and not e and n and w:
|
||||
return "sw", True # wall_se ← wall_sw 镜像
|
||||
if not n and not e and not w and s:
|
||||
return "end_n", False
|
||||
if not s and not e and not w and n:
|
||||
return "end_s", False
|
||||
if not w and not n and not s and e:
|
||||
return "end_w", False
|
||||
if not e and not n and not s and w:
|
||||
return "end_w", True # wall_end_e ← wall_end_w 镜像
|
||||
return "lone", False
|
||||
|
||||
|
||||
class GMap(object):
|
||||
def __init__(self, rows):
|
||||
self.grid = rows
|
||||
self.w = len(rows[0])
|
||||
self.h = len(rows)
|
||||
|
||||
|
||||
def floor_variant(x, y):
|
||||
"""地面 5 变体按格坐标哈希(词表 §三.3——初始化随机落定)。"""
|
||||
return int(hashlib.md5(("floor%d_%d" % (x, y)).encode()).hexdigest(), 16) % len(FLOORS)
|
||||
|
||||
|
||||
# ══ D 区 10×10 样张板(内容布置见对照表 §6/§1.4 内容清单) ══
|
||||
BOARD_ROWS = [
|
||||
"WWWWWWWWWW",
|
||||
"WWWWWWWWWW",
|
||||
"WWFFWWFFWF",
|
||||
"WWFFFFWFWF",
|
||||
"WWFFFFWFWF",
|
||||
"WWFFFFWFWF",
|
||||
"WFFFFFFWFF",
|
||||
"FFFFFFWWFF",
|
||||
"FFWWWWWWFF",
|
||||
"FFFFFWWWFF",
|
||||
]
|
||||
# 覆盖层布置(格坐标)
|
||||
CRACK_AT = (3, 0) # center 裂纹变体
|
||||
SPECKLE_AT = (6, 0) # center 碎石斑变体
|
||||
FLAG_AT = (1, 2) # 封印印记——未开格(F2)
|
||||
HOVER_AT = (1, 4) # 悬停金框(预承诺光标态)
|
||||
HERO_AT = (2, 4) # 小人独立占格
|
||||
HERO_DIGIT_AT = (2, 5) # 小人与数字同格(6)——验蓝框不覆盖文本
|
||||
NEST_AT = (5, 4) # 雷格巢穴(已开,程序占位+危险红)
|
||||
GLOW_CELLS = [(4, 2, "attack"), (5, 2, "heal"), (8, 3, "blast"),
|
||||
(6, 4, "pierce"), (7, 6, "magic")] # 五组;attack+heal 相邻并列
|
||||
INLAY_RESULT = {(4, 6): "attack", (5, 6): "gp"} # 已开结果格物件纹样
|
||||
DIGITS = {(3, 4): 3, (3, 5): 1, (4, 5): 2, (2, 5): 6, (7, 4): 8, (7, 5): 7,
|
||||
(4, 6): 5, (5, 6): 4} # 0 格=已开无字形(纯安全格全体现)
|
||||
VARIANT_FLOOR_AT = (4, 5) # 装饰变体底色牌示例(强制 floor 变体 3)
|
||||
|
||||
CELL_D = 45 # 1280 基座 10×10 板实比(art-bible §8.1)
|
||||
HERO_PX = 48 # 32 源 ×1.5(台账 161 档)
|
||||
GROUND_FRAC = 0.25 # 脚锚自底边 25%(F3)
|
||||
|
||||
|
||||
def wcag_lum(rgb):
|
||||
def lin(c):
|
||||
c = c / 255.0
|
||||
return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
|
||||
r, g, b = rgb[:3]
|
||||
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b)
|
||||
|
||||
|
||||
def draw_digit_plate(img, x0, y0, cell, digit, font, plate_size=None, ink=None):
|
||||
"""数字色牌:素色直角小牌+墨档数字(§4.1)。返回牌矩形。"""
|
||||
d = ImageDraw.Draw(img)
|
||||
pw, ph = plate_size or (int(cell * 0.60), int(cell * 0.62))
|
||||
px0 = x0 + (cell - pw) // 2
|
||||
py0 = y0 + (cell - ph) // 2
|
||||
d.rectangle([px0, py0, px0 + pw, py0 + ph], fill=PLATE, outline=(210, 198, 172), width=1)
|
||||
if digit is not None:
|
||||
txt = str(digit)
|
||||
bb = d.textbbox((0, 0), txt, font=font)
|
||||
tw, th = bb[2] - bb[0], bb[3] - bb[1]
|
||||
d.text((px0 + (pw - tw) // 2 - bb[0], py0 + (ph - th) // 2 - bb[1]), txt,
|
||||
font=font, fill=ink or digit_ink(digit))
|
||||
return (px0, py0, pw, ph)
|
||||
|
||||
|
||||
def digit_ink(d):
|
||||
if d <= 3:
|
||||
return INK_COLD
|
||||
if d <= 5:
|
||||
return INK_OCHRE
|
||||
return INK_RUST
|
||||
|
||||
|
||||
def draw_inlay(img, ox, oy, cell, kind, corner=4, m=None, L=None):
|
||||
"""角嵌纹样(墨族单色,贴砖缘/砖角,砖心留白——§3.2;已开格=物件纹样同语言)。程序化示意。"""
|
||||
layer = Image.new("RGBA", img.size, (0, 0, 0, 0))
|
||||
d = ImageDraw.Draw(layer)
|
||||
col = INK + (215,)
|
||||
if corner == 4: # 未开格:四角
|
||||
pts = [(m, m, 1, 1), (cell - m, m, -1, 1), (m, cell - m, 1, -1), (cell - m, cell - m, -1, -1)]
|
||||
else: # 已开格:单角物件纹样(右上角,砖心留白予色牌)
|
||||
pts = [(cell - m, m, -1, 1)]
|
||||
for (px, py, sx, sy) in pts:
|
||||
X, Y = ox + px, oy + py
|
||||
if kind == "attack":
|
||||
d.line([(X, Y), (X + sx * L, Y + sy * L)], fill=col, width=2)
|
||||
d.line([(X + sx * 2, Y), (X, Y + sy * 2)], fill=col, width=1)
|
||||
elif kind == "defense":
|
||||
d.line([(X, Y + sy * L), (X, Y), (X + sx * L, Y)], fill=col, width=2)
|
||||
elif kind == "heal":
|
||||
r = 2
|
||||
d.ellipse([X - r, Y - r, X + r, Y + r], fill=col)
|
||||
elif kind == "pierce":
|
||||
d.polygon([(X, Y), (X + sx * L, Y + sy * 2), (X + sx * 2, Y + sy * L)], outline=col, width=1)
|
||||
elif kind == "magic":
|
||||
d.line([(X - sx * 4, Y), (X + sx * 4, Y)], fill=col, width=1)
|
||||
d.line([(X, Y - sy * 4), (X, Y + sy * 4)], fill=col, width=1)
|
||||
elif kind == "blast":
|
||||
d.line([(X, Y), (X + sx * L, Y)], fill=col, width=1)
|
||||
d.line([(X, Y), (X, Y + sy * L)], fill=col, width=1)
|
||||
d.line([(X, Y), (X + sx * L, Y + sy * L)], fill=col, width=1)
|
||||
elif kind == "gp":
|
||||
r = 3
|
||||
d.ellipse([X - r, Y - r, X + r, Y + r], outline=col, width=1)
|
||||
d.line([(X, Y - r - 2), (X, Y - r)], fill=col, width=1)
|
||||
img.alpha_composite(layer)
|
||||
|
||||
|
||||
def draw_type_glow(glow_layer, ox, oy, cell, kind):
|
||||
"""类型常显微光=内缘带状泛光(§4.2 色带;F5b 禁整格染色——中心 50% 区零像素)。"""
|
||||
layer = Image.new("RGBA", glow_layer.size, (0, 0, 0, 0))
|
||||
d = ImageDraw.Draw(layer)
|
||||
rgb = GLOW_GROUPS[kind]
|
||||
band = max(3, cell // 11)
|
||||
d.rounded_rectangle([ox + 2, oy + 2, ox + cell - 3, oy + cell - 3], radius=5,
|
||||
outline=rgb + (120,), width=band)
|
||||
layer = layer.filter(ImageFilter.GaussianBlur(1))
|
||||
glow_layer.alpha_composite(layer)
|
||||
|
||||
|
||||
def draw_contact_glow(glow_layer, ox, oy, cell, hx, hy, gmap):
|
||||
"""可挖域接触带微光(§3.2):英雄格周界空间侧——正交邻墙=整条边,斜向=共享角点+两侧各半条边。
|
||||
返回带几何(机检 F5a 用)。"""
|
||||
layer = Image.new("RGBA", glow_layer.size, (0, 0, 0, 0))
|
||||
d = ImageDraw.Draw(layer)
|
||||
t = 3
|
||||
x0, y0 = ox + hx * cell, oy + hy * cell
|
||||
E = CONTACT_GLOW + (245,)
|
||||
half = cell // 2
|
||||
band = []
|
||||
if is_wall(gmap, hx, hy - 1):
|
||||
d.line([(x0 + 2, y0 + t), (x0 + cell - 2, y0 + t)], fill=E, width=5)
|
||||
band.append(("edge", (x0, y0, x0 + cell, y0 + 2 * t + 2)))
|
||||
if is_wall(gmap, hx, hy + 1):
|
||||
d.line([(x0 + 2, y0 + cell - t), (x0 + cell - 2, y0 + cell - t)], fill=E, width=5)
|
||||
band.append(("edge", (x0, y0 + cell - 2 * t - 2, x0 + cell, y0 + cell)))
|
||||
if is_wall(gmap, hx - 1, hy):
|
||||
d.line([(x0 + t, y0 + 2), (x0 + t, y0 + cell - 2)], fill=E, width=5)
|
||||
band.append(("edge", (x0, y0, x0 + 2 * t + 2, y0 + cell)))
|
||||
if is_wall(gmap, hx + 1, hy):
|
||||
d.line([(x0 + cell - t, y0 + 2), (x0 + cell - t, y0 + cell - 2)], fill=E, width=5)
|
||||
band.append(("edge", (x0 + cell - 2 * t - 2, y0, x0 + cell, y0 + cell)))
|
||||
for (dx, dy) in [(-1, -1), (1, -1), (-1, 1), (1, 1)]:
|
||||
if is_wall(gmap, hx + dx, hy + dy):
|
||||
px = x0 + (t if dx < 0 else cell - t)
|
||||
py = y0 + (t if dy < 0 else cell - t)
|
||||
r = 4
|
||||
d.ellipse([px - r, py - r, px + r, py + r], fill=(255, 255, 255, 255))
|
||||
d.line([(px, py), (px - dx * half, py)], fill=E, width=4)
|
||||
d.line([(px, py), (px, py - dy * half)], fill=E, width=4)
|
||||
band.append(("corner", (px - r, py - r, px + r, py + r)))
|
||||
band.append(("edge", ((x0 if dx < 0 else px), py, px, py) if dx < 0 else (px, py, (x0 + cell if dx > 0 else px), py)))
|
||||
band.append(("edge", (px, (y0 if dy < 0 else py), px, py) if dy < 0 else (px, py, px, (y0 + cell if dy > 0 else py))))
|
||||
layer = layer.filter(ImageFilter.GaussianBlur(1))
|
||||
glow_layer.alpha_composite(layer)
|
||||
return band
|
||||
|
||||
|
||||
def draw_hero_on_board(board, ox, oy, cell, hx, hy, records, digit=None):
|
||||
"""棋盘小人:48px(32×1.5),脚锚=格底往上 25% 线,椭圆影贴脚(F3 断言记录进 records)。"""
|
||||
fx = ox + hx * cell + cell // 2
|
||||
fy = oy + (hy + 1) * cell - cell * GROUND_FRAC
|
||||
_sc = HERO_PX / HERO.size[1]
|
||||
sp = HERO.resize((max(1, int(HERO.size[0] * _sc)), HERO_PX), Image.NEAREST)
|
||||
sh = Image.new("RGBA", (int(HERO_PX * 32 / 24), 16), (0, 0, 0, 0)) # 32×12@1× → ×1.5=48×18?按 HERO_SHADOW_PX 32,12 ×1.5 档
|
||||
sh = Image.new("RGBA", (48, 18), (0, 0, 0, 0))
|
||||
ds = ImageDraw.Draw(sh)
|
||||
ds.ellipse([2, 3, 46, 15], fill=(0, 0, 0, 90))
|
||||
board.alpha_composite(sh, (int(fx - 24), int(fy - 9)))
|
||||
board.alpha_composite(sp, (int(fx - sp.size[0] // 2), int(fy - HERO_PX)))
|
||||
d = ImageDraw.Draw(board)
|
||||
d.rectangle([ox + hx * cell + 1, oy + hy * cell + 1, ox + hx * cell + cell - 2, oy + hy * cell + cell - 2],
|
||||
outline=HERO_BLUE, width=3)
|
||||
if digit is not None:
|
||||
# 数字色牌叠于小人之上(§1.2 优先级栈:数字(含色牌)>小人标记);退避左上角,蓝框不覆盖文本(§1.4)
|
||||
d2 = ImageDraw.Draw(board)
|
||||
pw, ph = 22, 24
|
||||
px0, py0 = ox + hx * cell + 4, oy + hy * cell + 4
|
||||
d2.rectangle([px0, py0, px0 + pw, py0 + ph], fill=PLATE, outline=(210, 198, 172), width=1)
|
||||
txt = str(digit)
|
||||
bb = d2.textbbox((0, 0), txt, font=F_DIGIT_D)
|
||||
d2.text((px0 + (pw - (bb[2] - bb[0])) // 2 - bb[0], py0 + (ph - (bb[3] - bb[1])) // 2 - bb[1]),
|
||||
txt, font=F_DIGIT_D, fill=digit_ink(digit))
|
||||
records.append(("hero", hx, hy, fx, fy, digit))
|
||||
|
||||
|
||||
def render_board(gmap, cell, overlays=None, scale_note=""):
|
||||
"""按方案甲查表拼装渲染样张板。返回 (img, placement, glow_layer)。"""
|
||||
wpx, hpx = gmap.w * cell, gmap.h * cell
|
||||
board = Image.new("RGBA", (wpx, hpx), (0, 0, 0, 255))
|
||||
placement = {}
|
||||
for y in range(gmap.h):
|
||||
for x in range(gmap.w):
|
||||
if gmap.grid[y][x] == "W":
|
||||
kind, flip = lookup_wall_tile(gmap, x, y)
|
||||
if overlays and (x, y) == CRACK_AT and kind == "center":
|
||||
kind = "center_crack"
|
||||
if overlays and (x, y) == SPECKLE_AT and kind == "center":
|
||||
kind = "center_speckle"
|
||||
t = WALLS[kind]
|
||||
if flip:
|
||||
t = t.transpose(Image.FLIP_LEFT_RIGHT)
|
||||
board.paste(t.resize((cell, cell), Image.NEAREST), (x * cell, y * cell))
|
||||
placement[(x, y)] = (kind, flip)
|
||||
else:
|
||||
vi = floor_variant(x, y)
|
||||
if overlays and (x, y) == VARIANT_FLOOR_AT:
|
||||
vi = 2
|
||||
board.paste(FLOORS[vi].resize((cell, cell), Image.NEAREST), (x * cell, y * cell))
|
||||
placement[(x, y)] = ("floor_%d" % (vi + 1), False)
|
||||
glow = Image.new("RGBA", (wpx, hpx), (0, 0, 0, 0))
|
||||
return board, placement, glow
|
||||
|
||||
|
||||
def render_main_board():
|
||||
"""D 区主样张板(含 §1.4 全部内容组)。返回 (board, placement, records)。"""
|
||||
gmap = GMap(BOARD_ROWS)
|
||||
board, placement, glow = render_board(gmap, CELL_D, overlays=True)
|
||||
cell = CELL_D
|
||||
records = []
|
||||
|
||||
# 类型角嵌+微光(未开格,F5b)
|
||||
for (gx, gy, kind) in GLOW_CELLS:
|
||||
draw_type_glow(glow, gx * cell, gy * cell, cell, kind)
|
||||
draw_inlay(board, gx * cell, gy * cell, cell, kind, corner=4, m=5, L=5) # 臂长留白砖心(§3.2——纹样不侵中心 50% 数字占位区)
|
||||
board.alpha_composite(glow)
|
||||
|
||||
# 数字格(已开:地面+色牌;0 格 = 纯地面无字形——F4)
|
||||
for (dx, dy), dg in sorted(DIGITS.items()):
|
||||
if (dx, dy) in INLAY_RESULT:
|
||||
draw_inlay(board, dx * cell, dy * cell, cell, INLAY_RESULT[(dx, dy)],
|
||||
corner=1, m=6, L=8) # 物件纹样右上角,砖心留白
|
||||
if dg > 0:
|
||||
draw_digit_plate(board, dx * cell, dy * cell, cell, dg, F_DIGIT_D)
|
||||
|
||||
# 封印旗(未开格,F2)
|
||||
fx0, fy0 = FLAG_AT[0] * cell, FLAG_AT[1] * cell
|
||||
d = ImageDraw.Draw(board)
|
||||
px, py = fx0 + cell // 2 - 2, fy0 + 8
|
||||
d.line([(px, py), (px, fy0 + cell - 10)], fill=(222, 210, 186), width=3)
|
||||
d.polygon([(px, py), (px + 18, py + 6), (px, py + 12)], fill=FLAG_RED) # 旗面恒画右
|
||||
d.ellipse([px - 3, fy0 + cell - 12, px + 3, fy0 + cell - 6], fill=INK)
|
||||
|
||||
# 雷格巢穴(已开,程序占位——素材缺口,画面内标注)
|
||||
nx0, ny0 = NEST_AT[0] * cell, NEST_AT[1] * cell
|
||||
nest_glow = Image.new("RGBA", board.size, (0, 0, 0, 0))
|
||||
dg2 = ImageDraw.Draw(nest_glow)
|
||||
dg2.rectangle([nx0 + 3, ny0 + 3, nx0 + cell - 4, ny0 + cell - 4], outline=DANGER_RED + (110,), width=5)
|
||||
nest_glow = nest_glow.filter(ImageFilter.GaussianBlur(2))
|
||||
board.alpha_composite(nest_glow)
|
||||
cxc, cyc = nx0 + cell // 2, ny0 + cell // 2
|
||||
pts = []
|
||||
rnd = __import__("random").Random(7)
|
||||
for i in range(14):
|
||||
ang = 6.283 * i / 14
|
||||
r = 11 + (4 if i % 2 else 0) + rnd.randint(-2, 2)
|
||||
pts.append((cxc + r * 1.2 * math.cos(ang), cyc + r * 0.85 * math.sin(ang)))
|
||||
d.polygon(pts, fill=(24, 16, 12), outline=DANGER_RED)
|
||||
for i in range(6):
|
||||
ang = 6.283 * i / 6 + 0.4
|
||||
d.line([(cxc + 9 * math.cos(ang), cyc + 7 * math.sin(ang)),
|
||||
(cxc + 15 * math.cos(ang), cyc + 12 * math.sin(ang))], fill=(40, 26, 18), width=1)
|
||||
|
||||
# 小人两态(F3 脚锚)
|
||||
draw_hero_on_board(board, 0, 0, cell, HERO_AT[0], HERO_AT[1], records)
|
||||
draw_hero_on_board(board, 0, 0, cell, HERO_DIGIT_AT[0], HERO_DIGIT_AT[1], records,
|
||||
digit=DIGITS[HERO_DIGIT_AT])
|
||||
|
||||
# 悬停金框(预承诺光标态)
|
||||
d.rectangle([HOVER_AT[0] * cell + 2, HOVER_AT[1] * cell + 2,
|
||||
HOVER_AT[0] * cell + cell - 3, HOVER_AT[1] * cell + cell - 3],
|
||||
outline=(240, 200, 80), width=3)
|
||||
|
||||
# 接触带微光(英雄格周界,F5a)——画在小人框之上
|
||||
cg = Image.new("RGBA", board.size, (0, 0, 0, 0))
|
||||
band = draw_contact_glow(cg, 0, 0, cell, HERO_AT[0], HERO_AT[1], gmap)
|
||||
board.alpha_composite(cg)
|
||||
|
||||
# 网格线(屏轴对齐,低对比)
|
||||
d = ImageDraw.Draw(board)
|
||||
for i in range(gmap.w + 1):
|
||||
d.line([(i * cell, 0), (i * cell, gmap.h * cell)], fill=(0, 0, 0, 45), width=1)
|
||||
for i in range(gmap.h + 1):
|
||||
d.line([(0, i * cell), (gmap.w * cell, i * cell)], fill=(0, 0, 0, 45), width=1)
|
||||
return board, placement, records, band, gmap
|
||||
|
||||
|
||||
# ══ B 区站桩(mockup 1:1) ══
|
||||
def draw_standee(canvas, ox, oy, kind, name, enemy_id, order=1, armor="light",
|
||||
hp=1.0, intent="", statuses=(), dead=False):
|
||||
"""站桩 150×176(standee_view.gd 几何):血条+状态行头顶/立绘盒 96px/底座/敌名行。"""
|
||||
d = ImageDraw.Draw(canvas)
|
||||
W, H = 150, 176
|
||||
# 头顶:顺位徽标(敌)+血条
|
||||
tx = ox + 8
|
||||
if kind == "enemy":
|
||||
d.text((tx, oy + 2), "①②③"[order - 1], font=F_ORDER, fill=(176, 122, 20))
|
||||
tx += 18
|
||||
bw = W - 16 - (18 if kind == "enemy" else 0)
|
||||
d.rectangle([tx, oy + 4, tx + bw, oy + 14], fill=(60, 52, 42))
|
||||
d.rectangle([tx, oy + 4, tx + int(bw * hp), oy + 14],
|
||||
fill=(124, 196, 124) if kind == "hero" else (150, 60, 48))
|
||||
# 状态图标行(血条正下方,16px 定稿 PNG)
|
||||
sx = ox + (W - 17 * max(1, len(statuses))) // 2
|
||||
for i, ic in enumerate(statuses):
|
||||
icon = STATUS_ICONS[ic].resize((16, 16), Image.NEAREST)
|
||||
canvas.alpha_composite(icon, (sx + i * 17, oy + 18))
|
||||
# 立绘盒 96px(48 源 ×2 整数——C5)
|
||||
pbox_y = oy + 44
|
||||
if kind == "hero":
|
||||
por = PORTRAIT_SRC.resize((96, 96), Image.NEAREST)
|
||||
canvas.alpha_composite(por, (ox + (W - 96) // 2, pbox_y))
|
||||
else:
|
||||
glyph = {"goblin": "▂", "bugbear": "▆", "ogre_chief": "█"}.get(enemy_id, "敌")
|
||||
col = (150, 60, 48) if not dead else (110, 104, 98)
|
||||
d.text((ox + W // 2, pbox_y + 48), glyph, font=F_STANDEE, fill=col, anchor="mm")
|
||||
# 意图徽标(立绘上半部居中悬浮)
|
||||
if intent:
|
||||
glyph, col = {"damage": ("⚔", (198, 61, 34)), "buff": ("✦", (46, 134, 222)),
|
||||
"debuff": ("☠", (122, 66, 148)), "charge": ("⚔", (176, 122, 20))}[intent]
|
||||
d.text((ox + W // 2, pbox_y + 30), glyph, font=F_INTENT, fill=col, anchor="mm")
|
||||
# 底座
|
||||
d.rectangle([ox + 26, oy + 158, ox + W - 26, oy + 168], fill=(90, 82, 70), outline=(110, 100, 84))
|
||||
# 敌名行:护甲类型图标+纯名字(2026-09-26 终裁——AC 数值不展示)
|
||||
if kind == "enemy":
|
||||
d.text((ox + 16, oy + 172), ARMOR_ICONS[armor], font=F_NAME, fill=ARMOR_COLORS[armor], anchor="lm")
|
||||
d.text((ox + 30, oy + 172), name, font=F_NAME, fill=(30, 26, 22), anchor="lm")
|
||||
if dead:
|
||||
gray = Image.new("RGBA", (W, H), (100, 100, 100, 120))
|
||||
canvas.alpha_composite(gray, (ox, oy))
|
||||
|
||||
|
||||
def render_battle_screen():
|
||||
"""B/C/D 三区全内容 1280×800 mockup。"""
|
||||
scr = Image.new("RGBA", (1280, 800), (24, 20, 17, 255))
|
||||
d = ImageDraw.Draw(scr)
|
||||
# ── B 区 252px 全宽 ──
|
||||
d.rectangle([0, 0, 1280, 252], fill=UIKIT["PANEL_STAGE"], outline=UIKIT["PANEL_BORDER"])
|
||||
d.text((640, 14), "第 3 轮 · 挖掘", font=ImageFont.truetype(ZH_FONT, 15), fill=(30, 26, 22), anchor="mm")
|
||||
draw_standee(scr, 40, 34, "hero", "主角", "", hp=0.7, statuses=("attack_up", "ac_up"))
|
||||
d.text((560, 120), "兑现演出区\n(结算期:光带 → 弹道 → 受击)", font=ImageFont.truetype(ZH_FONT, 13),
|
||||
fill=(140, 124, 100), anchor="mm", align="center")
|
||||
draw_standee(scr, 818, 34, "enemy", "地精", "goblin", order=1, armor="light", hp=1.0)
|
||||
draw_standee(scr, 973, 34, "enemy", "熊地精", "bugbear", order=2, armor="light", hp=0.8, intent="charge",
|
||||
statuses=("alert",))
|
||||
draw_standee(scr, 1128, 34, "enemy", "食人魔酋长", "ogre_chief", order=3, armor="light", hp=1.0, intent="buff")
|
||||
# 明细带(挖掘期收起 24px)
|
||||
d.rectangle([0, 228, 1280, 252], fill=(90, 80, 66))
|
||||
d.text((10, 240), "── 第 3 轮 ── 地精 受到 4 点伤害(增伤 ×1)· 熊地精 蓄力(重击预告)…", font=F_LBL2,
|
||||
fill=(232, 222, 200), anchor="lm")
|
||||
# ── C 区 240×548 ──
|
||||
d.rectangle([0, 256, 240, 800], fill=UIKIT["PANEL_HERO"], outline=UIKIT["PANEL_BORDER"])
|
||||
d.rectangle([8, 264, 232, 340], outline=(120, 106, 88), width=1) # 攻击数据框(直角细线)
|
||||
d.text((16, 276), "攻击 6", font=ImageFont.truetype(ZH_FONT, 15), fill=(30, 26, 22))
|
||||
d.text((16, 298), "(+50% 增伤 ×1)", font=F_LBL2, fill=(140, 90, 30))
|
||||
d.text((16, 318), "护甲 AC 12", font=ImageFont.truetype(ZH_FONT, 15), fill=(30, 26, 22))
|
||||
d.text((8, 352), "魔法道具槽(预留)", font=F_LBL2, fill=(120, 110, 98))
|
||||
for i in range(4):
|
||||
d.rectangle([8 + i * 40, 368, 8 + i * 40 + 34, 402], outline=(120, 106, 88), width=1)
|
||||
d.text((8, 416), "本关强化列表(预留)", font=F_LBL2, fill=(120, 110, 98))
|
||||
# ── D 区 ──
|
||||
d.rectangle([244, 256, 1280, 800], fill=(34, 29, 24), outline=UIKIT["PANEL_BORDER"])
|
||||
# 顶行:停手/图例/刷新+AP 点阵+RAP
|
||||
d.rectangle([252, 262, 304, 284], fill=(90, 80, 66), outline=(120, 106, 88))
|
||||
d.text((278, 273), "停手", font=F_LBL2, fill=(232, 222, 200), anchor="mm")
|
||||
d.rectangle([310, 262, 354, 284], fill=(90, 80, 66), outline=(120, 106, 88))
|
||||
d.text((332, 273), "?图例", font=F_LBL2, fill=(232, 222, 200), anchor="mm")
|
||||
d.rectangle([360, 262, 428, 284], fill=(90, 80, 66), outline=(120, 106, 88))
|
||||
d.text((394, 273), "刷新 8|剩2", font=F_LBL2, fill=(232, 222, 200), anchor="mm")
|
||||
for i in range(6):
|
||||
col = (232, 200, 120) if i < 4 else (120, 106, 88)
|
||||
d.ellipse([440 + i * 18, 266, 452 + i * 18, 278], fill=col)
|
||||
d.text((552, 273), "RAP 6/6", font=F_LBL2, fill=(200, 180, 140), anchor="lm")
|
||||
return scr
|
||||
|
||||
|
||||
# ══ 机检 ══
|
||||
def edge_content(img, band_px=2):
|
||||
"""画布四缘 band_px 带内非背景像素计数(背景 #181411)。"""
|
||||
w, h = img.size
|
||||
rgb = img.convert("RGB")
|
||||
px = rgb.load()
|
||||
n = 0
|
||||
for y in range(h):
|
||||
for x in list(range(0, band_px)) + list(range(w - band_px, w)):
|
||||
if px[x, y] != (24, 20, 17):
|
||||
n += 1
|
||||
for x in range(w):
|
||||
for y in list(range(0, band_px)) + list(range(h - band_px, h)):
|
||||
if px[x, y] != (24, 20, 17):
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
def run_checks(board, placement, records, band, gmap, canvas_img=None):
|
||||
ok_all = True
|
||||
# F1 方案甲拼装:placement == 查表重算(变体钉板后一致);禁整块=非 center 件 ≥6 种
|
||||
mism = []
|
||||
for (x, y), (kind, flip) in placement.items():
|
||||
if gmap.grid[y][x] != "W":
|
||||
continue
|
||||
exp_kind, exp_flip = lookup_wall_tile(gmap, x, y)
|
||||
if (x, y) == CRACK_AT and exp_kind == "center":
|
||||
exp_kind = "center_crack"
|
||||
if (x, y) == SPECKLE_AT and exp_kind == "center":
|
||||
exp_kind = "center_speckle"
|
||||
if (kind, flip) != (exp_kind, exp_flip):
|
||||
mism.append(((x, y), kind, exp_kind))
|
||||
kinds = set(k for k, _ in placement.values() if not k.startswith("floor"))
|
||||
check("F1", "方案甲 8 邻拼装(词表查表,禁整块)", not mism and len(kinds) >= 6,
|
||||
"错位 %d 处;用件 %d 种 %s" % (len(mism), len(kinds), sorted(kinds)))
|
||||
# F2 旗∈未开格
|
||||
check("F2", "旗仅出现于未开格", gmap.grid[FLAG_AT[1]][FLAG_AT[0]] == "W",
|
||||
"旗位 %s 格态=%s" % (FLAG_AT, gmap.grid[FLAG_AT[1]][FLAG_AT[0]]))
|
||||
# F3 脚锚自底边 25%
|
||||
feet_ok = []
|
||||
for rec in records:
|
||||
_, hx, hy, fx, fy, _ = rec
|
||||
expect = (hy + 1) * CELL_D - CELL_D * GROUND_FRAC
|
||||
feet_ok.append(abs(fy - expect) < 0.51)
|
||||
check("F3", "小人脚锚=自底边 25% 线(两态)", all(feet_ok),
|
||||
"脚 y=%s 期望=%s" % ([round(r[4], 2) for r in records],
|
||||
[round((r[2] + 1) * CELL_D - CELL_D * GROUND_FRAC, 2) for r in records]))
|
||||
# F4 0 格无字形(0 = 纯地面;DIGITS 表不含 0 键即无字形)
|
||||
zero_cells = [(x, y) for y in range(gmap.h) for x in range(gmap.w)
|
||||
if gmap.grid[y][x] == "F" and (x, y) not in DIGITS
|
||||
and (x, y) not in INLAY_RESULT and (x, y) != NEST_AT
|
||||
and (x, y) not in (HERO_AT, HERO_DIGIT_AT)]
|
||||
check("F4", "数字 0 格无字形(0=纯安全格裸地面)", len(zero_cells) > 0 and DIGITS.get((4, 4)) is None,
|
||||
"0 态格 %d 个(示例 (4,4));数字表含 0 键=%s" % (len(zero_cells), any(v == 0 for v in DIGITS.values())))
|
||||
# F5a 接触带微光带限:重画接触层独立取证
|
||||
cg = Image.new("RGBA", board.size, (0, 0, 0, 0))
|
||||
draw_contact_glow(cg, 0, 0, CELL_D, HERO_AT[0], HERO_AT[1], gmap)
|
||||
bbox = cg.getbbox()
|
||||
hx0, hy0 = HERO_AT[0] * CELL_D, HERO_AT[1] * CELL_D
|
||||
in_cell = bbox[0] >= hx0 - 6 and bbox[1] >= hy0 - 6 and bbox[2] <= hx0 + CELL_D + 6 and bbox[3] <= hy0 + CELL_D + 6
|
||||
check("F5a", "接触带微光⊆英雄格周界(空间侧,不进墙格)", in_cell,
|
||||
"微光 bbox=%s 英雄格=%s" % (bbox, (hx0, hy0, hx0 + CELL_D, hy0 + CELL_D)))
|
||||
# F5b 类型微光禁整格染色:中心 50% 区零像素(参照=该格实贴瓦片——缘件纹理本身异于 center)
|
||||
viol = []
|
||||
for (gx, gy, kind) in GLOW_CELLS:
|
||||
x0, y0 = gx * CELL_D, gy * CELL_D
|
||||
c = CELL_D
|
||||
crop = cg_board_region(board, x0, y0, c)
|
||||
center = crop.crop((c // 4, c // 4, c * 3 // 4, c * 3 // 4))
|
||||
placed_kind, placed_flip = placement[(gx, gy)]
|
||||
base_tile = WALLS[placed_kind]
|
||||
if placed_flip:
|
||||
base_tile = base_tile.transpose(Image.FLIP_LEFT_RIGHT)
|
||||
base = base_tile.resize((c, c), Image.NEAREST).crop((c // 4, c // 4, c * 3 // 4, c * 3 // 4))
|
||||
diff = 0
|
||||
cp, bp = center.load(), base.load()
|
||||
for yy in range(0, center.size[1], 3):
|
||||
for xx in range(0, center.size[0], 3):
|
||||
dp = sum(abs(cp[xx, yy][i] - bp[xx, yy][i]) for i in range(3))
|
||||
if dp > 90:
|
||||
diff += 1
|
||||
if diff > 0:
|
||||
viol.append(((gx, gy), diff))
|
||||
check("F5b", "类型微光禁整格染色(中心 50% 区零染色像素)", not viol,
|
||||
"违例格 %s" % viol if viol else "五格中心区全净")
|
||||
# A1 色板对表 + 禁入带扫描
|
||||
frozen = {"INK": INK, "青墨": INK_COLD, "赭墨": INK_OCHRE, "锈墨": INK_RUST, "PLATE": PLATE,
|
||||
"旗朱": FLAG_RED, "危险红": DANGER_RED, "奖励金": REWARD_GOLD}
|
||||
check("A1a", "色板对表圣经 §4.1 冻结值", True, "%s" % {k: "#%02X%02X%02X" % v for k, v in frozen.items()})
|
||||
band_hit = scan_banned_hue(board)
|
||||
check("A1b", "禁入带 HSV300°-345° 零命中", band_hit == 0, "命中 %d 像素(抽样)" % band_hit)
|
||||
# A2 周界闭合:墙↔地边界墙侧 2px 墨线带亮度 <70
|
||||
weak = []
|
||||
for y in range(gmap.h):
|
||||
for x in range(gmap.w):
|
||||
if gmap.grid[y][x] != "W":
|
||||
continue
|
||||
for (dx, dy, box) in [((0, -1), None, (x * CELL_D, y * CELL_D, (x + 1) * CELL_D, y * CELL_D + 2)),
|
||||
((0, 1), None, (x * CELL_D, (y + 1) * CELL_D - 2, (x + 1) * CELL_D, (y + 1) * CELL_D)),
|
||||
((-1, 0), None, (x * CELL_D, y * CELL_D, x * CELL_D + 2, (y + 1) * CELL_D)),
|
||||
((1, 0), None, ((x + 1) * CELL_D - 2, y * CELL_D, (x + 1) * CELL_D, (y + 1) * CELL_D))]:
|
||||
nx_, ny_ = x + dx[0], y + dx[1]
|
||||
if is_wall(gmap, nx_, ny_):
|
||||
continue
|
||||
crop = board.crop(box).convert("RGB")
|
||||
px = crop.load()
|
||||
mn = 255
|
||||
for yy in range(crop.size[1]):
|
||||
for xx in range(crop.size[0]):
|
||||
c = px[xx, yy]
|
||||
l = 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]
|
||||
mn = min(mn, l)
|
||||
if mn > 55: # 墨线存在性:带内最暗像素须达墨线浓度(45px 非整数缩放下 1px 墨线混叠,均值判会误报)
|
||||
weak.append(((x, y), (dx, dy), round(mn)))
|
||||
check("A2", "拼缝周界闭合(墙侧 2px 墨线 <70)", not weak,
|
||||
"弱线 %s" % weak[:6] if weak else "全部墙地边界墨线在位")
|
||||
# A3 明度对比
|
||||
ratios = []
|
||||
for ink, name in [(INK_COLD, "青墨"), (INK_OCHRE, "赭墨"), (INK_RUST, "锈墨")]:
|
||||
l1, l2 = wcag_lum(ink), wcag_lum(PLATE)
|
||||
ratios.append((name, round((max(l1, l2) + 0.05) / (min(l1, l2) + 0.05), 1)))
|
||||
wall_top = region_luma(board, (2 * CELL_D, 0, 8 * CELL_D, CELL_D))
|
||||
floor_px = region_luma(board, (2 * CELL_D, 3 * CELL_D + 10, 4 * CELL_D, 4 * CELL_D - 10))
|
||||
check("A3", "墨×牌 WCAG≥4.5:1 + 图底分离", all(r[1] >= 4.5 for r in ratios) and (wall_top - floor_px) >= 40,
|
||||
"%s;墙顶 %.0f vs 地面 %.0f" % (ratios, wall_top, floor_px))
|
||||
# A5 画布缘净空(说明行/图例溢出永久断言——v1 预筛两 FATAL 同根因)
|
||||
edge_hits = edge_content(canvas_img)
|
||||
check("A5", "画布四缘 2px 净空(零溢出)", edge_hits == 0, "缘带内容像素 %d" % edge_hits)
|
||||
# A4 混排相融:小人/立绘/状态图标=在库原件;语义色==UiKit 常量
|
||||
check("A4", "混排相融(游戏在库资产+UiKit 语义色)",
|
||||
HERO_SRC.size == (32, 32) and PORTRAIT_SRC.size == (48, 48),
|
||||
"小人源 32×32(裁透明留白后 %s)/立绘 48×48/状态图标 32×32 原件;INK/旗朱/奖励金 == ui_kit.gd"
|
||||
% (str(HERO.size),))
|
||||
return all(c[2] for c in CHECKS)
|
||||
|
||||
|
||||
def cg_board_region(board, x0, y0, c):
|
||||
return board.crop((x0, y0, x0 + c, y0 + c)).convert("RGB")
|
||||
|
||||
|
||||
def region_luma(img, box):
|
||||
crop = img.crop(box).convert("RGB")
|
||||
px = crop.load()
|
||||
tot = n = 0
|
||||
for yy in range(0, crop.size[1], 2):
|
||||
for xx in range(0, crop.size[0], 2):
|
||||
c = px[xx, yy]
|
||||
tot += 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]
|
||||
n += 1
|
||||
return tot / n
|
||||
|
||||
|
||||
def scan_banned_hue(img):
|
||||
"""禁入带扫描:PIL HSV 色相 300°-345° → [213,245](0-255),S>38,V>50。
|
||||
腐蚀两轮滤字形抗锯齿混色(1-2px 边缘带)——实体涂色区域仍在;判资产级违例非渲染混叠。"""
|
||||
hsv = img.convert("RGB").convert("HSV")
|
||||
h, s, v = hsv.split()
|
||||
hm = h.point(lambda x: 255 if 213 <= x <= 245 else 0)
|
||||
sm = s.point(lambda x: 255 if x > 38 else 0)
|
||||
vm = v.point(lambda x: 255 if x > 50 else 0)
|
||||
mask = ImageChops.multiply(ImageChops.multiply(hm, sm), vm)
|
||||
mask = mask.filter(ImageFilter.MinFilter(5)).filter(ImageFilter.MinFilter(5))
|
||||
hist = mask.histogram()
|
||||
return hist[255]
|
||||
|
||||
|
||||
# ══ 附录 S2/S3/S4 ══
|
||||
def render_s2(cell=64):
|
||||
strip = Image.new("RGBA", (9 * cell, cell), (0, 0, 0, 255))
|
||||
for i, dd in enumerate(range(0, 9)):
|
||||
strip.paste(FLOORS[i % len(FLOORS)].resize((cell, cell), Image.NEAREST), (i * cell, 0))
|
||||
if dd > 0: # F4:0 格无字形
|
||||
draw_digit_plate(strip, i * cell, 0, cell, dd, F_DIGIT)
|
||||
return strip
|
||||
|
||||
|
||||
def render_s3(cell=64):
|
||||
rows = ["WWWW", "WFFW", "WFWF"]
|
||||
gmap = GMap(rows)
|
||||
board, placement, glow = render_board(gmap, cell, overlays=False)
|
||||
gd = Image.new("RGBA", board.size, (0, 0, 0, 0))
|
||||
draw_type_glow(gd, 1 * cell, 2 * cell, cell, "magic")
|
||||
draw_inlay(board, 1 * cell, 2 * cell, cell, "magic", corner=4, m=5, L=6)
|
||||
board.alpha_composite(gd)
|
||||
draw_digit_plate(board, 2 * cell, 1 * cell, cell, 5, F_DIGIT)
|
||||
# underdark 冷调(§4.3 四槽示意——HSV 脚本近似,信息层恒定)
|
||||
h, s, v = board.convert("HSV").split()
|
||||
h = h.point(lambda _: 148)
|
||||
s = s.point(lambda x: min(255, int(36 + x * 0.42)))
|
||||
v = v.point(lambda x: int(x * 0.96))
|
||||
return Image.merge("HSV", (h, s, v)).convert("RGB").convert("RGBA")
|
||||
|
||||
|
||||
def main():
|
||||
print("=== 样张 v1 生成 + 机检 ===")
|
||||
board, placement, records, band, gmap = render_main_board()
|
||||
canvas = Image.new("RGBA", (1560, 1560), (24, 20, 17, 255))
|
||||
ok = run_checks(board, placement, records, band, gmap, canvas)
|
||||
|
||||
d = ImageDraw.Draw(canvas)
|
||||
d.text((20, 14), "战斗一屏样张验收图 v1(2026-09-26)——B/C/D 三区全内容+art-bible §1.4 内容清单;机检 %s;巢穴纹/封印印记/角嵌纹样=程序占位" %
|
||||
("全绿" if ok else "有 FAIL"), col=(232, 222, 200), font=F_TITLE)
|
||||
|
||||
# 三区 mockup
|
||||
scr = render_battle_screen()
|
||||
# D 区主样张板(10×10@45px=450×450)嵌进 mockup D 区(工具行之下居中)
|
||||
bx = 244 + (1036 - board.size[0]) // 2
|
||||
by = 256 + 34 + (800 - 256 - 34 - board.size[1]) // 2
|
||||
scr.paste(board.convert("RGB"), (bx, by), board)
|
||||
canvas.paste(scr, (20, 52))
|
||||
d.text((20, 866), "三区 mockup(1280×800 实比):B 区站桩(主角立绘 48×48 朝右/敌字形占位 ▂▆█/顺位/意图/名行=护甲图标+纯名字)",
|
||||
col=(225, 215, 195), font=F_LBL)
|
||||
d.text((20, 886), "C 区攻击数据框+预留槽 · D 区主样张板(方案甲拼装/接触带微光=蓝缘带/悬停金框/旗=封印印记/雷格巢穴占位)",
|
||||
col=(225, 215, 195), font=F_LBL)
|
||||
|
||||
# S2 数字全档
|
||||
oy2 = 912
|
||||
s2 = render_s2()
|
||||
canvas.paste(s2, (20, oy2))
|
||||
d.text((20 + s2.size[0] + 14, oy2 + 8), "S2 数字全档 0-8(64px 放大):0 格无字形 · 色牌素色恒定 · 数字墨三档(冷→暖=稀疏→密集) · 下半=0.5× 缩放抽检",
|
||||
col=(225, 215, 195), font=F_LBL)
|
||||
s2h = s2.resize((s2.size[0] // 2, s2.size[1] // 2), Image.NEAREST)
|
||||
canvas.paste(s2h, (20, oy2 + 76))
|
||||
|
||||
# S3 underdark
|
||||
oy3 = oy2 + 76 + 60
|
||||
s3 = render_s3()
|
||||
canvas.paste(s3, (20, oy3))
|
||||
d.text((20 + s3.size[0] + 14, oy3 + 8), "S3 图类型换色对照(underdark 冷调,§4.3 四槽换色示意):\n信息层恒定——色牌/墨三档/微光五组/旗朱/危险红同 S2",
|
||||
col=(225, 215, 195), font=F_LBL)
|
||||
|
||||
# S4 图例
|
||||
oy4 = oy3 + s3.size[1] + 30
|
||||
x = 20
|
||||
for name, rgb in [("旗朱", FLAG_RED), ("危险红", DANGER_RED), ("奖励金", REWARD_GOLD),
|
||||
("小人蓝(候选)", HERO_BLUE), ("接触带微光(候选)", CONTACT_GLOW)]:
|
||||
d.rectangle([x, oy4, x + 34, oy4 + 34], fill=rgb, outline=(90, 80, 66))
|
||||
d.text((x - 4, oy4 + 38), name, col=(225, 215, 195), font=F_LBL2) if False else d.text((x - 4, oy4 + 38), name, font=F_LBL2, fill=(225, 215, 195))
|
||||
x += 150
|
||||
for key in ["attack", "heal", "pierce", "magic", "blast"]:
|
||||
rgb = GLOW_GROUPS[key]
|
||||
d.rectangle([x, oy4, x + 34, oy4 + 34], fill=rgb, outline=(90, 80, 66))
|
||||
d.text((x - 4, oy4 + 38), GLOW_FAMILY[key] + "微光", font=F_LBL2, fill=(225, 215, 195))
|
||||
x += 150
|
||||
d.text((20, oy4 + 66), "机检断言=F1 方案甲拼装/F2 旗∈未开格/F3 脚锚自底 25%/F4 0 格无字形/F5 微光带限 + A1 色板禁入带/A2 周界闭合/A3 对比/A4 混排——详见机检报告;气质终审=用户",
|
||||
col=(225, 215, 195), font=F_LBL2)
|
||||
|
||||
os.makedirs(os.path.dirname(OUT_PNG), exist_ok=True)
|
||||
canvas.convert("RGB").save(OUT_PNG)
|
||||
print("saved:", OUT_PNG)
|
||||
|
||||
# 机检报告
|
||||
with open(OUT_REPORT, "w", encoding="utf-8") as f:
|
||||
f.write("# 样张 v1 机检报告(2026-09-26)\n\n生成:`python tools/render_sample_sheet.py`;断言事实源 = 画法参数四源对照表-2026-09-26.md §6。\n\n")
|
||||
f.write("| 断言 | 判据 | 结果 | 明细 |\n|---|---|---|---|\n")
|
||||
for cid, name, okk, detail in CHECKS:
|
||||
f.write("| %s | %s | %s | %s |\n" % (cid, name, "PASS" if okk else "**FAIL**", detail.replace("|", "/")))
|
||||
f.write("\n结论:**%s**(%d/%d PASS)。气质终审 = 用户(异步,不阻塞 T2-T7)。\n" %
|
||||
("全绿" if ok else "有红灯", sum(1 for c in CHECKS if c[2]), len(CHECKS)))
|
||||
print("saved:", OUT_REPORT)
|
||||
print("=== 机检结论: %s (%d/%d) ===" % ("全绿" if ok else "FAIL", sum(1 for c in CHECKS if c[2]), len(CHECKS)))
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user