merge: M-1 迭代 1「测试兜底与表现先行」loop——六卡落籍(T1 单元测试九套件/T2 高墙 D 投影/T3 Fusion Pixel 字体/T4 关卡画面三拍板/T5 胜利结算 GP/T6 瓦片换色两套)
This commit is contained in:
commit
aa18eb9d96
33 files changed
+1811
-117
No files matched your search
@@ -449,11 +449,11 @@ func _on_battle_finished(payload: Dictionary) -> void:
|
||||
return
|
||||
"level_end":
|
||||
_end_overlay.open_victory(Narrator.battle_end_text("victory", true),
|
||||
"返回", board_view.get_global_rect())
|
||||
"返回", board_view.get_global_rect(), _gp_summary_text(payload))
|
||||
"standalone_end":
|
||||
if result == "victory":
|
||||
_end_overlay.open_victory(Narrator.battle_end_text("victory", false),
|
||||
"回到遭遇选择", board_view.get_global_rect())
|
||||
"回到遭遇选择", board_view.get_global_rect(), _gp_summary_text(payload))
|
||||
else:
|
||||
_end_overlay.open_defeat(Narrator.battle_end_text("defeat", false),
|
||||
"返回", board_view.get_global_rect())
|
||||
@@ -461,6 +461,13 @@ func _on_battle_finished(payload: Dictionary) -> void:
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
# 战利品行装配(battle.md OQ14——胜利总结结算展示 GP 总量+增量;账本语义零改动,纯呈现读数):
|
||||
# 增量 = battle_finished 载荷 gp_dropped(战斗域上抛);总量 = runner.gp 现值(LevelRunner 入账
|
||||
# 连接先于组合根 handler——此读数即战后入账完毕的总量)。
|
||||
func _gp_summary_text(payload: Dictionary) -> String:
|
||||
return "战利品 GP +%d · 合计 %d" % [int(payload.get("gp_dropped", 0)), int(runner.gp)]
|
||||
|
||||
|
||||
# ==================== core↔view 接线(战斗信号统一走这里)====================
|
||||
|
||||
func _wire_battle(settlement_ref) -> void:
|
||||
@@ -561,7 +568,7 @@ func _on_node_continue() -> void:
|
||||
|
||||
|
||||
func _on_choices_presented(candidates: Array) -> void:
|
||||
# 零自动推进的停点呈现:整幅节点图 + 候选按钮(战斗胜利/兑付确认统一到达此处)
|
||||
# 零自动推进的停点呈现:整幅节点图 + 图上点击选路(候选按钮退役 2026-09-28——战斗胜利/兑付确认统一到达此处)
|
||||
if runner == null:
|
||||
return
|
||||
log_view.append_bb(Narrator.route_line(candidates, runner))
|
||||
|
||||
@@ -40,6 +40,14 @@ static func build_battle_ui(root: Control, db) -> Dictionary:
|
||||
var settlement_fx := SettlementFxView.new()
|
||||
root.add_child(settlement_fx)
|
||||
|
||||
# CJK 字体随包(2026-09-27 用户拍板 Fusion Pixel OFL):Control 侧默认主题字体走 project.godot
|
||||
# gui/theme/custom_font;SubViewport 两世界不继承 Control 主题——显式注入挂点接线(C6 挂点链)。
|
||||
# 缺位守卫(load_font 返回 null = 引擎回退字体,与旧 Button 渲染同源,不炸)。
|
||||
var ui_font: Font = UiKit.load_font(UiKit.FONT_UI)
|
||||
if ui_font != null:
|
||||
stage.set_world_font(ui_font)
|
||||
board_zone.board_view.set_world_font(ui_font)
|
||||
|
||||
# GP/钥石余额位已随 2026-09-26 信息归属拍板退役(中/大循环资源迁出战斗屏——
|
||||
# GP 去向 = 关卡画面+战斗结算画面、钥石去向 = 结算画面+营地,呈现归对应 UX spec 批次)
|
||||
return {
|
||||
|
||||
@@ -16,6 +16,7 @@ const COL_WARM := UiKit.WARM
|
||||
var _dim: ColorRect
|
||||
var _seal: Panel
|
||||
var _verdict: Label
|
||||
var _gp_line: Label # 战利品行(OQ14 口径:胜利总结结算展示 GP 总量+增量——T5 接线 2026-09-28)
|
||||
var _review_btn: Button
|
||||
var _btn: Button
|
||||
var _board_rect := Rect2()
|
||||
@@ -36,6 +37,11 @@ func _init() -> void:
|
||||
_verdict.add_theme_color_override("font_color", COL_WARM)
|
||||
_verdict.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
box.add_child(_verdict)
|
||||
_gp_line = UiKit.label("", 16, true)
|
||||
_gp_line.add_theme_color_override("font_color", UiKit.GOLD)
|
||||
_gp_line.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_gp_line.visible = false
|
||||
box.add_child(_gp_line)
|
||||
_review_btn = UiKit.button("明细回看")
|
||||
_review_btn.pressed.connect(func(): review_pressed.emit())
|
||||
box.add_child(_review_btn)
|
||||
@@ -54,11 +60,15 @@ func _init() -> void:
|
||||
visible = false
|
||||
|
||||
|
||||
# 终局画面数据锚 = 战斗结算 判定行(verdict)——判定行文案由组合根装配传入
|
||||
func open_victory(verdict_line: String, continue_label: String, board_rect: Rect2) -> void:
|
||||
# 终局画面数据锚 = 战斗结算 判定行(verdict)——判定行文案由组合根装配传入。
|
||||
# gp_summary(OQ14 口径——胜利总结结算展示 GP 总量+增量):组合根装配「战利品 +增量 · 合计总量」
|
||||
# 传入(入账次序 = LevelRunner 入账先于组合根 handler,runner.gp 即战后总量);空串 = 不显示(失败侧恒空)。
|
||||
func open_victory(verdict_line: String, continue_label: String, board_rect: Rect2, gp_summary: String = "") -> void:
|
||||
_dim.color = Color(COL_WARM, 0.28)
|
||||
_verdict.text = verdict_line
|
||||
_verdict.add_theme_color_override("font_color", UiKit.INK_ON_GOLD)
|
||||
_gp_line.text = gp_summary
|
||||
_gp_line.visible = gp_summary != ""
|
||||
_btn.text = continue_label
|
||||
_board_rect = board_rect
|
||||
visible = true
|
||||
@@ -71,7 +81,7 @@ func open_defeat(verdict_line: String, continue_label: String, board_rect: Rect2
|
||||
_dim.color = Color(COL_DUSK, 0.45)
|
||||
_verdict.text = verdict_line
|
||||
_verdict.add_theme_color_override("font_color", COL_WARM)
|
||||
_btn.text = continue_label
|
||||
_gp_line.visible = false # GP 总量+增量归胜利总结结算(OQ14)——失败侧不呈现
|
||||
_board_rect = board_rect
|
||||
visible = true
|
||||
_seal.visible = false
|
||||
|
||||
@@ -22,6 +22,18 @@ var contact_corners: Array = [] # 斜向共享角码 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 = [] # 墙格地面朝向描边码 0=N/1=S/2=W/3=E(轮廓加强批——程序侧结构件)
|
||||
# ── 高墙感 D 投影档(2026-09-28 挑档落码——研究件四变体用户裁 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
|
||||
@@ -51,6 +63,16 @@ var bevel := 0.0: # 顶面倒角强度(0=关——立体原
|
||||
set(v):
|
||||
bevel = v
|
||||
queue_redraw()
|
||||
# ── D 投影档绘制常量(研究件实测取色——立面高光/双段暗带/地面投影;黑罩 alpha 叠加实测等效)──
|
||||
const FACADE_HIGHLIGHT_PX := 2.0 # 口沿高光线厚(像素)
|
||||
const FACADE_NEAR_FRAC := 0.16 # 立面近段高(格边比——研究件 10/64)
|
||||
const FACADE_FAR_FRAC := 0.25 # 立面远段高(格边比——研究件 16/64)
|
||||
const FACADE_HIGHLIGHT_COL := Color8(196, 172, 132) # 口沿高光(研究件取色暖米白)
|
||||
const FACADE_NEAR_COL := Color(0, 0, 0, 0.60) # 立面近段黑罩(墙顶 132→52 实测 α≈0.61)
|
||||
const FACADE_FAR_COL := Color(0, 0, 0, 0.82) # 立面远段黑罩(墙顶 132→24 实测 α≈0.82)
|
||||
const PROJECTION_NEAR_COL := Color(0, 0, 0, 0.28) # 投影贴墙段(地面 66→46 实测 α≈0.28)
|
||||
const PROJECTION_MID_COL := Color(0, 0, 0, 0.16) # 投影中段(渐隐)
|
||||
const PROJECTION_EDGE_COL := Color(0, 0, 0, 0.06) # 投影远段(渐隐收边)
|
||||
# ── 素材槽位(本批全 null = 程序化占位服役;art-bible §8.2 格面 32×32 暂定档)──
|
||||
var tex_base: Texture2D = null # ① 砖面底
|
||||
var tex_mutation: Texture2D = null # ② 变异纹带
|
||||
@@ -79,6 +101,8 @@ func _draw() -> void:
|
||||
2: or_ = Rect2(0, 0, ow, edge)
|
||||
3: or_ = Rect2(edge - ow, 0, ow, edge)
|
||||
draw_rect(or_, oc, true)
|
||||
_draw_south_facade()
|
||||
_draw_north_projection()
|
||||
# 瓦片态描边覆盖层(高墙瓦片接线批 2026-09-26):tex_base 在位时 BoardView 传入的 bg_sb
|
||||
# 已降级为「透明底 + 描边」载体——呼吸微光/主角蓝框画在瓦片之上(art-bible §1.2 叠层序
|
||||
# 不变:底 < 纹带 < 数字 < 旗 < 焦点高亮);素材缺位回退走 elif 程序化原样,零观感变化
|
||||
@@ -89,6 +113,7 @@ func _draw() -> void:
|
||||
# 带宽 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:
|
||||
_draw_north_projection() # 平色回退+微光并发的英雄格补画(瓦片路径已画——同像素重复无观感差)
|
||||
var band := 4.0
|
||||
var half := edge * 0.5
|
||||
for b in contact_bands:
|
||||
@@ -124,6 +149,8 @@ func _draw() -> void:
|
||||
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_south_facade() # 平色回退路径——D 投影档两件同样服役(结构归程序)
|
||||
_draw_north_projection()
|
||||
# 顶面倒角(立体原型批:光源左上——上/左内沿受光 2px、下/右内沿背光 2px;内缩 2px 让
|
||||
# 呼吸微光/主角蓝边框保持最外沿可见;强度随档位)
|
||||
if bevel > 0.0:
|
||||
@@ -170,6 +197,33 @@ func _draw() -> void:
|
||||
draw_rect(Rect2(1, 1, edge - 2.0, edge - 2.0), Color(hl_color, 0.55), false, 2.0) # 2px 内描边(旧 hl Panel 等价)
|
||||
|
||||
|
||||
# 南立面(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:
|
||||
return
|
||||
var far_h := edge * FACADE_FAR_FRAC
|
||||
var near_h := edge * FACADE_NEAR_FRAC
|
||||
var y_far := 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), FACADE_HIGHLIGHT_COL, true)
|
||||
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)
|
||||
|
||||
|
||||
# 地面投影(D 档件二——北邻墙格在本已开格北缘投渐隐暗带,自北缘向南三段衰减):
|
||||
# 坐标推导:三等分 proj_band,α 0.28/0.16/0.06(研究件实测贴墙 α≈0.28、带末渐融入地面)
|
||||
func _draw_north_projection() -> void:
|
||||
if 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)
|
||||
|
||||
|
||||
# 文字位绘制:水平居中 + 垂直居中(基线 = (格高 − 字高)/2 + 上伸部——旧 Button 居中对齐等价)
|
||||
func _draw_glyph(g: String, col: Color, fs: int) -> void:
|
||||
var f := font if font != null else ThemeDB.fallback_font
|
||||
|
||||
@@ -54,6 +54,9 @@ const SEP := 2 # 格间距(旧 GridContainer v/
|
||||
# §1.4 注记待 art-bible 并行批落籍后补写)
|
||||
const DEPTH_PX := 8.0 # 悬崖厚度(像素)
|
||||
const DEPTH_BEVEL := 0.6 # 顶面倒角强度
|
||||
# 高墙感 D 投影档(2026-09-28 挑档落码):地面投影带宽 = 格边比(研究件 64px 拼版实测 14/64≈0.22;
|
||||
# 立面/投影色参数在 CellUnit 常量——绘制侧单点)
|
||||
const WALL_PROJECTION_FRAC := 0.22
|
||||
|
||||
# 英雄单位演出参数(棋盘小人贴图批 2026-09-25——计划书 §3.B/C 时长档)
|
||||
const HERO_FRAME_PX := 32.0 # 英雄帧画布边长(素材 32×32 锁定——缩放比分母)
|
||||
@@ -76,23 +79,13 @@ const HERO_SHADOW_PX := Vector2(32, 12) # 脚下椭圆影屏幕尺寸(Godot
|
||||
# 路径常量落本文件单点(ui_kit.gd 由并行批占用,任务书 §2.5 禁入)。词表 =
|
||||
# design/research/board-tile-vocab-plan-a-2026-09-26.md(16 唯一墙瓦片 + 5 地面变体;
|
||||
# 东/东北/东南/end_e/in_ne/in_se 六镜像件运行时 flip_x 合成,不出文件——词表 §二)。
|
||||
const TEX_WALL_CENTER := "res://assets/art/board/terrain_wall_center_dungeon.png"
|
||||
const TEX_WALL_CENTER_CRACK := "res://assets/art/board/terrain_wall_center_crack_dungeon.png"
|
||||
const TEX_WALL_CENTER_SPECKLE := "res://assets/art/board/terrain_wall_center_speckle_dungeon.png"
|
||||
const TEX_WALL_N := "res://assets/art/board/terrain_wall_n_dungeon.png"
|
||||
const TEX_WALL_S := "res://assets/art/board/terrain_wall_s_dungeon.png"
|
||||
const TEX_WALL_W := "res://assets/art/board/terrain_wall_w_dungeon.png" # wall_e = 运行时镜像
|
||||
const TEX_WALL_NW := "res://assets/art/board/terrain_wall_nw_dungeon.png" # wall_ne = 运行时镜像
|
||||
const TEX_WALL_SW := "res://assets/art/board/terrain_wall_sw_dungeon.png" # wall_se = 运行时镜像
|
||||
const TEX_WALL_HRUN := "res://assets/art/board/terrain_wall_hrun_dungeon.png"
|
||||
const TEX_WALL_VRUN := "res://assets/art/board/terrain_wall_vrun_dungeon.png"
|
||||
const TEX_WALL_END_N := "res://assets/art/board/terrain_wall_end_n_dungeon.png"
|
||||
const TEX_WALL_END_S := "res://assets/art/board/terrain_wall_end_s_dungeon.png"
|
||||
const TEX_WALL_END_W := "res://assets/art/board/terrain_wall_end_w_dungeon.png" # wall_end_e = 运行时镜像
|
||||
const TEX_WALL_LONE := "res://assets/art/board/terrain_wall_lone_dungeon.png"
|
||||
const TEX_WALL_IN_NW := "res://assets/art/board/terrain_wall_in_nw_dungeon.png" # wall_in_ne = 运行时镜像
|
||||
const TEX_WALL_IN_SW := "res://assets/art/board/terrain_wall_in_sw_dungeon.png" # wall_in_se = 运行时镜像
|
||||
const TEX_FLOOR_FMT := "res://assets/art/board/terrain_floor_dungeon_%d.png" # 1..5 变体
|
||||
# 换色批(T6 2026-09-28):路径按 board_type 后缀参数化——terrain_<件名>_<board_type>.png;
|
||||
# 词表 §一 16 唯一墙瓦片名单单点在本常量数组。underdark/deep_underdark 两套 =
|
||||
# tools/recolor_terrain_tiles.py 程序换色产(台账 164 换色遗留清偿);任一套缺位 → 整批回落
|
||||
# dungeon 套(禁同板混渲染);dungeon 套亦缺位 → 程序化平色回退(fresh checkout 冒烟不炸)。
|
||||
const TILE_WALL_NAMES := ["wall_center", "wall_center_crack", "wall_center_speckle", "wall_n", "wall_s", "wall_w", "wall_nw", "wall_sw", "wall_hrun", "wall_vrun", "wall_end_n", "wall_end_s", "wall_end_w", "wall_lone", "wall_in_nw", "wall_in_sw"]
|
||||
const TILE_WALL_FMT := "res://assets/art/board/terrain_%s_%s.png" # %s=件名 %s=board_type
|
||||
const TILE_FLOOR_FMT := "res://assets/art/board/terrain_floor_%s_%d.png" # %s=board_type 1..%d 变体
|
||||
const TILE_FLOOR_VARIANTS := 5 # 地面瓦片变体数(词表 §一 floor_1..5)
|
||||
|
||||
var _wrap: CenterContainer
|
||||
@@ -131,6 +124,7 @@ var _hero_dig_tw: Tween
|
||||
var _tiles := {} # 词表键 → Texture2D(镜像件已预翻转;装载见 _load_board_tiles)
|
||||
var _floor_tiles: Array[Texture2D] = [] # 地面瓦片五变体(下标 0..4 = floor_1..5)
|
||||
var _tiles_ok := false # false = 素材缺位/未装载——bg_sb 程序化平色原路径
|
||||
var _tiles_board_type := "dungeon" # 当前装载瓦片套的 board_type(rebuild 按板切换——T6 换色批)
|
||||
|
||||
var cell_size: float: # 只读属性(ui_layout_probe 断言面——旧私有名 _cell 的契约化出口)
|
||||
get:
|
||||
@@ -252,6 +246,11 @@ func rebuild(board: BoardData, minesweeper) -> void:
|
||||
_r0[c] = true
|
||||
if _board == null:
|
||||
return
|
||||
# 瓦片套随板图型切换(T6 换色批——board_gen 词表 v1.1 dungeon/underdark/deep_underdark;
|
||||
# 同图型重复 rebuild 零重载,_tiles_board_type 守卫)
|
||||
var bt: String = _board.board_type if _board.board_type != "" else "dungeon"
|
||||
if bt != _tiles_board_type:
|
||||
_load_board_tiles(bt)
|
||||
var stride := _cell + float(SEP)
|
||||
for y in _board.height:
|
||||
for x in _board.width:
|
||||
@@ -683,12 +682,28 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
|
||||
u.glyph_color = digit_col
|
||||
u.plate = revealed and glyph != "" # 数字色牌(原则二——已开格字形全垫牌;旗在未开格不垫)
|
||||
u.outline_sides = []
|
||||
if _tiles_ok and not revealed:
|
||||
u.facade_south = false
|
||||
u.proj_band = 0.0
|
||||
var south_n: Vector2i = c + Vector2i(0, 1)
|
||||
var north_n: Vector2i = c + Vector2i(0, -1)
|
||||
var south_in: bool = south_n.x >= 0 and south_n.y >= 0 and south_n.x < _board.width and south_n.y < _board.height
|
||||
var north_in: bool = north_n.x >= 0 and north_n.y >= 0 and north_n.x < _board.width and north_n.y < _board.height
|
||||
if not revealed:
|
||||
# 高墙感 D 投影档(2026-09-28 挑档落码):南邻地面格 → 墙格南缘立「口沿高光+双段暗带」立面
|
||||
#(邻地面才画——墙-墙内界保墙群内聚,轮廓加强批同口径);板外=墙不画
|
||||
u.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):
|
||||
u.outline_sides.append(0 if d.y < 0 else (1 if d.y > 0 else (2 if d.x < 0 else 3)))
|
||||
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:
|
||||
continue # 南缘由立面件承载(高光+双段暗带)——2px 描边让位防三重叠黑
|
||||
u.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.badge_glyph = _update_badge(c)
|
||||
u.queue_redraw()
|
||||
|
||||
@@ -755,33 +770,37 @@ func _reset_breath() -> void:
|
||||
# 瓦片整批守卫装载(_init 单次调用):21 枚全成才启用——任一缺位按整批缺位处理(禁同板混渲染;
|
||||
# fresh checkout 无素材目录 → 静默全回退)。缺位不 push_error——可选素材缺位是预期路径非故障
|
||||
# (英雄批 load_tex 纪律同款,防冒烟刷屏)。镜像件运行时 flip_x 合成,不出文件。
|
||||
func _load_board_tiles() -> void:
|
||||
func _load_board_tiles(board_type: String = "dungeon") -> void:
|
||||
if board_type == "":
|
||||
board_type = "dungeon"
|
||||
var t := {
|
||||
"wall_center": UiKit.load_tex(TEX_WALL_CENTER),
|
||||
"wall_center_crack": UiKit.load_tex(TEX_WALL_CENTER_CRACK),
|
||||
"wall_center_speckle": UiKit.load_tex(TEX_WALL_CENTER_SPECKLE),
|
||||
"wall_n": UiKit.load_tex(TEX_WALL_N),
|
||||
"wall_s": UiKit.load_tex(TEX_WALL_S),
|
||||
"wall_w": UiKit.load_tex(TEX_WALL_W),
|
||||
"wall_nw": UiKit.load_tex(TEX_WALL_NW),
|
||||
"wall_sw": UiKit.load_tex(TEX_WALL_SW),
|
||||
"wall_hrun": UiKit.load_tex(TEX_WALL_HRUN),
|
||||
"wall_vrun": UiKit.load_tex(TEX_WALL_VRUN),
|
||||
"wall_end_n": UiKit.load_tex(TEX_WALL_END_N),
|
||||
"wall_end_s": UiKit.load_tex(TEX_WALL_END_S),
|
||||
"wall_end_w": UiKit.load_tex(TEX_WALL_END_W),
|
||||
"wall_lone": UiKit.load_tex(TEX_WALL_LONE),
|
||||
"wall_in_nw": UiKit.load_tex(TEX_WALL_IN_NW),
|
||||
"wall_in_sw": UiKit.load_tex(TEX_WALL_IN_SW),
|
||||
"wall_center": UiKit.load_tex(TILE_WALL_FMT % ["wall_center", board_type]),
|
||||
"wall_center_crack": UiKit.load_tex(TILE_WALL_FMT % ["wall_center_crack", board_type]),
|
||||
"wall_center_speckle": UiKit.load_tex(TILE_WALL_FMT % ["wall_center_speckle", board_type]),
|
||||
"wall_n": UiKit.load_tex(TILE_WALL_FMT % ["wall_n", board_type]),
|
||||
"wall_s": UiKit.load_tex(TILE_WALL_FMT % ["wall_s", board_type]),
|
||||
"wall_w": UiKit.load_tex(TILE_WALL_FMT % ["wall_w", board_type]),
|
||||
"wall_nw": UiKit.load_tex(TILE_WALL_FMT % ["wall_nw", board_type]),
|
||||
"wall_sw": UiKit.load_tex(TILE_WALL_FMT % ["wall_sw", board_type]),
|
||||
"wall_hrun": UiKit.load_tex(TILE_WALL_FMT % ["wall_hrun", board_type]),
|
||||
"wall_vrun": UiKit.load_tex(TILE_WALL_FMT % ["wall_vrun", board_type]),
|
||||
"wall_end_n": UiKit.load_tex(TILE_WALL_FMT % ["wall_end_n", board_type]),
|
||||
"wall_end_s": UiKit.load_tex(TILE_WALL_FMT % ["wall_end_s", board_type]),
|
||||
"wall_end_w": UiKit.load_tex(TILE_WALL_FMT % ["wall_end_w", board_type]),
|
||||
"wall_lone": UiKit.load_tex(TILE_WALL_FMT % ["wall_lone", board_type]),
|
||||
"wall_in_nw": UiKit.load_tex(TILE_WALL_FMT % ["wall_in_nw", board_type]),
|
||||
"wall_in_sw": UiKit.load_tex(TILE_WALL_FMT % ["wall_in_sw", board_type]),
|
||||
}
|
||||
var floors: Array[Texture2D] = []
|
||||
for i in range(1, TILE_FLOOR_VARIANTS + 1):
|
||||
floors.append(UiKit.load_tex(TEX_FLOOR_FMT % i))
|
||||
floors.append(UiKit.load_tex(TILE_FLOOR_FMT % [board_type, i]))
|
||||
for k in t:
|
||||
if t[k] == null:
|
||||
_request_dungeon_fallback(board_type)
|
||||
return
|
||||
for tex in floors:
|
||||
if tex == null:
|
||||
_request_dungeon_fallback(board_type)
|
||||
return
|
||||
# 六镜像件(词表 §二:wall_e←wall_w / wall_ne←wall_nw / wall_se←wall_sw /
|
||||
# wall_end_e←wall_end_w / wall_in_ne←wall_in_nw / wall_in_se←wall_in_sw;只水平翻转禁旋转
|
||||
@@ -794,9 +813,17 @@ func _load_board_tiles() -> void:
|
||||
t["wall_in_se"] = _flip_tex(t["wall_in_sw"] as Texture2D)
|
||||
_tiles = t
|
||||
_floor_tiles = floors
|
||||
_tiles_board_type = board_type
|
||||
_tiles_ok = true
|
||||
|
||||
|
||||
# 整批回落(T6 换色批):请求套缺位 → 回落 dungeon 套(禁同板混渲染);dungeon 本身缺位 =
|
||||
# 维持程序化平色回退原语义(_tiles_ok 恒 false,缺位不刷屏纪律不变)
|
||||
func _request_dungeon_fallback(requested: String) -> void:
|
||||
if requested != "dungeon":
|
||||
_load_board_tiles("dungeon")
|
||||
|
||||
|
||||
# 水平镜像贴图运行时合成(flip_h 语义):Image 级逐位翻转后重建 ImageTexture——CellUnit 零改动。
|
||||
# VRAM 压缩源先解压(flip_x 不支持压缩格式);32×32 原尺寸逐位翻转,无 ±1px 取整问题
|
||||
static func _flip_tex(tex: Texture2D) -> Texture2D:
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Date: 2026-09-17(分支节点图批——关卡节点地图 v1.0 图版选路呈现)
|
||||
# 节点图选路覆盖层(模态):兑付确认后由组合根打开——整幅节点图(方块地牢观感:节点 = 地牢房间
|
||||
# 方块、连线 = 通道)+ 当前位置高亮 + 可选下一跳亮边 + 候选按钮。零自动推进的玩家停点语义:
|
||||
# 唯一前进通道 = 点候选按钮 → route_chosen(node_id) → 组合根转 runner.pick_next_node。
|
||||
# 高内聚边界:只读 runner 成文出口(nodes()/visited_ids()/node_index/next_candidate_ids())做渲染,
|
||||
# 无任何写面;图形为程序化占位质量(占位纪律)。
|
||||
# 放弃入口(2026-09-21 用户拍板「放弃接口也要有」——台账 119;推翻 冒险结构.md「MVP 无界面入口」
|
||||
# 后置条款):选路停点 = 天然非战斗期(接口契约:战斗中放弃一律拒绝),按钮常驻本层底部;
|
||||
# 可见性归组合根(只在冒险模式开——放弃的语义对象是"整局冒险";调试两层无节点图天然不可达)。
|
||||
# 2026-09-28 关卡画面实装批(M-1 迭代 1 T4——level-map.md 三拍板落地):
|
||||
# ① 三停点标题定稿(布局决策 1——清偿台账 143:「继续前进(战斗房)」入口措辞矛盾退役)
|
||||
# ② 图上点击选路唯一通道(布局决策 2)——候选按钮整列退役;候选节点亮金描边 = 可点目标,
|
||||
# 悬停预承诺(描边加粗+指针)后点击提交;press_candidate 探针入口改走图上命中同路径
|
||||
# ③ 放弃冒险二次确认(布局决策 3)——首击转确认态(文案+危险红脉冲约 3 秒超时回落),二击提交
|
||||
# ④ 顶栏语境:关卡名(level_row.display_name)+ GP 常驻徽章(2026-09-26 二轮拍板去向落地面)
|
||||
# ⑤ 底部状态栏(布局决策 4):生命/属性/本关加成三组常读(零加成隐藏),放弃按钮右端异位
|
||||
# 节点图选路覆盖层(模态):兑付确认后由组合根打开。零自动推进的玩家停点语义:唯一前进通道 =
|
||||
# 图上点候选节点 → route_chosen(node_id) → 组合根转 runner.pick_next_node。
|
||||
# 高内聚边界:只读 runner 成文出口(nodes()/visited_ids()/node_index/next_candidate_ids()/level_row/
|
||||
# hero_hp/gp/effective_*/bonus)做渲染,无任何写面;图形为程序化占位质量(占位纪律)。
|
||||
# 放弃入口(2026-09-21 用户拍板「放弃接口也要有」——台账 119):选路停点 = 天然非战斗期(接口契约:
|
||||
# 战斗中放弃一律拒绝),按钮常驻底栏右端;可见性归组合根(只在冒险模式开——放弃的语义对象是
|
||||
# "整局冒险";调试两层无节点图天然不可达)。
|
||||
class_name RouteOverlay
|
||||
extends Control
|
||||
|
||||
@@ -20,12 +26,30 @@ const COL_ROOM_FUTURE := UiKit.DARK_UPCOMING # 未达——暗
|
||||
const COL_PASSAGE := UiKit.STONE # 通道——石灰
|
||||
const COL_PASSAGE_LIT := UiKit.GOLD_LIT # 候选通道——亮金
|
||||
const COL_BOSS_BORDER := UiKit.BOSS_PURPLE # Boss 边框——紫赤(色相独占本色;旗朱退役 2026-09-26)
|
||||
const COL_ABANDON := UiKit.RED_MINE # 破坏性动作红(危险红族)
|
||||
const NODE_LABELS := {"battle": "战", "reward": "赏", "rest": "疗", "boss": "王", "shop": "商"} # 商 = GP+商店房实施批 2026-09-21
|
||||
const TITLE_ENTRY := "出发——选择首个节点" # 入口停点(布局决策 1——台账 143 遗留清偿)
|
||||
const TITLE_SINGLE := "继续前进" # 单链停点
|
||||
const TITLE_FORK := "选择下一节点" # 岔路停点
|
||||
const NODE_HALF := Vector2(26, 18) # 节点方块半宽高(52×36 占位画布参数——样张批终稿)
|
||||
const HIT_PAD := 6.0 # 图上命中外扩(指宽容差)
|
||||
const CONFIRM_WINDOW_S := 3.0 # 放弃确认态窗口(秒——超时回落)
|
||||
|
||||
var _runner = null
|
||||
var _btn_box: VBoxContainer
|
||||
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 _cand_ids: Array = [] # 本停点候选 id 集(图上命中合法集)
|
||||
var _hover_id := -1 # 悬停预承诺候选(-1 = 无)
|
||||
var abandon_btn: Button # 放弃入口(可见性归组合根 set_abandon_available——只在冒险模式开)
|
||||
var _abandon_armed := false # 二次确认态(首击置位、二击提交、超时/重开回落)
|
||||
var _confirm_left := 0.0 # 确认态剩余秒数(_process 倒计时+脉冲驱动)
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
@@ -39,77 +63,244 @@ func _init() -> void:
|
||||
var box := VBoxContainer.new()
|
||||
box.add_theme_constant_override("separation", 10)
|
||||
center.add_child(box)
|
||||
_title = UiKit.label("选择下一跳", 18, true)
|
||||
# 顶栏语境(spec Layout:左关卡名 / 右 GP 常驻徽章——直角细线数据框)
|
||||
var top := HBoxContainer.new()
|
||||
top.add_theme_constant_override("separation", 12)
|
||||
box.add_child(top)
|
||||
_level_label = UiKit.label("", 16, true)
|
||||
_level_label.add_theme_color_override("font_color", UiKit.WARM)
|
||||
_level_label.custom_minimum_size.x = 420
|
||||
top.add_child(_level_label)
|
||||
var gp_box := PanelContainer.new()
|
||||
gp_box.add_theme_stylebox_override("panel", UiKit.panel_flat(UiKit.DARK_UPCOMING, UiKit.PANEL_BORDER, 0, 1))
|
||||
top.add_child(gp_box)
|
||||
var gp_row := HBoxContainer.new()
|
||||
gp_row.add_theme_constant_override("separation", 6)
|
||||
gp_box.add_child(gp_row)
|
||||
_gp_value = UiKit.label("", 16, true)
|
||||
_gp_value.add_theme_color_override("font_color", UiKit.GOLD)
|
||||
gp_row.add_child(_gp_value)
|
||||
_title = UiKit.label(TITLE_FORK, 18, true)
|
||||
_title.add_theme_color_override("font_color", UiKit.WARM)
|
||||
_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
box.add_child(_title)
|
||||
var canvas := Control.new()
|
||||
canvas.custom_minimum_size = Vector2(620, 240)
|
||||
canvas.draw.connect(_draw_graph.bind(canvas))
|
||||
box.add_child(canvas)
|
||||
_btn_box = VBoxContainer.new()
|
||||
_btn_box.add_theme_constant_override("separation", 6)
|
||||
box.add_child(_btn_box)
|
||||
abandon_btn = UiKit.button("🏳 放弃冒险(同失败结算——已获大奖励保留)")
|
||||
abandon_btn.add_theme_color_override("font_color", UiKit.RED_MINE) # 破坏性动作红(危险红族)
|
||||
abandon_btn.pressed.connect(_emit_abandon)
|
||||
# 节点图画布(图上点击 = 唯一提交通道——布局决策 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)
|
||||
# 底部状态栏(布局决策 4:生命/属性/本关加成三组 + 放弃按钮右端异位)
|
||||
var bottom := HBoxContainer.new()
|
||||
bottom.add_theme_constant_override("separation", 10)
|
||||
box.add_child(bottom)
|
||||
bottom.add_child(_status_box("生命", true))
|
||||
bottom.add_child(_status_box("属性", false))
|
||||
# 本关加成框(零加成隐藏——spec 布局决策 4)
|
||||
_bonus_row = HBoxContainer.new()
|
||||
_bonus_row.add_theme_constant_override("separation", 10)
|
||||
var bonus_box := PanelContainer.new()
|
||||
bonus_box.add_theme_stylebox_override("panel", UiKit.panel_flat(UiKit.DARK_UPCOMING, UiKit.PANEL_BORDER, 0, 1))
|
||||
_bonus_row.add_child(bonus_box)
|
||||
var bonus_inner := VBoxContainer.new()
|
||||
bonus_box.add_child(bonus_inner)
|
||||
bonus_inner.add_child(UiKit.label("本关加成", 12, false))
|
||||
_bonus_value = UiKit.label("", 15, true)
|
||||
bonus_inner.add_child(_bonus_value)
|
||||
bottom.add_child(_bonus_row)
|
||||
var spacer := Control.new()
|
||||
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
bottom.add_child(spacer)
|
||||
abandon_btn = UiKit.button("放弃冒险(同失败结算——已获大奖励保留)")
|
||||
abandon_btn.add_theme_color_override("font_color", COL_ABANDON) # 破坏性动作红(危险红族)
|
||||
abandon_btn.pressed.connect(_on_abandon_pressed)
|
||||
abandon_btn.visible = false
|
||||
box.add_child(abandon_btn)
|
||||
bottom.add_child(abandon_btn)
|
||||
visible = false
|
||||
|
||||
|
||||
# 状态数据框工厂(直角细线数据框——pattern 5 族;hp_box = 带迷你血条)
|
||||
func _status_box(caption: String, with_bar: bool) -> PanelContainer:
|
||||
var panel := PanelContainer.new()
|
||||
panel.add_theme_stylebox_override("panel", UiKit.panel_flat(UiKit.DARK_UPCOMING, UiKit.PANEL_BORDER, 0, 1))
|
||||
var inner := VBoxContainer.new()
|
||||
panel.add_child(inner)
|
||||
inner.add_child(UiKit.label(caption, 12, false))
|
||||
if with_bar:
|
||||
_hp_bar = Control.new()
|
||||
_hp_bar.custom_minimum_size = Vector2(120, 6)
|
||||
_hp_bar.draw.connect(_draw_hp_bar.bind(_hp_bar))
|
||||
inner.add_child(_hp_bar)
|
||||
var value := UiKit.label("", 15, true)
|
||||
inner.add_child(value)
|
||||
if caption == "生命":
|
||||
_hp_value = value
|
||||
else:
|
||||
_attr_value = value
|
||||
return panel
|
||||
|
||||
|
||||
func _draw_hp_bar(bar: Control) -> void:
|
||||
# 迷你血条(占位:暗底+金填充——比例 = hero_hp / effective_max_hp)
|
||||
if _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 full := Rect2(Vector2.ZERO, bar.size)
|
||||
bar.draw_rect(full, UiKit.DARK_UPCOMING)
|
||||
if ratio > 0.0:
|
||||
bar.draw_rect(Rect2(Vector2.ZERO, Vector2(bar.size.x * ratio, bar.size.y)), UiKit.GOLD)
|
||||
|
||||
|
||||
# 放弃入口可见性(组合根在冒险启动时开、默认关——语义对象 = 整局冒险)
|
||||
func set_abandon_available(v: bool) -> void:
|
||||
abandon_btn.visible = v
|
||||
|
||||
|
||||
func _emit_abandon() -> void:
|
||||
# 放弃二次确认(布局决策 3):首击转确认态(文案+危险红脉冲约 3 秒超时回落),二击才提交。
|
||||
# 单击不提交——ui_probe 战斗中 pressed.emit() 单击路径保持「未受理」语义(战斗中拒绝归组合根)。
|
||||
func _on_abandon_pressed() -> void:
|
||||
if not _abandon_armed:
|
||||
_abandon_armed = true
|
||||
_confirm_left = CONFIRM_WINDOW_S
|
||||
abandon_btn.text = "再点一次确认放弃——不可逆"
|
||||
return
|
||||
_reset_abandon_confirm()
|
||||
abandon_requested.emit()
|
||||
|
||||
|
||||
# 打开选路口(cands = 候选节点引用数组;runner 只读用于整图渲染)
|
||||
func _reset_abandon_confirm() -> void:
|
||||
_abandon_armed = false
|
||||
_confirm_left = 0.0
|
||||
abandon_btn.modulate = Color(1, 1, 1, 1)
|
||||
abandon_btn.text = "放弃冒险(同失败结算——已获大奖励保留)"
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not _abandon_armed:
|
||||
return
|
||||
_confirm_left -= delta
|
||||
if _confirm_left <= 0.0:
|
||||
_reset_abandon_confirm()
|
||||
return
|
||||
# 危险红边框脉冲(单段呼吸约 1Hz——闪烁基线 ≤3 次/秒内;减动效 = 静态半透不闪)
|
||||
if UiKit.motion_scale < 1.0:
|
||||
abandon_btn.modulate = Color(1, 1, 1, 0.75)
|
||||
else:
|
||||
var k := 0.5 + 0.5 * sin(_confirm_left * TAU)
|
||||
abandon_btn.modulate = Color(1, 1, 1, 0.65 + 0.35 * k)
|
||||
|
||||
|
||||
# 打开选路口(cands = 候选节点引用数组;runner 只读用于整图渲染与状态栏刷新)
|
||||
func open_choices(cands: Array, runner) -> void:
|
||||
_runner = runner
|
||||
for child in _btn_box.get_children():
|
||||
child.queue_free()
|
||||
_title.text = "选择下一跳(岔路口)" if cands.size() > 1 else "继续前进"
|
||||
_hover_id = -1
|
||||
_reset_abandon_confirm()
|
||||
var visited: Array = []
|
||||
if runner != null:
|
||||
visited = runner.visited_ids()
|
||||
if cands.size() > 1:
|
||||
_title.text = TITLE_FORK
|
||||
elif visited.is_empty():
|
||||
_title.text = TITLE_ENTRY # 入口停点(尚未走出第一步——台账 143 措辞矛盾清偿)
|
||||
else:
|
||||
_title.text = TITLE_SINGLE
|
||||
_cand_ids = []
|
||||
for cand in cands:
|
||||
var nid := _id_of(cand)
|
||||
var t := String(cand.get("type", ""))
|
||||
var label := "节点 %d · %s" % [nid, _type_name(t)] if cands.size() > 1 else "继续前进(%s)" % _type_name(t)
|
||||
var btn := UiKit.button(label)
|
||||
btn.pressed.connect(_emit_choice.bind(nid))
|
||||
_btn_box.add_child(btn)
|
||||
_cand_ids.append(_id_of(cand))
|
||||
_refresh_status()
|
||||
visible = true
|
||||
|
||||
|
||||
# 探针复用:按候选序号点击(真实按钮路径)
|
||||
func press_candidate(index: int) -> void:
|
||||
var btns: Array = _btn_box.get_children()
|
||||
if index >= 0 and index < btns.size():
|
||||
btns[index].pressed.emit()
|
||||
# 状态栏刷新(命令式纪律——组合根打开本屏/兑付确认回执后重查;零推送信号订阅)
|
||||
func _refresh_status() -> void:
|
||||
if _runner == null:
|
||||
return
|
||||
_level_label.text = String(_runner.level_row.get("display_name", ""))
|
||||
_gp_value.text = "◈ %d" % int(_runner.gp)
|
||||
var max_hp: int = _runner.effective_max_hp()
|
||||
_hp_value.text = "HP %d/%d" % [int(_runner.hero_hp), int(max_hp)]
|
||||
_hp_bar.queue_redraw()
|
||||
var atk_bonus := int(_runner.bonus.get("atk", 0))
|
||||
var ac_bonus := int(_runner.bonus.get("armor_class", 0))
|
||||
var atk_txt := "%d" % _runner.effective_atk()
|
||||
if atk_bonus > 0:
|
||||
atk_txt += " +%d" % atk_bonus # 加成角标(ASCII 加号——防像素字体缺 ⁺ 字形出豆腐块)
|
||||
var ac_txt := "%d" % _runner.effective_ac()
|
||||
if ac_bonus > 0:
|
||||
ac_txt += " +%d" % ac_bonus
|
||||
_attr_value.text = "攻击 %s · 护甲 AC %s" % [atk_txt, ac_txt]
|
||||
var hp_bonus := int(_runner.bonus.get("max_hp", 0))
|
||||
var parts: Array = []
|
||||
if atk_bonus > 0:
|
||||
parts.append("攻 +%d" % atk_bonus)
|
||||
if hp_bonus > 0:
|
||||
parts.append("上限 +%d" % hp_bonus)
|
||||
if ac_bonus > 0:
|
||||
parts.append("AC +%d" % ac_bonus)
|
||||
_bonus_value.text = " · ".join(parts)
|
||||
_bonus_row.visible = not parts.is_empty()
|
||||
|
||||
|
||||
func _emit_choice(nid: int) -> void:
|
||||
# ---- 图上命中(布局决策 2——唯一提交通道;探针 press_candidate 同路径) ----
|
||||
|
||||
func _on_canvas_input(ev: InputEvent) -> void:
|
||||
if _runner == null or not visible:
|
||||
return
|
||||
if ev is InputEventMouseMotion:
|
||||
var nid := _candidate_at(_canvas.get_local_mouse_position())
|
||||
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()
|
||||
elif ev is InputEventMouseButton and ev.button_index == MOUSE_BUTTON_LEFT and ev.pressed:
|
||||
var nid2 := _candidate_at(_canvas.get_local_mouse_position())
|
||||
if nid2 >= 0:
|
||||
_submit_choice(nid2)
|
||||
|
||||
|
||||
# 候选命中:位置 = 布局公式(与 _draw_graph 同源),矩形 = 节点方块 + 外扩容差;非候选格不响应
|
||||
func _candidate_at(local_pos: Vector2) -> int:
|
||||
if _runner == null:
|
||||
return -1
|
||||
var nodes: Array = _runner.nodes()
|
||||
var pos := _layout(nodes, _canvas)
|
||||
for nid in _cand_ids:
|
||||
var node := _node_by_id(nid, nodes)
|
||||
if node.is_empty():
|
||||
continue
|
||||
var np: Vector2 = pos[node]
|
||||
var rect := Rect2(np - NODE_HALF - Vector2(HIT_PAD, HIT_PAD), (NODE_HALF + Vector2(HIT_PAD, HIT_PAD)) * 2.0)
|
||||
if rect.has_point(local_pos):
|
||||
return nid
|
||||
return -1
|
||||
|
||||
|
||||
func _submit_choice(nid: int) -> void:
|
||||
visible = false
|
||||
route_chosen.emit(nid)
|
||||
|
||||
|
||||
# 探针复用:按候选序号走图上命中同路径(候选节点中心坐标 → 命中检测 → 提交——布局决策 2 探针面改造)
|
||||
func press_candidate(index: int) -> void:
|
||||
if index < 0 or index >= _cand_ids.size() or _runner == null:
|
||||
return
|
||||
var nodes: Array = _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 nid := _candidate_at(pos) # 节点中心必命中自身(命中面含节点方块)
|
||||
if nid >= 0:
|
||||
_submit_choice(nid)
|
||||
|
||||
|
||||
func _id_of(node: Dictionary) -> int:
|
||||
# 编号显示经行进器读面(编号空间解析单一出口——单链 = 下标 / 图 = node_depth)
|
||||
return _runner.node_id_of(node)
|
||||
|
||||
|
||||
func _type_name(t: String) -> String:
|
||||
match t:
|
||||
"battle": return "战斗房"
|
||||
"reward": return "奖励点"
|
||||
"rest": return "恢复点"
|
||||
"boss": return "Boss 房"
|
||||
"shop": return "商店房"
|
||||
return t
|
||||
|
||||
|
||||
# ---- 整图自绘(程序化占位——分层布局:层 = 入口 BFS 深度,同层垂直排布)----
|
||||
|
||||
func _draw_graph(canvas: Control) -> void:
|
||||
@@ -121,7 +312,7 @@ func _draw_graph(canvas: Control) -> void:
|
||||
# 当前节点 id(单链=下标 / 图=编号)——visited_ids() 已按编号空间解析
|
||||
var visited: Array = _runner.visited_ids()
|
||||
var cand_ids: Array = _runner.next_candidate_ids()
|
||||
# 连线(通道)先行画(方块覆盖其上)
|
||||
# 连线(通道)先行画(方块覆盖其上);悬停预承诺 = 所属通道再增亮(加粗)
|
||||
for node in nodes:
|
||||
var from: Vector2 = pos[node]
|
||||
for tid in _out_ids(node, nodes):
|
||||
@@ -130,8 +321,9 @@ func _draw_graph(canvas: Control) -> void:
|
||||
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 col := COL_PASSAGE_LIT if lit else COL_PASSAGE
|
||||
var width := 3.0 if lit else 1.5
|
||||
var width := 4.0 if hovered else (3.0 if lit else 1.5)
|
||||
_draw_passage(canvas, from, to, col, width)
|
||||
# 房间方块
|
||||
for node in nodes:
|
||||
@@ -143,13 +335,14 @@ func _draw_graph(canvas: Control) -> void:
|
||||
col = COL_ROOM_DONE
|
||||
if nid == current_id:
|
||||
col = COL_ROOM_CURRENT
|
||||
var rect := Rect2(p - Vector2(26, 18), Vector2(52, 36))
|
||||
var rect := Rect2(p - NODE_HALF, NODE_HALF * 2.0)
|
||||
canvas.draw_rect(rect, col)
|
||||
if t == "boss":
|
||||
canvas.draw_rect(rect, COL_BOSS_BORDER, false, 2.0)
|
||||
elif cand_ids.has(nid):
|
||||
canvas.draw_rect(rect, COL_PASSAGE_LIT, false, 2.0)
|
||||
var font := ThemeDB.fallback_font
|
||||
# 悬停预承诺 = 亮金描边加粗(4px),常态候选 = 亮金描边 2px(spec States 悬停预承诺行)
|
||||
canvas.draw_rect(rect, COL_PASSAGE_LIT, false, 4.0 if _hover_id == nid else 2.0)
|
||||
var font := get_theme_default_font()
|
||||
var label := String(NODE_LABELS.get(t, "?"))
|
||||
canvas.draw_string(font, p + Vector2(-6, 2), label, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, UiKit.INK_ROUTE)
|
||||
canvas.draw_string(font, p + Vector2(10, 14), str(nid), HORIZONTAL_ALIGNMENT_LEFT, -1, 10, UiKit.WARM_ROUTE_SUB)
|
||||
@@ -216,8 +409,8 @@ func _node_by_id(nid: int, nodes: Array) -> Dictionary:
|
||||
|
||||
# 通道画法:房间方块边缘到边缘的水平-折线(占位观感——地牢通道)
|
||||
func _draw_passage(canvas: Control, from: Vector2, to: Vector2, col: Color, width: float) -> void:
|
||||
var a := from + Vector2(26, 0)
|
||||
var b := to - Vector2(26, 0)
|
||||
var a := from + Vector2(NODE_HALF.x, 0)
|
||||
var b := to - Vector2(NODE_HALF.x, 0)
|
||||
if b.x < a.x:
|
||||
return
|
||||
var mid_x := (a.x + b.x) / 2.0
|
||||
|
||||
@@ -106,6 +106,17 @@ static func load_tex(path: String) -> Texture2D:
|
||||
return null
|
||||
|
||||
|
||||
# CJK 随包字体(2026-09-27 用户拍板 Fusion Pixel OFL——CJK 覆盖+授权干净;命名遵 art-bible §8.3
|
||||
# 全小写 snake_case 域前缀)。守卫装载同 load_tex 纪律:缺位返回 null(引擎回退字体接手,不炸)。
|
||||
const FONT_UI := "res://assets/fonts/ui_font_fusion_pixel_zh_hans.ttf"
|
||||
|
||||
static func load_font(path: String) -> Font:
|
||||
if ResourceLoader.exists(path):
|
||||
var res: Resource = load(path)
|
||||
return res as Font
|
||||
return null
|
||||
|
||||
|
||||
# ── StyleBox 工厂 ──
|
||||
# margin = (左, 上, 右, 下);border_w = 0 即无边框(根底/遮罩用)
|
||||
static func panel_flat(bg: Color, border: Color, corner: int = 0, border_w: int = 1, margin: Vector4 = Vector4.ZERO) -> StyleBoxFlat:
|
||||
|
||||
Reference in new issue
Block a user