合并(迭代45): 呈现层状态件批+gdstyle尾波补件批两卡合回——卡①单格两绘制域件(cell_base_layer/cell_mark_overlay)+站桩两状态件(standee_vitals/standee_motion)建件·宿主30→10与19→10真实清偿·探针u.face改址(含设计清单漏计第五处L1修复)·golden三张0.00%;卡②board_hover悬停域件+breakpoint_store断点域件(R2整族归件,v1.1用户裁)·27键冻结·键账31/27/23留债;独立验收审计通过零缺漏;两停机呈报裁决链完整(L3③潜伏红定谳绿=T0实跑十八断言绿·前提已被413124b0消解/规格缺口R2裁决)
This commit is contained in:
commit
f817ab2ddf
18 files changed
+1092
-921
No files matched your search
+55
-231
@@ -27,19 +27,14 @@ var levels_total := 0 # 序列总关数(读 level 表行
|
||||
var master_seed := 0 # 本局主种子(摘要第五件事)
|
||||
var summary := {} # 最近一次结算的冒险摘要(idle 后保留——重开才新生成)
|
||||
var _runner: LevelRunner # 当前关卡行进器(每关新建——关卡域状态天然隔离)
|
||||
var _pending_next_level := false # 大奖励已兑付、等 continue_to_next_level 确认
|
||||
var _last_level_payload := {} # 最近一次关卡结束载荷(失败结算的败退节点明细来源——数值验证取数面)
|
||||
var _last_treasure_cleared := 0 # additive(最近一次关卡末清空计数,summary 取数面)
|
||||
var _treasure_unlocked := {} # aux 解锁门控集(params.treasure_unlocked 接收面)
|
||||
var _attunement: Array[Variant] = [] # 同调装配集(params.attunement 接收面,start_level 外下传 LevelRunner)
|
||||
var _save_manager = null # 断点承载注入(null = 不写断点;模拟/冒烟缺省)
|
||||
var _resume_bp := {} # 续玩快照暂存(resume_adventure → _start_current_level 单跳传递)
|
||||
var _loadout_seeded := false # 同调行已种标记(每冒险一次——首关 bind 时点投行;行跨关存活后续关零动作)
|
||||
# ---- 三选一停点(编排迁 CurioStop 件——停点三态/刷新三源/凭证出箱/信号面)----
|
||||
var _curio: CurioDeck = null # 令牌池件(_init 建;纯池——多重集/抽签/cap/凭证判定唯一宿主)
|
||||
var _stop: CurioStop = null # 停点件(_init 建;每冒险一次——关卡换实例不换件)
|
||||
var _refresh_bank: RefreshBank = null # 刷新账件(_init 建——三源账与付费报价;宿主预算注入与断点片段直收)
|
||||
var _loadout: AttuneLoadout = null # 同调配装件(_init 建;充能单源化——物化行唯一事实源)
|
||||
var _bp_store: BreakpointStore # 断点组装与回放件(_init 建;六成员+27 键登记单源+读写侧机组随件——宿主读写经件)
|
||||
var adventure_inventory: TreasureItems # 冒险级秘宝库存(单源化——同调未用行跨关存活的承载面;关卡域语义归 level 使用面)
|
||||
var _tree_rules: Array[Variant] = [] # 树 rule 键集(Fork 激活侧 effects 透传;下传 enc.tree_rules 消费面)
|
||||
var _shop_mode := "legacy" # 商店模式("legacy"=固定三件加定价〔off 对照承载,缺省零扰动〕/"pool"=共享池抽 3 买 1 重抽;真实入口 main.gd 显式投 pool)
|
||||
@@ -57,6 +52,8 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
|
||||
_snapshot_breakpoint)
|
||||
_loadout = AttuneLoadout.new(db, _attunement)
|
||||
adventure_inventory = TreasureItems.new(db)
|
||||
_bp_store = BreakpointStore.new({"db": db, "rng": rng, "stats": _stats, "wallet": _wallet,
|
||||
"journal": _journal, "curio": _curio, "stop": _stop, "bank": _refresh_bank, "loadout": _loadout})
|
||||
|
||||
|
||||
# 件出口(组件交出非委托壳——呈现/测量面取主角账三件与令牌池单点;领域拆分批接口块)
|
||||
@@ -107,7 +104,7 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
|
||||
_reset_curio_state()
|
||||
levels_total = db.run.level.level_count() # 表访问收口(关数读 level 表行数)
|
||||
level_index = 1
|
||||
_pending_next_level = false
|
||||
_bp_store._pending_next_level = false
|
||||
_shop_mode = "pool" if String(params.get("shop_mode", "legacy")) == "pool" else "legacy" # 缺省 legacy(off 对照零扰动)
|
||||
_treasure_unlocked = params.get("treasure_unlocked", {}) if params.get("treasure_unlocked", {}) is Dictionary else {}
|
||||
_attunement = params.get("attunement", []) if params.get("attunement", []) is Array else [] # additive
|
||||
@@ -115,7 +112,7 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
|
||||
_refresh_bank.set_refresh_y(maxi(0, int(params.get("attune_refresh_y", 0)))) # additive(Y 冒险周期池)
|
||||
_loadout.set_attunement(_attunement) # 装配集注入(活引用——种行面)
|
||||
adventure_inventory.reset() # 冒险级库存重置(新局空库存)
|
||||
_loadout_seeded = false # 同调行投行延至首关 bind 时点(库存就位后)
|
||||
_bp_store._loadout_seeded = false # 同调行投行延至首关 bind 时点(库存就位后)
|
||||
adventure_counters = {} # 冒险桶清零(效果引擎 adv_ 计数器宿主——冒险开始归零)
|
||||
# 刷新练习开局注入(refresh_stock_grant 装配即得一次性加仓:
|
||||
# 装配集 kind 分发扫描;只入存量账、临时账不动=V8 契约;resume 不重注入〔存量随断点已在〕)
|
||||
@@ -152,99 +149,50 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
|
||||
return false
|
||||
master_seed = int(bp.get("master_seed", 0))
|
||||
rng.restore_adventure(master_seed, int(bp.get("rng_counter", 0)))
|
||||
_resume_ledger(bp, params)
|
||||
_resume_curio_state(bp)
|
||||
_resume_params(bp, params)
|
||||
_resume_counters(bp)
|
||||
# ---- 断点回放(R2 整族归件——件侧语句入 _bp_store、宿主侧语句按原语句序留本壳,序逐步保形)----
|
||||
_bp_store.restore_ledger(bp, params) # 续玩账面回填——主角账三件片段各还原各键(件侧;clamp 类夹 ≥0;日记在档=推导比对载入、缺键=旧档汇总直载)
|
||||
summary = {}
|
||||
levels_total = db.run.level.level_count() # 表访问收口(关数读 level 表行数)
|
||||
level_index = int(bp.get("level_index", 1))
|
||||
_bp_store._pending_next_level = false
|
||||
_reset_curio_state() # 续玩三选一态回填前置清理(池与件计数归件、停点态归本类;桶归宿主字典)
|
||||
_bp_store.restore_curio_state(bp) # 续玩三选一态回填(池余库/获取计数/特性持有集——件侧;dual_merge/keep_on_null 策略见 BREAKPOINT_FIELDS)
|
||||
_bp_store.apply_resume_params(params) # 续玩参数面——数值账规范化与越界钳制(件侧)
|
||||
_shop_mode = "pool" if String(params.get("shop_mode", "legacy")) == "pool" else "legacy" # 续玩参数面同 start_adventure(缺省 legacy)
|
||||
_treasure_unlocked = params.get("treasure_unlocked", {}) if params.get("treasure_unlocked", {}) is Dictionary else {}
|
||||
_attunement = params.get("attunement", []) if params.get("attunement", []) is Array else [] # additive
|
||||
_refresh_bank.set_refresh_x_cap(_attune_refresh_x_total()) # 刷新账 X 基数预算注入(续玩参数面同 start)
|
||||
_bp_store.restore_stop_bank_fragments(bp, params) # 停点件/刷新账片段回填(件侧——凭证计数;刷新存量/Y 池缺键回退参数面)
|
||||
_tree_rules = params.get("tree_rules", []) if params.get("tree_rules", []) is Array else [] # A/B 面 rule 键集(续玩参数面同 start_adventure 同式,消费面 set_tree_rules 下传)
|
||||
# 续玩计数器回填(adventure 域计数——缺键 = 旧版断点防御空集;_reset_curio_state 已清零在先——桶归宿主字典)
|
||||
var bp_counters = _bp_store._bp_read("adventure_counters", bp)
|
||||
if bp_counters is Dictionary:
|
||||
adventure_counters = (bp_counters as Dictionary).duplicate(true)
|
||||
var bp_pending: Dictionary[Variant, Variant] = bp.get("bonus_pending", {}) as Dictionary
|
||||
var bp_result: Dictionary[Variant, Variant] = bp.get("bonus_result", {}) as Dictionary
|
||||
if not bp_pending.is_empty():
|
||||
_pending_next_level = true
|
||||
_bp_store._pending_next_level = true
|
||||
_state = State.RUNNING
|
||||
_stop.restore_pending(bp_pending) # 选择停点直回(续玩不重跑关卡——信号经停点件中继)
|
||||
return true
|
||||
if not bp_result.is_empty():
|
||||
_pending_next_level = true
|
||||
_bp_store._pending_next_level = true
|
||||
_stop.restore_resolved(bp_result) # 已兑付不重兑付只重发(置态+重发收编一步——信号面归停点件)
|
||||
_state = State.RUNNING
|
||||
return true
|
||||
_resume_bp = bp.duplicate(true)
|
||||
_bp_store._resume_bp = bp.duplicate(true)
|
||||
# 充能读侧以行集为源(键值仅校验读数——回填时点在首关 bind 行集就位后,经 _loadout_on_level_inventory)
|
||||
_loadout_seeded = false
|
||||
_bp_store._loadout_seeded = false
|
||||
_loadout.set_attunement(_attunement)
|
||||
adventure_inventory.reset()
|
||||
_state = State.RUNNING
|
||||
return _start_current_level()
|
||||
|
||||
|
||||
# 续玩账面回填(生命/特性/钱包/加算区/已购档/钥石/逐杀明细/真视——主角账三件片段各还原各键;
|
||||
# bonus 子键 clamp 类夹 ≥0;日记在档=推导比对载入、缺键=旧档汇总直载)
|
||||
func _resume_ledger(bp: Dictionary, params: Dictionary) -> void:
|
||||
_stats.restore_fragment(bp, params)
|
||||
_wallet.restore_fragment(bp, params)
|
||||
_journal.restore_fragment(bp, params)
|
||||
summary = {}
|
||||
levels_total = db.run.level.level_count() # 表访问收口(关数读 level 表行数)
|
||||
level_index = int(bp.get("level_index", 1))
|
||||
_pending_next_level = false
|
||||
|
||||
|
||||
# 续玩三选一态回填(池与件计数归件、停点态归本类;dual_merge/keep_on_null 策略见 BREAKPOINT_FIELDS)
|
||||
func _resume_curio_state(bp: Dictionary) -> void:
|
||||
_reset_curio_state()
|
||||
var bp_taken: Dictionary[Variant, Variant] = bp.get("bonus_taken", {}) as Dictionary
|
||||
for id in _curio.taken.keys():
|
||||
_curio.taken[id] = maxi(0, int(bp_taken.get(id, 0)))
|
||||
for id in bp_taken.keys():
|
||||
_curio.taken[String(id)] = maxi(0, int(bp_taken.get(id, 0))) # 身份维键(relic:<feat_id>)随快照还原
|
||||
# 令牌池余库还原(TP0 v2.2——快照缺键 = 旧版断点,保持满库重建态:旧档只读一版周期的防御口径;
|
||||
# relic 令牌 feat_id 随还原)
|
||||
var bp_deck = _bp_read("bonus_deck", bp)
|
||||
if bp_deck is Array:
|
||||
var restored: Array[Variant] = []
|
||||
for t in (bp_deck as Array):
|
||||
var tok: Dictionary[Variant, Variant] = t as Dictionary
|
||||
var rtok := {"kind": String(tok.get("kind", "")), "tier": String(tok.get("tier", ""))}
|
||||
if String(rtok["kind"]) == "relic":
|
||||
rtok["feat_id"] = String(tok.get("feat_id", "")) # relic 令牌 feat_id 随还原
|
||||
restored.append(rtok)
|
||||
_curio.deck = restored
|
||||
# 特性持有集还原(缺键 = 旧版断点防御空集——主角数值账字段单点)
|
||||
_stats.feats = []
|
||||
for r in (bp.get("relics", []) as Array):
|
||||
_stats.feats.append(String(r))
|
||||
|
||||
# 续玩参数面回填(同 start_adventure 参数面;生命账本越界钳制在此落——clamp 类)
|
||||
func _resume_params(bp: Dictionary, params: Dictionary) -> void:
|
||||
_stats.rebase_override(params.get("hero_override", {})) # 续玩参数面同 start_adventure(规范化归数值账)
|
||||
_stats.attack_type = String(params.get("attack_type", "")) # 续玩参数面同 start_adventure(账号层选中态随 resume 重注入)
|
||||
_stats.armor_type = String(params.get("armor_type", ""))
|
||||
# 生命账本越界钳制(T17——spawn 守卫 EC16 免疫):断点可能带出高于生效上限的账本值
|
||||
#(如调试浮窗调高期写出的值,实例:hero_hp=28 > 基础 26)——恢复后钳到生效上限,
|
||||
# 否则首战 spawn「hero_hp 入参越界」拒收 → 「敌人系统 spawn 被拒」fatal。钳制式单点归
|
||||
# 数值账 clamp_hp_to_resume_cap(保守口径不含加算区——与原钳制式逐字同)。
|
||||
_stats.clamp_hp_to_resume_cap(int(db.combat.hero.max_hp))
|
||||
_shop_mode = "pool" if String(params.get("shop_mode", "legacy")) == "pool" else "legacy" # 续玩参数面同 start_adventure(缺省 legacy)
|
||||
_treasure_unlocked = params.get("treasure_unlocked", {}) if params.get("treasure_unlocked", {}) is Dictionary else {}
|
||||
_attunement = params.get("attunement", []) if params.get("attunement", []) is Array else [] # additive
|
||||
_refresh_bank.set_refresh_x_cap(_attune_refresh_x_total()) # 刷新账 X 基数预算注入(续玩参数面同 start)
|
||||
_stop.restore_fragment(bp, params) # 停点件片段回填(凭证计数)
|
||||
_refresh_bank.restore_fragment(bp, params) # 刷新账片段回填(刷新存量/Y 池——Y 缺键回退参数面 params_fallback 类)
|
||||
_tree_rules = params.get("tree_rules", []) if params.get("tree_rules", []) is Array else [] # A/B 面 rule 键集(续玩参数面同 start_adventure 同式,消费面 set_tree_rules 下传)
|
||||
|
||||
|
||||
# 续玩计数器回填(冒险域计数——凭证计数/刷新存量/Y 池随停点件片段回填)
|
||||
func _resume_counters(bp: Dictionary) -> void:
|
||||
# adventure 域计数器回填(缺键 = 旧版断点防御空集;_reset_curio_state 已清零在先——桶归宿主字典)
|
||||
var bp_counters = _bp_read("adventure_counters", bp)
|
||||
if bp_counters is Dictionary:
|
||||
adventure_counters = (bp_counters as Dictionary).duplicate(true)
|
||||
|
||||
|
||||
func _reset_curio_state() -> void:
|
||||
_curio.reset() # 池与件获取计数(池半归件;停点态归停点件下行清理)
|
||||
_stop.reset_adventure_state()
|
||||
_loadout_seeded = false
|
||||
_bp_store._loadout_seeded = false
|
||||
_stats.feats = []
|
||||
adventure_counters = {}
|
||||
|
||||
@@ -263,7 +211,7 @@ func effective_max_hp() -> int:
|
||||
|
||||
# 断点承载注入(组合根冒险开始时调用;模拟/冒烟不注入 = 断点面整体静默)
|
||||
func set_save_manager(sm: SaveManagerCore) -> void:
|
||||
_save_manager = sm
|
||||
_bp_store._save_manager = sm
|
||||
|
||||
|
||||
# 当前关卡行进器读面(呈现层/冒烟用——每关新建实例,调用方须在 level_started 后重取)
|
||||
@@ -278,13 +226,13 @@ func apply_rest(pct: int, hp: int, max_hp: int) -> int:
|
||||
|
||||
# 大奖励确认 → 进下一关(仅兑付后受理——零自动推进的冒险级停点;三选一未兑付拒绝推进)
|
||||
func continue_to_next_level() -> bool:
|
||||
if not _pending_next_level:
|
||||
if not _bp_store._pending_next_level:
|
||||
push_error("[冒险结构] continue_to_next_level 拒绝——无待确认的关卡推进")
|
||||
return false
|
||||
if _stop.curio_stop_pending():
|
||||
push_error("[冒险结构] continue_to_next_level 拒绝——三选一停点未兑付(零自动推进契约)")
|
||||
return false
|
||||
_pending_next_level = false
|
||||
_bp_store._pending_next_level = false
|
||||
_stop.clear_resolved()
|
||||
level_index += 1
|
||||
return _start_current_level()
|
||||
@@ -343,9 +291,9 @@ func _loadout_on_level_inventory() -> void:
|
||||
var inv = _runner.treasure_inventory
|
||||
if inv == null:
|
||||
return
|
||||
if not _loadout_seeded:
|
||||
_loadout_seeded = true
|
||||
var bp_charges: Variant = _bp_read("attunement_charges", _resume_bp, null) if not _resume_bp.is_empty() else null
|
||||
if not _bp_store._loadout_seeded:
|
||||
_bp_store._loadout_seeded = true
|
||||
var bp_charges: Variant = _bp_store._bp_read("attunement_charges", _bp_store._resume_bp, null) if not _bp_store._resume_bp.is_empty() else null
|
||||
if bp_charges is Dictionary:
|
||||
_loadout.restore_fragment({"attunement_charges": bp_charges}, inv) # 旧档读侧:行集为源+键值校验读数
|
||||
else:
|
||||
@@ -368,8 +316,8 @@ func _loadout_on_level_inventory() -> void:
|
||||
# apply_adventure_summary 逐关卡完成关次置位〕;末关 Boss 照常入账;明细随摘要 additive 键供结算面归因)
|
||||
func _boss_keystone_drop(li: int) -> void:
|
||||
var first := true
|
||||
if _save_manager != null:
|
||||
first = not _save_manager.meta_state.career_state().boss_first_kill.has(str(li))
|
||||
if _bp_store._save_manager != null:
|
||||
first = not _bp_store._save_manager.meta_state.career_state().boss_first_kill.has(str(li))
|
||||
var row: Dictionary[Variant, Variant] = db.crossrun.meta.boss_keystone_row(li)
|
||||
var amount: int = int(row.get("first_kill", 0)) if first else int(row.get("repeat_kill", 0))
|
||||
_journal.record_keystone_drop({"level": li, "first": first, "amount": amount}) # 逐杀明细+累计单写原语
|
||||
@@ -419,9 +367,9 @@ func _start_current_level() -> bool:
|
||||
# 断点续玩分支:图由主种子+参数档重生成同图(产图与计数器号位无关,停点零重抽语义随之消解),
|
||||
# resume_state 带停点句柄与已到序——重建到停点后经 present_resumed_choice 呈现(信号顺序契约同入口)。
|
||||
var resume_state: Dictionary[Variant, Variant] = {}
|
||||
if not _resume_bp.is_empty():
|
||||
resume_state = {"node_id": int(_bp_read("node_id", _resume_bp, -1)),
|
||||
"visited_ids": _bp_read("visited_ids", _resume_bp, []) as Array}
|
||||
if not _bp_store._resume_bp.is_empty():
|
||||
resume_state = {"node_id": int(_bp_store._bp_read("node_id", _bp_store._resume_bp, -1)),
|
||||
"visited_ids": _bp_store._bp_read("visited_ids", _bp_store._resume_bp, []) as Array}
|
||||
# 接口①开始关卡:生命经主角账共享交接(账面当前值即开局生命——冒险开始置满 / 大奖励回满已写好);
|
||||
# 主角账三件与冒险桶注入(构造注入 ctx——拆件批 T3 收口形态)。
|
||||
# defer_entry=true 起每关(含首关)——先 emit level_started(呈现层在
|
||||
@@ -431,7 +379,7 @@ func _start_current_level() -> bool:
|
||||
if ok:
|
||||
_emit_level_started()
|
||||
_present_level_entry(resume_state)
|
||||
_resume_bp = {}
|
||||
_bp_store._resume_bp = {}
|
||||
return ok
|
||||
|
||||
|
||||
@@ -455,7 +403,7 @@ func _present_level_entry(resume_state: Dictionary) -> void:
|
||||
if resume_state.is_empty() or int(resume_state.get("node_id", -1)) < 0:
|
||||
_runner.present_entry_choice()
|
||||
else:
|
||||
_runner.restore_treasure_items(_bp_read("aux_items", _resume_bp, []) as Array)
|
||||
_runner.restore_treasure_items(_bp_store._bp_read("aux_items", _bp_store._resume_bp, []) as Array)
|
||||
_runner.present_resumed_choice()
|
||||
|
||||
|
||||
@@ -464,7 +412,7 @@ func _on_level_finished(payload: Dictionary) -> void:
|
||||
if state != "running":
|
||||
push_error("[冒险结构] 重复关卡结束宣布拒绝——state=%s(账本与摘要零变化)" % state)
|
||||
return
|
||||
_last_level_payload = payload.duplicate(true)
|
||||
_bp_store._last_level_payload = payload.duplicate(true)
|
||||
var result := String(payload.get("result", ""))
|
||||
if result == "defeat":
|
||||
_stats.hero_hp = 0 # 倒下记 0(生命账本战后回写口径)
|
||||
@@ -473,7 +421,7 @@ func _on_level_finished(payload: Dictionary) -> void:
|
||||
if result != "victory":
|
||||
push_error("[冒险结构] 关卡结束结果出域: %s" % result)
|
||||
return
|
||||
_last_treasure_cleared = int(payload.get("aux_cleared", 0)) # additive(末关 summary 取数面)
|
||||
_bp_store._last_treasure_cleared = int(payload.get("aux_cleared", 0)) # additive(末关 summary 取数面)
|
||||
# Boss 钥石逐杀掉落(击杀掉落渠道,末关照常入账;首杀/复杀读 meta.tres boss_keystone)
|
||||
_boss_keystone_drop(level_index)
|
||||
if level_index < levels_total:
|
||||
@@ -481,7 +429,7 @@ func _on_level_finished(payload: Dictionary) -> void:
|
||||
#(信号同步派发——处理器内立即确认时 _pending_next_level 必须已就位);
|
||||
# 停点件内部分回血先行 → 加权洗牌掷 3 → 三选一停点(顺序即契约——回血按当刻账本与上限结算,
|
||||
# 选择停点零自动推进)。载荷_base 随附钱包读数(停点件零钱包依赖——GP 件入账回调仍走 grant_curio_gp)
|
||||
_pending_next_level = true
|
||||
_bp_store._pending_next_level = true
|
||||
var stop_base: Dictionary[Variant, Variant] = payload.duplicate(true)
|
||||
stop_base["gp"] = _wallet.gp
|
||||
_stop.open_level_clear(stop_base)
|
||||
@@ -499,7 +447,7 @@ func _settle(result: String) -> void:
|
||||
"stopped_at": 0 if result == "completed" else level_index,
|
||||
"keystone_total": _journal.keystone_gained(),
|
||||
"master_seed": master_seed,
|
||||
"aux_cleared_last": _last_treasure_cleared, # additive 扩签(末关清空反馈;非末关走 bonus 载荷)
|
||||
"aux_cleared_last": _bp_store._last_treasure_cleared, # additive 扩签(末关清空反馈;非末关走 bonus 载荷)
|
||||
"keystone_drops": _journal.keystone_drop_log(), # additive 扩签(逐杀掉落明细;settle 入账明细/复盘归因数据源)
|
||||
"vision_used_delta": _journal.vision_used(), # additive 扩签(真视使用本局计数;settle 聚合入档)
|
||||
"pool_stock": _pool_stock_snapshot(), # additive 扩签(终局池读数:清账前快照,商店买走 vs 奖励领走 vs 枯竭的终局面)
|
||||
@@ -507,19 +455,19 @@ func _settle(result: String) -> void:
|
||||
}
|
||||
# 败退明细(数值验证工具取数面——tools/balance_verifier.py 止步节点分布数据源;
|
||||
# 仅失败局附加,键缺省 = 未败退,既有五件事消费方零感知)
|
||||
if result == "failed" and not _last_level_payload.is_empty():
|
||||
if result == "failed" and not _bp_store._last_level_payload.is_empty():
|
||||
summary["defeat"] = {
|
||||
"level_index": level_index,
|
||||
"node_id": int(_last_level_payload.get("node_id", -1)),
|
||||
"node_type": String(_last_level_payload.get("node_type", "")),
|
||||
"node_id": int(_bp_store._last_level_payload.get("node_id", -1)),
|
||||
"node_type": String(_bp_store._last_level_payload.get("node_type", "")),
|
||||
}
|
||||
if _runner != null:
|
||||
summary["gp_final"] = int(_runner.gp) # gp_final 清账前快照(终局面板取数源=runner 钱包单点)
|
||||
_runner.close_wallet() # GP 冒险收束清零(三结局都清——经转发发 wallet_changed "clear" 供日志/呈现)
|
||||
_reset_curio_state() # 三选一停点态收束清零(结算受理只算一次语义——重复宣布零变化)
|
||||
if _save_manager != null:
|
||||
_save_manager.clear_breakpoint() # 三结局清断点(退出冒险=放弃流程+清除断点)
|
||||
_save_manager.write_to_disk()
|
||||
if _bp_store._save_manager != null:
|
||||
_bp_store._save_manager.clear_breakpoint() # 三结局清断点(退出冒险=放弃流程+清除断点)
|
||||
_bp_store._save_manager.write_to_disk()
|
||||
_state = State.SETTLING
|
||||
adventure_finished.emit(summary)
|
||||
_state = State.IDLE
|
||||
@@ -530,131 +478,7 @@ func _on_level_choices_presented(_candidates: Array) -> void:
|
||||
_snapshot_breakpoint()
|
||||
|
||||
|
||||
# ---- 断点快照(字段登记单源:写侧拼装与读侧回退策略同登记一表)----
|
||||
# 键序即快照键序(27 顶层键——写侧/读侧/键集断言三面同源)。读侧回退策略词表:
|
||||
# plain 无特殊回退面(缺键取代码内缺省)/clamp 数值夹 ≥0/keep_on_null 缺键或类型不符保持既有态
|
||||
# dual_merge 双向合并(bonus_taken)/params_fallback 缺键回退参数面(attune_refresh_y)
|
||||
# token_migration 旧版候选形态迁移(bonus_pending.candidates)
|
||||
# 注入缺省 null = 整面静默(模拟/冒烟零扰动)。
|
||||
const BREAKPOINT_FIELDS := {
|
||||
"level_index": {"strategy": "plain"},
|
||||
"node_id": {"strategy": "plain"},
|
||||
"hero_hp": {"strategy": "clamp"},
|
||||
"bonus": {"strategy": "clamp"},
|
||||
"gp": {"strategy": "plain"},
|
||||
"shop_purchased": {"strategy": "plain"},
|
||||
"shop_shelf": {"strategy": "plain"},
|
||||
"shop_visit_buys": {"strategy": "plain"},
|
||||
"attunement_charges": {"strategy": "keep_on_null"},
|
||||
"graph_gen_v2": {"strategy": "plain"},
|
||||
"master_seed": {"strategy": "plain"},
|
||||
"rng_counter": {"strategy": "plain"},
|
||||
"keystone": {"strategy": "plain"},
|
||||
"visited_ids": {"strategy": "plain"},
|
||||
"aux_items": {"strategy": "plain"},
|
||||
"bonus_taken": {"strategy": "dual_merge"},
|
||||
"bonus_deck": {"strategy": "keep_on_null"},
|
||||
"relics": {"strategy": "plain"},
|
||||
"bonus_pending": {"strategy": "token_migration"},
|
||||
"feat_ticket_pity": {"strategy": "plain"},
|
||||
"feat_ticket_sold": {"strategy": "plain"},
|
||||
"adventure_counters": {"strategy": "keep_on_null"},
|
||||
"vision_counter_used": {"strategy": "clamp"},
|
||||
"refresh_stock": {"strategy": "clamp"},
|
||||
"attune_refresh_y": {"strategy": "params_fallback"},
|
||||
"bonus_result": {"strategy": "plain"},
|
||||
"keystone_drops": {"strategy": "plain"},
|
||||
"gain_journal": {"strategy": "plain"}, # 获得日记(拆件批 T4 扩列——唯一声明的断点扩展;旧档缺键=汇总直载跳过比对)
|
||||
}
|
||||
|
||||
|
||||
# ---- 断点快照委托(组装与读侧策略机组归 breakpoint_store 件——六成员+27 键登记单源随件;
|
||||
# 本壳一行转发:CurioStop 构造注入的 Callable 目标与本类 _on_level_choices_presented 调用点零改)----
|
||||
func _snapshot_breakpoint() -> void:
|
||||
if _save_manager == null or state != "running":
|
||||
return
|
||||
var parts: Dictionary[Variant, Variant] = _bp_parts()
|
||||
var bp: Dictionary[Variant, Variant] = {}
|
||||
for key in BREAKPOINT_FIELDS.keys():
|
||||
bp[key] = parts[key]
|
||||
_save_manager.set_breakpoint(bp)
|
||||
_save_manager.write_to_disk()
|
||||
|
||||
|
||||
# 断点写侧取值(键集以 BREAKPOINT_FIELDS 为准——未登记键即取值越界,fail-fast)
|
||||
func _bp_parts() -> Dictionary[Variant, Variant]:
|
||||
var stats_frag: Dictionary[Variant, Variant] = _stats.breakpoint_fragment()
|
||||
var wallet_frag: Dictionary[Variant, Variant] = _wallet.breakpoint_fragment()
|
||||
var journal_frag: Dictionary[Variant, Variant] = _journal.breakpoint_fragment()
|
||||
var bank_frag: Dictionary[Variant, Variant] = _refresh_bank.breakpoint_fragment()
|
||||
var stop_frag: Dictionary[Variant, Variant] = _stop.breakpoint_fragment()
|
||||
var level_frag: Dictionary[Variant, Variant] = _runner.breakpoint_fragment() if _runner != null else {
|
||||
"node_id": -1, "visited_ids": [] as Array, "aux_items": [] as Array,
|
||||
"shop_shelf": [] as Array, "shop_visit_buys": 0} # 三选一停点续玩分支 _runner 为 null——节点域按缺省落档
|
||||
# 该两态的续玩只消费 bonus_* 附随件(选择停点/已兑付停点快照)
|
||||
var parts: Dictionary[Variant, Variant] = {
|
||||
"level_index": level_index,
|
||||
"node_id": int(level_frag.get("node_id", -1)),
|
||||
"hero_hp": int(stats_frag.get("hero_hp", 0)),
|
||||
"bonus": journal_frag.get("bonus", {}),
|
||||
"gp": int(wallet_frag.get("gp", 0)),
|
||||
"shop_purchased": journal_frag.get("shop_purchased", {}),
|
||||
"shop_shelf": level_frag.get("shop_shelf", []), # 货架入断点(离开店/非店停点 = 空架合法态)
|
||||
"shop_visit_buys": int(level_frag.get("shop_visit_buys", 0)), # 本次进店已购次数(A 链定价输入)
|
||||
"attunement_charges": (_loadout.breakpoint_fragment(_runner.treasure_inventory) if _runner != null and _runner.treasure_inventory != null else {"attunement_charges": {}}).get("attunement_charges", {}), # 同调主动件充能余量随断点(行派生——save v4 域键名策略不变)
|
||||
"graph_gen_v2": 1, # 生成图换代代差标记(缺键 = 换代前旧断点 → resume 显式拒绝)
|
||||
"master_seed": master_seed,
|
||||
"rng_counter": rng.counter(),
|
||||
"keystone": int(journal_frag.get("keystone", 0)),
|
||||
"visited_ids": level_frag.get("visited_ids", []),
|
||||
"aux_items": level_frag.get("aux_items", []),
|
||||
"bonus_taken": _curio.taken.duplicate(true), # 三选一件获取次数(kind 维度派生账本——断点附随件与读数承载)
|
||||
"bonus_deck": _curio.deck.duplicate(true), # 令牌池余库(TP0 v2.2——池消耗即上限,续玩余库逐枚还原)
|
||||
"relics": stats_frag.get("relics", []), # 特性持有集(局内随机渠道获得态续玩防丢失)
|
||||
"bonus_pending": stop_frag.get("bonus_pending", {}), # 三选一停点态(空 = 无停点——选择停点可中断契约)
|
||||
"feat_ticket_pity": int(stop_frag.get("feat_ticket_pity", 0)), # T5 专长凭证保底计数(扩列向后兼容——旧档缺键缺省 0)
|
||||
"feat_ticket_sold": int(stop_frag.get("feat_ticket_sold", 0)), # T5 专长凭证限购计数(同上)
|
||||
"adventure_counters": adventure_counters.duplicate(true), # adventure 域计数器随断点(续玩跨战账不丢——桶归宿主字典)
|
||||
"vision_counter_used": int(journal_frag.get("vision_counter_used", 0)), # 真视使用计数随断点(续玩跨战账不丢)
|
||||
"refresh_stock": int(bank_frag.get("refresh_stock", 0)), # 刷新存量账随断点
|
||||
"attune_refresh_y": int(bank_frag.get("attune_refresh_y", 0)), # Y 冒险周期池余量随断点(扩列向后兼容——旧档缺键回退参数面)
|
||||
"bonus_result": stop_frag.get("bonus_result", {}), # 已兑付待推进态(空 = 无——中断续玩不重兑付)
|
||||
"keystone_drops": journal_frag.get("keystone_drops", []), # 逐杀掉落明细(摘要归因续接)
|
||||
"gain_journal": journal_frag.get("gain_journal", {}), # 获得日记(唯一声明的断点扩列——日记为现行事实源)
|
||||
}
|
||||
# 拼盘完整性自断言(fail-fast——片段并集与登记键集不符即中止落盘)
|
||||
var stray: Array[Variant] = []
|
||||
for key in parts.keys():
|
||||
if not BREAKPOINT_FIELDS.has(key):
|
||||
stray.append(key)
|
||||
if not stray.is_empty():
|
||||
push_error("[冒险结构] 断点拼盘含未登记键: %s(fail-fast——片段与登记单源失同步)" % str(stray))
|
||||
return parts
|
||||
|
||||
|
||||
|
||||
# 读侧单点(BREAKPOINT_FIELDS 策略标签分发;未登记键 fail-fast 不静默按 plain 兜)。
|
||||
# 二次消费面(_start_current_level 经 _resume_bp 取 node_id/visited_ids/aux_items)同走此口。
|
||||
func _bp_read(key: String, bp: Dictionary, fallback: Variant = null) -> Variant:
|
||||
if not BREAKPOINT_FIELDS.has(key):
|
||||
push_error("[冒险结构] 断点字段未登记(读侧): %s" % key)
|
||||
var v: Variant = bp.get(key, fallback)
|
||||
if _bp_strategy(key) == "keep_on_null" and v != null and not (v is Dictionary or v is Array):
|
||||
return null # 类型不符——保持既有态(调用方跳过覆盖)
|
||||
return v
|
||||
|
||||
|
||||
# 读侧策略标签(BREAKPOINT_FIELDS 读侧消费面单点)
|
||||
func _bp_strategy(key: String) -> String:
|
||||
return String((BREAKPOINT_FIELDS.get(key, {}) as Dictionary).get("strategy", "plain"))
|
||||
|
||||
|
||||
# 数值类读侧(clamp 夹 ≥0 / params_fallback 断点缺键回退参数面整额注入 / plain 直取)
|
||||
func _bp_read_int(key: String, bp: Dictionary, fallback: int, params: Dictionary = {}) -> int:
|
||||
match _bp_strategy(key):
|
||||
"clamp":
|
||||
return maxi(0, int(bp.get(key, fallback)))
|
||||
"params_fallback":
|
||||
return maxi(0, int(bp.get(key, int(params.get(key, fallback)))))
|
||||
_:
|
||||
return int(bp.get(key, fallback))
|
||||
|
||||
|
||||
_bp_store.snapshot(state, level_index, master_seed, _runner, adventure_counters)
|
||||
@@ -0,0 +1,218 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## 存档断点组装与回放唯一承载(AdventureRunner 拆件——单一变化原因=断点快照键面与回放策略):
|
||||
## 断点快照拼盘、回放回填、读侧策略分发;BREAKPOINT_FIELDS 27 键登记单源随件(键名/策略标签/键序冻结)。
|
||||
## 构造=引用束字典注入(db/rng/主角账三件/curio/stop/bank/loadout——件禁反向持宿主);
|
||||
## 快照宿主输入(state/level_index/master_seed/runner/adventure_counters)逐调传参;
|
||||
## resume_adventure 留宿主(壳+宿主侧回填语句——所有权缝分家,语句序逐步保形)。
|
||||
class_name BreakpointStore
|
||||
extends RefCounted
|
||||
|
||||
# ---- 断点快照(字段登记单源:写侧拼装与读侧回退策略同登记一表)----
|
||||
# 键序即快照键序(27 顶层键——写侧/读侧/键集断言三面同源)。读侧回退策略词表:
|
||||
# plain 无特殊回退面(缺键取代码内缺省)/clamp 数值夹 ≥0/keep_on_null 缺键或类型不符保持既有态
|
||||
# dual_merge 双向合并(bonus_taken)/params_fallback 缺键回退参数面(attune_refresh_y)
|
||||
# token_migration 旧版候选形态迁移(bonus_pending.candidates)
|
||||
# 注入缺省 null = 整面静默(模拟/冒烟零扰动)。
|
||||
const BREAKPOINT_FIELDS := {
|
||||
"level_index": {"strategy": "plain"},
|
||||
"node_id": {"strategy": "plain"},
|
||||
"hero_hp": {"strategy": "clamp"},
|
||||
"bonus": {"strategy": "clamp"},
|
||||
"gp": {"strategy": "plain"},
|
||||
"shop_purchased": {"strategy": "plain"},
|
||||
"shop_shelf": {"strategy": "plain"},
|
||||
"shop_visit_buys": {"strategy": "plain"},
|
||||
"attunement_charges": {"strategy": "keep_on_null"},
|
||||
"graph_gen_v2": {"strategy": "plain"},
|
||||
"master_seed": {"strategy": "plain"},
|
||||
"rng_counter": {"strategy": "plain"},
|
||||
"keystone": {"strategy": "plain"},
|
||||
"visited_ids": {"strategy": "plain"},
|
||||
"aux_items": {"strategy": "plain"},
|
||||
"bonus_taken": {"strategy": "dual_merge"},
|
||||
"bonus_deck": {"strategy": "keep_on_null"},
|
||||
"relics": {"strategy": "plain"},
|
||||
"bonus_pending": {"strategy": "token_migration"},
|
||||
"feat_ticket_pity": {"strategy": "plain"},
|
||||
"feat_ticket_sold": {"strategy": "plain"},
|
||||
"adventure_counters": {"strategy": "keep_on_null"},
|
||||
"vision_counter_used": {"strategy": "clamp"},
|
||||
"refresh_stock": {"strategy": "clamp"},
|
||||
"attune_refresh_y": {"strategy": "params_fallback"},
|
||||
"bonus_result": {"strategy": "plain"},
|
||||
"keystone_drops": {"strategy": "plain"},
|
||||
"gain_journal": {"strategy": "plain"}, # 获得日记(拆件批 T4 扩列——唯一声明的断点扩展;旧档缺键=汇总直载跳过比对)
|
||||
}
|
||||
|
||||
# 引用束(宿主构造注入——成员名与宿主侧原名一致,迁移函数体逐字)
|
||||
var db: ConfigDb
|
||||
var rng: RngServiceCore
|
||||
var _stats: HeroStats
|
||||
var _wallet: GpWallet
|
||||
var _journal: GainJournal
|
||||
var _curio: CurioDeck = null # 令牌池件(池余库/获取计数随断点)
|
||||
var _stop: CurioStop = null # 停点件(停点态/凭证计数片段)
|
||||
var _refresh_bank: RefreshBank = null # 刷新账件(刷新存量/Y 池片段)
|
||||
var _loadout: AttuneLoadout = null # 同调配装件(充能行集读侧)
|
||||
# 断点域六成员(自宿主收编——宿主其余读写点经本件)
|
||||
var _save_manager = null # 断点承载注入(null = 不写断点;模拟/冒烟缺省)
|
||||
var _resume_bp := {} # 续玩快照暂存(resume_adventure → _start_current_level 单跳传递)
|
||||
var _pending_next_level := false # 大奖励已兑付、等 continue_to_next_level 确认
|
||||
var _last_level_payload := {} # 最近一次关卡结束载荷(失败结算的败退节点明细来源——数值验证取数面)
|
||||
var _last_treasure_cleared := 0 # additive(最近一次关卡末清空计数,summary 取数面)
|
||||
var _loadout_seeded := false # 同调行已种标记(每冒险一次——首关 bind 时点投行;行跨关存活后续关零动作)
|
||||
|
||||
|
||||
func _init(refs: Dictionary) -> void:
|
||||
db = refs["db"]
|
||||
rng = refs["rng"]
|
||||
_stats = refs["stats"]
|
||||
_wallet = refs["wallet"]
|
||||
_journal = refs["journal"]
|
||||
_curio = refs["curio"]
|
||||
_stop = refs["stop"]
|
||||
_refresh_bank = refs["bank"]
|
||||
_loadout = refs["loadout"]
|
||||
|
||||
|
||||
# 断点快照拼盘落盘(宿主 _snapshot_breakpoint 委托目标——宿主输入逐调传参)
|
||||
func snapshot(state: String, level_index: int, master_seed: int, runner: LevelRunner, counters: Dictionary[Variant, Variant]) -> void:
|
||||
if _save_manager == null or state != "running":
|
||||
return
|
||||
var parts: Dictionary[Variant, Variant] = _bp_parts(level_index, master_seed, runner, counters)
|
||||
var bp: Dictionary[Variant, Variant] = {}
|
||||
for key in BREAKPOINT_FIELDS.keys():
|
||||
bp[key] = parts[key]
|
||||
_save_manager.set_breakpoint(bp)
|
||||
_save_manager.write_to_disk()
|
||||
|
||||
|
||||
# 断点写侧取值(键集以 BREAKPOINT_FIELDS 为准——未登记键即取值越界,fail-fast)
|
||||
func _bp_parts(level_index: int, master_seed: int, runner: LevelRunner, counters: Dictionary[Variant, Variant]) -> Dictionary[Variant, Variant]:
|
||||
var stats_frag: Dictionary[Variant, Variant] = _stats.breakpoint_fragment()
|
||||
var wallet_frag: Dictionary[Variant, Variant] = _wallet.breakpoint_fragment()
|
||||
var journal_frag: Dictionary[Variant, Variant] = _journal.breakpoint_fragment()
|
||||
var bank_frag: Dictionary[Variant, Variant] = _refresh_bank.breakpoint_fragment()
|
||||
var stop_frag: Dictionary[Variant, Variant] = _stop.breakpoint_fragment()
|
||||
var level_frag: Dictionary[Variant, Variant] = runner.breakpoint_fragment() if runner != null else {
|
||||
"node_id": -1, "visited_ids": [] as Array, "aux_items": [] as Array,
|
||||
"shop_shelf": [] as Array, "shop_visit_buys": 0} # 三选一停点续玩分支 runner 为 null——节点域按缺省落档
|
||||
# 该两态的续玩只消费 bonus_* 附随件(选择停点/已兑付停点快照)
|
||||
var parts: Dictionary[Variant, Variant] = {
|
||||
"level_index": level_index,
|
||||
"node_id": int(level_frag.get("node_id", -1)),
|
||||
"hero_hp": int(stats_frag.get("hero_hp", 0)),
|
||||
"bonus": journal_frag.get("bonus", {}),
|
||||
"gp": int(wallet_frag.get("gp", 0)),
|
||||
"shop_purchased": journal_frag.get("shop_purchased", {}),
|
||||
"shop_shelf": level_frag.get("shop_shelf", []), # 货架入断点(离开店/非店停点 = 空架合法态)
|
||||
"shop_visit_buys": int(level_frag.get("shop_visit_buys", 0)), # 本次进店已购次数(A 链定价输入)
|
||||
"attunement_charges": (_loadout.breakpoint_fragment(runner.treasure_inventory) if runner != null and runner.treasure_inventory != null else {"attunement_charges": {}}).get("attunement_charges", {}), # 同调主动件充能余量随断点(行派生——save v4 域键名策略不变)
|
||||
"graph_gen_v2": 1, # 生成图换代代差标记(缺键 = 换代前旧断点 → resume 显式拒绝)
|
||||
"master_seed": master_seed,
|
||||
"rng_counter": rng.counter(),
|
||||
"keystone": int(journal_frag.get("keystone", 0)),
|
||||
"visited_ids": level_frag.get("visited_ids", []),
|
||||
"aux_items": level_frag.get("aux_items", []),
|
||||
"bonus_taken": _curio.taken.duplicate(true), # 三选一件获取次数(kind 维度派生账本——断点附随件与读数承载)
|
||||
"bonus_deck": _curio.deck.duplicate(true), # 令牌池余库(TP0 v2.2——池消耗即上限,续玩余库逐枚还原)
|
||||
"relics": stats_frag.get("relics", []), # 特性持有集(局内随机渠道获得态续玩防丢失)
|
||||
"bonus_pending": stop_frag.get("bonus_pending", {}), # 三选一停点态(空 = 无停点——选择停点可中断契约)
|
||||
"feat_ticket_pity": int(stop_frag.get("feat_ticket_pity", 0)), # T5 专长凭证保底计数(扩列向后兼容——旧档缺键缺省 0)
|
||||
"feat_ticket_sold": int(stop_frag.get("feat_ticket_sold", 0)), # T5 专长凭证限购计数(同上)
|
||||
"adventure_counters": counters.duplicate(true), # adventure 域计数器随断点(续玩跨战账不丢——桶归宿主字典)
|
||||
"vision_counter_used": int(journal_frag.get("vision_counter_used", 0)), # 真视使用计数随断点(续玩跨战账不丢)
|
||||
"refresh_stock": int(bank_frag.get("refresh_stock", 0)), # 刷新存量账随断点
|
||||
"attune_refresh_y": int(bank_frag.get("attune_refresh_y", 0)), # Y 冒险周期池余量随断点(扩列向后兼容——旧档缺键回退参数面)
|
||||
"bonus_result": stop_frag.get("bonus_result", {}), # 已兑付待推进态(空 = 无——中断续玩不重兑付)
|
||||
"keystone_drops": journal_frag.get("keystone_drops", []), # 逐杀掉落明细(摘要归因续接)
|
||||
"gain_journal": journal_frag.get("gain_journal", {}), # 获得日记(唯一声明的断点扩列——日记为现行事实源)
|
||||
}
|
||||
# 拼盘完整性自断言(fail-fast——片段并集与登记键集不符即中止落盘)
|
||||
var stray: Array[Variant] = []
|
||||
for key in parts.keys():
|
||||
if not BREAKPOINT_FIELDS.has(key):
|
||||
stray.append(key)
|
||||
if not stray.is_empty():
|
||||
push_error("[冒险结构] 断点拼盘含未登记键: %s(fail-fast——片段与登记单源失同步)" % str(stray))
|
||||
return parts
|
||||
|
||||
|
||||
# 续玩账面回填(生命/特性/钱包/加算区/已购档/钥石/逐杀明细/真视——主角账三件片段各还原各键;
|
||||
# bonus 子键 clamp 类夹 ≥0;日记在档=推导比对载入、缺键=旧档汇总直载)
|
||||
#(宿主侧账面语句——summary/levels_total/level_index/_pending_next_level——按语句序留宿主壳)
|
||||
func restore_ledger(bp: Dictionary, params: Dictionary) -> void:
|
||||
_stats.restore_fragment(bp, params)
|
||||
_wallet.restore_fragment(bp, params)
|
||||
_journal.restore_fragment(bp, params)
|
||||
|
||||
|
||||
# 续玩三选一态回填(池与件计数归件、停点态归宿主;dual_merge/keep_on_null 策略见 BREAKPOINT_FIELDS)
|
||||
#(前置清理 _reset_curio_state 留宿主——桶归宿主字典;本函数承接其后的件侧还原语句,序逐步保形)
|
||||
func restore_curio_state(bp: Dictionary) -> void:
|
||||
var bp_taken: Dictionary[Variant, Variant] = bp.get("bonus_taken", {}) as Dictionary
|
||||
for id in _curio.taken.keys():
|
||||
_curio.taken[id] = maxi(0, int(bp_taken.get(id, 0)))
|
||||
for id in bp_taken.keys():
|
||||
_curio.taken[String(id)] = maxi(0, int(bp_taken.get(id, 0))) # 身份维键(relic:<feat_id>)随快照还原
|
||||
# 令牌池余库还原(TP0 v2.2——快照缺键 = 旧版断点,保持满库重建态:旧档只读一版周期的防御口径;
|
||||
# relic 令牌 feat_id 随还原)
|
||||
var bp_deck = _bp_read("bonus_deck", bp)
|
||||
if bp_deck is Array:
|
||||
var restored: Array[Variant] = []
|
||||
for t in (bp_deck as Array):
|
||||
var tok: Dictionary[Variant, Variant] = t as Dictionary
|
||||
var rtok := {"kind": String(tok.get("kind", "")), "tier": String(tok.get("tier", ""))}
|
||||
if String(rtok["kind"]) == "relic":
|
||||
rtok["feat_id"] = String(tok.get("feat_id", "")) # relic 令牌 feat_id 随还原
|
||||
restored.append(rtok)
|
||||
_curio.deck = restored
|
||||
# 特性持有集还原(缺键 = 旧版断点防御空集——主角数值账字段单点)
|
||||
_stats.feats = []
|
||||
for r in (bp.get("relics", []) as Array):
|
||||
_stats.feats.append(String(r))
|
||||
|
||||
|
||||
# 续玩参数面——数值账规范化与越界钳制(件侧;宿主侧四重绑定与预算注入按语句序夹在两件侧调用之间)
|
||||
func apply_resume_params(params: Dictionary) -> void:
|
||||
_stats.rebase_override(params.get("hero_override", {})) # 续玩参数面同 start_adventure(规范化归数值账)
|
||||
_stats.attack_type = String(params.get("attack_type", "")) # 续玩参数面同 start_adventure(账号层选中态随 resume 重注入)
|
||||
_stats.armor_type = String(params.get("armor_type", ""))
|
||||
# 生命账本越界钳制(T17——spawn 守卫 EC16 免疫):断点可能带出高于生效上限的账本值
|
||||
#(如调试浮窗调高期写出的值,实例:hero_hp=28 > 基础 26)——恢复后钳到生效上限,
|
||||
# 否则首战 spawn「hero_hp 入参越界」拒收 → 「敌人系统 spawn 被拒」fatal。钳制式单点归
|
||||
# 数值账 clamp_hp_to_resume_cap(保守口径不含加算区——与原钳制式逐字同)。
|
||||
_stats.clamp_hp_to_resume_cap(int(db.combat.hero.max_hp))
|
||||
|
||||
|
||||
# 停点件/刷新账片段回填(凭证计数;刷新存量/Y 池——Y 缺键回退参数面 params_fallback 类)
|
||||
func restore_stop_bank_fragments(bp: Dictionary, params: Dictionary) -> void:
|
||||
_stop.restore_fragment(bp, params)
|
||||
_refresh_bank.restore_fragment(bp, params)
|
||||
|
||||
|
||||
# 读侧单点(BREAKPOINT_FIELDS 策略标签分发;未登记键 fail-fast 不静默按 plain 兜)。
|
||||
# 二次消费面(_start_current_level 经 _resume_bp 取 node_id/visited_ids/aux_items)同走此口。
|
||||
func _bp_read(key: String, bp: Dictionary, fallback: Variant = null) -> Variant:
|
||||
if not BREAKPOINT_FIELDS.has(key):
|
||||
push_error("[冒险结构] 断点字段未登记(读侧): %s" % key)
|
||||
var v: Variant = bp.get(key, fallback)
|
||||
if _bp_strategy(key) == "keep_on_null" and v != null and not (v is Dictionary or v is Array):
|
||||
return null # 类型不符——保持既有态(调用方跳过覆盖)
|
||||
return v
|
||||
|
||||
|
||||
# 读侧策略标签(BREAKPOINT_FIELDS 读侧消费面单点)
|
||||
func _bp_strategy(key: String) -> String:
|
||||
return String((BREAKPOINT_FIELDS.get(key, {}) as Dictionary).get("strategy", "plain"))
|
||||
|
||||
|
||||
# 数值类读侧(clamp 夹 ≥0 / params_fallback 断点缺键回退参数面整额注入 / plain 直取)
|
||||
func _bp_read_int(key: String, bp: Dictionary, fallback: int, params: Dictionary = {}) -> int:
|
||||
match _bp_strategy(key):
|
||||
"clamp":
|
||||
return maxi(0, int(bp.get(key, fallback)))
|
||||
"params_fallback":
|
||||
return maxi(0, int(bp.get(key, int(params.get(key, fallback)))))
|
||||
_:
|
||||
return int(bp.get(key, fallback))
|
||||
@@ -0,0 +1 @@
|
||||
uid://cw5qdd012h728
|
||||
@@ -1,128 +1,31 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## CellUnit——BoardView SubViewport 世界内逐格渲染件(Node2D),全部程序化 _draw 占位视觉(纹理槽位已留未装载)。
|
||||
## CellUnit——BoardView SubViewport 世界内逐格渲染件(Node2D),被动绘制壳:_draw 六步时序器
|
||||
## +视野迷雾域+微光子件生命周期+格件几何;基座层与标识叠加两绘制域件 _init 自建持有
|
||||
## (cell_base_layer/cell_mark_overlay——单一变化原因=迷雾规格与格件节点生命周期)。
|
||||
## 无 class_name(类名缓存面纪律)——使用方经 preload 引用。
|
||||
## 内部状态件(u3 子域拆件——契约 9 职责语句「单格绘制输入集」):本件为被动绘制壳,
|
||||
## 状态件 _CellPaintState 承载全部绘制入参(单一变化原因=格面呈现),setter 触发宿主重绘语义
|
||||
## 经 _owner 回引保持逐字;探针直读冻结成员(glyph/fog_state/plate/glyph_color)与 _glow_layer 住宿主。
|
||||
## 探针直读冻结成员(glyph/fog_state/plate/glyph_color)与 _glow_layer 住宿主;
|
||||
## hl_color/hl_visible/dot_visible/mutation_mark 探针读 u.face.*。
|
||||
extends Node2D
|
||||
|
||||
const _GLOW_LAYER := preload("res://presentation/views/cell_type_glow_layer.gd")
|
||||
const _BASE_LAYER := preload("res://presentation/views/cell_base_layer.gd")
|
||||
const _MARK_OVERLAY := preload("res://presentation/views/cell_mark_overlay.gd")
|
||||
|
||||
# ── 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_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) # 投影远段(渐隐收边)
|
||||
# ── E 格高墙面(圣经 v3.5 冻结①——地面格南邻墙→南部 30% 承接墙面:墙顶纹理原亮度
|
||||
# 直贴零暗档零阴影;墙脚阴线只画地面/墙面交界;(b)-(c) 交界
|
||||
# 无边框由 BoardView 侧跳过墙格北向描边承载)──
|
||||
const E_FACADE_FRAC := 0.30 # 墙面承接高(格边比 30%)
|
||||
|
||||
|
||||
## 单格绘制输入集(被动绘制壳的全部绘制入参——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 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 cell := Vector2i.ZERO # 板坐标(诊断观测面)
|
||||
# ── 未开格类型微光(英雄 8 向相邻未开格的组粒度类型提示)
|
||||
# 作用集 = 可挖域 ∩ 身份可查格,由 BoardView 侧派生下发;本件只承接绘制与子件生命周期。
|
||||
# 静态:不呼吸、不入动效预算;相位门控(结算/道具窗口态隐藏)在 BoardView 侧。
|
||||
var glow_group := 0:
|
||||
set(v):
|
||||
if glow_group != v:
|
||||
glow_group = v
|
||||
_sync_glow_layer()
|
||||
var glyph := "": # 文字位——数字/雷/封印/人/?(现行视觉封印与数字互斥,同位承载;探针直读冻结)
|
||||
set(v):
|
||||
glyph = v
|
||||
@@ -141,268 +44,51 @@ var fog_state := 0: # 探针直读冻结
|
||||
queue_redraw()
|
||||
|
||||
var _glow_layer: Node2D = null
|
||||
var base = null # 格面基座层绘制域件(_init 自建)
|
||||
var face = null # 格面标识与叠加层绘制域件(_init 自建)
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
st._owner = self
|
||||
base = _BASE_LAYER.new(self)
|
||||
face = _MARK_OVERLAY.new(self)
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
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 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 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 提亮)
|
||||
func _draw_contact_or_fallback(rect: Rect2) -> void:
|
||||
if st.contact_col.a > 0.0:
|
||||
_draw_north_projection() # 平色回退+微光并发的英雄格补画(瓦片路径已画——同像素重复无观感差)
|
||||
_draw_e_facade() # E 格墙面补画(同上——英雄格兼 E 格时瓦片路径已画,重复无害)
|
||||
_draw_contact_bands()
|
||||
elif st.bg_sb != null:
|
||||
draw_style_box(st.bg_sb, rect)
|
||||
_draw_south_facade() # 平色回退路径——D 投影档两件同样服役(结构归程序)
|
||||
_draw_north_projection()
|
||||
_draw_bevel()
|
||||
|
||||
|
||||
## ②③④ 中层域:变异纹带与标记/数字色牌与字形/徽章/封印
|
||||
func _draw_glyph_layer(rect: Rect2) -> void:
|
||||
# ② 变异纹带(无程序化纹带——槽位先行,贴图落位即用)
|
||||
if st.tex_mutation != null:
|
||||
draw_texture_rect(st.tex_mutation, rect, false)
|
||||
# ③ 数字/徽章(数字 ≥ 格高 50% 墨族对格底 ≥4.5:1;徽章右下角金,遮蔽态 = "?")
|
||||
if glyph != "":
|
||||
if plate:
|
||||
_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(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(st.badge_glyph)
|
||||
# ④ 封印(现行视觉封印文字在 ③ 文字位承载——独立槽位留给封印图)
|
||||
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 st.dot_visible:
|
||||
var ds := 6.0
|
||||
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, st.edge - 2.0, st.edge - 2.0), Color(st.hl_color, 0.55), false, 2.0) # 2px 内描边
|
||||
_draw_fog_layer()
|
||||
# ⑦ 中立移除退色层(退色降档:暗幕压顶全层覆盖,暗淡记忆态同族呈现语义;
|
||||
# 顶层于迷雾之上——移除格不再参与迷雾语义。本实现移除集=未揭示危险格(无数字无封印印))
|
||||
if st.removed_dim:
|
||||
draw_rect(Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)), Color(0.12, 0.10, 0.09, 0.62), true)
|
||||
var rect := Rect2(Vector2.ZERO, Vector2(edge, edge))
|
||||
base.draw_base(self, rect) # 基座层整树(描边/立面/投影/接触带/倒角与其补画序)
|
||||
face.draw_marks(self, rect) # 变异标记
|
||||
face.draw_glyph_layer(self, rect, glyph, glyph_color, plate) # 字形/色牌/徽章/封印
|
||||
face.draw_overlay_under(self, rect) # 点迹+高亮(雾层由宿主夹在其后)
|
||||
_draw_fog_layer(rect) # 宿主迷雾域(原位——雾夹在 hl 与 removed 之间)
|
||||
face.draw_removed_dim(self, rect) # 退色层
|
||||
|
||||
|
||||
## ⑥ 视野迷雾层(最顶遮罩,§6.5 落文):
|
||||
## 全黑=不透明纯黑盖全部叠层(含封印印/徽章/高亮——封印印记随格隐没,§6.5 显式推论);
|
||||
## 记忆态=记忆板素色压平(纹理免疫——色牌/封印印/徽章随板隐没)+数字记忆墨重画(数字可读硬指标 ≥4.5:1)
|
||||
func _draw_fog_layer() -> void:
|
||||
func _draw_fog_layer(rect: Rect2) -> void:
|
||||
if fog_state == 1:
|
||||
draw_rect(Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)), Color(0, 0, 0, 1.0), true)
|
||||
draw_rect(rect, 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 st.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))
|
||||
_draw_fog_memory(rect)
|
||||
|
||||
|
||||
## 记忆态换装域(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)
|
||||
func _draw_fog_memory(rect: Rect2) -> void:
|
||||
if base.tex_base != null:
|
||||
draw_texture_rect(base.tex_base, rect, false)
|
||||
draw_rect(rect, 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 st.facade_south:
|
||||
return
|
||||
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, 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 st.proj_band <= 0.0:
|
||||
return
|
||||
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 移植):本格已开且南邻为墙 →
|
||||
# 南部 30% 承接墙面。材质 = 墙顶 center 瓦片顶部 30% 区带**原亮度**直贴(零暗档零阴影);
|
||||
# 墙脚阴线只画地面/墙面交界(strip 顶缘 1px);墙面底缘(格界处)不画任何线——直接接南侧墙顶
|
||||
# (白边导致两截)。仅瓦片态服役(tex_facade 缺位 = 平色回退原语义不动)。
|
||||
func _draw_e_facade() -> void:
|
||||
if not st.facade_strip or st.tex_facade == null:
|
||||
return
|
||||
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, st.edge, 1.0), UiKit.E_FACADE_FOOT_COL, true)
|
||||
|
||||
|
||||
# 文字位绘制:水平居中 + 垂直居中(基线 = (格高 − 字高)/2 + 上伸部)
|
||||
func _draw_glyph(g: String, col: Color, fs: int) -> void:
|
||||
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, (st.edge - (asc + desc)) * 0.5 + asc), g,
|
||||
HORIZONTAL_ALIGNMENT_CENTER, st.edge, fs, col)
|
||||
face.draw_centered_text(self, rect, glyph, UiKit.FOG_MEMORY_INK, maxi(18, int(ceil(edge * 0.5)))) # 字号下限 18(§7.4 冻结公式同源)
|
||||
|
||||
|
||||
# 微光子件生命周期:有组才建、组变即换、无组即撤——作用集 ≤8 格常驻,
|
||||
# 其余格零节点零绘制成本。与父件 _draw() 分离的三条理由见本件头注(命令序列/重绘预算/材质挂点)。
|
||||
func _sync_glow_layer() -> void:
|
||||
if st.glow_group == 0:
|
||||
if glow_group == 0:
|
||||
if _glow_layer != null and is_instance_valid(_glow_layer):
|
||||
_glow_layer.queue_free()
|
||||
_glow_layer = null
|
||||
@@ -410,15 +96,5 @@ 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(st.edge, st.glow_group)
|
||||
_glow_layer.set_badge_mask(st.badge_glyph != "")
|
||||
|
||||
|
||||
# 徽章绘制:右下角金(文字贴角内缩 2–3px)
|
||||
func _draw_badge(g: String) -> void:
|
||||
var f := st.font if st.font != null else ThemeDB.fallback_font
|
||||
if f == null:
|
||||
return
|
||||
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(st.edge - sz.x - 2.0, st.edge - 3.0), g, HORIZONTAL_ALIGNMENT_LEFT, -1, fs, UiKit.GOLD)
|
||||
_glow_layer.setup(edge, glow_group)
|
||||
_glow_layer.set_badge_mask(face.badge_glyph != "")
|
||||
@@ -0,0 +1,130 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## 悬停预承诺呈现域件(BoardView 拆件——单一变化原因=悬停反馈语义):
|
||||
## 三语义光标、目标格边框、BFS 路径预览、chord 双击首遇锚的唯一承载(自 board_view 收编 11 函数族)。
|
||||
## 无 class_name(类名缓存面纪律)——经宿主 board_view preload 引用;构造=引用束注入
|
||||
## (cells/minesweeper/diggable 共享引用+格命中换算/壳级光标写入/hover_forbidden 与 chord 发射注入 Callable——
|
||||
## 输入经宿主三入站口委托,禁反向持宿主引用);_r0 出生展开区成员留宿主。
|
||||
extends RefCounted
|
||||
|
||||
const COL_HL_TARGET := UiKit.GOLD # 悬停目标格边框(金)
|
||||
const COL_SEAL := UiKit.SEAL_BLACK # 封印黑(与宿主 :658 双侧服役——各自 alias UiKit 单源)
|
||||
const CURSOR_DIG := Input.CURSOR_CROSS # 挖——镐形自绘纹理载体
|
||||
const CURSOR_MOVE_SEM := Input.CURSOR_ARROW # 移动——箭头(命名避开 Control.CURSOR_MOVE 原生成员遮蔽)
|
||||
const CHORD_WINDOW_MS := 350
|
||||
|
||||
var _cells := {} # 共享引用(宿主容器同对象——rebuild 只 clear 不重绑)
|
||||
var _minesweeper = null # 排雷玩法 只读查询引用(宿主 rebuild 重绑时同拍回写本成员)
|
||||
var _diggable := {} # 共享引用(可挖域——呼吸微光作用集同源)
|
||||
var _cb_cell_at: Callable # 格命中换算(宿主 _cell_at——整图公式单点留宿主)
|
||||
var _cb_cursor: Callable # 壳级光标写入(宿主 mouse_default_cursor_shape 单点)
|
||||
var _cb_forbidden: Callable # hover_forbidden 信号发射(首遇 toast 去重归组合根)
|
||||
var _cb_chord: Callable # chord_attempted 信号发射(chord 肌肉记忆提示锚)
|
||||
var _hover := Vector2i(-9, -9)
|
||||
var _last_click := {"cell": Vector2i(-9, -9), "ms": -100000}
|
||||
|
||||
|
||||
func _init(bundle: Dictionary) -> void:
|
||||
_cells = bundle["cells"]
|
||||
_minesweeper = bundle["minesweeper"]
|
||||
_diggable = bundle["diggable"]
|
||||
_cb_cell_at = bundle["cell_at"]
|
||||
_cb_cursor = bundle["cursor"]
|
||||
_cb_forbidden = bundle["forbidden"]
|
||||
_cb_chord = bundle["chord"]
|
||||
|
||||
|
||||
func _hover_at(pos: Vector2) -> void:
|
||||
var c: Vector2i = _cb_cell_at.call(pos)
|
||||
if c.x < 0:
|
||||
if _hover.x >= 0:
|
||||
_on_cell_unhover(_hover)
|
||||
elif c != _hover:
|
||||
if _hover.x >= 0:
|
||||
_on_cell_unhover(_hover)
|
||||
_on_cell_hover(c)
|
||||
|
||||
|
||||
func _on_shell_mouse_exited() -> void:
|
||||
if _hover.x >= 0:
|
||||
_on_cell_unhover(_hover)
|
||||
|
||||
|
||||
# chord 肌肉记忆首遇锚:同格双左击快开尝试(350ms 窗)——每 run 一次提示由组合根去重
|
||||
func _detect_chord(c: Vector2i) -> void:
|
||||
var now := Time.get_ticks_msec()
|
||||
if _last_click.cell == c and now - int(_last_click.ms) <= CHORD_WINDOW_MS:
|
||||
_cb_chord.call(c)
|
||||
_last_click.ms = -100000
|
||||
return
|
||||
_last_click = {"cell": c, "ms": now}
|
||||
|
||||
|
||||
func _on_cell_hover(c: Vector2i) -> void:
|
||||
_hover = c
|
||||
_apply_hover_visuals(c)
|
||||
|
||||
|
||||
func _on_cell_unhover(c: Vector2i) -> void:
|
||||
if _hover == c:
|
||||
_hover = Vector2i(-9, -9)
|
||||
_clear_hover_visuals(c)
|
||||
|
||||
|
||||
func _refresh_hover_after_paint() -> void:
|
||||
# 状态随 refresh 变化后光标/边框语义需重算(悬停格四格态可能已变)
|
||||
if _hover.x >= 0 and _cells.has(_hover):
|
||||
_apply_hover_visuals(_hover)
|
||||
|
||||
|
||||
func _apply_hover_visuals(c: Vector2i) -> void:
|
||||
if _minesweeper == null or not _cells.has(c):
|
||||
return
|
||||
_clear_hover_visuals(c)
|
||||
var u: Node2D = _cells[c]
|
||||
var revealed: bool = _minesweeper.is_revealed(c)
|
||||
var seal: bool = _minesweeper.has_seal(c)
|
||||
var adjacent: bool = _diggable.has(c)
|
||||
if revealed:
|
||||
u.face.hl_visible = true
|
||||
u.face.hl_color = COL_HL_TARGET # 移动目标格边框(金)
|
||||
_set_cursor_shape(c, CURSOR_MOVE_SEM)
|
||||
_show_path_preview(c)
|
||||
elif adjacent:
|
||||
u.face.hl_visible = true
|
||||
u.face.hl_color = COL_HL_TARGET # 挖掘目标格边框(相邻封印格同此——左键直接挖)
|
||||
_set_cursor_shape(c, CURSOR_DIG)
|
||||
elif seal:
|
||||
u.face.hl_visible = true
|
||||
u.face.hl_color = COL_SEAL # 已承诺封印标记(封印黑;无交互可供)
|
||||
_set_cursor_shape(c, Input.CURSOR_FORBIDDEN)
|
||||
else:
|
||||
u.face.hl_visible = true
|
||||
u.face.hl_color = UiKit.RED_FORBID # 不可及域灰禁框(禁止光标+首遇 toast 双通道)
|
||||
_set_cursor_shape(c, Input.CURSOR_FORBIDDEN)
|
||||
_cb_forbidden.call(c)
|
||||
|
||||
|
||||
func _clear_hover_visuals(c: Vector2i) -> void:
|
||||
if not _cells.has(c):
|
||||
return
|
||||
_cells[c].face.hl_visible = false
|
||||
_hide_path_preview()
|
||||
|
||||
|
||||
func _set_cursor_shape(c: Vector2i, shape: int) -> void:
|
||||
_cb_cursor.call(shape) # 壳级光标(悬停格语义决定——单格悬停恒一)
|
||||
|
||||
|
||||
func _show_path_preview(c: Vector2i) -> void:
|
||||
# 合法性指示:悬停已开格 → BFS 路径蓝细线点迹(compute_move_path 只读面)
|
||||
if _minesweeper == null:
|
||||
return
|
||||
var path: Array[Variant] = _minesweeper.compute_move_path(c)
|
||||
for pc in path:
|
||||
if _cells.has(pc):
|
||||
_cells[pc].face.dot_visible = true
|
||||
|
||||
|
||||
func _hide_path_preview() -> void:
|
||||
for c in _cells:
|
||||
_cells[c].face.dot_visible = false
|
||||
@@ -0,0 +1 @@
|
||||
uid://dojreaughx7tf
|
||||
@@ -12,6 +12,7 @@ const _CELL_UNIT := preload("res://presentation/views/board_cell_unit.gd")
|
||||
const _BOARD_WORLD := preload("res://presentation/views/board_world.gd")
|
||||
const _TILE_PALETTE := preload("res://presentation/views/tile_palette.gd")
|
||||
const _HERO_SPRITE := preload("res://presentation/views/hero_sprite.gd")
|
||||
const _BOARD_HOVER := preload("res://presentation/views/board_hover.gd")
|
||||
|
||||
const COL_BRICK := UiKit.BRICK # 未开——掩尘完整地砖(色板原语 #C9AF83)
|
||||
const COL_FLOOR := UiKit.FLOOR # 已开——裸土 #B39A78(碎裂裸露地面)
|
||||
@@ -20,17 +21,13 @@ const COL_HERO := UiKit.HERO_BLUE # 小人边框——小人蓝
|
||||
const COL_SEAL := UiKit.SEAL_BLACK # 封印黑 #000000
|
||||
const COL_GOLD := UiKit.GOLD # 奖励金 #F0A828
|
||||
const COL_CONTACT := UiKit.CONTACT_GLOW # 接触带微光基色(蓝族浅一档·近白蓝——§3.2 v2.8)
|
||||
const COL_HL_TARGET := UiKit.GOLD # 悬停目标格边框(金)
|
||||
const REWARD_BADGE := { # 已开格格面资源身份 icon(cell_type 九型键)
|
||||
"attack": "攻", "defense": "守", "heal": "医", "blast": "爆", "weaken": "弱", "pierce": "穿", "magic": "魔",
|
||||
"action": "行", # 行 = 行动点型(ap_up/ap_drain 两行)
|
||||
"gp": "金", # 金 = GP 型(gp_gain/gp_loss 两行;金色系 ~40° 相位,禁入带紫赤 300–345° 外,取色 UiKit.GOLD 徽标色单点)
|
||||
}
|
||||
const BADGE_PLACEHOLDER := "?" # 未知图标键占位纹样(HUD框架:禁静默空图标)
|
||||
const CURSOR_DIG := Input.CURSOR_CROSS # 挖——镐形自绘纹理载体
|
||||
const CURSOR_SEAL := Input.CURSOR_POINTING_HAND # 封印——封印形自绘纹理载体
|
||||
const CURSOR_MOVE_SEM := Input.CURSOR_ARROW # 移动——箭头(命名避开 Control.CURSOR_MOVE 原生成员遮蔽)
|
||||
const CHORD_WINDOW_MS := 350
|
||||
const SEP := 2 # 格间距(格尺寸公式与命中换算共用)
|
||||
|
||||
# 棋盘立体参数
|
||||
@@ -67,8 +64,6 @@ var _contact_unit: Node2D = null # 接触带承载单元(=英雄格 CellUnit
|
||||
var _install_tw: Tween # 入场淡入 tween(rebuild 重入先杀)
|
||||
var _ripple_tws: Array[Tween] = [] # R0 涟漪逐格 tween(rebuild 重入全杀)
|
||||
var _dig_glow := false # 挖掘期才允许唯一在动元素(art-bible §2.1)
|
||||
var _hover := Vector2i(-9, -9)
|
||||
var _last_click := {"cell": Vector2i(-9, -9), "ms": -100000}
|
||||
var _r0 := {} # 出生展开区 R₀(地图战斗资源 mapping 资格域 = S∖R₀——徽章查询跳过,防 E19 诊断噪音)
|
||||
var _desat_mat: ShaderMaterial
|
||||
var _desat_tween: Tween
|
||||
@@ -77,6 +72,7 @@ var _cell := 46.0 # 当前格边长(set_cell_area 按容器尺寸重
|
||||
# ── 拆出件(状态单点在件内)──
|
||||
var _palette = _TILE_PALETTE.new() # 瓦片件:装载缓存+查表+明度/墨族静态算法
|
||||
var _hero_sprite = _HERO_SPRITE.new() # 英雄精灵件:_init 挂为本件子级(tween 需在树上)
|
||||
var _hover_comp: _BOARD_HOVER # 悬停预承诺呈现域件:三语义光标/目标格边框/路径预览/chord 首遇锚(_init 建为子件——HeroSprite 挂载先例)
|
||||
|
||||
# ── 件成员同名转发(探针直读面零改动:读 _palette.seal_tex、读写 _hero_sprite.tex_idle;其余为随迁状态单点再导出)──
|
||||
# ---- 视野迷雾(纯呈现层;现行事实源=HUD框架「视野迷雾」节 v0.13,渲染规格=art-bible §6.5 v3.5)----
|
||||
@@ -120,6 +116,25 @@ func _init() -> void:
|
||||
_hero_sprite.tex_idle = UiKit.load_tex(UiKit.TEX_HERO_IDLE)
|
||||
_hero_sprite.tex_walk = UiKit.load_tex(UiKit.TEX_HERO_WALK)
|
||||
_palette.load_board_tiles() # 高墙/地面瓦片整批守卫装载(缺位静默回退——英雄同款纪律,禁刷屏)
|
||||
_hover_comp = _BOARD_HOVER.new({
|
||||
"cells": _cells, "minesweeper": _minesweeper, "diggable": _diggable,
|
||||
"cell_at": _cell_at, "cursor": _set_shell_cursor,
|
||||
"forbidden": _emit_hover_forbidden, "chord": _emit_chord_attempted,
|
||||
})
|
||||
|
||||
|
||||
# ---- 悬停域注入面(件回调宿主的壳级单点——Callable 目标)----
|
||||
|
||||
func _set_shell_cursor(shape: int) -> void:
|
||||
mouse_default_cursor_shape = shape # 壳级光标(悬停格语义决定——单格悬停恒一)
|
||||
|
||||
|
||||
func _emit_hover_forbidden(c: Vector2i) -> void:
|
||||
hover_forbidden.emit(c) ## 非相邻未开格悬停(首遇 toast 归组合根去重)
|
||||
|
||||
|
||||
func _emit_chord_attempted(c: Vector2i) -> void:
|
||||
chord_attempted.emit(c) ## 双击快开尝试(chord 肌肉记忆提示锚)
|
||||
|
||||
|
||||
func board() -> BoardData:
|
||||
@@ -128,7 +143,7 @@ func board() -> BoardData:
|
||||
|
||||
func _ready() -> void:
|
||||
_apply_custom_cursors()
|
||||
mouse_exited.connect(_on_shell_mouse_exited)
|
||||
mouse_exited.connect(_hover_comp._on_shell_mouse_exited)
|
||||
|
||||
|
||||
# 结算期/道具窗口态:D 区去饱和 + 金橙偏移锁输入(明度不变——art-bible §2);
|
||||
@@ -182,7 +197,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].st.font = f
|
||||
_cells[c].face.font = f
|
||||
_cells[c].queue_redraw()
|
||||
|
||||
|
||||
@@ -201,7 +216,7 @@ func set_cell_area(avail: Vector2) -> void:
|
||||
var stride := _cell + float(SEP)
|
||||
for c in _cells:
|
||||
var u: Node2D = _cells[c]
|
||||
u.st.edge = _cell
|
||||
u.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 防飞陈旧落点)
|
||||
@@ -224,6 +239,7 @@ func _refresh_slab() -> void:
|
||||
func rebuild(board: BoardData, minesweeper: MinesweeperRules) -> void:
|
||||
_board = board
|
||||
_minesweeper = minesweeper
|
||||
_hover_comp._minesweeper = _minesweeper # 件内只读引用随宿主重绑同拍回写(每战新排雷玩法对象)
|
||||
_palette.bind_board(_board, _minesweeper) # 查表族棋盘态随板同步(换板后首拍 refresh 前置)
|
||||
for child in _world.get_children():
|
||||
child.queue_free()
|
||||
@@ -234,7 +250,7 @@ func rebuild(board: BoardData, minesweeper: MinesweeperRules) -> void:
|
||||
_contact_unit = null
|
||||
_hero_sprite.last = Vector2i(-99, -99)
|
||||
_r0.clear()
|
||||
_hover = Vector2i(-9, -9)
|
||||
_hover_comp._hover = Vector2i(-9, -9)
|
||||
for c in _board.compute_r0():
|
||||
_r0[c] = true
|
||||
# 瓦片套随板图型切换(board_gen 词表 v1.1 dungeon/underdark/deep_underdark;
|
||||
@@ -255,17 +271,17 @@ func rebuild(board: BoardData, minesweeper: MinesweeperRules) -> void:
|
||||
# 逐格构建器(rebuild 建格循环唯一出函面):.new() 与属性装配逐字迁入,循环体只调构建器
|
||||
func _build_cell_unit(c: Vector2i, stride: float) -> Node2D:
|
||||
var u: Node2D = _CELL_UNIT.new()
|
||||
u.st.cell = c
|
||||
u.st.edge = _cell
|
||||
u.cell = c
|
||||
u.edge = _cell
|
||||
u.position = Vector2(c) * stride
|
||||
u.st.bevel = DEPTH_BEVEL # 新建格单元继承
|
||||
u.base.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.st.font = _world.board_font
|
||||
u.face.font = _world.board_font
|
||||
_world.add_child(u)
|
||||
return u
|
||||
|
||||
@@ -283,7 +299,7 @@ func refresh() -> void:
|
||||
for y in _board.height:
|
||||
for x in _board.width:
|
||||
_paint_cell(Vector2i(x, y), hero)
|
||||
_refresh_hover_after_paint()
|
||||
_hover_comp._refresh_hover_after_paint()
|
||||
_apply_contact_bands()
|
||||
_update_hero(hero) # 英雄精灵 diff 驱动
|
||||
|
||||
@@ -433,33 +449,18 @@ func _update_world_size() -> void:
|
||||
|
||||
|
||||
# 真实输入路径(Node2D 迁移批 Decision 3):壳层 _gui_input → 整图公式换算 → 复用既有 hover/点选/chord 出口
|
||||
#(悬停族已拆 board_hover 件——本壳留三入站口委托)
|
||||
func _gui_input(ev: InputEvent) -> void:
|
||||
if _minesweeper == null:
|
||||
return
|
||||
if ev is InputEventMouseMotion:
|
||||
_hover_at(ev.position)
|
||||
_hover_comp._hover_at(ev.position)
|
||||
elif ev is InputEventMouseButton and ev.pressed:
|
||||
var c := _cell_at(ev.position)
|
||||
if c.x >= 0:
|
||||
_on_cell_input(c, ev)
|
||||
|
||||
|
||||
func _hover_at(pos: Vector2) -> void:
|
||||
var c := _cell_at(pos)
|
||||
if c.x < 0:
|
||||
if _hover.x >= 0:
|
||||
_on_cell_unhover(_hover)
|
||||
elif c != _hover:
|
||||
if _hover.x >= 0:
|
||||
_on_cell_unhover(_hover)
|
||||
_on_cell_hover(c)
|
||||
|
||||
|
||||
func _on_shell_mouse_exited() -> void:
|
||||
if _hover.x >= 0:
|
||||
_on_cell_unhover(_hover)
|
||||
|
||||
|
||||
func _on_cell_input(c: Vector2i, ev: InputEvent) -> void:
|
||||
if _minesweeper == null or not (ev is InputEventMouseButton and ev.pressed):
|
||||
return
|
||||
@@ -468,7 +469,7 @@ func _on_cell_input(c: Vector2i, ev: InputEvent) -> void:
|
||||
action_requested.emit(c, "move")
|
||||
else:
|
||||
action_requested.emit(c, "dig")
|
||||
_detect_chord(c)
|
||||
_hover_comp._detect_chord(c)
|
||||
elif ev.button_index == MOUSE_BUTTON_RIGHT:
|
||||
if not _minesweeper.has_seal(c):
|
||||
action_requested.emit(c, "seal_place")
|
||||
@@ -476,89 +477,6 @@ func _on_cell_input(c: Vector2i, ev: InputEvent) -> void:
|
||||
# 左键相邻封印格直接挖(规则引擎放行)。
|
||||
|
||||
|
||||
# chord 肌肉记忆首遇锚:同格双左击快开尝试(350ms 窗)——每 run 一次提示由组合根去重
|
||||
func _detect_chord(c: Vector2i) -> void:
|
||||
var now := Time.get_ticks_msec()
|
||||
if _last_click.cell == c and now - int(_last_click.ms) <= CHORD_WINDOW_MS:
|
||||
chord_attempted.emit(c)
|
||||
_last_click.ms = -100000
|
||||
return
|
||||
_last_click = {"cell": c, "ms": now}
|
||||
|
||||
|
||||
# ---- 悬停预承诺(pattern 4):三语义光标 + 目标格边框 + BFS 路径预览 ----
|
||||
|
||||
func _on_cell_hover(c: Vector2i) -> void:
|
||||
_hover = c
|
||||
_apply_hover_visuals(c)
|
||||
|
||||
|
||||
func _on_cell_unhover(c: Vector2i) -> void:
|
||||
if _hover == c:
|
||||
_hover = Vector2i(-9, -9)
|
||||
_clear_hover_visuals(c)
|
||||
|
||||
|
||||
func _refresh_hover_after_paint() -> void:
|
||||
# 状态随 refresh 变化后光标/边框语义需重算(悬停格四格态可能已变)
|
||||
if _hover.x >= 0 and _cells.has(_hover):
|
||||
_apply_hover_visuals(_hover)
|
||||
|
||||
|
||||
func _apply_hover_visuals(c: Vector2i) -> void:
|
||||
if _minesweeper == null or not _cells.has(c):
|
||||
return
|
||||
_clear_hover_visuals(c)
|
||||
var u: Node2D = _cells[c]
|
||||
var revealed: bool = _minesweeper.is_revealed(c)
|
||||
var seal: bool = _minesweeper.has_seal(c)
|
||||
var adjacent: bool = _diggable.has(c)
|
||||
if revealed:
|
||||
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.st.hl_visible = true
|
||||
u.st.hl_color = COL_HL_TARGET # 挖掘目标格边框(相邻封印格同此——左键直接挖)
|
||||
_set_cursor_shape(c, CURSOR_DIG)
|
||||
elif seal:
|
||||
u.st.hl_visible = true
|
||||
u.st.hl_color = COL_SEAL # 已承诺封印标记(封印黑;无交互可供)
|
||||
_set_cursor_shape(c, Input.CURSOR_FORBIDDEN)
|
||||
else:
|
||||
u.st.hl_visible = true
|
||||
u.st.hl_color = UiKit.RED_FORBID # 不可及域灰禁框(禁止光标+首遇 toast 双通道)
|
||||
_set_cursor_shape(c, Input.CURSOR_FORBIDDEN)
|
||||
hover_forbidden.emit(c)
|
||||
|
||||
|
||||
func _clear_hover_visuals(c: Vector2i) -> void:
|
||||
if not _cells.has(c):
|
||||
return
|
||||
_cells[c].st.hl_visible = false
|
||||
_hide_path_preview()
|
||||
|
||||
|
||||
func _set_cursor_shape(c: Vector2i, shape: int) -> void:
|
||||
mouse_default_cursor_shape = shape # 壳级光标(悬停格语义决定——单格悬停恒一)
|
||||
|
||||
|
||||
func _show_path_preview(c: Vector2i) -> void:
|
||||
# 合法性指示:悬停已开格 → BFS 路径蓝细线点迹(compute_move_path 只读面)
|
||||
if _minesweeper == null:
|
||||
return
|
||||
var path: Array[Variant] = _minesweeper.compute_move_path(c)
|
||||
for pc in path:
|
||||
if _cells.has(pc):
|
||||
_cells[pc].st.dot_visible = true
|
||||
|
||||
|
||||
func _hide_path_preview() -> void:
|
||||
for c in _cells:
|
||||
_cells[c].st.dot_visible = false
|
||||
|
||||
|
||||
# ---- 奖励徽章(已开格身份 icon——封印之下、纹带之上、不进数字区) ----
|
||||
|
||||
func _update_badge(c: Vector2i) -> String:
|
||||
@@ -613,19 +531,19 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
|
||||
var is_hero: bool = c == hero
|
||||
# 封印槽统一维护:封印格=封印印记纹理;其余格清 null(封印格挖开后残留在 tex_seal
|
||||
# 会让 _draw 无条件把封印印画在已开格上——tex_seal 绘制无 revealed 条件)。缺盘走字形回退。
|
||||
u.st.tex_seal = _palette.seal_tex if (seal and not revealed and _palette.seal_tex != null) else null
|
||||
u.face.tex_seal = _palette.seal_tex if (seal and not revealed and _palette.seal_tex != null) else null
|
||||
if removed:
|
||||
revealed = false # 移除格固定未开呈现基座(退色暗幕层承载;危险格本无数字,周边数字冻结)
|
||||
seal = false
|
||||
u.st.tex_seal = null
|
||||
u.face.tex_seal = null
|
||||
# 绘制层五段(字形与色牌→基底瓦片→立面投影→秘宝/变异叠加→迷雾)——段间交换经单元状态面
|
||||
#(u.st/u.glyph/u.glyph_color),唯一例外=基底色 bg 经 ink 段返回值交 base 段(无单元场域承载)
|
||||
#(u.base/u.face 域件与 u.glyph/u.glyph_color),唯一例外=基底色 bg 经 ink 段返回值交 base 段(无单元场域承载)
|
||||
var bg: Color = _paint_cell_ink(c, u, revealed, seal)
|
||||
_paint_cell_base(c, u, revealed, is_hero, bg)
|
||||
_paint_cell_facade(c, u, revealed)
|
||||
_paint_cell_overlays(c, u, revealed)
|
||||
_paint_cell_fog(c, u, revealed, hero)
|
||||
u.st.removed_dim = removed
|
||||
u.face.removed_dim = removed
|
||||
u.queue_redraw()
|
||||
|
||||
|
||||
@@ -696,17 +614,17 @@ func _paint_cell_base(c: Vector2i, u: Node2D, revealed: bool, is_hero: bool, bg:
|
||||
# 贴图缺位回退字形「人」(项目纹理槽惯例——贴图在位=格内字形退役,单位本体由独立精灵承载)
|
||||
u.glyph = "人"
|
||||
u.glyph_color = COL_HERO
|
||||
u.st.tex_base = tile
|
||||
u.st.bg_sb = sb
|
||||
u.base.tex_base = tile
|
||||
u.base.bg_sb = sb
|
||||
|
||||
|
||||
# 立面投影段:未开墙格南缘立面/轮廓描边码;已开格北缘投影暗带/E 格高墙面——段内重置与赋值同序
|
||||
func _paint_cell_facade(c: Vector2i, u: Node2D, revealed: bool) -> void:
|
||||
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
|
||||
u.base.outline_sides.assign([]) # typed Array[int](assign 保持引用语义)
|
||||
u.base.facade_south = false
|
||||
u.base.proj_band = 0.0
|
||||
u.base.tex_facade = null
|
||||
u.base.facade_strip = false
|
||||
# 封印槽不在此重置:尾部重置会与此上方条件赋值构成双写踩踏(把封印纹理恒清 null=素材在位时封印不可见);
|
||||
# 上方 else 分支已是全路径清残留,尾部重置系冗余且有害
|
||||
var south_n: Vector2i = c + Vector2i(0, 1)
|
||||
@@ -716,26 +634,26 @@ func _paint_cell_facade(c: Vector2i, u: Node2D, revealed: bool) -> void:
|
||||
if not revealed:
|
||||
# 高墙感 D 投影档:南邻地面格 → 墙格南缘立「口沿高光+双段暗带」立面
|
||||
#(邻地面才画——墙-墙内界保墙群内聚);板外=墙不画
|
||||
u.st.facade_south = south_in and _minesweeper.is_revealed(south_n)
|
||||
u.base.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.st.facade_south:
|
||||
if code == 1 and u.base.facade_south:
|
||||
continue # 南缘由立面件承载(高光+双段暗带)——2px 描边让位防三重叠黑
|
||||
if code == 0 and _palette.tiles_ok:
|
||||
continue # v3.5 冻结① (b)-(c) 交界无边框:墙面直贴接墙顶(E 格墙面件在邻格承载)——北向描边不画(平色回退态保留描边)
|
||||
u.st.outline_sides.append(code)
|
||||
u.base.outline_sides.append(code)
|
||||
else:
|
||||
# 地面投影:北邻墙格在本已开格北缘投渐隐暗带(ITB 同款高度线索——带宽按格边比例常量)
|
||||
if north_in and not _minesweeper.is_revealed(north_n):
|
||||
u.st.proj_band = u.st.edge * WALL_PROJECTION_FRAC
|
||||
u.base.proj_band = u.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.st.facade_strip = true
|
||||
u.st.tex_facade = _palette.tiles.get("wall_center")
|
||||
u.base.facade_strip = true
|
||||
u.base.tex_facade = _palette.tiles.get("wall_center")
|
||||
|
||||
|
||||
# 秘宝/变异叠加段:真视红框/预读徽章/变异角标/奖励徽章/类型微光组号——glyph/digit_col 局部镜像
|
||||
@@ -746,21 +664,21 @@ func _paint_cell_overlays(c: Vector2i, u: Node2D, revealed: bool) -> void:
|
||||
var glyph: String = u.glyph
|
||||
var digit_col: Color = u.glyph_color
|
||||
if not revealed:
|
||||
u.st.treasure_mark = "mine_reveal" if _treasure_reveal.has(c) else ""
|
||||
u.face.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.st.badge_glyph = glyph
|
||||
u.face.badge_glyph = glyph
|
||||
# ②' 变异角标:未开格带伤害型变异 → 危险红角标(已开=威胁已兑现标记退位);
|
||||
# 回调缺位 = 无标记(程序化角标,tex_mutation 纹带槽暂留)
|
||||
if not revealed and _vmark_cb.is_valid():
|
||||
u.st.mutation_mark = String(_vmark_cb.call(c))
|
||||
u.face.mutation_mark = String(_vmark_cb.call(c))
|
||||
else:
|
||||
u.st.mutation_mark = ""
|
||||
u.st.badge_glyph = _update_badge(c)
|
||||
u.st.glow_group = int(_glow.get(c, UiKit.GLOW_NONE))
|
||||
u.face.mutation_mark = ""
|
||||
u.face.badge_glyph = _update_badge(c)
|
||||
u.glow_group = int(_glow.get(c, UiKit.GLOW_NONE))
|
||||
|
||||
|
||||
# 迷雾段:灯光区外未开格全黑/已开格记忆态灰显——纯呈现零逻辑(🔴 契约 1)
|
||||
@@ -803,7 +721,7 @@ func _sync_glow_to_units() -> void:
|
||||
for c in touched:
|
||||
var u: Node2D = _cells.get(c)
|
||||
if u != null:
|
||||
u.st.glow_group = int(_glow.get(c, UiKit.GLOW_NONE))
|
||||
u.glow_group = int(_glow.get(c, UiKit.GLOW_NONE))
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
@@ -815,14 +733,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.st.contact_col = COL_CONTACT.lerp(UiKit.CONTACT_GLOW_DEEP, 0.55 - 0.35 * k)
|
||||
_contact_unit.base.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.st.contact_col = Color(0, 0, 0, 0)
|
||||
_contact_unit.base.contact_col = Color(0, 0, 0, 0)
|
||||
_contact_unit.queue_redraw()
|
||||
_contact_unit = null
|
||||
if _board == null or _minesweeper == null or not _dig_glow:
|
||||
@@ -839,19 +757,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.st.contact_bands = bands
|
||||
hu.st.contact_corners = corners
|
||||
hu.base.contact_bands = bands
|
||||
hu.base.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.st.contact_col = COL_CONTACT.lerp(UiKit.CONTACT_GLOW_DEEP, 0.55 - 0.35 * k)
|
||||
hu.base.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.st.contact_col = Color(0, 0, 0, 0)
|
||||
_contact_unit.base.contact_col = Color(0, 0, 0, 0)
|
||||
_contact_unit.queue_redraw()
|
||||
|
||||
|
||||
@@ -898,9 +816,9 @@ func diggable_probe() -> Dictionary:
|
||||
return _diggable.duplicate()
|
||||
|
||||
|
||||
# 悬停格只读出口(case① 悬停到达断言消费面)
|
||||
# 悬停格只读出口(case① 悬停到达断言消费面——读件投影,返回语义零变化)
|
||||
func hover_probe() -> Vector2i:
|
||||
return _hover
|
||||
return _hover_comp._hover
|
||||
|
||||
|
||||
# 迷雾参数只读快照(case⑮ 参数漂移断言消费面)
|
||||
@@ -926,5 +844,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.st.edge, u.st.edge))
|
||||
var local := Rect2(u.global_position, Vector2(u.edge, u.edge))
|
||||
return Rect2(host.position + local.position * k, local.size * k)
|
||||
@@ -0,0 +1,204 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## 格面基座层绘制域件(CellUnit 拆件——单一变化原因=基座层视觉规格):
|
||||
## 底面/描边/高墙立面/地面投影/接触带微光/顶面倒角的全部绘制入参与绘制命令。
|
||||
## 无 class_name(类名缓存面纪律)——经宿主 board_cell_unit preload 引用;宿主 _draw 按层序调
|
||||
## draw_base(canvas, rect) 把绘制命令落到宿主画布;setter 经 _owner 回引触发宿主重绘语义逐字。
|
||||
extends RefCounted
|
||||
|
||||
# ── 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_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) # 投影远段(渐隐收边)
|
||||
# ── E 格高墙面(圣经 v3.5 冻结①——地面格南邻墙→南部 30% 承接墙面:墙顶纹理原亮度
|
||||
# 直贴零暗档零阴影;墙脚阴线只画地面/墙面交界;(b)-(c) 交界
|
||||
# 无边框由 BoardView 侧跳过墙格北向描边承载)──
|
||||
const E_FACADE_FRAC := 0.30 # 墙面承接高(格边比 30%)
|
||||
|
||||
var _owner: Node2D = null
|
||||
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 bevel := 0.0: # 顶面倒角强度(0=关;档位 0/0.35/0.6/1.0)
|
||||
set(v):
|
||||
bevel = v
|
||||
_owner.queue_redraw()
|
||||
var tex_base: Texture2D = null # ① 砖面底(宿主迷雾记忆态公开单向读)
|
||||
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()
|
||||
|
||||
|
||||
func _init(owner: Node2D) -> void:
|
||||
_owner = owner
|
||||
|
||||
|
||||
## ① 砖面底层域(占位 = StyleBoxFlat:格底色 + 暗化描边 + 3px 圆角;瓦片态描边覆盖层;
|
||||
## 呼吸微光/主角蓝框由 BoardView 改 bg_sb 后触发重绘。素材缺位回退走 elif 程序化原样,零观感变化)
|
||||
func draw_base(canvas: CanvasItem, rect: Rect2) -> void:
|
||||
if tex_base != null:
|
||||
canvas.draw_texture_rect(tex_base, rect, false)
|
||||
# 轮廓加强(轮廓线不明显):墙格地面朝向边补 2px 深墨线(§8.5 结构归程序——
|
||||
# 素材 1px 墨线在 45/36px 非整数缩放下视觉衰减,程序线保底;墙-墙内界不画,保墙群内聚;
|
||||
# 坐标推导:各边内贴 2px 矩形,相邻边矩形全长相交=周界闭合)
|
||||
if not outline_sides.is_empty():
|
||||
_draw_outline_sides(canvas, rect)
|
||||
_draw_south_facade(canvas, rect)
|
||||
_draw_north_projection(canvas, rect)
|
||||
_draw_e_facade(canvas, rect)
|
||||
# 瓦片态描边覆盖层:tex_base 在位时 BoardView 传入的 bg_sb
|
||||
# 已降级为「透明底 + 描边」载体——呼吸微光/主角蓝框画在瓦片之上(art-bible §1.2 叠层序
|
||||
# 不变:底 < 纹带 < 数字 < 封印 < 焦点高亮);素材缺位回退走 elif 程序化原样,零观感变化
|
||||
if bg_sb != null:
|
||||
canvas.draw_style_box(bg_sb, rect)
|
||||
_draw_contact_or_fallback(canvas, 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 提亮)
|
||||
func _draw_contact_or_fallback(canvas: CanvasItem, rect: Rect2) -> void:
|
||||
if contact_col.a > 0.0:
|
||||
_draw_north_projection(canvas, rect) # 平色回退+微光并发的英雄格补画(瓦片路径已画——同像素重复无观感差)
|
||||
_draw_e_facade(canvas, rect) # E 格墙面补画(同上——英雄格兼 E 格时瓦片路径已画,重复无害)
|
||||
_draw_contact_bands(canvas, rect)
|
||||
elif bg_sb != null:
|
||||
canvas.draw_style_box(bg_sb, rect)
|
||||
_draw_south_facade(canvas, rect) # 平色回退路径——D 投影档两件同样服役(结构归程序)
|
||||
_draw_north_projection(canvas, rect)
|
||||
_draw_bevel(canvas, rect)
|
||||
|
||||
|
||||
## 墙格轮廓域:地面朝向边补 2px 深墨线(各边内贴 2px 矩形,相邻边矩形全长相交=周界闭合)
|
||||
func _draw_outline_sides(canvas: CanvasItem, rect: Rect2) -> void:
|
||||
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, rect.size.x, ow)
|
||||
1: or_ = Rect2(0, rect.size.x - ow, rect.size.x, ow)
|
||||
2: or_ = Rect2(0, 0, ow, rect.size.x)
|
||||
3: or_ = Rect2(rect.size.x - ow, 0, ow, rect.size.x)
|
||||
canvas.draw_rect(or_, oc, true)
|
||||
|
||||
|
||||
## 接触带微光域:正交整条边 + 斜向共享角点与两侧半条边
|
||||
func _draw_contact_bands(canvas: CanvasItem, rect: Rect2) -> void:
|
||||
var band := 4.0
|
||||
var half := rect.size.x * 0.5
|
||||
for b in contact_bands:
|
||||
var r := Rect2()
|
||||
match int(b):
|
||||
0: r = Rect2(0, 0, rect.size.x, band)
|
||||
1: r = Rect2(0, rect.size.x - band, rect.size.x, band)
|
||||
2: r = Rect2(0, 0, band, rect.size.x)
|
||||
3: r = Rect2(rect.size.x - band, 0, band, rect.size.x)
|
||||
canvas.draw_rect(r, contact_col, true)
|
||||
for cc in contact_corners:
|
||||
_draw_contact_corner(canvas, rect, 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(canvas: CanvasItem, rect: Rect2, cc: int, band: float, half: float) -> void:
|
||||
var pt := Vector2.ZERO
|
||||
match cc:
|
||||
0: pt = Vector2(0, 0)
|
||||
1: pt = Vector2(rect.size.x, 0)
|
||||
2: pt = Vector2(0, rect.size.x)
|
||||
3: pt = Vector2(rect.size.x, rect.size.x)
|
||||
canvas.draw_circle(pt, 6.0, contact_col)
|
||||
canvas.draw_circle(pt, 2.5, Color(1, 1, 1, minf(1.0, contact_col.a * 0.95)))
|
||||
if cc == 0 or cc == 1:
|
||||
# N 侧横带:NW 自 x=0、NE 自 x=edge-half
|
||||
canvas.draw_rect(Rect2(pt.x if cc == 0 else pt.x - half, 0.0, half, band), contact_col, true)
|
||||
if cc == 2 or cc == 3:
|
||||
# S 侧横带:SW 自 x=0、SE 自 x=edge-half
|
||||
canvas.draw_rect(Rect2(pt.x if cc == 2 else pt.x - half, rect.size.x - band, half, band), contact_col, true)
|
||||
if cc == 0 or cc == 2:
|
||||
# W 侧竖带:NW 自 y=0、SW 自 y=edge-half
|
||||
canvas.draw_rect(Rect2(0.0, pt.y if cc == 0 else pt.y - half, band, half), contact_col, true)
|
||||
if cc == 1 or cc == 3:
|
||||
# E 侧竖带:NE 自 y=0、SE 自 y=edge-half
|
||||
canvas.draw_rect(Rect2(rect.size.x - band, pt.y if cc == 1 else pt.y - half, band, half), contact_col, true)
|
||||
|
||||
|
||||
## 顶面倒角域(光源左上——上/左内沿受光 2px、下/右内沿背光 2px;内缩 2px 让
|
||||
## 呼吸微光/主角蓝边框保持最外沿可见;强度随档位)
|
||||
func _draw_bevel(canvas: CanvasItem, rect: Rect2) -> void:
|
||||
if bevel <= 0.0:
|
||||
return
|
||||
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
|
||||
canvas.draw_rect(Rect2(t, t, rect.size.x - t * 2.0, t), lit, true)
|
||||
canvas.draw_rect(Rect2(t, t, t, rect.size.x - t * 2.0), lit, true)
|
||||
canvas.draw_rect(Rect2(t, rect.size.x - t * 2.0, rect.size.x - t * 2.0, t), dark, true)
|
||||
canvas.draw_rect(Rect2(rect.size.x - t * 2.0, t, t, rect.size.x - t * 2.0), dark, true)
|
||||
|
||||
|
||||
# 南立面(D 档件一——墙格南缘内侧,自底边向上:双段暗带在下、口沿高光线贴其上沿):
|
||||
# 坐标推导(自格底边起算):远段=[edge−far, edge],近段=[edge−far−near, edge−far],
|
||||
# 高光线=[edge−far−near−hl, edge−far−near]——与研究件「墙顶面→高光→暗段→更暗段→格界」下行序一致
|
||||
func _draw_south_facade(canvas: CanvasItem, rect: Rect2) -> void:
|
||||
if not facade_south:
|
||||
return
|
||||
var far_h := rect.size.x * FACADE_FAR_FRAC
|
||||
var near_h := rect.size.x * FACADE_NEAR_FRAC
|
||||
var y_far := rect.size.x - far_h
|
||||
var y_near := y_far - near_h
|
||||
var y_hl := y_near - FACADE_HIGHLIGHT_PX
|
||||
canvas.draw_rect(Rect2(0.0, y_hl, rect.size.x, FACADE_HIGHLIGHT_PX), UiKit.STAND_STONE_RIM, true) # 口沿高光(UiKit.STAND_STONE_RIM 并档——同值单点)
|
||||
canvas.draw_rect(Rect2(0.0, y_near, rect.size.x, near_h), FACADE_NEAR_COL, true)
|
||||
canvas.draw_rect(Rect2(0.0, y_far, rect.size.x, far_h), FACADE_FAR_COL, true)
|
||||
|
||||
|
||||
# 地面投影(D 档件二——北邻墙格在本已开格北缘投渐隐暗带,自北缘向南三段衰减):
|
||||
# 坐标推导:三等分 proj_band,α 0.28/0.16/0.06(研究件实测贴墙 α≈0.28、带末渐融入地面)
|
||||
func _draw_north_projection(canvas: CanvasItem, rect: Rect2) -> void:
|
||||
if proj_band <= 0.0:
|
||||
return
|
||||
var seg := proj_band / 3.0
|
||||
canvas.draw_rect(Rect2(0.0, 0.0, rect.size.x, seg), PROJECTION_NEAR_COL, true)
|
||||
canvas.draw_rect(Rect2(0.0, seg, rect.size.x, seg), PROJECTION_MID_COL, true)
|
||||
canvas.draw_rect(Rect2(0.0, seg * 2.0, rect.size.x, seg), PROJECTION_EDGE_COL, true)
|
||||
|
||||
|
||||
# E 格高墙面(圣经 v3.5 冻结①——方向稿 draw_facade 移植):本格已开且南邻为墙 →
|
||||
# 南部 30% 承接墙面。材质 = 墙顶 center 瓦片顶部 30% 区带**原亮度**直贴(零暗档零阴影);
|
||||
# 墙脚阴线只画地面/墙面交界(strip 顶缘 1px);墙面底缘(格界处)不画任何线——直接接南侧墙顶
|
||||
# (白边导致两截)。仅瓦片态服役(tex_facade 缺位 = 平色回退原语义不动)。
|
||||
func _draw_e_facade(canvas: CanvasItem, rect: Rect2) -> void:
|
||||
if not facade_strip or tex_facade == null:
|
||||
return
|
||||
var strip_h := rect.size.x * E_FACADE_FRAC
|
||||
var y0 := rect.size.x - strip_h
|
||||
var ts := tex_facade.get_size()
|
||||
canvas.draw_texture_rect_region(tex_facade, Rect2(0.0, y0, rect.size.x, strip_h),
|
||||
Rect2(0.0, 0.0, ts.x, ts.y * E_FACADE_FRAC))
|
||||
canvas.draw_rect(Rect2(0.0, y0, rect.size.x, 1.0), UiKit.E_FACADE_FOOT_COL, true)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c2msg115y3oir
|
||||
@@ -0,0 +1,145 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## 格面标识与顶层叠加绘制域件(CellUnit 拆件——单一变化原因=标识与叠加层视觉规格):
|
||||
## 变异标记/数字色牌与字形/奖励徽章/封印/路径点迹/焦点高亮/中立移除退色的全部绘制入参与绘制命令。
|
||||
## 无 class_name(类名缓存面纪律)——经宿主 board_cell_unit preload 引用;宿主 _draw 按层序调
|
||||
## draw_marks/draw_glyph_layer/draw_overlay_under/draw_removed_dim(雾层由宿主夹在 overlay_under 与 removed_dim 之间);
|
||||
## setter 经 _owner 回引触发宿主重绘语义逐字(badge_glyph 的 glow mask 回引 _owner._glow_layer 原样)。
|
||||
extends RefCounted
|
||||
|
||||
var _owner: Node2D = null
|
||||
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 注入)
|
||||
# ── 素材槽位(现全 null = 程序化占位服役;art-bible §8.2 格面 32×32 暂定档)──
|
||||
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()
|
||||
# 变异角标(程序化,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()
|
||||
var removed_dim := false: # 中立移除退色层(BoardView paint 按排雷玩法 is_removed 写入)
|
||||
set(v):
|
||||
removed_dim = v
|
||||
_owner.queue_redraw()
|
||||
|
||||
|
||||
func _init(owner: Node2D) -> void:
|
||||
_owner = owner
|
||||
|
||||
|
||||
## 变异标记域:真视格=危险红内缩框/未开格伤害型变异=左下角危险红三角
|
||||
func draw_marks(canvas: CanvasItem, rect: Rect2) -> void:
|
||||
if treasure_mark == "mine_reveal":
|
||||
var inset := rect.size.x * 0.14
|
||||
canvas.draw_rect(Rect2(inset, inset, rect.size.x - inset * 2.0, rect.size.x - inset * 2.0),
|
||||
Color(UiKit.RED_MINE, 0.9), false, 2.0)
|
||||
if mutation_mark == "flat_damage":
|
||||
var mw := rect.size.x * 0.28
|
||||
var mpts := PackedVector2Array([Vector2(0, rect.size.x - mw), Vector2(0, rect.size.x), Vector2(mw, rect.size.x)])
|
||||
canvas.draw_colored_polygon(mpts, Color(UiKit.RED_MINE, 0.85))
|
||||
|
||||
|
||||
## ②③④ 中层域:变异纹带与标记/数字色牌与字形/徽章/封印
|
||||
##(glyph/glyph_color/plate 三者住宿主冻结面——探针直读成员,作参数传入)
|
||||
func draw_glyph_layer(canvas: CanvasItem, rect: Rect2, glyph: String, glyph_color: Color, plate: bool) -> void:
|
||||
# ② 变异纹带(无程序化纹带——槽位先行,贴图落位即用)
|
||||
if tex_mutation != null:
|
||||
canvas.draw_texture_rect(tex_mutation, rect, false)
|
||||
# ③ 数字/徽章(数字 ≥ 格高 50% 墨族对格底 ≥4.5:1;徽章右下角金,遮蔽态 = "?")
|
||||
if glyph != "":
|
||||
if plate:
|
||||
_draw_digit_plate(canvas, rect)
|
||||
if tex_glyph != null:
|
||||
canvas.draw_texture_rect(tex_glyph, rect, false)
|
||||
else:
|
||||
draw_centered_text(canvas, rect, glyph, glyph_color, maxi(18, int(ceil(rect.size.x * 0.5)))) # 字号下限 18(§7.4 冻结公式)
|
||||
if badge_glyph != "":
|
||||
if tex_badge != null:
|
||||
canvas.draw_texture_rect(tex_badge, rect, false)
|
||||
else:
|
||||
_draw_badge(canvas, rect, badge_glyph)
|
||||
# ④ 封印(现行视觉封印文字在 ③ 文字位承载——独立槽位留给封印图)
|
||||
if tex_seal != null:
|
||||
canvas.draw_texture_rect(tex_seal, rect, false)
|
||||
|
||||
|
||||
## 数字色牌后备域(圣经 v3.5 冻结④:数字亮墨直画已开格底须垫牌;
|
||||
## 牌=后备件——雷/「?」维持牌载保读性(牌上深墨族 12:1);
|
||||
## 牌尺寸 0.60/0.62 格边居中;封印=未开格不垫牌)
|
||||
func _draw_digit_plate(canvas: CanvasItem, rect: Rect2) -> void:
|
||||
var pw := rect.size.x * 0.60
|
||||
var ph := rect.size.x * 0.62
|
||||
var pr := Rect2((rect.size.x - pw) * 0.5, (rect.size.x - ph) * 0.5, pw, ph)
|
||||
canvas.draw_rect(pr, UiKit.DIGIT_PLATE, true)
|
||||
canvas.draw_rect(pr, UiKit.DIGIT_PLATE_EDGE, false, 1.0)
|
||||
|
||||
|
||||
## ⑤ 路径点迹 + 焦点高亮(悬停态覆盖层,不参与命中)——宿主时序器在其后插雾层
|
||||
func draw_overlay_under(canvas: CanvasItem, rect: Rect2) -> void:
|
||||
if dot_visible:
|
||||
var ds := 6.0
|
||||
canvas.draw_rect(Rect2(Vector2((rect.size.x - ds) * 0.5, (rect.size.x - ds) * 0.5), Vector2(ds, ds)), UiKit.HERO_BLUE, true)
|
||||
if hl_visible:
|
||||
# 悬停高亮减淡(金框太明显——绘制时 55% alpha,hl_color 属性与探针断言面不动)
|
||||
canvas.draw_rect(Rect2(1, 1, rect.size.x - 2.0, rect.size.x - 2.0), Color(hl_color, 0.55), false, 2.0) # 2px 内描边
|
||||
|
||||
|
||||
## ⑦ 中立移除退色层(退色降档:暗幕压顶全层覆盖,暗淡记忆态同族呈现语义;
|
||||
## 顶层于迷雾之上——移除格不再参与迷雾语义。本实现移除集=未揭示危险格(无数字无封印印))
|
||||
func draw_removed_dim(canvas: CanvasItem, rect: Rect2) -> void:
|
||||
if removed_dim:
|
||||
canvas.draw_rect(rect, Color(0.12, 0.10, 0.09, 0.62), true)
|
||||
|
||||
|
||||
## 文字位绘制单点(宿主迷雾记忆态复用——同公式同字号;水平居中 + 垂直居中,基线 = (格高 − 字高)/2 + 上伸部)
|
||||
func draw_centered_text(canvas: CanvasItem, rect: Rect2, text: String, col: Color, fs: int) -> void:
|
||||
var f := font if font != null else ThemeDB.fallback_font
|
||||
if f == null:
|
||||
return
|
||||
var asc := f.get_ascent(fs)
|
||||
var desc := f.get_descent(fs)
|
||||
canvas.draw_string(f, Vector2(0.0, (rect.size.x - (asc + desc)) * 0.5 + asc), text,
|
||||
HORIZONTAL_ALIGNMENT_CENTER, rect.size.x, fs, col)
|
||||
|
||||
|
||||
# 徽章绘制:右下角金(文字贴角内缩 2–3px)
|
||||
func _draw_badge(canvas: CanvasItem, rect: Rect2, g: String) -> void:
|
||||
var f := font if font != null else ThemeDB.fallback_font
|
||||
if f == null:
|
||||
return
|
||||
var fs := maxi(10, int(rect.size.x * 0.28))
|
||||
var sz := f.get_string_size(g, HORIZONTAL_ALIGNMENT_LEFT, -1, fs)
|
||||
canvas.draw_string(f, Vector2(rect.size.x - sz.x - 2.0, rect.size.x - 3.0), g, HORIZONTAL_ALIGNMENT_LEFT, -1, fs, UiKit.GOLD)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c60e3su2qd1yq
|
||||
@@ -2,9 +2,9 @@
|
||||
## 站桩本体:B 区主角/敌人无框立绘+底座+血条+顺位/意图徽标的世界精灵(Node2D)。
|
||||
## 站桩形态为定案——禁回卡片式;立绘素材缺位回退字形占位;击杀呈现禁瞬消(灰化塌陷)。
|
||||
## 无 class_name——经 preload 引用;壳层坐标换算单点在 BattleStageView.world_to_shell()。
|
||||
## 内部状态件(u4 子域拆件——契约 9 职责语句「站桩演出状态」):单一变化原因=站桩呈现与演出,
|
||||
## 成员共同表达站桩的行进态/演出计数/资产挂点。外部直访冻结成员(box/font/slot_row/has_world_hp_bar)
|
||||
## 与源码锚承载成员(_intent_charge——theme 源锚句所在行逐字保留)住宿主。
|
||||
## 内部状态两件(u4 拆件——st=身份命值态 standee_vitals、mo=资产演出态 standee_motion,_init 自建);
|
||||
## 外部直访冻结成员(box/font/slot_row/has_world_hp_bar)与源码锚承载成员(_intent_charge——theme
|
||||
## 源锚句所在行逐字保留)与三个演出探针计数器住宿主。
|
||||
extends Node2D
|
||||
|
||||
const CHARGE_SKILL_IDS := ["heavy"]
|
||||
@@ -16,65 +16,28 @@ const ARMOR_ICONS := {"light": "◇", "ward": "◆", "heavy": "▣"}
|
||||
const DEATH_SHARD_N := 5 # 碎块数(§8.4 冻结值)
|
||||
const HIT_SPARK_N := 6 # 命中火花数(打击瞬间可读性一拍)
|
||||
|
||||
const _VITALS := preload("res://presentation/views/standee_vitals.gd")
|
||||
const _MOTION := preload("res://presentation/views/standee_motion.gd")
|
||||
|
||||
## 站桩演出状态(全部内部态单点——含 setter 触发宿主重绘语义,_owner 回引保持逐字)
|
||||
class _StandeeState extends RefCounted:
|
||||
var _owner: Node2D = null
|
||||
var _kind := "enemy"
|
||||
var _portrait_sprite: Sprite2D = null # 资产挂点(null = 未落位,字形在位)
|
||||
var _portrait_tex: Texture2D = null # 落位纹理引用(set_portrait_texture 装载;字形回退判据)
|
||||
# 主角三态切换:state 名 → 纹理(battle_stage_view 组合点注册);缺态=保持现纹理
|
||||
# (缺位回退链延伸——三态素材缺任一不炸不空)。敌站桩不注册不消费。
|
||||
var _hero_tex := {}
|
||||
var _hero_state: int = 0
|
||||
var _state_revert_tw: Tween
|
||||
var _order := 1
|
||||
var _intent_glyph := ""
|
||||
var _intent_col := Color.WHITE
|
||||
var _intent_visible := false
|
||||
var _intent_scale := 1.0: # 意图弹入动画参数
|
||||
set(v):
|
||||
_intent_scale = v
|
||||
_owner.queue_redraw()
|
||||
var _glyph_rot := 0.0: # 立绘前倾角(贴图态无此动画)
|
||||
set(v):
|
||||
_glyph_rot = v
|
||||
_owner.queue_redraw()
|
||||
var _display_name := ""
|
||||
var _enemy_id := ""
|
||||
var _armor := "light"
|
||||
var _dead := false
|
||||
var _hp_max := 1.0
|
||||
var _hp_cur := 0 # 血量数字显示面(HUD框架 验收「血量数字可读」)
|
||||
var hp_value := 1.0: # 血条值 0..1(直角递减——tween_property 目标,逐行刷新重启不累积)
|
||||
set(v):
|
||||
hp_value = clampf(v, 0.0, 1.0)
|
||||
_owner.queue_redraw()
|
||||
var _hp_tween: Tween
|
||||
var _hp_flash_tw: Tween
|
||||
var _death_shards := 0 # 探针出口计数(碎块节点回收后仍保留读数)
|
||||
var _hit_sparks := 0 # 探针出口计数(命中火花累计)
|
||||
var _flash_count := 0 # 探针出口计数(受击闪白触发次数——主角路由断言面)
|
||||
|
||||
|
||||
var st := _StandeeState.new()
|
||||
var st: _VITALS # 身份与命值态件实例(名字沿用——外部路径串 "st.hp_value" 与约 40 处引用零改)
|
||||
var mo: _MOTION # 资产与演出态件实例(_init 自建)
|
||||
var box := Vector2(150, 176) # 站桩盒尺寸(实际高随壳层 row 布局注入;外部直访冻结)
|
||||
var font: Font = null # 世界字体槽位(null = ThemeDB.fallback_font——world.world_font 注入;外部直访冻结)
|
||||
var slot_row: StatusSlotRow # 状态效果唯一展示面 = 壳层伴随件(挂 BattleStageView 壳层,本体进世界;外部直访冻结)
|
||||
var _intent_charge := false # 蓄力档(pid ∈ CHARGE_SKILL_IDS——意图三档 ③ 分支判据;源码锚承载成员住宿主)
|
||||
# 血条存在性探针出口(世界直角血条常在;外部直访冻结)
|
||||
var has_world_hp_bar := true
|
||||
# 演出探针计数器(写入方=宿主演出函数、读取方=宿主 fx_probe 出口——仪器跟着驱动它的函数走)
|
||||
var _death_shards := 0 # 探针出口计数(碎块节点回收后仍保留读数)
|
||||
var _hit_sparks := 0 # 探针出口计数(命中火花累计)
|
||||
var _flash_count := 0 # 探针出口计数(受击闪白触发次数——主角路由断言面)
|
||||
|
||||
|
||||
func _init(kind: String = "enemy") -> void:
|
||||
st._owner = self
|
||||
st._hero_state = HeroState.IDLE
|
||||
st = _VITALS.new(self)
|
||||
mo = _MOTION.new(self)
|
||||
st._kind = kind
|
||||
|
||||
|
||||
enum HeroState { IDLE, RESET, ATTACK, HIT } # 主角三态+复位哨兵(外部 String 契约=register/set 形参零变化)
|
||||
const _HS_NAME := {HeroState.IDLE: "idle", HeroState.RESET: "reset", HeroState.ATTACK: "attack", HeroState.HIT: "hit"}
|
||||
const _HS_VALUE := {"idle": HeroState.IDLE, "reset": HeroState.RESET, "attack": HeroState.ATTACK, "hit": HeroState.HIT}
|
||||
mo._hero_state = _MOTION.HeroState.IDLE
|
||||
|
||||
|
||||
func _efont() -> Font:
|
||||
@@ -92,7 +55,7 @@ func enemy_id() -> String:
|
||||
|
||||
# 意图徽标探针出口
|
||||
func intent_probe() -> Dictionary:
|
||||
return {"visible": st._intent_visible, "text": st._intent_glyph}
|
||||
return {"visible": mo._intent_visible, "text": mo._intent_glyph}
|
||||
|
||||
|
||||
# 名字行探针出口(ui_probe 呈现口径 + 本批 V6/V8 断言面:armor_icon = 绑定纹理路径〔缺位回退字形〕、
|
||||
@@ -118,7 +81,7 @@ func set_data(d: Dictionary) -> void:
|
||||
st._enemy_id = String(d.get("enemy_id", ""))
|
||||
st._armor = String(d.get("armor_type", "light"))
|
||||
st._hp_max = float(maxi(1, int(d.get("max_hp", 1))))
|
||||
_set_hp(int(d.get("hp", 0)))
|
||||
st.apply_hp(int(d.get("hp", 0)))
|
||||
_set_order(int(d.get("order", 1)))
|
||||
set_pending(String(d.get("pending_type", "")), String(d.get("pending_skill", "")))
|
||||
set_effects(d.get("effects", []))
|
||||
@@ -129,21 +92,11 @@ func set_data(d: Dictionary) -> void:
|
||||
|
||||
func set_hero(hp: int, max_hp: int) -> void:
|
||||
st._hp_max = float(maxi(1, max_hp))
|
||||
_set_hp(hp)
|
||||
st.apply_hp(hp)
|
||||
if st._dead and hp > 0:
|
||||
_revive() # 新战斗开场 HP 回正——主角死亡残位随之复活(敌侧 set_data 同款语义)
|
||||
|
||||
|
||||
# 血条短 tween 递减(直角递减占位);逐行刷新时重启 tween 不累积
|
||||
func _set_hp(hp: int) -> void:
|
||||
st._hp_cur = clampi(hp, 0, int(st._hp_max))
|
||||
var target := float(st._hp_cur) / st._hp_max
|
||||
if st._hp_tween != null and st._hp_tween.is_valid():
|
||||
st._hp_tween.kill()
|
||||
st._hp_tween = create_tween()
|
||||
st._hp_tween.tween_property(st, "hp_value", target, UiKit.motion(0.15))
|
||||
|
||||
|
||||
# 主角/敌方共用——状态效果唯一展示面(壳层伴随件转发;空态 = 图标行隐藏不占位)
|
||||
func set_effects(effects: Array) -> void:
|
||||
if slot_row == null:
|
||||
@@ -182,12 +135,12 @@ func _set_order(i: int) -> void:
|
||||
func set_pending(ptype: String, pid: String) -> void:
|
||||
if st._dead:
|
||||
_intent_charge = false
|
||||
st._intent_visible = false
|
||||
mo._intent_visible = false
|
||||
queue_redraw()
|
||||
return
|
||||
if ptype == "" or ptype == "basic":
|
||||
_intent_charge = false
|
||||
st._intent_visible = false # 普通攻击零表示(完全不可预测——§2.4)
|
||||
mo._intent_visible = false # 普通攻击零表示(完全不可预测——§2.4)
|
||||
queue_redraw()
|
||||
return
|
||||
var glyph := "⚔" # 攻击系(damage)
|
||||
@@ -201,60 +154,15 @@ func set_pending(ptype: String, pid: String) -> void:
|
||||
_intent_charge = pid in CHARGE_SKILL_IDS
|
||||
if _intent_charge:
|
||||
col = UiKit.GOLD_INK # 重击类蓄力档(深金墨——浅垫牌上 ≥4.5:1)
|
||||
st._intent_glyph = glyph
|
||||
st._intent_col = col
|
||||
st._intent_visible = true
|
||||
_play_intent_motion()
|
||||
mo._intent_glyph = glyph
|
||||
mo._intent_col = col
|
||||
mo._intent_visible = true
|
||||
mo.play_intent_motion(_intent_charge)
|
||||
|
||||
|
||||
# 意图三档动作动画(§4.6.4 三档:①普通攻击=零表示〔set_pending 早退〕
|
||||
# ②特殊准备=徽标弹入+二次弹跳(高举语义,运动手段 §2)③重击/硬化=缓推蓄力+定格微顿+回落;
|
||||
# 三档运动签名互异,灰度/剪影层可辨)。摇定时一次性短动画,随后静帧保持。
|
||||
func _play_intent_motion() -> void:
|
||||
var tw := create_tween()
|
||||
tw.tween_property(st, "_intent_scale", 1.6, UiKit.motion(0.10))
|
||||
tw.tween_property(st, "_intent_scale", 1.0, UiKit.motion(0.16))
|
||||
tw.parallel().tween_property(st, "_glyph_rot", 4.0, UiKit.motion(0.10))
|
||||
tw.tween_property(st, "_glyph_rot", 0.0, UiKit.motion(0.14))
|
||||
if _intent_charge:
|
||||
tw.tween_property(st, "_intent_scale", 1.22, UiKit.motion(0.28)).set_trans(Tween.TRANS_SINE) # ③蓄力缓推
|
||||
tw.tween_interval(UiKit.motion(0.40)) # 定格微顿
|
||||
tw.tween_property(st, "_intent_scale", 1.0, UiKit.motion(0.18))
|
||||
else:
|
||||
tw.tween_property(st, "_intent_scale", 1.26, UiKit.motion(0.11)) # ②二次弹跳(高举)
|
||||
tw.tween_property(st, "_intent_scale", 1.0, UiKit.motion(0.13))
|
||||
|
||||
|
||||
# 资产挂点:tex 落位后字形隐退、纹理立绘顶位——零代码换装契约;tex=null 回退字形。
|
||||
# flip = 立绘朝向翻转(素材基准朝左时传 true 呈现朝右)。盒高 96px = 48px 立绘源整数 2×
|
||||
# (圣经 §8.2 禁非整数缩放)——源换代时随源重算。
|
||||
# 资产挂点转调壳(签名外形零变——box 由宿主传入件内)
|
||||
func set_portrait_texture(tex: Texture2D, flip: bool = false) -> void:
|
||||
if tex == null:
|
||||
if st._portrait_sprite != null:
|
||||
st._portrait_sprite.visible = false
|
||||
queue_redraw()
|
||||
return
|
||||
st._portrait_tex = tex
|
||||
if st._portrait_sprite == null:
|
||||
st._portrait_sprite = Sprite2D.new()
|
||||
st._portrait_sprite.centered = false
|
||||
st._portrait_sprite.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST # 像素帧放大防糊
|
||||
add_child(st._portrait_sprite)
|
||||
st._portrait_sprite.texture = tex
|
||||
st._portrait_sprite.flip_h = flip
|
||||
st._portrait_sprite.visible = true
|
||||
_place_portrait()
|
||||
|
||||
|
||||
func _place_portrait() -> void:
|
||||
# 立绘盒 = (0,44)-(box.x, box.y-36);48 源 ×2 整数档居中
|
||||
if st._portrait_sprite == null or st._portrait_tex == null:
|
||||
return
|
||||
var box_h := box.y - 44.0 - 36.0
|
||||
var w := st._portrait_tex.get_width() * 2.0
|
||||
var h := st._portrait_tex.get_height() * 2.0
|
||||
st._portrait_sprite.scale = Vector2(2.0, 2.0) # 整数 2×(C5)
|
||||
st._portrait_sprite.position = Vector2((box.x - w) * 0.5, 44.0 + (box_h - h) * 0.5)
|
||||
mo.set_portrait_texture(tex, flip, box)
|
||||
|
||||
|
||||
func mark_dead() -> void:
|
||||
@@ -268,8 +176,8 @@ func mark_dead() -> void:
|
||||
tw.tween_property(self, "rotation_degrees", 0.0, UiKit.motion(0.08))
|
||||
_spawn_death_shards() # 碎裂退场(§4.6.4 语义硬规则:灰化+碎裂禁瞬间消失;§8.4 v2 冻结 5 块 0.45s)
|
||||
_intent_charge = false
|
||||
st._intent_visible = false # 死亡残位意图退位
|
||||
_set_hp(0)
|
||||
mo._intent_visible = false # 死亡残位意图退位
|
||||
st.apply_hp(0)
|
||||
if slot_row != null:
|
||||
slot_row.visible = false # 状态行随 mark_dead 同拍隐藏
|
||||
|
||||
@@ -279,7 +187,7 @@ func mark_dead() -> void:
|
||||
|
||||
# 死亡碎块弹飞:立绘盒中心五块碎石抛物线散落(程序化色块——确定性散布,禁全局随机)
|
||||
func _spawn_death_shards() -> void:
|
||||
st._death_shards = DEATH_SHARD_N
|
||||
_death_shards = DEATH_SHARD_N
|
||||
var center := _portrait_rect().get_center()
|
||||
for i in DEATH_SHARD_N:
|
||||
_spawn_death_shard(i, center)
|
||||
@@ -305,7 +213,7 @@ func _spawn_death_shard(i: int, center: Vector2) -> void:
|
||||
|
||||
# 演出探针出口(fx_probe——死亡碎裂/命中火花/闪白计数断言面)
|
||||
func fx_probe() -> Dictionary:
|
||||
return {"death_shards": st._death_shards, "hit_sparks": st._hit_sparks, "flash_count": st._flash_count}
|
||||
return {"death_shards": _death_shards, "hit_sparks": _hit_sparks, "flash_count": _flash_count}
|
||||
|
||||
|
||||
# 命中瞬间打击特效:立绘盒中心六枚短火花放射(程序化色块,确定性方向
|
||||
@@ -319,7 +227,7 @@ func play_hit_impact() -> void:
|
||||
|
||||
## 单火花域(循环体提函数——放射弹道+渐隐+回收)
|
||||
func _spawn_hit_spark(i: int, center: Vector2) -> void:
|
||||
st._hit_sparks += 1
|
||||
_hit_sparks += 1
|
||||
var spark := ColorRect.new()
|
||||
spark.color = UiKit.GOLD_LIT if i % 2 == 0 else UiKit.WARM
|
||||
spark.size = Vector2(4, 3)
|
||||
@@ -340,7 +248,7 @@ func is_dead() -> bool:
|
||||
|
||||
# 受击闪白(结算演出占位——§2.2 受击反馈;强度时长维持现行)
|
||||
func flash_hit() -> void:
|
||||
st._flash_count += 1
|
||||
_flash_count += 1
|
||||
if st._hp_flash_tw != null and st._hp_flash_tw.is_valid():
|
||||
st._hp_flash_tw.kill()
|
||||
st._hp_flash_tw = create_tween()
|
||||
@@ -353,8 +261,7 @@ func _revive() -> void:
|
||||
modulate = Color(1, 1, 1)
|
||||
rotation_degrees = 0.0
|
||||
if st._kind == "hero":
|
||||
st._hero_state = HeroState.RESET # 强制回 idle 态(绕过同态早退)
|
||||
set_hero_state("idle")
|
||||
mo.reset_state()
|
||||
if slot_row != null:
|
||||
slot_row.visible = true
|
||||
queue_redraw()
|
||||
@@ -372,45 +279,24 @@ func _portrait_glyph() -> String:
|
||||
|
||||
# 立绘落位探针出口(站桩纹理非空断言面;缺位回退链保持的对照面)
|
||||
func portrait_probe() -> Dictionary:
|
||||
return {"has_tex": st._portrait_sprite != null and st._portrait_sprite.texture != null}
|
||||
return {"has_tex": mo._portrait_sprite != null and mo._portrait_sprite.texture != null}
|
||||
|
||||
|
||||
# ---- 主角三态切换(攻击演出→attack/受击→hit/结算回 idle;§8.4 帧数档挂点) ----
|
||||
|
||||
# 组合点注册(battle_stage_view _init——UiKit 常量 load_tex,缺位 null 不注册=该态保持现纹理)
|
||||
# 组合点注册转调壳(签名外形零变)
|
||||
func register_hero_state(state: String, tex: Texture2D) -> void:
|
||||
if tex != null:
|
||||
st._hero_tex[state] = tex
|
||||
mo.register_hero_state(state, tex)
|
||||
|
||||
|
||||
# 状态切换:现纹理换注册态(attack/hit);未注册态名或缺位 = 保持现纹理不炸。
|
||||
# 非 idle 态自动短驻回归 idle(0.35s——演出一拍即回,结算收尾另有显式 idle 兜底)。
|
||||
# 状态切换转调壳(签名外形零变——is_hero 由宿主 vitals 参判传入,早退序逐字在件内)
|
||||
func set_hero_state(state: String) -> void:
|
||||
var target: int = _HS_VALUE.get(state, -1)
|
||||
if st._kind != "hero" or target == st._hero_state:
|
||||
return
|
||||
if not st._hero_tex.has(state) and state != "idle":
|
||||
return
|
||||
st._hero_state = target
|
||||
if st._portrait_sprite != null and st._hero_tex.has(state):
|
||||
st._portrait_sprite.texture = st._hero_tex[state]
|
||||
if st._state_revert_tw != null and st._state_revert_tw.is_valid():
|
||||
st._state_revert_tw.kill()
|
||||
if state != "idle":
|
||||
st._state_revert_tw = create_tween()
|
||||
st._state_revert_tw.tween_interval(UiKit.motion(0.35))
|
||||
st._state_revert_tw.tween_callback(_settle_hero_idle)
|
||||
mo.apply_hero_state(state, st._kind == "hero")
|
||||
|
||||
|
||||
func _settle_hero_idle() -> void:
|
||||
st._hero_state = HeroState.IDLE
|
||||
if st._portrait_sprite != null and st._hero_tex.has("idle"):
|
||||
st._portrait_sprite.texture = st._hero_tex["idle"]
|
||||
|
||||
|
||||
# 三态探针出口(fx_probe——当前态名+注册态集合)
|
||||
# 三态探针出口(fx_probe——当前态名+注册态集合;读件投影外形零变)
|
||||
func hero_state_probe() -> Dictionary:
|
||||
return {"state": _HS_NAME.get(st._hero_state, "idle"), "registered": st._hero_tex.keys()}
|
||||
return mo.state_probe()
|
||||
|
||||
|
||||
func _portrait_color() -> Color:
|
||||
@@ -535,7 +421,7 @@ func _draw_hp_bar(f: Font) -> void:
|
||||
|
||||
## 立绘域:贴图在位 = Sprite2D(set_portrait_texture 落位);缺位 = 字形回退
|
||||
func _draw_portrait(f: Font) -> void:
|
||||
if st._portrait_sprite != null and st._portrait_sprite.texture != null and st._portrait_sprite.visible:
|
||||
if mo._portrait_sprite != null and mo._portrait_sprite.texture != null and mo._portrait_sprite.visible:
|
||||
return
|
||||
var prect := _portrait_rect()
|
||||
var glyph := _portrait_glyph()
|
||||
@@ -543,7 +429,7 @@ func _draw_portrait(f: Font) -> void:
|
||||
# 坐标系注记:draw_set_transform 原点已置于 center——draw_string 的
|
||||
# pos 是**变换后的局部坐标**,须传相对 center 的偏移(-w/2 水平居中、+0.35h 基线下移);
|
||||
# 传绝对 center 会成双重偏移、把字形画到盒外。
|
||||
draw_set_transform(center, deg_to_rad(st._glyph_rot), Vector2.ONE)
|
||||
draw_set_transform(center, deg_to_rad(mo._glyph_rot), Vector2.ONE)
|
||||
var ts := f.get_string_size(glyph, HORIZONTAL_ALIGNMENT_LEFT, -1, 42)
|
||||
f.draw_string(get_canvas_item(), Vector2(-ts.x * 0.5, ts.y * 0.35), glyph,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 42, _portrait_color())
|
||||
@@ -552,18 +438,17 @@ func _draw_portrait(f: Font) -> void:
|
||||
|
||||
## 意图徽标域(敌 only——立绘上半部居中悬浮,CENTER_TOP 74..98 等价):弹入 scale 动画
|
||||
func _draw_intent_badge(f: Font) -> void:
|
||||
if not st._intent_visible:
|
||||
if not mo._intent_visible:
|
||||
return
|
||||
var icenter := Vector2(box.x * 0.5, 86.0)
|
||||
draw_set_transform(icenter, 0.0, Vector2(st._intent_scale, st._intent_scale))
|
||||
var its := f.get_string_size(st._intent_glyph, HORIZONTAL_ALIGNMENT_LEFT, -1, 15)
|
||||
draw_set_transform(icenter, 0.0, Vector2(mo._intent_scale, mo._intent_scale))
|
||||
var its := f.get_string_size(mo._intent_glyph, HORIZONTAL_ALIGNMENT_LEFT, -1, 15)
|
||||
# 对比度垫牌(四色墨直压立绘纹理/暗舞台底 1.5-3.3:1 违 §7.2
|
||||
# 「文字禁直压纹理底」;垫 PANEL_DATA 素牌后 atk 5.3/buf 4.8/debuff 4.7/charge 4.9 全 ≥4.5:1;
|
||||
# atk/debuff 零换墨、buf 换 HERO_BLUE_DEEP、charge 换 GOLD_INK;随弹入 transform 同步缩放)
|
||||
draw_rect(Rect2(-its.x * 0.5 - 4.0, -its.y * 0.75, its.x + 8.0, its.y * 1.1), UiKit.PANEL_DATA, true)
|
||||
f.draw_string(get_canvas_item(), Vector2(-its.x * 0.5, its.y * 0.35), st._intent_glyph,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 15, st._intent_col) # 局部坐标(同上注记——双重偏移修复)
|
||||
draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE)
|
||||
f.draw_string(get_canvas_item(), Vector2(-its.x * 0.5, its.y * 0.35), mo._intent_glyph,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 15, mo._intent_col) # 局部坐标(同上注记——双重偏移修复)
|
||||
|
||||
|
||||
## 名字行域(敌 only——AC 数值不展示,类型图标化):整行垫 HP_BAR_BACK 素底牌(§6.1/§7.2
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## 站桩资产与演出态件(StandeeBody 拆件——单一变化原因=美术素材落位规则与演出编排设计):
|
||||
## 站桩的资产挂点与一次性演出——立绘精灵落位与换装、主角三态注册与 0.35s 短驻回归、意图徽标弹入动画。
|
||||
## 无 class_name(类名缓存面纪律)——经宿主 standee_body preload 引用(mo 实例);
|
||||
## 两个属性 setter 经 _owner 回引触发宿主重绘语义逐字;tween 经 _owner 创建(同一 Node 生命绑定)。
|
||||
extends RefCounted
|
||||
|
||||
enum HeroState { IDLE, RESET, ATTACK, HIT } # 主角三态+复位哨兵(外部 String 契约=register/set 形参零变化)
|
||||
const _HS_NAME := {HeroState.IDLE: "idle", HeroState.RESET: "reset", HeroState.ATTACK: "attack", HeroState.HIT: "hit"}
|
||||
const _HS_VALUE := {"idle": HeroState.IDLE, "reset": HeroState.RESET, "attack": HeroState.ATTACK, "hit": HeroState.HIT}
|
||||
|
||||
var _owner: Node2D = null
|
||||
var _portrait_sprite: Sprite2D = null # 资产挂点(null = 未落位,字形在位)
|
||||
var _portrait_tex: Texture2D = null # 落位纹理引用(set_portrait_texture 装载;字形回退判据)
|
||||
# 主角三态切换:state 名 → 纹理(battle_stage_view 组合点注册);缺态=保持现纹理
|
||||
# (缺位回退链延伸——三态素材缺任一不炸不空)。敌站桩不注册不消费。
|
||||
var _hero_tex := {}
|
||||
var _hero_state: int = 0
|
||||
var _state_revert_tw: Tween
|
||||
var _intent_glyph := ""
|
||||
var _intent_col := Color.WHITE
|
||||
var _intent_visible := false
|
||||
var _intent_scale := 1.0: # 意图弹入动画参数
|
||||
set(v):
|
||||
_intent_scale = v
|
||||
_owner.queue_redraw()
|
||||
var _glyph_rot := 0.0: # 立绘前倾角(贴图态无此动画)
|
||||
set(v):
|
||||
_glyph_rot = v
|
||||
_owner.queue_redraw()
|
||||
|
||||
|
||||
func _init(owner: Node2D) -> void:
|
||||
_owner = owner
|
||||
|
||||
|
||||
# 资产挂点:tex 落位后字形隐退、纹理立绘顶位——零代码换装契约;tex=null 回退字形。
|
||||
# flip = 立绘朝向翻转(素材基准朝左时传 true 呈现朝右)。盒高 96px = 48px 立绘源整数 2×
|
||||
# (圣经 §8.2 禁非整数缩放)——源换代时随源重算。(box = 宿主冻结 var,由宿主传入)
|
||||
func set_portrait_texture(tex: Texture2D, flip: bool, box: Vector2) -> void:
|
||||
if tex == null:
|
||||
if _portrait_sprite != null:
|
||||
_portrait_sprite.visible = false
|
||||
_owner.queue_redraw()
|
||||
return
|
||||
_portrait_tex = tex
|
||||
if _portrait_sprite == null:
|
||||
_portrait_sprite = Sprite2D.new()
|
||||
_portrait_sprite.centered = false
|
||||
_portrait_sprite.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST # 像素帧放大防糊
|
||||
_owner.add_child(_portrait_sprite)
|
||||
_portrait_sprite.texture = tex
|
||||
_portrait_sprite.flip_h = flip
|
||||
_portrait_sprite.visible = true
|
||||
_place_portrait(box)
|
||||
|
||||
|
||||
func _place_portrait(box: Vector2) -> void:
|
||||
# 立绘盒 = (0,44)-(box.x, box.y-36);48 源 ×2 整数档居中
|
||||
if _portrait_sprite == null or _portrait_tex == null:
|
||||
return
|
||||
var box_h := box.y - 44.0 - 36.0
|
||||
var w := _portrait_tex.get_width() * 2.0
|
||||
var h := _portrait_tex.get_height() * 2.0
|
||||
_portrait_sprite.scale = Vector2(2.0, 2.0) # 整数 2×(C5)
|
||||
_portrait_sprite.position = Vector2((box.x - w) * 0.5, 44.0 + (box_h - h) * 0.5)
|
||||
|
||||
|
||||
# 组合点注册(battle_stage_view _init——UiKit 常量 load_tex,缺位 null 不注册=该态保持现纹理)
|
||||
func register_hero_state(state: String, tex: Texture2D) -> void:
|
||||
if tex != null:
|
||||
_hero_tex[state] = tex
|
||||
|
||||
|
||||
# 状态切换:现纹理换注册态(attack/hit);未注册态名或缺位 = 保持现纹理不炸。
|
||||
# 非 idle 态自动短驻回归 idle(0.35s——演出一拍即回,结算收尾另有显式 idle 兜底)。
|
||||
#(is_hero = 宿主 vitals._kind == "hero" 参判传入——三连早退条件序逐字)
|
||||
func apply_hero_state(state: String, is_hero: bool) -> void:
|
||||
var target: int = _HS_VALUE.get(state, -1)
|
||||
if not is_hero or target == _hero_state:
|
||||
return
|
||||
if not _hero_tex.has(state) and state != "idle":
|
||||
return
|
||||
_hero_state = target
|
||||
if _portrait_sprite != null and _hero_tex.has(state):
|
||||
_portrait_sprite.texture = _hero_tex[state]
|
||||
if _state_revert_tw != null and _state_revert_tw.is_valid():
|
||||
_state_revert_tw.kill()
|
||||
if state != "idle":
|
||||
_state_revert_tw = _owner.create_tween()
|
||||
_state_revert_tw.tween_interval(UiKit.motion(0.35))
|
||||
_state_revert_tw.tween_callback(_settle_hero_idle)
|
||||
|
||||
|
||||
func _settle_hero_idle() -> void:
|
||||
_hero_state = HeroState.IDLE
|
||||
if _portrait_sprite != null and _hero_tex.has("idle"):
|
||||
_portrait_sprite.texture = _hero_tex["idle"]
|
||||
|
||||
|
||||
# 复位强制绕同态早退两拍封装(原 _revive 内 hero 分支——先置 RESET 哨兵再回 idle)
|
||||
func reset_state() -> void:
|
||||
_hero_state = HeroState.RESET # 强制回 idle 态(绕过同态早退)
|
||||
apply_hero_state("idle", true)
|
||||
|
||||
|
||||
# 意图三档动作动画(§4.6.4 三档:①普通攻击=零表示〔set_pending 早退〕
|
||||
# ②特殊准备=徽标弹入+二次弹跳(高举语义,运动手段 §2)③重击/硬化=缓推蓄力+定格微顿+回落;
|
||||
# 三档运动签名互异,灰度/剪影层可辨)。摇定时一次性短动画,随后静帧保持。
|
||||
#(charge = 宿主 _intent_charge 分档布尔参判传入)
|
||||
func play_intent_motion(charge: bool) -> void:
|
||||
var tw := _owner.create_tween()
|
||||
tw.tween_property(self, "_intent_scale", 1.6, UiKit.motion(0.10))
|
||||
tw.tween_property(self, "_intent_scale", 1.0, UiKit.motion(0.16))
|
||||
tw.parallel().tween_property(self, "_glyph_rot", 4.0, UiKit.motion(0.10))
|
||||
tw.tween_property(self, "_glyph_rot", 0.0, UiKit.motion(0.14))
|
||||
if charge:
|
||||
tw.tween_property(self, "_intent_scale", 1.22, UiKit.motion(0.28)).set_trans(Tween.TRANS_SINE) # ③蓄力缓推
|
||||
tw.tween_interval(UiKit.motion(0.40)) # 定格微顿
|
||||
tw.tween_property(self, "_intent_scale", 1.0, UiKit.motion(0.18))
|
||||
else:
|
||||
tw.tween_property(self, "_intent_scale", 1.26, UiKit.motion(0.11)) # ②二次弹跳(高举)
|
||||
tw.tween_property(self, "_intent_scale", 1.0, UiKit.motion(0.13))
|
||||
|
||||
|
||||
# 三态探针出口(fx_probe——当前态名+注册态集合;宿主 hero_state_probe 读件投影,外形零变)
|
||||
func state_probe() -> Dictionary:
|
||||
return {"state": _HS_NAME.get(_hero_state, "idle"), "registered": _hero_tex.keys()}
|
||||
@@ -0,0 +1 @@
|
||||
uid://cdfo2jgp2w7ei
|
||||
@@ -0,0 +1,36 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## 站桩身份与命值态件(StandeeBody 拆件——单一变化原因=战斗结算快照投影的字段形态):
|
||||
## 站桩是谁、活没活、血量几何的唯一账面——身份五元组(种类/名/id/护甲/顺位)+存活位+血量账与两条血量系 tween 槽。
|
||||
## 无 class_name(类名缓存面纪律)——经宿主 standee_body preload 引用(st 实例名沿用);
|
||||
## hp_value 属性 setter 经 _owner 回引触发宿主重绘语义逐字。
|
||||
extends RefCounted
|
||||
|
||||
var _owner: Node2D = null
|
||||
var _kind := "enemy"
|
||||
var _display_name := ""
|
||||
var _enemy_id := ""
|
||||
var _armor := "light"
|
||||
var _order := 1
|
||||
var _dead := false
|
||||
var _hp_max := 1.0
|
||||
var _hp_cur := 0 # 血量数字显示面(HUD框架 验收「血量数字可读」)
|
||||
var hp_value := 1.0: # 血条值 0..1(直角递减——tween_property 目标,逐行刷新重启不累积)
|
||||
set(v):
|
||||
hp_value = clampf(v, 0.0, 1.0)
|
||||
_owner.queue_redraw()
|
||||
var _hp_tween: Tween
|
||||
var _hp_flash_tw: Tween
|
||||
|
||||
|
||||
func _init(owner: Node2D) -> void:
|
||||
_owner = owner
|
||||
|
||||
|
||||
# 血条短 tween 递减(直角递减占位);逐行刷新时重启 tween 不累积
|
||||
func apply_hp(hp: int) -> void:
|
||||
_hp_cur = clampi(hp, 0, int(_hp_max))
|
||||
var target := float(_hp_cur) / _hp_max
|
||||
if _hp_tween != null and _hp_tween.is_valid():
|
||||
_hp_tween.kill()
|
||||
_hp_tween = _owner.create_tween()
|
||||
_hp_tween.tween_property(self, "hp_value", target, UiKit.motion(0.15))
|
||||
@@ -0,0 +1 @@
|
||||
uid://bjd7t5nmepbey
|
||||
@@ -231,8 +231,8 @@ func _ready() -> void:
|
||||
if bv.hover_probe() != seal_t:
|
||||
_problems.append("case① 悬停未到达(_hover=%s 应=%s)" % [bv.hover_probe(), seal_t])
|
||||
var u1 = bv.cells_probe()[seal_t]
|
||||
if not u1.st.hl_visible or u1.st.hl_color != UiKit.GOLD:
|
||||
_problems.append("case① 悬停金框缺失(visible=%s color=%s)" % [str(u1.st.hl_visible), str(u1.st.hl_color)])
|
||||
if not u1.face.hl_visible or u1.face.hl_color != UiKit.GOLD:
|
||||
_problems.append("case① 悬停金框缺失(visible=%s color=%s)" % [str(u1.face.hl_visible), str(u1.face.hl_color)])
|
||||
if bv.mouse_default_cursor_shape != Input.CURSOR_CROSS:
|
||||
_problems.append("case① 挖掘光标未切十字(%d)" % bv.mouse_default_cursor_shape)
|
||||
# ---- case② 悬停禁入:hover_forbidden 信号 + 灰禁红框 ----
|
||||
@@ -240,14 +240,14 @@ func _ready() -> void:
|
||||
await get_tree().process_frame
|
||||
if _forbids.is_empty() or _forbids[_forbids.size() - 1] != forbid_t:
|
||||
_problems.append("case② hover_forbidden 未达(%s)" % [str(_forbids)])
|
||||
if bv.cells_probe()[forbid_t].st.hl_color != UiKit.RED_FORBID:
|
||||
_problems.append("case② 禁入红框缺失(color=%s)" % str(bv.cells_probe()[forbid_t].st.hl_color))
|
||||
if bv.cells_probe()[forbid_t].face.hl_color != UiKit.RED_FORBID:
|
||||
_problems.append("case② 禁入红框缺失(color=%s)" % str(bv.cells_probe()[forbid_t].face.hl_color))
|
||||
# ---- case③ 悬停已开格:路径预览点迹 + 移动光标 ----
|
||||
_push_motion(_pos(bv, reveal_t))
|
||||
await get_tree().process_frame
|
||||
var dots := 0
|
||||
for c in bv.cells_probe():
|
||||
if bv.cells_probe()[c].st.dot_visible:
|
||||
if bv.cells_probe()[c].face.dot_visible:
|
||||
dots += 1
|
||||
if dots < 1:
|
||||
_problems.append("case③ 路径预览点迹未亮(dots=0)")
|
||||
@@ -266,8 +266,8 @@ func _ready() -> void:
|
||||
# 掩盖)——有盘断言纹理在槽、缺盘断言字形回退,两分支防双写踩踏复活 ----
|
||||
var seal_unit = bv.cells_probe().get(seal_t)
|
||||
if bv._palette.seal_tex != null:
|
||||
if seal_unit == null or seal_unit.st.tex_seal != bv._palette.seal_tex:
|
||||
_problems.append("case④b 封印纹理槽未挂封印印记纹理(tex_seal=%s)" % ["无单元" if seal_unit == null else str(seal_unit.st.tex_seal)])
|
||||
if seal_unit == null or seal_unit.face.tex_seal != bv._palette.seal_tex:
|
||||
_problems.append("case④b 封印纹理槽未挂封印印记纹理(tex_seal=%s)" % ["无单元" if seal_unit == null else str(seal_unit.face.tex_seal)])
|
||||
elif seal_unit == null or seal_unit.glyph != "封":
|
||||
_problems.append("case④b 缺盘字形回退缺失(glyph=%s)" % ["无单元" if seal_unit == null else String(seal_unit.glyph)])
|
||||
# ---- case⑤ 双击 chord:禁入格双左击(核心拒挖零副作用)→ chord_attempted ----
|
||||
@@ -371,12 +371,12 @@ func _ready() -> void:
|
||||
if mark_t.x >= 0:
|
||||
bv.set_variant_mark_resolver(func(_c): return "flat_damage")
|
||||
bv.refresh()
|
||||
if String(bv.cells_probe()[mark_t].st.mutation_mark) != "flat_damage":
|
||||
_problems.append("case⑭ 变异角标未落位(mutation_mark=%s)" % str(bv.cells_probe()[mark_t].st.mutation_mark))
|
||||
if String(bv.cells_probe()[mark_t].face.mutation_mark) != "flat_damage":
|
||||
_problems.append("case⑭ 变异角标未落位(mutation_mark=%s)" % str(bv.cells_probe()[mark_t].face.mutation_mark))
|
||||
bv.set_variant_mark_resolver(Callable())
|
||||
bv.refresh()
|
||||
if String(bv.cells_probe()[mark_t].st.mutation_mark) != "":
|
||||
_problems.append("case⑭ 角标未随 resolver 复位(mutation_mark=%s)" % str(bv.cells_probe()[mark_t].st.mutation_mark))
|
||||
if String(bv.cells_probe()[mark_t].face.mutation_mark) != "":
|
||||
_problems.append("case⑭ 角标未随 resolver 复位(mutation_mark=%s)" % str(bv.cells_probe()[mark_t].face.mutation_mark))
|
||||
else:
|
||||
_problems.append("case⑭ 无未开格可验(板面异常)")
|
||||
# ---- case⑮ 视野迷雾(三态呈态/半径圈/移动跟随/开关两态零扰动/提灯插值位;
|
||||
|
||||
@@ -8,6 +8,7 @@ extends SceneTree
|
||||
|
||||
const SaveManagerScript := preload("res://core/save_manager.gd")
|
||||
const AdventureRunnerScript := preload("res://core/adventure_runner.gd")
|
||||
const BreakpointStoreScript := preload("res://core/breakpoint_store.gd")
|
||||
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:check/汇总退出样板单点(静默收集通道)
|
||||
|
||||
var _bp_path := "user://test_breakpoint_resume.json"
|
||||
@@ -182,7 +183,7 @@ func _init() -> void:
|
||||
"P7 同种子重开局同图(开局入口复现承载)〔v7〕")
|
||||
|
||||
# ---- Part 8:断点字段登记单源(登记键集 == 写侧实拼字典键集;键名增删即红)----
|
||||
var fields: Dictionary = AdventureRunnerScript.BREAKPOINT_FIELDS
|
||||
var fields: Dictionary = BreakpointStoreScript.BREAKPOINT_FIELDS # 断点域拆件改址——登记单源随件(判据零变化)
|
||||
var reg_keys: Array = fields.keys()
|
||||
reg_keys.sort()
|
||||
var bp_keys: Array = bp_j.keys()
|
||||
|
||||
Reference in new issue
Block a user