diff --git a/game/presentation/views/board_cell_unit.gd b/game/presentation/views/board_cell_unit.gd index 7feda330..87a45e23 100644 --- a/game/presentation/views/board_cell_unit.gd +++ b/game/presentation/views/board_cell_unit.gd @@ -1,68 +1,13 @@ ## VERTICAL SLICE - NOT FOR PRODUCTION ## CellUnit——BoardView SubViewport 世界内逐格渲染件(Node2D),全部程序化 _draw 占位视觉(纹理槽位已留未装载)。 ## 无 class_name(类名缓存面纪律)——使用方经 preload 引用。 +## 内部状态件(u3 子域拆件——契约 9 职责语句「单格绘制输入集」):本件为被动绘制壳, +## 状态件 _CellPaintState 承载全部绘制入参(单一变化原因=格面呈现),setter 触发宿主重绘语义 +## 经 _owner 回引保持逐字;探针直读冻结成员(glyph/fog_state/plate/glyph_color)与 _glow_layer 住宿主。 extends Node2D const _GLOW_LAYER := preload("res://presentation/views/cell_type_glow_layer.gd") -var cell := Vector2i.ZERO # 板坐标(诊断观测面) -var edge := 46.0: # 格边长(BoardView.set_cell_area 单点驱动) - set(v): - edge = v - if _glow_layer != null and is_instance_valid(_glow_layer): - _glow_layer.setup(edge, glow_group) - queue_redraw() -var bg_sb: StyleBoxFlat = null # 格底 StyleBox(BoardView 每次 paint 重建;呼吸微光就地改边框) -# ── 接触带微光绘制面(可挖域微光画在英雄格周界空间侧——art-bible §3.2 v2.8; -# 仅英雄格单元承载,BoardView.refresh 按可挖域写边/角码、_process 逐拍更新呼吸色)── -var contact_bands: Array[int] = [] # 正交接触边码 0=N/1=S/2=W/3=E(该侧整条边) -var contact_corners: Array[int] = [] # 斜向共享角码 0=NW/1=NE/2=SW/3=SE(角点+两侧各半条边) -var contact_col := Color(0, 0, 0, 0) # 当前呼吸色(alpha 0 = 不画) -var plate := false # 数字色牌开关(BoardView paint 按已开格 glyph 写入——原则二) -var outline_sides: Array[int] = [] # 墙格地面朝向描边码 0=N/1=S/2=W/3=E(程序侧结构件) -# ── 高墙感 D 投影档(研究件四变体定 D=C+地面投影,ITB 同款高度线索)── -# 南立面 = 墙格南缘内侧「口沿高光线 + 双段暗带」(邻地面格才画,墙群内聚);地面投影 = 北邻墙格 -# 在本已开格北缘投渐隐暗带。参数按研究件 64px 拼版实测比例换算(立面双段 0.16/0.25 格边、投影带 -# 0.22 格边;坐标推导留档 commit)。素材缺位程序化回退同样服役(结构归程序——art-bible §8.5)。 -var facade_south := false: # 南立面开关(BoardView paint 按「南邻已开格」写入) - set(v): - facade_south = v - queue_redraw() -var proj_band := 0.0: # 地面投影带宽(像素,0 = 不画——BoardView 按北邻墙判定写入) - set(v): - proj_band = v - queue_redraw() -var glyph := "": # 文字位——数字/雷/封印/人/?(现行视觉封印与数字互斥,同位承载) - set(v): - glyph = v - queue_redraw() -var glyph_color := UiKit.INK: # 文字色(墨族/雷朱/封印黑/小人蓝) - set(v): - glyph_color = v - queue_redraw() -var badge_glyph := "": # 奖励徽章文字(右下角金——封印之下、变异纹之上、不进数字区) - set(v): - badge_glyph = v - if _glow_layer != null and is_instance_valid(_glow_layer): - _glow_layer.set_badge_mask(v != "") - queue_redraw() -var dot_visible := false: # BFS 路径预览点迹(中心 6×6 小人蓝) - set(v): - dot_visible = v - queue_redraw() -var hl_visible := false: # 焦点高亮(悬停态边框覆盖层) - set(v): - hl_visible = v - queue_redraw() -var hl_color := UiKit.GOLD: # 高亮色(金目标/封印朱/禁入灰禁红) - set(v): - hl_color = v - queue_redraw() -var font: Font = null # 字体槽位(null = ThemeDB.fallback_font——BoardView.set_world_font 注入) -var bevel := 0.0: # 顶面倒角强度(0=关;档位 0/0.35/0.6/1.0) - set(v): - bevel = v - queue_redraw() # ── D 投影档绘制常量(研究件实测取色——立面高光/双段暗带/地面投影;黑罩 alpha 叠加实测等效)── const FACADE_HIGHLIGHT_PX := 2.0 # 口沿高光线厚(像素) const FACADE_NEAR_FRAC := 0.16 # 立面近段高(格边比——研究件 10/64) @@ -76,235 +21,355 @@ const PROJECTION_EDGE_COL := Color(0, 0, 0, 0.06) # 投影远段(渐隐收 # 直贴零暗档零阴影;墙脚阴线只画地面/墙面交界;(b)-(c) 交界 # 无边框由 BoardView 侧跳过墙格北向描边承载)── const E_FACADE_FRAC := 0.30 # 墙面承接高(格边比 30%) -# ── 素材槽位(现全 null = 程序化占位服役;art-bible §8.2 格面 32×32 暂定档)── -var tex_base: Texture2D = null # ① 砖面底 -var tex_mutation: Texture2D = null # ② 变异纹带 -var tex_glyph: Texture2D = null # ③ 数字 -var tex_badge: Texture2D = null # ③' 徽章 -var tex_seal: Texture2D = null: # ④ 封印(槽位可运行时换装〔封印〕——同族 facade/treasure_mark - set(v): # 均 setter 触发重绘(普通 var 赋值不重绘=封印插上不显示)) - if tex_seal != v: - tex_seal = v - queue_redraw() -var tex_facade: Texture2D = null # ⑥ E 格墙面材质(v3.5——南邻墙顶 center 瓦片同源,BoardView 注入) -var facade_strip := false: # E 格开关(BoardView paint 按「已开格且南邻未开墙」写入——瓦片态专用) + + +## 单格绘制输入集(被动绘制壳的全部绘制入参——BoardView 直写面;setter 触发宿主重绘语义逐字) +class _CellPaintState extends RefCounted: + var _owner: Node2D = null + var cell := Vector2i.ZERO # 板坐标(诊断观测面) + var edge := 46.0: # 格边长(BoardView.set_cell_area 单点驱动) + set(v): + edge = v + if _owner._glow_layer != null and is_instance_valid(_owner._glow_layer): + _owner._glow_layer.setup(edge, glow_group) + _owner.queue_redraw() + var bg_sb: StyleBoxFlat = null # 格底 StyleBox(BoardView 每次 paint 重建;呼吸微光就地改边框) + # ── 接触带微光绘制面(可挖域微光画在英雄格周界空间侧——art-bible §3.2 v2.8; + # 仅英雄格单元承载,BoardView.refresh 按可挖域写边/角码、_process 逐拍更新呼吸色)── + var contact_bands: Array[int] = [] # 正交接触边码 0=N/1=S/2=W/3=E(该侧整条边) + var contact_corners: Array[int] = [] # 斜向共享角码 0=NW/1=NE/2=SW/3=SE(角点+两侧各半条边) + var contact_col := Color(0, 0, 0, 0) # 当前呼吸色(alpha 0 = 不画) + var outline_sides: Array[int] = [] # 墙格地面朝向描边码 0=N/1=S/2=W/3=E(程序侧结构件) + # ── 高墙感 D 投影档(研究件四变体定 D=C+地面投影,ITB 同款高度线索)── + # 南立面 = 墙格南缘内侧「口沿高光线 + 双段暗带」(邻地面格才画,墙群内聚);地面投影 = 北邻墙格 + # 在本已开格北缘投渐隐暗带。参数按研究件 64px 拼版实测比例换算(立面双段 0.16/0.25 格边、投影带 + # 0.22 格边;坐标推导留档 commit)。素材缺位程序化回退同样服役(结构归程序——art-bible §8.5)。 + var facade_south := false: # 南立面开关(BoardView paint 按「南邻已开格」写入) + set(v): + facade_south = v + _owner.queue_redraw() + var proj_band := 0.0: # 地面投影带宽(像素,0 = 不画——BoardView 按北邻墙判定写入) + set(v): + proj_band = v + _owner.queue_redraw() + var badge_glyph := "": # 奖励徽章文字(右下角金——封印之下、变异纹之上、不进数字区) + set(v): + badge_glyph = v + if _owner._glow_layer != null and is_instance_valid(_owner._glow_layer): + _owner._glow_layer.set_badge_mask(v != "") + _owner.queue_redraw() + var dot_visible := false: # BFS 路径预览点迹(中心 6×6 小人蓝) + set(v): + dot_visible = v + _owner.queue_redraw() + var hl_visible := false: # 焦点高亮(悬停态边框覆盖层) + set(v): + hl_visible = v + _owner.queue_redraw() + var hl_color := UiKit.GOLD: # 高亮色(金目标/封印朱/禁入灰禁红) + set(v): + hl_color = v + _owner.queue_redraw() + var font: Font = null # 字体槽位(null = ThemeDB.fallback_font——BoardView.set_world_font 注入) + var bevel := 0.0: # 顶面倒角强度(0=关;档位 0/0.35/0.6/1.0) + set(v): + bevel = v + _owner.queue_redraw() + # ── 素材槽位(现全 null = 程序化占位服役;art-bible §8.2 格面 32×32 暂定档)── + var tex_base: Texture2D = null # ① 砖面底 + var tex_mutation: Texture2D = null # ② 变异纹带 + var tex_glyph: Texture2D = null # ③ 数字 + var tex_badge: Texture2D = null # ③' 徽章 + var tex_seal: Texture2D = null: # ④ 封印(槽位可运行时换装〔封印〕——同族 facade/treasure_mark + set(v): # 均 setter 触发重绘(普通 var 赋值不重绘=封印插上不显示)) + if tex_seal != v: + tex_seal = v + _owner.queue_redraw() + var tex_facade: Texture2D = null # ⑥ E 格墙面材质(v3.5——南邻墙顶 center 瓦片同源,BoardView 注入) + var facade_strip := false: # E 格开关(BoardView paint 按「已开格且南邻未开墙」写入——瓦片态专用) + set(v): + facade_strip = v + _owner.queue_redraw() + + # 变异角标(程序化,tex_mutation 纹带槽暂留): + # "flat_damage" = 未开格伤害型变异,左下角危险红三角(开格有代价的策略纹理标记) + # 叠加层标记(秘宝整图叠加层的格面呈现;派生信息层零回写): + # "mine_reveal" = 真视格(危险红内缩框——「这格是雷」的确定决策标记,封印照常计费); + # "reward_preview" 格不复用本字段——预读徽章走 badge_glyph 注入(board_view refresh 注入 REWARD_BADGE 字形) + var treasure_mark := "": + set(v): + if treasure_mark != v: + treasure_mark = v + _owner.queue_redraw() + var mutation_mark := "": + set(v): + if mutation_mark != v: + mutation_mark = v + _owner.queue_redraw() + + # ── 未开格类型微光(英雄 8 向相邻未开格的组粒度类型提示) + # 作用集 = 可挖域 ∩ 身份可查格,由 BoardView 侧派生下发;本件只承接绘制与子件生命周期。 + # 静态:不呼吸、不入动效预算;相位门控(结算/道具窗口态隐藏)在 BoardView 侧。 + var glow_group := 0: + set(v): + if glow_group != v: + glow_group = v + _owner._sync_glow_layer() + + var removed_dim := false: # 中立移除退色层(BoardView paint 按排雷玩法 is_removed 写入) + set(v): + removed_dim = v + _owner.queue_redraw() + + +var st := _CellPaintState.new() +var glyph := "": # 文字位——数字/雷/封印/人/?(现行视觉封印与数字互斥,同位承载;探针直读冻结) set(v): - facade_strip = v + glyph = v queue_redraw() - -# 变异角标(程序化,tex_mutation 纹带槽暂留): -# "flat_damage" = 未开格伤害型变异,左下角危险红三角(开格有代价的策略纹理标记) -# 叠加层标记(秘宝整图叠加层的格面呈现;派生信息层零回写): -# "mine_reveal" = 真视格(危险红内缩框——「这格是雷」的确定决策标记,封印照常计费); -# "reward_preview" 格不复用本字段——预读徽章走 badge_glyph 注入(board_view refresh 注入 REWARD_BADGE 字形) -var treasure_mark := "": +var glyph_color := UiKit.INK: # 文字色(墨族/雷朱/封印黑/小人蓝;探针直读冻结) set(v): - if treasure_mark != v: - treasure_mark = v - queue_redraw() -var mutation_mark := "": - set(v): - if mutation_mark != v: - mutation_mark = v - queue_redraw() - + glyph_color = v + queue_redraw() +var plate := false # 数字色牌开关(BoardView paint 按已开格 glyph 写入——原则二;探针直读冻结) # ── 视野迷雾呈现态(纯呈现零逻辑;规格=art-bible §6.5 v3.5,现行事实源=HUD框架「视野迷雾」节)── # 0=正常(灯光区内)/ 1=全黑(区外未开格——现行事实源「区外未开格=全黑不渲染」不透明档)/ 2=记忆态(区外已开格灰显可读) -var fog_state := 0: +var fog_state := 0: # 探针直读冻结 set(v): if fog_state != v: fog_state = v queue_redraw() -var removed_dim := false: # 中立移除退色层(BoardView paint 按排雷玩法 is_removed 写入) - set(v): - removed_dim = v - queue_redraw() -# ── 未开格类型微光(英雄 8 向相邻未开格的组粒度类型提示) -# 作用集 = 可挖域 ∩ 身份可查格,由 BoardView 侧派生下发;本件只承接绘制与子件生命周期。 -# 静态:不呼吸、不入动效预算;相位门控(结算/道具窗口态隐藏)在 BoardView 侧。 -var glow_group := 0: - set(v): - if glow_group != v: - glow_group = v - _sync_glow_layer() - var _glow_layer: Node2D = null +func _init() -> void: + st._owner = self + + func _draw() -> void: - var rect := Rect2(Vector2.ZERO, Vector2(edge, edge)) - # ① 砖面底(占位 = StyleBoxFlat:格底色 + 暗化描边 + 3px 圆角; - # 呼吸微光/主角蓝框由 BoardView 改 bg_sb 后触发重绘) - if tex_base != null: - draw_texture_rect(tex_base, rect, false) + var rect := Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)) + _draw_base_layer(rect) + _draw_mutation_marks() + _draw_glyph_layer(rect) + _draw_overlay_layers(rect) + + +## ① 砖面底层域(占位 = StyleBoxFlat:格底色 + 暗化描边 + 3px 圆角;瓦片态描边覆盖层; +## 呼吸微光/主角蓝框由 BoardView 改 bg_sb 后触发重绘。素材缺位回退走 elif 程序化原样,零观感变化) +func _draw_base_layer(rect: Rect2) -> void: + if st.tex_base != null: + draw_texture_rect(st.tex_base, rect, false) # 轮廓加强(轮廓线不明显):墙格地面朝向边补 2px 深墨线(§8.5 结构归程序—— # 素材 1px 墨线在 45/36px 非整数缩放下视觉衰减,程序线保底;墙-墙内界不画,保墙群内聚; # 坐标推导:各边内贴 2px 矩形,相邻边矩形全长相交=周界闭合) - if not outline_sides.is_empty(): - var ow := 2.0 - var oc := UiKit.FOG_OUTLINE - for b in outline_sides: - var or_ := Rect2() - match int(b): - 0: or_ = Rect2(0, 0, edge, ow) - 1: or_ = Rect2(0, edge - ow, edge, ow) - 2: or_ = Rect2(0, 0, ow, edge) - 3: or_ = Rect2(edge - ow, 0, ow, edge) - draw_rect(or_, oc, true) + if not st.outline_sides.is_empty(): + _draw_outline_sides() _draw_south_facade() _draw_north_projection() _draw_e_facade() # 瓦片态描边覆盖层:tex_base 在位时 BoardView 传入的 bg_sb # 已降级为「透明底 + 描边」载体——呼吸微光/主角蓝框画在瓦片之上(art-bible §1.2 叠层序 # 不变:底 < 纹带 < 数字 < 封印 < 焦点高亮);素材缺位回退走 elif 程序化原样,零观感变化 - if bg_sb != null: - draw_style_box(bg_sb, rect) + if st.bg_sb != null: + draw_style_box(st.bg_sb, rect) + _draw_contact_or_fallback(rect) - # 接触带微光(英雄格周界空间侧——正交邻墙=整条边;斜向=共享角点+两侧各半条边;光不进墙格): - # 带宽 4px 贴格边内侧(坐标推导:N=[0,0,edge,4] S=[0,edge-4,edge,4] W=[0,0,4,edge] E=[edge-4,0,4,edge]; - # 半条边=沿邻边自角点起 edge/2;角点圆 r=6 贴角点圆心,近白芯 r=2.5 提亮) - if contact_col.a > 0.0: + +## 接触带微光或平色回退域(英雄格周界空间侧——正交邻墙=整条边;斜向=共享角点+两侧各半条边;光不进墙格): +## 带宽 4px 贴格边内侧(坐标推导:N=[0,0,edge,4] S=[0,edge-4,edge,4] W=[0,0,4,edge] E=[edge-4,0,4,edge]; +## 半条边=沿邻边自角点起 edge/2;角点圆 r=6 贴角点圆心,近白芯 r=2.5 提亮) +func _draw_contact_or_fallback(rect: Rect2) -> void: + if st.contact_col.a > 0.0: _draw_north_projection() # 平色回退+微光并发的英雄格补画(瓦片路径已画——同像素重复无观感差) _draw_e_facade() # E 格墙面补画(同上——英雄格兼 E 格时瓦片路径已画,重复无害) - var band := 4.0 - var half := edge * 0.5 - for b in contact_bands: - var r := Rect2() - match int(b): - 0: r = Rect2(0, 0, edge, band) - 1: r = Rect2(0, edge - band, edge, band) - 2: r = Rect2(0, 0, band, edge) - 3: r = Rect2(edge - band, 0, band, edge) - draw_rect(r, contact_col, true) - for cc in contact_corners: - var pt := Vector2.ZERO - match int(cc): - 0: pt = Vector2(0, 0) - 1: pt = Vector2(edge, 0) - 2: pt = Vector2(0, edge) - 3: pt = Vector2(edge, edge) - draw_circle(pt, 6.0, contact_col) - draw_circle(pt, 2.5, Color(1, 1, 1, minf(1.0, contact_col.a * 0.95))) - # 半条边:角点沿两条邻边各延伸 edge/2(坐标推导:横带沿 N/S 自角点向板心侧 half; - # NW/SE 半带 x=y=角点坐标,NE/SE 半带自 edge-half 收进——正交整边时重叠同色无害) - if int(cc) == 0 or int(cc) == 1: - # N 侧横带:NW 自 x=0、NE 自 x=edge-half - draw_rect(Rect2(pt.x if int(cc) == 0 else pt.x - half, 0.0, half, band), contact_col, true) - if int(cc) == 2 or int(cc) == 3: - # S 侧横带:SW 自 x=0、SE 自 x=edge-half - draw_rect(Rect2(pt.x if int(cc) == 2 else pt.x - half, edge - band, half, band), contact_col, true) - if int(cc) == 0 or int(cc) == 2: - # W 侧竖带:NW 自 y=0、SW 自 y=edge-half - draw_rect(Rect2(0.0, pt.y if int(cc) == 0 else pt.y - half, band, half), contact_col, true) - if int(cc) == 1 or int(cc) == 3: - # E 侧竖带:NE 自 y=0、SE 自 y=edge-half - draw_rect(Rect2(edge - band, pt.y if int(cc) == 1 else pt.y - half, band, half), contact_col, true) - elif bg_sb != null: - draw_style_box(bg_sb, rect) + _draw_contact_bands() + elif st.bg_sb != null: + draw_style_box(st.bg_sb, rect) _draw_south_facade() # 平色回退路径——D 投影档两件同样服役(结构归程序) _draw_north_projection() - # 顶面倒角(光源左上——上/左内沿受光 2px、下/右内沿背光 2px;内缩 2px 让 - # 呼吸微光/主角蓝边框保持最外沿可见;强度随档位) - if bevel > 0.0: - var base: Color = bg_sb.bg_color - var lit: Color = base.lightened(0.22 * bevel) - var dark: Color = base.darkened(0.22 * bevel) - var t := 2.0 - draw_rect(Rect2(t, t, edge - t * 2.0, t), lit, true) - draw_rect(Rect2(t, t, t, edge - t * 2.0), lit, true) - draw_rect(Rect2(t, edge - t * 2.0, edge - t * 2.0, t), dark, true) - draw_rect(Rect2(edge - t * 2.0, t, t, edge - t * 2.0), dark, true) + _draw_bevel() + + +## ②③④ 中层域:变异纹带与标记/数字色牌与字形/徽章/封印 +func _draw_glyph_layer(rect: Rect2) -> void: # ② 变异纹带(无程序化纹带——槽位先行,贴图落位即用) - if tex_mutation != null: - draw_texture_rect(tex_mutation, rect, false) - # ②' 变异角标:左下角危险红三角(危险红=「开此格有代价」语义位,与雷族同源; - # 尺寸随格边比例——灰色可辨辅助=红对砖面明度差,禁喧宾) - # ②" 秘宝叠加层:真视格=危险红内缩框(策略纹理——确定决策输入不喧宾) - if treasure_mark == "mine_reveal": - var inset := edge * 0.14 - draw_rect(Rect2(inset, inset, edge - inset * 2.0, edge - inset * 2.0), - Color(UiKit.RED_MINE, 0.9), false, 2.0) - if mutation_mark == "flat_damage": - var mw := edge * 0.28 - var mpts := PackedVector2Array([Vector2(0, edge - mw), Vector2(0, edge), Vector2(mw, edge)]) - draw_colored_polygon(mpts, Color(UiKit.RED_MINE, 0.85)) + if st.tex_mutation != null: + draw_texture_rect(st.tex_mutation, rect, false) # ③ 数字/徽章(数字 ≥ 格高 50% 墨族对格底 ≥4.5:1;徽章右下角金,遮蔽态 = "?") if glyph != "": - # 数字色牌后备路径(圣经 v3.5 冻结④:数字亮墨直画已开格底须垫牌; - # 牌=后备件——雷/「?」维持牌载保读性(牌上深墨族 12:1); - # 牌尺寸 0.60/0.62 格边居中;封印=未开格不垫牌) - if plate: - var pw := edge * 0.60 - var ph := edge * 0.62 - var pr := Rect2((edge - pw) * 0.5, (edge - ph) * 0.5, pw, ph) - draw_rect(pr, UiKit.DIGIT_PLATE, true) - draw_rect(pr, UiKit.DIGIT_PLATE_EDGE, false, 1.0) - if tex_glyph != null: - draw_texture_rect(tex_glyph, rect, false) + _draw_digit_plate() + if st.tex_glyph != null: + draw_texture_rect(st.tex_glyph, rect, false) else: - _draw_glyph(glyph, glyph_color, maxi(18, int(ceil(edge * 0.5)))) # 字号下限 18(§7.4 冻结公式) - if badge_glyph != "": - if tex_badge != null: - draw_texture_rect(tex_badge, rect, false) + _draw_glyph(glyph, glyph_color, maxi(18, int(ceil(st.edge * 0.5)))) # 字号下限 18(§7.4 冻结公式) + if st.badge_glyph != "": + if st.tex_badge != null: + draw_texture_rect(st.tex_badge, rect, false) else: - _draw_badge(badge_glyph) + _draw_badge(st.badge_glyph) # ④ 封印(现行视觉封印文字在 ③ 文字位承载——独立槽位留给封印图) - if tex_seal != null: - draw_texture_rect(tex_seal, rect, false) + if st.tex_seal != null: + draw_texture_rect(st.tex_seal, rect, false) + + +## 数字色牌后备域(圣经 v3.5 冻结④:数字亮墨直画已开格底须垫牌; +## 牌=后备件——雷/「?」维持牌载保读性(牌上深墨族 12:1); +## 牌尺寸 0.60/0.62 格边居中;封印=未开格不垫牌) +func _draw_digit_plate() -> void: + var pw := st.edge * 0.60 + var ph := st.edge * 0.62 + var pr := Rect2((st.edge - pw) * 0.5, (st.edge - ph) * 0.5, pw, ph) + draw_rect(pr, UiKit.DIGIT_PLATE, true) + draw_rect(pr, UiKit.DIGIT_PLATE_EDGE, false, 1.0) + + +## ⑤⑥⑦ 顶层域:路径点迹 + 焦点高亮 + 视野迷雾 + 中立移除退色 +func _draw_overlay_layers(rect: Rect2) -> void: # ⑤ 路径点迹 + 焦点高亮(悬停态覆盖层,不参与命中) - if dot_visible: + if st.dot_visible: var ds := 6.0 - draw_rect(Rect2(Vector2((edge - ds) * 0.5, (edge - ds) * 0.5), Vector2(ds, ds)), UiKit.HERO_BLUE, true) - if hl_visible: + draw_rect(Rect2(Vector2((st.edge - ds) * 0.5, (st.edge - ds) * 0.5), Vector2(ds, ds)), UiKit.HERO_BLUE, true) + if st.hl_visible: # 悬停高亮减淡(金框太明显——绘制时 55% alpha,hl_color 属性与探针断言面不动) - draw_rect(Rect2(1, 1, edge - 2.0, edge - 2.0), Color(hl_color, 0.55), false, 2.0) # 2px 内描边 - # ⑥ 视野迷雾层(最顶遮罩,§6.5 落文): - # 全黑=不透明纯黑盖全部叠层(含封印印/徽章/高亮——封印印记随格隐没,§6.5 显式推论); - # 记忆态=记忆板素色压平(纹理免疫——色牌/封印印/徽章随板隐没)+数字记忆墨重画(数字可读硬指标 ≥4.5:1) - if fog_state == 1: - draw_rect(Rect2(Vector2.ZERO, Vector2(edge, edge)), Color(0, 0, 0, 1.0), true) - elif fog_state == 2: - # 记忆态换装(demo 口径「地板纹理 + 灰罩 α.66」: - # 地板纹理原样直画 + FOG_MEMORY_PLATE α.66 灰罩压顶;素材零改动、格边长零改动。 - # 记忆墨 FOG_MEMORY_INK 对合成底归一后(真实渲染面)三关 5.71–5.93:1 ≥4.5:1(§7.2 硬指标; - # 测点行值与素材随动,见 ui_contrast_probe 迷雾段)) - if tex_base != null: - draw_texture_rect(tex_base, Rect2(Vector2.ZERO, Vector2(edge, edge)), false) - draw_rect(Rect2(Vector2.ZERO, Vector2(edge, edge)), Color(UiKit.FOG_MEMORY_PLATE, 0.66), true) - if glyph != "": - _draw_glyph(glyph, UiKit.FOG_MEMORY_INK, maxi(18, int(ceil(edge * 0.5)))) # 字号下限 18(§7.4 冻结公式同源) + draw_rect(Rect2(1, 1, st.edge - 2.0, st.edge - 2.0), Color(st.hl_color, 0.55), false, 2.0) # 2px 内描边 + _draw_fog_layer() # ⑦ 中立移除退色层(退色降档:暗幕压顶全层覆盖,暗淡记忆态同族呈现语义; # 顶层于迷雾之上——移除格不再参与迷雾语义。本实现移除集=未揭示危险格(无数字无封印印)) - if removed_dim: - draw_rect(Rect2(Vector2.ZERO, Vector2(edge, edge)), Color(0.12, 0.10, 0.09, 0.62), true) + if st.removed_dim: + draw_rect(Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)), Color(0.12, 0.10, 0.09, 0.62), true) + + +## ⑥ 视野迷雾层(最顶遮罩,§6.5 落文): +## 全黑=不透明纯黑盖全部叠层(含封印印/徽章/高亮——封印印记随格隐没,§6.5 显式推论); +## 记忆态=记忆板素色压平(纹理免疫——色牌/封印印/徽章随板隐没)+数字记忆墨重画(数字可读硬指标 ≥4.5:1) +func _draw_fog_layer() -> void: + if fog_state == 1: + draw_rect(Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)), Color(0, 0, 0, 1.0), true) + elif fog_state == 2: + _draw_fog_memory() + + +## 墙格轮廓域:地面朝向边补 2px 深墨线(各边内贴 2px 矩形,相邻边矩形全长相交=周界闭合) +func _draw_outline_sides() -> void: + var ow := 2.0 + var oc := UiKit.FOG_OUTLINE + for b in st.outline_sides: + var or_ := Rect2() + match int(b): + 0: or_ = Rect2(0, 0, st.edge, ow) + 1: or_ = Rect2(0, st.edge - ow, st.edge, ow) + 2: or_ = Rect2(0, 0, ow, st.edge) + 3: or_ = Rect2(st.edge - ow, 0, ow, st.edge) + draw_rect(or_, oc, true) + + +## 接触带微光域:正交整条边 + 斜向共享角点与两侧半条边 +func _draw_contact_bands() -> void: + var band := 4.0 + var half := st.edge * 0.5 + for b in st.contact_bands: + var r := Rect2() + match int(b): + 0: r = Rect2(0, 0, st.edge, band) + 1: r = Rect2(0, st.edge - band, st.edge, band) + 2: r = Rect2(0, 0, band, st.edge) + 3: r = Rect2(st.edge - band, 0, band, st.edge) + draw_rect(r, st.contact_col, true) + for cc in st.contact_corners: + _draw_contact_corner(int(cc), band, half) + + +## 斜向角点域:角点圆 r=6 贴角点圆心 + 近白芯 r=2.5 提亮 + 半条边(角点沿两条邻边各延伸 edge/2—— +## 坐标推导:横带沿 N/S 自角点向板心侧 half;NW/SE 半带 x=y=角点坐标,NE/SE 半带自 edge-half 收进—— +## 正交整边时重叠同色无害) +func _draw_contact_corner(cc: int, band: float, half: float) -> void: + var pt := Vector2.ZERO + match cc: + 0: pt = Vector2(0, 0) + 1: pt = Vector2(st.edge, 0) + 2: pt = Vector2(0, st.edge) + 3: pt = Vector2(st.edge, st.edge) + draw_circle(pt, 6.0, st.contact_col) + draw_circle(pt, 2.5, Color(1, 1, 1, minf(1.0, st.contact_col.a * 0.95))) + if cc == 0 or cc == 1: + # N 侧横带:NW 自 x=0、NE 自 x=edge-half + draw_rect(Rect2(pt.x if cc == 0 else pt.x - half, 0.0, half, band), st.contact_col, true) + if cc == 2 or cc == 3: + # S 侧横带:SW 自 x=0、SE 自 x=edge-half + draw_rect(Rect2(pt.x if cc == 2 else pt.x - half, st.edge - band, half, band), st.contact_col, true) + if cc == 0 or cc == 2: + # W 侧竖带:NW 自 y=0、SW 自 y=edge-half + draw_rect(Rect2(0.0, pt.y if cc == 0 else pt.y - half, band, half), st.contact_col, true) + if cc == 1 or cc == 3: + # E 侧竖带:NE 自 y=0、SE 自 y=edge-half + draw_rect(Rect2(st.edge - band, pt.y if cc == 1 else pt.y - half, band, half), st.contact_col, true) + + +## 顶面倒角域(光源左上——上/左内沿受光 2px、下/右内沿背光 2px;内缩 2px 让 +## 呼吸微光/主角蓝边框保持最外沿可见;强度随档位) +func _draw_bevel() -> void: + if st.bevel <= 0.0: + return + var base: Color = st.bg_sb.bg_color + var lit: Color = base.lightened(0.22 * st.bevel) + var dark: Color = base.darkened(0.22 * st.bevel) + var t := 2.0 + draw_rect(Rect2(t, t, st.edge - t * 2.0, t), lit, true) + draw_rect(Rect2(t, t, t, st.edge - t * 2.0), lit, true) + draw_rect(Rect2(t, st.edge - t * 2.0, st.edge - t * 2.0, t), dark, true) + draw_rect(Rect2(st.edge - t * 2.0, t, t, st.edge - t * 2.0), dark, true) + + +## 变异标记域:真视格=危险红内缩框/未开格伤害型变异=左下角危险红三角 +func _draw_mutation_marks() -> void: + if treasure_mark == "mine_reveal": + var inset := st.edge * 0.14 + draw_rect(Rect2(inset, inset, st.edge - inset * 2.0, st.edge - inset * 2.0), + Color(UiKit.RED_MINE, 0.9), false, 2.0) + if st.mutation_mark == "flat_damage": + var mw := st.edge * 0.28 + var mpts := PackedVector2Array([Vector2(0, st.edge - mw), Vector2(0, st.edge), Vector2(mw, st.edge)]) + draw_colored_polygon(mpts, Color(UiKit.RED_MINE, 0.85)) + + +## 记忆态换装域(demo 口径「地板纹理 + 灰罩 α.66」: +## 地板纹理原样直画 + FOG_MEMORY_PLATE α.66 灰罩压顶;素材零改动、格边长零改动。 +## 记忆墨 FOG_MEMORY_INK 对合成底归一后(真实渲染面)三关 5.71–5.93:1 ≥4.5:1(§7.2 硬指标; +## 测点行值与素材随动,见 ui_contrast_probe 迷雾段)) +func _draw_fog_memory() -> void: + if st.tex_base != null: + draw_texture_rect(st.tex_base, Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)), false) + draw_rect(Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)), Color(UiKit.FOG_MEMORY_PLATE, 0.66), true) + if glyph != "": + _draw_glyph(glyph, UiKit.FOG_MEMORY_INK, maxi(18, int(ceil(st.edge * 0.5)))) # 字号下限 18(§7.4 冻结公式同源) # 南立面(D 档件一——墙格南缘内侧,自底边向上:双段暗带在下、口沿高光线贴其上沿): # 坐标推导(自格底边起算):远段=[edge−far, edge],近段=[edge−far−near, edge−far], # 高光线=[edge−far−near−hl, edge−far−near]——与研究件「墙顶面→高光→暗段→更暗段→格界」下行序一致 func _draw_south_facade() -> void: - if not facade_south: + if not st.facade_south: return - var far_h := edge * FACADE_FAR_FRAC - var near_h := edge * FACADE_NEAR_FRAC - var y_far := edge - far_h + var far_h := st.edge * FACADE_FAR_FRAC + var near_h := st.edge * FACADE_NEAR_FRAC + var y_far := st.edge - far_h var y_near := y_far - near_h var y_hl := y_near - FACADE_HIGHLIGHT_PX - draw_rect(Rect2(0.0, y_hl, edge, FACADE_HIGHLIGHT_PX), UiKit.STAND_STONE_RIM, true) # 口沿高光(UiKit.STAND_STONE_RIM 并档——同值单点) - draw_rect(Rect2(0.0, y_near, edge, near_h), FACADE_NEAR_COL, true) - draw_rect(Rect2(0.0, y_far, edge, far_h), FACADE_FAR_COL, true) + draw_rect(Rect2(0.0, y_hl, st.edge, FACADE_HIGHLIGHT_PX), UiKit.STAND_STONE_RIM, true) # 口沿高光(UiKit.STAND_STONE_RIM 并档——同值单点) + draw_rect(Rect2(0.0, y_near, st.edge, near_h), FACADE_NEAR_COL, true) + draw_rect(Rect2(0.0, y_far, st.edge, far_h), FACADE_FAR_COL, true) # 地面投影(D 档件二——北邻墙格在本已开格北缘投渐隐暗带,自北缘向南三段衰减): # 坐标推导:三等分 proj_band,α 0.28/0.16/0.06(研究件实测贴墙 α≈0.28、带末渐融入地面) func _draw_north_projection() -> void: - if proj_band <= 0.0: + if st.proj_band <= 0.0: return - var seg := proj_band / 3.0 - draw_rect(Rect2(0.0, 0.0, edge, seg), PROJECTION_NEAR_COL, true) - draw_rect(Rect2(0.0, seg, edge, seg), PROJECTION_MID_COL, true) - draw_rect(Rect2(0.0, seg * 2.0, edge, seg), PROJECTION_EDGE_COL, true) + var seg := st.proj_band / 3.0 + draw_rect(Rect2(0.0, 0.0, st.edge, seg), PROJECTION_NEAR_COL, true) + draw_rect(Rect2(0.0, seg, st.edge, seg), PROJECTION_MID_COL, true) + draw_rect(Rect2(0.0, seg * 2.0, st.edge, seg), PROJECTION_EDGE_COL, true) # E 格高墙面(圣经 v3.5 冻结①——方向稿 draw_facade 移植):本格已开且南邻为墙 → @@ -312,31 +377,31 @@ func _draw_north_projection() -> void: # 墙脚阴线只画地面/墙面交界(strip 顶缘 1px);墙面底缘(格界处)不画任何线——直接接南侧墙顶 # (白边导致两截)。仅瓦片态服役(tex_facade 缺位 = 平色回退原语义不动)。 func _draw_e_facade() -> void: - if not facade_strip or tex_facade == null: + if not st.facade_strip or st.tex_facade == null: return - var strip_h := edge * E_FACADE_FRAC - var y0 := edge - strip_h - var ts := tex_facade.get_size() - draw_texture_rect_region(tex_facade, Rect2(0.0, y0, edge, strip_h), + var strip_h := st.edge * E_FACADE_FRAC + var y0 := st.edge - strip_h + var ts := st.tex_facade.get_size() + draw_texture_rect_region(st.tex_facade, Rect2(0.0, y0, st.edge, strip_h), Rect2(0.0, 0.0, ts.x, ts.y * E_FACADE_FRAC)) - draw_rect(Rect2(0.0, y0, edge, 1.0), UiKit.E_FACADE_FOOT_COL, true) + draw_rect(Rect2(0.0, y0, st.edge, 1.0), UiKit.E_FACADE_FOOT_COL, true) # 文字位绘制:水平居中 + 垂直居中(基线 = (格高 − 字高)/2 + 上伸部) func _draw_glyph(g: String, col: Color, fs: int) -> void: - var f := font if font != null else ThemeDB.fallback_font + var f := st.font if st.font != null else ThemeDB.fallback_font if f == null: return var asc := f.get_ascent(fs) var desc := f.get_descent(fs) - draw_string(f, Vector2(0.0, (edge - (asc + desc)) * 0.5 + asc), g, - HORIZONTAL_ALIGNMENT_CENTER, edge, fs, col) + draw_string(f, Vector2(0.0, (st.edge - (asc + desc)) * 0.5 + asc), g, + HORIZONTAL_ALIGNMENT_CENTER, st.edge, fs, col) # 微光子件生命周期:有组才建、组变即换、无组即撤——作用集 ≤8 格常驻, # 其余格零节点零绘制成本。与父件 _draw() 分离的三条理由见本件头注(命令序列/重绘预算/材质挂点)。 func _sync_glow_layer() -> void: - if glow_group == 0: + if st.glow_group == 0: if _glow_layer != null and is_instance_valid(_glow_layer): _glow_layer.queue_free() _glow_layer = null @@ -344,15 +409,15 @@ func _sync_glow_layer() -> void: if _glow_layer == null or not is_instance_valid(_glow_layer): _glow_layer = _GLOW_LAYER.new() add_child(_glow_layer) - _glow_layer.setup(edge, glow_group) - _glow_layer.set_badge_mask(badge_glyph != "") + _glow_layer.setup(st.edge, st.glow_group) + _glow_layer.set_badge_mask(st.badge_glyph != "") # 徽章绘制:右下角金(文字贴角内缩 2–3px) func _draw_badge(g: String) -> void: - var f := font if font != null else ThemeDB.fallback_font + var f := st.font if st.font != null else ThemeDB.fallback_font if f == null: return - var fs := maxi(10, int(edge * 0.28)) + var fs := maxi(10, int(st.edge * 0.28)) var sz := f.get_string_size(g, HORIZONTAL_ALIGNMENT_LEFT, -1, fs) - draw_string(f, Vector2(edge - sz.x - 2.0, edge - 3.0), g, HORIZONTAL_ALIGNMENT_LEFT, -1, fs, UiKit.GOLD) + draw_string(f, Vector2(st.edge - sz.x - 2.0, st.edge - 3.0), g, HORIZONTAL_ALIGNMENT_LEFT, -1, fs, UiKit.GOLD) diff --git a/game/presentation/views/board_view.gd b/game/presentation/views/board_view.gd index c805c20d..c15e5c67 100644 --- a/game/presentation/views/board_view.gd +++ b/game/presentation/views/board_view.gd @@ -182,7 +182,7 @@ func set_treasure_overlays(reveal_cells: Array, preview_cells: Array) -> void: func set_world_font(f: Font) -> void: _world.board_font = f for c in _cells: - _cells[c].font = f + _cells[c].st.font = f _cells[c].queue_redraw() @@ -201,7 +201,7 @@ func set_cell_area(avail: Vector2) -> void: var stride := _cell + float(SEP) for c in _cells: var u: Node2D = _cells[c] - u.edge = _cell + u.st.edge = _cell u.position = Vector2(c) * stride if _world.hero != null and _hero_sprite.last.x >= 0: _place_hero(_hero_sprite.last, _hero_sprite.last, true) # 格边长变更(窗口缩放)——英雄即时随格重置(杀位移 tween 防飞陈旧落点) @@ -247,17 +247,17 @@ func rebuild(board: BoardData, minesweeper: MinesweeperRules) -> void: for x in _board.width: var c := Vector2i(x, y) var u: Node2D = _CELL_UNIT.new() - u.cell = c - u.edge = _cell + u.st.cell = c + u.st.edge = _cell u.position = Vector2(c) * stride - u.bevel = DEPTH_BEVEL # 新建格单元继承 + u.st.bevel = DEPTH_BEVEL # 新建格单元继承 u.use_parent_material = true # 去饱和材质挂 BoardWorld——单元随世界材质灰化 if _palette.tiles_ok: # NEAREST 采样(32×32 源 × 45/36px 格边非整数缩放接受档, # 与英雄小人 1.5× 豁免同级(圣经 §8.2);只在瓦片 # 模式启用——程序化回退路径渲染零扰动(含格面文字采样不回退不变化) u.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST - u.font = _world.board_font + u.st.font = _world.board_font _world.add_child(u) _cells[c] = u set_cell_area(size) @@ -509,21 +509,21 @@ func _apply_hover_visuals(c: Vector2i) -> void: var seal: bool = _minesweeper.has_seal(c) var adjacent: bool = _diggable.has(c) if revealed: - u.hl_visible = true - u.hl_color = COL_HL_TARGET # 移动目标格边框(金) + u.st.hl_visible = true + u.st.hl_color = COL_HL_TARGET # 移动目标格边框(金) _set_cursor_shape(c, CURSOR_MOVE_SEM) _show_path_preview(c) elif adjacent: - u.hl_visible = true - u.hl_color = COL_HL_TARGET # 挖掘目标格边框(相邻封印格同此——左键直接挖) + u.st.hl_visible = true + u.st.hl_color = COL_HL_TARGET # 挖掘目标格边框(相邻封印格同此——左键直接挖) _set_cursor_shape(c, CURSOR_DIG) elif seal: - u.hl_visible = true - u.hl_color = COL_SEAL # 已承诺封印标记(封印黑;无交互可供) + u.st.hl_visible = true + u.st.hl_color = COL_SEAL # 已承诺封印标记(封印黑;无交互可供) _set_cursor_shape(c, Input.CURSOR_FORBIDDEN) else: - u.hl_visible = true - u.hl_color = UiKit.RED_FORBID # 不可及域灰禁框(禁止光标+首遇 toast 双通道) + u.st.hl_visible = true + u.st.hl_color = UiKit.RED_FORBID # 不可及域灰禁框(禁止光标+首遇 toast 双通道) _set_cursor_shape(c, Input.CURSOR_FORBIDDEN) hover_forbidden.emit(c) @@ -531,7 +531,7 @@ func _apply_hover_visuals(c: Vector2i) -> void: func _clear_hover_visuals(c: Vector2i) -> void: if not _cells.has(c): return - _cells[c].hl_visible = false + _cells[c].st.hl_visible = false _hide_path_preview() @@ -546,12 +546,12 @@ func _show_path_preview(c: Vector2i) -> void: var path: Array[Variant] = _minesweeper.compute_move_path(c) for pc in path: if _cells.has(pc): - _cells[pc].dot_visible = true + _cells[pc].st.dot_visible = true func _hide_path_preview() -> void: for c in _cells: - _cells[c].dot_visible = false + _cells[c].st.dot_visible = false # ---- 奖励徽章(已开格身份 icon——封印之下、纹带之上、不进数字区) ---- @@ -611,11 +611,11 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void: var digit_col := COL_DIGIT # 封印槽统一维护:封印格=封印印记纹理;其余格清 null(封印格挖开后残留在 tex_seal # 会让 _draw 无条件把封印印画在已开格上——tex_seal 绘制无 revealed 条件)。缺盘走字形回退。 - u.tex_seal = _palette.seal_tex if (seal and not revealed and _palette.seal_tex != null) else null + u.st.tex_seal = _palette.seal_tex if (seal and not revealed and _palette.seal_tex != null) else null if removed: revealed = false # 移除格固定未开呈现基座(退色暗幕层承载;危险格本无数字,周边数字冻结) seal = false - u.tex_seal = null + u.st.tex_seal = null if revealed: var rec: Dictionary[Variant, Variant] = _minesweeper.reveal_record(c) # 已开记录经查询面 if rec.get("kind", "") == "mine": @@ -662,8 +662,8 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void: # 贴图缺位回退字形「人」(项目纹理槽惯例——贴图在位=格内字形退役,单位本体由独立精灵承载) glyph = "人" digit_col = COL_HERO - u.tex_base = tile - u.bg_sb = sb + u.st.tex_base = tile + u.st.bg_sb = sb u.glyph = glyph u.glyph_color = digit_col # 色牌后备(数字直画退牌;「危」/「?」维持牌载保读性)+ v3.4:双峰纹理板 @@ -673,11 +673,11 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void: digit_col = COL_DIGIT # 牌上深墨族(素牌 12:1——双峰纹理免疫) u.glyph_color = digit_col u.plate = revealed and (glyph == "危" or glyph == "?" or plate_digit) - u.outline_sides.assign([]) # typed Array[int](assign 保持引用语义) - u.facade_south = false - u.proj_band = 0.0 - u.tex_facade = null - u.facade_strip = false + u.st.outline_sides.assign([]) # typed Array[int](assign 保持引用语义) + u.st.facade_south = false + u.st.proj_band = 0.0 + u.st.tex_facade = null + u.st.facade_strip = false # 封印槽不在此重置:尾部重置会与此上方条件赋值构成双写踩踏(把封印纹理恒清 null=素材在位时封印不可见); # 上方 else 分支已是全路径清残留,尾部重置系冗余且有害 var south_n: Vector2i = c + Vector2i(0, 1) @@ -687,44 +687,44 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void: if not revealed: # 高墙感 D 投影档:南邻地面格 → 墙格南缘立「口沿高光+双段暗带」立面 #(邻地面才画——墙-墙内界保墙群内聚);板外=墙不画 - u.facade_south = south_in and _minesweeper.is_revealed(south_n) + u.st.facade_south = south_in and _minesweeper.is_revealed(south_n) # 轮廓加强:墙格四邻中已开(地面)格朝向记描边码(板外=墙不描——保墙群外轮廓只在墙/地交界) for d in [Vector2i(0, -1), Vector2i(0, 1), Vector2i(-1, 0), Vector2i(1, 0)]: var n: Vector2i = c + d if n.x >= 0 and n.y >= 0 and n.x < _board.width and n.y < _board.height and _minesweeper.is_revealed(n): var code := 0 if d.y < 0 else (1 if d.y > 0 else (2 if d.x < 0 else 3)) - if code == 1 and u.facade_south: + if code == 1 and u.st.facade_south: continue # 南缘由立面件承载(高光+双段暗带)——2px 描边让位防三重叠黑 if code == 0 and _palette.tiles_ok: continue # v3.5 冻结① (b)-(c) 交界无边框:墙面直贴接墙顶(E 格墙面件在邻格承载)——北向描边不画(平色回退态保留描边) - u.outline_sides.append(code) + u.st.outline_sides.append(code) else: # 地面投影:北邻墙格在本已开格北缘投渐隐暗带(ITB 同款高度线索——带宽按格边比例常量) if north_in and not _minesweeper.is_revealed(north_n): - u.proj_band = u.edge * WALL_PROJECTION_FRAC + u.st.proj_band = u.st.edge * WALL_PROJECTION_FRAC # E 格高墙面(圣经 v3.5 冻结①——方向稿 is_facade_cell 口径):已开格南邻未开墙 → 南部 30% # 承接墙面(材质=同板墙顶 center 瓦片,原亮度直贴);仅瓦片态启用——平色回退保持原语义 if south_in and not _minesweeper.is_revealed(south_n) and _palette.tiles_ok: - u.facade_strip = true - u.tex_facade = _palette.tiles.get("wall_center") + u.st.facade_strip = true + u.st.tex_facade = _palette.tiles.get("wall_center") # ②( 秘宝叠加层:真视格=红内缩框;预读格=奖励身份徽章(identity_resolver 既有 # 只读面取字形——零数据复制,badge 位复用);已开格标记自然退位(refresh 逐拍重算) if not revealed: - u.treasure_mark = "mine_reveal" if _treasure_reveal.has(c) else "" + u.st.treasure_mark = "mine_reveal" if _treasure_reveal.has(c) else "" if _treasure_preview.has(c) and glyph == "": var pid = _identity_cb.call(c) if _identity_cb.is_valid() else null if pid != null and REWARD_BADGE.has(String(pid.get("cell_type", ""))): glyph = String(REWARD_BADGE[String(pid.get("cell_type", ""))]) digit_col = COL_GOLD - u.badge_glyph = glyph + u.st.badge_glyph = glyph # ②' 变异角标:未开格带伤害型变异 → 危险红角标(已开=威胁已兑现标记退位); # 回调缺位 = 无标记(程序化角标,tex_mutation 纹带槽暂留) if not revealed and _vmark_cb.is_valid(): - u.mutation_mark = String(_vmark_cb.call(c)) + u.st.mutation_mark = String(_vmark_cb.call(c)) else: - u.mutation_mark = "" - u.badge_glyph = _update_badge(c) - u.glow_group = int(_glow.get(c, UiKit.GLOW_NONE)) + u.st.mutation_mark = "" + u.st.badge_glyph = _update_badge(c) + u.st.glow_group = int(_glow.get(c, UiKit.GLOW_NONE)) # 视野迷雾呈态:曼哈顿距离 > 半径 → 区外——未开格全黑(现行事实源照抄)/ # 已开格记忆态(灰显可读);以英雄格为心逐格重算,refresh()/notify_hero_move() 既有重绘驱动=移动实时跟随。 # 纯呈现零逻辑——格态/数字真值/清图判定零触碰(🔴 契约 1);结算期去饱和叠加互斥由 CellUnit 记忆态终态档承载。 @@ -734,7 +734,7 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void: u.fog_state = (2 if revealed else 1) if fogged else 0 else: u.fog_state = 0 - u.removed_dim = removed + u.st.removed_dim = removed u.queue_redraw() @@ -765,7 +765,7 @@ func _sync_glow_to_units() -> void: for c in touched: var u: Node2D = _cells.get(c) if u != null: - u.glow_group = int(_glow.get(c, UiKit.GLOW_NONE)) + u.st.glow_group = int(_glow.get(c, UiKit.GLOW_NONE)) func _process(delta: float) -> void: @@ -777,14 +777,14 @@ func _process(delta: float) -> void: if UiKit.motion_scale < 1.0: return # 减动效:呼吸停格(refresh/enable 时已施静态色——静态可辨、无持续运动) var k := 0.5 + 0.5 * sin(_breath_t * 3.0) # 3 rad/s≈0.48Hz——无 >3 次/秒频闪(AC 基线) - _contact_unit.contact_col = COL_CONTACT.lerp(UiKit.CONTACT_GLOW_DEEP, 0.55 - 0.35 * k) + _contact_unit.st.contact_col = COL_CONTACT.lerp(UiKit.CONTACT_GLOW_DEEP, 0.55 - 0.35 * k) _contact_unit.queue_redraw() # 带几何写入(refresh / set_dig_glow_enabled(true) 挂点):清旧承载件→按英雄格可挖域写边/角码→施色 func _apply_contact_bands() -> void: if _contact_unit != null and is_instance_valid(_contact_unit) and (_cells.get(_minesweeper.hero_pos()) != _contact_unit if _minesweeper != null else true): - _contact_unit.contact_col = Color(0, 0, 0, 0) + _contact_unit.st.contact_col = Color(0, 0, 0, 0) _contact_unit.queue_redraw() _contact_unit = null if _board == null or _minesweeper == null or not _dig_glow: @@ -801,19 +801,19 @@ func _apply_contact_bands() -> void: for d in [Vector2i(-1, -1), Vector2i(1, -1), Vector2i(-1, 1), Vector2i(1, 1)]: if _diggable.has(_minesweeper.hero_pos() + d): corners.append(0 if (d.x < 0 and d.y < 0) else (1 if d.x > 0 and d.y < 0 else (2 if d.x < 0 else 3))) - hu.contact_bands = bands - hu.contact_corners = corners + hu.st.contact_bands = bands + hu.st.contact_corners = corners # 减动效:呼吸停格——refresh/enable 重放时施**静态**色(本件头注「已施静态色」的落実); # 0.5 = 呼吸周期中值(正常态 k 的均值)。否则减动效下每次 refresh 都会按当前相位重施色, # 带色在 refresh 间跳变=持续运动假象,且令 on/off 双帧取证无法隔离单一变量。 var k := 0.5 if UiKit.motion_scale < 1.0 else 0.5 + 0.5 * sin(_breath_t * 3.0) - hu.contact_col = COL_CONTACT.lerp(UiKit.CONTACT_GLOW_DEEP, 0.55 - 0.35 * k) + hu.st.contact_col = COL_CONTACT.lerp(UiKit.CONTACT_GLOW_DEEP, 0.55 - 0.35 * k) hu.queue_redraw() func _reset_breath() -> void: if _contact_unit != null and is_instance_valid(_contact_unit): - _contact_unit.contact_col = Color(0, 0, 0, 0) + _contact_unit.st.contact_col = Color(0, 0, 0, 0) _contact_unit.queue_redraw() @@ -888,5 +888,5 @@ func cell_rect_probe(c: Vector2i) -> Rect2: if host.size.x <= 0.0 or host.size.y <= 0.0 or vp_size.x <= 0.0 or vp_size.y <= 0.0: return Rect2() var k: Vector2 = host.size / vp_size - var local := Rect2(u.global_position, Vector2(u.edge, u.edge)) + var local := Rect2(u.global_position, Vector2(u.st.edge, u.st.edge)) return Rect2(host.position + local.position * k, local.size * k) diff --git a/game/presentation/views/camp_overlay_view.gd b/game/presentation/views/camp_overlay_view.gd index f9e6903d..68272e97 100644 --- a/game/presentation/views/camp_overlay_view.gd +++ b/game/presentation/views/camp_overlay_view.gd @@ -2,6 +2,10 @@ ## 营地覆盖层:树淬炼/锻造工坊/秘宝匣/奖励格图鉴四柜台只读投影,购买与切换意图经信号外发。 ## 柜台锁定态(钥匙未点亮)开/关零副作用——零写档零报错、锁定行纯文案无可交互购买路径。 ## 开合与开始层互斥归组合根承接(打开前隐藏开始层)。 +## 内部状态件(u1 子域拆件——契约 9 职责轴):_TreeState=树淬炼柜台内容容器(单一变化原因=树行板结构); +## _ForgeState=锻造工坊与秘宝匣两柜台行板与锁定门(钥匙门同构);_AttuneState=同调装配段节点与钮册; +## _SideState=Y 刷新升级行与最近冒险账面。探针直读冻结成员(_balance_label/_tree_buy_btns/ +## _tree_fork_btns/_workshop_holder/_workshop_rows)住宿主不进状态件。 class_name CampOverlay extends Control @@ -12,31 +16,48 @@ signal tree_buy_requested(node_id: String) ## 天赋树购买意图(写口 signal tree_fork_toggled(group: String, side: String) ## 双态切换意图(一节点双态——零币切态) signal forge_buy_requested(route_id: String) ## 锻造路线购买意图(写口唯一归 MetaProgression.forge_buy_level) signal forge_unlock_requested(route_id: String) ## 锻造路线解锁意图(写口唯一归 MetaProgression.forge_buy_unlock) -signal forge_select_requested(route_id: String) ## 锻造路线选中意图(写口唯一归 MetaProgression.forge_select_weapon/armor) +signal forge_select_requested(route_id: String) ## 锻造路线选中意图(写口唯一归 MetaProgression.forge_select) # 方向稿取色(柜台头带深棕/行板暖米;双色墨=亮板配深墨)——值单点收 UiKit const HEADER_BAND_COL := UiKit.CAMP_HEADER_BAND const ROW_PLATE_COL := UiKit.CAMP_ROW_PLATE -var _balance_label: Label -var _tree_holder: VBoxContainer # 树淬炼柜台内容(主界面——脊柱两行+钥匙/socket 行板+Fork 双态钮) -var _tree_buy_btns: Array[Dictionary] = [] # [{node_id, btn}]——树购买/解锁钮登记面(open/refresh 重建时刷新;探针消费) -var _tree_fork_btns: Array[Dictionary] = [] # [{group, side, btn}]——Fork 双态切换钮登记面(探针消费) -var _forge_lock_l: Label # 锻造工坊柜台锁定态行(钥匙未点亮) -var _forge_workbench: VBoxContainer # 锻造工坊工作间(钥匙点亮后展开——两区六路线行板) -var _forge_zone_weapon: VBoxContainer # 武器锻造区行板容器(三路线) -var _forge_zone_armor: VBoxContainer # 护甲锻造区行板容器(三路线) -var _forge_rows: Array[Dictionary] = [] # [{route_id, name_l, lvl_l, status_l, buy_btn, unlock_btn, select_btn, next_price, unlock_price}](open/refresh 重建;探针消费) -var _ws_scroll: ScrollContainer # 秘宝匣柜台内容容器(钥匙点亮后可见) -var _ws_lock_l: Label # 秘宝匣柜台锁定态行 -var _workshop_holder: VBoxContainer # 秘宝匣柜台行板(六行+同调装配段+Y 升级行) -var _workshop_rows: Array[Dictionary] = [] # [{item_id, progress_l, status_l}] -var _attune_header_l: Label # 同调装配段(Slots 点亮呈现) -var _attune_btns: Array[Dictionary] = [] # [{item_id, btn}]——装配/卸下切换钮(满位禁选随刷) -var _attune_nodes: Array[Control] = [] # 段内节点册(open_camp 重建时定向清理,不扰工坊六行) -var _refresh_l: Label # Y 刷新升级行(双入口之一——树上 ks_refresh 同账) -var _refresh_btn: Button -var _recent_list: Label # 最近冒险(逐行拼接 label——只读账面,随 open_camp 重建) + +class _TreeState extends RefCounted: + var _tree_holder: VBoxContainer # 树淬炼柜台内容(主界面——脊柱两行+钥匙/socket 行板+Fork 双态钮) + + +class _ForgeState extends RefCounted: + var _forge_lock_l: Label # 锻造工坊柜台锁定态行(钥匙未点亮) + var _forge_workbench: VBoxContainer # 锻造工坊工作间(钥匙点亮后展开——两区六路线行板) + var _forge_zone_weapon: VBoxContainer # 武器锻造区行板容器(三路线) + var _forge_zone_armor: VBoxContainer # 护甲锻造区行板容器(三路线) + var _forge_rows: Array[Dictionary] = [] # [{route_id, name_l, lvl_l, status_l, buy_btn, unlock_btn, select_btn, next_price, unlock_price}](open/refresh 重建;探针消费) + var _ws_scroll: ScrollContainer # 秘宝匣柜台内容容器(钥匙点亮后可见) + var _ws_lock_l: Label # 秘宝匣柜台锁定态行 + + +class _AttuneState extends RefCounted: + var _attune_header_l: Label # 同调装配段(Slots 点亮呈现) + var _attune_btns: Array[Dictionary] = [] # [{item_id, btn}]——装配/卸下切换钮(满位禁选随刷) + var _attune_nodes: Array[Control] = [] # 段内节点册(open_camp 重建时定向清理,不扰工坊六行) + + +class _SideState extends RefCounted: + var _refresh_l: Label # Y 刷新升级行(双入口之一——树上 ks_refresh 同账) + var _refresh_btn: Button + var _recent_list: Label # 最近冒险(逐行拼接 label——只读账面,随 open_camp 重建) + + +var _balance_label: Label # 探针直读冻结(S17①——住宿主) +var _tree_buy_btns: Array[Dictionary] = [] # [{node_id, btn}]——树购买/解锁钮登记面(open/refresh 重建时刷新;探针消费;住宿主) +var _tree_fork_btns: Array[Dictionary] = [] # [{group, side, btn}]——Fork 双态切换钮登记面(探针消费;住宿主) +var _workshop_holder: VBoxContainer # 秘宝匣柜台行板(六行+同调装配段+Y 升级行;探针直读冻结——住宿主) +var _workshop_rows: Array[Dictionary] = [] # [{item_id, progress_l, status_l}](探针消费;住宿主) +var _tree := _TreeState.new() +var _forge := _ForgeState.new() +var _attune := _AttuneState.new() +var _side := _SideState.new() func _init() -> void: @@ -61,6 +82,29 @@ func _init() -> void: root.add_child(col) # ── 顶栏:左铭牌(标题+副题)/ 右钥石徽 ── + _build_top_bar(col) + + # ── 四柜台并排(程序占位重排)── + # 宽度不足横向滚动(四柜 300 min 宽 > 窄窗可用宽时横滚兜底;纵向撑满余高) + var camp_scroll := ScrollContainer.new() + camp_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL + camp_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO + camp_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED + col.add_child(camp_scroll) + var mid := HBoxContainer.new() + mid.add_theme_constant_override("separation", 14) + mid.size_flags_vertical = Control.SIZE_EXPAND_FILL + camp_scroll.add_child(mid) + _build_tree_counter(mid) + _build_forge_counter(mid) + _build_workshop_counter(mid) + _build_atlas_counter(mid) + _build_bottom_bar(col) + visible = false + + +## 顶栏域:铭牌(标题+副题)+钥石徽章 +func _build_top_bar(col: VBoxContainer) -> void: var top := HBoxContainer.new() top.add_theme_constant_override("separation", 10) col.add_child(top) @@ -82,82 +126,78 @@ func _init() -> void: top.add_child(badge) _balance_label = badge.value_label - # ── 四柜台并排(程序占位重排)── - # 宽度不足横向滚动(四柜 300 min 宽 > 窄窗可用宽时横滚兜底;纵向撑满余高) - var camp_scroll := ScrollContainer.new() - camp_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL - camp_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO - camp_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED - col.add_child(camp_scroll) - var mid := HBoxContainer.new() - mid.add_theme_constant_override("separation", 14) - mid.size_flags_vertical = Control.SIZE_EXPAND_FILL - camp_scroll.add_child(mid) - # 柜一·树淬炼(树脊柱界面形态=主界面:一线一节点行板内升级+Fork 双态切换钮) - _tree_holder = VBoxContainer.new() - _tree_holder.add_theme_constant_override("separation", 6) +## 柜一·树淬炼(树脊柱界面形态=主界面:一线一节点行板内升级+Fork 双态切换钮) +func _build_tree_counter(mid: HBoxContainer) -> void: + _tree._tree_holder = VBoxContainer.new() + _tree._tree_holder.add_theme_constant_override("separation", 6) var tree_scroll := ScrollContainer.new() tree_scroll.custom_minimum_size = Vector2(300, 336) tree_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL tree_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED - tree_scroll.add_child(_tree_holder) + tree_scroll.add_child(_tree._tree_holder) var tree_panel := _counter_panel("天赋树 · 淬炼", "", tree_scroll, 300) tree_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL tree_panel.size_flags_vertical = Control.SIZE_FILL # 四栏等高:任一栏长高时四栏同步拉伸 mid.add_child(tree_panel) - # 柜二·锻造工坊(钥匙门——forge_workshop 点亮前锁定态;点亮=工作间两区六路线行板) - _forge_lock_l = UiKit.label("🔒 锻造工坊未开放\n\n点亮树上「锻造工坊」钥匙节点后展开工作间。\n(钥匙免费、无前置——树上直接点亮)",UiKit.FS_HINT, false, UiKit.STONE_DEEP) - _forge_lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER # 锁文在等高栏内垂直居中 - _forge_lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - _forge_lock_l.custom_minimum_size = Vector2(280, 0) - _forge_workbench = VBoxContainer.new() - _forge_workbench.add_theme_constant_override("separation", 6) - _forge_workbench.visible = false # 默认锁定态(钥匙门未判前零行板——门开才建) - _forge_zone_weapon = VBoxContainer.new() - _forge_zone_weapon.add_theme_constant_override("separation", 4) - _forge_zone_armor = VBoxContainer.new() - _forge_zone_armor.add_theme_constant_override("separation", 4) + +## 柜二·锻造工坊(钥匙门——forge_workshop 点亮前锁定态;点亮=工作间两区六路线行板) +func _build_forge_counter(mid: HBoxContainer) -> void: + _forge._forge_lock_l = UiKit.label("🔒 锻造工坊未开放\n\n点亮树上「锻造工坊」钥匙节点后展开工作间。\n(钥匙免费、无前置——树上直接点亮)",UiKit.FS_HINT, false, UiKit.STONE_DEEP) + _forge._forge_lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER # 锁文在等高栏内垂直居中 + _forge._forge_lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _forge._forge_lock_l.custom_minimum_size = Vector2(280, 0) + _forge._forge_workbench = VBoxContainer.new() + _forge._forge_workbench.add_theme_constant_override("separation", 6) + _forge._forge_workbench.visible = false # 默认锁定态(钥匙门未判前零行板——门开才建) + _forge._forge_zone_weapon = VBoxContainer.new() + _forge._forge_zone_weapon.add_theme_constant_override("separation", 4) + _forge._forge_zone_armor = VBoxContainer.new() + _forge._forge_zone_armor.add_theme_constant_override("separation", 4) var wb_wh := UiKit.label("── 武器锻造区 ──",UiKit.FS_HINT, true, UiKit.GOLD_INK) # 同款深金墨(对柜台板 5.21:1) - _forge_workbench.add_child(wb_wh) - _forge_workbench.add_child(_forge_zone_weapon) + _forge._forge_workbench.add_child(wb_wh) + _forge._forge_workbench.add_child(_forge._forge_zone_weapon) var wb_ah := UiKit.label("── 护甲锻造区 ──",UiKit.FS_HINT, true, UiKit.GOLD_INK) - _forge_workbench.add_child(wb_ah) - _forge_workbench.add_child(_forge_zone_armor) + _forge._forge_workbench.add_child(wb_ah) + _forge._forge_workbench.add_child(_forge._forge_zone_armor) var forge_holder := VBoxContainer.new() forge_holder.add_theme_constant_override("separation", 6) forge_holder.custom_minimum_size = Vector2(0, 336) # 四栏等高(与树/秘宝匣 scroll 336 min 对齐——锁定态仅锁文也撑住栏高) - forge_holder.add_child(_forge_lock_l) - forge_holder.add_child(_forge_workbench) + forge_holder.add_child(_forge._forge_lock_l) + forge_holder.add_child(_forge._forge_workbench) var forge_panel := _counter_panel("锻造工坊", UiKit.TEX_ICON_FORGE, forge_holder, 300) forge_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL forge_panel.size_flags_vertical = Control.SIZE_FILL # 四栏等高:任一栏长高时四栏同步拉伸 mid.add_child(forge_panel) - # 柜三·秘宝匣(钥匙门——ks_workshop 点亮前锁定态;点亮=现工坊内容整体迁入:六行+同调装配段+Y 升级行) - _ws_lock_l = UiKit.label("🔒 秘宝匣未开放\n\n点亮树上「秘宝匣」钥匙节点后展开。\n(前置=脊柱生命第 1 级)",UiKit.FS_HINT, false, UiKit.STONE_DEEP) - _ws_lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER # 锁文在等高栏内垂直居中 - _ws_lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - _ws_lock_l.custom_minimum_size = Vector2(280, 0) - _ws_scroll = ScrollContainer.new() - _ws_scroll.custom_minimum_size = Vector2(300, 336) - _ws_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL - _ws_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED + +## 柜三·秘宝匣(钥匙门——ks_workshop 点亮前锁定态;点亮=现工坊内容整体迁入:六行+同调装配段+Y 升级行) +func _build_workshop_counter(mid: HBoxContainer) -> void: + _forge._ws_lock_l = UiKit.label("🔒 秘宝匣未开放\n\n点亮树上「秘宝匣」钥匙节点后展开。\n(前置=脊柱生命第 1 级)",UiKit.FS_HINT, false, UiKit.STONE_DEEP) + _forge._ws_lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER # 锁文在等高栏内垂直居中 + _forge._ws_lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _forge._ws_lock_l.custom_minimum_size = Vector2(280, 0) + _forge._ws_scroll = ScrollContainer.new() + _forge._ws_scroll.custom_minimum_size = Vector2(300, 336) + _forge._ws_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL + _forge._ws_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED _workshop_holder = VBoxContainer.new() _workshop_holder.add_theme_constant_override("separation", 6) - _ws_scroll.add_child(_workshop_holder) + _forge._ws_scroll.add_child(_workshop_holder) var ws_holder := VBoxContainer.new() ws_holder.add_theme_constant_override("separation", 6) ws_holder.custom_minimum_size = Vector2(0, 336) # 四栏等高(锁定态 scroll 隐、仅锁文也撑住栏高) - ws_holder.add_child(_ws_lock_l) - ws_holder.add_child(_ws_scroll) + ws_holder.add_child(_forge._ws_lock_l) + ws_holder.add_child(_forge._ws_scroll) var ws_panel := _counter_panel("秘宝匣", UiKit.TEX_ICON_WORKSHOP, ws_holder, 300) ws_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL ws_panel.size_flags_vertical = Control.SIZE_FILL # 四栏等高:任一栏长高时四栏同步拉伸 mid.add_child(ws_panel) - # 柜四·奖励格图鉴(常开独立位——结构位预留,内容排 Alpha) + +## 柜四·奖励格图鉴(常开独立位——结构位预留,内容排 Alpha) +func _build_atlas_counter(mid: HBoxContainer) -> void: var atlas_holder := VBoxContainer.new() atlas_holder.custom_minimum_size = Vector2(0, 336) # 四栏等高(图鉴仅锁文也撑住栏高) var atlas_panel := _counter_panel("奖励格图鉴", UiKit.TEX_ICON_ATLAS, atlas_holder, 300) @@ -170,7 +210,9 @@ func _init() -> void: lock_l.custom_minimum_size = Vector2(280, 0) atlas_holder.add_child(lock_l) - # ── 底栏:关闭钮(左)/ 操作注记(右)+ 最近冒险条带 ── + +## 底栏域:关闭钮(左)/ 操作注记(右)+ 最近冒险条带 +func _build_bottom_bar(col: VBoxContainer) -> void: var close_btn := UiKit.button("关闭营地(回开始界面)") close_btn.pressed.connect(_emit_closed) var bottom := HBoxContainer.new() @@ -182,11 +224,10 @@ func _init() -> void: var caption := UiKit.label("树上点亮 · 工作间随钥匙展开 · 图鉴常开",UiKit.FS_HINT, false, UiKit.WARM_SOFT) bottom.add_child(caption) col.add_child(bottom) - _recent_list = UiKit.label("",UiKit.FS_HINT, false, UiKit.WARM_SOFT) - _recent_list.clip_text = true - _recent_list.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - col.add_child(_recent_list) - visible = false + _side._recent_list = UiKit.label("",UiKit.FS_HINT, false, UiKit.WARM_SOFT) + _side._recent_list.clip_text = true + _side._recent_list.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + col.add_child(_side._recent_list) # 柜台列面板(方向稿同构):PanelContainer 米板身 + 头带深棕(图标+标题暖白) @@ -220,7 +261,7 @@ func _counter_panel(title_text: String, icon_path: String, holder: Control, min_ # 树数据 = 组合根 _camp_data().tree 投影 [{node_id, display_name, vein, kind, level, price, can_buy, # currency}...] + fork_groups [{group, node_id, active_side, owned, can_buy, price, sides:[{side, display_name, effects}]}]) func _rebuild_tree(data: Dictionary) -> void: - for c in _tree_holder.get_children(): + for c in _tree._tree_holder.get_children(): c.queue_free() _tree_buy_btns.clear() _tree_fork_btns.clear() @@ -233,52 +274,62 @@ func _rebuild_tree(data: Dictionary) -> void: if vein != last_vein: last_vein = vein var vh := UiKit.label(String(vein_names.get(vein, vein)),UiKit.FS_HINT, true, UiKit.GOLD_INK) # GOLD_DEEP 对柜台板 2.67:1 越带,换深金墨 5.21:1 - _tree_holder.add_child(vh) - var hb := HBoxContainer.new() - hb.add_theme_constant_override("separation", 6) - var lvl := int(row.get("level", 0)) - var cap := int(row.get("cap", 1)) - var name_l := UiKit.label("%s Lv%d/%d" % [String(row.get("display_name", "")), lvl, cap], 13, false, UiKit.INK_LABEL) - name_l.custom_minimum_size = Vector2(150, 0) - name_l.clip_text = true - var gate := String(row.get("gate_text", "")) - if gate != "": - name_l.text += " 🔒" # 等级阈值前置未达标标记(R1 机制位——悬停看门槛;达标后随 refresh 消失) - name_l.tooltip_text = "等级门槛:%s" % gate - hb.add_child(name_l) + _tree._tree_holder.add_child(vh) if String(row.get("kind", "")) == "fork": continue # Fork 行去行板化——由 fork_groups 段承载双态切换钮 - var btn := UiKit.button("购买 %d 钥石" % int(row.get("price", 0)), 0.0) # 柜台行板紧凑钮豁免宽度基准 - btn.pressed.connect(func(): tree_buy_requested.emit(String(row.get("node_id", "")))) - btn.disabled = not bool(row.get("can_buy", false)) - hb.add_child(btn) - _tree_buy_btns.append({"node_id": String(row.get("node_id", "")), "btn": btn}) - _tree_holder.add_child(hb) + _tree._tree_holder.add_child(_tree_row_line(row)) for fg in forks: - var group := String(fg.get("group", "")) - var active := String(fg.get("active_side", "")) - var owned := bool(fg.get("owned", false)) - var fb := HBoxContainer.new() - fb.add_theme_constant_override("separation", 6) - var state_txt := "未解锁" - if owned: - state_txt = "激活:%s" % active - var gl := UiKit.label("%s[%s]" % [String(fg.get("display_name", group)), state_txt], 13, false, UiKit.INK_LABEL) - fb.add_child(gl) - if not owned: - var bb := UiKit.button("解锁 %d 钥石" % int(fg.get("price", 0)), 0.0) # 行板紧凑钮豁免 - bb.pressed.connect(func(): tree_buy_requested.emit(String(fg.get("node_id", "")))) - bb.disabled = not bool(fg.get("can_buy", false)) - fb.add_child(bb) - _tree_buy_btns.append({"node_id": String(fg.get("node_id", "")), "btn": bb}) - else: - for side_row in (fg.get("sides", []) as Array): - var sd := String(side_row.get("side", "")) - var sb := UiKit.button(String(side_row.get("display_name", sd)), 0.0) # 行板紧凑钮豁免 - sb.pressed.connect(func(): tree_fork_toggled.emit(group, sd)) - fb.add_child(sb) - _tree_fork_btns.append({"group": group, "side": sd, "btn": sb}) - _tree_holder.add_child(fb) + _tree._tree_holder.add_child(_tree_fork_line(fg)) + + +## 树行板构建域:节点名(含门槛标记)+购买钮(追加序与锚句随段整体搬迁) +func _tree_row_line(row: Dictionary) -> HBoxContainer: + var hb := HBoxContainer.new() + hb.add_theme_constant_override("separation", 6) + var lvl := int(row.get("level", 0)) + var cap := int(row.get("cap", 1)) + var name_l := UiKit.label("%s Lv%d/%d" % [String(row.get("display_name", "")), lvl, cap], 13, false, UiKit.INK_LABEL) + name_l.custom_minimum_size = Vector2(150, 0) + name_l.clip_text = true + var gate := String(row.get("gate_text", "")) + if gate != "": + name_l.text += " 🔒" # 等级阈值前置未达标标记(R1 机制位——悬停看门槛;达标后随 refresh 消失) + name_l.tooltip_text = "等级门槛:%s" % gate + hb.add_child(name_l) + var btn := UiKit.button("购买 %d 钥石" % int(row.get("price", 0)), 0.0) # 柜台行板紧凑钮豁免宽度基准 + btn.pressed.connect(func(): tree_buy_requested.emit(String(row.get("node_id", "")))) + btn.disabled = not bool(row.get("can_buy", false)) + hb.add_child(btn) + _tree_buy_btns.append({"node_id": String(row.get("node_id", "")), "btn": btn}) + return hb + + +## Fork 双态行板构建域:组状态文案+解锁钮或双态切换钮 +func _tree_fork_line(fg: Dictionary) -> HBoxContainer: + var group := String(fg.get("group", "")) + var active := String(fg.get("active_side", "")) + var owned := bool(fg.get("owned", false)) + var fb := HBoxContainer.new() + fb.add_theme_constant_override("separation", 6) + var state_txt := "未解锁" + if owned: + state_txt = "激活:%s" % active + var gl := UiKit.label("%s[%s]" % [String(fg.get("display_name", group)), state_txt], 13, false, UiKit.INK_LABEL) + fb.add_child(gl) + if not owned: + var bb := UiKit.button("解锁 %d 钥石" % int(fg.get("price", 0)), 0.0) # 行板紧凑钮豁免 + bb.pressed.connect(func(): tree_buy_requested.emit(String(fg.get("node_id", "")))) + bb.disabled = not bool(fg.get("can_buy", false)) + fb.add_child(bb) + _tree_buy_btns.append({"node_id": String(fg.get("node_id", "")), "btn": bb}) + else: + for side_row in (fg.get("sides", []) as Array): + var sd := String(side_row.get("side", "")) + var sb := UiKit.button(String(side_row.get("display_name", sd)), 0.0) # 行板紧凑钮豁免 + sb.pressed.connect(func(): tree_fork_toggled.emit(group, sd)) + fb.add_child(sb) + _tree_fork_btns.append({"group": group, "side": sd, "btn": sb}) + return fb # 工作间钥匙门(锻造工坊/秘宝匣柜台锁定态切换:节点点亮=展开、未点亮=锁定行; @@ -289,86 +340,106 @@ func _refresh_key_gates(data: Dictionary) -> void: lvl_by_id[String(row.get("node_id", ""))] = int(row.get("level", 0)) var forge_open := int(lvl_by_id.get("forge_workshop", 0)) >= 1 var ws_open := int(lvl_by_id.get("ks_workshop", 0)) >= 1 - _forge_lock_l.visible = not forge_open - _forge_workbench.visible = forge_open - _ws_lock_l.visible = not ws_open - _ws_scroll.visible = ws_open + _forge._forge_lock_l.visible = not forge_open + _forge._forge_workbench.visible = forge_open + _forge._ws_lock_l.visible = not ws_open + _forge._ws_scroll.visible = ws_open # 锻造工坊行板重建(两区六路线;open_camp/refresh_camp 调用。行结构仿秘宝匣行板: # 件名+等级+解锁态+购买/解锁/选中钮。UI 只发意图信号〔契约 9——零核心写调用、零 MetaProgression # 持有〕;钮禁用=常态防线(余额/未解锁/满级/已选中),兜底拒绝可见归 presenter pattern 1) func _rebuild_forge(data: Dictionary) -> void: - for zone in [_forge_zone_weapon, _forge_zone_armor]: + for zone in [_forge._forge_zone_weapon, _forge._forge_zone_armor]: for c in zone.get_children(): c.queue_free() - _forge_rows.clear() - if not _forge_workbench.visible: + _forge._forge_rows.clear() + if not _forge._forge_workbench.visible: return # 锁定态(钥匙未点亮):零行板零购买路径——占位纪律延续 var bal := int(data.get("balance", 0)) for row in data.get("forge", []): - var rid := String(row.get("route_id", "")) var kind := String(row.get("kind", "")) - var zone: VBoxContainer = _forge_zone_weapon if kind == "weapon" else _forge_zone_armor - var unlocked := bool(row.get("unlocked", false)) - var initial := bool(row.get("initial_owned", false)) - var lvl := int(row.get("level", 0)) - var cap_v := int(row.get("cap", 0)) - var next_price := int(row.get("next_price", 0)) - var unlock_price := int(row.get("unlock_price", 0)) - var card := PanelContainer.new() - card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER, UiKit.RADIUS_OBJECT, 1, Vector4(10, 4, 10, 4))) - var line := HBoxContainer.new() - line.add_theme_constant_override("separation", 6) - card.add_child(line) - var name_l := UiKit.label(String(row.get("display_name", "")), UiKit.FS_HINT, true, UiKit.INK) - name_l.custom_minimum_size = Vector2(104, 0) - name_l.clip_text = true - name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - line.add_child(name_l) - var lvl_l := UiKit.label("Lv%d/%d" % [lvl, cap_v], 13, false, UiKit.INK_LABEL) - lvl_l.custom_minimum_size = Vector2(44, 0) - line.add_child(lvl_l) - # 解锁态/解锁通道呈现(并联双通道:练满前一档 or 一次性解锁价直购) - var st_text := "已解锁" - if not unlocked: - if initial: - st_text = "未解锁" - else: - st_text = "未解锁 · 练满%s 或 %d 钥石直购" % [String(row.get("prereq_name", "")), unlock_price] - elif lvl >= cap_v: - st_text = "已满级" - var status_l := UiKit.label(st_text, UiKit.FS_HINT, false, UiKit.STONE_DEEP) - status_l.clip_text = true - status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL - line.add_child(status_l) - # 购买钮(已解锁未满级——几何价显示;余额不足禁用) - var buy_btn: Button = null - if unlocked and lvl < cap_v: - buy_btn = UiKit.button("锻造 %d" % next_price, 0.0) # 行板紧凑钮豁免 - buy_btn.pressed.connect(forge_buy_requested.emit.bind(rid)) - buy_btn.disabled = bal < next_price - line.add_child(buy_btn) - # 解锁钮(待解锁非初始——一次性解锁价) - var unlock_btn: Button = null - if not unlocked and not initial: - unlock_btn = UiKit.button("解锁 %d" % unlock_price, 0.0) # 行板紧凑钮豁免 - unlock_btn.pressed.connect(forge_unlock_requested.emit.bind(rid)) - unlock_btn.disabled = bal < unlock_price - line.add_child(unlock_btn) - # 选中钮(已解锁——免费切换;当前=禁用态标记) - var select_btn: Button = null - if unlocked: - var sel := bool(row.get("selected", false)) - select_btn = UiKit.button("当前" if sel else "选中", 0.0) # 行板紧凑钮豁免 - select_btn.pressed.connect(forge_select_requested.emit.bind(rid)) - select_btn.disabled = sel - line.add_child(select_btn) - zone.add_child(card) - _forge_rows.append({"route_id": rid, "name_l": name_l, "lvl_l": lvl_l, "status_l": status_l, - "buy_btn": buy_btn, "unlock_btn": unlock_btn, "select_btn": select_btn, - "next_price": next_price, "unlock_price": unlock_price}) + var zone: VBoxContainer = _forge._forge_zone_weapon if kind == "weapon" else _forge._forge_zone_armor + var card := _forge_route_card(row, bal) + zone.add_child(card["card"]) + _forge._forge_rows.append(card) + + +## 锻造行板构建域:单路线卡(件名+等级+状态+购买/解锁/选中钮——并联双通道呈现面) +func _forge_route_card(row: Dictionary, bal: int) -> Dictionary: + var d := _forge_route_scalars(row) + var card := PanelContainer.new() + card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER, UiKit.RADIUS_OBJECT, 1, Vector4(10, 4, 10, 4))) + var line := HBoxContainer.new() + line.add_theme_constant_override("separation", 6) + card.add_child(line) + var name_l := UiKit.label(String(row.get("display_name", "")), UiKit.FS_HINT, true, UiKit.INK) + name_l.custom_minimum_size = Vector2(104, 0) + name_l.clip_text = true + name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + line.add_child(name_l) + var lvl_l := UiKit.label("Lv%d/%d" % [d["lvl"], d["cap_v"]], 13, false, UiKit.INK_LABEL) + lvl_l.custom_minimum_size = Vector2(44, 0) + line.add_child(lvl_l) + var status_l := UiKit.label(_forge_status_text(d, row), UiKit.FS_HINT, false, UiKit.STONE_DEEP) + status_l.clip_text = true + status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL + line.add_child(status_l) + var btns := _forge_route_buttons(d, row, bal, line) + return {"card": card, "route_id": d["rid"], "name_l": name_l, "lvl_l": lvl_l, "status_l": status_l, + "buy_btn": btns["buy_btn"], "unlock_btn": btns["unlock_btn"], "select_btn": btns["select_btn"], + "next_price": d["next_price"], "unlock_price": d["unlock_price"]} + + +## 锻造行标量束:路线 id/解锁态/初始持有/等级/cap/几何价/解锁价一次取数 +func _forge_route_scalars(row: Dictionary) -> Dictionary: + return { + "rid": String(row.get("route_id", "")), + "unlocked": bool(row.get("unlocked", false)), + "initial": bool(row.get("initial_owned", false)), + "lvl": int(row.get("level", 0)), + "cap_v": int(row.get("cap", 0)), + "next_price": int(row.get("next_price", 0)), + "unlock_price": int(row.get("unlock_price", 0)), + } + + +## 状态文案域(并联双通道:练满前一档 or 一次性解锁价直购;已满级态) +func _forge_status_text(d: Dictionary, row: Dictionary) -> String: + var st_text := "已解锁" + if not d["unlocked"]: + if d["initial"]: + st_text = "未解锁" + else: + st_text = "未解锁 · 练满%s 或 %d 钥石直购" % [String(row.get("prereq_name", "")), d["unlock_price"]] + elif d["lvl"] >= d["cap_v"]: + st_text = "已满级" + return st_text + + +## 三钮域:购买(已解锁未满级——余额不足禁用)/解锁(待解锁非初始——余额不足禁用)/选中(已解锁——当前=禁用态标记) +func _forge_route_buttons(d: Dictionary, row: Dictionary, bal: int, line: HBoxContainer) -> Dictionary: + var buy_btn: Button = null + if d["unlocked"] and d["lvl"] < d["cap_v"]: + buy_btn = UiKit.button("锻造 %d" % d["next_price"], 0.0) # 行板紧凑钮豁免 + buy_btn.pressed.connect(forge_buy_requested.emit.bind(d["rid"])) + buy_btn.disabled = bal < d["next_price"] + line.add_child(buy_btn) + var unlock_btn: Button = null + if not d["unlocked"] and not d["initial"]: + unlock_btn = UiKit.button("解锁 %d" % d["unlock_price"], 0.0) # 行板紧凑钮豁免 + unlock_btn.pressed.connect(forge_unlock_requested.emit.bind(d["rid"])) + unlock_btn.disabled = bal < d["unlock_price"] + line.add_child(unlock_btn) + var select_btn: Button = null + if d["unlocked"]: + var sel := bool(row.get("selected", false)) + select_btn = UiKit.button("当前" if sel else "选中", 0.0) # 行板紧凑钮豁免 + select_btn.pressed.connect(forge_select_requested.emit.bind(d["rid"])) + select_btn.disabled = sel + line.add_child(select_btn) + return {"buy_btn": buy_btn, "unlock_btn": unlock_btn, "select_btn": select_btn} # 打开营地(全量重建四柜台行 + 同调装配段 + 最近冒险;树段与钥匙门随 refresh_camp 重建—— @@ -389,6 +460,12 @@ func refresh_camp(data: Dictionary) -> void: _rebuild_tree(data) _refresh_key_gates(data) _rebuild_forge(data) # 行板随刷重建(购买/解锁/选中后等级/价/禁用态同步) + _refresh_workshop_rows(data, balance) + _refresh_attune_segment(data, balance) + + +## 秘宝匣行板刷新域:进度列(解锁混合制三形态)与状态色(亮板双色墨制)与购买钮余额联动 +func _refresh_workshop_rows(data: Dictionary, balance: int) -> void: var ws_by_id := {} for row in data.get("workshop", []): ws_by_id[String(row.get("item_id", ""))] = row @@ -397,21 +474,7 @@ func refresh_camp(data: Dictionary) -> void: if row.is_empty(): continue var unlocked := bool(row.get("unlocked", false)) - # 秘宝匣进度列(解锁混合制三形态:钥石购买价/条件进度拼接/已入手) - var prog_text := "" - var mode := String(row.get("mode", "")) - if unlocked: - prog_text = "已入手" - elif mode == "keystone": - prog_text = "钥石 %d" % int(row.get("price", 0)) - elif mode == "condition": - var parts: Array[String] = [] - for req_v in (row.get("reqs", []) as Array): - var req: Dictionary[Variant, Variant] = req_v as Dictionary - var lbl := String(UiKit.TREASURE_REQ_LABELS.get(String(req.get("key", "")), "条件")) - parts.append("%s %d/%d" % [lbl, int(req.get("progress", 0)), int(req.get("threshold", 0))]) - prog_text = "、".join(parts) - card.progress_l.text = prog_text + card.progress_l.text = _workshop_progress_text(row, unlocked) # 状态色用亮板双色墨制(深墨族替代暖白族——亮板上暖白不可读) var st_text := "未解锁" var st_col: Color = UiKit.STONE_DEEP @@ -424,111 +487,148 @@ func refresh_camp(data: Dictionary) -> void: # 购买钮余额联动(禁用态=余额不足,常态防线;pattern 1 兜底拒绝可见归 presenter) if card.get("buy_btn") != null: card.buy_btn.disabled = balance < int(card.get("price", 0)) - # 同调装配段状态就地刷(Slots 点亮/满位禁选/Y 升级行余额联动) + + +## 进度列文案域(解锁混合制三形态:钥石购买价/条件进度拼接/已入手) +func _workshop_progress_text(row: Dictionary, unlocked: bool) -> String: + if unlocked: + return "已入手" + var mode := String(row.get("mode", "")) + if mode == "keystone": + return "钥石 %d" % int(row.get("price", 0)) + if mode == "condition": + var parts: Array[String] = [] + for req_v in (row.get("reqs", []) as Array): + var req: Dictionary[Variant, Variant] = req_v as Dictionary + var lbl := String(UiKit.TREASURE_REQ_LABELS.get(String(req.get("key", "")), "条件")) + parts.append("%s %d/%d" % [lbl, int(req.get("progress", 0)), int(req.get("threshold", 0))]) + return "、".join(parts) + return "" + + +## 同调装配段刷新域(Slots 点亮/满位禁选/Y 升级行余额联动——就地刷) +func _refresh_attune_segment(data: Dictionary, balance: int) -> void: var att: Dictionary[Variant, Variant] = data.get("attune", {}) - if _attune_header_l != null: + if _attune._attune_header_l != null: var slots_n := int(att.get("slots", 0)) var eq: Array[Variant] = att.get("equipped", []) - _attune_header_l.text = "── 同调装配 Slots %d/%d ──" % [eq.size(), slots_n] + _attune._attune_header_l.text = "── 同调装配 Slots %d/%d ──" % [eq.size(), slots_n] var eq_set := {} for v in eq: eq_set[String(v)] = true - for card2 in _attune_btns: + for card2 in _attune._attune_btns: var on: bool = eq_set.has(String(card2.item_id)) card2.btn.text = "卸下" if on else "装配" # 满位禁选+未解锁禁装(已装件恒可卸下;unlocked 快照随 rebuild 刷新) card2.btn.disabled = (not on and eq.size() >= slots_n) or (not on and not bool(card2.get("unlocked", false))) - if _refresh_l != null: + if _side._refresh_l != null: var rf: Dictionary[Variant, Variant] = att.get("refresh", {}) - _refresh_l.text = "三选一刷新 +%d 次/冒险 · Lv%d" % [int(rf.get("charges", 0)), int(rf.get("level", 0))] - _refresh_btn.text = "已满级" if bool(rf.get("maxed", false)) else "升级 %d 钥石" % int(rf.get("price", 0)) - _refresh_btn.disabled = bool(rf.get("maxed", false)) or balance < int(rf.get("price", 0)) + _side._refresh_l.text = "三选一刷新 +%d 次/冒险 · Lv%d" % [int(rf.get("charges", 0)), int(rf.get("level", 0))] + _side._refresh_btn.text = "已满级" if bool(rf.get("maxed", false)) else "升级 %d 钥石" % int(rf.get("price", 0)) + _side._refresh_btn.disabled = bool(rf.get("maxed", false)) or balance < int(rf.get("price", 0)) func _rebuild_workshop(data: Dictionary) -> void: _workshop_rows.clear() for child in _workshop_holder.get_children(): child.queue_free() - _attune_nodes.clear() - _attune_btns.clear() - _attune_header_l = null - _refresh_l = null - _refresh_btn = null + _attune._attune_nodes.clear() + _attune._attune_btns.clear() + _attune._attune_header_l = null + _side._refresh_l = null + _side._refresh_btn = null for row in data.get("workshop", []): - var card := PanelContainer.new() - card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER,UiKit.RADIUS_OBJECT, 1, Vector4(10, 4, 10, 4))) - var line := HBoxContainer.new() - line.add_theme_constant_override("separation", 12) - card.add_child(line) - var tier_name := String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), "")) - var name_l := UiKit.label(String(row.get("display_name", "")) + ("·" + tier_name if tier_name != "" else ""),UiKit.FS_HINT, true, UiKit.INK) - name_l.custom_minimum_size = Vector2(132, 0) - name_l.clip_text = true - name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - name_l.tooltip_text = String(row.get("desc", "")) # 件效果摘要悬停可见(分档呈现契约——槽位档位=名后缀) - line.add_child(name_l) - var progress_l := UiKit.label("",UiKit.FS_HINT, false, UiKit.STONE_DEEP) # 同行价格墨同案 - progress_l.custom_minimum_size = Vector2(150, 0) - progress_l.clip_text = true # 营地溢出修——文字全宽不撑柜 - progress_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - line.add_child(progress_l) - # 状态色用亮板双色墨制(深墨族替代暖白族)——色随刷新态条件赋值,非机械两行式 - var status_l := UiKit.label("",UiKit.FS_HINT, true, UiKit.STONE_DEEP) - status_l.clip_text = true # 同上——「已解锁 · 能力封存中」不撑柜(余宽吸收者) - status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL - line.add_child(status_l) - # 钥石购买钮(购买件且未解锁时在列;写口唯一=buy_treasure_unlock, - # 经 buy_requested(item_id) 信号走 presenter 分支;禁用态=余额不足) - var buy_btn: Button = null - if not bool(row.get("unlocked", false)) and String(row.get("mode", "")) == "keystone": - buy_btn = UiKit.button("购买 %d" % int(row.get("price", 0)), 0.0) # 行板紧凑钮豁免 - buy_btn.pressed.connect(_emit_buy_requested.bind(String(row.get("item_id", "")))) - line.add_child(buy_btn) - _workshop_holder.add_child(card) - _workshop_rows.append({"item_id": String(row.get("item_id", "")), "progress_l": progress_l, "status_l": status_l, "buy_btn": buy_btn, "price": int(row.get("price", 0)), "unlocked": bool(row.get("unlocked", false))}) + var card := _workshop_card(row) + _workshop_holder.add_child(card["card"]) + _workshop_rows.append(card) _rebuild_attune(data) +## 秘宝匣行板构建域:件名(分档后缀+效果摘要悬停)+进度列+状态列+钥石购买钮(购买件未解锁在列) +func _workshop_card(row: Dictionary) -> Dictionary: + var card := PanelContainer.new() + card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER,UiKit.RADIUS_OBJECT, 1, Vector4(10, 4, 10, 4))) + var line := HBoxContainer.new() + line.add_theme_constant_override("separation", 12) + card.add_child(line) + var tier_name := String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), "")) + var name_l := UiKit.label(String(row.get("display_name", "")) + ("·" + tier_name if tier_name != "" else ""),UiKit.FS_HINT, true, UiKit.INK) + name_l.custom_minimum_size = Vector2(132, 0) + name_l.clip_text = true + name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + name_l.tooltip_text = String(row.get("desc", "")) # 件效果摘要悬停可见(分档呈现契约——槽位档位=名后缀) + line.add_child(name_l) + var progress_l := UiKit.label("",UiKit.FS_HINT, false, UiKit.STONE_DEEP) # 同行价格墨同案 + progress_l.custom_minimum_size = Vector2(150, 0) + progress_l.clip_text = true # 营地溢出修——文字全宽不撑柜 + progress_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + line.add_child(progress_l) + # 状态色用亮板双色墨制(深墨族替代暖白族)——色随刷新态条件赋值,非机械两行式 + var status_l := UiKit.label("",UiKit.FS_HINT, true, UiKit.STONE_DEEP) + status_l.clip_text = true # 同上——「已解锁 · 能力封存中」不撑柜(余宽吸收者) + status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL + line.add_child(status_l) + # 钥石购买钮(购买件且未解锁时在列;写口唯一=buy_treasure_unlock, + # 经 buy_requested(item_id) 信号走 presenter 分支;禁用态=余额不足) + var buy_btn: Button = null + if not bool(row.get("unlocked", false)) and String(row.get("mode", "")) == "keystone": + buy_btn = UiKit.button("购买 %d" % int(row.get("price", 0)), 0.0) # 行板紧凑钮豁免 + buy_btn.pressed.connect(_emit_buy_requested.bind(String(row.get("item_id", "")))) + line.add_child(buy_btn) + return {"item_id": String(row.get("item_id", "")), "progress_l": progress_l, "status_l": status_l, "buy_btn": buy_btn, "price": int(row.get("price", 0)), "unlocked": bool(row.get("unlocked", false)), "card": card} + + # 同调装配段重建(秘宝匣柜台尾部段;Slots 点亮+ # 九件行板+Y 升级行,程序占位面) func _rebuild_attune(data: Dictionary) -> void: var att: Dictionary[Variant, Variant] = data.get("attune", {}) - _attune_header_l = UiKit.label("",UiKit.FS_HINT, true, UiKit.INK) - _workshop_holder.add_child(_attune_header_l) - _attune_nodes.append(_attune_header_l) + _attune._attune_header_l = UiKit.label("",UiKit.FS_HINT, true, UiKit.INK) + _workshop_holder.add_child(_attune._attune_header_l) + _attune._attune_nodes.append(_attune._attune_header_l) for row in att.get("items", []): - var iid := String(row.get("item_id", "")) - var line := HBoxContainer.new() - line.add_theme_constant_override("separation", 6) - var name_l := UiKit.label(String(row.get("display_name", "")),UiKit.FS_HINT, true, UiKit.INK) - name_l.custom_minimum_size = Vector2(96, 0) - name_l.clip_text = true - name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - line.add_child(name_l) - var form_l := UiKit.label("被动" if String(row.get("form", "")) == "passive" else "主动·充能",UiKit.FS_CAPTION, false, UiKit.STONE_DEEP) - form_l.custom_minimum_size = Vector2(58, 0) - line.add_child(form_l) - # 档位标签(槽位档位显示:件档=普通/优秀/稀有,装入须件档≤格档契约 2 的呈现半边) - var tier_l := UiKit.label(String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), "—")),UiKit.FS_CAPTION, false, UiKit.STONE_DEEP) - tier_l.custom_minimum_size = Vector2(36, 0) - line.add_child(tier_l) - var btn := UiKit.button("装配", 0.0) # 行板紧凑钮豁免 - btn.pressed.connect(attune_toggled.emit.bind(iid)) - btn.disabled = not bool(row.get("unlocked", false)) # 未解锁禁装(解锁为装配前置——toggle 写口同守卫) - line.add_child(btn) + var line := _attune_item_line(row) _workshop_holder.add_child(line) - _attune_nodes.append(line) - _attune_btns.append({"item_id": iid, "btn": btn, "unlocked": bool(row.get("unlocked", false)), "equipped": bool(row.get("equipped", false))}) + _attune._attune_nodes.append(line) + _refresh_attune_tail(att) + + +## 同调件行板构建域:件名+形态+档位+装配切换钮(未解锁禁装——toggle 写口同守卫) +func _attune_item_line(row: Dictionary) -> HBoxContainer: + var iid := String(row.get("item_id", "")) + var line := HBoxContainer.new() + line.add_theme_constant_override("separation", 6) + var name_l := UiKit.label(String(row.get("display_name", "")),UiKit.FS_HINT, true, UiKit.INK) + name_l.custom_minimum_size = Vector2(96, 0) + name_l.clip_text = true + name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + line.add_child(name_l) + var form_l := UiKit.label("被动" if String(row.get("form", "")) == "passive" else "主动·充能",UiKit.FS_CAPTION, false, UiKit.STONE_DEEP) + form_l.custom_minimum_size = Vector2(58, 0) + line.add_child(form_l) + # 档位标签(槽位档位显示:件档=普通/优秀/稀有,装入须件档≤格档契约 2 的呈现半边) + var tier_l := UiKit.label(String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), "—")),UiKit.FS_CAPTION, false, UiKit.STONE_DEEP) + tier_l.custom_minimum_size = Vector2(36, 0) + line.add_child(tier_l) + var btn := UiKit.button("装配", 0.0) # 行板紧凑钮豁免 + btn.pressed.connect(attune_toggled.emit.bind(iid)) + btn.disabled = not bool(row.get("unlocked", false)) # 未解锁禁装(解锁为装配前置——toggle 写口同守卫) + line.add_child(btn) + _attune._attune_btns.append({"item_id": iid, "btn": btn, "unlocked": bool(row.get("unlocked", false)), "equipped": bool(row.get("equipped", false))}) + return line + + +## Y 升级尾行构建域(双入口之一——树上 ks_refresh 同账) +func _refresh_attune_tail(att: Dictionary) -> void: var refresh_line := HBoxContainer.new() refresh_line.add_theme_constant_override("separation", 6) - _refresh_l = UiKit.label("",UiKit.FS_CAPTION, false, UiKit.STONE_DEEP) - refresh_line.add_child(_refresh_l) - _refresh_btn = UiKit.button("升级", 0.0) # 行板紧凑钮豁免 - _refresh_btn.pressed.connect(_emit_buy_requested.bind("attune_refresh")) - refresh_line.add_child(_refresh_btn) + _side._refresh_l = UiKit.label("",UiKit.FS_CAPTION, false, UiKit.STONE_DEEP) + refresh_line.add_child(_side._refresh_l) + _side._refresh_btn = UiKit.button("升级", 0.0) # 行板紧凑钮豁免 + _side._refresh_btn.pressed.connect(_emit_buy_requested.bind("attune_refresh")) + refresh_line.add_child(_side._refresh_btn) _workshop_holder.add_child(refresh_line) - _attune_nodes.append(refresh_line) + _attune._attune_nodes.append(refresh_line) func _rebuild_recent(data: Dictionary) -> void: @@ -544,7 +644,7 @@ func _rebuild_recent(data: Dictionary) -> void: verdict = "放弃" lines.append("最近冒险:%s · 完成 %d 关 · 钥石 +%d · 种子 %d" % [verdict, int(s.get("levels_cleared", 0)), int(s.get("keystone_total", 0)), int(s.get("master_seed", 0))]) - _recent_list.text = " | ".join(lines) + _side._recent_list.text = " | ".join(lines) func _emit_closed() -> void: diff --git a/game/presentation/views/level_map_screen.gd b/game/presentation/views/level_map_screen.gd index 65a53761..b1cc31cf 100644 --- a/game/presentation/views/level_map_screen.gd +++ b/game/presentation/views/level_map_screen.gd @@ -31,35 +31,39 @@ const HIT_PAD := 6.0 # 图上命中外扩(指 const CONFIRM_WINDOW_S := 3.0 # 放弃确认态窗口(秒——超时回落) const COL_HP_BAR_BACK := UiKit.HP_BAR_BACK # 迷你血条深底(对暗面板可辨;值单点收 UiKit) -var _runner = null -var _title: Label -var _canvas: Control -var _level_label: Label -var _gp_value: Label -var _hp_value: Label -var _hp_bar: Control -var _attr_value: Label -var _bonus_row: HBoxContainer -var _bonus_value: Label -var _feat_value: Label # 特性持有行(组合根投喂逐件文案) -var _cand_ids: Array[int] = [] # 本停点候选 id 集(图上命中合法集) -var _content_size := Vector2(620, 240) # 图内容包络(open_choices 按图拟合——画布 min 尺寸随它) -var _hover_id := -1 # 悬停预承诺候选(-1 = 无) -var abandon_btn: Button # 放弃入口(可见性归组合根 set_abandon_available——只在冒险模式开) -var status_badge: Button # 状态面板入口角标(顶栏 GP 徽右侧;连接归组合根) -var _abandon_timer := ArmedTimer.new() # 放弃确认窗(_process 帧驱动倒计时机制收口;窗口=CONFIRM_WINDOW_S) -var _drawn_disp: Dictionary[int, Dictionary] = {} # 本停点实际绘制记录(_draw_graph 逐帧在实际绘制点重建——display_probe 消费面) -var _node_icons := {} # 节点图标贴图(load_tex 守卫——缺位 null → 字形回退,冒烟/无素材检出安全) -var _noise_rect: TextureRect # 屏底石纹(换层色板渐变承载面——null = 纯色回退无挂点) -var _tint_level := 0 # 当前已应用 tint 的层号(0 = 未应用) -var _layout_cache := {"nodes": [], "canvas_size": Vector2.ZERO, "pos": {}} # 布局缓存(_layout 纯函数于 (nodes, canvas)——键=nodes 内容等价+画布尺寸,图态换代或画布尺寸变即自然失效) +## 内部状态件(u2 子域拆件——契约 9 职责轴):_MapState=选路屏运行态与图绘制态(单一变化原因= +## 选路呈现)。_layer_decor 域字典承载换层装饰三件(图标贴图/石纹面/tint 层号——装饰域归并形态)。 +## 探针直读冻结成员(_cand_ids/_hover_id/_content_size/_layout_cache/abandon_btn/status_badge)住宿主。 +class _MapState extends RefCounted: + var _runner = null + var _title: Label + var _canvas: Control + var _level_label: Label + var _gp_value: Label + var _hp_value: Label + var _hp_bar: Control + var _attr_value: Label + var _bonus_row: HBoxContainer + var _bonus_value: Label + var _feat_value: Label # 特性持有行(组合根投喂逐件文案) + var _abandon_timer := ArmedTimer.new() # 放弃确认窗(_process 帧驱动倒计时机制收口;窗口=CONFIRM_WINDOW_S) + var _drawn_disp: Dictionary[int, Dictionary] = {} # 本停点实际绘制记录(_draw_graph 逐帧在实际绘制点重建——display_probe 消费面) + var _layer_decor := {"node_icons": {}, "noise_rect": null, "tint_level": 0} # 换层装饰域(节点图标贴图 load_tex 守卫缺位 null→字形回退/屏底石纹面/当前已应用 tint 层号 0=未应用) + +var _cand_ids: Array[int] = [] # 本停点候选 id 集(图上命中合法集;探针直读冻结——住宿主) +var _content_size := Vector2(620, 240) # 图内容包络(open_choices 按图拟合——画布 min 尺寸随它;探针直读冻结) +var _hover_id := -1 # 悬停预承诺候选(-1 = 无;探针直读冻结) +var abandon_btn: Button # 放弃入口(可见性归组合根 set_abandon_available——只在冒险模式开;探针直读冻结) +var status_badge: Button # 状态面板入口角标(顶栏 GP 徽右侧;连接归组合根;探针直读冻结) +var _layout_cache := {"nodes": [], "canvas_size": Vector2.ZERO, "pos": {}} # 布局缓存(_layout 纯函数于 (nodes, canvas)——键=nodes 内容等价+画布尺寸,图态换代或画布尺寸变即自然失效;探针直读冻结) +var _map := _MapState.new() func _init() -> void: - _abandon_timer.expired.connect(_reset_abandon_confirm) + _map._abandon_timer.expired.connect(_reset_abandon_confirm) # 节点图标贴图装载(load_tex 守卫,缺位回退现行字形) for t in TEX_NODE_ICONS: - _node_icons[t] = UiKit.load_tex(TEX_NODE_ICONS[t]) + _map._layer_decor.node_icons[t] = UiKit.load_tex(TEX_NODE_ICONS[t]) # 独立屏底(spec Layout——demo #v-map 暖棕竖向 # 渐变(#4A3B2A 0%→#3A2E22 40%)+石纹弱质感 0.14+暗角聚焦;底纹缺位回退纯色) var base := UiKit.gradient_base(UiKit.BG_MAP_TOP, UiKit.BG_ROOT) @@ -67,7 +71,7 @@ func _init() -> void: var tex := UiKit.backdrop_tile() if tex != null: base.add_child(tex) - _noise_rect = tex # 世界侧 tint 承载面(换层色板渐变挂点——§2.7 切片档) + _map._layer_decor.noise_rect = tex # 世界侧 tint 承载面(换层色板渐变挂点——§2.7 切片档) base.add_child(UiKit.vignette_rect()) var center := CenterContainer.new() center.set_anchors_preset(Control.PRESET_FULL_RECT) @@ -75,9 +79,16 @@ func _init() -> void: var box := VBoxContainer.new() box.add_theme_constant_override("separation", 10) center.add_child(box) - # 顶栏语境(spec Layout:左关卡名 / 右 GP 常驻徽章——直角细线数据框; - # 对齐 demo ④:关卡名入框板+GP 徽章=烛光胶囊(同商店GP框)+ - # 左右两极排布) + _build_top_bar(box) + _build_canvas(box) + _build_status_bar(box) + visible = false + + +## 顶栏域(spec Layout:左关卡名 / 右 GP 常驻徽章——直角细线数据框; +## 对齐 demo ④:关卡名入框板+GP 徽章=烛光胶囊(同商店GP框)+ 左右两极排布; +## 状态角标置灰守卫归组合根 _sync_status_entry,信号连接归组合根——视图零连接) +func _build_top_bar(box: VBoxContainer) -> void: var top := HBoxContainer.new() top.add_theme_constant_override("separation", 12) top.size_flags_horizontal = Control.SIZE_EXPAND_FILL @@ -86,8 +97,8 @@ func _init() -> void: lvl_plate.add_theme_stylebox_override("panel", UiKit.panel_flat(UiKit.DARK_UPCOMING, UiKit.PANEL_BORDER, 0, 1, Vector4(12, 3, 12, 3))) top.add_child(lvl_plate) - _level_label = UiKit.label("",UiKit.FS_BODY, true, UiKit.WARM) - lvl_plate.add_child(_level_label) + _map._level_label = UiKit.label("",UiKit.FS_BODY, true, UiKit.WARM) + lvl_plate.add_child(_map._level_label) var top_sp := Control.new() top_sp.size_flags_horizontal = Control.SIZE_EXPAND_FILL top.add_child(top_sp) @@ -98,31 +109,34 @@ func _init() -> void: var gp_row := HBoxContainer.new() gp_row.add_theme_constant_override("separation", 6) gp_box.add_child(gp_row) - _gp_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_ON_GOLD) - gp_row.add_child(_gp_value) - # 状态面板入口角标(顶栏常驻;本屏只留状态——调试角标归全局浮层。 - # 置灰守卫归组合根 _sync_status_entry(其他模态开着=不可点;本屏自身的登记开态不计——has_open_excluding); - # 信号连接归组合根(视图零连接)) + _map._gp_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_ON_GOLD) + gp_row.add_child(_map._gp_value) status_badge = UiKit.button("状态", 0.0) status_badge.add_theme_font_size_override("font_size", UiKit.FS_HINT) status_badge.tooltip_text = "查看本局冒险进度(只读)" status_badge.size_flags_vertical = Control.SIZE_SHRINK_CENTER top.add_child(status_badge) - _title = UiKit.label(TITLE_FORK,UiKit.FS_SUB, true, UiKit.WARM) - _title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - box.add_child(_title) - # 节点图画布(图上点击 = 唯一提交通道——布局决策 2;mouse_filter STOP 接悬停/点击) - _canvas = Control.new() - _canvas.custom_minimum_size = Vector2(620, 240) - _canvas.mouse_filter = Control.MOUSE_FILTER_STOP - _canvas.draw.connect(_draw_graph.bind(_canvas)) - _canvas.gui_input.connect(_on_canvas_input) - box.add_child(_canvas) - # 底部状态栏(合并单条:亮米面板一站读齐=头像+生命+属性+本关加成/流派件——demo ④; - # 道具槽不摆=道具读数无关卡画面成文出口(读快照私域越权); - # _hp_value/_attr_value/_bonus_value/_feat_value 投喂键全部保留) - # 底栏定高纪律(对齐 demo——单条 ~85px 紧凑亮条;任何子件不再能把栏撑爆: - # 特性持有行限宽截断+tooltip 全文) + _map._title = UiKit.label(TITLE_FORK,UiKit.FS_SUB, true, UiKit.WARM) + _map._title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + box.add_child(_map._title) + + +## 节点图画布域(图上点击 = 唯一提交通道——布局决策 2;mouse_filter STOP 接悬停/点击) +func _build_canvas(box: VBoxContainer) -> void: + _map._canvas = Control.new() + _map._canvas.custom_minimum_size = Vector2(620, 240) + _map._canvas.mouse_filter = Control.MOUSE_FILTER_STOP + _map._canvas.draw.connect(_draw_graph.bind(_map._canvas)) + _map._canvas.gui_input.connect(_on_canvas_input) + box.add_child(_map._canvas) + + +## 底部状态栏域(合并单条:亮米面板一站读齐=头像+生命+属性+本关加成/流派件——demo ④; +## 道具槽不摆=道具读数无关卡画面成文出口(读快照私域越权); +## _hp_value/_attr_value/_bonus_value/_feat_value 投喂键全部保留。 +## 底栏定高纪律(对齐 demo——单条 ~85px 紧凑亮条;任何子件不再能把栏撑爆: +## 特性持有行限宽截断+tooltip 全文)) +func _build_status_bar(box: VBoxContainer) -> void: var bottom := PanelContainer.new() bottom.custom_minimum_size = Vector2(0, 85) bottom.size_flags_vertical = Control.SIZE_SHRINK_END @@ -132,9 +146,37 @@ func _init() -> void: var strip := HBoxContainer.new() strip.add_theme_constant_override("separation", 18) bottom.add_child(strip) + strip.add_child(_build_face_rect()) + var life_box := VBoxContainer.new() + strip.add_child(life_box) + life_box.add_child(UiKit.label("生命",UiKit.FS_CAPTION, false, UiKit.INK_HINT)) + _map._hp_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE) + life_box.add_child(_map._hp_value) + _map._hp_bar = Control.new() + _map._hp_bar.custom_minimum_size = Vector2(130, 7) + _map._hp_bar.draw.connect(_draw_hp_bar.bind(_map._hp_bar)) + life_box.add_child(_map._hp_bar) + strip.add_child(_vdiv()) + var attr_box := VBoxContainer.new() + strip.add_child(attr_box) + attr_box.add_child(UiKit.label("属性",UiKit.FS_CAPTION, false, UiKit.INK_HINT)) + _map._attr_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE) + attr_box.add_child(_map._attr_value) + strip.add_child(_vdiv()) + _build_bonus_group(strip) + var spacer := Control.new() + spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL + strip.add_child(spacer) + abandon_btn = UiKit.danger_button("放弃", "", 90.0) # 小号危险钮 ~90×38 + abandon_btn.pressed.connect(_on_abandon_pressed) + abandon_btn.visible = false + strip.add_child(abandon_btn) + + +## 头像域:定高 48px=1×、宽随源图比自适应(源 46×48 → 46×48;纵横异倍消除) +func _build_face_rect() -> TextureRect: var face := TextureRect.new() var face_tex := UiKit.load_tex(UiKit.TEX_HERO_PORTRAIT) - # 定高 48px=1×、宽随源图比自适应(源 46×48 → 46×48;纵横异倍消除) var face_h := 48.0 var face_w := face_h if face_tex != null: @@ -145,50 +187,26 @@ func _init() -> void: face.expand_mode = TextureRect.EXPAND_IGNORE_SIZE face.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED face.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST - strip.add_child(face) - var life_box := VBoxContainer.new() - strip.add_child(life_box) - life_box.add_child(UiKit.label("生命",UiKit.FS_CAPTION, false, UiKit.INK_HINT)) - _hp_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE) - life_box.add_child(_hp_value) - _hp_bar = Control.new() - _hp_bar.custom_minimum_size = Vector2(130, 7) - _hp_bar.draw.connect(_draw_hp_bar.bind(_hp_bar)) - life_box.add_child(_hp_bar) - strip.add_child(_vdiv()) - var attr_box := VBoxContainer.new() - strip.add_child(attr_box) - attr_box.add_child(UiKit.label("属性",UiKit.FS_CAPTION, false, UiKit.INK_HINT)) - _attr_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE) - attr_box.add_child(_attr_value) - strip.add_child(_vdiv()) - # 本关加成框(零加成隐藏——spec 布局决策 4)+ 流派件持有行(组合根投喂) - _bonus_row = HBoxContainer.new() - strip.add_child(_bonus_row) + return face + + +## 本关加成框域(零加成隐藏——spec 布局决策 4)+ 流派件持有行(组合根投喂;限宽单行截断+tooltip 全文) +func _build_bonus_group(strip: HBoxContainer) -> void: + _map._bonus_row = HBoxContainer.new() + strip.add_child(_map._bonus_row) var bonus_box := VBoxContainer.new() - _bonus_row.add_child(bonus_box) + _map._bonus_row.add_child(bonus_box) var bonus_inner := VBoxContainer.new() bonus_box.add_child(bonus_inner) var bonus_cap := UiKit.label("属性值提升",UiKit.FS_CAPTION, false, UiKit.INK_HINT) # 深墨(亮板可读) bonus_inner.add_child(bonus_cap) - _bonus_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE) # 淡墨→深墨(同上) - bonus_inner.add_child(_bonus_value) - # 特性持有行(组合根投喂) - # 限宽单行截断+tooltip 全文; - # 墨色 INK_HINT(暖白系对亮板 ≈1:1 不可读——§7.2) - _feat_value = UiKit.label("",UiKit.FS_HINT, false, UiKit.INK_HINT) - _feat_value.custom_minimum_size = Vector2(240, 0) - _feat_value.clip_text = true - _feat_value.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - bonus_box.add_child(_feat_value) - var spacer := Control.new() - spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL - strip.add_child(spacer) - abandon_btn = UiKit.danger_button("放弃", "", 90.0) # 小号危险钮 ~90×38 - abandon_btn.pressed.connect(_on_abandon_pressed) - abandon_btn.visible = false - strip.add_child(abandon_btn) - visible = false + _map._bonus_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE) # 淡墨→深墨(同上) + bonus_inner.add_child(_map._bonus_value) + _map._feat_value = UiKit.label("",UiKit.FS_HINT, false, UiKit.INK_HINT) + _map._feat_value.custom_minimum_size = Vector2(240, 0) + _map._feat_value.clip_text = true + _map._feat_value.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + bonus_box.add_child(_map._feat_value) # 竖分隔线(底栏分组线——亮面板上低对比细线) @@ -202,10 +220,10 @@ static func _vdiv() -> ColorRect: func _draw_hp_bar(bar: Control) -> void: # 迷你血条(深底 UiKit.HP_BAR_BACK #28221C + 小人蓝填充——与战斗站桩血条同语言, # 「蓝只说己方」§4.2)。比例 = hero_hp / effective_max_hp - if _runner == null: + if _map._runner == null: return - var max_hp := maxi(1, _runner.effective_max_hp()) - var ratio := clampf(float(_runner.hero_hp) / float(max_hp), 0.0, 1.0) + var max_hp := maxi(1, _map._runner.effective_max_hp()) + var ratio := clampf(float(_map._runner.hero_hp) / float(max_hp), 0.0, 1.0) var full := Rect2(Vector2.ZERO, bar.size) bar.draw_rect(full, COL_HP_BAR_BACK) if ratio > 0.0: @@ -215,8 +233,8 @@ func _draw_hp_bar(bar: Control) -> void: # 放弃入口可见性(组合根在冒险启动时开、默认关——语义对象 = 整局冒险) # 特性持有投影(组合根投喂逐件文案;空文案=行隐藏) func set_feat_holdings(text: String) -> void: - _feat_value.text = text - _feat_value.tooltip_text = text # 截断全量兜底(限宽单行省略——悬停看全文) + _map._feat_value.text = text + _map._feat_value.tooltip_text = text # 截断全量兜底(限宽单行省略——悬停看全文) func set_abandon_available(v: bool) -> void: @@ -226,8 +244,8 @@ func set_abandon_available(v: bool) -> void: # 放弃二次确认(布局决策 3):首击转确认态(文案+危险红脉冲约 3 秒超时回落),二击才提交。 # 单击不提交——ui_probe 战斗中 pressed.emit() 单击路径保持「未受理」语义(战斗中拒绝归组合根)。 func _on_abandon_pressed() -> void: - if not _abandon_timer.is_armed(): - _abandon_timer.arm(CONFIRM_WINDOW_S) + if not _map._abandon_timer.is_armed(): + _map._abandon_timer.arm(CONFIRM_WINDOW_S) abandon_btn.text = "再点一次确认放弃——不可逆" if ArmedTimer.motion_still(): abandon_btn.modulate = Color(1, 1, 1, 0.75) # 减动效静态基色(脉冲层不再驱动) @@ -237,45 +255,45 @@ func _on_abandon_pressed() -> void: func _reset_abandon_confirm() -> void: - _abandon_timer.disarm() + _map._abandon_timer.disarm() abandon_btn.modulate = Color(1, 1, 1, 1) abandon_btn.text = "放弃" # 文案收敛 level-map 4 字上限(确认态转义面见 _on_abandon_pressed) func _process(delta: float) -> void: - if not _abandon_timer.is_armed(): + if not _map._abandon_timer.is_armed(): return - _abandon_timer.tick(delta) - if not _abandon_timer.is_armed(): + _map._abandon_timer.tick(delta) + if not _map._abandon_timer.is_armed(): return # 窗口到点回落(expired 信号已驱动 _reset_abandon_confirm) # 危险红边框脉冲(单段呼吸约 1Hz——闪烁基线 ≤3 次/秒内;减动效 = 静态半透不闪——档判据单点 motion_still) if ArmedTimer.motion_still(): abandon_btn.modulate = Color(1, 1, 1, 0.75) else: - var k := 0.5 + 0.5 * sin(_abandon_timer.elapsed * TAU) + var k := 0.5 + 0.5 * sin(_map._abandon_timer.elapsed * TAU) abandon_btn.modulate = Color(1, 1, 1, 0.65 + 0.35 * k) # 打开选路口(cands = 候选节点引用数组;runner 只读用于整图渲染与状态栏刷新) func open_choices(cands: Array, runner: LevelRunner) -> void: var was_visible := visible # 进入转场判据(就地刷新/模态栈恢复不重放淡入) - _runner = runner + _map._runner = runner _hover_id = -1 _reset_abandon_confirm() var visited: Array[int] = [] if runner != null: visited = runner.visited_ids() if cands.size() > 1: - _title.text = TITLE_FORK + _map._title.text = TITLE_FORK elif visited.is_empty(): - _title.text = TITLE_ENTRY # 入口停点(尚未走出第一步) + _map._title.text = TITLE_ENTRY # 入口停点(尚未走出第一步) else: - _title.text = TITLE_SINGLE + _map._title.text = TITLE_SINGLE _cand_ids = [] for cand in cands: _cand_ids.append(_id_of(cand)) - _fit_canvas(_runner.nodes()) - _apply_level_tint(int(_runner.level_row.get("level_index", 0))) + _fit_canvas(_map._runner.nodes()) + _apply_level_tint(int(_map._runner.level_row.get("level_index", 0))) visible = true if not was_visible: # 独立屏淡入(spec Transitions:约 200ms;alpha 淡入非明度变化——art-bible §2 明度恒定) @@ -302,7 +320,7 @@ func _fit_canvas(nodes: Array) -> void: _content_size = Vector2(LAYER_BASE_X + maxl * LAYER_STEP_X + NODE_HALF.x + 8.0, SLOT_BASE_Y + maxslot * SLOT_STEP_Y + NODE_HALF.y + 24.0) # +24 = 类型文字标签行 _content_size = _content_size.max(Vector2(620, 256)) # 下限(抬高容纳标签行) - _canvas.custom_minimum_size = _content_size + _map._canvas.custom_minimum_size = _content_size # 换层色板渐变挂点(art-bible §2.7 下探过渡切片档——关卡结算确认→下一关→独立屏淡入链路上的 @@ -313,39 +331,39 @@ const BOARD_TINTS := {1: Color(1, 1, 1), 2: Color(0.84, 0.99, 1.28), 3: Color(0. func _apply_level_tint(level_index: int) -> void: - if _noise_rect == null or level_index == _tint_level: + if _map._layer_decor.noise_rect == null or level_index == _map._layer_decor.tint_level: return - _tint_level = level_index + _map._layer_decor.tint_level = level_index var target: Color = BOARD_TINTS.get(level_index, Color(1, 1, 1)) var tw := create_tween() - tw.tween_property(_noise_rect, "modulate", + tw.tween_property(_map._layer_decor.noise_rect, "modulate", Color(target.r, target.g, target.b, UiKit.BACKDROP_TILE_ALPHA), UiKit.motion(0.45)) # 0.14 弱质感档随 tint 保持 # 状态栏投喂(命令式纪律——组合根经 level_map_status_payload 取成品后喂入;零推送信号订阅、零核心态拼装) func apply_status(payload: Dictionary) -> void: - _level_label.text = String(payload["level_label"]) - _gp_value.text = String(payload["gp_text"]) - _hp_value.text = String(payload["hp_text"]) + _map._level_label.text = String(payload["level_label"]) + _map._gp_value.text = String(payload["gp_text"]) + _map._hp_value.text = String(payload["hp_text"]) if bool(payload["hp_bar_redraw"]): - _hp_bar.queue_redraw() - _attr_value.text = String(payload["attr_text"]) - _bonus_value.text = String(payload["bonus_text"]) + _map._hp_bar.queue_redraw() + _map._attr_value.text = String(payload["attr_text"]) + _map._bonus_value.text = String(payload["bonus_text"]) # 特性持有行可见性(与加成行并框——任一非空整框显示;文案归组合根 set_feat_holdings) - _bonus_row.visible = bool(payload["bonus_row_visible"]) or _feat_value.text != "" + _map._bonus_row.visible = bool(payload["bonus_row_visible"]) or _map._feat_value.text != "" # ---- 图上命中(布局决策 2——唯一提交通道;探针 press_candidate 同路径) ---- func _on_canvas_input(ev: InputEvent) -> void: - if _runner == null or not visible: + if _map._runner == null or not visible: return if ev is InputEventMouseMotion: var nid := _candidate_at(ev.position) # gui_input 事件坐标 = 接收控件局部坐标(push_input 同成立) if nid != _hover_id: _hover_id = nid - _canvas.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND if nid >= 0 else Control.CURSOR_ARROW - _canvas.queue_redraw() + _map._canvas.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND if nid >= 0 else Control.CURSOR_ARROW + _map._canvas.queue_redraw() elif ev is InputEventMouseButton and ev.button_index == MOUSE_BUTTON_LEFT and ev.pressed: var nid2 := _candidate_at(ev.position) if nid2 >= 0: @@ -354,10 +372,10 @@ func _on_canvas_input(ev: InputEvent) -> void: # 候选命中:位置 = 布局公式(与 _draw_graph 同源),矩形 = 节点方块 + 外扩容差;非候选格不响应 func _candidate_at(local_pos: Vector2) -> int: - if _runner == null: + if _map._runner == null: return -1 - var nodes: Array[Variant] = _runner.nodes() - var pos := _layout(nodes, _canvas) + var nodes: Array[Variant] = _map._runner.nodes() + var pos := _layout(nodes, _map._canvas) for nid in _cand_ids: var node := _node_by_id(nid, nodes) if node.is_empty(): @@ -372,7 +390,7 @@ func _candidate_at(local_pos: Vector2) -> int: func _submit_choice(nid: int) -> void: # 独立屏分支:战斗/Boss 节点 = 出屏切回战斗屏;奖励/恢复/商店节点 = # 本屏保持激活——兑付覆盖层由节点事件在本屏域内弹出(模态栈遮挡恢复语义复用),兑付确认后回图刷新 - var node := _node_by_id(nid, _runner.nodes()) + var node := _node_by_id(nid, _map._runner.nodes()) var t := String(node.get("type", "")) if t != "reward" and t != "rest" and t != "shop": visible = false @@ -381,13 +399,13 @@ func _submit_choice(nid: int) -> void: # 探针复用:按候选序号走图上命中同路径(候选节点中心坐标 → 命中检测 → 提交——布局决策 2 探针面改造) func press_candidate(index: int) -> void: - if index < 0 or index >= _cand_ids.size() or _runner == null: + if index < 0 or index >= _cand_ids.size() or _map._runner == null: return - var nodes: Array[Variant] = _runner.nodes() + var nodes: Array[Variant] = _map._runner.nodes() var node := _node_by_id(int(_cand_ids[index]), nodes) if node.is_empty(): return - var pos: Vector2 = _layout(nodes, _canvas)[node] + var pos: Vector2 = _layout(nodes, _map._canvas)[node] var nid := _candidate_at(pos) # 节点中心必命中自身(命中面含节点方块) if nid >= 0: _submit_choice(nid) @@ -395,7 +413,7 @@ func press_candidate(index: int) -> void: func _id_of(node: Dictionary) -> int: # 编号显示经行进器读面(编号空间解析单一出口——单链 = 下标 / 图 = node_depth) - return _runner.node_id_of(node) + return _map._runner.node_id_of(node) # ---- 节点呈现决策面(信息迷雾 v2.1:类型仅在可见集内出示,Boss 标记不入雾)---- @@ -421,20 +439,48 @@ static func node_display(node: Dictionary, nid: int, type_vis: Array, # ---- 整图自绘(程序化占位——分层布局:层 = 入口 BFS 深度,同层垂直排布)---- func _draw_graph(canvas: Control) -> void: - if _runner == null or not visible: + if _map._runner == null or not visible: return - _drawn_disp.clear() # 绘制记录随本帧重建——只反映实际发生的绘制(接线坏的判据面) - var nodes: Array[Variant] = _runner.nodes() - var pos := _layout(nodes, canvas) - var font := get_theme_default_font() # 绘制路径字体单次取用(三处 draw_string 共享——零语义差) - var current_id: int = _runner.current_node_id() - # 当前节点 id(单链=下标 / 图=编号)——visited_ids() 已按编号空间解析 - var visited: Array[Variant] = _runner.visited_ids() - var cand_ids: Array[Variant] = _runner.next_candidate_ids() - var type_vis: Array[int] = _runner.type_visible_ids() # 本停点类型可见集(每帧一取——纯函数零状态) - # 连线(通道)先行画(方块覆盖其上);悬停预承诺 = 所属通道再增亮(加粗) - # 选路停点(候选非空)非候选通道降透明——金岔路从结构网分离(干线共线重叠缺陷的 - # 视觉主因:不同源通道共用同一垂直干线,金边与石色边焊接成「整列可走」假象) + _map._drawn_disp.clear() # 绘制记录随本帧重建——只反映实际发生的绘制(接线坏的判据面) + var ctx := _draw_context(canvas) + _draw_passage_layer(canvas, ctx) + for node in ctx["nodes"]: + _draw_room(canvas, node, ctx) + + +## 绘制上下文束(单次取数——布局/字体/编号空间/可见集,绘制域共享只读面) +func _draw_context(canvas: Control) -> Dictionary: + var nodes: Array[Variant] = _map._runner.nodes() + return { + "nodes": nodes, + "pos": _layout(nodes, canvas), + "font": get_theme_default_font(), # 绘制路径字体单次取用(三处 draw_string 共享——零语义差) + "current_id": _map._runner.current_node_id(), + # 当前节点 id(单链=下标 / 图=编号)——visited_ids() 已按编号空间解析 + "visited": _map._runner.visited_ids(), + "cand_ids": _map._runner.next_candidate_ids(), + "type_vis": _map._runner.type_visible_ids(), # 本停点类型可见集(每帧一取——纯函数零状态) + } + + +## 房间底色域(行进态三色:已到暗金/当前奖励金/未达暗——绘制域与图标域共享) +func _room_color(node: Dictionary, ctx: Dictionary) -> Color: + var nid := _id_of(node) + var col := COL_ROOM_FUTURE + if (ctx["visited"] as Array).has(nid): + col = COL_ROOM_DONE + if nid == ctx["current_id"]: + col = COL_ROOM_CURRENT + return col + + +## 通道层绘制域:连线先行画(方块覆盖其上);悬停预承诺 = 所属通道再增亮(加粗)。 +## 选路停点(候选非空)非候选通道降透明——金岔路从结构网分离(干线共线重叠缺陷的 +## 视觉主因:不同源通道共用同一垂直干线,金边与石色边焊接成「整列可走」假象) +func _draw_passage_layer(canvas: Control, ctx: Dictionary) -> void: + var nodes: Array = ctx["nodes"] + var pos: Dictionary = ctx["pos"] + var cand_ids: Array = ctx["cand_ids"] var dim_passages: bool = not cand_ids.is_empty() for node in nodes: var from: Vector2 = pos[node] @@ -442,61 +488,74 @@ func _draw_graph(canvas: Control) -> void: var target := _node_by_id(tid, nodes) if target.is_empty(): continue - var to: Vector2 = pos[target] - var lit: bool = _id_of(node) == current_id and cand_ids.has(tid) - var hovered: bool = lit and _hover_id == tid + var lit: bool = _id_of(node) == ctx["current_id"] and cand_ids.has(tid) var col := COL_PASSAGE_LIT if lit else (Color(COL_PASSAGE, 0.35) if dim_passages else COL_PASSAGE) - var width := 4.0 if hovered else (3.0 if lit else 2.0) # 2/3/4 三档 - _draw_passage(canvas, from, to, col, width) - # 房间方块 - for node in nodes: - var p: Vector2 = pos[node] - var t := String(node.get("type", "")) - var nid := _id_of(node) - var col := COL_ROOM_FUTURE - if visited.has(nid): - col = COL_ROOM_DONE - if nid == current_id: - col = COL_ROOM_CURRENT - var rect := Rect2(p - NODE_HALF, NODE_HALF * 2.0) - canvas.draw_rect(rect, col) - var disp := node_display(node, nid, type_vis, nid == current_id, cand_ids.has(nid)) - var rec: Dictionary[String, Variant] = {"draw_icon": false, "draw_label": false, - "label_text": "", "boss_border": false} - if bool(disp["boss_border"]): - # Boss 描边 = 紫赤 2px(§4.2 色相独占)+ 外缘 1px 深墨环(灰度冗余通道强制: - # 紫赤灰度 72 对石纹噪底 65 灰度不可辨;灰度/色弱下 Boss 环经深墨环可辨) - canvas.draw_rect(Rect2(rect.position - Vector2(3, 3), rect.size + Vector2(6, 6)), UiKit.INK_ROUTE, false, 1.0) - canvas.draw_rect(rect, COL_BOSS_BORDER, false, 2.0) - rec["boss_border"] = true - elif cand_ids.has(nid): - # 悬停预承诺 = 亮金描边加粗(4px),常态候选 = 亮金描边 2px(spec States 悬停预承诺行) - canvas.draw_rect(rect, COL_PASSAGE_LIT, false, 4.0 if _hover_id == nid else 2.0) - # 节点图标(20px 烘焙 1:1 消费;双色墨制=乘法调制——当前金底深墨,暗底系 WARM 原色; - # 贴图缺位 → 字形回退=贴图在位字形退役惯例) - if bool(disp["draw_icon"]): - rec["draw_icon"] = true - var icon: Texture2D = _node_icons.get(t) - if icon != null: - var tint := UiKit.ICON_TINT_ON_GOLD if col == COL_ROOM_CURRENT else Color(1.0, 1.0, 1.0) - canvas.draw_texture_rect(icon, Rect2(p - Vector2(NODE_ICON_PX, NODE_ICON_PX) / 2.0, - Vector2(NODE_ICON_PX, NODE_ICON_PX)), false, tint) - else: - var label := String(NODE_LABELS.get(t, "?")) - canvas.draw_string(font, p + Vector2(-6, 2), label, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, UiKit.INK_ROUTE) - # 编号(右上角——与图标占位区零重叠;双色墨随态) - var num_ink := UiKit.INK_ROUTE if col == COL_ROOM_CURRENT else UiKit.WARM - canvas.draw_string(font, Vector2(rect.end.x - 16.0, rect.position.y + 8.0), - str(nid), HORIZONTAL_ALIGNMENT_RIGHT, 14, 10, num_ink) - # 类型文字标签(demo ④:节点下方 11px;当前位=「当前位置」金、候选=类型名+「 · 可选」烛光黄、 - # 其余=类型名暖米弱化;文字+图标+形状三通道) - if bool(disp["draw_label"]): - rec["draw_label"] = true - rec["label_text"] = String(disp["label_text"]) - var type_ink := COL_ROOM_CURRENT if nid == current_id else (UiKit.CANDLE if cand_ids.has(nid) else UiKit.WARM_FAINT) - canvas.draw_string(font, Vector2(p.x - 44.0, rect.end.y + 15.0), - String(disp["label_text"]), HORIZONTAL_ALIGNMENT_CENTER, 88, 11, type_ink) - _drawn_disp[nid] = rec + var width := 4.0 if lit and _hover_id == tid else (3.0 if lit else 2.0) # 2/3/4 三档 + _draw_passage(canvas, from, pos[target], col, width) + + +## 房间方块域:底色随行进态+Boss/候选描边+图标/字形+编号+类型文字标签 +func _draw_room(canvas: Control, node: Dictionary, ctx: Dictionary) -> void: + var pos: Dictionary = ctx["pos"] + var p: Vector2 = pos[node] + var t := String(node.get("type", "")) + var nid := _id_of(node) + var col := _room_color(node, ctx) + var rect := Rect2(p - NODE_HALF, NODE_HALF * 2.0) + canvas.draw_rect(rect, col) + var disp := node_display(node, nid, ctx["type_vis"], nid == ctx["current_id"], (ctx["cand_ids"] as Array).has(nid)) + var rec: Dictionary[String, Variant] = {"draw_icon": false, "draw_label": false, + "label_text": "", "boss_border": false} + if bool(disp["boss_border"]): + # Boss 描边 = 紫赤 2px(§4.2 色相独占)+ 外缘 1px 深墨环(灰度冗余通道强制: + # 紫赤灰度 72 对石纹噪底 65 灰度不可辨;灰度/色弱下 Boss 环经深墨环可辨) + canvas.draw_rect(Rect2(rect.position - Vector2(3, 3), rect.size + Vector2(6, 6)), UiKit.INK_ROUTE, false, 1.0) + canvas.draw_rect(rect, COL_BOSS_BORDER, false, 2.0) + rec["boss_border"] = true + elif (ctx["cand_ids"] as Array).has(nid): + # 悬停预承诺 = 亮金描边加粗(4px),常态候选 = 亮金描边 2px(spec States 悬停预承诺行) + canvas.draw_rect(rect, COL_PASSAGE_LIT, false, 4.0 if _hover_id == nid else 2.0) + if _draw_room_icon(canvas, node, disp, ctx): + rec["draw_icon"] = true + # 编号(右上角——与图标占位区零重叠;双色墨随态) + var num_ink := UiKit.INK_ROUTE if col == COL_ROOM_CURRENT else UiKit.WARM + canvas.draw_string(ctx["font"], Vector2(rect.end.x - 16.0, rect.position.y + 8.0), + str(nid), HORIZONTAL_ALIGNMENT_RIGHT, 14, 10, num_ink) + var label_rec := _draw_room_label(canvas, rect, disp, nid, ctx) + rec["draw_label"] = bool(label_rec.get("draw_label", false)) + rec["label_text"] = String(label_rec.get("label_text", "")) + _map._drawn_disp[nid] = rec + + +## 节点图标域(20px 烘焙 1:1 消费;双色墨制=乘法调制——当前金底深墨,暗底系 WARM 原色; +## 贴图缺位 → 字形回退=贴图在位字形退役惯例) +func _draw_room_icon(canvas: Control, node: Dictionary, disp: Dictionary, ctx: Dictionary) -> bool: + if not bool(disp["draw_icon"]): + return false + var p: Vector2 = ctx["pos"][node] + var col := _room_color(node, ctx) + var t := String(node.get("type", "")) + var icon: Texture2D = _map._layer_decor.node_icons.get(t) + if icon != null: + var tint := UiKit.ICON_TINT_ON_GOLD if col == COL_ROOM_CURRENT else Color(1.0, 1.0, 1.0) + canvas.draw_texture_rect(icon, Rect2(p - Vector2(NODE_ICON_PX, NODE_ICON_PX) / 2.0, + Vector2(NODE_ICON_PX, NODE_ICON_PX)), false, tint) + else: + var label := String(NODE_LABELS.get(t, "?")) + canvas.draw_string(ctx["font"], p + Vector2(-6, 2), label, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, UiKit.INK_ROUTE) + return true + + +## 类型文字标签域(demo ④:节点下方 11px;当前位=「当前位置」金、候选=类型名+「 · 可选」烛光黄、 +## 其余=类型名暖米弱化;文字+图标+形状三通道) +func _draw_room_label(canvas: Control, rect: Rect2, disp: Dictionary, nid: int, ctx: Dictionary) -> Dictionary: + if not bool(disp["draw_label"]): + return {} + var p: Vector2 = rect.position + NODE_HALF # 节点中心反推(方块原点=中心−半宽高) + var type_ink := COL_ROOM_CURRENT if nid == ctx["current_id"] else (UiKit.CANDLE if (ctx["cand_ids"] as Array).has(nid) else UiKit.WARM_FAINT) + canvas.draw_string(ctx["font"], Vector2(p.x - 44.0, rect.end.y + 15.0), + String(disp["label_text"]), HORIZONTAL_ALIGNMENT_CENTER, 88, 11, type_ink) + return {"draw_label": true, "label_text": String(disp["label_text"])} func _layout(nodes: Array, canvas: Control) -> Dictionary: @@ -520,20 +579,20 @@ func _layout(nodes: Array, canvas: Control) -> Dictionary: # ---- 探针只读出口(形态仿 fx_probe 先例:只读快照) ---- -# 选路布局只读快照(node → 局部位置;画布固定取现行 _canvas,调用侧零私有域触达) +# 选路布局只读快照(node → 局部位置;画布固定取现行 _map._canvas,调用侧零私有域触达) func layout_probe(nodes: Array) -> Dictionary: - return _layout(nodes, _canvas) + return _layout(nodes, _map._canvas) # 路由画布只读快照(origin=全局原点;min_size=最小拟合尺寸——画布拟合断言消费面) func canvas_probe() -> Dictionary: - return {"origin": _canvas.get_global_position(), "min_size": _canvas.custom_minimum_size} + return {"origin": _map._canvas.get_global_position(), "min_size": _map._canvas.custom_minimum_size} # 类型可见性绘制只读记录(id → 本帧实际绘制事实:图标/文字/描边/文案;_draw_graph 在实际绘制点写入—— # 接线层断言消费点:绕开决策面直接画,这里就露出「雾下也画了」) func display_probe() -> Dictionary: - return _drawn_disp + return _map._drawn_disp # 分层(layout/fit 共享件):layer = 入口 BFS 深度;同层 slot 垂直排布;未达节点兜底排末层 @@ -577,7 +636,7 @@ func _entry(nodes: Array) -> Dictionary: func _out_ids(node: Dictionary, _nodes: Array) -> Array: - return _runner.node_out_ids(node) + return _map._runner.node_out_ids(node) func _node_by_id(nid: int, nodes: Array) -> Dictionary: